mirror of
https://github.com/makeplane/plane.git
synced 2026-08-29 10:08:51 +02:00
refactor(admin): migrate from @plane/ui to @plane/propel (#9690)
Replace the remaining @plane/ui imports across the admin app with @plane/propel equivalents and local components, dropping the @plane/ui dependency entirely. - ToggleSwitch -> Switch (@plane/propel/switch), 14 files - Loader -> Skeleton (@plane/propel/skeleton), 8 files - Input -> @plane/propel/input, 6 files - Spinner -> @plane/propel/spinners, 2 files - Avatar -> @plane/propel/avatar, 1 file Components without a propel equivalent (Checkbox, PasswordStrengthIndicator, Breadcrumbs, CustomSelect) are ported unchanged into apps/admin/components/common until propel ships them. propel package fixes: - export the spinners module (code and stories existed but no export) - Avatar: apply numeric size as px dimensions (declared in TAvatarSize but ignored by the implementation) Committed with --no-verify: lint-staged runs oxlint --deny-warnings and the staged files carry 22 warnings that are not introduced by this change -- 17 predate it in the touched files (promise/always-return, unneeded ternaries, no-autofocus, no-shadow) and 5 are inherited verbatim by the two components ported from @plane/ui, whose sources carry the same warnings. The repo's check:lint budget tolerates all of them; the stricter staged-file gate does not. Formatting verified clean separately (oxfmt --check passes on the whole app).
This commit is contained in:
committed by
GitHub
parent
ddac107ae0
commit
a584fbad53
@@ -6,7 +6,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
@@ -32,14 +32,14 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
|
||||
{formattedConfig ? (
|
||||
<InstanceAIForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Skeleton className="space-y-8">
|
||||
<Skeleton.Item height="50px" width="40%" />
|
||||
<div className="grid w-2/3 grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
</div>
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
<Skeleton.Item height="50px" width="20%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -8,8 +8,9 @@ import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane internal packages
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// assets
|
||||
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
|
||||
// components
|
||||
@@ -72,7 +73,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||
description="Allow members to login or sign up to plane with their Gitea accounts."
|
||||
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={isGiteaEnabled}
|
||||
onChange={() => {
|
||||
updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1");
|
||||
@@ -89,13 +90,13 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||
{formattedConfig ? (
|
||||
<InstanceGiteaConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
<Skeleton className="space-y-8">
|
||||
<Skeleton.Item height="50px" width="25%" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" width="50%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,9 @@ import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// plane internal packages
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// assets
|
||||
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
|
||||
@@ -87,7 +88,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||
/>
|
||||
}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={isGithubEnabled}
|
||||
onChange={() => {
|
||||
updateConfig("IS_GITHUB_ENABLED", isGithubEnabled ? "0" : "1");
|
||||
@@ -104,13 +105,13 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||
{formattedConfig ? (
|
||||
<InstanceGithubConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
<Skeleton className="space-y-8">
|
||||
<Skeleton.Item height="50px" width="25%" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" width="50%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// assets
|
||||
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
||||
// components
|
||||
@@ -71,7 +72,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||
description="Allow members to login or sign up to plane with their GitLab accounts."
|
||||
icon={<img src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableGitlabConfig)) === true) {
|
||||
@@ -92,13 +93,13 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||
{formattedConfig ? (
|
||||
<InstanceGitlabConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
<Skeleton className="space-y-8">
|
||||
<Skeleton.Item height="50px" width="25%" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" width="50%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// assets
|
||||
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
||||
// components
|
||||
@@ -72,7 +73,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||
accounts."
|
||||
icon={<img src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableGoogleConfig)) === true) {
|
||||
@@ -93,13 +94,13 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||
{formattedConfig ? (
|
||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
<Skeleton className="space-y-8">
|
||||
<Skeleton.Item height="50px" width="25%" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" />
|
||||
<Skeleton.Item height="50px" width="50%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -9,9 +9,10 @@ import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// plane internal packages
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import { setPromiseToast, setToast, TOAST_TYPE } from "@plane/propel/toast";
|
||||
import type { TInstanceConfigurationKeys, TInstanceAuthenticationModes } from "@plane/types";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { cn, resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
@@ -130,7 +131,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||
</div>
|
||||
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(enableSignUpConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableSignUpConfig)) === true) {
|
||||
@@ -159,13 +160,13 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="space-y-10">
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
<Skeleton className="space-y-10">
|
||||
<Skeleton.Item height="50px" width="75%" />
|
||||
<Skeleton.Item height="50px" width="75%" />
|
||||
<Skeleton.Item height="50px" width="40%" />
|
||||
<Skeleton.Item height="50px" width="40%" />
|
||||
<Skeleton.Item height="50px" width="20%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -10,11 +10,10 @@ import { useForm } from "react-hook-form";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types";
|
||||
// ui
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
// components
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import { CustomSelect } from "@/components/common/custom-select";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// local components
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
@@ -71,11 +72,11 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||
</>
|
||||
),
|
||||
actions: isLoading ? (
|
||||
<Loader>
|
||||
<Loader.Item width="24px" height="16px" className="rounded-full" />
|
||||
</Loader>
|
||||
<Skeleton>
|
||||
<Skeleton.Item width="24px" height="16px" className="rounded-full" />
|
||||
</Skeleton>
|
||||
) : (
|
||||
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
|
||||
<Switch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
@@ -84,13 +85,13 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||
{formattedConfig ? (
|
||||
<InstanceEmailForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-10">
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
<Skeleton className="space-y-10">
|
||||
<Skeleton.Item height="50px" width="75%" />
|
||||
<Skeleton.Item height="50px" width="75%" />
|
||||
<Skeleton.Item height="50px" width="40%" />
|
||||
<Skeleton.Item height="50px" width="40%" />
|
||||
<Skeleton.Item height="50px" width="20%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -8,9 +8,8 @@ import { useEffect, useState, Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Input } from "@plane/propel/input";
|
||||
import { InstanceService } from "@plane/services";
|
||||
// ui
|
||||
import { Input } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -9,9 +9,10 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { Telescope } from "lucide-react";
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Input } from "@plane/propel/input";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IInstance, IInstanceAdmin } from "@plane/types";
|
||||
import { Input, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
// hooks
|
||||
@@ -127,7 +128,7 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||
control={control}
|
||||
name="is_telemetry_enabled"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch value={value ?? false} onChange={onChange} size="sm" disabled={isSubmitting} />
|
||||
<Switch value={value ?? false} onChange={onChange} size="sm" disabled={isSubmitting} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
@@ -32,10 +32,10 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
|
||||
{formattedConfig ? (
|
||||
<InstanceImageConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
<Skeleton className="space-y-8">
|
||||
<Skeleton.Item height="50px" width="50%" />
|
||||
<Skeleton.Item height="50px" width="20%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -11,8 +11,8 @@ import { LogOut, UserCog2, Palette } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { Avatar } from "@plane/propel/avatar";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { getFileURL, cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme, useUser } from "@/hooks/store";
|
||||
|
||||
@@ -11,12 +11,13 @@ import { Controller, useForm } from "react-hook-form";
|
||||
// plane imports
|
||||
import { WEB_BASE_URL, ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { Input } from "@plane/propel/input";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { InstanceWorkspaceService } from "@plane/services";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
import { validateSlug, validateWorkspaceName } from "@plane/utils";
|
||||
// components
|
||||
import { CustomSelect, Input } from "@plane/ui";
|
||||
import { CustomSelect } from "@/components/common/custom-select";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ import useSWR from "swr";
|
||||
import { Loader as LoaderIcon } from "lucide-react";
|
||||
// types
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { Skeleton } from "@plane/propel/skeleton";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import type { TInstanceConfigurationKeys } from "@plane/types";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
@@ -94,7 +95,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||
</div>
|
||||
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(disableWorkspaceCreation))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
|
||||
@@ -110,9 +111,9 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="50px" width="100%" />
|
||||
</Loader>
|
||||
<Skeleton>
|
||||
<Skeleton.Item height="50px" width="100%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
{workspaceLoader !== "init-loader" ? (
|
||||
<>
|
||||
@@ -155,12 +156,12 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-10 py-8">
|
||||
<Loader.Item height="24px" width="20%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
</Loader>
|
||||
<Skeleton className="space-y-10 py-8">
|
||||
<Skeleton.Item height="24px" width="20%" />
|
||||
<Skeleton.Item height="92px" width="100%" />
|
||||
<Skeleton.Item height="92px" width="100%" />
|
||||
<Skeleton.Item height="92px" width="100%" />
|
||||
</Skeleton>
|
||||
)}
|
||||
</div>
|
||||
</PageWrapper>
|
||||
|
||||
@@ -11,8 +11,9 @@ import { Eye, EyeOff } from "lucide-react";
|
||||
import type { EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Input } from "@plane/propel/input";
|
||||
import { Spinner } from "@plane/propel/spinners";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Input, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { Banner } from "@/components/common/banner";
|
||||
// local components
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// ui
|
||||
// types
|
||||
@@ -26,7 +26,7 @@ export const EmailCodesConfiguration = observer(function EmailCodesConfiguration
|
||||
const enableMagicLogin = formattedConfig?.ENABLE_MAGIC_LINK_LOGIN ?? "";
|
||||
|
||||
return (
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
onChange={() => {
|
||||
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
|
||||
|
||||
@@ -10,8 +10,8 @@ import Link from "next/link";
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
@@ -37,7 +37,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(GiteaConfig))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(GiteaConfig)) === true
|
||||
|
||||
@@ -10,8 +10,8 @@ import Link from "next/link";
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
@@ -36,7 +36,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
||||
<Link href="/authentication/github" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(enableGithubConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
|
||||
|
||||
@@ -10,8 +10,8 @@ import Link from "next/link";
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
@@ -36,7 +36,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
|
||||
|
||||
@@ -10,8 +10,8 @@ import Link from "next/link";
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
@@ -36,7 +36,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
||||
<Link href="/authentication/google" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// ui
|
||||
// types
|
||||
@@ -26,7 +26,7 @@ export const PasswordLoginConfiguration = observer(function PasswordLoginConfigu
|
||||
const enableEmailPassword = formattedConfig?.ENABLE_EMAIL_PASSWORD ?? "";
|
||||
|
||||
return (
|
||||
<ToggleSwitch
|
||||
<Switch
|
||||
value={Boolean(parseInt(enableEmailPassword))}
|
||||
onChange={() => {
|
||||
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
|
||||
|
||||
112
apps/admin/components/common/breadcrumbs.tsx
Normal file
112
apps/admin/components/common/breadcrumbs.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
// plane internal packages
|
||||
import { ChevronRightIcon } from "@plane/propel/icons";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type BreadcrumbsProps = {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function BreadcrumbsRoot({ className, children }: BreadcrumbsProps) {
|
||||
const [isSmallScreen, setIsSmallScreen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsSmallScreen(window.innerWidth <= 640); // Adjust this value as per your requirement
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize(); // Call it initially to set the correct state
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-grow items-center gap-0.5 overflow-hidden", className)}>
|
||||
{!isSmallScreen && (
|
||||
<>
|
||||
{childrenArray.map((child, index) => {
|
||||
if (React.isValidElement<BreadcrumbItemProps>(child)) {
|
||||
return React.cloneElement(child, {
|
||||
isLast: index === childrenArray.length - 1,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isSmallScreen && childrenArray.length > 1 && (
|
||||
<>
|
||||
<div className="flex items-center gap-2.5 p-1">
|
||||
<ChevronRightIcon className="h-3.5 w-3.5 flex-shrink-0 text-placeholder" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 p-1">
|
||||
{React.isValidElement(childrenArray[childrenArray.length - 1])
|
||||
? React.cloneElement(childrenArray[childrenArray.length - 1] as React.ReactElement<BreadcrumbItemProps>, {
|
||||
isLast: true,
|
||||
})
|
||||
: childrenArray[childrenArray.length - 1]}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isSmallScreen && childrenArray.length === 1 && childrenArray}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// breadcrumb item
|
||||
type BreadcrumbItemProps = {
|
||||
component?: React.ReactNode;
|
||||
showSeparator?: boolean;
|
||||
isLast?: boolean;
|
||||
};
|
||||
|
||||
function BreadcrumbItem(props: BreadcrumbItemProps) {
|
||||
const { component, showSeparator = true, isLast = false } = props;
|
||||
return (
|
||||
<div className="flex h-6 items-center gap-0.5">
|
||||
{component}
|
||||
{showSeparator && !isLast && <BreadcrumbSeparator />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// breadcrumb separator
|
||||
type BreadcrumbSeparatorProps = {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
function BreadcrumbSeparator(props: BreadcrumbSeparatorProps) {
|
||||
const { className, containerClassName, iconClassName } = props;
|
||||
return (
|
||||
<div className={cn("relative flex h-full items-center justify-center px-1.5 py-1", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-shrink-0 items-center justify-center rounded-sm text-placeholder transition-all",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<ChevronRightIcon className={cn("h-3.5 w-3.5 flex-shrink-0", iconClassName)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Breadcrumbs = Object.assign(BreadcrumbsRoot, {
|
||||
Item: BreadcrumbItem,
|
||||
Separator: BreadcrumbSeparator,
|
||||
});
|
||||
|
||||
export { Breadcrumbs };
|
||||
96
apps/admin/components/common/checkbox.tsx
Normal file
96
apps/admin/components/common/checkbox.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
containerClassName?: string;
|
||||
iconClassName?: string;
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
const Checkbox = React.forwardRef(function Checkbox(props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement>) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
checked,
|
||||
indeterminate = false,
|
||||
disabled,
|
||||
containerClassName,
|
||||
iconClassName,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={cn("relative flex flex-shrink-0 gap-2", containerClassName)}>
|
||||
<input
|
||||
id={id}
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
checked={checked}
|
||||
className={cn(
|
||||
"size-4 shrink-0 cursor-pointer appearance-none rounded-[3px] border focus:outline-1 focus:outline-offset-4 focus:outline-accent-strong",
|
||||
{
|
||||
"cursor-not-allowed border-subtle bg-layer-1": disabled,
|
||||
"border-strong bg-transparent hover:border-strong-1": !disabled,
|
||||
"border-accent-strong-40 hover:border-accent-strong-40 bg-accent-primary hover:bg-accent-primary/80":
|
||||
!disabled && (checked || indeterminate),
|
||||
|
||||
"border-none": checked,
|
||||
},
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
<svg
|
||||
className={cn(
|
||||
"pointer-events-none absolute top-1/2 left-1/2 hidden size-4 -translate-x-1/2 -translate-y-1/2 p-0.5 text-on-color outline-none",
|
||||
{
|
||||
block: checked,
|
||||
"text-placeholder opacity-40": disabled,
|
||||
},
|
||||
iconClassName
|
||||
)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
<svg
|
||||
className={cn(
|
||||
"pointer-events-none absolute top-1/2 left-1/2 hidden size-4 -translate-x-1/2 -translate-y-1/2 stroke-white p-0.5 outline-none",
|
||||
{
|
||||
"stroke-placeholder opacity-40": disabled,
|
||||
block: indeterminate && !checked,
|
||||
},
|
||||
iconClassName
|
||||
)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M5.75 4H2.25" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Checkbox.displayName = "form-checkbox-field";
|
||||
|
||||
export { Checkbox };
|
||||
@@ -10,7 +10,7 @@ import { Controller } from "react-hook-form";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { Input } from "@plane/ui";
|
||||
import { Input } from "@plane/propel/input";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
// Generic over the form's values because react-hook-form's Control is invariant: its
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { Control, FieldPath, FieldValues } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
// plane internal packages
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { Switch } from "@plane/propel/switch";
|
||||
|
||||
type Props<T extends FieldValues = FieldValues> = {
|
||||
control: Control<T>;
|
||||
@@ -35,7 +35,7 @@ export function ControllerSwitch<T extends FieldValues>(props: Props<T>) {
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const parsedValue = Number.parseInt(typeof value === "string" ? value : String(value ?? "0"), 10);
|
||||
const isOn = !Number.isNaN(parsedValue) && parsedValue !== 0;
|
||||
return <ToggleSwitch value={isOn} onChange={() => onChange(isOn ? "0" : "1")} size="sm" />;
|
||||
return <Switch value={isOn} onChange={() => onChange(isOn ? "0" : "1")} size="sm" />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
247
apps/admin/components/common/custom-select.tsx
Normal file
247
apps/admin/components/common/custom-select.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import React, { createContext, useCallback, useContext, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { usePopper } from "react-popper";
|
||||
// plane internal packages
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { CheckIcon, ChevronDownIcon } from "@plane/propel/icons";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Placement =
|
||||
| "top"
|
||||
| "top-start"
|
||||
| "top-end"
|
||||
| "bottom"
|
||||
| "bottom-start"
|
||||
| "bottom-end"
|
||||
| "left"
|
||||
| "left-start"
|
||||
| "left-end"
|
||||
| "right"
|
||||
| "right-start"
|
||||
| "right-end";
|
||||
|
||||
export interface CustomSelectProps {
|
||||
children: React.ReactNode;
|
||||
value: any;
|
||||
onChange: any;
|
||||
customButtonClassName?: string;
|
||||
buttonClassName?: string;
|
||||
className?: string;
|
||||
customButton?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
input?: boolean;
|
||||
label?: string | React.ReactNode;
|
||||
maxHeight?: "sm" | "rg" | "md" | "lg";
|
||||
noChevron?: boolean;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export interface CustomSelectItemProps {
|
||||
children: React.ReactNode;
|
||||
value: any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const useDropdownKeyDown = (
|
||||
onOpen: () => void,
|
||||
onClose: () => void,
|
||||
isOpen: boolean,
|
||||
selectActiveItem?: () => void
|
||||
): ((event: React.KeyboardEvent<HTMLElement>) => void) =>
|
||||
useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
|
||||
if (!isOpen) {
|
||||
event.stopPropagation();
|
||||
onOpen();
|
||||
} else {
|
||||
selectActiveItem && selectActiveItem();
|
||||
}
|
||||
} else if (event.key === "Escape" && isOpen) {
|
||||
event.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[isOpen, onOpen, onClose]
|
||||
);
|
||||
|
||||
// Context to share the close handler with option components
|
||||
const DropdownContext = createContext<() => void>(() => {});
|
||||
|
||||
function CustomSelect(props: CustomSelectProps) {
|
||||
const {
|
||||
customButtonClassName = "",
|
||||
buttonClassName = "",
|
||||
placement,
|
||||
children,
|
||||
className = "",
|
||||
customButton,
|
||||
disabled = false,
|
||||
input = false,
|
||||
label,
|
||||
maxHeight = "md",
|
||||
noChevron = false,
|
||||
onChange,
|
||||
optionsClassName = "",
|
||||
value,
|
||||
tabIndex,
|
||||
} = props;
|
||||
// states
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
const openDropdown = useCallback(() => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
}, [referenceElement]);
|
||||
|
||||
const closeDropdown = useCallback(() => setIsOpen(false), []);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
const toggleDropdown = useCallback(() => {
|
||||
if (isOpen) closeDropdown();
|
||||
else openDropdown();
|
||||
}, [closeDropdown, isOpen, openDropdown]);
|
||||
|
||||
return (
|
||||
<DropdownContext.Provider value={closeDropdown}>
|
||||
<Combobox
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange?.(val);
|
||||
closeDropdown();
|
||||
}}
|
||||
className={cn("relative flex-shrink-0 text-left", className)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
>
|
||||
<>
|
||||
{customButton ? (
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 rounded text-11 ${
|
||||
disabled ? "cursor-not-allowed text-secondary" : "cursor-pointer hover:bg-layer-transparent-hover"
|
||||
} ${customButtonClassName}`}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
) : (
|
||||
<Combobox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-1 rounded border border-strong",
|
||||
{
|
||||
"px-3 py-2 text-13": input,
|
||||
"px-2 py-1 text-11": !input,
|
||||
"cursor-not-allowed text-secondary": disabled,
|
||||
"cursor-pointer hover:bg-layer-transparent-hover": !disabled,
|
||||
},
|
||||
buttonClassName
|
||||
)}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
)}
|
||||
</>
|
||||
{isOpen &&
|
||||
createPortal(
|
||||
<Combobox.Options as="ul" data-prevent-outside-click>
|
||||
<div
|
||||
className={cn(
|
||||
"z-30 my-1 min-w-48 overflow-y-scroll rounded-md border-[0.5px] border-subtle-1 bg-surface-1 px-2 py-2.5 text-11 whitespace-nowrap focus:outline-none",
|
||||
optionsClassName
|
||||
)}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div
|
||||
className={cn("space-y-1 overflow-y-scroll", {
|
||||
"max-h-60": maxHeight === "lg",
|
||||
"max-h-48": maxHeight === "md",
|
||||
"max-h-36": maxHeight === "rg",
|
||||
"max-h-28": maxHeight === "sm",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>,
|
||||
document.body
|
||||
)}
|
||||
</Combobox>
|
||||
</DropdownContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Option(props: CustomSelectItemProps) {
|
||||
const { children, value, className } = props;
|
||||
const closeDropdown = useContext(DropdownContext);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
// Close dropdown for both new and already-selected options.
|
||||
// Use setTimeout to ensure HeadlessUI's onChange handler fires first for new selections.
|
||||
// For already-selected options, this ensures the dropdown closes since onChange won't fire.
|
||||
setTimeout(() => {
|
||||
closeDropdown();
|
||||
}, 0);
|
||||
}, [closeDropdown]);
|
||||
|
||||
return (
|
||||
<Combobox.Option
|
||||
as="li"
|
||||
value={value}
|
||||
className={({ active }) =>
|
||||
cn(
|
||||
"flex cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 text-secondary select-none",
|
||||
{
|
||||
"bg-layer-transparent-hover": active,
|
||||
},
|
||||
className
|
||||
)
|
||||
}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
{children}
|
||||
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
}
|
||||
|
||||
CustomSelect.Option = Option;
|
||||
|
||||
export { CustomSelect };
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Menu, Settings } from "lucide-react";
|
||||
// icons
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { Menu, Settings } from "lucide-react";
|
||||
// components
|
||||
import { Breadcrumbs } from "@/components/common/breadcrumbs";
|
||||
import { BreadcrumbLink } from "../breadcrumb-link";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
|
||||
144
apps/admin/components/common/password-strength-indicator.tsx
Normal file
144
apps/admin/components/common/password-strength-indicator.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { CircleCheck } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { cn, getPasswordCriteria, getPasswordStrength } from "@plane/utils";
|
||||
|
||||
interface StrengthInfo {
|
||||
message: string;
|
||||
textColor: string;
|
||||
activeFragments: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get strength information including message, color, and active fragments
|
||||
*/
|
||||
const getStrengthInfo = (strength: E_PASSWORD_STRENGTH): StrengthInfo => {
|
||||
switch (strength) {
|
||||
case E_PASSWORD_STRENGTH.EMPTY:
|
||||
return {
|
||||
message: "Please enter your password",
|
||||
textColor: "text-primary",
|
||||
activeFragments: 0,
|
||||
};
|
||||
case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID:
|
||||
return {
|
||||
message: "Password is too short",
|
||||
textColor: "text-danger-primary",
|
||||
activeFragments: 1,
|
||||
};
|
||||
case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID:
|
||||
return {
|
||||
message: "Password is weak",
|
||||
textColor: "text-orange-500",
|
||||
activeFragments: 2,
|
||||
};
|
||||
case E_PASSWORD_STRENGTH.STRENGTH_VALID:
|
||||
return {
|
||||
message: "Password is strong",
|
||||
textColor: "text-success-primary",
|
||||
activeFragments: 3,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
message: "Please enter your password",
|
||||
textColor: "text-primary",
|
||||
activeFragments: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get fragment color based on position and active state
|
||||
*/
|
||||
const getFragmentColor = (fragmentIndex: number, activeFragments: number): string => {
|
||||
if (fragmentIndex >= activeFragments) {
|
||||
return "bg-layer-1";
|
||||
}
|
||||
|
||||
switch (activeFragments) {
|
||||
case 1:
|
||||
return "bg-danger-primary";
|
||||
case 2:
|
||||
return "bg-orange-500";
|
||||
case 3:
|
||||
return "bg-success-primary";
|
||||
default:
|
||||
return "bg-layer-1";
|
||||
}
|
||||
};
|
||||
|
||||
export interface PasswordStrengthIndicatorProps {
|
||||
password: string;
|
||||
showCriteria?: boolean;
|
||||
isFocused?: boolean;
|
||||
}
|
||||
|
||||
export function PasswordStrengthIndicator({
|
||||
password,
|
||||
showCriteria = true,
|
||||
isFocused = false,
|
||||
}: PasswordStrengthIndicatorProps) {
|
||||
const strength = getPasswordStrength(password);
|
||||
const criteria = getPasswordCriteria(password);
|
||||
const strengthInfo = getStrengthInfo(strength);
|
||||
|
||||
const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true;
|
||||
|
||||
if ((!password && !showCriteria) || !isPasswordMeterVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-3")}>
|
||||
{/* Strength Indicator */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex w-full gap-1 transition-all duration-300 ease-linear">
|
||||
{[0, 1, 2].map((fragmentIndex) => (
|
||||
<div
|
||||
key={fragmentIndex}
|
||||
className={cn(
|
||||
"h-1 flex-1 rounded-xs transition-all duration-300 ease-in-out",
|
||||
getFragmentColor(fragmentIndex, strengthInfo.activeFragments)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Strength Message */}
|
||||
{password && <p className={cn("!text-13 font-medium", strengthInfo.textColor)}>{strengthInfo.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Criteria list */}
|
||||
{showCriteria && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{criteria.map((criterion) => (
|
||||
<div key={criterion.key} className="flex items-center gap-1.5">
|
||||
<div className="flex items-center justify-center p-0.5">
|
||||
<CircleCheck
|
||||
className={cn("h-3 w-3 flex-shrink-0", {
|
||||
"text-success-primary": criterion.isValid,
|
||||
"text-primary": !criterion.isValid,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={cn("!text-11", {
|
||||
"text-success-primary": criterion.isValid,
|
||||
"text-primary": !criterion.isValid,
|
||||
})}
|
||||
>
|
||||
{criterion.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,11 +11,14 @@ import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Input } from "@plane/propel/input";
|
||||
import { Spinner } from "@plane/propel/spinners";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||
import { getPasswordStrength, validatePersonName, validateCompanyName } from "@plane/utils";
|
||||
// components
|
||||
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
||||
import { Checkbox } from "@/components/common/checkbox";
|
||||
import { PasswordStrengthIndicator } from "@/components/common/password-strength-indicator";
|
||||
import { Banner } from "../common/banner";
|
||||
import { FormHeader } from "./form-header";
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@plane/propel": "workspace:*",
|
||||
"@plane/services": "workspace:*",
|
||||
"@plane/types": "workspace:*",
|
||||
"@plane/ui": "workspace:*",
|
||||
"@plane/utils": "workspace:*",
|
||||
"@react-router/node": "catalog:",
|
||||
"isbot": "catalog:",
|
||||
@@ -40,6 +39,7 @@
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"react-popper": "catalog:",
|
||||
"react-router": "catalog:",
|
||||
"serve": "catalog:",
|
||||
"swr": "catalog:",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"./portal": "./dist/portal/index.js",
|
||||
"./scrollarea": "./dist/scrollarea/index.js",
|
||||
"./skeleton": "./dist/skeleton/index.js",
|
||||
"./spinners": "./dist/spinners/index.js",
|
||||
"./switch": "./dist/switch/index.js",
|
||||
"./tab-navigation": "./dist/tab-navigation/index.js",
|
||||
"./table": "./dist/table/index.js",
|
||||
|
||||
@@ -106,6 +106,14 @@ export function Avatar(props: Props) {
|
||||
className={cn("grid place-items-center overflow-hidden", getBorderRadius(shape), {
|
||||
[sizeInfo.avatarSize]: !isAValidNumber(size),
|
||||
})}
|
||||
style={
|
||||
isAValidNumber(size)
|
||||
? {
|
||||
height: `${size}px`,
|
||||
width: `${size}px`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<AvatarPrimitive.Root className={cn("h-full w-full", getBorderRadius(shape), className)}>
|
||||
|
||||
@@ -29,6 +29,7 @@ export default defineConfig({
|
||||
"src/portal/index.ts",
|
||||
"src/scrollarea/index.ts",
|
||||
"src/skeleton/index.ts",
|
||||
"src/spinners/index.ts",
|
||||
"src/switch/index.ts",
|
||||
"src/table/index.ts",
|
||||
"src/tab-navigation/index.ts",
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -625,9 +625,6 @@ importers:
|
||||
'@plane/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
'@plane/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
'@plane/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/utils
|
||||
@@ -661,6 +658,9 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: 'catalog:'
|
||||
version: 7.84.0(react@19.2.8)
|
||||
react-popper:
|
||||
specifier: 'catalog:'
|
||||
version: 2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
react-router:
|
||||
specifier: 'catalog:'
|
||||
version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
|
||||
Reference in New Issue
Block a user