snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
.next/
.react-router/
.turbo/
.vite/
build/
dist/
node_modules/
out/
pnpm-lock.yaml
storybook-static/

View File

@@ -0,0 +1,35 @@
{
"name": "@plane/hooks",
"version": "1.4.2",
"private": true,
"description": "React hooks that are shared across multiple apps internally",
"license": "AGPL-3.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch --no-clean",
"check:lint": "oxlint --max-warnings=4 .",
"check:types": "tsc --noEmit",
"check:format": "oxfmt --check .",
"fix:lint": "oxlint --fix .",
"fix:format": "oxfmt .",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
},
"dependencies": {
"react": "catalog:"
},
"devDependencies": {
"@plane/typescript-config": "workspace:*",
"@types/node": "catalog:",
"@types/react": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "./use-hash-scroll";
export * from "./use-local-storage";
export * from "./use-outside-click-detector";
export * from "./use-platform-os";

View File

@@ -0,0 +1,134 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useCallback, useEffect, useState } from "react";
type TArgs = {
elementId: string;
pathname: string;
scrollDelay?: number;
};
type TReturnType = {
isHashMatch: boolean;
hashIds: string[];
scrollToElement: () => boolean;
};
/**
* Custom hook for handling hash-based scrolling to a specific element
* Supports multiple IDs in URL hash (comma-separated, space-separated, or other delimiters)
*
* @param {TArgs} args - The ID of the element to scroll to
* @returns {TReturnType} Object containing hash match status and scroll function
*/
export const useHashScroll = (args: TArgs): TReturnType => {
const { elementId, pathname, scrollDelay = 200 } = args;
// State to track if the current hash contains the provided element ID
const [isHashMatch, setIsHashMatch] = useState(false);
// State to track all IDs found in the hash
const [hashIds, setHashIds] = useState<string[]>([]);
/**
* Scrolls to the element with the provided ID
* @returns {boolean} - Whether the scroll was successful
*/
const scrollToElement = useCallback((): boolean => {
try {
const element = document.getElementById(elementId);
if (element) {
setTimeout(() => {
element.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}, scrollDelay);
return true;
}
return false;
} catch (error) {
console.warn("Hash scroll error:", error);
return false;
}
}, [elementId, scrollDelay]);
/**
* Extracts multiple IDs from hash string
* Supports various delimiters: comma, space, pipe, semicolon
* @param {string} hashString - The hash part of the URL
* @returns {string[]} - Array of clean ID strings
*/
const extractIdsFromHash = (hashString: string | null): string[] => {
if (!hashString) return [];
// Split by common delimiters and clean up
return hashString
.split(/[,\s|;]+/) // Split by comma, space, pipe, or semicolon
.map((id) => id.trim()) // Remove whitespace
.filter((id) => id.length > 0); // Remove empty strings
};
/**
* Get current hash from window.location
* @returns {string | null} - Current hash without the # symbol
*/
const getCurrentHash = (): string | null => {
if (typeof window === "undefined") return null;
const hash = window.location.hash;
return hash ? hash.slice(1) : null; // Remove the # symbol
};
// Effect to handle hash changes and initial load
useEffect(() => {
if (!elementId) {
setIsHashMatch(false);
setHashIds([]);
return;
}
const handleHashChange = () => {
const hash = getCurrentHash();
// Extract all IDs from the hash
const idsInHash = extractIdsFromHash(hash);
setHashIds(idsInHash);
// Check if provided element ID is present in the hash
const hashMatches = idsInHash.includes(elementId);
setIsHashMatch(hashMatches);
// If hash matches, attempt to scroll to the element
if (hashMatches) {
scrollToElement();
}
};
// Handle initial load
handleHashChange();
// Listen for hash changes
window.addEventListener("hashchange", handleHashChange);
return () => {
window.removeEventListener("hashchange", handleHashChange);
};
}, [elementId, pathname, scrollToElement]); // Include pathname to handle route changes
// Return object with hash match status and utility functions
return {
// Whether the current URL hash contains the provided element ID
isHashMatch,
// Array of all IDs found in the current hash
hashIds,
// Manually trigger scroll to the element
scrollToElement,
};
};

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState, useEffect, useCallback } from "react";
export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
if (typeof window === "undefined" || typeof window === "undefined") return defaultValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (_error) {
window.localStorage.removeItem(key);
return defaultValue;
}
};
export const setValueIntoLocalStorage = (key: string, value: any) => {
if (typeof window === "undefined" || typeof window === "undefined") return false;
try {
window.localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (_error) {
return false;
}
};
export const useLocalStorage = <T,>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T | null>(() => getValueFromLocalStorage(key, initialValue));
const setValue = useCallback(
(value: T) => {
window.localStorage.setItem(key, JSON.stringify(value));
setStoredValue(value);
window.dispatchEvent(new Event(`local-storage:${key}`));
},
[key]
);
const clearValue = useCallback(() => {
window.localStorage.removeItem(key);
setStoredValue(null);
window.dispatchEvent(new Event(`local-storage:${key}`));
}, [key]);
const reHydrate = useCallback(() => {
const data = getValueFromLocalStorage(key, initialValue);
setStoredValue(data);
}, [key, initialValue]);
useEffect(() => {
window.addEventListener(`local-storage:${key}`, reHydrate);
return () => {
window.removeEventListener(`local-storage:${key}`, reHydrate);
};
}, [key, reHydrate]);
return { storedValue, setValue, clearValue } as const;
};

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type React from "react";
import { useEffect, useLayoutEffect, useRef } from "react";
export const useOutsideClickDetector = (
ref: React.RefObject<HTMLElement | null> | any,
callback: () => void,
useCapture = false,
enabled = true
) => {
// Keep the latest callback/useCapture in refs so the document listener binds once per
// consumer instead of re-attaching on every render.
const callbackRef = useRef(callback);
const useCaptureRef = useRef(useCapture);
useLayoutEffect(() => {
callbackRef.current = callback;
useCaptureRef.current = useCapture;
});
useEffect(() => {
if (!enabled) return;
const handleClick = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as any)) {
// check for the closest element with attribute name data-prevent-outside-click
const preventOutsideClickElement = (event.target as unknown as HTMLElement | undefined)?.closest(
"[data-prevent-outside-click]"
);
// if the closest element with attribute name data-prevent-outside-click is found, return
if (preventOutsideClickElement) {
return;
}
// else call the callback
callbackRef.current();
}
};
const capture = useCaptureRef.current;
document.addEventListener("mousedown", handleClick, capture);
return () => {
document.removeEventListener("mousedown", handleClick, capture);
};
}, [ref, enabled, useCapture]);
};

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState, useEffect } from "react";
export const usePlatformOS = () => {
const [platformData, setPlatformData] = useState({
isMobile: false,
platform: "",
});
useEffect(() => {
const detectPlatform = () => {
const userAgent = window.navigator.userAgent;
const isMobile = /iPhone|iPad|iPod|Android/i.test(userAgent);
let platform = "";
if (!isMobile) {
if (userAgent.indexOf("Win") !== -1) {
platform = "Windows";
} else if (userAgent.indexOf("Mac") !== -1) {
platform = "MacOS";
} else if (userAgent.indexOf("Linux") !== -1) {
platform = "Linux";
} else {
platform = "Unknown";
}
}
setPlatformData({ isMobile, platform });
};
detectPlatform();
}, []);
return platformData;
};

View File

@@ -0,0 +1,5 @@
{
"extends": "@plane/typescript-config/react-library.json",
"include": ["./src"],
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
platform: "neutral",
exports: true,
});