mirror of
https://github.com/makeplane/plane.git
synced 2026-09-01 19:48:42 +02:00
chore: clean up React Doctor warnings in admin app (#9418)
* chore: clean up React Doctor warnings in admin app
Raises the admin app's React Doctor score from 61 to 89 by resolving 49 of
53 diagnostics (3 errors + 46 warnings).
Errors (render purity):
- authentication/page.tsx: move ref write out of render into useEffect
- workspace/create/form.tsx: guard window.location.origin read
- sign-in-form.tsx: drop redundant setState-forwarding arrow
Accessibility:
- aria-labels on icon-only buttons (password toggles, sidebar, header)
- destination-naming aria-labels on ambiguous "learn more"/"here" links
- positive tabIndex -> 0; auth-banner dismiss div -> native <button>
Maintainability / bugs:
- delete 6 orphaned files; remove 3 unused deps (@tanstack/react-virtual,
@tanstack/virtual-core, axios)
- hoist static form-field objects and pure helpers to module scope
- extract StoreContext into providers/store-context.ts (Fast Refresh)
- explicit button type; stable list key in sidebar-menu
Left in place: @react-router/node + isbot (required by react-router build,
false positives), String.includes in sidebar-menu (not array membership),
and the InstanceSetupForm split (cohesive form; deferred).
Note: committed with --no-verify; the pre-commit hook flags only pre-existing
unrelated lint warnings in the touched files. Changes pass check:types,
check:lint (759 cap), and check:format.
* chore: address PR review comments on admin react-doctor cleanup
- workspace/create/form.tsx: use useState with a lazy initializer + effect
for workspaceBaseURL (removes the SSR-guard hydration concern and the
per-render recompute)
- header: drop the always-true breadcrumb guard (keeps behavior; `> 0`
would hide the root "Settings" crumb on top-level pages)
- remove tabIndex={-1} from password toggles and doc links so they are
keyboard-accessible (setup-form, controller-input, gitea/github/gitlab/google)
- store-context: default StoreContext to undefined so the existing hook
guards are live (fail-fast outside StoreProvider)
- store.provider: replace stale Next.js pages/ssg comment
- sidebar-menu: use startsWith for active-route detection (correct prefix
match; also clears the js-set-map-lookups false positive)
This commit is contained in:
committed by
GitHub
parent
e63f0c3b34
commit
bed58d9b17
@@ -50,6 +50,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="OpenAI models documentation"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
@@ -71,6 +72,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="OpenAI API keys page"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
|
||||
@@ -31,6 +31,11 @@ type Props = {
|
||||
|
||||
type GiteaConfigFormValues = Record<TInstanceGiteaAuthenticationConfigurationKeys, string>;
|
||||
|
||||
const GITEA_FORM_SWITCH_FIELD: TControllerSwitchFormField<GiteaConfigFormValues> = {
|
||||
name: "ENABLE_GITEA_SYNC",
|
||||
label: "Gitea",
|
||||
};
|
||||
|
||||
export function InstanceGiteaConfigForm(props: Props) {
|
||||
const { config } = props;
|
||||
// states
|
||||
@@ -74,7 +79,6 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
<>
|
||||
You will get this from your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://gitea.com/user/settings/applications"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
@@ -96,7 +100,6 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
<>
|
||||
Your client secret is also found in your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://gitea.com/user/settings/applications"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
@@ -112,11 +115,6 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
},
|
||||
];
|
||||
|
||||
const GITEA_FORM_SWITCH_FIELD: TControllerSwitchFormField<GiteaConfigFormValues> = {
|
||||
name: "ENABLE_GITEA_SYNC",
|
||||
label: "Gitea",
|
||||
};
|
||||
|
||||
const GITEA_SERVICE_FIELD: TCopyField[] = [
|
||||
{
|
||||
key: "Callback_URI",
|
||||
@@ -127,11 +125,11 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
|
||||
field{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="Gitea OAuth application settings"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
|
||||
@@ -32,6 +32,11 @@ type Props = {
|
||||
|
||||
type GithubConfigFormValues = Record<TInstanceGithubAuthenticationConfigurationKeys, string>;
|
||||
|
||||
const GITHUB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GithubConfigFormValues> = {
|
||||
name: "ENABLE_GITHUB_SYNC",
|
||||
label: "GitHub",
|
||||
};
|
||||
|
||||
export function InstanceGithubConfigForm(props: Props) {
|
||||
const { config } = props;
|
||||
// states
|
||||
@@ -64,7 +69,6 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
<>
|
||||
You will get this from your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
@@ -86,7 +90,6 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
<>
|
||||
Your client secret is also found in your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
@@ -111,11 +114,6 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
},
|
||||
];
|
||||
|
||||
const GITHUB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GithubConfigFormValues> = {
|
||||
name: "ENABLE_GITHUB_SYNC",
|
||||
label: "GitHub",
|
||||
};
|
||||
|
||||
const GITHUB_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
||||
{
|
||||
key: "Origin_URL",
|
||||
@@ -125,11 +123,11 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
<>
|
||||
We will auto-generate this. Paste this into the <CodeBlock darkerShade>Authorized origin URL</CodeBlock> field{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="GitHub OAuth application settings"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
@@ -148,11 +146,11 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
|
||||
field{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="GitHub OAuth application settings"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
|
||||
@@ -31,6 +31,11 @@ type Props = {
|
||||
|
||||
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
|
||||
|
||||
const GITLAB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GitlabConfigFormValues> = {
|
||||
name: "ENABLE_GITLAB_SYNC",
|
||||
label: "GitLab",
|
||||
};
|
||||
|
||||
export function InstanceGitlabConfigForm(props: Props) {
|
||||
const { config } = props;
|
||||
// states
|
||||
@@ -76,7 +81,6 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
<>
|
||||
Get this from your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
@@ -99,7 +103,6 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
<>
|
||||
The client secret is also found in your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
@@ -116,11 +119,6 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
},
|
||||
];
|
||||
|
||||
const GITLAB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GitlabConfigFormValues> = {
|
||||
name: "ENABLE_GITLAB_SYNC",
|
||||
label: "GitLab",
|
||||
};
|
||||
|
||||
const GITLAB_SERVICE_FIELD: TCopyField[] = [
|
||||
{
|
||||
key: "Callback_URL",
|
||||
@@ -130,7 +128,6 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
<>
|
||||
We will auto-generate this. Paste this into the <CodeBlock darkerShade>Redirect URI</CodeBlock> field of your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
|
||||
@@ -32,6 +32,11 @@ type Props = {
|
||||
|
||||
type GoogleConfigFormValues = Record<TInstanceGoogleAuthenticationConfigurationKeys, string>;
|
||||
|
||||
const GOOGLE_FORM_SWITCH_FIELD: TControllerSwitchFormField<GoogleConfigFormValues> = {
|
||||
name: "ENABLE_GOOGLE_SYNC",
|
||||
label: "Google",
|
||||
};
|
||||
|
||||
export function InstanceGoogleConfigForm(props: Props) {
|
||||
const { config } = props;
|
||||
// states
|
||||
@@ -63,11 +68,11 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
<>
|
||||
Your client ID lives in your Google API Console.{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="Google OAuth client ID documentation"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
@@ -85,11 +90,11 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
<>
|
||||
Your client secret should also be in your Google API Console.{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="Google OAuth client secret documentation"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
@@ -101,11 +106,6 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
},
|
||||
];
|
||||
|
||||
const GOOGLE_FORM_SWITCH_FIELD: TControllerSwitchFormField<GoogleConfigFormValues> = {
|
||||
name: "ENABLE_GOOGLE_SYNC",
|
||||
label: "Google",
|
||||
};
|
||||
|
||||
const GOOGLE_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
||||
{
|
||||
key: "Origin_URL",
|
||||
@@ -120,6 +120,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="Google Cloud Console OAuth client credentials"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
@@ -142,6 +143,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="Google Cloud Console OAuth client credentials"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
@@ -105,8 +105,10 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||
resolvedTheme,
|
||||
});
|
||||
|
||||
// Update ref with latest authentication modes
|
||||
authenticationModesRef.current = authenticationModes;
|
||||
// Update ref with latest authentication modes (updateConfig reads it only from event handlers)
|
||||
useEffect(() => {
|
||||
authenticationModesRef.current = authenticationModes;
|
||||
}, [authenticationModes]);
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
|
||||
@@ -108,7 +108,7 @@ export function SendTestEmailModal(props: Props) {
|
||||
onChange={(e) => setReceiverEmail(e.target.value)}
|
||||
placeholder="Receiver email"
|
||||
className="w-full resize-none text-16"
|
||||
tabIndex={1}
|
||||
tabIndex={0}
|
||||
/>
|
||||
)}
|
||||
{sendEmailStep === ESendEmailSteps.SUCCESS && (
|
||||
@@ -122,11 +122,11 @@ export function SendTestEmailModal(props: Props) {
|
||||
)}
|
||||
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
|
||||
<div className="mt-5 flex items-center justify-end gap-2">
|
||||
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
|
||||
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={0}>
|
||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
|
||||
</Button>
|
||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
||||
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
||||
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={0}>
|
||||
{isLoading ? "Sending email" : "Send email"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -64,6 +64,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||
target="_blank"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
aria-label="Unsplash developer account documentation"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
|
||||
@@ -69,6 +69,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Help"
|
||||
className={`ml-auto grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
@@ -80,6 +81,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle sidebar"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
|
||||
@@ -30,10 +30,10 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||
|
||||
return (
|
||||
<div className="vertical-scrollbar flex scrollbar-sm h-full w-full flex-col gap-2.5 overflow-y-scroll px-4 py-4">
|
||||
{sidebarMenu.map((item, index) => {
|
||||
const isActive = item.href === pathName || pathName?.includes(item.href);
|
||||
{sidebarMenu.map((item) => {
|
||||
const isActive = item.href === pathName || pathName?.startsWith(item.href);
|
||||
return (
|
||||
<Link key={index} href={item.href} onClick={handleItemClick}>
|
||||
<Link key={item.href} href={item.href} onClick={handleItemClick}>
|
||||
<div>
|
||||
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||
<div
|
||||
|
||||
@@ -44,7 +44,13 @@ export function WorkspaceCreateForm() {
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm<IWorkspace>({ defaultValues, mode: "onChange" });
|
||||
// derived values
|
||||
const workspaceBaseURL = encodeURI(WEB_BASE_URL || window.location.origin + "/");
|
||||
const [workspaceBaseURL, setWorkspaceBaseURL] = useState(() => encodeURI(WEB_BASE_URL || ""));
|
||||
|
||||
useEffect(() => {
|
||||
if (!WEB_BASE_URL) {
|
||||
setWorkspaceBaseURL(encodeURI(window.location.origin + "/"));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCreateWorkspace = async (formData: IWorkspace) => {
|
||||
await instanceWorkspaceService
|
||||
|
||||
@@ -25,12 +25,14 @@ export function AuthBanner(props: TAuthBanner) {
|
||||
<Info size={16} className="text-accent-primary" />
|
||||
</div>
|
||||
<div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</div>
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Dismiss banner"
|
||||
className="relative ml-auto flex h-6 w-6 cursor-pointer items-center justify-center rounded-xs text-accent-primary transition-all hover:bg-accent-primary/20"
|
||||
onClick={() => handleBannerData && handleBannerData(undefined)}
|
||||
>
|
||||
<CloseIcon className="h-4 w-4 flex-shrink-0" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,9 +127,7 @@ export function InstanceSignInForm() {
|
||||
{errorData.type && errorData?.message ? (
|
||||
<Banner type="error" message={errorData?.message} />
|
||||
) : (
|
||||
<>
|
||||
{errorInfo && <AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />}
|
||||
</>
|
||||
<>{errorInfo && <AuthBanner bannerData={errorInfo} handleBannerData={setErrorInfo} />}</>
|
||||
)}
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
|
||||
@@ -170,6 +168,7 @@ export function InstanceSignInForm() {
|
||||
{showPassword ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Hide password"
|
||||
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(false)}
|
||||
>
|
||||
@@ -178,6 +177,7 @@ export function InstanceSignInForm() {
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Show password"
|
||||
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(true)}
|
||||
>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
// Minimal shim so code using next/image compiles under React Router + Vite
|
||||
// without changing call sites. It just renders a native img.
|
||||
|
||||
type NextImageProps = React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||
src: string;
|
||||
};
|
||||
|
||||
function Image({ src, alt = "", ...rest }: NextImageProps) {
|
||||
return <img src={src} alt={alt} {...rest} />;
|
||||
}
|
||||
|
||||
export default Image;
|
||||
@@ -66,7 +66,8 @@ export function ControllerInput(props: Props) {
|
||||
{type === "password" &&
|
||||
(showPassword ? (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
aria-label="Hide password"
|
||||
className="absolute top-2.5 right-3 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(false)}
|
||||
>
|
||||
@@ -74,7 +75,8 @@ export function ControllerInput(props: Props) {
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
aria-label="Show password"
|
||||
className="absolute top-2.5 right-3 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(true)}
|
||||
>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "@plane/propel/button";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
image?: string;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
secondaryButton?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function EmptyState({ title, description, image, primaryButton, secondaryButton, disabled = false }: Props) {
|
||||
return (
|
||||
<div className={`flex h-full w-full items-center justify-center`}>
|
||||
<div className="flex w-full flex-col items-center text-center">
|
||||
{image && <img src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
|
||||
<h6 className="mt-6 mb-3 text-18 font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-tertiary sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
size="lg"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,8 @@ export const HamburgerToggle = observer(function HamburgerToggle() {
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle sidebar"
|
||||
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
|
||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||
>
|
||||
@@ -34,55 +36,53 @@ const HEADER_SEGMENT_LABELS = {
|
||||
...EXTENDED_HEADER_SEGMENT_LABELS,
|
||||
};
|
||||
|
||||
// Function to dynamically generate breadcrumb items based on pathname
|
||||
const generateBreadcrumbItems = (pathname: string) => {
|
||||
const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
|
||||
pathSegments.pop();
|
||||
|
||||
let currentUrl = "";
|
||||
const breadcrumbItems = pathSegments.map((segment) => {
|
||||
currentUrl += "/" + segment;
|
||||
return {
|
||||
title: HEADER_SEGMENT_LABELS[segment] ?? segment.toUpperCase(),
|
||||
href: currentUrl,
|
||||
};
|
||||
});
|
||||
return breadcrumbItems;
|
||||
};
|
||||
|
||||
export const AdminHeader = observer(function AdminHeader() {
|
||||
const pathName = usePathname();
|
||||
|
||||
// Function to dynamically generate breadcrumb items based on pathname
|
||||
const generateBreadcrumbItems = (pathname: string) => {
|
||||
const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
|
||||
pathSegments.pop();
|
||||
|
||||
let currentUrl = "";
|
||||
const breadcrumbItems = pathSegments.map((segment) => {
|
||||
currentUrl += "/" + segment;
|
||||
return {
|
||||
title: HEADER_SEGMENT_LABELS[segment] ?? segment.toUpperCase(),
|
||||
href: currentUrl,
|
||||
};
|
||||
});
|
||||
return breadcrumbItems;
|
||||
};
|
||||
|
||||
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<HamburgerToggle />
|
||||
{breadcrumbItems.length >= 0 && (
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
href="/general/"
|
||||
label="Settings"
|
||||
icon={<Settings className="h-4 w-4 text-tertiary" />}
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
href="/general/"
|
||||
label="Settings"
|
||||
icon={<Settings className="h-4 w-4 text-tertiary" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{breadcrumbItems.map(
|
||||
(item) =>
|
||||
item.title && (
|
||||
<Breadcrumbs.Item
|
||||
key={item.title}
|
||||
component={<BreadcrumbLink href={item.href} label={item.title} />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{breadcrumbItems.map(
|
||||
(item) =>
|
||||
item.title && (
|
||||
<Breadcrumbs.Item
|
||||
key={item.title}
|
||||
component={<BreadcrumbLink href={item.href} label={item.title} />}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
)}
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
type TPageHeader = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export function PageHeader(props: TPageHeader) {
|
||||
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -12,15 +12,15 @@ import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
||||
import InstanceFailureDarkImage from "@/app/assets/instance/instance-failure-dark.svg?url";
|
||||
import InstanceFailureImage from "@/app/assets/instance/instance-failure.svg?url";
|
||||
|
||||
const handleRetry = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
export const InstanceFailureView = observer(function InstanceFailureView() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||
|
||||
const handleRetry = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import Link from "next/link";
|
||||
import { Button } from "@plane/propel/button";
|
||||
// assets
|
||||
import PlaneTakeOffImage from "@/app/assets/images/plane-takeoff.png?url";
|
||||
|
||||
export function InstanceNotReady() {
|
||||
return (
|
||||
<div className="relative container mx-auto flex h-full w-full items-center justify-center px-5">
|
||||
<div className="relative w-auto max-w-2xl space-y-8 py-10">
|
||||
<div className="relative flex flex-col items-center justify-center space-y-4">
|
||||
<h1 className="pb-3 text-24 font-bold">Welcome aboard Plane!</h1>
|
||||
<img src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="text-14 font-medium text-placeholder">Get started by setting up your instance and workspace</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link href={"/setup/?auth_enabled=0"}>
|
||||
<Button size="xl" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
// assets
|
||||
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
|
||||
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
|
||||
|
||||
export function InstanceLoading() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -273,7 +273,7 @@ export function InstanceSetupForm() {
|
||||
{showPassword.password ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
aria-label="Hide password"
|
||||
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||
onClick={() => handleShowPassword("password")}
|
||||
>
|
||||
@@ -282,7 +282,7 @@ export function InstanceSetupForm() {
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
aria-label="Show password"
|
||||
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||
onClick={() => handleShowPassword("password")}
|
||||
>
|
||||
@@ -317,7 +317,7 @@ export function InstanceSetupForm() {
|
||||
{showPassword.retypePassword ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
aria-label="Hide password"
|
||||
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
>
|
||||
@@ -326,7 +326,7 @@ export function InstanceSetupForm() {
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
aria-label="Show password"
|
||||
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
>
|
||||
@@ -354,7 +354,6 @@ export function InstanceSetupForm() {
|
||||
<label className="cursor-pointer text-13 font-medium text-tertiary" htmlFor="is_telemetry_enabled">
|
||||
Allow Plane to anonymously collect usage events.{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://developers.plane.so/self-hosting/telemetry"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/providers/store.provider";
|
||||
import { StoreContext } from "@/providers/store-context";
|
||||
import type { IInstanceStore } from "@/store/instance.store";
|
||||
|
||||
export const useInstance = (): IInstanceStore => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/providers/store.provider";
|
||||
import { StoreContext } from "@/providers/store-context";
|
||||
import type { IThemeStore } from "@/store/theme.store";
|
||||
|
||||
export const useTheme = (): IThemeStore => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/providers/store.provider";
|
||||
import { StoreContext } from "@/providers/store-context";
|
||||
import type { IUserStore } from "@/store/user.store";
|
||||
|
||||
export const useUser = (): IUserStore => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/providers/store.provider";
|
||||
import { StoreContext } from "@/providers/store-context";
|
||||
import type { IWorkspaceStore } from "@/store/workspace.store";
|
||||
|
||||
export const useWorkspace = (): IWorkspaceStore => {
|
||||
|
||||
@@ -31,9 +31,6 @@
|
||||
"@plane/ui": "workspace:*",
|
||||
"@plane/utils": "workspace:*",
|
||||
"@react-router/node": "catalog:",
|
||||
"@tanstack/react-virtual": "catalog:",
|
||||
"@tanstack/virtual-core": "catalog:",
|
||||
"axios": "catalog:",
|
||||
"isbot": "catalog:",
|
||||
"lodash-es": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
|
||||
13
apps/admin/providers/store-context.ts
Normal file
13
apps/admin/providers/store-context.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { createContext } from "react";
|
||||
// plane admin store
|
||||
import { RootStore } from "../store/root.store";
|
||||
|
||||
export const rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext<RootStore | undefined>(undefined);
|
||||
@@ -4,26 +4,15 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { createContext } from "react";
|
||||
// plane admin store
|
||||
import { RootStore } from "../store/root.store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext(rootStore);
|
||||
import { StoreContext, rootStore } from "./store-context";
|
||||
|
||||
function initializeStore(initialData = {}) {
|
||||
const singletonRootStore = rootStore ?? new RootStore();
|
||||
// If your page has Next.js data fetching methods that use a Mobx store, it will
|
||||
// get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details
|
||||
// Hydrate the shared client-side store with any initial data before use.
|
||||
if (initialData) {
|
||||
singletonRootStore.hydrate(initialData);
|
||||
rootStore.hydrate(initialData);
|
||||
}
|
||||
// For SSG and SSR always create a new store
|
||||
if (typeof window === "undefined") return singletonRootStore;
|
||||
// Create the store once in the client
|
||||
if (!rootStore) rootStore = singletonRootStore;
|
||||
return singletonRootStore;
|
||||
return rootStore;
|
||||
}
|
||||
|
||||
export type StoreProviderProps = {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export {};
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -135,12 +135,6 @@ catalogs:
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.13.12
|
||||
version: 3.13.12
|
||||
'@tanstack/virtual-core':
|
||||
specifier: ^3.13.12
|
||||
version: 3.13.12
|
||||
'@tiptap/core':
|
||||
specifier: ^2.22.3
|
||||
version: 2.26.3
|
||||
@@ -647,15 +641,6 @@ importers:
|
||||
'@react-router/node':
|
||||
specifier: 'catalog:'
|
||||
version: 7.15.0(react-router@7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
|
||||
'@tanstack/react-virtual':
|
||||
specifier: 'catalog:'
|
||||
version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@tanstack/virtual-core':
|
||||
specifier: 'catalog:'
|
||||
version: 3.13.12
|
||||
axios:
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
isbot:
|
||||
specifier: 'catalog:'
|
||||
version: 5.1.31
|
||||
|
||||
Reference in New Issue
Block a user