feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
import { useRouter as useBProgressRouter } from "@bprogress/next";
export function useRouter() {
const router = useBProgressRouter();
return router;
}

View File

@@ -0,0 +1 @@
export * from "./AppProgressBar";

View File

@@ -0,0 +1,47 @@
"use client";
import type { FC } from "react";
import React, { useEffect } from "react";
import { Intercom, show, hide, onHide } from "@intercom/messenger-js-sdk";
import { observer } from "mobx-react";
// store hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useTransient } from "@/hooks/store/use-transient";
import { useUser } from "@/hooks/store/user";
export type IntercomProviderProps = {
children: React.ReactNode;
};
const IntercomProvider: FC<IntercomProviderProps> = observer((props) => {
const { children } = props;
// hooks
const { data: user } = useUser();
const { config } = useInstance();
const { isIntercomToggle, toggleIntercom } = useTransient();
useEffect(() => {
if (isIntercomToggle) show();
else hide();
}, [isIntercomToggle]);
onHide(() => {
toggleIntercom(false);
});
useEffect(() => {
if (user && config?.is_intercom_enabled && config.intercom_app_id) {
Intercom({
app_id: config.intercom_app_id || "",
user_id: user.id,
name: `${user.first_name} ${user.last_name}`,
email: user.email,
hide_default_launcher: true,
});
}
}, [user, config, toggleIntercom]);
return <>{children}</>;
});
export default IntercomProvider;

View File

@@ -0,0 +1,27 @@
import { isEmpty } from "lodash-es";
export const storage = {
set: (key: string, value: object | string | boolean): void => {
if (typeof window === undefined || typeof window === "undefined" || !key || !value) return undefined;
const tempValue: string | undefined = value
? ["string", "boolean"].includes(typeof value)
? value.toString()
: isEmpty(value)
? undefined
: JSON.stringify(value)
: undefined;
if (!tempValue) return undefined;
window.localStorage.setItem(key, tempValue);
},
get: (key: string): string | undefined => {
if (typeof window === undefined || typeof window === "undefined") return undefined;
const item = window.localStorage.getItem(key);
return item ? item : undefined;
},
remove: (key: string): void => {
if (typeof window === undefined || typeof window === "undefined" || !key) return undefined;
window.localStorage.removeItem(key);
},
};

View File

@@ -0,0 +1 @@
export * from "./requestIdleCallback";

View File

@@ -0,0 +1,24 @@
if (typeof window !== "undefined" && window) {
// Add request callback polyfill to browser in case it does not exist
window.requestIdleCallback =
window.requestIdleCallback ??
function (cb) {
const start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
window.cancelIdleCallback =
window.cancelIdleCallback ??
function (id) {
clearTimeout(id);
};
}
export {};

View File

@@ -0,0 +1,110 @@
"use client";
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import dynamic from "next/dynamic";
import { useParams } from "next/navigation";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
// constants
import { GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants";
// helpers
import { getUserRole } from "@plane/utils";
// hooks
import { captureClick, joinEventGroup } from "@/helpers/event-tracker.helper";
import { useInstance } from "@/hooks/store/use-instance";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUser, useUserPermissions } from "@/hooks/store/user";
// dynamic imports
const PostHogPageView = dynamic(() => import("@/lib/posthog-view"), { ssr: false });
export interface IPosthogWrapper {
children: ReactNode;
}
const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
const { children } = props;
const { data: user } = useUser();
const { currentWorkspace } = useWorkspace();
const { instance } = useInstance();
const { workspaceSlug, projectId } = useParams();
const { getWorkspaceRoleByWorkspaceSlug, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(
workspaceSlug?.toString(),
projectId?.toString()
);
const currentWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug?.toString());
const is_telemetry_enabled = instance?.is_telemetry_enabled || false;
const is_posthog_enabled =
process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && is_telemetry_enabled;
useEffect(() => {
if (user) {
// Identify sends an event, so you want may want to limit how often you call it
posthog?.identify(user.email, {
id: user.id,
first_name: user.first_name,
last_name: user.last_name,
email: user.email,
workspace_role: currentWorkspaceRole ? getUserRole(currentWorkspaceRole) : undefined,
project_role: currentProjectRole ? getUserRole(currentProjectRole) : undefined,
});
if (currentWorkspace) {
joinEventGroup(GROUP_WORKSPACE_TRACKER_EVENT, currentWorkspace?.id, {
date: new Date().toDateString(),
workspace_id: currentWorkspace?.id,
});
}
}
}, [user, currentProjectRole, currentWorkspaceRole, currentWorkspace]);
useEffect(() => {
if (process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: "/ingest",
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
debug: process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "1", // Debug mode based on the environment variable
autocapture: false,
capture_pageview: false, // Disable automatic pageview capture, as we capture manually
capture_pageleave: true,
disable_session_recording: true,
});
}
}, []);
useEffect(() => {
const clickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Use closest to find the nearest parent element with data-ph-element attribute
const elementWithAttribute = target.closest("[data-ph-element]") as HTMLElement;
if (elementWithAttribute) {
const element = elementWithAttribute.getAttribute("data-ph-element");
if (element) {
captureClick({ elementName: element });
}
}
};
if (is_posthog_enabled) {
document.addEventListener("click", clickHandler);
}
return () => {
document.removeEventListener("click", clickHandler);
};
}, [is_posthog_enabled]);
if (is_posthog_enabled)
return (
<PHProvider client={posthog}>
<PostHogPageView />
{children}
</PHProvider>
);
return <>{children}</>;
});
export default PostHogProvider;

View File

@@ -0,0 +1,26 @@
"use client";
import { useEffect } from "react";
import { usePathname, useSearchParams } from "next/navigation";
// posthog
import { usePostHog } from "posthog-js/react";
export default function PostHogPageView(): null {
const pathname = usePathname();
const searchParams = useSearchParams();
const posthog = usePostHog();
useEffect(() => {
// Track pageviews
if (pathname && posthog) {
let url = window.origin + pathname;
if (searchParams.toString()) {
url = url + `?${searchParams.toString()}`;
}
posthog.capture("$pageview", {
$current_url: url,
});
}
}, [pathname, searchParams, posthog]);
return null;
}

View File

@@ -0,0 +1,23 @@
"use client";
import type { ReactElement } from "react";
import { createContext } from "react";
// plane web store
import { RootStore } from "@/plane-web/store/root.store";
export let rootStore = new RootStore();
export const StoreContext = createContext<RootStore>(rootStore);
const initializeStore = () => {
const newRootStore = rootStore ?? new RootStore();
if (typeof window === "undefined") return newRootStore;
if (!rootStore) rootStore = newRootStore;
return newRootStore;
};
export const store = initializeStore();
export const StoreProvider = ({ children }: { children: ReactElement }) => (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);

View File

@@ -0,0 +1,140 @@
"use client";
import type { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import { useSearchParams, usePathname } from "next/navigation";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUser, useUserProfile, useUserSettings } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
type TPageType = EPageTypes;
type TAuthenticationWrapper = {
children: ReactNode;
pageType?: TPageType;
};
const isValidURL = (url: string): boolean => {
const disallowedSchemes = /^(https?|ftp):\/\//i;
return !disallowedSchemes.test(url);
};
export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props) => {
const pathname = usePathname();
const router = useAppRouter();
const searchParams = useSearchParams();
const nextPath = searchParams.get("next_path");
// props
const { children, pageType = EPageTypes.AUTHENTICATED } = props;
// hooks
const { isLoading: isUserLoading, data: currentUser, fetchCurrentUser } = useUser();
const { data: currentUserProfile } = useUserProfile();
const { data: currentUserSettings } = useUserSettings();
const { loader: workspacesLoader, workspaces } = useWorkspace();
const { isLoading: isUserSWRLoading } = useSWR("USER_INFORMATION", async () => await fetchCurrentUser(), {
revalidateOnFocus: false,
shouldRetryOnError: false,
});
const isUserOnboard =
currentUserProfile?.is_onboarded ||
(currentUserProfile?.onboarding_step?.profile_complete &&
currentUserProfile?.onboarding_step?.workspace_create &&
currentUserProfile?.onboarding_step?.workspace_invite &&
currentUserProfile?.onboarding_step?.workspace_join) ||
false;
const getWorkspaceRedirectionUrl = (): string => {
let redirectionRoute = "/create-workspace";
// validating the nextPath from the router query
if (nextPath && isValidURL(nextPath.toString())) {
redirectionRoute = nextPath.toString();
return redirectionRoute;
}
// validate the last and fallback workspace_slug
const currentWorkspaceSlug =
currentUserSettings?.workspace?.last_workspace_slug || currentUserSettings?.workspace?.fallback_workspace_slug;
// validate the current workspace_slug is available in the user's workspace list
const isCurrentWorkspaceValid = Object.values(workspaces || {}).findIndex(
(workspace) => workspace.slug === currentWorkspaceSlug
);
if (isCurrentWorkspaceValid >= 0) redirectionRoute = `/${currentWorkspaceSlug}`;
return redirectionRoute;
};
if ((isUserSWRLoading || isUserLoading || workspacesLoader) && !currentUser?.id)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
if (pageType === EPageTypes.PUBLIC) return <>{children}</>;
if (pageType === EPageTypes.NON_AUTHENTICATED) {
if (!currentUser?.id) return <>{children}</>;
else {
if (currentUserProfile?.id && isUserOnboard) {
const currentRedirectRoute = getWorkspaceRedirectionUrl();
router.push(currentRedirectRoute);
return <></>;
} else {
router.push("/onboarding");
return <></>;
}
}
}
if (pageType === EPageTypes.ONBOARDING) {
if (!currentUser?.id) {
router.push(`/${pathname ? `?next_path=${pathname}` : ``}`);
return <></>;
} else {
if (currentUser && currentUserProfile?.id && isUserOnboard) {
const currentRedirectRoute = getWorkspaceRedirectionUrl();
router.replace(currentRedirectRoute);
return <></>;
} else return <>{children}</>;
}
}
if (pageType === EPageTypes.SET_PASSWORD) {
if (!currentUser?.id) {
router.push(`/${pathname ? `?next_path=${pathname}` : ``}`);
return <></>;
} else {
if (currentUser && !currentUser?.is_password_autoset && currentUserProfile?.id && isUserOnboard) {
const currentRedirectRoute = getWorkspaceRedirectionUrl();
router.push(currentRedirectRoute);
return <></>;
} else return <>{children}</>;
}
}
if (pageType === EPageTypes.AUTHENTICATED) {
if (currentUser?.id) {
if (currentUserProfile && currentUserProfile?.id && isUserOnboard) return <>{children}</>;
else {
router.push(`/onboarding`);
return <></>;
}
} else {
router.push(`/${pathname ? `?next_path=${pathname}` : ``}`);
return <></>;
}
}
return <>{children}</>;
});

View File

@@ -0,0 +1,42 @@
import type { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { InstanceNotReady, MaintenanceView } from "@/components/instance";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
type TInstanceWrapper = {
children: ReactNode;
};
export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
const { children } = props;
// store
const { isLoading, instance, error, fetchInstanceInfo } = useInstance();
const { isLoading: isInstanceSWRLoading, error: instanceSWRError } = useSWR(
"INSTANCE_INFORMATION",
async () => await fetchInstanceInfo(),
{ revalidateOnFocus: false }
);
// loading state
if ((isLoading || isInstanceSWRLoading) && !instance)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
if (instanceSWRError) return <MaintenanceView />;
// something went wrong while in the request
if (error && error?.status === "error") return <>{children}</>;
// instance is not ready and setup is not done
if (instance?.is_setup_done === false) return <InstanceNotReady />;
return <>{children}</>;
});

View File

@@ -0,0 +1,71 @@
import type { ReactNode, FC } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useTheme } from "next-themes";
import type { TLanguage } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
// helpers
import { applyTheme, unsetCustomCssVariables } from "@plane/utils";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useRouterParams } from "@/hooks/store/use-router-params";
import { useUserProfile } from "@/hooks/store/user";
type TStoreWrapper = {
children: ReactNode;
};
const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
const { children } = props;
// theme
const { setTheme } = useTheme();
// router
const params = useParams();
// store hooks
const { setQuery } = useRouterParams();
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const { data: userProfile } = useUserProfile();
const { changeLanguage } = useTranslation();
/**
* Sidebar collapsed fetching from local storage
*/
useEffect(() => {
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue);
}, [sidebarCollapsed, setTheme, toggleSidebar]);
/**
* Setting up the theme of the user by fetching it from local storage
*/
useEffect(() => {
if (!userProfile?.theme?.theme) return;
const currentTheme = userProfile?.theme?.theme || "system";
const currentThemePalette = userProfile?.theme?.palette;
if (currentTheme) {
setTheme(currentTheme);
if (currentTheme === "custom" && currentThemePalette) {
applyTheme(
currentThemePalette !== ",,,," ? currentThemePalette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
false
);
} else unsetCustomCssVariables();
}
}, [userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme]);
useEffect(() => {
if (!userProfile?.language) return;
changeLanguage(userProfile?.language as TLanguage);
}, [userProfile?.language, changeLanguage]);
useEffect(() => {
if (!params) return;
setQuery(params);
}, [params, setQuery]);
return <>{children}</>;
});
export default StoreWrapper;