Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2337263a37 | ||
|
|
e8bdc47d6a | ||
|
|
710138f3a1 | ||
|
|
0e8128594b | ||
|
|
350107d6c1 | ||
|
|
73e0e8d529 | ||
|
|
5247fedd23 | ||
|
|
0560849f88 | ||
|
|
79537cd1df | ||
|
|
1126ca30b0 | ||
|
|
69fe581fd8 | ||
|
|
e09d986497 | ||
|
|
044003e7ec | ||
|
|
a8b6930486 | ||
|
|
685e3fa2f8 | ||
|
|
ec6e682044 | ||
|
|
b7aa25f2c6 | ||
|
|
cf7f891bcb | ||
|
|
3faf768112 | ||
|
|
be0d1871f0 | ||
|
|
c07f7b7c1e | ||
|
|
1d4cde9ba0 | ||
|
|
c4dd4bd02f | ||
|
|
d71dfe8f86 | ||
|
|
33b6405695 | ||
|
|
68fd2463f4 | ||
|
|
a60d74a3c0 | ||
|
|
76ffe52cd5 | ||
|
|
b65b1b4828 | ||
|
|
e5063cd280 | ||
|
|
9448f642f0 | ||
|
|
443f8b793d | ||
|
|
96fa9ab15b | ||
|
|
d5bad5aedc | ||
|
|
63096ceb8c | ||
|
|
9b58b9259e | ||
|
|
1e235600b7 | ||
|
|
04b0d7ee9f | ||
|
|
5a3f709e72 | ||
|
|
5ef33ab798 | ||
|
|
27806fbe20 |
@@ -102,3 +102,5 @@ dev-editor
|
||||
storybook-static
|
||||
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
temp/
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceGiteaAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { CodeBlock } from "@/components/common/code-block";
|
||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import type { TCopyField } from "@/components/common/copy-field";
|
||||
import { CopyField } from "@/components/common/copy-field";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
config: IFormattedInstanceConfiguration;
|
||||
};
|
||||
|
||||
type GiteaConfigFormValues = Record<TInstanceGiteaAuthenticationConfigurationKeys, string>;
|
||||
|
||||
export const InstanceGiteaConfigForm: FC<Props> = (props) => {
|
||||
const { config } = props;
|
||||
// states
|
||||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { updateInstanceConfigurations } = useInstance();
|
||||
// form data
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
formState: { errors, isDirty, isSubmitting },
|
||||
} = useForm<GiteaConfigFormValues>({
|
||||
defaultValues: {
|
||||
GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com",
|
||||
GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"],
|
||||
GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"],
|
||||
},
|
||||
});
|
||||
|
||||
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
|
||||
|
||||
const GITEA_FORM_FIELDS: TControllerInputFormField[] = [
|
||||
{
|
||||
key: "GITEA_HOST",
|
||||
type: "text",
|
||||
label: "Gitea Host",
|
||||
description: (
|
||||
<>Use the URL of your Gitea instance. For the official Gitea instance, use "https://gitea.com".</>
|
||||
),
|
||||
placeholder: "https://gitea.com",
|
||||
error: Boolean(errors.GITEA_HOST),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "GITEA_CLIENT_ID",
|
||||
type: "text",
|
||||
label: "Client ID",
|
||||
description: (
|
||||
<>
|
||||
You will get this from your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://gitea.com/user/settings/applications"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Gitea OAuth application settings.
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
placeholder: "70a44354520df8bd9bcd",
|
||||
error: Boolean(errors.GITEA_CLIENT_ID),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "GITEA_CLIENT_SECRET",
|
||||
type: "password",
|
||||
label: "Client secret",
|
||||
description: (
|
||||
<>
|
||||
Your client secret is also found in your{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://gitea.com/user/settings/applications"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Gitea OAuth application settings.
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
placeholder: "9b0050f94ec1b744e32ce79ea4ffacd40d4119cb",
|
||||
error: Boolean(errors.GITEA_CLIENT_SECRET),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
const GITEA_SERVICE_FIELD: TCopyField[] = [
|
||||
{
|
||||
key: "Callback_URI",
|
||||
label: "Callback URI",
|
||||
url: `${originURL}/auth/gitea/callback/`,
|
||||
description: (
|
||||
<>
|
||||
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-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onSubmit = async (formData: GiteaConfigFormValues) => {
|
||||
const payload: Partial<GiteaConfigFormValues> = { ...formData };
|
||||
|
||||
await updateInstanceConfigurations(payload)
|
||||
.then((response = []) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your Gitea authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
|
||||
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
|
||||
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
|
||||
});
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
if (isDirty) {
|
||||
e.preventDefault();
|
||||
setIsDiscardChangesModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDiscardModal
|
||||
isOpen={isDiscardChangesModalOpen}
|
||||
onDiscardHref="/authentication"
|
||||
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
||||
/>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
||||
<div className="pt-2.5 text-xl font-medium">Gitea-provided details for Plane</div>
|
||||
{GITEA_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
control={control}
|
||||
type={field.type}
|
||||
name={field.key}
|
||||
label={field.label}
|
||||
description={field.description}
|
||||
placeholder={field.placeholder}
|
||||
error={field.error}
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for Gitea</div>
|
||||
{GITEA_SERVICE_FIELD.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Gitea Authentication - God Mode",
|
||||
};
|
||||
|
||||
export default function GiteaAuthenticationLayout({ children }: { children: ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// plane internal packages
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// icons
|
||||
import giteaLogo from "@/public/logos/gitea-logo.svg";
|
||||
//local components
|
||||
import { InstanceGiteaConfigForm } from "./form";
|
||||
|
||||
const InstanceGiteaAuthenticationPage = observer(() => {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
// state
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// config
|
||||
const enableGiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? "";
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
const updateConfig = async (key: "IS_GITEA_ENABLED", value: string) => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const payload = {
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||
|
||||
setPromiseToast(updateConfigPromise, {
|
||||
loading: "Saving Configuration...",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||
},
|
||||
error: {
|
||||
title: "Error",
|
||||
message: () => "Failed to save configuration",
|
||||
},
|
||||
});
|
||||
|
||||
await updateConfigPromise
|
||||
.then(() => {
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
const isGiteaEnabled = enableGiteaConfig === "1";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<AuthenticationMethodCard
|
||||
name="Gitea"
|
||||
description="Allow members to login or sign up to plane with their Gitea accounts."
|
||||
icon={<Image src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={isGiteaEnabled}
|
||||
onChange={() => {
|
||||
updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default InstanceGiteaAuthenticationPage;
|
||||
@@ -44,7 +44,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
|
||||
loading: "Saving Configuration...",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `GitHub authentication is now ${value ? "active" : "disabled"}.`,
|
||||
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||
},
|
||||
error: {
|
||||
title: "Error",
|
||||
|
||||
@@ -38,7 +38,7 @@ const InstanceGitlabAuthenticationPage = observer(() => {
|
||||
loading: "Saving Configuration...",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `GitLab authentication is now ${value ? "active" : "disabled"}.`,
|
||||
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||
},
|
||||
error: {
|
||||
title: "Error",
|
||||
|
||||
@@ -38,7 +38,7 @@ const InstanceGoogleAuthenticationPage = observer(() => {
|
||||
loading: "Saving Configuration...",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `Google authentication is now ${value ? "active" : "disabled"}.`,
|
||||
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||
},
|
||||
error: {
|
||||
title: "Error",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { FC } from "react";
|
||||
import { Info, X } from "lucide-react";
|
||||
import { Info } from "lucide-react";
|
||||
// plane constants
|
||||
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
||||
// icons
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
|
||||
type TAuthBanner = {
|
||||
bannerData: TAdminAuthErrorInfo | undefined;
|
||||
@@ -22,7 +24,7 @@ export const AuthBanner: FC<TAuthBanner> = (props) => {
|
||||
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
|
||||
onClick={() => handleBannerData && handleBannerData(undefined)}
|
||||
>
|
||||
<X className="w-4 h-4 flex-shrink-0" />
|
||||
<CloseIcon className="w-4 h-4 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
|
||||
import { GiteaConfiguration } from "@/components/authentication/gitea-config";
|
||||
import { GithubConfiguration } from "@/components/authentication/github-config";
|
||||
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
|
||||
import { GoogleConfiguration } from "@/components/authentication/google-config";
|
||||
@@ -19,6 +20,7 @@ import { PasswordLoginConfiguration } from "@/components/authentication/password
|
||||
// plane admin components
|
||||
import { UpgradeButton } from "@/plane-admin/components/common";
|
||||
// assets
|
||||
import giteaLogo from "@/public/logos/gitea-logo.svg";
|
||||
import githubLightModeImage from "@/public/logos/github-black.png";
|
||||
import githubDarkModeImage from "@/public/logos/github-white.png";
|
||||
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
|
||||
@@ -80,6 +82,13 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
|
||||
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
||||
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "gitea",
|
||||
name: "Gitea",
|
||||
description: "Allow members to log in or sign up to plane with their Gitea accounts.",
|
||||
icon: <Image src={giteaLogo} height={20} width={20} alt="Gitea Logo" />,
|
||||
config: <GiteaConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "oidc",
|
||||
name: "OIDC",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// icons
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const GiteaConfiguration: React.FC<Props> = observer((props) => {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
// derived values
|
||||
const GiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? "";
|
||||
const GiteaConfigured =
|
||||
!!formattedConfig?.GITEA_HOST && !!formattedConfig?.GITEA_CLIENT_ID && !!formattedConfig?.GITEA_CLIENT_SECRET;
|
||||
|
||||
return (
|
||||
<>
|
||||
{GiteaConfigured ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(GiteaConfig))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(GiteaConfig)) === true
|
||||
? updateConfig("IS_GITEA_ENABLED", "0")
|
||||
: updateConfig("IS_GITEA_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/authentication/gitea"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -393,7 +393,7 @@ class IssueLinkCreateSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IssueLink
|
||||
fields = ["url", "issue_id"]
|
||||
fields = ["title", "url", "issue_id"]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Third party imports
|
||||
import random
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
@@ -24,6 +25,47 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
and workspace association for new project initialization.
|
||||
"""
|
||||
|
||||
PROJECT_ICON_DEFAULT_COLORS = [
|
||||
"#95999f",
|
||||
"#6d7b8a",
|
||||
"#5e6ad2",
|
||||
"#02b5ed",
|
||||
"#02b55c",
|
||||
"#f2be02",
|
||||
"#e57a00",
|
||||
"#f38e82",
|
||||
]
|
||||
PROJECT_ICON_DEFAULT_ICONS = [
|
||||
"home",
|
||||
"apps",
|
||||
"settings",
|
||||
"star",
|
||||
"favorite",
|
||||
"done",
|
||||
"check_circle",
|
||||
"add_task",
|
||||
"create_new_folder",
|
||||
"dataset",
|
||||
"terminal",
|
||||
"key",
|
||||
"rocket",
|
||||
"public",
|
||||
"quiz",
|
||||
"mood",
|
||||
"gavel",
|
||||
"eco",
|
||||
"diamond",
|
||||
"forest",
|
||||
"bolt",
|
||||
"sync",
|
||||
"cached",
|
||||
"library_add",
|
||||
"view_timeline",
|
||||
"view_kanban",
|
||||
"empty_dashboard",
|
||||
"cycle",
|
||||
]
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = [
|
||||
@@ -44,10 +86,10 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
"archive_in",
|
||||
"close_in",
|
||||
"timezone",
|
||||
"logo_props",
|
||||
"external_source",
|
||||
"external_id",
|
||||
"is_issue_type_enabled",
|
||||
"is_time_tracking_enabled",
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@@ -57,6 +99,7 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"logo_props",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -86,6 +129,16 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
if ProjectIdentifier.objects.filter(name=identifier, workspace_id=self.context["workspace_id"]).exists():
|
||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||
|
||||
if validated_data.get("logo_props", None) is None:
|
||||
# Generate a random icon and color for the project icon
|
||||
validated_data["logo_props"] = {
|
||||
"in_use": "icon",
|
||||
"icon": {
|
||||
"name": random.choice(self.PROJECT_ICON_DEFAULT_ICONS),
|
||||
"color": random.choice(self.PROJECT_ICON_DEFAULT_COLORS),
|
||||
},
|
||||
}
|
||||
|
||||
project = Project.objects.create(**validated_data, workspace_id=self.context["workspace_id"])
|
||||
return project
|
||||
|
||||
|
||||
@@ -1221,7 +1221,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
pk=cycle_id,
|
||||
)
|
||||
# transfer work items only when cycle is completed (passed the end data)
|
||||
if old_cycle.end_date is not None and old_cycle.end_date < timezone.now():
|
||||
if old_cycle.end_date is not None and old_cycle.end_date > timezone.now():
|
||||
return Response(
|
||||
{"error": "The old cycle is not completed yet"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -987,12 +987,12 @@ class LabelDetailAPIEndpoint(LabelListCreateAPIEndpoint):
|
||||
serializer = LabelCreateUpdateSerializer(label, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
str(request.data.get("external_id"))
|
||||
and (label.external_id != str(request.data.get("external_id")))
|
||||
request.data.get("external_id")
|
||||
and request.data.get("external_source")
|
||||
and Label.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get("external_source", label.external_source),
|
||||
external_source=request.data.get("external_source"),
|
||||
external_id=request.data.get("external_id"),
|
||||
)
|
||||
.exclude(id=pk)
|
||||
@@ -1695,23 +1695,27 @@ class IssueActivityDetailAPIEndpoint(BaseAPIView):
|
||||
Retrieve details of a specific activity.
|
||||
Excludes comment, vote, reaction, and draft activities.
|
||||
"""
|
||||
issue_activities = (
|
||||
IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
issue_activity = (
|
||||
(
|
||||
IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id, id=pk)
|
||||
.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.filter(project__archived_at__isnull=True)
|
||||
.select_related("actor", "workspace", "issue", "project")
|
||||
)
|
||||
.filter(project__archived_at__isnull=True)
|
||||
.select_related("actor", "workspace", "issue", "project")
|
||||
).order_by(request.GET.get("order_by", "created_at"))
|
||||
.order_by(request.GET.get("order_by", "created_at"))
|
||||
.first()
|
||||
)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issue_activities),
|
||||
on_results=lambda issue_activity: IssueActivitySerializer(
|
||||
issue_activity, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
if not issue_activity:
|
||||
return Response({"message": "Activity not found.", "code": "NOT_FOUND"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return Response(
|
||||
IssueActivitySerializer(issue_activity, fields=self.fields, expand=self.expand).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -871,6 +871,8 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
|
||||
module_id=module_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
|
||||
module_name = module_issue.module.name if module_issue.module is not None else ""
|
||||
module_issue.delete()
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
@@ -878,7 +880,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
current_instance=json.dumps({"module_name": module_name}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -43,22 +43,25 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
also show related workspace if found
|
||||
"""
|
||||
|
||||
def filter_workspaces(self, query, slug, project_id, workspace_search):
|
||||
def filter_workspaces(self, query, _slug, _project_id, _workspace_search):
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
if query:
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
return (
|
||||
Workspace.objects.filter(q, workspace_member__member=self.request.user)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values("name", "id", "slug")
|
||||
)
|
||||
|
||||
def filter_projects(self, query, slug, project_id, workspace_search):
|
||||
def filter_projects(self, query, slug, _project_id, _workspace_search):
|
||||
fields = ["name", "identifier"]
|
||||
q = Q()
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
if query:
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
return (
|
||||
Project.objects.filter(
|
||||
q,
|
||||
@@ -67,6 +70,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
archived_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values("name", "id", "identifier", "workspace__slug")
|
||||
)
|
||||
@@ -74,14 +78,15 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
def filter_issues(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name", "sequence_id", "project__identifier"]
|
||||
q = Q()
|
||||
for field in fields:
|
||||
if field == "sequence_id":
|
||||
# Match whole integers only (exclude decimal numbers)
|
||||
sequences = re.findall(r"\b\d+\b", query)
|
||||
for sequence_id in sequences:
|
||||
q |= Q(**{"sequence_id": sequence_id})
|
||||
else:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
if query:
|
||||
for field in fields:
|
||||
if field == "sequence_id":
|
||||
# Match whole integers only (exclude decimal numbers)
|
||||
sequences = re.findall(r"\b\d+\b", query)
|
||||
for sequence_id in sequences:
|
||||
q |= Q(**{"sequence_id": sequence_id})
|
||||
else:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
issues = Issue.issue_objects.filter(
|
||||
q,
|
||||
@@ -106,8 +111,9 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
def filter_cycles(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
if query:
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
cycles = Cycle.objects.filter(
|
||||
q,
|
||||
@@ -120,13 +126,20 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
if workspace_search == "false" and project_id:
|
||||
cycles = cycles.filter(project_id=project_id)
|
||||
|
||||
return cycles.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
|
||||
return (
|
||||
cycles.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name", "id", "project_id", "project__identifier", "workspace__slug"
|
||||
)
|
||||
)
|
||||
|
||||
def filter_modules(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
if query:
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
modules = Module.objects.filter(
|
||||
q,
|
||||
@@ -139,13 +152,20 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
if workspace_search == "false" and project_id:
|
||||
modules = modules.filter(project_id=project_id)
|
||||
|
||||
return modules.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
|
||||
return (
|
||||
modules.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name", "id", "project_id", "project__identifier", "workspace__slug"
|
||||
)
|
||||
)
|
||||
|
||||
def filter_pages(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
if query:
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
pages = (
|
||||
Page.objects.filter(
|
||||
@@ -157,7 +177,9 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
|
||||
ArrayAgg(
|
||||
"projects__id", distinct=True, filter=~Q(projects__id=True)
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
)
|
||||
@@ -174,19 +196,28 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
if workspace_search == "false" and project_id:
|
||||
project_subquery = ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=project_id).values_list(
|
||||
"project_id", flat=True
|
||||
)[:1]
|
||||
project_subquery = ProjectPage.objects.filter(
|
||||
page_id=OuterRef("id"), project_id=project_id
|
||||
).values_list("project_id", flat=True)[:1]
|
||||
|
||||
pages = pages.annotate(project_id=Subquery(project_subquery)).filter(project_id=project_id)
|
||||
pages = pages.annotate(project_id=Subquery(project_subquery)).filter(
|
||||
project_id=project_id
|
||||
)
|
||||
|
||||
return pages.distinct().values("name", "id", "project_ids", "project_identifiers", "workspace__slug")
|
||||
return (
|
||||
pages.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name", "id", "project_ids", "project_identifiers", "workspace__slug"
|
||||
)
|
||||
)
|
||||
|
||||
def filter_views(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
if query:
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
issue_views = IssueView.objects.filter(
|
||||
q,
|
||||
@@ -199,29 +230,57 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
if workspace_search == "false" and project_id:
|
||||
issue_views = issue_views.filter(project_id=project_id)
|
||||
|
||||
return issue_views.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
|
||||
return (
|
||||
issue_views.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name", "id", "project_id", "project__identifier", "workspace__slug"
|
||||
)
|
||||
)
|
||||
|
||||
def filter_intakes(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name", "sequence_id", "project__identifier"]
|
||||
q = Q()
|
||||
if query:
|
||||
for field in fields:
|
||||
if field == "sequence_id":
|
||||
# Match whole integers only (exclude decimal numbers)
|
||||
sequences = re.findall(r"\b\d+\b", query)
|
||||
for sequence_id in sequences:
|
||||
q |= Q(**{"sequence_id": sequence_id})
|
||||
else:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
q,
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__archived_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
).filter(models.Q(issue_intake__status=0) | models.Q(issue_intake__status=-2))
|
||||
|
||||
if workspace_search == "false" and project_id:
|
||||
issues = issues.filter(project_id=project_id)
|
||||
|
||||
return (
|
||||
issues.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name",
|
||||
"id",
|
||||
"sequence_id",
|
||||
"project__identifier",
|
||||
"project_id",
|
||||
"workspace__slug",
|
||||
)[:100]
|
||||
)
|
||||
|
||||
def get(self, request, slug):
|
||||
query = request.query_params.get("search", False)
|
||||
entities_param = request.query_params.get("entities")
|
||||
workspace_search = request.query_params.get("workspace_search", "false")
|
||||
project_id = request.query_params.get("project_id", False)
|
||||
|
||||
if not query:
|
||||
return Response(
|
||||
{
|
||||
"results": {
|
||||
"workspace": [],
|
||||
"project": [],
|
||||
"issue": [],
|
||||
"cycle": [],
|
||||
"module": [],
|
||||
"issue_view": [],
|
||||
"page": [],
|
||||
}
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
MODELS_MAPPER = {
|
||||
"workspace": self.filter_workspaces,
|
||||
"project": self.filter_projects,
|
||||
@@ -230,13 +289,27 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
"module": self.filter_modules,
|
||||
"issue_view": self.filter_views,
|
||||
"page": self.filter_pages,
|
||||
"intake": self.filter_intakes,
|
||||
}
|
||||
|
||||
# Determine which entities to search
|
||||
if entities_param:
|
||||
requested_entities = [
|
||||
e.strip() for e in entities_param.split(",") if e.strip()
|
||||
]
|
||||
requested_entities = [e for e in requested_entities if e in MODELS_MAPPER]
|
||||
else:
|
||||
requested_entities = list(MODELS_MAPPER.keys())
|
||||
|
||||
results = {}
|
||||
|
||||
for model in MODELS_MAPPER.keys():
|
||||
func = MODELS_MAPPER.get(model, None)
|
||||
results[model] = func(query, slug, project_id, workspace_search)
|
||||
for entity in requested_entities:
|
||||
func = MODELS_MAPPER.get(entity)
|
||||
if func:
|
||||
results[entity] = func(
|
||||
query or None, slug, project_id, workspace_search
|
||||
)
|
||||
|
||||
return Response({"results": results}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -294,29 +367,15 @@ class SearchEndpoint(BaseAPIView):
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
if issue_id:
|
||||
issue_created_by = (
|
||||
Issue.objects.filter(id=issue_id).values_list("created_by_id", flat=True).first()
|
||||
)
|
||||
users = (
|
||||
users.filter(Q(role__gt=10) | Q(member_id=issue_created_by))
|
||||
.distinct()
|
||||
.values(
|
||||
"member__avatar_url",
|
||||
"member__display_name",
|
||||
"member__id",
|
||||
)
|
||||
)
|
||||
else:
|
||||
users = (
|
||||
users.filter(Q(role__gt=10))
|
||||
.distinct()
|
||||
.values(
|
||||
"member__avatar_url",
|
||||
"member__display_name",
|
||||
"member__id",
|
||||
)
|
||||
users = (
|
||||
users
|
||||
.distinct()
|
||||
.values(
|
||||
"member__avatar_url",
|
||||
"member__display_name",
|
||||
"member__id",
|
||||
)
|
||||
)
|
||||
|
||||
response_data["user_mention"] = list(users[:count])
|
||||
|
||||
@@ -330,12 +389,15 @@ class SearchEndpoint(BaseAPIView):
|
||||
projects = (
|
||||
Project.objects.filter(
|
||||
q,
|
||||
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
||||
Q(project_projectmember__member=self.request.user)
|
||||
| Q(network=2),
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
|
||||
.values(
|
||||
"name", "id", "identifier", "logo_props", "workspace__slug"
|
||||
)[:count]
|
||||
)
|
||||
response_data["project"] = list(projects)
|
||||
|
||||
@@ -394,16 +456,20 @@ class SearchEndpoint(BaseAPIView):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(
|
||||
start_date__gt=timezone.now(),
|
||||
then=Value("UPCOMING"),
|
||||
),
|
||||
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||
When(
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
end_date__lt=timezone.now(), then=Value("COMPLETED")
|
||||
),
|
||||
When(
|
||||
Q(start_date__isnull=True)
|
||||
& Q(end_date__isnull=True),
|
||||
then=Value("DRAFT"),
|
||||
),
|
||||
default=Value("DRAFT"),
|
||||
@@ -521,7 +587,9 @@ class SearchEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.values("member__avatar_url", "member__display_name", "member__id")[:count]
|
||||
.values(
|
||||
"member__avatar_url", "member__display_name", "member__id"
|
||||
)[:count]
|
||||
)
|
||||
response_data["user_mention"] = list(users)
|
||||
|
||||
@@ -535,12 +603,15 @@ class SearchEndpoint(BaseAPIView):
|
||||
projects = (
|
||||
Project.objects.filter(
|
||||
q,
|
||||
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
||||
Q(project_projectmember__member=self.request.user)
|
||||
| Q(network=2),
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
|
||||
.values(
|
||||
"name", "id", "identifier", "logo_props", "workspace__slug"
|
||||
)[:count]
|
||||
)
|
||||
response_data["project"] = list(projects)
|
||||
|
||||
@@ -597,16 +668,20 @@ class SearchEndpoint(BaseAPIView):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(
|
||||
start_date__gt=timezone.now(),
|
||||
then=Value("UPCOMING"),
|
||||
),
|
||||
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||
When(
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
end_date__lt=timezone.now(), then=Value("COMPLETED")
|
||||
),
|
||||
When(
|
||||
Q(start_date__isnull=True)
|
||||
& Q(end_date__isnull=True),
|
||||
then=Value("DRAFT"),
|
||||
),
|
||||
default=Value("DRAFT"),
|
||||
|
||||
@@ -38,9 +38,11 @@ AUTHENTICATION_ERROR_CODES = {
|
||||
"GITHUB_NOT_CONFIGURED": 5110,
|
||||
"GITHUB_USER_NOT_IN_ORG": 5122,
|
||||
"GITLAB_NOT_CONFIGURED": 5111,
|
||||
"GITEA_NOT_CONFIGURED": 5112,
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
|
||||
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
|
||||
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
|
||||
"GITEA_OAUTH_PROVIDER_ERROR": 5123,
|
||||
# Reset Password
|
||||
"INVALID_PASSWORD_TOKEN": 5125,
|
||||
"EXPIRED_PASSWORD_TOKEN": 5130,
|
||||
|
||||
@@ -48,6 +48,8 @@ class OauthAdapter(Adapter):
|
||||
return "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
elif self.provider == "gitlab":
|
||||
return "GITLAB_OAUTH_PROVIDER_ERROR"
|
||||
elif self.provider == "gitea":
|
||||
return "GITEA_OAUTH_PROVIDER_ERROR"
|
||||
else:
|
||||
return "OAUTH_NOT_CONFIGURED"
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import urlencode, urlparse
|
||||
import pytz
|
||||
import requests
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.adapter.oauth import OauthAdapter
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
|
||||
|
||||
class GiteaOAuthProvider(OauthAdapter):
|
||||
provider = "gitea"
|
||||
scope = "openid email profile"
|
||||
|
||||
def __init__(self, request, code=None, state=None, callback=None):
|
||||
(GITEA_CLIENT_ID, GITEA_CLIENT_SECRET, GITEA_HOST) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GITEA_CLIENT_ID",
|
||||
"default": os.environ.get("GITEA_CLIENT_ID"),
|
||||
},
|
||||
{
|
||||
"key": "GITEA_CLIENT_SECRET",
|
||||
"default": os.environ.get("GITEA_CLIENT_SECRET"),
|
||||
},
|
||||
{
|
||||
"key": "GITEA_HOST",
|
||||
"default": os.environ.get("GITEA_HOST"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if not (GITEA_CLIENT_ID and GITEA_CLIENT_SECRET and GITEA_HOST):
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_NOT_CONFIGURED"],
|
||||
error_message="GITEA_NOT_CONFIGURED",
|
||||
)
|
||||
|
||||
# Enforce scheme and normalize trailing slash(es)
|
||||
parsed = urlparse(GITEA_HOST)
|
||||
if not parsed.scheme or parsed.scheme not in ("https", "http"):
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_NOT_CONFIGURED"],
|
||||
error_message="GITEA_NOT_CONFIGURED", # avoid leaking details to query params
|
||||
)
|
||||
GITEA_HOST = GITEA_HOST.rstrip("/")
|
||||
|
||||
# Set URLs based on the host
|
||||
self.token_url = f"{GITEA_HOST}/login/oauth/access_token"
|
||||
self.userinfo_url = f"{GITEA_HOST}/api/v1/user"
|
||||
|
||||
client_id = GITEA_CLIENT_ID
|
||||
client_secret = GITEA_CLIENT_SECRET
|
||||
|
||||
redirect_uri = f"{'https' if request.is_secure() else 'http'}://{request.get_host()}/auth/gitea/callback/"
|
||||
url_params = {
|
||||
"client_id": client_id,
|
||||
"scope": self.scope,
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code",
|
||||
"state": state,
|
||||
}
|
||||
auth_url = f"{GITEA_HOST}/login/oauth/authorize?{urlencode(url_params)}"
|
||||
|
||||
super().__init__(
|
||||
request,
|
||||
self.provider,
|
||||
client_id,
|
||||
self.scope,
|
||||
redirect_uri,
|
||||
auth_url,
|
||||
self.token_url,
|
||||
self.userinfo_url,
|
||||
client_secret,
|
||||
code,
|
||||
callback=callback,
|
||||
)
|
||||
|
||||
def set_token_data(self):
|
||||
data = {
|
||||
"code": self.code,
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
}
|
||||
headers = {"Accept": "application/json"}
|
||||
token_response = self.get_user_token(data=data, headers=headers)
|
||||
super().set_token_data(
|
||||
{
|
||||
"access_token": token_response.get("access_token"),
|
||||
"refresh_token": token_response.get("refresh_token", None),
|
||||
"access_token_expired_at": (
|
||||
datetime.now(tz=pytz.utc) + timedelta(seconds=token_response.get("expires_in"))
|
||||
if token_response.get("expires_in")
|
||||
else None
|
||||
),
|
||||
"refresh_token_expired_at": (
|
||||
datetime.fromtimestamp(
|
||||
token_response.get("refresh_token_expired_at"), tz=pytz.utc
|
||||
)
|
||||
if token_response.get("refresh_token_expired_at")
|
||||
else None
|
||||
),
|
||||
"id_token": token_response.get("id_token", ""),
|
||||
}
|
||||
)
|
||||
|
||||
def __get_email(self, headers):
|
||||
try:
|
||||
# Gitea may not provide email in user response, so fetch it separately
|
||||
emails_url = f"{self.userinfo_url}/emails"
|
||||
response = requests.get(emails_url, headers=headers)
|
||||
if not response.ok:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR: Failed to fetch emails",
|
||||
)
|
||||
emails_response = response.json()
|
||||
|
||||
if not emails_response:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR: No emails found",
|
||||
)
|
||||
# Prefer primary+verified, then any verified, then primary, else first
|
||||
email = next((e.get("email") for e in emails_response if e.get("primary") and e.get("verified")), None)
|
||||
if not email:
|
||||
email = next((e.get("email") for e in emails_response if e.get("verified")), None)
|
||||
if not email:
|
||||
email = next((e.get("email") for e in emails_response if e.get("primary")), None)
|
||||
if not email and emails_response:
|
||||
# If no primary email, use the first one
|
||||
email = emails_response[0].get("email")
|
||||
return email
|
||||
except requests.RequestException:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR: Exception occurred while fetching emails",
|
||||
)
|
||||
|
||||
def set_user_data(self):
|
||||
user_info_response = self.get_user_response()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token_data.get('access_token')}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
# Get email if not provided in user info
|
||||
email = user_info_response.get("email")
|
||||
if not email:
|
||||
email = self.__get_email(headers=headers)
|
||||
|
||||
super().set_user_data(
|
||||
{
|
||||
"email": email,
|
||||
"user": {
|
||||
"provider_id": str(user_info_response.get("id")),
|
||||
"email": email,
|
||||
"avatar": user_info_response.get("avatar_url"),
|
||||
"first_name": user_info_response.get("full_name") or user_info_response.get("login"),
|
||||
"last_name": "", # Gitea doesn't provide separate first/last name
|
||||
"is_password_autoset": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -36,6 +36,10 @@ from .views import (
|
||||
SignInAuthSpaceEndpoint,
|
||||
SignUpAuthSpaceEndpoint,
|
||||
SignOutAuthSpaceEndpoint,
|
||||
GiteaCallbackEndpoint,
|
||||
GiteaOauthInitiateEndpoint,
|
||||
GiteaCallbackSpaceEndpoint,
|
||||
GiteaOauthInitiateSpaceEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -129,4 +133,17 @@ urlpatterns = [
|
||||
),
|
||||
path("change-password/", ChangePasswordEndpoint.as_view(), name="forgot-password"),
|
||||
path("set-password/", SetUserPasswordEndpoint.as_view(), name="set-password"),
|
||||
## Gitea Oauth
|
||||
path("gitea/", GiteaOauthInitiateEndpoint.as_view(), name="gitea-initiate"),
|
||||
path("gitea/callback/", GiteaCallbackEndpoint.as_view(), name="gitea-callback"),
|
||||
path(
|
||||
"spaces/gitea/",
|
||||
GiteaOauthInitiateSpaceEndpoint.as_view(),
|
||||
name="space-gitea-initiate",
|
||||
),
|
||||
path(
|
||||
"spaces/gitea/callback/",
|
||||
GiteaCallbackSpaceEndpoint.as_view(),
|
||||
name="space-gitea-callback",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -5,6 +5,7 @@ from .app.check import EmailCheckEndpoint
|
||||
from .app.email import SignInAuthEndpoint, SignUpAuthEndpoint
|
||||
from .app.github import GitHubCallbackEndpoint, GitHubOauthInitiateEndpoint
|
||||
from .app.gitlab import GitLabCallbackEndpoint, GitLabOauthInitiateEndpoint
|
||||
from .app.gitea import GiteaCallbackEndpoint, GiteaOauthInitiateEndpoint
|
||||
from .app.google import GoogleCallbackEndpoint, GoogleOauthInitiateEndpoint
|
||||
from .app.magic import MagicGenerateEndpoint, MagicSignInEndpoint, MagicSignUpEndpoint
|
||||
|
||||
@@ -17,6 +18,8 @@ from .space.github import GitHubCallbackSpaceEndpoint, GitHubOauthInitiateSpaceE
|
||||
|
||||
from .space.gitlab import GitLabCallbackSpaceEndpoint, GitLabOauthInitiateSpaceEndpoint
|
||||
|
||||
from .space.gitea import GiteaCallbackSpaceEndpoint, GiteaOauthInitiateSpaceEndpoint
|
||||
|
||||
from .space.google import GoogleCallbackSpaceEndpoint, GoogleOauthInitiateSpaceEndpoint
|
||||
|
||||
from .space.magic import (
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import uuid
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.gitea import GiteaOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||
from plane.authentication.utils.user_auth_workflow import post_user_auth_workflow
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GiteaOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(validate_next_path(next_path))
|
||||
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GiteaOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GiteaCallbackEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
base_host = request.session.get("host")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GiteaOAuthProvider(
|
||||
request=request, code=code, callback=post_user_auth_workflow
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host, path)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -0,0 +1,100 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
from urllib.parse import urlencode
|
||||
|
||||
# Django import
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.gitea import GiteaOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GiteaOauthInitiateSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_space=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(validate_next_path(next_path))
|
||||
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
provider = GiteaOAuthProvider(request=request, state=state)
|
||||
request.session["state"] = state
|
||||
auth_url = provider.get_auth_url()
|
||||
return HttpResponseRedirect(auth_url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class GiteaCallbackSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
code = request.GET.get("code")
|
||||
state = request.GET.get("state")
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITEA_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GiteaOAuthProvider(request=request, code=code)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# Process workspace and project invitations
|
||||
# redirect to referer path
|
||||
url = (
|
||||
f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -86,7 +86,6 @@ def get_issue_prefetches():
|
||||
]
|
||||
|
||||
|
||||
|
||||
def save_webhook_log(
|
||||
webhook: Webhook,
|
||||
request_method: str,
|
||||
@@ -98,10 +97,9 @@ def save_webhook_log(
|
||||
retry_count: int,
|
||||
event_type: str,
|
||||
) -> None:
|
||||
|
||||
# webhook_logs
|
||||
mongo_collection = MongoConnection.get_collection("webhook_logs")
|
||||
|
||||
|
||||
log_data = {
|
||||
"workspace_id": str(webhook.workspace_id),
|
||||
"webhook": str(webhook.id),
|
||||
@@ -123,7 +121,7 @@ def save_webhook_log(
|
||||
logger.info("Webhook log saved successfully to mongo")
|
||||
mongo_save_success = True
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
log_exception(e, warning=True)
|
||||
logger.error(f"Failed to save webhook log: {e}")
|
||||
mongo_save_success = False
|
||||
|
||||
@@ -134,7 +132,7 @@ def save_webhook_log(
|
||||
WebhookLog.objects.create(**log_data)
|
||||
logger.info("Webhook log saved successfully to database")
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
log_exception(e, warning=True)
|
||||
logger.error(f"Failed to save webhook log: {e}")
|
||||
|
||||
|
||||
@@ -244,7 +242,7 @@ def send_webhook_deactivation_email(webhook_id: str, receiver_id: str, current_s
|
||||
msg.send()
|
||||
logger.info("Email sent successfully.")
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
log_exception(e, warning=True)
|
||||
logger.error(f"Failed to send email: {e}")
|
||||
|
||||
|
||||
|
||||
@@ -171,8 +171,12 @@ def fetch_and_encode_favicon(
|
||||
@shared_task
|
||||
def crawl_work_item_link_title(id: str, url: str) -> None:
|
||||
meta_data = crawl_work_item_link_title_and_favicon(url)
|
||||
issue_link = IssueLink.objects.get(id=id)
|
||||
|
||||
try:
|
||||
issue_link = IssueLink.objects.get(id=id)
|
||||
except IssueLink.DoesNotExist:
|
||||
logger.warning(f"IssueLink not found for the id {id} and the url {url}")
|
||||
return
|
||||
|
||||
issue_link.metadata = meta_data
|
||||
|
||||
issue_link.save()
|
||||
|
||||
@@ -50,6 +50,7 @@ class InstanceEndpoint(BaseAPIView):
|
||||
IS_GITHUB_ENABLED,
|
||||
GITHUB_APP_NAME,
|
||||
IS_GITLAB_ENABLED,
|
||||
IS_GITEA_ENABLED,
|
||||
EMAIL_HOST,
|
||||
ENABLE_MAGIC_LINK_LOGIN,
|
||||
ENABLE_EMAIL_PASSWORD,
|
||||
@@ -86,6 +87,10 @@ class InstanceEndpoint(BaseAPIView):
|
||||
"key": "IS_GITLAB_ENABLED",
|
||||
"default": os.environ.get("IS_GITLAB_ENABLED", "0"),
|
||||
},
|
||||
{
|
||||
"key": "IS_GITEA_ENABLED",
|
||||
"default": os.environ.get("IS_GITEA_ENABLED", "0"),
|
||||
},
|
||||
{"key": "EMAIL_HOST", "default": os.environ.get("EMAIL_HOST", "")},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
@@ -134,6 +139,7 @@ class InstanceEndpoint(BaseAPIView):
|
||||
data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1"
|
||||
data["is_github_enabled"] = IS_GITHUB_ENABLED == "1"
|
||||
data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1"
|
||||
data["is_gitea_enabled"] = IS_GITEA_ENABLED == "1"
|
||||
data["is_magic_login_enabled"] = ENABLE_MAGIC_LINK_LOGIN == "1"
|
||||
data["is_email_password_enabled"] = ENABLE_EMAIL_PASSWORD == "1"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.utils.instance_config_variables import instance_config_variables
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -21,175 +22,7 @@ class Command(BaseCommand):
|
||||
if not os.environ.get(item):
|
||||
raise CommandError(f"{item} env variable is required.")
|
||||
|
||||
config_keys = [
|
||||
# Authentication Settings
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"value": os.environ.get("ENABLE_SIGNUP", "1"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"value": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
"category": "WORKSPACE_MANAGEMENT",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_EMAIL_PASSWORD",
|
||||
"value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"value": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_ID",
|
||||
"value": os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
"category": "GOOGLE",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_SECRET",
|
||||
"value": os.environ.get("GOOGLE_CLIENT_SECRET"),
|
||||
"category": "GOOGLE",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
"value": os.environ.get("GITHUB_CLIENT_ID"),
|
||||
"category": "GITHUB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_SECRET",
|
||||
"value": os.environ.get("GITHUB_CLIENT_SECRET"),
|
||||
"category": "GITHUB",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_ORGANIZATION_ID",
|
||||
"value": os.environ.get("GITHUB_ORGANIZATION_ID"),
|
||||
"category": "GITHUB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITLAB_HOST",
|
||||
"value": os.environ.get("GITLAB_HOST"),
|
||||
"category": "GITLAB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITLAB_CLIENT_ID",
|
||||
"value": os.environ.get("GITLAB_CLIENT_ID"),
|
||||
"category": "GITLAB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_SMTP",
|
||||
"value": os.environ.get("ENABLE_SMTP", "0"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITLAB_CLIENT_SECRET",
|
||||
"value": os.environ.get("GITLAB_CLIENT_SECRET"),
|
||||
"category": "GITLAB",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"value": os.environ.get("EMAIL_HOST", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"value": os.environ.get("EMAIL_HOST_USER", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"value": os.environ.get("EMAIL_HOST_PASSWORD", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_PORT",
|
||||
"value": os.environ.get("EMAIL_PORT", "587"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_FROM",
|
||||
"value": os.environ.get("EMAIL_FROM", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_USE_TLS",
|
||||
"value": os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_USE_SSL",
|
||||
"value": os.environ.get("EMAIL_USE_SSL", "0"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "LLM_API_KEY",
|
||||
"value": os.environ.get("LLM_API_KEY"),
|
||||
"category": "AI",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "LLM_PROVIDER",
|
||||
"value": os.environ.get("LLM_PROVIDER", "openai"),
|
||||
"category": "AI",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "LLM_MODEL",
|
||||
"value": os.environ.get("LLM_MODEL", "gpt-4o-mini"),
|
||||
"category": "AI",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
# Deprecated, use LLM_MODEL
|
||||
{
|
||||
"key": "GPT_ENGINE",
|
||||
"value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "UNSPLASH_ACCESS_KEY",
|
||||
"value": os.environ.get("UNSPLASH_ACCESS_KEY", ""),
|
||||
"category": "UNSPLASH",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
# intercom settings
|
||||
{
|
||||
"key": "IS_INTERCOM_ENABLED",
|
||||
"value": os.environ.get("IS_INTERCOM_ENABLED", "1"),
|
||||
"category": "INTERCOM",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "INTERCOM_APP_ID",
|
||||
"value": os.environ.get("INTERCOM_APP_ID", ""),
|
||||
"category": "INTERCOM",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
]
|
||||
|
||||
for item in config_keys:
|
||||
for item in instance_config_variables:
|
||||
obj, created = InstanceConfiguration.objects.get_or_create(key=item.get("key"))
|
||||
if created:
|
||||
obj.category = item.get("category")
|
||||
@@ -203,7 +36,7 @@ class Command(BaseCommand):
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(f"{obj.key} configuration already exists"))
|
||||
|
||||
keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED"]
|
||||
keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED", "IS_GITEA_ENABLED"]
|
||||
if not InstanceConfiguration.objects.filter(key__in=keys).exists():
|
||||
for key in keys:
|
||||
if key == "IS_GOOGLE_ENABLED":
|
||||
@@ -282,6 +115,34 @@ class Command(BaseCommand):
|
||||
is_encrypted=False,
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable."))
|
||||
if key == "IS_GITEA_ENABLED":
|
||||
GITEA_HOST, GITEA_CLIENT_ID, GITEA_CLIENT_SECRET = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GITEA_HOST",
|
||||
"default": os.environ.get("GITEA_HOST", ""),
|
||||
},
|
||||
{
|
||||
"key": "GITEA_CLIENT_ID",
|
||||
"default": os.environ.get("GITEA_CLIENT_ID", ""),
|
||||
},
|
||||
{
|
||||
"key": "GITEA_CLIENT_SECRET",
|
||||
"default": os.environ.get("GITEA_CLIENT_SECRET", ""),
|
||||
},
|
||||
]
|
||||
)
|
||||
if bool(GITEA_HOST) and bool(GITEA_CLIENT_ID) and bool(GITEA_CLIENT_SECRET):
|
||||
value = "1"
|
||||
else:
|
||||
value = "0"
|
||||
InstanceConfiguration.objects.create(
|
||||
key="IS_GITEA_ENABLED",
|
||||
value=value,
|
||||
category="AUTHENTICATION",
|
||||
is_encrypted=False,
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable."))
|
||||
else:
|
||||
for key in keys:
|
||||
self.stdout.write(self.style.WARNING(f"{key} configuration already exists"))
|
||||
|
||||
@@ -237,10 +237,6 @@ def validate_html_content(html_content: str):
|
||||
except Exception:
|
||||
summary = str(diff)
|
||||
logger.warning(f"HTML sanitization removals: {summary}")
|
||||
log_exception(
|
||||
ValueError(f"HTML sanitization removals: {summary}"),
|
||||
warning=True,
|
||||
)
|
||||
return True, None, clean_html
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .core import core_config_variables
|
||||
from .extended import extended_config_variables
|
||||
|
||||
instance_config_variables = [*core_config_variables, *extended_config_variables]
|
||||
@@ -0,0 +1,233 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
authentication_config_variables = [
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"value": os.environ.get("ENABLE_SIGNUP", "1"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_EMAIL_PASSWORD",
|
||||
"value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"value": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"),
|
||||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
]
|
||||
|
||||
workspace_management_config_variables = [
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"value": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
"category": "WORKSPACE_MANAGEMENT",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
]
|
||||
|
||||
google_config_variables = [
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_ID",
|
||||
"value": os.environ.get("GOOGLE_CLIENT_ID"),
|
||||
"category": "GOOGLE",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GOOGLE_CLIENT_SECRET",
|
||||
"value": os.environ.get("GOOGLE_CLIENT_SECRET"),
|
||||
"category": "GOOGLE",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
]
|
||||
|
||||
github_config_variables = [
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
"value": os.environ.get("GITHUB_CLIENT_ID"),
|
||||
"category": "GITHUB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_CLIENT_SECRET",
|
||||
"value": os.environ.get("GITHUB_CLIENT_SECRET"),
|
||||
"category": "GITHUB",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "GITHUB_ORGANIZATION_ID",
|
||||
"value": os.environ.get("GITHUB_ORGANIZATION_ID"),
|
||||
"category": "GITHUB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
gitlab_config_variables = [
|
||||
{
|
||||
"key": "GITLAB_HOST",
|
||||
"value": os.environ.get("GITLAB_HOST"),
|
||||
"category": "GITLAB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITLAB_CLIENT_ID",
|
||||
"value": os.environ.get("GITLAB_CLIENT_ID"),
|
||||
"category": "GITLAB",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITLAB_CLIENT_SECRET",
|
||||
"value": os.environ.get("GITLAB_CLIENT_SECRET"),
|
||||
"category": "GITLAB",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
]
|
||||
|
||||
gitea_config_variables = [
|
||||
{
|
||||
"key": "IS_GITEA_ENABLED",
|
||||
"value": os.environ.get("IS_GITEA_ENABLED", "0"),
|
||||
"category": "GITEA",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITEA_HOST",
|
||||
"value": os.environ.get("GITEA_HOST"),
|
||||
"category": "GITEA",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITEA_CLIENT_ID",
|
||||
"value": os.environ.get("GITEA_CLIENT_ID"),
|
||||
"category": "GITEA",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "GITEA_CLIENT_SECRET",
|
||||
"value": os.environ.get("GITEA_CLIENT_SECRET"),
|
||||
"category": "GITEA",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
]
|
||||
|
||||
smtp_config_variables = [
|
||||
{
|
||||
"key": "ENABLE_SMTP",
|
||||
"value": os.environ.get("ENABLE_SMTP", "0"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"value": os.environ.get("EMAIL_HOST", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"value": os.environ.get("EMAIL_HOST_USER", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"value": os.environ.get("EMAIL_HOST_PASSWORD", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_PORT",
|
||||
"value": os.environ.get("EMAIL_PORT", "587"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_FROM",
|
||||
"value": os.environ.get("EMAIL_FROM", ""),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_USE_TLS",
|
||||
"value": os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_USE_SSL",
|
||||
"value": os.environ.get("EMAIL_USE_SSL", "0"),
|
||||
"category": "SMTP",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
]
|
||||
|
||||
llm_config_variables = [
|
||||
{
|
||||
"key": "LLM_API_KEY",
|
||||
"value": os.environ.get("LLM_API_KEY"),
|
||||
"category": "AI",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
{
|
||||
"key": "LLM_PROVIDER",
|
||||
"value": os.environ.get("LLM_PROVIDER", "openai"),
|
||||
"category": "AI",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "LLM_MODEL",
|
||||
"value": os.environ.get("LLM_MODEL", "gpt-4o-mini"),
|
||||
"category": "AI",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
# Deprecated, use LLM_MODEL
|
||||
{
|
||||
"key": "GPT_ENGINE",
|
||||
"value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
|
||||
"category": "AI",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
]
|
||||
|
||||
unsplash_config_variables = [
|
||||
{
|
||||
"key": "UNSPLASH_ACCESS_KEY",
|
||||
"value": os.environ.get("UNSPLASH_ACCESS_KEY", ""),
|
||||
"category": "UNSPLASH",
|
||||
"is_encrypted": True,
|
||||
},
|
||||
]
|
||||
|
||||
intercom_config_variables = [
|
||||
{
|
||||
"key": "IS_INTERCOM_ENABLED",
|
||||
"value": os.environ.get("IS_INTERCOM_ENABLED", "1"),
|
||||
"category": "INTERCOM",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "INTERCOM_APP_ID",
|
||||
"value": os.environ.get("INTERCOM_APP_ID", ""),
|
||||
"category": "INTERCOM",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
]
|
||||
|
||||
core_config_variables = [
|
||||
*authentication_config_variables,
|
||||
*workspace_management_config_variables,
|
||||
*google_config_variables,
|
||||
*github_config_variables,
|
||||
*gitlab_config_variables,
|
||||
*gitea_config_variables,
|
||||
*smtp_config_variables,
|
||||
*llm_config_variables,
|
||||
*unsplash_config_variables,
|
||||
*intercom_config_variables,
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
extended_config_variables = []
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { Info, X } from "lucide-react";
|
||||
import { Info } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// helpers
|
||||
import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||
|
||||
@@ -24,7 +25,7 @@ export const AuthBanner: FC<TAuthBanner> = (props) => {
|
||||
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
|
||||
onClick={() => handleBannerData && handleBannerData(undefined)}
|
||||
>
|
||||
<X className="w-4 h-4 flex-shrink-0" />
|
||||
<CloseIcon className="w-4 h-4 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import GithubLightLogo from "/public/logos/github-black.png";
|
||||
import GithubDarkLogo from "/public/logos/github-dark.svg";
|
||||
import GitlabLogo from "/public/logos/gitlab-logo.svg";
|
||||
import GoogleLogo from "/public/logos/google-logo.svg";
|
||||
import GiteaLogo from "/public/logos/gitea-logo.svg";
|
||||
// local imports
|
||||
import { TermsAndConditions } from "../terms-and-conditions";
|
||||
import { AuthBanner } from "./auth-banner";
|
||||
@@ -92,7 +93,12 @@ export const AuthRoot: FC = observer(() => {
|
||||
const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
|
||||
const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
|
||||
const isOAuthEnabled =
|
||||
(config && (config?.is_google_enabled || config?.is_github_enabled || config?.is_gitlab_enabled)) || false;
|
||||
(config &&
|
||||
(config?.is_google_enabled ||
|
||||
config?.is_github_enabled ||
|
||||
config?.is_gitlab_enabled ||
|
||||
config?.is_gitea_enabled)) ||
|
||||
false;
|
||||
|
||||
// submit handler- email verification
|
||||
const handleEmailVerification = async (data: IEmailCheckData) => {
|
||||
@@ -189,6 +195,15 @@ export const AuthRoot: FC = observer(() => {
|
||||
},
|
||||
enabled: config?.is_gitlab_enabled,
|
||||
},
|
||||
{
|
||||
id: "gitea",
|
||||
text: `${content} with Gitea`,
|
||||
icon: <Image src={GiteaLogo} height={18} width={18} alt="Gitea Logo" />,
|
||||
onClick: () => {
|
||||
window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`);
|
||||
},
|
||||
enabled: config?.is_gitea_enabled,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import type { TFilters } from "@/types/issue";
|
||||
// components
|
||||
import { AppliedPriorityFilters } from "./priority";
|
||||
@@ -55,7 +55,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
<CloseIcon height={12} width={12} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
{t("common.clear_all")}
|
||||
<X size={12} strokeWidth={2} />
|
||||
<CloseIcon height={12} width={12} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import type { IIssueLabel } from "@/types/issue";
|
||||
|
||||
@@ -34,7 +34,7 @@ export const AppliedLabelsFilters: React.FC<Props> = (props) => {
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(labelId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
<CloseIcon height={10} width={10} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { PriorityIcon } from "@plane/propel/icons";
|
||||
import { CloseIcon, PriorityIcon } from "@plane/propel/icons";
|
||||
import type { TIssuePriorities } from "@plane/propel/icons";
|
||||
|
||||
type Props = {
|
||||
@@ -25,7 +24,7 @@ export const AppliedPriorityFilters: React.FC<Props> = (props) => {
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(priority)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
<CloseIcon height={10} width={10} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import { CloseIcon, StateGroupIcon } from "@plane/propel/icons";
|
||||
// hooks
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
|
||||
@@ -34,7 +33,7 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(stateId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
<CloseIcon height={10} width={10} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
// lucide icons
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
// icons
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons";
|
||||
|
||||
interface IFilterHeader {
|
||||
title: string;
|
||||
@@ -18,7 +17,7 @@ export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }
|
||||
className="grid h-5 w-5 flex-shrink-0 place-items-center rounded hover:bg-custom-background-80"
|
||||
onClick={handleIsPreviewEnabled}
|
||||
>
|
||||
{isPreviewEnabled ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
{isPreviewEnabled ? <ChevronUpIcon height={14} width={14} /> : <ChevronDownIcon height={14} width={14} />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { Search } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import type { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
|
||||
// local imports
|
||||
@@ -37,7 +38,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
{filtersSearchQuery !== "" && (
|
||||
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
|
||||
<X className="text-custom-text-300" size={12} strokeWidth={2} />
|
||||
<CloseIcon className="text-custom-text-300" height={12} width={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Circle, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Circle } from "lucide-react";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons";
|
||||
// mobx
|
||||
|
||||
interface IHeaderSubGroupByCard {
|
||||
@@ -20,7 +21,7 @@ export const HeaderSubGroupByCard: FC<IHeaderSubGroupByCard> = observer((props)
|
||||
onClick={() => toggleExpanded()}
|
||||
>
|
||||
<div className="flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80">
|
||||
{isExpanded ? <ChevronUp width={14} strokeWidth={2} /> : <ChevronDown width={14} strokeWidth={2} />}
|
||||
{isExpanded ? <ChevronUpIcon width={14} strokeWidth={2} /> : <ChevronDownIcon width={14} strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
<div className="flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarCheck2 } from "lucide-react";
|
||||
import { DueDatePropertyIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
@@ -33,7 +33,7 @@ export const IssueBlockDate = observer((props: Props) => {
|
||||
"border-[0.5px] border-custom-border-300": shouldShowBorder,
|
||||
})}
|
||||
>
|
||||
<CalendarCheck2 className="size-3 flex-shrink-0" />
|
||||
<DueDatePropertyIcon className="size-3 flex-shrink-0" />
|
||||
{formattedDate ? formattedDate : "No Date"}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Tags } from "lucide-react";
|
||||
import { LabelPropertyIcon } from "@plane/propel/icons";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
// hooks
|
||||
@@ -25,7 +25,7 @@ export const IssueBlockLabels = observer(({ labelIds, shouldShowLabel = false }:
|
||||
<div
|
||||
className={`flex h-full items-center justify-center gap-2 rounded px-2.5 py-1 text-xs border-[0.5px] border-custom-border-300`}
|
||||
>
|
||||
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<LabelPropertyIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
{shouldShowLabel && <span>No Labels</span>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Users } from "lucide-react";
|
||||
import { MembersPropertyIcon } from "@plane/propel/icons";
|
||||
// plane ui
|
||||
import { Avatar, AvatarGroup } from "@plane/ui";
|
||||
// plane utils
|
||||
@@ -49,7 +49,11 @@ export const ButtonAvatars: React.FC<AvatarProps> = observer((props: AvatarProps
|
||||
}
|
||||
}
|
||||
|
||||
return Icon ? <Icon className="h-3 w-3 flex-shrink-0" /> : <Users className="h-3 w-3 mx-[4px] flex-shrink-0" />;
|
||||
return Icon ? (
|
||||
<Icon className="h-3 w-3 flex-shrink-0" />
|
||||
) : (
|
||||
<MembersPropertyIcon className="h-3 w-3 mx-[4px] flex-shrink-0" />
|
||||
);
|
||||
});
|
||||
|
||||
export const IssueBlockMembers = observer(({ memberIds, shouldShowBorder = true }: Props) => {
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import type { LucideProps } from "lucide-react";
|
||||
import { List, Kanban } from "lucide-react";
|
||||
import type { TIssueLayout } from "@plane/constants";
|
||||
import { ListLayoutIcon, BoardLayoutIcon } from "@plane/propel/icons";
|
||||
import type { ISvgIcons } from "@plane/propel/icons";
|
||||
|
||||
export const IssueLayoutIcon = ({
|
||||
layout,
|
||||
size,
|
||||
...props
|
||||
}: { layout: TIssueLayout; size?: number } & Omit<ISvgIcons, "width" | "height">) => {
|
||||
const iconProps = {
|
||||
...props,
|
||||
...(size && { width: size, height: size }),
|
||||
};
|
||||
|
||||
export const IssueLayoutIcon = ({ layout, ...props }: { layout: TIssueLayout } & LucideProps) => {
|
||||
switch (layout) {
|
||||
case "list":
|
||||
return <List {...props} />;
|
||||
return <ListLayoutIcon {...iconProps} />;
|
||||
case "kanban":
|
||||
return <Kanban {...props} />;
|
||||
return <BoardLayoutIcon {...iconProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Check, MessageSquare, MoreVertical, X } from "lucide-react";
|
||||
import { Check, MessageSquare, MoreVertical } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
import type { TIssuePublicComment } from "@plane/types";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// components
|
||||
@@ -136,7 +137,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" strokeWidth={2} />
|
||||
<CloseIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { CalendarCheck2, Signal } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { DoubleCircleIcon, StateGroupIcon } from "@plane/propel/icons";
|
||||
import { StatePropertyIcon, StateGroupIcon, PriorityPropertyIcon, DueDatePropertyIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { cn, getIssuePriorityFilters } from "@plane/utils";
|
||||
// components
|
||||
@@ -66,7 +65,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDet
|
||||
<div className={`space-y-2 ${mode === "full" ? "pt-3" : ""}`}>
|
||||
<div className="flex items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<DoubleCircleIcon className="size-4 flex-shrink-0" />
|
||||
<StatePropertyIcon className="size-4 flex-shrink-0" />
|
||||
<span>State</span>
|
||||
</div>
|
||||
<div className="w-3/4 flex items-center gap-1.5 py-0.5 text-sm">
|
||||
@@ -77,7 +76,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDet
|
||||
|
||||
<div className="flex items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Signal className="size-4 flex-shrink-0" />
|
||||
<PriorityPropertyIcon className="size-4 flex-shrink-0" />
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
@@ -106,7 +105,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDet
|
||||
|
||||
<div className="flex items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarCheck2 className="size-4 flex-shrink-0" />
|
||||
<DueDatePropertyIcon className="size-4 flex-shrink-0" />
|
||||
<span>Due date</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -116,7 +115,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDet
|
||||
"text-red-500": shouldHighlightIssueDueDate(issueDetails.target_date, state?.group),
|
||||
})}
|
||||
>
|
||||
<CalendarCheck2 className="size-3" />
|
||||
<DueDatePropertyIcon className="size-3" />
|
||||
{renderFormattedDate(issueDetails.target_date)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -6,20 +6,18 @@ import { useRouter } from "next/navigation";
|
||||
// plane package imports
|
||||
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateDetailed } from "@plane/propel/empty-state";
|
||||
import { Tabs } from "@plane/ui";
|
||||
import type { TabItem } from "@plane/ui";
|
||||
// components
|
||||
import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
// hooks
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { getAnalyticsTabs } from "@/plane-web/components/analytics/tabs";
|
||||
|
||||
type Props = {
|
||||
@@ -46,9 +44,6 @@ const AnalyticsPage = observer((props: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// helper hooks
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" });
|
||||
|
||||
// permissions
|
||||
const canPerformEmptyStateActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
@@ -96,22 +91,20 @@ const AnalyticsPage = observer((props: Props) => {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DetailedEmptyState
|
||||
title={t("workspace_analytics.empty_state.general.title")}
|
||||
description={t("workspace_analytics.empty_state.general.description")}
|
||||
assetPath={resolvedPath}
|
||||
customPrimaryButton={
|
||||
<ComicBoxButton
|
||||
label={t("workspace_analytics.empty_state.general.primary_button.text")}
|
||||
title={t("workspace_analytics.empty_state.general.primary_button.comic.title")}
|
||||
description={t("workspace_analytics.empty_state.general.primary_button.comic.description")}
|
||||
onClick={() => {
|
||||
<EmptyStateDetailed
|
||||
assetKey="project"
|
||||
title={t("workspace_projects.empty_state.no_projects.title")}
|
||||
description={t("workspace_projects.empty_state.no_projects.description")}
|
||||
actions={[
|
||||
{
|
||||
label: "Create a project",
|
||||
onClick: () => {
|
||||
toggleCreateProjectModal(true);
|
||||
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
|
||||
}}
|
||||
disabled={!canPerformEmptyStateActions}
|
||||
/>
|
||||
}
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { observer } from "mobx-react";
|
||||
import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider";
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
// plane web components
|
||||
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
|
||||
import { ProjectAppSidebar } from "./_sidebar";
|
||||
|
||||
const WorkspaceLayoutContent = observer(({ children }: { children: React.ReactNode }) => (
|
||||
<>
|
||||
<ProjectsAppPowerKProvider />
|
||||
<WorkspaceAuthWrapper>
|
||||
<div className="relative flex flex-col h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
|
||||
<div id="full-screen-portal" className="inset-0 absolute w-full" />
|
||||
<div className="relative flex size-full overflow-hidden">
|
||||
<ProjectAppSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</WorkspaceAuthWrapper>
|
||||
</>
|
||||
));
|
||||
|
||||
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AuthenticationWrapper>
|
||||
<CommandPalette />
|
||||
<WorkspaceAuthWrapper>
|
||||
<div className="relative flex flex-col h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
|
||||
<div id="full-screen-portal" className="inset-0 absolute w-full" />
|
||||
<div className="relative flex size-full overflow-hidden">
|
||||
<ProjectAppSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</WorkspaceAuthWrapper>
|
||||
<WorkspaceLayoutContent>{children}</WorkspaceLayoutContent>
|
||||
</AuthenticationWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { ChevronDown, PanelRight } from "lucide-react";
|
||||
import { PanelRight } from "lucide-react";
|
||||
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { YourWorkIcon } from "@plane/propel/icons";
|
||||
import { YourWorkIcon, ChevronDownIcon } from "@plane/propel/icons";
|
||||
import type { IUserProfileProjectSegregation } from "@plane/types";
|
||||
import { Breadcrumbs, Header, CustomMenu } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
@@ -75,7 +75,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||
customButton={
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1.5">
|
||||
<span className="flex flex-grow justify-center text-sm text-custom-text-200">{type}</span>
|
||||
<ChevronDown className="h-4 w-4 text-custom-text-400" />
|
||||
<ChevronDownIcon className="h-4 w-4 text-custom-text-400" />
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
@@ -88,7 +88,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
customButton={
|
||||
<div className="flex flex-center text-sm text-custom-text-200">
|
||||
{t("common.layout")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="flex flex-center text-custom-text-200 text-sm"
|
||||
@@ -117,7 +117,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
||||
menuButton={
|
||||
<div className="flex flex-center text-sm text-custom-text-200">
|
||||
{t("common.display")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
+33
-12
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// ui
|
||||
import { Banner } from "@plane/propel/banner";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { ArchiveIcon } from "@plane/propel/icons";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
@@ -16,6 +19,7 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
const ArchivedIssueDetailsPage = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, archivedIssueId } = useParams();
|
||||
const router = useRouter();
|
||||
// states
|
||||
// hooks
|
||||
const {
|
||||
@@ -62,18 +66,35 @@ const ArchivedIssueDetailsPage = observer(() => {
|
||||
</div>
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<div className="h-full w-full space-y-3 divide-y-2 divide-custom-border-200 overflow-y-auto">
|
||||
{workspaceSlug && projectId && archivedIssueId && (
|
||||
<IssueDetailRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={archivedIssueId.toString()}
|
||||
is_archived
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
<Banner
|
||||
variant="warning"
|
||||
title="This work item has been archived. Visit the Archives section to restore it."
|
||||
icon={<ArchiveIcon className="size-4" />}
|
||||
action={
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/archives/issues/`)}
|
||||
>
|
||||
Go to archives
|
||||
</Button>
|
||||
}
|
||||
className="border-b border-custom-border-200"
|
||||
/>
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<div className="h-full w-full space-y-3 divide-y-2 divide-custom-border-200 overflow-y-auto">
|
||||
{workspaceSlug && projectId && archivedIssueId && (
|
||||
<IssueDetailRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={archivedIssueId.toString()}
|
||||
is_archived
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon, ChevronDownIcon } from "@plane/propel/icons";
|
||||
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
@@ -20,12 +20,12 @@ import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
const SUPPORTED_LAYOUTS = [
|
||||
{ key: "list", titleTranslationKey: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar },
|
||||
{ key: "list", titleTranslationKey: "issue.layouts.list", icon: ListLayoutIcon },
|
||||
{ key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: BoardLayoutIcon },
|
||||
{ key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: CalendarLayoutIcon },
|
||||
];
|
||||
|
||||
export const CycleIssuesMobileHeader = () => {
|
||||
export const CycleIssuesMobileHeader = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, cycleId } = useParams();
|
||||
// states
|
||||
@@ -123,7 +123,7 @@ export const CycleIssuesMobileHeader = () => {
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
{t("common.display")}
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
<ChevronDownIcon className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -151,4 +151,4 @@ export const CycleIssuesMobileHeader = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
+8
-7
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { GanttChartSquare, LayoutGrid, List } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ISvgIcons } from "@plane/propel/icons";
|
||||
import { TimelineLayoutIcon, GridLayoutIcon, ListLayoutIcon } from "@plane/propel/icons";
|
||||
// plane package imports
|
||||
import type { TCycleLayoutOptions } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
@@ -13,22 +14,22 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
const CYCLE_VIEW_LAYOUTS: {
|
||||
key: TCycleLayoutOptions;
|
||||
icon: LucideIcon;
|
||||
icon: React.FC<ISvgIcons>;
|
||||
title: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "list",
|
||||
icon: List,
|
||||
icon: ListLayoutIcon,
|
||||
title: "List layout",
|
||||
},
|
||||
{
|
||||
key: "board",
|
||||
icon: LayoutGrid,
|
||||
icon: GridLayoutIcon,
|
||||
title: "Gallery layout",
|
||||
},
|
||||
{
|
||||
key: "gantt",
|
||||
icon: GanttChartSquare,
|
||||
icon: TimelineLayoutIcon,
|
||||
title: "Timeline layout",
|
||||
},
|
||||
];
|
||||
@@ -45,7 +46,7 @@ export const CyclesListMobileHeader = observer(() => {
|
||||
// placement="bottom-start"
|
||||
customButton={
|
||||
<span className="flex items-center gap-2">
|
||||
<List className="h-4 w-4" />
|
||||
<ListLayoutIcon className="h-4 w-4" />
|
||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
+14
-17
@@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, CYCLE_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateDetailed } from "@plane/propel/empty-state";
|
||||
import type { TCycleFilters } from "@plane/types";
|
||||
import { EUserProjectRoles } from "@plane/types";
|
||||
// components
|
||||
@@ -15,7 +16,6 @@ import { PageHead } from "@/components/core/page-title";
|
||||
import { CycleAppliedFiltersList } from "@/components/cycles/applied-filters";
|
||||
import { CyclesView } from "@/components/cycles/cycles-view";
|
||||
import { CycleCreateUpdateModal } from "@/components/cycles/modal";
|
||||
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader";
|
||||
// hooks
|
||||
@@ -96,22 +96,19 @@ const ProjectCyclesPage = observer(() => {
|
||||
/>
|
||||
{totalCycles === 0 ? (
|
||||
<div className="h-full place-items-center">
|
||||
<DetailedEmptyState
|
||||
title={t("project_cycles.empty_state.general.title")}
|
||||
description={t("project_cycles.empty_state.general.description")}
|
||||
assetPath={resolvedPath}
|
||||
customPrimaryButton={
|
||||
<ComicBoxButton
|
||||
label={t("project_cycles.empty_state.general.primary_button.text")}
|
||||
title={t("project_cycles.empty_state.general.primary_button.comic.title")}
|
||||
description={t("project_cycles.empty_state.general.primary_button.comic.description")}
|
||||
data-ph-element={CYCLE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON}
|
||||
onClick={() => {
|
||||
setCreateModal(true);
|
||||
}}
|
||||
disabled={!hasMemberLevelPermission}
|
||||
/>
|
||||
}
|
||||
<EmptyStateDetailed
|
||||
assetKey="cycle"
|
||||
title={t("project_empty_state.cycles.title")}
|
||||
description={t("project_empty_state.cycles.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("project_empty_state.cycles.cta_primary"),
|
||||
onClick: () => setCreateModal(true),
|
||||
variant: "primary",
|
||||
disabled: !hasMemberLevelPermission,
|
||||
"data-ph-element": CYCLE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+2
-2
@@ -3,10 +3,10 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
@@ -79,7 +79,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
||||
menuButton={
|
||||
<span className="flex items-center text-sm text-custom-text-200">
|
||||
{t("common.display")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
||||
+5
-6
@@ -3,11 +3,10 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon, ChevronDownIcon } from "@plane/propel/icons";
|
||||
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
@@ -21,9 +20,9 @@ import { useModule } from "@/hooks/store/use-module";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
const SUPPORTED_LAYOUTS = [
|
||||
{ key: "list", i18n_title: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", i18n_title: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", i18n_title: "issue.layouts.calendar", icon: Calendar },
|
||||
{ key: "list", i18n_title: "issue.layouts.list", icon: ListLayoutIcon },
|
||||
{ key: "kanban", i18n_title: "issue.layouts.kanban", icon: BoardLayoutIcon },
|
||||
{ key: "calendar", i18n_title: "issue.layouts.calendar", icon: CalendarLayoutIcon },
|
||||
];
|
||||
|
||||
export const ModuleIssuesMobileHeader = observer(() => {
|
||||
@@ -108,7 +107,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
||||
menuButton={
|
||||
<span className="flex items-center text-sm text-custom-text-200">
|
||||
Display
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { MODULE_VIEW_LAYOUTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
import { CustomMenu, Row } from "@plane/ui";
|
||||
import { ModuleLayoutIcon } from "@/components/modules";
|
||||
import { useModuleFilter } from "@/hooks/store/use-module-filter";
|
||||
@@ -22,7 +22,7 @@ export const ModulesListMobileHeader = observer(() => {
|
||||
// placement="bottom-start"
|
||||
customButton={
|
||||
<Row className="flex flex-grow justify-center text-custom-text-200 text-sm gap-2">
|
||||
<span>Layout</span> <ChevronDown className="h-4 w-4 text-custom-text-200 my-auto" strokeWidth={1} />
|
||||
<span>Layout</span> <ChevronDownIcon className="h-4 w-4 text-custom-text-200 my-auto" strokeWidth={1} />
|
||||
</Row>
|
||||
}
|
||||
customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm"
|
||||
|
||||
+3
-2
@@ -2,7 +2,8 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { ChevronDown, ListFilter } from "lucide-react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
// components
|
||||
import { Row } from "@plane/ui";
|
||||
import { FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
@@ -42,7 +43,7 @@ export const ViewMobileHeader = observer(() => {
|
||||
menuButton={
|
||||
<Row className="flex items-center text-sm text-custom-text-200">
|
||||
Filters
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
|
||||
</Row>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { ContentWrapper } from "@/components/core/content-wrapper";
|
||||
import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider";
|
||||
import { SettingsHeader } from "@/components/settings/header";
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
|
||||
@@ -10,7 +10,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
|
||||
return (
|
||||
<AuthenticationWrapper>
|
||||
<WorkspaceAuthWrapper>
|
||||
<CommandPalette />
|
||||
<ProjectsAppPowerKProvider />
|
||||
<div className="relative flex h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{/* Header */}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SettingsSidebar } from "@/components/settings/sidebar";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
|
||||
|
||||
const ICONS = {
|
||||
export const WORKSPACE_SETTINGS_ICONS = {
|
||||
general: Building,
|
||||
members: Users,
|
||||
export: ArrowUpToLine,
|
||||
@@ -30,7 +30,7 @@ export const WorkspaceActionIcons = ({
|
||||
className?: string;
|
||||
}) => {
|
||||
if (type === undefined) return null;
|
||||
const Icon = ICONS[type as keyof typeof ICONS];
|
||||
const Icon = WORKSPACE_SETTINGS_ICONS[type as keyof typeof WORKSPACE_SETTINGS_ICONS];
|
||||
if (!Icon) return null;
|
||||
return <Icon size={size} className={className} strokeWidth={2} />;
|
||||
};
|
||||
|
||||
+17
-17
@@ -8,9 +8,9 @@ import useSWR from "swr";
|
||||
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
|
||||
import { SettingsHeading } from "@/components/settings/heading";
|
||||
import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook";
|
||||
@@ -20,7 +20,6 @@ import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useWebhook } from "@/hooks/store/use-webhook";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
const WebhooksListPage = observer(() => {
|
||||
// states
|
||||
@@ -35,7 +34,6 @@ const WebhooksListPage = observer(() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/webhooks" });
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
|
||||
@@ -90,21 +88,23 @@ const WebhooksListPage = observer(() => {
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<DetailedEmptyState
|
||||
className="!p-0"
|
||||
title=""
|
||||
description=""
|
||||
assetPath={resolvedPath}
|
||||
size="md"
|
||||
primaryButton={{
|
||||
text: t("workspace_settings.settings.webhooks.add_webhook"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON,
|
||||
});
|
||||
setShowCreateWebhookModal(true);
|
||||
<EmptyStateCompact
|
||||
assetKey="webhook"
|
||||
title={t("settings_empty_state.webhooks.title")}
|
||||
description={t("settings_empty_state.webhooks.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("settings_empty_state.webhooks.cta_primary"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON,
|
||||
});
|
||||
setShowCreateWebhookModal(true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
]}
|
||||
align="start"
|
||||
rootClassName="py-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+16
-17
@@ -7,18 +7,17 @@ import useSWR from "swr";
|
||||
import { PROFILE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// component
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import { APITokenService } from "@plane/services";
|
||||
import { CreateApiTokenModal } from "@/components/api-token/modal/create-token-modal";
|
||||
import { ApiTokenListItem } from "@/components/api-token/token-list-item";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import { SettingsHeading } from "@/components/settings/heading";
|
||||
import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token";
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// store hooks
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
@@ -30,8 +29,6 @@ const ApiTokensPage = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" });
|
||||
|
||||
const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list());
|
||||
|
||||
@@ -70,7 +67,7 @@ const ApiTokensPage = observer(() => {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex h-full w-full flex-col py-">
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
@@ -84,24 +81,26 @@ const ApiTokensPage = observer(() => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<DetailedEmptyState
|
||||
title=""
|
||||
description=""
|
||||
assetPath={resolvedPath}
|
||||
className="w-full !p-0 justify-center mx-auto"
|
||||
size="md"
|
||||
primaryButton={{
|
||||
text: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
|
||||
<EmptyStateCompact
|
||||
assetKey="token"
|
||||
assetClassName="size-20"
|
||||
title={t("settings_empty_state.tokens.title")}
|
||||
description={t("settings_empty_state.tokens.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("settings_empty_state.tokens.cta_primary"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
},
|
||||
]}
|
||||
align="start"
|
||||
rootClassName="py-20"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Metadata, Viewport } from "next";
|
||||
import { PreloadResources } from "./layout.preload";
|
||||
|
||||
// styles
|
||||
import "@/styles/command-pallette.css";
|
||||
import "@/styles/power-k.css";
|
||||
import "@/styles/emoji.css";
|
||||
import "@plane/propel/styles/react-day-picker";
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
// components
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
// wrappers
|
||||
import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider";
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
// layout
|
||||
import { ProfileLayoutSidebar } from "./sidebar";
|
||||
@@ -17,7 +16,7 @@ export default function ProfileSettingsLayout(props: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandPalette />
|
||||
<ProjectsAppPowerKProvider />
|
||||
<AuthenticationWrapper>
|
||||
<div className="relative flex h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
|
||||
<ProfileLayoutSidebar />
|
||||
|
||||
@@ -5,22 +5,12 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
// icons
|
||||
import {
|
||||
ChevronLeft,
|
||||
LogOut,
|
||||
MoveLeft,
|
||||
Activity,
|
||||
Bell,
|
||||
CircleUser,
|
||||
KeyRound,
|
||||
Settings2,
|
||||
CirclePlus,
|
||||
Mails,
|
||||
} from "lucide-react";
|
||||
import { LogOut, MoveLeft, Activity, Bell, CircleUser, KeyRound, Settings2, CirclePlus, Mails } from "lucide-react";
|
||||
// plane imports
|
||||
import { PROFILE_ACTION_LINKS } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ChevronLeftIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn, getFileURL } from "@plane/utils";
|
||||
@@ -141,7 +131,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
}`}
|
||||
>
|
||||
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
|
||||
<ChevronLeft className="h-5 w-5" strokeWidth={1} />
|
||||
<ChevronLeftIcon className="h-5 w-5" strokeWidth={1} />
|
||||
</span>
|
||||
{!sidebarCollapsed && (
|
||||
<h4 className="truncate text-lg font-semibold text-custom-text-200">{t("profile_settings")}</h4>
|
||||
|
||||
@@ -4,7 +4,8 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { Boxes, Check, Share2, Star, User2, X } from "lucide-react";
|
||||
import { Boxes, Check, Share2, Star, User2 } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space";
|
||||
@@ -85,7 +86,7 @@ const WorkspaceInvitationPage = observer(() => {
|
||||
description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account."
|
||||
>
|
||||
<EmptySpaceItem Icon={Check} title="Accept" action={handleAccept} />
|
||||
<EmptySpaceItem Icon={X} title="Ignore" action={handleReject} />
|
||||
<EmptySpaceItem Icon={CloseIcon} title="Ignore" action={handleReject} />
|
||||
</EmptySpace>
|
||||
)
|
||||
) : error || invitationDetail?.responded_at ? (
|
||||
|
||||
@@ -93,7 +93,7 @@ export const commandGroups: TCommandGroups = {
|
||||
if (!!projectId && page?.project_ids?.includes(projectId)) redirectProjectId = projectId;
|
||||
return redirectProjectId
|
||||
? `/${page?.workspace__slug}/projects/${redirectProjectId}/pages/${page?.id}`
|
||||
: `/${page?.workspace__slug}/pages/${page?.id}`;
|
||||
: `/${page?.workspace__slug}/wiki/${page?.id}`;
|
||||
},
|
||||
title: "Pages",
|
||||
},
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./actions";
|
||||
export * from "./modals";
|
||||
export * from "./helpers";
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./workspace-level";
|
||||
export * from "./project-level";
|
||||
export * from "./issue-level";
|
||||
+18
-15
@@ -15,21 +15,23 @@ import { useUser } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
|
||||
export type TIssueLevelModalsProps = {
|
||||
projectId: string | undefined;
|
||||
issueId: string | undefined;
|
||||
export type TWorkItemLevelModalsProps = {
|
||||
workItemIdentifier: string | undefined;
|
||||
};
|
||||
|
||||
export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) => {
|
||||
const { projectId, issueId } = props;
|
||||
export const WorkItemLevelModals: FC<TWorkItemLevelModalsProps> = observer((props) => {
|
||||
const { workItemIdentifier } = props;
|
||||
// router
|
||||
const { workspaceSlug, cycleId, moduleId } = useParams();
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
issue: { getIssueById, getIssueIdByIdentifier },
|
||||
} = useIssueDetail();
|
||||
// derived values
|
||||
const workItemId = workItemIdentifier ? getIssueIdByIdentifier(workItemIdentifier) : undefined;
|
||||
const workItemDetails = workItemId ? getIssueById(workItemId) : undefined;
|
||||
|
||||
const { removeIssue: removeEpic } = useIssuesActions(EIssuesStoreType.EPIC);
|
||||
const { removeIssue: removeWorkItem } = useIssuesActions(EIssuesStoreType.PROJECT);
|
||||
@@ -44,13 +46,12 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
|
||||
createWorkItemAllowedProjectIds,
|
||||
} = useCommandPalette();
|
||||
// derived values
|
||||
const issueDetails = issueId ? getIssueById(issueId) : undefined;
|
||||
const { fetchSubIssues: fetchSubWorkItems } = useIssueDetail();
|
||||
const { fetchSubIssues: fetchEpicSubWorkItems } = useIssueDetail(EIssueServiceType.EPICS);
|
||||
|
||||
const handleDeleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const isEpic = issueDetails?.is_epic;
|
||||
const isEpic = workItemDetails?.is_epic;
|
||||
const deleteAction = isEpic ? removeEpic : removeWorkItem;
|
||||
const redirectPath = `/${workspaceSlug}/projects/${projectId}/${isEpic ? "epics" : "issues"}`;
|
||||
|
||||
@@ -62,10 +63,10 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
|
||||
};
|
||||
|
||||
const handleCreateIssueSubmit = async (newIssue: TIssue) => {
|
||||
if (!workspaceSlug || !newIssue.project_id || !newIssue.id || newIssue.parent_id !== issueDetails?.id) return;
|
||||
if (!workspaceSlug || !newIssue.project_id || !newIssue.id || newIssue.parent_id !== workItemDetails?.id) return;
|
||||
|
||||
const fetchAction = issueDetails?.is_epic ? fetchEpicSubWorkItems : fetchSubWorkItems;
|
||||
await fetchAction(workspaceSlug?.toString(), newIssue.project_id, issueDetails.id);
|
||||
const fetchAction = workItemDetails?.is_epic ? fetchEpicSubWorkItems : fetchSubWorkItems;
|
||||
await fetchAction(workspaceSlug?.toString(), newIssue.project_id, workItemDetails.id);
|
||||
};
|
||||
|
||||
const getCreateIssueModalData = () => {
|
||||
@@ -83,13 +84,15 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
|
||||
onSubmit={handleCreateIssueSubmit}
|
||||
allowedProjectIds={createWorkItemAllowedProjectIds}
|
||||
/>
|
||||
{workspaceSlug && projectId && issueId && issueDetails && (
|
||||
{workspaceSlug && workItemId && workItemDetails && workItemDetails.project_id && (
|
||||
<DeleteIssueModal
|
||||
handleClose={() => toggleDeleteIssueModal(false)}
|
||||
isOpen={isDeleteIssueModalOpen}
|
||||
data={issueDetails}
|
||||
onSubmit={() => handleDeleteIssue(workspaceSlug.toString(), projectId?.toString(), issueId?.toString())}
|
||||
isEpic={issueDetails?.is_epic}
|
||||
data={workItemDetails}
|
||||
onSubmit={() =>
|
||||
handleDeleteIssue(workspaceSlug.toString(), workItemDetails.project_id!, workItemId?.toString())
|
||||
}
|
||||
isEpic={workItemDetails?.is_epic}
|
||||
/>
|
||||
)}
|
||||
<BulkDeleteIssuesModal
|
||||
@@ -0,0 +1,6 @@
|
||||
// core
|
||||
import type { TPowerKModalPageDetails } from "@/components/power-k/ui/modal/constants";
|
||||
// local imports
|
||||
import type { TPowerKPageTypeExtended } from "./types";
|
||||
|
||||
export const POWER_K_MODAL_PAGE_DETAILS_EXTENDED: Record<TPowerKPageTypeExtended, TPowerKModalPageDetails> = {};
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
|
||||
// local imports
|
||||
import type { TPowerKContextTypeExtended } from "./types";
|
||||
|
||||
export const detectExtendedContextFromURL = (_params: Params): TPowerKContextTypeExtended | null => null;
|
||||
@@ -0,0 +1,8 @@
|
||||
// local imports
|
||||
import type { TPowerKContextTypeExtended } from "../types";
|
||||
|
||||
type TArgs = {
|
||||
activeContext: TPowerKContextTypeExtended | null;
|
||||
};
|
||||
|
||||
export const useExtendedContextIndicator = (_args: TArgs): string | null => null;
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,11 @@
|
||||
// components
|
||||
import type { TPowerKCommandConfig } from "@/components/power-k/core/types";
|
||||
import type { ContextBasedActionsProps, TContextEntityMap } from "@/components/power-k/ui/pages/context-based";
|
||||
// local imports
|
||||
import type { TPowerKContextTypeExtended } from "../../types";
|
||||
|
||||
export const CONTEXT_ENTITY_MAP_EXTENDED: Record<TPowerKContextTypeExtended, TContextEntityMap> = {};
|
||||
|
||||
export const PowerKContextBasedActionsExtended: React.FC<ContextBasedActionsProps> = () => null;
|
||||
|
||||
export const usePowerKContextBasedExtendedActions = (): TPowerKCommandConfig[] => [];
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane types
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import type { IState } from "@plane/types";
|
||||
// components
|
||||
import { PowerKModalCommandItem } from "@/components/power-k/ui/modal/command-item";
|
||||
|
||||
export type TPowerKProjectStatesMenuItemsProps = {
|
||||
handleSelect: (stateId: string) => void;
|
||||
projectId: string | undefined;
|
||||
selectedStateId: string | undefined;
|
||||
states: IState[];
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const PowerKProjectStatesMenuItems: React.FC<TPowerKProjectStatesMenuItemsProps> = observer((props) => {
|
||||
const { handleSelect, selectedStateId, states } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{states.map((state) => (
|
||||
<PowerKModalCommandItem
|
||||
key={state.id}
|
||||
iconNode={<StateGroupIcon stateGroup={state.group} color={state.color} className="shrink-0 size-3.5" />}
|
||||
label={state.name}
|
||||
isSelected={state.id === selectedStateId}
|
||||
onSelect={() => handleSelect(state.id)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Command } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import type { TPowerKContext } from "@/components/power-k/core/types";
|
||||
// plane web imports
|
||||
import { PowerKModalCommandItem } from "@/components/power-k/ui/modal/command-item";
|
||||
|
||||
export type TPowerKModalNoSearchResultsCommandProps = {
|
||||
context: TPowerKContext;
|
||||
searchTerm: string;
|
||||
updateSearchTerm: (value: string) => void;
|
||||
};
|
||||
|
||||
export const PowerKModalNoSearchResultsCommand: React.FC<TPowerKModalNoSearchResultsCommandProps> = (props) => {
|
||||
const { updateSearchTerm } = props;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Command.Group>
|
||||
<PowerKModalCommandItem
|
||||
icon={Search}
|
||||
value="no-results"
|
||||
label={
|
||||
<p className="flex items-center gap-2">
|
||||
{t("power_k.search_menu.no_results")}{" "}
|
||||
<span className="shrink-0 text-sm text-custom-text-300">{t("power_k.search_menu.clear_search")}</span>
|
||||
</p>
|
||||
}
|
||||
onSelect={() => updateSearchTerm("")}
|
||||
/>
|
||||
</Command.Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
|
||||
// components
|
||||
import type { TPowerKSearchResultGroupDetails } from "@/components/power-k/ui/modal/search-results-map";
|
||||
// local imports
|
||||
import type { TPowerKSearchResultsKeysExtended } from "../types";
|
||||
|
||||
type TSearchResultsGroupsMapExtended = Record<TPowerKSearchResultsKeysExtended, TPowerKSearchResultGroupDetails>;
|
||||
|
||||
export const SEARCH_RESULTS_GROUPS_MAP_EXTENDED: TSearchResultsGroupsMapExtended = {};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type TPowerKContextTypeExtended = never;
|
||||
|
||||
export type TPowerKPageTypeExtended = never;
|
||||
|
||||
export type TPowerKSearchResultsKeysExtended = never;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { TIssue } from "@plane/types";
|
||||
|
||||
export type TDateAlertProps = {
|
||||
date: string;
|
||||
workItem: TIssue;
|
||||
projectId: string;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const DateAlert = (props: TDateAlertProps) => <></>;
|
||||
@@ -1,20 +1,26 @@
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
CalendarCheck2,
|
||||
CalendarClock,
|
||||
CalendarDays,
|
||||
LayersIcon,
|
||||
Link2,
|
||||
Paperclip,
|
||||
Signal,
|
||||
Tag,
|
||||
Triangle,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { CalendarDays, LayersIcon, Link2, Paperclip } from "lucide-react";
|
||||
// types
|
||||
import { ISSUE_GROUP_BY_OPTIONS } from "@plane/constants";
|
||||
import type { ISvgIcons } from "@plane/propel/icons";
|
||||
import { CycleIcon, DoubleCircleIcon, ModuleIcon } from "@plane/propel/icons";
|
||||
import type { IGroupByColumn, IIssueDisplayProperties, TGetColumns, TSpreadsheetColumn } from "@plane/types";
|
||||
import {
|
||||
CycleIcon,
|
||||
StatePropertyIcon,
|
||||
ModuleIcon,
|
||||
MembersPropertyIcon,
|
||||
DueDatePropertyIcon,
|
||||
EstimatePropertyIcon,
|
||||
LabelPropertyIcon,
|
||||
PriorityPropertyIcon,
|
||||
StartDatePropertyIcon,
|
||||
} from "@plane/propel/icons";
|
||||
import type {
|
||||
IGroupByColumn,
|
||||
IIssueDisplayProperties,
|
||||
TGetColumns,
|
||||
TIssueGroupByOptions,
|
||||
TSpreadsheetColumn,
|
||||
} from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
SpreadsheetAssigneeColumn,
|
||||
@@ -66,16 +72,16 @@ export const getScopeMemberIds = ({ isWorkspaceLevel, projectId }: TGetColumns):
|
||||
export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined;
|
||||
|
||||
export const SpreadSheetPropertyIconMap: Record<string, FC<ISvgIcons>> = {
|
||||
Users: Users,
|
||||
MembersPropertyIcon: MembersPropertyIcon,
|
||||
CalenderDays: CalendarDays,
|
||||
CalenderCheck2: CalendarCheck2,
|
||||
Triangle: Triangle,
|
||||
Tag: Tag,
|
||||
DueDatePropertyIcon: DueDatePropertyIcon,
|
||||
EstimatePropertyIcon: EstimatePropertyIcon,
|
||||
LabelPropertyIcon: LabelPropertyIcon,
|
||||
ModuleIcon: ModuleIcon,
|
||||
ContrastIcon: CycleIcon,
|
||||
Signal: Signal,
|
||||
CalendarClock: CalendarClock,
|
||||
DoubleCircleIcon: DoubleCircleIcon,
|
||||
PriorityPropertyIcon: PriorityPropertyIcon,
|
||||
StartDatePropertyIcon: StartDatePropertyIcon,
|
||||
StatePropertyIcon: StatePropertyIcon,
|
||||
Link2: Link2,
|
||||
Paperclip: Paperclip,
|
||||
LayersIcon: LayersIcon,
|
||||
@@ -97,3 +103,13 @@ export const SPREADSHEET_COLUMNS: { [key in keyof IIssueDisplayProperties]: TSpr
|
||||
updated_on: SpreadsheetUpdatedOnColumn,
|
||||
attachment_count: SpreadsheetAttachmentColumn,
|
||||
};
|
||||
|
||||
export const useGroupByOptions = (
|
||||
options: TIssueGroupByOptions[]
|
||||
): {
|
||||
key: TIssueGroupByOptions;
|
||||
titleTranslationKey: string;
|
||||
}[] => {
|
||||
const groupByOptions = ISSUE_GROUP_BY_OPTIONS.filter((option) => options.includes(option.key));
|
||||
return groupByOptions;
|
||||
};
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { ChevronRight, CornerDownRight, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react";
|
||||
import { CornerDownRight, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react";
|
||||
// plane editor
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { ChevronRightIcon } from "@plane/propel/icons";
|
||||
// plane ui
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
// components
|
||||
@@ -174,7 +175,7 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
|
||||
<item.icon className="flex-shrink-0 size-3" />
|
||||
{item.label}
|
||||
</span>
|
||||
<ChevronRight
|
||||
<ChevronRightIcon
|
||||
className={cn("flex-shrink-0 size-3 opacity-0 pointer-events-none transition-opacity", {
|
||||
"opacity-100 pointer-events-auto": isActiveTask,
|
||||
})}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ChevronDown, ListFilter } from "lucide-react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
import type { TProjectFilters } from "@plane/types";
|
||||
import { calculateTotalFilters } from "@plane/utils";
|
||||
// components
|
||||
@@ -72,7 +73,7 @@ export const ProjectsListMobileHeader = observer(() => {
|
||||
<div className="flex text-sm items-center gap-2 neutral-primary text-custom-text-200">
|
||||
<ListFilter className="h-3 w-3" />
|
||||
{t("common.filters")}
|
||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||
<ChevronDownIcon className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CircleDot, CopyPlus, XCircle } from "lucide-react";
|
||||
import { RelatedIcon } from "@plane/propel/icons";
|
||||
import { CircleDot, XCircle } from "lucide-react";
|
||||
import { RelatedIcon, DuplicatePropertyIcon } from "@plane/propel/icons";
|
||||
import type { TRelationObject } from "@/components/issues/issue-detail-widgets/relations";
|
||||
import type { TIssueRelationTypes } from "../../types";
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ISSUE_RELATION_OPTIONS: Record<TIssueRelationTypes, TRelationObject
|
||||
key: "duplicate",
|
||||
i18n_label: "issue.relation.duplicate",
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
icon: (size) => <CopyPlus size={size} className="text-custom-text-200" />,
|
||||
icon: (size) => <DuplicatePropertyIcon width={size} height={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
blocked_by: {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
// types
|
||||
import { WORKSPACE_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Collapsible } from "@plane/ui";
|
||||
@@ -42,7 +42,7 @@ export const DeleteWorkspaceSection: FC<TDeleteWorkspace> = observer((props) =>
|
||||
<span className="text-lg tracking-tight">
|
||||
{t("workspace_settings.settings.general.delete_workspace")}
|
||||
</span>
|
||||
{isOpen ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
{isOpen ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// hooks
|
||||
// components
|
||||
import { SidebarSearchButton } from "@/components/sidebar/search-button";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
// hooks
|
||||
import { usePowerK } from "@/hooks/store/use-power-k";
|
||||
|
||||
export const AppSearch = observer(() => {
|
||||
// store hooks
|
||||
const { toggleCommandPaletteModal } = useCommandPalette();
|
||||
const { togglePowerKModal } = usePowerK();
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCommandPaletteModal(true)}
|
||||
onClick={() => togglePowerKModal(true)}
|
||||
aria-label={t("aria_labels.projects_sidebar.open_command_palette")}
|
||||
>
|
||||
<SidebarSearchButton isActive={false} />
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import {
|
||||
AtSign,
|
||||
Briefcase,
|
||||
Calendar,
|
||||
CalendarCheck2,
|
||||
CalendarClock,
|
||||
CircleUserRound,
|
||||
SignalHigh,
|
||||
Tag,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { AtSign, Briefcase, Calendar } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
CycleGroupIcon,
|
||||
CycleIcon,
|
||||
ModuleIcon,
|
||||
DoubleCircleIcon,
|
||||
StatePropertyIcon,
|
||||
PriorityIcon,
|
||||
StateGroupIcon,
|
||||
MembersPropertyIcon,
|
||||
LabelPropertyIcon,
|
||||
StartDatePropertyIcon,
|
||||
DueDatePropertyIcon,
|
||||
UserCirclePropertyIcon,
|
||||
PriorityPropertyIcon,
|
||||
} from "@plane/propel/icons";
|
||||
import type {
|
||||
ICycle,
|
||||
@@ -149,7 +145,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getStateGroupFilterConfig<TWorkItemFilterProperty>("state_group")({
|
||||
isEnabled: isFilterEnabled("state_group"),
|
||||
filterIcon: DoubleCircleIcon,
|
||||
filterIcon: StatePropertyIcon,
|
||||
getOptionIcon: (stateGroupKey) => <StateGroupIcon stateGroup={stateGroupKey} />,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
@@ -161,7 +157,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getStateFilterConfig<TWorkItemFilterProperty>("state_id")({
|
||||
isEnabled: isFilterEnabled("state_id") && workItemStates !== undefined,
|
||||
filterIcon: DoubleCircleIcon,
|
||||
filterIcon: StatePropertyIcon,
|
||||
getOptionIcon: (state) => <StateGroupIcon stateGroup={state.group} color={state.color} />,
|
||||
states: workItemStates ?? [],
|
||||
...operatorConfigs,
|
||||
@@ -174,7 +170,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getLabelFilterConfig<TWorkItemFilterProperty>("label_id")({
|
||||
isEnabled: isFilterEnabled("label_id") && workItemLabels !== undefined,
|
||||
filterIcon: Tag,
|
||||
filterIcon: LabelPropertyIcon,
|
||||
labels: workItemLabels ?? [],
|
||||
getOptionIcon: (color) => (
|
||||
<span className="flex flex-shrink-0 size-2.5 rounded-full" style={{ backgroundColor: color }} />
|
||||
@@ -215,7 +211,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getAssigneeFilterConfig<TWorkItemFilterProperty>("assignee_id")({
|
||||
isEnabled: isFilterEnabled("assignee_id") && members !== undefined,
|
||||
filterIcon: Users,
|
||||
filterIcon: MembersPropertyIcon,
|
||||
members: members ?? [],
|
||||
getOptionIcon: (memberDetails) => (
|
||||
<Avatar
|
||||
@@ -255,7 +251,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getCreatedByFilterConfig<TWorkItemFilterProperty>("created_by_id")({
|
||||
isEnabled: isFilterEnabled("created_by_id") && members !== undefined,
|
||||
filterIcon: CircleUserRound,
|
||||
filterIcon: UserCirclePropertyIcon,
|
||||
members: members ?? [],
|
||||
getOptionIcon: (memberDetails) => (
|
||||
<Avatar
|
||||
@@ -275,7 +271,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getSubscriberFilterConfig<TWorkItemFilterProperty>("subscriber_id")({
|
||||
isEnabled: isFilterEnabled("subscriber_id") && members !== undefined,
|
||||
filterIcon: Users,
|
||||
filterIcon: MembersPropertyIcon,
|
||||
members: members ?? [],
|
||||
getOptionIcon: (memberDetails) => (
|
||||
<Avatar
|
||||
@@ -295,7 +291,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getPriorityFilterConfig<TWorkItemFilterProperty>("priority")({
|
||||
isEnabled: isFilterEnabled("priority"),
|
||||
filterIcon: SignalHigh,
|
||||
filterIcon: PriorityPropertyIcon,
|
||||
getOptionIcon: (priority) => <PriorityIcon priority={priority} />,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
@@ -307,7 +303,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getStartDateFilterConfig<TWorkItemFilterProperty>("start_date")({
|
||||
isEnabled: true,
|
||||
filterIcon: CalendarClock,
|
||||
filterIcon: StartDatePropertyIcon,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[operatorConfigs]
|
||||
@@ -318,7 +314,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps):
|
||||
() =>
|
||||
getTargetDateFilterConfig<TWorkItemFilterProperty>("target_date")({
|
||||
isEnabled: true,
|
||||
filterIcon: CalendarCheck2,
|
||||
filterIcon: DueDatePropertyIcon,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[operatorConfigs]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { IIssueDisplayFilterOptions } from "@plane/types";
|
||||
|
||||
export const getEnabledDisplayFilters = (displayFilters: IIssueDisplayFilterOptions) => displayFilters;
|
||||
@@ -112,10 +112,11 @@ export class IssueActivityStore implements IIssueActivityStore {
|
||||
comments.forEach((commentId) => {
|
||||
const comment = currentStore.comment.getCommentById(commentId);
|
||||
if (!comment) return;
|
||||
const commentTimestamp = comment.edited_at ?? comment.updated_at ?? comment.created_at;
|
||||
activityComments.push({
|
||||
id: comment.id,
|
||||
activity_type: EActivityFilterType.COMMENT,
|
||||
created_at: comment.created_at,
|
||||
created_at: commentTimestamp,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { TPage, TPageExtended } from "@plane/types";
|
||||
import type { RootStore } from "@/plane-web/store/root.store";
|
||||
import type { TBasePageServices } from "@/store/pages/base-page";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export type TExtendedBasePageServices = {};
|
||||
|
||||
export type TExtendedPageInstance = TPageExtended & {
|
||||
asJSONExtended: TPageExtended;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { makeObservable } from "mobx";
|
||||
// types
|
||||
import type { IBasePowerKStore } from "@/store/base-power-k.store";
|
||||
import { BasePowerKStore } from "@/store/base-power-k.store";
|
||||
|
||||
export type IPowerKStore = IBasePowerKStore;
|
||||
|
||||
export class PowerKStore extends BasePowerKStore implements IPowerKStore {
|
||||
constructor() {
|
||||
super();
|
||||
makeObservable(this, {});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { FC } from "react";
|
||||
import { Info, X } from "lucide-react";
|
||||
// plane imports
|
||||
import { Info } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// plane imports
|
||||
// helpers
|
||||
import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||
|
||||
@@ -32,7 +33,7 @@ export const AuthBanner: FC<TAuthBanner> = (props) => {
|
||||
onClick={() => handleBannerData?.(undefined)}
|
||||
aria-label={t("aria_labels.auth_forms.close_alert")}
|
||||
>
|
||||
<X className="size-4" />
|
||||
<CloseIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { X } from "lucide-react";
|
||||
import { Popover } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
|
||||
export const ForgotPasswordPopover = () => {
|
||||
// popper-js refs
|
||||
@@ -51,7 +51,7 @@ export const ForgotPasswordPopover = () => {
|
||||
onClick={() => close()}
|
||||
aria-label={t("aria_labels.auth_forms.close_popover")}
|
||||
>
|
||||
<X className="size-3 text-custom-text-200" />
|
||||
<CloseIcon className="size-3 text-custom-text-200" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,11 +4,12 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// icons
|
||||
import { Eye, EyeOff, Info, X, XCircle } from "lucide-react";
|
||||
import { Eye, EyeOff, Info, XCircle } from "lucide-react";
|
||||
// plane imports
|
||||
import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_EVENTS, AUTH_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||
import { getPasswordStrength } from "@plane/utils";
|
||||
// components
|
||||
@@ -134,7 +135,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-red-500/20 text-custom-primary-100/80"
|
||||
onClick={() => setBannerMessage(false)}
|
||||
>
|
||||
<X className="w-4 h-4 flex-shrink-0 text-red-500" />
|
||||
<CloseIcon className="w-4 h-4 flex-shrink-0 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,18 +14,17 @@ import {
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Search, X } from "lucide-react";
|
||||
// plane package imports
|
||||
import { Search } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// plane package imports
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table";
|
||||
import { cn } from "@plane/utils";
|
||||
// plane web components
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import AnalyticsEmptyState from "../empty-state";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
@@ -42,7 +41,6 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
||||
const { t } = useTranslation();
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-table" });
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
@@ -119,7 +117,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -156,14 +154,12 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="p-0">
|
||||
<div className="flex h-[350px] w-full items-center justify-center border border-custom-border-100 ">
|
||||
<AnalyticsEmptyState
|
||||
title={t("workspace_analytics.empty_state.customized_insights.title")}
|
||||
description={t("workspace_analytics.empty_state.customized_insights.description")}
|
||||
className="border-0"
|
||||
assetPath={resolvedPath}
|
||||
/>
|
||||
</div>
|
||||
<EmptyStateCompact
|
||||
assetKey="unknown"
|
||||
assetClassName="size-20"
|
||||
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
|
||||
title={t("workspace_empty_state.analytics_work_items.title")}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@@ -4,15 +4,14 @@ import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane package imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import type { TChartData } from "@plane/types";
|
||||
// hooks
|
||||
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||
// services
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { AnalyticsService } from "@/services/analytics.service";
|
||||
// plane web components
|
||||
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
||||
import AnalyticsEmptyState from "../empty-state";
|
||||
import { ProjectInsightsLoader } from "../loaders";
|
||||
|
||||
const RadarChart = dynamic(() =>
|
||||
@@ -29,7 +28,6 @@ const ProjectInsights = observer(() => {
|
||||
const workspaceSlug = params.workspaceSlug.toString();
|
||||
const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } =
|
||||
useAnalytics();
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-radar" });
|
||||
|
||||
const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(
|
||||
`radar-chart-project-insights-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
|
||||
@@ -56,11 +54,11 @@ const ProjectInsights = observer(() => {
|
||||
{isLoadingProjectInsight ? (
|
||||
<ProjectInsightsLoader />
|
||||
) : projectInsightsData && projectInsightsData?.length == 0 ? (
|
||||
<AnalyticsEmptyState
|
||||
title={t("workspace_analytics.empty_state.project_insights.title")}
|
||||
description={t("workspace_analytics.empty_state.project_insights.description")}
|
||||
className="h-[300px]"
|
||||
assetPath={resolvedPath}
|
||||
<EmptyStateCompact
|
||||
assetKey="unknown"
|
||||
assetClassName="size-20"
|
||||
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
|
||||
title={t("workspace_empty_state.analytics_work_items.title")}
|
||||
/>
|
||||
) : (
|
||||
<div className="gap-8 lg:flex">
|
||||
|
||||
@@ -5,16 +5,15 @@ import useSWR from "swr";
|
||||
// plane package imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { AreaChart } from "@plane/propel/charts/area-chart";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import type { IChartResponse, TChartData } from "@plane/types";
|
||||
import { renderFormattedDate } from "@plane/utils";
|
||||
// hooks
|
||||
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||
// services
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { AnalyticsService } from "@/services/analytics.service";
|
||||
// plane web components
|
||||
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
||||
import AnalyticsEmptyState from "../empty-state";
|
||||
import { ChartLoader } from "../loaders";
|
||||
|
||||
const analyticsService = new AnalyticsService();
|
||||
@@ -31,7 +30,6 @@ const CreatedVsResolved = observer(() => {
|
||||
const params = useParams();
|
||||
const { t } = useTranslation();
|
||||
const workspaceSlug = params.workspaceSlug.toString();
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-area" });
|
||||
const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR(
|
||||
`created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}-${isEpic}`,
|
||||
() =>
|
||||
@@ -121,11 +119,11 @@ const CreatedVsResolved = observer(() => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<AnalyticsEmptyState
|
||||
title={t("workspace_analytics.empty_state.created_vs_resolved.title")}
|
||||
description={t("workspace_analytics.empty_state.created_vs_resolved.description")}
|
||||
className="h-[350px]"
|
||||
assetPath={resolvedPath}
|
||||
<EmptyStateCompact
|
||||
assetKey="unknown"
|
||||
assetClassName="size-20"
|
||||
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
|
||||
title={t("workspace_empty_state.analytics_work_items.title")}
|
||||
/>
|
||||
)}
|
||||
</AnalyticsSectionWrapper>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
// plane package imports
|
||||
import { Expand, Shrink, X } from "lucide-react";
|
||||
import { Expand, Shrink } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
import type { ICycle, IModule } from "@plane/types";
|
||||
// icons
|
||||
|
||||
@@ -34,7 +35,7 @@ export const WorkItemsModalHeader: React.FC<Props> = observer((props) => {
|
||||
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X size={14} strokeWidth={2} />
|
||||
<CloseIcon height={14} width={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user