feat: language support (#6472)
* chore: ln support modules constants * fix: translation key * chore: empty state refactor (#6404) * chore: asset path helper hook added * chore: detailed and simple empty state component added * chore: section empty state component added * chore: language translation for all empty states * chore: new empty state implementation * improvement: add more translations * improvement: user permissions and workspace draft empty state * chore: update translation structure * chore: inbox empty states * chore: disabled project features empty state * chore: active cycle progress empty state * chore: notification empty state * chore: connections translation * chore: issue comment, relation, bulk delete, and command k empty state translation * chore: project pages empty state and translations * chore: project module and view related empty state * chore: remove project draft related empty state * chore: project cycle, views and archived issues empty state * chore: project cycles related empty state * chore: project settings empty state * chore: profile issue and acitivity empty state * chore: workspace settings realted constants * chore: stickies and home widgets empty state * chore: remove all reference to deprecated empty state component and constnats * chore: add support to ignore theme in resolved asset path hook * chore: minor updates * fix: build errors --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * fix: language support fo profile (#6461) * fix: ln support fo profile * fix: merge changes * fix: merge changes * [WEB-3165]feat: language support for issues (#6452) * * chore: moved issue constants to packages * chore: restructured issue constants * improvement: added translations to issue constants * chore: updated translation structure * * chore: updated chinese, spanish and french translation * chore: updated translation for issues mobile header * chore: updated spanish translation * chore: removed translation for issue priorities * fix: build errors * chore: minor updates --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: migrated filters.ts to packages (#6459) Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: workspace drafts constant moved to plane constant package * feat: home language support without stickies (#6443) * feat: home language support without stickies * fix: home sidebar * fix: added missing keys * fix: show all btn * fix: recents empty state * chore: translation update * feat: workspace constant language support and refactor (#6462) * chore: workspace constant language support and refactor * chore: workspace constant language support and refactor * chore: code refactor * chore: code refactor * merge conflict * chore: code refactor --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: tab indices constant moved to plane package (#6464) * chore: notification language support and refactor * chore: ln support for inbox constants (#6432) * chore: ln support for inbox constants * fix: snooze duration * fix: enum * fix: translation keys * fix: inbox status icon * fix: status icon * fix: naming --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * fix: ln support for views constants (#6431) * fix: ln support for views constants * fix: added translation * fix: translation keys * fix: access * chore: code refactor * chore: ln support workspace projects constants (#6429) * chore: ln support workspace projects constants * fix: translation key * fix: removed state translation * fix: removed state translation * fi: added translations * Chore: theme language support and refactor (#6465) * chore: themes language support and refactor * chore: theme language support and refactor * fix * [WEB-3173] chore: language support for cycles constant file (#6415) * chore: ln support for cycles constant file * fix: added chinese * fix: lint * fix: translation key * fix: build errors * minor updates * chore: minor translation update * chore: minor translation update * refactor: move labels contants to packages * refactor: move swr, file and error related constants to packages * chore: timezones constant moved to plane package * chore: metadata constant code refactor * chore: code refactor * fix: dashboard constants moved * chore: code refactor (#6478) * refactor: spreadsheet constants * chore: drafts language support (#6485) * chore: workspace drafts language support * chore: code refactor * feat: ln support for notifications (#6486) * feat: ln support for notifications * fix: translations * * refactor: moved page constants to packages (#6480) * fix: removed use-client * chore: removed unnecessary commnets * chore: workspace draft language support (#6490) * chore: workspace drafts language support * chore: code refactor * chore: draft language support * Feat constant event tracker (#6479) * fix: event tracjer constants * fix: constants event tracker * feat: language translation - projects list (#6493) * feat: added translation to projects list page * chore: restructured translation file * chore: module language support (#6499) * chore: module language support added * chore: code refactor * chore: workspace views language support (#6492) * chore: workspace views language support * chore: code refactor * feat: custom analytics language support (#6494) * feat: custom analytics language support * fix: key * fix: refactoring --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: minor improvements * feat: language support for intake (#6498) * feat: language support for intake * fix: key name * refactor: authentications related translations * feat: language support issues (#6501) * enhancement: added translations for issue list view * chore: added translations for issue detail widgets * chore: added missing translations * chore: modified issue to work items * chore: updated translations * Feat: workspace settings language support (#6508) * feat: language support for workspace settings * fix: lint * fix: export title * chore project settings language support (#6502) * chore: project settings language support * chore: code refactor * refactor: workspace creation related translations * chore: renamed issues to work items * fix: build errors * fix: lint * chore: modified translations * chore: remove duplicate * improvement: french translation * chore: chinese translation improvement * fix: japanese translations * chore: added spanish translation * minor improvements * fix: miscelleous language translations * fix: clear_all key * fix: moved user permission constants (#6516) * feat: language support for issues (#6513) * chore: added language support to issue detail widgets * improvement: added translation for issue detail * enhancement: added language trasnlation to issue layouts * chore: translation improvement (#6518) * feat: language support description (#6519) * enhancement: added language support for description * fix: updated keys * chore: renamed issue to work item (#6522) * chore: replace missing issue occurances to work items * fix: build errors * minor improvements * fix: profile links * Feat ln cycles (#6528) * feat: added language support for cycles * feat: added language support for cycles * chore: added core.json * fix: translation keys * fix: translation keys (#6530) * fix: changed sidebar keys * fix: removed extras * fix: updated keys * chore: optimize translation imports * fix: updated keys (#6534) * fix: updated keys * fix-sub work items toasts * chore: add missing translation and minor fixes * chore: code refactor * fix: language support keys (#6553) * minor improvements * minor fixes * fix: remove lucide import from constants package * chore: regenerate all translations * chore: addded chinese and japanese translation files * chore: remove all from translations * fix: added member * fix: language support keys (#6558) * fix: renamed keys * fix: space app * chore: renamed issues to work items * chore: update site manifest * chore: updated translations * fix: lang keys * chore: update translations --------- Co-authored-by: gakshita <akshitagoyal1516@gmail.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Co-authored-by: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com> Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so> Co-authored-by: Vamsi krishna <matalav55@gmail.com> Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com>
This commit is contained in:
parent
e244f48776
commit
d36c3acbf7
693 changed files with 18182 additions and 10485 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspaceMemberInvitation } from "@plane/types";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
|
|
@ -21,38 +23,40 @@ type TAuthHeader = {
|
|||
const Titles = {
|
||||
[EAuthModes.SIGN_IN]: {
|
||||
[EAuthSteps.EMAIL]: {
|
||||
header: "Log in or sign up",
|
||||
header: "auth.sign_in.header.step.email.header",
|
||||
subHeader: "",
|
||||
},
|
||||
[EAuthSteps.PASSWORD]: {
|
||||
header: "Log in or sign up",
|
||||
subHeader: "Use your email-password combination to log in.",
|
||||
header: "auth.sign_in.header.step.password.header",
|
||||
subHeader: "auth.sign_in.header.step.password.sub_header",
|
||||
},
|
||||
[EAuthSteps.UNIQUE_CODE]: {
|
||||
header: "Log in or Sign up",
|
||||
subHeader: "Log in using a unique code sent to the email address above.",
|
||||
header: "auth.sign_in.header.step.unique_code.header",
|
||||
subHeader: "auth.sign_in.header.step.unique_code.sub_header",
|
||||
},
|
||||
},
|
||||
[EAuthModes.SIGN_UP]: {
|
||||
[EAuthSteps.EMAIL]: {
|
||||
header: "Sign up",
|
||||
header: "auth.sign_up.header.step.email.header",
|
||||
subHeader: "",
|
||||
},
|
||||
[EAuthSteps.PASSWORD]: {
|
||||
header: "Sign up",
|
||||
subHeader: "Sign up using an email-password combination.",
|
||||
header: "auth.sign_up.header.step.password.header",
|
||||
subHeader: "auth.sign_up.header.step.password.sub_header",
|
||||
},
|
||||
[EAuthSteps.UNIQUE_CODE]: {
|
||||
header: "Sign up",
|
||||
subHeader: "Sign up using a unique code sent to the email address above.",
|
||||
header: "auth.sign_up.header.step.unique_code.header",
|
||||
subHeader: "auth.sign_up.header.step.unique_code.sub_header",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const workSpaceService = new WorkspaceService();
|
||||
|
||||
export const AuthHeader: FC<TAuthHeader> = (props) => {
|
||||
export const AuthHeader: FC<TAuthHeader> = observer((props) => {
|
||||
const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep, children } = props;
|
||||
// plane imports
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: invitation, isLoading } = useSWR(
|
||||
workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
|
||||
|
|
@ -74,13 +78,12 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
|
|||
return {
|
||||
header: (
|
||||
<div className="relative inline-flex items-center gap-2">
|
||||
Join <WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9 flex-shrink-0" />{" "}
|
||||
{t("common.join")}{" "}
|
||||
<WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9 flex-shrink-0" />{" "}
|
||||
{workspace.name}
|
||||
</div>
|
||||
),
|
||||
subHeader: `${
|
||||
mode == EAuthModes.SIGN_UP ? "Create an account" : "Sign in"
|
||||
} to start managing work with your team.`,
|
||||
subHeader: mode == EAuthModes.SIGN_UP ? "auth.sign_up.header.label" : "auth.sign_in.header.label",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -99,10 +102,12 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
|
|||
return (
|
||||
<>
|
||||
<div className="space-y-1 text-center">
|
||||
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
|
||||
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
|
||||
<h3 className="text-3xl font-bold text-onboarding-text-100">
|
||||
{typeof header === "string" ? t(header) : header}
|
||||
</h3>
|
||||
<p className="font-medium text-onboarding-text-400">{t(subHeader)}</p>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
|
|
@ -51,6 +52,8 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
|
|||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||
const [isExistingEmail, setIsExistingEmail] = useState(false);
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { config } = useInstance();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { FC, FormEvent, useMemo, useRef, useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { CircleAlert, XCircle } from "lucide-react";
|
||||
// types
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
|
@ -22,9 +22,10 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
|||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [email, setEmail] = useState(defaultEmail);
|
||||
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
const emailError = useMemo(
|
||||
() => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined),
|
||||
() => (email && !checkEmailValidity(email) ? { email: "auth.common.email.errors.invalid" } : undefined),
|
||||
[email]
|
||||
);
|
||||
|
||||
|
|
@ -40,14 +41,14 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
|||
|
||||
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
|
||||
|
||||
const [isFocused, setIsFocused] = useState(true)
|
||||
const [isFocused, setIsFocused] = useState(true);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
Email
|
||||
{t("auth.common.email.label")}
|
||||
</label>
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -55,8 +56,12 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
|||
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100`
|
||||
)}
|
||||
tabIndex={-1}
|
||||
onFocus={() => {setIsFocused(true)}}
|
||||
onBlur={() => {setIsFocused(false)}}
|
||||
onFocus={() => {
|
||||
setIsFocused(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false);
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
id="email"
|
||||
|
|
@ -64,13 +69,13 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
|||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
placeholder={t("auth.common.email.placeholder")}
|
||||
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
/>
|
||||
{email.length > 0 && (
|
||||
{email.length > 0 && (
|
||||
<XCircle
|
||||
className="h-[46px] w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs"
|
||||
onClick={() => {
|
||||
|
|
@ -83,13 +88,13 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
|||
{emailError?.email && !isFocused && (
|
||||
<p className="flex items-center gap-1 text-xs text-red-600 px-0.5">
|
||||
<CircleAlert height={12} width={12} />
|
||||
{emailError.email}
|
||||
{t(emailError.email)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : t("common.continue")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ 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";
|
||||
|
||||
export const ForgotPasswordPopover = () => {
|
||||
// popper-js refs
|
||||
|
|
@ -19,6 +21,8 @@ export const ForgotPasswordPopover = () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
|
|
@ -28,7 +32,7 @@ export const ForgotPasswordPopover = () => {
|
|||
ref={setReferenceElement}
|
||||
className="text-xs font-medium text-custom-primary-100 outline-none"
|
||||
>
|
||||
Forgot your password?
|
||||
{t("auth.common.forgot_password")}
|
||||
</button>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="fixed z-10">
|
||||
|
|
@ -40,9 +44,7 @@ export const ForgotPasswordPopover = () => {
|
|||
{...attributes.popper}
|
||||
>
|
||||
<span className="flex-shrink-0">🤥</span>
|
||||
<p className="text-xs">
|
||||
We see that your god hasn{"'"}t enabled SMTP, we will not be able to send a password reset link
|
||||
</p>
|
||||
<p className="text-xs">{t("auth.forgot_password.errors.smtp_not_enabled")}</p>
|
||||
<button type="button" className="flex-shrink-0" onClick={() => close()}>
|
||||
<X className="h-3 w-3 text-onboarding-text-200" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -5,17 +5,13 @@ import { observer } from "mobx-react";
|
|||
import Link from "next/link";
|
||||
// icons
|
||||
import { Eye, EyeOff, Info, X, XCircle } from "lucide-react";
|
||||
// ui
|
||||
// plane imports
|
||||
import { FORGOT_PASSWORD, SIGN_IN_WITH_CODE, SIGN_IN_WITH_PASSWORD, SIGN_UP_WITH_PASSWORD } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account";
|
||||
// constants
|
||||
import {
|
||||
FORGOT_PASSWORD,
|
||||
SIGN_IN_WITH_CODE,
|
||||
SIGN_IN_WITH_PASSWORD,
|
||||
SIGN_UP_WITH_PASSWORD,
|
||||
} from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
|
|
@ -49,6 +45,8 @@ const authService = new AuthService();
|
|||
|
||||
export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
const { email, isSMTPConfigured, handleAuthStep, handleEmailClear, mode, nextPath } = props;
|
||||
// plane imports
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
// ref
|
||||
|
|
@ -92,7 +90,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
href={`/accounts/forgot-password?email=${encodeURIComponent(email)}`}
|
||||
className="text-xs font-medium text-custom-primary-100"
|
||||
>
|
||||
Forgot your password?
|
||||
{t("auth.common.forgot_password")}
|
||||
</Link>
|
||||
) : (
|
||||
<ForgotPasswordPopover />
|
||||
|
|
@ -134,7 +132,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
|
||||
<Info size={16} className="text-red-500" />
|
||||
</div>
|
||||
<div className="w-full text-sm font-medium text-red-500">Try setting-up a strong password to proceed</div>
|
||||
<div className="w-full text-sm font-medium text-red-500">{t("auth.sign_up.errors.password.strength")}</div>
|
||||
<div
|
||||
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)}
|
||||
|
|
@ -158,7 +156,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
if (isPasswordValid) {
|
||||
setIsSubmitting(true);
|
||||
captureEvent(mode === EAuthModes.SIGN_IN ? SIGN_IN_WITH_PASSWORD : SIGN_UP_WITH_PASSWORD);
|
||||
formRef.current && formRef.current.submit(); // Manually submit the form if the condition is met
|
||||
if (formRef.current) formRef.current.submit(); // Manually submit the form if the condition is met
|
||||
} else {
|
||||
setBannerMessage(true);
|
||||
}
|
||||
|
|
@ -170,7 +168,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
{nextPath && <input type="hidden" value={nextPath} name="next_path" />}
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
|
||||
Email
|
||||
{t("auth.common.email.label")}
|
||||
</label>
|
||||
<div
|
||||
className={`relative flex items-center rounded-md bg-onboarding-background-200 border border-onboarding-border-100`}
|
||||
|
|
@ -181,7 +179,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
type="email"
|
||||
value={passwordFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
placeholder={t("auth.common.email.placeholder")}
|
||||
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`}
|
||||
disabled
|
||||
/>
|
||||
|
|
@ -196,7 +194,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
|
||||
{mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"}
|
||||
{mode === EAuthModes.SIGN_IN ? t("auth.common.password.label") : t("auth.common.password.set_password")}
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
|
|
@ -204,7 +202,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
name="password"
|
||||
value={passwordFormData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
placeholder="Enter password"
|
||||
placeholder={t("auth.common.password.placeholder")}
|
||||
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
onFocus={() => setIsPasswordInputFocused(true)}
|
||||
onBlur={() => setIsPasswordInputFocused(false)}
|
||||
|
|
@ -229,7 +227,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
{mode === EAuthModes.SIGN_UP && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
||||
Confirm password
|
||||
{t("auth.common.password.confirm_password.label")}
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
|
|
@ -237,7 +235,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
name="confirm_password"
|
||||
value={passwordFormData.confirm_password}
|
||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||
placeholder="Confirm password"
|
||||
placeholder={t("auth.common.password.confirm_password.placeholder")}
|
||||
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
onFocus={() => setIsRetryPasswordInputFocused(true)}
|
||||
onBlur={() => setIsRetryPasswordInputFocused(false)}
|
||||
|
|
@ -256,7 +254,9 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
</div>
|
||||
{!!passwordFormData.confirm_password &&
|
||||
passwordFormData.password !== passwordFormData.confirm_password &&
|
||||
renderPasswordMatchError && <span className="text-sm text-red-500">Passwords don{"'"}t match</span>}
|
||||
renderPasswordMatchError && (
|
||||
<span className="text-sm text-red-500">{t("auth.common.password.errors.match")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -267,9 +267,9 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
{isSubmitting ? (
|
||||
<Spinner height="20px" width="20px" />
|
||||
) : isSMTPConfigured ? (
|
||||
"Continue"
|
||||
t("common.continue")
|
||||
) : (
|
||||
"Go to workspace"
|
||||
t("common.go_to_workspace")
|
||||
)}
|
||||
</Button>
|
||||
{isSMTPConfigured && (
|
||||
|
|
@ -280,7 +280,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
Sign in with unique code
|
||||
{t("auth.common.sign_in_with_unique_code")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CircleCheck, XCircle } from "lucide-react";
|
||||
import { CODE_VERIFIED } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// constants
|
||||
import { CODE_VERIFIED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { EAuthModes } from "@/helpers/authentication.helper";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
|
|
@ -49,6 +50,8 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
|
|||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// timer
|
||||
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0);
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFormChange = (key: keyof TUniqueCodeFormValues, value: string) =>
|
||||
setUniqueCodeFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
|
@ -94,7 +97,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
|
|||
{nextPath && <input type="hidden" value={nextPath} name="next_path" />}
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
|
||||
Email
|
||||
{t("auth.common.email.label")}
|
||||
</label>
|
||||
<div
|
||||
className={`relative flex items-center rounded-md bg-onboarding-background-200 border border-onboarding-border-100`}
|
||||
|
|
@ -105,7 +108,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
|
|||
type="email"
|
||||
value={uniqueCodeFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
placeholder={t("auth.common.email.placeholder")}
|
||||
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`}
|
||||
autoComplete="on"
|
||||
disabled
|
||||
|
|
@ -121,20 +124,20 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
|
|||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="code">
|
||||
Unique code
|
||||
{t("auth.common.unique_code.label")}
|
||||
</label>
|
||||
<Input
|
||||
name="code"
|
||||
value={uniqueCodeFormData.code}
|
||||
onChange={(e) => handleFormChange("code", e.target.value)}
|
||||
placeholder="gets-sets-flys"
|
||||
placeholder={t("auth.common.unique_code.placeholder")}
|
||||
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between px-1 text-xs pt-1">
|
||||
<p className="flex items-center gap-1 font-medium text-green-700">
|
||||
<CircleCheck height={12} width={12} />
|
||||
Paste the code sent to your email
|
||||
{t("auth.common.unique_code.paste_code")}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -147,17 +150,17 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
|
|||
disabled={isRequestNewCodeDisabled}
|
||||
>
|
||||
{resendTimerCode > 0
|
||||
? `Resend in ${resendTimerCode}s`
|
||||
? t("auth.common.resend_in", { seconds: resendTimerCode })
|
||||
: isRequestingNewCode
|
||||
? "Requesting new code"
|
||||
: "Resend"}
|
||||
? t("auth.common.unique_code.requesting_new_code")
|
||||
: t("common.resend")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isRequestingNewCode ? "Sending code" : isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||
{isRequestingNewCode ? t("auth.common.unique_code.sending_code") : isSubmitting ? <Spinner height="20px" width="20px" /> : t("common.continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -26,35 +26,35 @@ export const PasswordStrengthMeter: FC<TPasswordStrengthMeter> = (props) => {
|
|||
case E_PASSWORD_STRENGTH.EMPTY: {
|
||||
return {
|
||||
bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
|
||||
text: t("please_enter_your_password"),
|
||||
text: t("auth.common.password.errors.empty"),
|
||||
textColor: "text-custom-text-100",
|
||||
};
|
||||
}
|
||||
case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID: {
|
||||
return {
|
||||
bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`],
|
||||
text: t("password_length_should_me_more_than_8_characters"),
|
||||
text: t("auth.common.password.errors.length"),
|
||||
textColor: "text-red-500",
|
||||
};
|
||||
}
|
||||
case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID: {
|
||||
return {
|
||||
bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`],
|
||||
text: t("password_is_weak"),
|
||||
text: t("auth.common.password.errors.strength.weak"),
|
||||
textColor: "text-red-500",
|
||||
};
|
||||
}
|
||||
case E_PASSWORD_STRENGTH.STRENGTH_VALID: {
|
||||
return {
|
||||
bars: [`bg-green-500`, `bg-green-500`, `bg-green-500`],
|
||||
text: t("password_is_strong"),
|
||||
text: t("auth.common.password.errors.strength.strong"),
|
||||
textColor: "text-green-500",
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
|
||||
text: t("please_enter_your_password"),
|
||||
text: t("auth.common.password.errors.empty"),
|
||||
textColor: "text-custom-text-100",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
|
|||
} else tooltipValue = datum.id;
|
||||
} else {
|
||||
if (ANALYTICS_DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue;
|
||||
else tooltipValue = datum.id === "count" ? "Issue count" : "Estimate";
|
||||
else tooltipValue = datum.id === "count" ? "Work item count" : "Estimate";
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const AnalyticsGraph: React.FC<Props> = ({ analytics, barGraphData, param
|
|||
let data: number[] = [];
|
||||
|
||||
if (params.segment)
|
||||
// find the total no of issues in each segment
|
||||
// find the total no of work items in each segment
|
||||
data = Object.keys(analytics.distribution).map((segment) => {
|
||||
let totalSegmentIssues = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export const CustomAnalyticsMainContent: React.FC<Props> = (props) => {
|
|||
) : (
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
<div className="space-y-4 text-custom-text-200">
|
||||
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
|
||||
<p className="text-sm">No matching work items found. Try changing the parameters.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
|||
// icons
|
||||
import { Contrast, LayoutGrid, Users, Loader as Spinner } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { Logo } from "@/components/common";
|
||||
|
|
@ -20,11 +21,12 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
|
|||
const { projectIds, isLoading, isUpdating } = props;
|
||||
// store hooks
|
||||
const { getProjectById, getProjectAnalyticsCountById } = useProject();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-4 h-full">
|
||||
<div className="flex gap-2 items-center">
|
||||
<h4 className="font-medium">Selected Projects</h4>
|
||||
<h4 className="font-medium">{t("workspace_analytics.selected_projects")}</h4>
|
||||
{isUpdating && <Spinner className="animate-spin size-3" />}
|
||||
</div>
|
||||
<div className="relative space-y-6 overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-md">
|
||||
|
|
@ -57,21 +59,21 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
|
|||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||
<h6>Total members</h6>
|
||||
<h6>{t("workspace_analytics.total_members")}</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{projectAnalyticsCount?.total_members}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||
<h6>Total cycles</h6>
|
||||
<h6>{t("workspace_analytics.total_cycles")}</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{projectAnalyticsCount?.total_cycles}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||
<h6>Total modules</h6>
|
||||
<h6>{t("workspace_analytics.total_modules")}</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{projectAnalyticsCount?.total_modules}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { NETWORK_CHOICES } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Logo } from "@/components/common";
|
||||
// constants
|
||||
import { NETWORK_CHOICES } from "@/constants/project";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
|
|
@ -16,6 +17,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||
const { getCycleById } = useCycle();
|
||||
const { getModuleById } = useModule();
|
||||
const { getUserDetails } = useMember();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
|
|
@ -91,7 +93,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||
<div className="mt-4 space-y-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Network</h6>
|
||||
<span>{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ?? ""}</span>
|
||||
<span>{t(NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.i18n_label ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import useSWR, { mutate } from "swr";
|
|||
// icons
|
||||
import { CalendarDays, Download, RefreshCw } from "lucide-react";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
|
@ -42,6 +43,7 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||
const { data: currentUser } = useUser();
|
||||
const { workspaceProjectIds, getProjectById, fetchProjectAnalyticsCount } = useProject();
|
||||
const { getWorkspaceById } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
const { fetchCycleDetails, getCycleById } = useCycle();
|
||||
const { fetchModuleDetails, getModuleById } = useModule();
|
||||
// fetch project analytics count
|
||||
|
|
@ -160,7 +162,7 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||
<LayersIcon height={14} width={14} />
|
||||
{analytics ? analytics.total : "..."}
|
||||
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
|
||||
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>{t("work_items")}</div>
|
||||
</div>
|
||||
{isProjectLevel && (
|
||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||
|
|
@ -199,10 +201,10 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||
}}
|
||||
>
|
||||
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Refresh</div>
|
||||
<div className={cn(isProjectLevel ? "hidden md:block" : "", "capitalize")}>{t("refresh")}</div>
|
||||
</Button>
|
||||
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
|
||||
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Export as CSV</div>
|
||||
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>{t("exporter.csv.short_description")}</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react";
|
|||
import { Tab } from "@headlessui/react";
|
||||
// plane package imports
|
||||
import { ANALYTICS_TABS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, IModule, IProject } from "@plane/types";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
|
||||
|
|
@ -16,7 +17,7 @@ type Props = {
|
|||
|
||||
export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props) => {
|
||||
const { fullScreen, cycleDetails, moduleDetails } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Tab.Group as={React.Fragment}>
|
||||
<Tab.List as="div" className="flex space-x-2 border-b h-[50px] border-custom-border-200 px-0 md:px-3">
|
||||
|
|
@ -28,7 +29,7 @@ export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props
|
|||
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{tab.title}
|
||||
{t(tab.i18n_title)}
|
||||
<div
|
||||
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,52 +1,58 @@
|
|||
// plane imports
|
||||
import { STATE_GROUPS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types";
|
||||
// constants
|
||||
import { Card } from "@plane/ui";
|
||||
import { STATE_GROUPS } from "@/constants/state";
|
||||
|
||||
type Props = {
|
||||
defaultAnalytics: IDefaultAnalyticsResponse;
|
||||
};
|
||||
|
||||
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<Card>
|
||||
<div>
|
||||
<h4 className="text-base font-medium text-custom-text-100">Total open tasks</h4>
|
||||
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
|
||||
</div>
|
||||
<div className="space-y-6 pb-2">
|
||||
{defaultAnalytics?.open_issues_classified.map((group) => {
|
||||
const percentage = ((group.state_count / defaultAnalytics.total_issues) * 100).toFixed(0);
|
||||
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div key={group.state_group} className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
return (
|
||||
<Card>
|
||||
<div>
|
||||
<h4 className="text-base font-medium text-custom-text-100">{t("workspace_analytics.open_tasks")}</h4>
|
||||
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
|
||||
</div>
|
||||
<div className="space-y-6 pb-2">
|
||||
{defaultAnalytics?.open_issues_classified.map((group) => {
|
||||
const percentage = ((group.state_count / defaultAnalytics.total_issues) * 100).toFixed(0);
|
||||
|
||||
return (
|
||||
<div key={group.state_group} className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: STATE_GROUPS[group.state_group as TStateGroups].color,
|
||||
}}
|
||||
/>
|
||||
<h6 className="capitalize">{group.state_group}</h6>
|
||||
<span className="ml-1 rounded-3xl bg-custom-background-80 px-2 py-0.5 text-[0.65rem] text-custom-text-200">
|
||||
{group.state_count}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-custom-text-200">{percentage}%</p>
|
||||
</div>
|
||||
<div className="bar relative h-1 w-full rounded bg-custom-background-80">
|
||||
<div
|
||||
className="absolute left-0 top-0 h-1 rounded duration-300"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: STATE_GROUPS[group.state_group as TStateGroups].color,
|
||||
}}
|
||||
/>
|
||||
<h6 className="capitalize">{group.state_group}</h6>
|
||||
<span className="ml-1 rounded-3xl bg-custom-background-80 px-2 py-0.5 text-[0.65rem] text-custom-text-200">
|
||||
{group.state_count}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-custom-text-200">{percentage}%</p>
|
||||
</div>
|
||||
<div className="bar relative h-1 w-full rounded bg-custom-background-80">
|
||||
<div
|
||||
className="absolute left-0 top-0 h-1 rounded duration-300"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: STATE_GROUPS[group.state_group as TStateGroups].color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// plane ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Card } from "@plane/ui";
|
||||
// components
|
||||
import { ProfileEmptyState } from "@/components/ui";
|
||||
|
|
@ -21,45 +22,48 @@ type Props = {
|
|||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const AnalyticsLeaderBoard: React.FC<Props> = ({ users, title, emptyStateMessage, workspaceSlug }) => (
|
||||
<Card>
|
||||
<h6 className="text-base font-medium">{title}</h6>
|
||||
{users.length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
{users.map((user) => (
|
||||
<a
|
||||
key={user?.display_name ?? "None"}
|
||||
href={`/${workspaceSlug}/profile/${user.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start justify-between gap-4 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{user.avatar_url && user.avatar_url !== "" ? (
|
||||
<div className="relative h-4 w-4 flex-shrink-0 rounded-full">
|
||||
<img
|
||||
src={getFileURL(user.avatar_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
||||
alt={user?.display_name ?? "None"}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 text-[11px] capitalize text-white">
|
||||
{user?.display_name !== "" ? user?.display_name?.[0] : "?"}
|
||||
</div>
|
||||
)}
|
||||
<span className="break-words text-custom-text-200">
|
||||
{user?.display_name !== "" ? `${user?.display_name}` : "No assignee"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-shrink-0">{user.count}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState title="No Data yet" description={emptyStateMessage} image={emptyUsers} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
export const AnalyticsLeaderBoard: React.FC<Props> = ({ users, title, emptyStateMessage, workspaceSlug }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<h6 className="text-base font-medium">{title}</h6>
|
||||
{users.length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
{users.map((user) => (
|
||||
<a
|
||||
key={user?.display_name ?? "None"}
|
||||
href={`/${workspaceSlug}/profile/${user.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start justify-between gap-4 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{user.avatar_url && user.avatar_url !== "" ? (
|
||||
<div className="relative h-4 w-4 flex-shrink-0 rounded-full">
|
||||
<img
|
||||
src={getFileURL(user.avatar_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
||||
alt={user?.display_name ?? "None"}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 text-[11px] capitalize text-white">
|
||||
{user?.display_name !== "" ? user?.display_name?.[0] : "?"}
|
||||
</div>
|
||||
)}
|
||||
<span className="break-words text-custom-text-200">
|
||||
{user?.display_name !== "" ? `${user?.display_name}` : "No assignee"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-shrink-0">{user.count}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState title={t("no_data_yet")} description={emptyStateMessage} image={emptyUsers} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button, ContentWrapper, Loader } from "@plane/ui";
|
||||
// components
|
||||
import { AnalyticsDemand, AnalyticsLeaderBoard, AnalyticsScope, AnalyticsYearWiseIssues } from "@/components/analytics";
|
||||
|
|
@ -21,6 +22,7 @@ export const ScopeAndDemand: React.FC<Props> = (props) => {
|
|||
const { fullScreen = true } = props;
|
||||
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isProjectLevel = projectId ? true : false;
|
||||
|
||||
|
|
@ -66,8 +68,8 @@ export const ScopeAndDemand: React.FC<Props> = (props) => {
|
|||
count: user?.count,
|
||||
id: user?.created_by__id,
|
||||
}))}
|
||||
title="Most issues created"
|
||||
emptyStateMessage="Co-workers and the number of issues created by them appears here."
|
||||
title={t("workspace_analytics.most_work_items_created.title")}
|
||||
emptyStateMessage={t("workspace_analytics.most_work_items_created.empty_state")}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
<AnalyticsLeaderBoard
|
||||
|
|
@ -79,8 +81,8 @@ export const ScopeAndDemand: React.FC<Props> = (props) => {
|
|||
count: user?.count,
|
||||
id: user?.assignees__id,
|
||||
}))}
|
||||
title="Most issues closed"
|
||||
emptyStateMessage="Co-workers and the number of issues closed by them appears here."
|
||||
title={t("workspace_analytics.most_work_items_closed.title")}
|
||||
emptyStateMessage={t("workspace_analytics.most_work_items_closed.empty_state")}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
<div className={fullScreen ? "md:col-span-2" : ""}>
|
||||
|
|
@ -99,10 +101,10 @@ export const ScopeAndDemand: React.FC<Props> = (props) => {
|
|||
) : (
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
<div className="space-y-4 text-custom-text-200">
|
||||
<p className="text-sm">There was some error in fetching the data.</p>
|
||||
<p className="text-sm">{t("workspace_analytics.error")}</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button variant="primary" onClick={() => mutateDefaultAnalytics()}>
|
||||
Refresh
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// plane types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IDefaultAnalyticsUser } from "@plane/types";
|
||||
// plane ui
|
||||
import { Card } from "@plane/ui";
|
||||
|
|
@ -14,82 +15,85 @@ type Props = {
|
|||
pendingAssignedIssues: IDefaultAnalyticsUser[];
|
||||
};
|
||||
|
||||
export const AnalyticsScope: React.FC<Props> = ({ pendingUnAssignedIssuesUser, pendingAssignedIssues }) => (
|
||||
<Card>
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h6 className="text-base font-medium">Pending issues</h6>
|
||||
{pendingUnAssignedIssuesUser && (
|
||||
<div className="relative flex items-center py-1 px-3 rounded-md gap-2 text-xs text-custom-primary-100 bg-custom-primary-100/10">
|
||||
Unassigned: {pendingUnAssignedIssuesUser.count}
|
||||
export const AnalyticsScope: React.FC<Props> = ({ pendingUnAssignedIssuesUser, pendingAssignedIssues }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h6 className="text-base font-medium">{t("workspace_analytics.pending_work_items.title")}</h6>
|
||||
{pendingUnAssignedIssuesUser && (
|
||||
<div className="relative flex items-center py-1 px-3 rounded-md gap-2 text-xs text-custom-primary-100 bg-custom-primary-100/10">
|
||||
{t("unassigned")}: {pendingUnAssignedIssuesUser.count}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pendingAssignedIssues && pendingAssignedIssues.length > 0 ? (
|
||||
<BarGraph
|
||||
data={pendingAssignedIssues}
|
||||
indexBy="assignees__id"
|
||||
keys={["count"]}
|
||||
height="250px"
|
||||
colors={() => `#f97316`}
|
||||
customYAxisTickValues={pendingAssignedIssues.map((d) => (d.count > 0 ? d.count : 50))}
|
||||
tooltip={(datum) => {
|
||||
const assignee = pendingAssignedIssues.find((a) => a.assignees__id === `${datum.indexValue}`);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
<span className="font-medium text-custom-text-200">
|
||||
{assignee ? assignee.assignees__display_name : "No assignee"}:{" "}
|
||||
</span>
|
||||
{datum.value}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
axisBottom={{
|
||||
renderTick: (datum) => {
|
||||
const assignee = pendingAssignedIssues[datum.tickIndex] ?? "";
|
||||
|
||||
if (assignee && assignee?.assignees__avatar_url && assignee?.assignees__avatar_url !== "")
|
||||
return (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<image
|
||||
x={-8}
|
||||
y={10}
|
||||
width={16}
|
||||
height={16}
|
||||
xlinkHref={getFileURL(assignee?.assignees__avatar_url)}
|
||||
style={{ clipPath: "circle(50%)" }}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<circle cy={18} r={8} fill="#374151" />
|
||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||
{datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
},
|
||||
}}
|
||||
margin={{ top: 20 }}
|
||||
theme={{
|
||||
axis: {},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState
|
||||
title={t("no_data_yet")}
|
||||
description={t("workspace_analytics.pending_work_items.empty_state")}
|
||||
image={emptyBarGraph}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pendingAssignedIssues && pendingAssignedIssues.length > 0 ? (
|
||||
<BarGraph
|
||||
data={pendingAssignedIssues}
|
||||
indexBy="assignees__id"
|
||||
keys={["count"]}
|
||||
height="250px"
|
||||
colors={() => `#f97316`}
|
||||
customYAxisTickValues={pendingAssignedIssues.map((d) => (d.count > 0 ? d.count : 50))}
|
||||
tooltip={(datum) => {
|
||||
const assignee = pendingAssignedIssues.find((a) => a.assignees__id === `${datum.indexValue}`);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
<span className="font-medium text-custom-text-200">
|
||||
{assignee ? assignee.assignees__display_name : "No assignee"}:{" "}
|
||||
</span>
|
||||
{datum.value}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
axisBottom={{
|
||||
renderTick: (datum) => {
|
||||
const assignee = pendingAssignedIssues[datum.tickIndex] ?? "";
|
||||
|
||||
if (assignee && assignee?.assignees__avatar_url && assignee?.assignees__avatar_url !== "")
|
||||
return (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<image
|
||||
x={-8}
|
||||
y={10}
|
||||
width={16}
|
||||
height={16}
|
||||
xlinkHref={getFileURL(assignee?.assignees__avatar_url)}
|
||||
style={{ clipPath: "circle(50%)" }}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<circle cy={18} r={8} fill="#374151" />
|
||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||
{datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
},
|
||||
}}
|
||||
margin={{ top: 20 }}
|
||||
theme={{
|
||||
axis: {},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState
|
||||
title="No Data yet"
|
||||
description="Analysis of pending issues by co-workers appears here."
|
||||
image={emptyBarGraph}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IDefaultAnalyticsResponse } from "@plane/types";
|
||||
import { Card } from "@plane/ui";
|
||||
import { LineGraph, ProfileEmptyState } from "@/components/ui";
|
||||
|
|
@ -12,49 +13,52 @@ type Props = {
|
|||
defaultAnalytics: IDefaultAnalyticsResponse;
|
||||
};
|
||||
|
||||
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<Card>
|
||||
<h1 className="py-3 text-base font-medium">Issues closed in a year</h1>
|
||||
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
|
||||
<LineGraph
|
||||
data={[
|
||||
{
|
||||
id: "issues_closed",
|
||||
color: "rgb(var(--color-primary-100))",
|
||||
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
|
||||
x: month.shortTitle,
|
||||
y:
|
||||
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))?.count ||
|
||||
0,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
|
||||
height="300px"
|
||||
colors={(datum) => datum.color}
|
||||
curve="monotoneX"
|
||||
margin={{ top: 20 }}
|
||||
enableSlices="x"
|
||||
sliceTooltip={(datum) => (
|
||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
{datum.slice.points[0].data.yFormatted}
|
||||
<span className="text-custom-text-200"> issues closed in </span>
|
||||
{datum.slice.points[0].data.xFormatted}
|
||||
</div>
|
||||
)}
|
||||
theme={{
|
||||
background: "rgb(var(--color-background-100))",
|
||||
}}
|
||||
enableArea
|
||||
/>
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState
|
||||
title="No Data yet"
|
||||
description="Close issues to view analysis of the same in the form of a graph."
|
||||
image={emptyGraph}
|
||||
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<h1 className="py-3 text-base font-medium">{t("workspace_analytics.work_items_closed_in_a_year.title")}</h1>
|
||||
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
|
||||
<LineGraph
|
||||
data={[
|
||||
{
|
||||
id: "issues_closed",
|
||||
color: "rgb(var(--color-primary-100))",
|
||||
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
|
||||
x: t(month.shortTitle),
|
||||
y:
|
||||
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))
|
||||
?.count || 0,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
|
||||
height="300px"
|
||||
colors={(datum) => datum.color}
|
||||
curve="monotoneX"
|
||||
margin={{ top: 20 }}
|
||||
enableSlices="x"
|
||||
sliceTooltip={(datum) => (
|
||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
{datum.slice.points[0].data.yFormatted}
|
||||
<span className="text-custom-text-200"> {t("workspace_analytics.work_items_closed_in")} </span>
|
||||
{datum.slice.points[0].data.xFormatted}
|
||||
</div>
|
||||
)}
|
||||
theme={{
|
||||
background: "rgb(var(--color-background-100))",
|
||||
}}
|
||||
enableArea
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState
|
||||
title={t("no_data_yet")}
|
||||
description={t("workspace_analytics.work_items_closed_in_a_year.empty_state")}
|
||||
image={emptyGraph}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState, FC } from "react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { mutate } from "swr";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IApiToken } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
|
@ -26,6 +27,7 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
|
|||
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
|
|
@ -42,8 +44,8 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
|
|||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Token deleted successfully.",
|
||||
title: t("workspace_settings.settings.api_tokens.delete.success.title"),
|
||||
message: t("workspace_settings.settings.api_tokens.delete.success.message"),
|
||||
});
|
||||
|
||||
mutate<IApiToken[]>(
|
||||
|
|
@ -57,8 +59,8 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
|
|||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: err?.message ?? "Something went wrong. Please try again.",
|
||||
title: t("workspace_settings.settings.api_tokens.delete.error.title"),
|
||||
message: err?.message ?? t("workspace_settings.settings.api_tokens.delete.error.message"),
|
||||
})
|
||||
)
|
||||
.finally(() => setDeleteLoading(false));
|
||||
|
|
@ -70,12 +72,8 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
|
|||
handleSubmit={handleDeletion}
|
||||
isSubmitting={deleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete API token"
|
||||
content={
|
||||
<>
|
||||
Any application using this token will no longer have the access to Plane data. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
title={t("workspace_settings.settings.api_tokens.delete.title")}
|
||||
content={<>{t("workspace_settings.settings.api_tokens.delete.description")} </>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { add } from "date-fns";
|
|||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Calendar } from "lucide-react";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IApiToken } from "@plane/types";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
|
@ -76,6 +77,8 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
|||
reset,
|
||||
watch,
|
||||
} = useForm<IApiToken>({ defaultValues });
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFormSubmit = async (data: IApiToken) => {
|
||||
// if never expires is toggled off, and the user has not selected a custom date or a predefined date, show an error
|
||||
|
|
@ -115,19 +118,21 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
|||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Create token</h3>
|
||||
<h3 className="text-xl font-medium text-custom-text-200">
|
||||
{t("workspace_settings.settings.api_tokens.create_token")}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="label"
|
||||
rules={{
|
||||
required: "Title is required",
|
||||
required: t("title_is_required"),
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
message: t("title_should_be_less_than_255_characters"),
|
||||
},
|
||||
validate: (val) => val.trim() !== "" || "Title is required",
|
||||
validate: (val) => val.trim() !== "" || t("title_is_required"),
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
|
|
@ -135,7 +140,7 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.label)}
|
||||
placeholder="Title"
|
||||
placeholder={t("title")}
|
||||
className="w-full text-base"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -150,7 +155,7 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.description)}
|
||||
placeholder="Description"
|
||||
placeholder={t("description")}
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -229,14 +234,16 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
|||
<div className="flex cursor-pointer items-center justify-center">
|
||||
<ToggleSwitch value={neverExpires} onChange={() => {}} size="sm" />
|
||||
</div>
|
||||
<span className="text-xs">Never expires</span>
|
||||
<span className="text-xs">{t("workspace_settings.settings.api_tokens.never_expires")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Generating" : "Generate token"}
|
||||
{isSubmitting
|
||||
? t("workspace_settings.settings.api_tokens.generating")
|
||||
: t("workspace_settings.settings.api_tokens.generate_token")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { Copy } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IApiToken } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
|
@ -19,12 +20,13 @@ type Props = {
|
|||
export const GeneratedTokenDetails: React.FC<Props> = (props) => {
|
||||
const { handleClose, tokenDetails } = props;
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
const copyApiToken = (token: string) => {
|
||||
copyTextToClipboard(token).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Token copied to clipboard.",
|
||||
title: `${t("success")}!`,
|
||||
message: t("workspace_setting.token_copied"),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
@ -32,11 +34,8 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
|
|||
return (
|
||||
<div className="w-full p-5">
|
||||
<div className="w-full space-y-3 text-wrap">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3>
|
||||
<p className="text-sm text-custom-text-400">
|
||||
Copy and save this secret key in Plane Pages. You can{"'"}t see this key after you hit Close. A CSV file
|
||||
containing the key has been downloaded.
|
||||
</p>
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{t("workspace_settings.key_created")}</h3>
|
||||
<p className="text-sm text-custom-text-400">{t("workspace_settings.copy_key")}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -53,7 +52,7 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
|
|||
{tokenDetails.expired_at ? `Expires ${renderFormattedDate(tokenDetails.expired_at)}` : "Never expires"}
|
||||
</p>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Close
|
||||
{t("close")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const ARCHIVES_TAB_LIST: {
|
|||
}[] = [
|
||||
{
|
||||
key: "issues",
|
||||
label: "Issues",
|
||||
label: "Work items",
|
||||
shouldRender: () => true,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { ArchiveRestore } from "lucide-react";
|
||||
// types
|
||||
import { PROJECT_AUTOMATION_MONTHS,EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui";
|
||||
// component
|
||||
import { SelectMonthModal } from "@/components/automation";
|
||||
// constants
|
||||
import { PROJECT_AUTOMATION_MONTHS } from "@/constants/project";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||
|
|
@ -30,6 +30,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||
const [monthModal, setmonthModal] = useState(false);
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentProjectDetails } = useProject();
|
||||
|
||||
|
|
@ -56,9 +57,9 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||
<ArchiveRestore className="h-4 w-4 flex-shrink-0 text-custom-text-100" />
|
||||
</div>
|
||||
<div className="">
|
||||
<h4 className="text-sm font-medium">Auto-archive closed issues</h4>
|
||||
<h4 className="text-sm font-medium">{t("project_settings.automations.auto-archive.title")}</h4>
|
||||
<p className="text-sm tracking-tight text-custom-text-200">
|
||||
Plane will auto archive issues that have been completed or canceled.
|
||||
{t("project_settings.automations.auto-archive.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -78,7 +79,9 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||
currentProjectDetails.archive_in !== 0 && (
|
||||
<div className="mx-6">
|
||||
<div className="flex w-full items-center justify-between gap-2 rounded border border-custom-border-200 bg-custom-background-90 px-5 py-4">
|
||||
<div className="w-1/2 text-sm font-medium">Auto-archive issues that are closed for</div>
|
||||
<div className="w-1/2 text-sm font-medium">
|
||||
{t("project_settings.automations.auto-archive.duration")}
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<CustomSelect
|
||||
value={currentProjectDetails?.archive_in}
|
||||
|
|
@ -93,8 +96,8 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||
>
|
||||
<>
|
||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||
<CustomSelect.Option key={month.label} value={month.value}>
|
||||
<span className="text-sm">{month.label}</span>
|
||||
<CustomSelect.Option key={month.i18n_label} value={month.value}>
|
||||
<span className="text-sm">{t(month.i18n_label, { month: month.value })}</span>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
|
||||
|
|
@ -103,7 +106,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-sm text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setmonthModal(true)}
|
||||
>
|
||||
Customize time range
|
||||
{t("customize_time_range")}
|
||||
</button>
|
||||
</>
|
||||
</CustomSelect>
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ import { useParams } from "next/navigation";
|
|||
// icons
|
||||
import { ArchiveX } from "lucide-react";
|
||||
// types
|
||||
import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui";
|
||||
// component
|
||||
import { SelectMonthModal } from "@/components/automation";
|
||||
// constants
|
||||
import { PROJECT_AUTOMATION_MONTHS } from "@/constants/project";
|
||||
// hooks
|
||||
import { useProject, useProjectState, useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||
|
|
@ -31,6 +31,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
const { currentProjectDetails } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// const stateGroups = projectStateStore.groupedProjectStates ?? undefined;
|
||||
|
||||
|
|
@ -82,9 +83,9 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
<ArchiveX className="h-4 w-4 flex-shrink-0 text-red-500" />
|
||||
</div>
|
||||
<div className="">
|
||||
<h4 className="text-sm font-medium">Auto-close issues</h4>
|
||||
<h4 className="text-sm font-medium">{t("project_settings.automations.auto-close.title")}</h4>
|
||||
<p className="text-sm tracking-tight text-custom-text-200">
|
||||
Plane will automatically close issues that haven{"'"}t been completed or canceled.
|
||||
{t("project_settings.automations.auto-close.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -105,7 +106,9 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
<div className="mx-6">
|
||||
<div className="flex flex-col rounded border border-custom-border-200 bg-custom-background-90">
|
||||
<div className="flex w-full items-center justify-between gap-2 px-5 py-4">
|
||||
<div className="w-1/2 text-sm font-medium">Auto-close issues that are inactive for</div>
|
||||
<div className="w-1/2 text-sm font-medium">
|
||||
{t("project_settings.automations.auto-close.duration")}
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<CustomSelect
|
||||
value={currentProjectDetails?.close_in}
|
||||
|
|
@ -120,8 +123,8 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
>
|
||||
<>
|
||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||
<CustomSelect.Option key={month.label} value={month.value}>
|
||||
{month.label}
|
||||
<CustomSelect.Option key={month.i18n_label} value={month.value}>
|
||||
{t(month.i18n_label, { month: month.value })}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
<button
|
||||
|
|
@ -129,7 +132,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setmonthModal(true)}
|
||||
>
|
||||
Customize time range
|
||||
{t("customize_time_range")}
|
||||
</button>
|
||||
</>
|
||||
</CustomSelect>
|
||||
|
|
@ -137,7 +140,9 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-between gap-2 px-5 py-4">
|
||||
<div className="w-1/2 text-sm font-medium">Auto-close status</div>
|
||||
<div className="w-1/2 text-sm font-medium">
|
||||
{t("project_settings.automations.auto-close.auto_close_status")}
|
||||
</div>
|
||||
<div className="w-1/2 ">
|
||||
<CustomSearchSelect
|
||||
value={currentProjectDetails?.default_state ?? defaultState}
|
||||
|
|
@ -162,7 +167,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
)}
|
||||
{selectedOption?.name
|
||||
? selectedOption.name
|
||||
: (currentDefaultState?.name ?? <span className="text-custom-text-200">State</span>)}
|
||||
: (currentDefaultState?.name ?? <span className="text-custom-text-200">{t("state")}</span>)}
|
||||
</div>
|
||||
}
|
||||
onChange={(val: string) => {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Command.Group heading="Issue actions">
|
||||
<Command.Group heading="Work item actions">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Change state...");
|
||||
|
|
@ -143,7 +143,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
|||
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
Delete issue
|
||||
Delete work item
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
|
|
@ -155,7 +155,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
|||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
Copy issue URL
|
||||
Copy work item URL
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { Check } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { EIssuesStoreType, ISSUE_PRIORITIES } from "@plane/constants";
|
||||
// plane types
|
||||
import { TIssue, TIssuePriorities } from "@plane/types";
|
||||
// mobx store
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
import { ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
import { useIssues } from "@/hooks/store";
|
||||
// ui
|
||||
// types
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import { Command } from "cmdk";
|
|||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Settings } from "lucide-react";
|
||||
// plane imports
|
||||
import { THEME_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { THEME_OPTIONS } from "@/constants/themes";
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
|||
const { setTheme } = useTheme();
|
||||
// hooks
|
||||
const { updateUserTheme } = useUserProfile();
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
|
|
@ -53,7 +55,7 @@ export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
|||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Settings className="h-4 w-4 text-custom-text-200" />
|
||||
{theme.label}
|
||||
{t(theme.i18n_label)}
|
||||
</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import { Command } from "cmdk";
|
|||
// hooks
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
import { WORKSPACE_SETTINGS_LINKS, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { SettingIcon } from "@/components/icons";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane wev constants
|
||||
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
|
||||
// plane web helpers
|
||||
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
|
|||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// mobx store
|
||||
const { t } = useTranslation();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
|
||||
|
|
@ -46,8 +48,8 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
|
|||
>
|
||||
<Link href={`/${workspaceSlug}${setting.href}`}>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<setting.Icon className="h-4 w-4 text-custom-text-200" />
|
||||
{setting.label}
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
{t(setting.i18n_label)}
|
||||
</div>
|
||||
</Link>
|
||||
</Command.Item>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import { useParams } from "next/navigation";
|
|||
import useSWR from "swr";
|
||||
import { FolderPlus, Search, Settings } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspaceSearchResults } from "@plane/types";
|
||||
// ui
|
||||
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
|
|
@ -23,9 +24,7 @@ import {
|
|||
CommandPaletteThemeActions,
|
||||
CommandPaletteWorkspaceSettingsActions,
|
||||
} from "@/components/command-palette";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
|
|
@ -36,21 +35,20 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
|||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { IssueService } from "@/services/issue";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const CommandModal: React.FC = observer(() => {
|
||||
// hooks
|
||||
const { workspaceProjectIds } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { canPerformAnyCreateAction } = useUser();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
// states
|
||||
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
||||
const [resultsCount, setResultsCount] = useState(0);
|
||||
|
|
@ -70,26 +68,25 @@ export const CommandModal: React.FC = observer(() => {
|
|||
});
|
||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||
const [pages, setPages] = useState<string[]>([]);
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { workspaceProjectIds } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { canPerformAnyCreateAction } = useUser();
|
||||
const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } =
|
||||
useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// router params
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
|
||||
// derived values
|
||||
const page = pages[pages.length - 1];
|
||||
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||
|
||||
const { baseTabIndex } = getTabIndex(undefined, isMobile);
|
||||
|
||||
const canPerformWorkspaceActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
|
||||
|
||||
// TODO: update this to mobx store
|
||||
const { data: issueDetails } = useSWR(
|
||||
|
|
@ -268,7 +265,7 @@ export const CommandModal: React.FC = observer(() => {
|
|||
|
||||
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<EmptyState type={EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE} layout="screen-simple" />
|
||||
<SimpleEmptyState title={t("command_k.empty_state.search.title")} assetPath={resolvedPath} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -304,7 +301,7 @@ export const CommandModal: React.FC = observer(() => {
|
|||
workspaceProjectIds &&
|
||||
workspaceProjectIds.length > 0 &&
|
||||
canPerformAnyCreateAction && (
|
||||
<Command.Group heading="Issue">
|
||||
<Command.Group heading="Work item">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
|
|
@ -315,7 +312,7 @@ export const CommandModal: React.FC = observer(() => {
|
|||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<LayersIcon className="h-3.5 w-3.5" />
|
||||
Create new issue
|
||||
Create new work item
|
||||
</div>
|
||||
<kbd>C</kbd>
|
||||
</Command.Item>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { useCallback, useEffect, FC, useMemo } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
|
||||
|
|
@ -19,7 +20,6 @@ import {
|
|||
WorkspaceLevelModals,
|
||||
} from "@/plane-web/components/command-palette/modals";
|
||||
// plane web constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
// plane web helpers
|
||||
import {
|
||||
getGlobalShortcutsList,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const commandGroups: {
|
|||
),
|
||||
path: (issue: IWorkspaceIssueSearchResult) =>
|
||||
`/${issue?.workspace__slug}/projects/${issue?.project_id}/issues/${issue?.id}`,
|
||||
title: "Issues",
|
||||
title: "Work items",
|
||||
},
|
||||
issue_view: {
|
||||
icon: <Layers className="h-3 w-3" />,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { LucideIcon } from "lucide-react";
|
||||
// plane ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
|
@ -9,38 +10,44 @@ type Props = {
|
|||
value: number;
|
||||
accessSpecifiers: {
|
||||
key: number;
|
||||
label: string;
|
||||
i18n_label?: string;
|
||||
label?: string;
|
||||
icon: LucideIcon;
|
||||
}[];
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
// TODO: Remove label once i18n is done
|
||||
export const AccessField = (props: Props) => {
|
||||
const { onChange, value, accessSpecifiers, isMobile = false } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[1px] border-custom-border-200 p-1">
|
||||
{accessSpecifiers.map((access, index) => (
|
||||
<Tooltip key={access.key} tooltipContent={access.label} isMobile={isMobile}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(access.key)}
|
||||
className={cn(
|
||||
"flex-shrink-0 relative flex justify-center items-center w-5 h-5 rounded-sm p-1 transition-all",
|
||||
value === access.key ? "bg-custom-background-80" : "hover:bg-custom-background-80"
|
||||
)}
|
||||
tabIndex={2 + index}
|
||||
>
|
||||
<access.icon
|
||||
{accessSpecifiers.map((access, index) => {
|
||||
const label = access.i18n_label ? t(access.i18n_label) : access.label;
|
||||
return (
|
||||
<Tooltip key={access.key} tooltipContent={label} isMobile={isMobile}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(access.key)}
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 transition-all",
|
||||
value === access.key ? "text-custom-text-100" : "text-custom-text-400"
|
||||
"flex-shrink-0 relative flex justify-center items-center w-5 h-5 rounded-sm p-1 transition-all",
|
||||
value === access.key ? "bg-custom-background-80" : "hover:bg-custom-background-80"
|
||||
)}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
tabIndex={2 + index}
|
||||
>
|
||||
<access.icon
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 transition-all",
|
||||
value === access.key ? "text-custom-text-100" : "text-custom-text-400"
|
||||
)}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ export const messages = (activity: TProjectActivity): { message: string | ReactN
|
|||
};
|
||||
case "is_issue_type_enabled":
|
||||
return {
|
||||
message: <>{getBooleanActionText(newValue)} issue types</>,
|
||||
message: <>{getBooleanActionText(newValue)} work item types</>,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
// constants
|
||||
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
|
||||
// plane constants
|
||||
import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "@/helpers/string.helper";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
// plane constants
|
||||
import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants";
|
||||
import { DateFilterModal } from "@/components/core";
|
||||
import { FilterHeader, FilterOption } from "@/components/issues";
|
||||
// constants
|
||||
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
|
||||
// helpers
|
||||
import { isInDateFormat } from "@/helpers/date-time.helper";
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const LatestFeatureBlock = () => {
|
|||
<div className="h-[90%]">
|
||||
<Image
|
||||
src={latestFeatures}
|
||||
alt="Plane Issues"
|
||||
alt="Plane Work items"
|
||||
className={`-mt-2 ml-10 h-full rounded-md ${
|
||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||
}`}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipContent={activity?.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}
|
||||
tooltipContent={activity?.issue_detail ? activity.issue_detail.name : "This work item has been deleted"}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
{activity?.issue_detail ? (
|
||||
|
|
@ -54,7 +54,7 @@ export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||
</a>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 font-medium text-custom-text-100 whitespace-nowrap">
|
||||
{" an Issue"}{" "}
|
||||
{" a work item"}{" "}
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
|
@ -100,20 +100,20 @@ const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; works
|
|||
|
||||
const inboxActivityMessage = {
|
||||
declined: {
|
||||
showIssue: "declined issue",
|
||||
noIssue: "declined this issue from intake.",
|
||||
showIssue: "declined work item",
|
||||
noIssue: "declined this work item from intake.",
|
||||
},
|
||||
snoozed: {
|
||||
showIssue: "snoozed issue",
|
||||
noIssue: "snoozed this issue.",
|
||||
showIssue: "snoozed work item",
|
||||
noIssue: "snoozed this work item.",
|
||||
},
|
||||
accepted: {
|
||||
showIssue: "accepted issue",
|
||||
noIssue: "accepted this issue from intake.",
|
||||
showIssue: "accepted work item",
|
||||
noIssue: "accepted this work item from intake.",
|
||||
},
|
||||
markedDuplicate: {
|
||||
showIssue: "declined issue",
|
||||
noIssue: "declined this issue from intake by marking a duplicate issue.",
|
||||
showIssue: "declined work item",
|
||||
noIssue: "declined this work item from intake by marking a duplicate work item.",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ const getInboxUserActivityMessage = (activity: IIssueActivity, showIssue: boolea
|
|||
case "2":
|
||||
return showIssue ? inboxActivityMessage.markedDuplicate.showIssue : inboxActivityMessage.markedDuplicate.noIssue;
|
||||
default:
|
||||
return "updated intake issue status.";
|
||||
return "updated intake work item status.";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -393,7 +393,7 @@ const activityDetails: {
|
|||
return (
|
||||
<>
|
||||
<span className="flex-shrink-0">
|
||||
added {showIssue ? <IssueLink activity={activity} /> : "this issue"}{" "}
|
||||
added {showIssue ? <IssueLink activity={activity} /> : "this work item"}{" "}
|
||||
<span className="whitespace-nowrap">to the cycle</span>{" "}
|
||||
</span>
|
||||
<a
|
||||
|
|
@ -442,7 +442,7 @@ const activityDetails: {
|
|||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added {showIssue ? <IssueLink activity={activity} /> : "this issue"} to the module{" "}
|
||||
added {showIssue ? <IssueLink activity={activity} /> : "this work item"} to the module{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
|
|
@ -551,7 +551,7 @@ const activityDetails: {
|
|||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked that {showIssue ? <IssueLink activity={activity} /> : "this issue"} relates to{" "}
|
||||
marked that {showIssue ? <IssueLink activity={activity} /> : "this work item"} relates to{" "}
|
||||
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
|
|
@ -570,14 +570,14 @@ const activityDetails: {
|
|||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} is blocking issue{" "}
|
||||
marked {showIssue ? <IssueLink activity={activity} /> : "this work item"} is blocking work item{" "}
|
||||
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the blocking issue{" "}
|
||||
removed the blocking work item{" "}
|
||||
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
|
|
@ -589,14 +589,14 @@ const activityDetails: {
|
|||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} is being blocked by{" "}
|
||||
marked {showIssue ? <IssueLink activity={activity} /> : "this work item"} is being blocked by{" "}
|
||||
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed {showIssue ? <IssueLink activity={activity} /> : "this issue"} being blocked by issue{" "}
|
||||
removed {showIssue ? <IssueLink activity={activity} /> : "this work item"} being blocked by work item{" "}
|
||||
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
|
|
@ -608,14 +608,14 @@ const activityDetails: {
|
|||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} as duplicate of{" "}
|
||||
marked {showIssue ? <IssueLink activity={activity} /> : "this work item"} as duplicate of{" "}
|
||||
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed {showIssue ? <IssueLink activity={activity} /> : "this issue"} as a duplicate of{" "}
|
||||
removed {showIssue ? <IssueLink activity={activity} /> : "this work item"} as a duplicate of{" "}
|
||||
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
|
|
@ -709,7 +709,7 @@ const activityDetails: {
|
|||
<IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
{activity.verb === "2" && ` from intake by marking a duplicate issue.`}
|
||||
{activity.verb === "2" && ` from intake by marking a duplicate work item.`}
|
||||
</>
|
||||
),
|
||||
icon: <Intake className="size-3 text-custom-text-200" aria-hidden="true" />,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
interface IContentOverflowWrapper {
|
||||
|
|
@ -31,6 +32,9 @@ export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper)
|
|||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentRef?.current) return;
|
||||
|
||||
|
|
@ -142,7 +146,7 @@ export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper)
|
|||
onClick={handleToggle}
|
||||
disabled={isTransitioning}
|
||||
>
|
||||
{showAll ? "Show less" : "Show all"}
|
||||
{showAll ? t("show_less") : t("show_all")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,14 +8,13 @@ import { useDropzone } from "react-dropzone";
|
|||
import { Control, Controller } from "react-hook-form";
|
||||
import useSWR from "swr";
|
||||
import { Tab, Popover } from "@headlessui/react";
|
||||
// plane helpers
|
||||
// plane imports
|
||||
import { MAX_FILE_SIZE } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// plane types
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// ui
|
||||
import { Button, Input, Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { MAX_STATIC_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
|
|
@ -92,7 +91,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
|||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".webp"],
|
||||
},
|
||||
maxSize: MAX_STATIC_FILE_SIZE,
|
||||
maxSize: MAX_FILE_SIZE,
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
|
|
|||
|
|
@ -6,19 +6,18 @@ import { useParams } from "next/navigation";
|
|||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { Search } from "lucide-react";
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ISearchIssueResponse, IUser } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
// services
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { ProjectService } from "@/services/project";
|
||||
// local components
|
||||
import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item";
|
||||
|
|
@ -39,16 +38,19 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
|||
const { isOpen, onClose } = props;
|
||||
// router params
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// hooks
|
||||
const {
|
||||
issues: { removeBulkIssues },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
// hooks
|
||||
const {
|
||||
issues: { removeBulkIssues },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const debouncedSearchTerm: string = useDebounce(query, 500);
|
||||
const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
|
||||
const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId) return;
|
||||
|
|
@ -88,7 +90,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
|||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Please select at least one issue.",
|
||||
message: "Please select at least one work item.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -100,7 +102,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
|||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Issues deleted successfully!",
|
||||
message: "Work items deleted successfully!",
|
||||
});
|
||||
handleClose();
|
||||
})
|
||||
|
|
@ -117,7 +119,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
|||
issues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select issues to delete</h2>
|
||||
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select work items to delete</h2>
|
||||
)}
|
||||
<ul className="text-sm text-custom-text-200">
|
||||
{issues.map((issue) => (
|
||||
|
|
@ -131,12 +133,11 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
|||
</li>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<EmptyState
|
||||
type={
|
||||
query === "" ? EmptyStateType.ISSUE_RELATION_EMPTY_STATE : EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
|
||||
}
|
||||
layout="screen-simple"
|
||||
/>
|
||||
{query === "" ? (
|
||||
<SimpleEmptyState title={t("issue_relation.empty_state.no_issues.title")} assetPath={issuesResolvedPath} />
|
||||
) : (
|
||||
<SimpleEmptyState title={t("issue_relation.empty_state.search.title")} assetPath={searchResolvedPath} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -203,7 +204,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
|||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" onClick={handleSubmit(handleDelete)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Deleting..." : "Delete selected issues"}
|
||||
{isSubmitting ? "Deleting..." : "Delete selected work items"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Rocket, Search, X } from "lucide-react";
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -33,6 +35,8 @@ type Props = {
|
|||
const projectService = new ProjectService();
|
||||
|
||||
export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
|
|
@ -66,8 +70,8 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||
if (selectedIssues.length === 0) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Please select at least one issue.",
|
||||
title: t("toast.error"),
|
||||
message: t("issue.select.error"),
|
||||
});
|
||||
|
||||
return;
|
||||
|
|
@ -140,7 +144,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||
/>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-sm text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0"
|
||||
placeholder="Type to search..."
|
||||
placeholder={t("common.search.placeholder")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
tabIndex={baseTabIndex}
|
||||
|
|
@ -174,7 +178,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
No issues selected
|
||||
{t("issue.select.empty")}
|
||||
</div>
|
||||
)}
|
||||
{workspaceLevelToggle && (
|
||||
|
|
@ -193,7 +197,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
Workspace Level
|
||||
{t("common.workspace_level")}
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
@ -204,6 +208,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||
static
|
||||
className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto"
|
||||
>
|
||||
{/* TODO: Translate here */}
|
||||
{searchTerm !== "" && (
|
||||
<h5 className="mx-2 text-[0.825rem] text-custom-text-200">
|
||||
Search results for{" "}
|
||||
|
|
@ -288,11 +293,11 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||
</Combobox>
|
||||
<div className="flex items-center justify-end gap-2 p-3">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
{selectedIssues.length > 0 && (
|
||||
<Button variant="primary" size="sm" onClick={onSubmit} loading={isSubmitting}>
|
||||
{isSubmitting ? "Adding..." : "Add selected issues"}
|
||||
{isSubmitting ? t("common.adding") : t("issue.select.add_selected")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from "react";
|
||||
// components
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// types
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// constants
|
||||
// components
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
interface EmptyStateProps {
|
||||
issues: ISearchIssueResponse[];
|
||||
|
|
@ -19,18 +19,28 @@ export const IssueSearchModalEmptyState: React.FC<EmptyStateProps> = ({
|
|||
debouncedSearchTerm,
|
||||
isSearching,
|
||||
}) => {
|
||||
const renderEmptyState = (type: EmptyStateType) => (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<EmptyState type={type} layout="screen-simple" />
|
||||
</div>
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
|
||||
const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" });
|
||||
|
||||
const EmptyStateContainer = ({ children }: { children: React.ReactNode }) => (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">{children}</div>
|
||||
);
|
||||
|
||||
const emptyState =
|
||||
issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching
|
||||
? renderEmptyState(EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE)
|
||||
: issues.length === 0
|
||||
? renderEmptyState(EmptyStateType.ISSUE_RELATION_EMPTY_STATE)
|
||||
: null;
|
||||
|
||||
return emptyState;
|
||||
if (issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching) {
|
||||
return (
|
||||
<EmptyStateContainer>
|
||||
<SimpleEmptyState title={t("issue_relation.empty_state.no_issues.title")} assetPath={issuesResolvedPath} />
|
||||
</EmptyStateContainer>
|
||||
);
|
||||
} else if (issues.length === 0) {
|
||||
return (
|
||||
<EmptyStateContainer>
|
||||
<SimpleEmptyState title={t("issue_relation.empty_state.search.title")} assetPath={searchResolvedPath} />
|
||||
</EmptyStateContainer>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ import { observer } from "mobx-react";
|
|||
import { useDropzone } from "react-dropzone";
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// plane types
|
||||
// plane imports
|
||||
import { MAX_FILE_SIZE } from "@plane/constants";
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// hooks
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { MAX_STATIC_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { getAssetIdFromUrl, getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
|
|
@ -40,7 +38,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".webp"],
|
||||
},
|
||||
maxSize: MAX_STATIC_FILE_SIZE,
|
||||
maxSize: MAX_FILE_SIZE,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ import { useParams } from "next/navigation";
|
|||
import { useDropzone } from "react-dropzone";
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// plane types
|
||||
// plane imports
|
||||
import { MAX_FILE_SIZE } from "@plane/constants";
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// hooks
|
||||
import { Button } from "@plane/ui";
|
||||
// constants
|
||||
import { MAX_STATIC_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { getAssetIdFromUrl, getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
|
|
@ -48,7 +46,7 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".webp"],
|
||||
},
|
||||
maxSize: MAX_STATIC_FILE_SIZE,
|
||||
maxSize: MAX_FILE_SIZE,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const ProgressChart: React.FC<Props> = ({
|
|||
endDate,
|
||||
totalIssues,
|
||||
className = "",
|
||||
plotTitle = "issues",
|
||||
plotTitle = "work items",
|
||||
}) => {
|
||||
const chartData = Object.keys(distribution ?? []).map((key) => ({
|
||||
currentDate: renderFormattedDateWithoutYear(key),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
// plane imports
|
||||
import { I_THEME_OPTION, THEME_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// constants
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
import { THEME_OPTIONS, I_THEME_OPTION } from "@/constants/themes";
|
||||
// ui
|
||||
|
||||
type Props = {
|
||||
|
|
|
|||
|
|
@ -6,17 +6,16 @@ import { observer } from "mobx-react";
|
|||
import { CalendarCheck } from "lucide-react";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// types
|
||||
// plane imports
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui";
|
||||
// components
|
||||
import { SingleProgressStats } from "@/components/core";
|
||||
import { StateDropdown } from "@/components/dropdowns";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper";
|
||||
|
|
@ -26,6 +25,7 @@ import { useIssueDetail, useIssues } from "@/hooks/store";
|
|||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// plane web components
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// store
|
||||
import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
|
||||
|
|
@ -41,11 +41,18 @@ export type ActiveCycleStatsProps = {
|
|||
|
||||
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props;
|
||||
|
||||
// local storage
|
||||
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
|
||||
|
||||
// refs
|
||||
const issuesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
// states
|
||||
const [issuesLoaderElement, setIssueLoaderElement] = useState<HTMLDivElement | null>(null);
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const priorityResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/priority" });
|
||||
const assigneesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/assignee" });
|
||||
const labelsResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/label" });
|
||||
|
||||
const currentValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
|
|
@ -119,7 +126,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
)
|
||||
}
|
||||
>
|
||||
Priority Issues
|
||||
{t("project_cycles.active_cycle.priority_issue")}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
|
|
@ -132,7 +139,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
)
|
||||
}
|
||||
>
|
||||
Assignees
|
||||
{t("project_cycles.active_cycle.assignees")}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
|
|
@ -145,7 +152,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
)
|
||||
}
|
||||
>
|
||||
Labels
|
||||
{t("project_cycles.active_cycle.labels")}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
|
||||
|
|
@ -231,10 +238,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE}
|
||||
layout="screen-simple"
|
||||
size="sm"
|
||||
<SimpleEmptyState
|
||||
title={t("active_cycle.empty_state.priority_issue.title")}
|
||||
assetPath={priorityResolvedPath}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -283,7 +289,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
||||
</div>
|
||||
<span>No assignee</span>
|
||||
<span>{t("no_assignee")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
|
|
@ -293,10 +299,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
})
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE}
|
||||
layout="screen-simple"
|
||||
size="sm"
|
||||
<SimpleEmptyState
|
||||
title={t("active_cycle.empty_state.assignee.title")}
|
||||
assetPath={assigneesResolvedPath}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -336,7 +341,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
<SimpleEmptyState title={t("active_cycle.empty_state.label.title")} assetPath={labelsResolvedPath} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, TCycleEstimateType } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import ProgressChart from "@/components/core/sidebar/progress-chart";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { useCycle, useProjectEstimates } from "@/hooks/store";
|
||||
import { useCycle } from "@/hooks/store";
|
||||
// plane web constants
|
||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { EstimateTypeDropdown } from "../dropdowns/estimate-type-dropdown";
|
||||
|
||||
export type ActiveCycleProductivityProps = {
|
||||
|
|
@ -21,11 +22,13 @@ export type ActiveCycleProductivityProps = {
|
|||
|
||||
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, cycle } = props;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { getEstimateTypeByCycleId, setEstimateType } = useCycle();
|
||||
|
||||
// derived values
|
||||
const estimateType: TCycleEstimateType = (cycle && getEstimateTypeByCycleId(cycle.id)) || "issues";
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/chart" });
|
||||
|
||||
const onChange = async (value: TCycleEstimateType) => {
|
||||
if (!workspaceSlug || !projectId || !cycle || !cycle.id) return;
|
||||
|
|
@ -40,7 +43,9 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
|
|||
<div className="flex flex-col min-h-[17rem] gap-5 px-3.5 py-4 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||
<div className="relative flex items-center justify-between gap-4">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}>
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">
|
||||
{t("project_cycles.active_cycle.issue_burndown")}
|
||||
</h3>
|
||||
</Link>
|
||||
<EstimateTypeDropdown value={estimateType} onChange={onChange} cycleId={cycle.id} projectId={projectId} />
|
||||
</div>
|
||||
|
|
@ -53,17 +58,17 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
|
|||
<div className="flex items-center gap-3 text-custom-text-300">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-[#A9BBD0]" />
|
||||
<span>Ideal</span>
|
||||
<span>{t("project_cycles.active_cycle.ideal")}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-[#4C8FFF]" />
|
||||
<span>Current</span>
|
||||
<span>{t("project_cycles.active_cycle.current")}</span>
|
||||
</div>
|
||||
</div>
|
||||
{estimateType === "points" ? (
|
||||
<span>{`Pending points - ${cycle.backlog_estimate_points + cycle.unstarted_estimate_points + cycle.started_estimate_points}`}</span>
|
||||
) : (
|
||||
<span>{`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}</span>
|
||||
<span>{`Pending work items - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -84,7 +89,7 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
|
|||
startDate={cycle.start_date ?? ""}
|
||||
endDate={cycle.end_date ?? ""}
|
||||
totalIssues={cycle.total_issues || 0}
|
||||
plotTitle={"issues"}
|
||||
plotTitle={"work items"}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
|
|
@ -95,7 +100,7 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
|
|||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
<SimpleEmptyState title={t("active_cycle.empty_state.chart.title")} assetPath={resolvedPath} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ import { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
// plane package imports
|
||||
import { PROGRESS_STATE_GROUPS_DETAILS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, IIssueFilterOptions } from "@plane/types";
|
||||
import { LinearProgressIndicator, Loader } from "@plane/ui";
|
||||
// components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
export type ActiveCycleProgressProps = {
|
||||
cycle: ICycle | null;
|
||||
|
|
@ -22,9 +22,10 @@ export type ActiveCycleProgressProps = {
|
|||
|
||||
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props) => {
|
||||
const { handleFiltersUpdate, cycle } = props;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { groupedProjectStates } = useProjectState();
|
||||
|
||||
// derived values
|
||||
const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({
|
||||
id: index,
|
||||
|
|
@ -40,16 +41,17 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props
|
|||
backlog: cycle?.backlog_issues,
|
||||
}
|
||||
: {};
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/progress" });
|
||||
|
||||
return cycle && cycle.hasOwnProperty("started_issues") ? (
|
||||
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">{t("project_cycles.active_cycle.progress")}</h3>
|
||||
{cycle.total_issues > 0 && (
|
||||
<span className="flex gap-1 text-sm text-custom-text-400 font-medium whitespace-nowrap rounded-sm px-3 py-1 ">
|
||||
{`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${
|
||||
cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue"
|
||||
cycle.completed_issues + cycle.cancelled_issues > 1 ? "Work items" : "Work item"
|
||||
} closed`}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -82,7 +84,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props
|
|||
<span className="text-custom-text-300 capitalize font-medium w-16">{group}</span>
|
||||
</div>
|
||||
<span className="text-custom-text-300">{`${groupedIssues[group]} ${
|
||||
groupedIssues[group] > 1 ? "Issues" : "Issue"
|
||||
groupedIssues[group] > 1 ? "Work items" : "Work item"
|
||||
}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -93,7 +95,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props
|
|||
<span className="flex items-center gap-2 text-sm text-custom-text-300">
|
||||
<span>
|
||||
{`${cycle.cancelled_issues} cancelled ${
|
||||
cycle.cancelled_issues > 1 ? "issues are" : "issue is"
|
||||
cycle.cancelled_issues > 1 ? "work items are" : "work item is"
|
||||
} excluded from this report.`}{" "}
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -101,7 +103,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props
|
|||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
<SimpleEmptyState title={t("active_cycle.empty_state.progress.title")} assetPath={resolvedPath} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useSearchParams } from "next/navigation";
|
|||
import { ChevronUp, ChevronDown } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, IIssueFilterOptions, TCyclePlotType, TProgressSnapshot } from "@plane/types";
|
||||
// components
|
||||
import { CycleProgressStats } from "@/components/cycles";
|
||||
|
|
@ -30,7 +31,7 @@ type Options = {
|
|||
};
|
||||
|
||||
export const cycleEstimateOptions: Options[] = [
|
||||
{ value: "issues", label: "Issues" },
|
||||
{ value: "issues", label: "Work items" },
|
||||
{ value: "points", label: "Points" },
|
||||
];
|
||||
export const cycleChartOptions: Options[] = [
|
||||
|
|
@ -63,6 +64,7 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
|||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const cycleDetails = validateCycleSnapshot(getCycleById(cycleId));
|
||||
|
|
@ -138,7 +140,9 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
|||
{isCycleDateValid ? (
|
||||
<div className="relative w-full flex justify-between items-center gap-2">
|
||||
<Disclosure.Button className="relative flex items-center gap-2 w-full">
|
||||
<div className="font-medium text-custom-text-200 text-sm">Progress</div>
|
||||
<div className="font-medium text-custom-text-200 text-sm">
|
||||
{t("project_cycles.active_cycle.progress")}
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Button className="ml-auto">
|
||||
{open ? (
|
||||
|
|
@ -150,7 +154,9 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
|||
</div>
|
||||
) : (
|
||||
<div className="relative w-full flex justify-between items-center gap-2">
|
||||
<div className="font-medium text-custom-text-200 text-sm">Progress</div>
|
||||
<div className="font-medium text-custom-text-200 text-sm">
|
||||
{t("project_cycles.active_cycle.progress")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
IIssueFilters,
|
||||
|
|
@ -73,6 +74,7 @@ type TStateStatComponent = {
|
|||
|
||||
export const AssigneeStatComponent = observer((props: TAssigneeStatComponent) => {
|
||||
const { distribution, isEditable, filters, handleFiltersUpdate } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{distribution && distribution.length > 0 ? (
|
||||
|
|
@ -104,7 +106,7 @@ export const AssigneeStatComponent = observer((props: TAssigneeStatComponent) =>
|
|||
<div className="h-4 w-4 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
||||
</div>
|
||||
<span>No assignee</span>
|
||||
<span>{t("no_assignee")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee?.completed ?? 0}
|
||||
|
|
@ -117,7 +119,7 @@ export const AssigneeStatComponent = observer((props: TAssigneeStatComponent) =>
|
|||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<Image src={emptyMembers} className="h-12 w-12" alt="empty members" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">No assignees yet</h6>
|
||||
<h6 className="text-base text-custom-text-300">{t("no_assignee")}</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -126,6 +128,7 @@ export const AssigneeStatComponent = observer((props: TAssigneeStatComponent) =>
|
|||
|
||||
export const LabelStatComponent = observer((props: TLabelStatComponent) => {
|
||||
const { distribution, isEditable, filters, handleFiltersUpdate } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{distribution && distribution.length > 0 ? (
|
||||
|
|
@ -142,7 +145,7 @@ export const LabelStatComponent = observer((props: TLabelStatComponent) => {
|
|||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-ellipsis truncate">{label.title ?? "No labels"}</span>
|
||||
<span className="text-xs text-ellipsis truncate">{label.title ?? t("no_labels_yet")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
|
|
@ -165,7 +168,7 @@ export const LabelStatComponent = observer((props: TLabelStatComponent) => {
|
|||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label.title ?? "No labels"}</span>
|
||||
<span className="text-xs">{label.title ?? t("no_labels_yet")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
|
|
@ -179,7 +182,7 @@ export const LabelStatComponent = observer((props: TLabelStatComponent) => {
|
|||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<Image src={emptyLabel} className="h-12 w-12" alt="empty label" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">No labels yet</h6>
|
||||
<h6 className="text-base text-custom-text-300">{t("no_labels_yet")}</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -222,15 +225,15 @@ export const StateStatComponent = observer((props: TStateStatComponent) => {
|
|||
const progressStats = [
|
||||
{
|
||||
key: "stat-states",
|
||||
title: "States",
|
||||
i18n_title: "common.states",
|
||||
},
|
||||
{
|
||||
key: "stat-assignees",
|
||||
title: "Assignees",
|
||||
i18n_title: "common.assignees",
|
||||
},
|
||||
{
|
||||
key: "stat-labels",
|
||||
title: "Labels",
|
||||
i18n_title: "common.labels",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -267,6 +270,7 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
|
|||
`cycle-analytics-tab-${cycleId}`,
|
||||
"stat-assignees"
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const currentTabIndex = (tab: string): number => progressStats.findIndex((stat) => stat.key === tab);
|
||||
|
||||
|
|
@ -337,7 +341,7 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
|
|||
key={stat.key}
|
||||
onClick={() => setCycleTab(stat.key)}
|
||||
>
|
||||
{stat.title}
|
||||
{t(stat.i18n_title)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import isEmpty from "lodash/isEmpty";
|
|||
import { observer } from "mobx-react";
|
||||
import { LayersIcon, SquareUser, Users } from "lucide-react";
|
||||
// plane types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar, AvatarGroup, TextArea } from "@plane/ui";
|
||||
|
|
@ -24,6 +25,7 @@ export const CycleSidebarDetails: FC<Props> = observer((props) => {
|
|||
// hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const areEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId.toString());
|
||||
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
|
||||
|
|
@ -32,10 +34,10 @@ export const CycleSidebarDetails: FC<Props> = observer((props) => {
|
|||
const issueCount =
|
||||
isCompleted && !isEmpty(cycleDetails?.progress_snapshot)
|
||||
? cycleDetails?.progress_snapshot?.total_issues === 0
|
||||
? "0 Issue"
|
||||
? `0 ${t("common.work_item")}`
|
||||
: `${cycleDetails?.progress_snapshot?.completed_issues}/${cycleDetails?.progress_snapshot?.total_issues}`
|
||||
: cycleDetails?.total_issues === 0
|
||||
? "0 Issue"
|
||||
? `0 ${t("common.work_item")}`
|
||||
: `${cycleDetails?.completed_issues}/${cycleDetails?.total_issues}`;
|
||||
const estimateType = areEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
|
||||
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined;
|
||||
|
|
@ -51,10 +53,10 @@ export const CycleSidebarDetails: FC<Props> = observer((props) => {
|
|||
const issueEstimatePointCount =
|
||||
isCompleted && !isEmpty(cycleDetails?.progress_snapshot)
|
||||
? cycleDetails?.progress_snapshot.total_issues === 0
|
||||
? "0 Issue"
|
||||
? `0 ${t("common.work_item")}`
|
||||
: `${cycleDetails?.progress_snapshot.completed_estimate_points}/${cycleDetails?.progress_snapshot.total_estimate_points}`
|
||||
: cycleDetails?.total_issues === 0
|
||||
? "0 Issue"
|
||||
? `0 ${t("common.work_item")}`
|
||||
: `${cycleDetails?.completed_estimate_points}/${cycleDetails?.total_estimate_points}`;
|
||||
return (
|
||||
<div className="flex flex-col gap-5 w-full">
|
||||
|
|
@ -70,7 +72,7 @@ export const CycleSidebarDetails: FC<Props> = observer((props) => {
|
|||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<SquareUser className="h-4 w-4" />
|
||||
<span className="text-base">Lead</span>
|
||||
<span className="text-base">{t("lead")}</span>
|
||||
</div>
|
||||
<div className="flex w-3/5 items-center rounded-sm">
|
||||
<div className="flex items-center gap-2.5">
|
||||
|
|
@ -83,7 +85,7 @@ export const CycleSidebarDetails: FC<Props> = observer((props) => {
|
|||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<Users className="h-4 w-4" />
|
||||
<span className="text-base">Members</span>
|
||||
<span className="text-base">{t("members")}</span>
|
||||
</div>
|
||||
<div className="flex w-3/5 items-center rounded-sm">
|
||||
<div className="flex items-center gap-2.5">
|
||||
|
|
@ -104,7 +106,7 @@ export const CycleSidebarDetails: FC<Props> = observer((props) => {
|
|||
</AvatarGroup>
|
||||
</>
|
||||
) : (
|
||||
<span className="px-1.5 text-sm text-custom-text-300">No assignees</span>
|
||||
<span className="px-1.5 text-sm text-custom-text-300">{t("no_assignee")}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -113,7 +115,7 @@ export const CycleSidebarDetails: FC<Props> = observer((props) => {
|
|||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<LayersIcon className="h-4 w-4" />
|
||||
<span className="text-base">Issues</span>
|
||||
<span className="text-base">{t("work_items")}</span>
|
||||
</div>
|
||||
<div className="flex w-3/5 items-center">
|
||||
<span className="px-1.5 text-sm text-custom-text-300">{issueCount}</span>
|
||||
|
|
@ -127,7 +129,7 @@ export const CycleSidebarDetails: FC<Props> = observer((props) => {
|
|||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<LayersIcon className="h-4 w-4" />
|
||||
<span className="text-base">Points</span>
|
||||
<span className="text-base">{t("points")}</span>
|
||||
</div>
|
||||
<div className="flex w-3/5 items-center">
|
||||
<span className="px-1.5 text-sm text-custom-text-300">{issueEstimatePointCount}</span>
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ import { observer } from "mobx-react";
|
|||
import { Controller, useForm } from "react-hook-form";
|
||||
import { ArchiveIcon, ArchiveRestoreIcon, ChevronRight, EllipsisIcon, LinkIcon, Trash2 } from "lucide-react";
|
||||
// types
|
||||
import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// components
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
// constants
|
||||
import { CYCLE_STATUS } from "@/constants/cycle";
|
||||
import { CYCLE_UPDATED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper";
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
|
|
@ -20,7 +19,6 @@ import { copyUrlToClipboard } from "@/helpers/string.helper";
|
|||
import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
// local components
|
||||
|
|
@ -53,6 +51,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
const { allowPermissions } = useUserPermissions();
|
||||
const { updateCycleDetails, restoreCycle } = useCycle();
|
||||
const { setTrackElement, captureCycleEvent } = useEventTracker();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// form info
|
||||
const { control, reset } = useForm({
|
||||
|
|
@ -71,16 +70,16 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Restore success",
|
||||
message: "Your cycle can be found in project cycles.",
|
||||
title: t("project_cycles.action.restore.success.title"),
|
||||
message: t("project_cycles.action.restore.success.description"),
|
||||
});
|
||||
router.push(`/${workspaceSlug.toString()}/projects/${projectId.toString()}/archives/cycles`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Cycle could not be restored. Please try again.",
|
||||
title: t("project_cycles.action.restore.failed.title"),
|
||||
message: t("project_cycles.action.restore.failed.description"),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
@ -90,14 +89,14 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link Copied!",
|
||||
message: "Cycle link copied to clipboard.",
|
||||
title: t("common.link_copied"),
|
||||
message: t("common.link_copied_to_clipboard"),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Some error occurred",
|
||||
title: t("common.errors.default.message"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -167,15 +166,14 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
submitChanges(payload, "date_range");
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Cycle updated successfully.",
|
||||
title: t("project_cycles.action.update.success.title"),
|
||||
message: t("project_cycles.action.update.success.description"),
|
||||
});
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message:
|
||||
"You already have a cycle on the given dates, if you want to create a draft cycle, you can do that by removing both the dates.",
|
||||
title: t("project_cycles.action.update.failed.title"),
|
||||
message: t("project_cycles.action.update.error.already_exists"),
|
||||
});
|
||||
reset({ ...cycleDetails });
|
||||
}
|
||||
|
|
@ -232,15 +230,15 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
{isCompleted ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
Archive cycle
|
||||
{t("common.archive")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
<div className="-mt-1">
|
||||
<p>Archive cycle</p>
|
||||
<p>{t("common.archive")}</p>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Only completed cycles <br /> can be archived.
|
||||
{t("project_cycles.only_completed_cycles_can_be_archived")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -251,7 +249,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
<CustomMenu.MenuItem onClick={handleRestoreCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<ArchiveRestoreIcon className="h-3 w-3" />
|
||||
<span>Restore cycle</span>
|
||||
<span>{t("project_cycles.action.restore.title")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
|
@ -264,7 +262,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete cycle</span>
|
||||
<span>{t("delete")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
|
@ -283,7 +281,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
backgroundColor: `${currentCycle.color}20`,
|
||||
}}
|
||||
>
|
||||
{currentCycle.title}
|
||||
{t(currentCycle.i18n_title)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// helpers
|
||||
import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters";
|
||||
import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "@/helpers/string.helper";
|
||||
// constants
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TCycleFilters } from "@plane/types";
|
||||
// hooks
|
||||
import { Tag } from "@plane/ui";
|
||||
import { AppliedDateFilters, AppliedStatusFilters } from "@/components/cycles";
|
||||
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
// components
|
||||
// helpers
|
||||
|
|
@ -26,6 +27,7 @@ export const CycleAppliedFiltersList: React.FC<Props> = observer((props) => {
|
|||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props;
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!appliedFilters) return null;
|
||||
|
||||
|
|
@ -77,7 +79,7 @@ export const CycleAppliedFiltersList: React.FC<Props> = observer((props) => {
|
|||
{isEditingAllowed && (
|
||||
<button type="button" onClick={handleClearAllFilters}>
|
||||
<Tag>
|
||||
Clear all
|
||||
{t("common.clear_all")}
|
||||
<X size={12} strokeWidth={2} />
|
||||
</Tag>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
import { CYCLE_STATUS } from "@/constants/cycle";
|
||||
import { CYCLE_STATUS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -11,6 +12,7 @@ type Props = {
|
|||
|
||||
export const AppliedStatusFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, values, editable } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -25,7 +27,7 @@ export const AppliedStatusFilters: React.FC<Props> = observer((props) => {
|
|||
statusDetails?.textColor
|
||||
)}
|
||||
>
|
||||
{statusDetails?.title}
|
||||
{statusDetails && t(statusDetails?.i18n_title)}
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -2,28 +2,31 @@ import React from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// types
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TCycleFilters } from "@plane/types";
|
||||
// components
|
||||
import { ArchivedCyclesView, CycleAppliedFiltersList } from "@/components/cycles";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { CycleModuleListLayout } from "@/components/ui";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useCycle, useCycleFilter } from "@/hooks/store";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
export const ArchivedCycleLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { fetchArchivedCycles, currentProjectArchivedCycleIds, loader } = useCycle();
|
||||
// cycle filters hook
|
||||
const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useCycleFilter();
|
||||
// derived values
|
||||
const totalArchivedCycles = currentProjectArchivedCycleIds?.length ?? 0;
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-cycles" });
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `ARCHIVED_CYCLES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||
|
|
@ -64,7 +67,11 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => {
|
|||
)}
|
||||
{totalArchivedCycles === 0 ? (
|
||||
<div className="h-full place-items-center">
|
||||
<EmptyState type={EmptyStateType.PROJECT_ARCHIVED_NO_CYCLES} />
|
||||
<DetailedEmptyState
|
||||
title={t("project_cycles.empty_state.archived.title")}
|
||||
description={t("project_cycles.empty_state.archived.description")}
|
||||
assetPath={resolvedPath}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ListFilter, Search, X } from "lucide-react";
|
|||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TCycleFilters } from "@plane/types";
|
||||
// components
|
||||
import { CycleFiltersSelection } from "@/components/cycles";
|
||||
|
|
@ -25,6 +26,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// hooks
|
||||
const { currentProjectFilters, searchQuery, updateFilters, updateSearchQuery } = useCycleFilter();
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
|
||||
// outside click detector hook
|
||||
|
|
@ -114,7 +116,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
// components
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CyclesList } from "@/components/cycles";
|
||||
// ui
|
||||
import { CycleModuleListLayout } from "@/components/ui";
|
||||
|
|
@ -21,6 +22,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
|||
// store hooks
|
||||
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader, currentProjectActiveCycleId } = useCycle();
|
||||
const { searchQuery } = useCycleFilter();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const filteredCycleIds = getFilteredCycleIds(projectId, false);
|
||||
const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId);
|
||||
|
|
@ -39,11 +41,11 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
|||
className="mx-auto h-36 w-36 sm:h-48 sm:w-48"
|
||||
alt="No matching cycles"
|
||||
/>
|
||||
<h5 className="mb-1 mt-7 text-xl font-medium">No matching cycles</h5>
|
||||
<h5 className="mb-1 mt-7 text-xl font-medium">{t("project_cycles.no_matching_cycles")}</h5>
|
||||
<p className="text-base text-custom-text-400">
|
||||
{searchQuery.trim() === ""
|
||||
? "Remove the filters to see all cycles"
|
||||
: "Remove the search criteria to see all cycles"}
|
||||
? t("project_cycles.remove_filters_to_see_all_cycles")
|
||||
: t("project_cycles.remove_search_criteria_to_see_all_cycles")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import { useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
// types
|
||||
import { PROJECT_ERROR_MESSAGES, CYCLE_DELETED } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { CYCLE_DELETED } from "@/constants/event-tracker";
|
||||
import { PROJECT_ERROR_MESSAGES } from "@/constants/project";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
|
@ -29,6 +29,7 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
|||
// store hooks
|
||||
const { captureCycleEvent } = useEventTracker();
|
||||
const { deleteCycle } = useCycle();
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { cycleId } = useParams();
|
||||
|
|
@ -59,9 +60,9 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
|||
? PROJECT_ERROR_MESSAGES.permissionError
|
||||
: PROJECT_ERROR_MESSAGES.cycleDeleteError;
|
||||
setToast({
|
||||
title: currentError.title,
|
||||
title: t(currentError.i18n_title),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: currentError.message,
|
||||
message: currentError.i18n_message && t(currentError.i18n_message),
|
||||
});
|
||||
captureCycleEvent({
|
||||
eventName: CYCLE_DELETED,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
// constants
|
||||
import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants";
|
||||
// components
|
||||
import { DateFilterModal } from "@/components/core";
|
||||
import { FilterHeader, FilterOption } from "@/components/issues";
|
||||
// constants
|
||||
import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters";
|
||||
|
||||
// helpers
|
||||
import { isInDateFormat } from "@/helpers/date-time.helper";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
// constants
|
||||
import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants";
|
||||
// components
|
||||
import { DateFilterModal } from "@/components/core";
|
||||
import { FilterHeader, FilterOption } from "@/components/issues";
|
||||
// constants
|
||||
import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters";
|
||||
|
||||
// helpers
|
||||
import { isInDateFormat } from "@/helpers/date-time.helper";
|
||||
|
||||
|
|
@ -17,7 +17,6 @@ type Props = {
|
|||
|
||||
export const FilterStartDate: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CYCLE_STATUS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TCycleGroups } from "@plane/types";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "@/components/issues";
|
||||
// types
|
||||
import { CYCLE_STATUS } from "@/constants/cycle";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
|
|
@ -17,7 +18,8 @@ export const FilterStatus: React.FC<Props> = observer((props) => {
|
|||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
// states
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
//hooks
|
||||
const { t } = useTranslation();
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
const filteredOptions = CYCLE_STATUS.filter((p) => p.value.includes(searchQuery.toLowerCase()));
|
||||
|
||||
|
|
@ -36,7 +38,7 @@ export const FilterStatus: React.FC<Props> = observer((props) => {
|
|||
key={status.value}
|
||||
isChecked={appliedFilters?.includes(status.value) ? true : false}
|
||||
onClick={() => handleUpdate(status.value)}
|
||||
title={status.title}
|
||||
title={t(status.i18n_title)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@
|
|||
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// plane imports
|
||||
import { ETabIndices } from "@plane/constants";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
// components
|
||||
import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
|
||||
// constants
|
||||
import { ETabIndices } from "@/constants/tab-indices";
|
||||
// helpers
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldRenderProject } from "@/helpers/project.helper";
|
||||
|
|
@ -34,6 +36,7 @@ const defaultValues: Partial<ICycle> = {
|
|||
|
||||
export const CycleForm: React.FC<Props> = (props) => {
|
||||
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props;
|
||||
const { t } = useTranslation();
|
||||
// form data
|
||||
const {
|
||||
formState: { errors, isSubmitting, dirtyFields },
|
||||
|
|
@ -84,7 +87,9 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||
)}
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{status ? "Update" : "Create"} cycle</h3>
|
||||
<h3 className="text-xl font-medium text-custom-text-200">
|
||||
{status ? t("project_cycles.update_cycle") : t("project_cycles.create_cycle")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
|
|
@ -92,17 +97,17 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||
name="name"
|
||||
control={control}
|
||||
rules={{
|
||||
required: "Title is required",
|
||||
required: t("title_is_required"),
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
message: t("title_should_be_less_than_255_characters"),
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
placeholder={t("title")}
|
||||
className="w-full text-base"
|
||||
value={value}
|
||||
inputSize="md"
|
||||
|
|
@ -122,7 +127,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
placeholder={t("description")}
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
|
|
@ -171,10 +176,16 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={getIndex("submit")}>
|
||||
{data ? (isSubmitting ? "Updating" : "Update Cycle") : isSubmitting ? "Creating" : "Create Cycle"}
|
||||
{data
|
||||
? isSubmitting
|
||||
? t("common.updating")
|
||||
: t("project_cycles.update_cycle")
|
||||
: isSubmitting
|
||||
? t("common.creating")
|
||||
: t("project_cycles.create_cycle")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { useParams, usePathname, useSearchParams } from "next/navigation";
|
|||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Eye, Users } from "lucide-react";
|
||||
// types
|
||||
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// ui
|
||||
import {
|
||||
|
|
@ -24,7 +26,6 @@ import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
|
|||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
// constants
|
||||
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
|
|
@ -36,7 +37,6 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
|||
// plane web components
|
||||
import { CycleAdditionalActions } from "@/plane-web/components/cycles";
|
||||
// plane web constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
|
||||
|
|
@ -64,6 +64,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
|
@ -111,14 +112,14 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
);
|
||||
|
||||
setPromiseToast(addToFavoritePromise, {
|
||||
loading: "Adding cycle to favorites...",
|
||||
loading: t("project_cycles.action.favorite.loading"),
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Cycle added to favorites.",
|
||||
title: t("project_cycles.action.favorite.success.title"),
|
||||
message: () => t("project_cycles.action.favorite.success.description"),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Couldn't add the cycle to favorites. Please try again.",
|
||||
title: t("project_cycles.action.favorite.failed.title"),
|
||||
message: () => t("project_cycles.action.favorite.failed.description"),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -140,14 +141,14 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
});
|
||||
|
||||
setPromiseToast(removeFromFavoritePromise, {
|
||||
loading: "Removing cycle from favorites...",
|
||||
loading: t("project_cycles.action.unfavorite.loading"),
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Cycle removed from favorites.",
|
||||
title: t("project_cycles.action.unfavorite.success.title"),
|
||||
message: () => t("project_cycles.action.unfavorite.success.description"),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Couldn't remove the cycle from favorites. Please try again.",
|
||||
title: t("project_cycles.action.unfavorite.failed.title"),
|
||||
message: () => t("project_cycles.action.unfavorite.failed.description"),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -187,15 +188,14 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
submitChanges(payload);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Cycle updated successfully.",
|
||||
title: t("project_cycles.action.update.success.title"),
|
||||
message: t("project_cycles.action.update.success.description"),
|
||||
});
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message:
|
||||
"You already have a cycle on the given dates, if you want to create a draft cycle, you can do that by removing both the dates.",
|
||||
title: t("project_cycles.action.update.failed.title"),
|
||||
message: t("project_cycles.action.update.error.already_exists"),
|
||||
});
|
||||
reset({ ...cycleDetails });
|
||||
}
|
||||
|
|
@ -239,7 +239,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
className={`z-[1] flex text-custom-primary-200 text-xs gap-1 flex-shrink-0 ${isMobile || (isActive && !searchParams.has("peekCycle")) ? "flex" : "hidden group-hover:flex"}`}
|
||||
>
|
||||
<Eye className="h-4 w-4 my-auto text-custom-primary-200" />
|
||||
<span>More details</span>
|
||||
<span>{t("project_cycles.more_details")}</span>
|
||||
</button>
|
||||
|
||||
{showIssueCount && (
|
||||
|
|
@ -258,7 +258,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
}}
|
||||
>
|
||||
<TransferIcon className="fill-custom-primary-200 w-4" />
|
||||
<span>Transfer {cycleDetails.pending_issues} issues</span>
|
||||
<span>Transfer {cycleDetails.pending_issues} work items</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// components
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ContentWrapper, ERowVariant } from "@plane/ui";
|
||||
import { ListLayout } from "@/components/core/list";
|
||||
import { CycleListGroupHeader, CyclePeekOverview, CyclesListMap } from "@/components/cycles";
|
||||
|
|
@ -18,6 +19,7 @@ export interface ICyclesList {
|
|||
|
||||
export const CyclesList: FC<ICyclesList> = observer((props) => {
|
||||
const { completedCycleIds, upcomingCycleIds, cycleIds, workspaceSlug, projectId, isArchived = false } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ContentWrapper variant={ERowVariant.HUGGING} className="flex-row">
|
||||
|
|
@ -36,7 +38,7 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
|
|||
<>
|
||||
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
|
||||
<CycleListGroupHeader
|
||||
title="Upcoming cycle"
|
||||
title={t("project_cycles.upcoming_cycle.label")}
|
||||
type="upcoming"
|
||||
count={upcomingCycleIds.length}
|
||||
showCount
|
||||
|
|
@ -55,7 +57,7 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
|
|||
<>
|
||||
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
|
||||
<CycleListGroupHeader
|
||||
title="Completed cycle"
|
||||
title={t("project_cycles.completed_cycle.label")}
|
||||
type="completed"
|
||||
count={completedCycleIds.length}
|
||||
showCount
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import React, { useEffect, useState } from "react";
|
|||
import { format } from "date-fns";
|
||||
import { mutate } from "swr";
|
||||
// types
|
||||
import { CYCLE_CREATED, CYCLE_UPDATED } from "@plane/constants";
|
||||
import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { CycleForm } from "@/components/cycles";
|
||||
// constants
|
||||
import { CYCLE_CREATED, CYCLE_UPDATED } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle, useProject } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { observer } from "mobx-react";
|
|||
// icons
|
||||
import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
|
||||
// ui
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles";
|
||||
|
|
@ -16,7 +18,6 @@ import { copyUrlToClipboard } from "@/helpers/string.helper";
|
|||
import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useEndCycle, EndCycleModal } from "@/plane-web/components/cycles";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
|
|
@ -37,6 +38,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
|||
const { setTrackElement } = useEventTracker();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { getCycleById, restoreCycle } = useCycle();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const cycleDetails = getCycleById(cycleId);
|
||||
const isArchived = !!cycleDetails?.archived_at;
|
||||
|
|
@ -57,8 +59,8 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
|||
copyUrlToClipboard(cycleLink).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link Copied!",
|
||||
message: "Cycle link copied to clipboard.",
|
||||
title: t("common.link_copied"),
|
||||
message: t("common.link_copied_to_clipboard"),
|
||||
});
|
||||
});
|
||||
const handleOpenInNewTab = () => window.open(`/${cycleLink}`, "_blank");
|
||||
|
|
@ -75,16 +77,16 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
|||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Restore success",
|
||||
message: "Your cycle can be found in project cycles.",
|
||||
title: t("project_cycles.action.restore.success.title"),
|
||||
message: t("project_cycles.action.restore.success.description"),
|
||||
});
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archives/cycles`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Cycle could not be restored. Please try again.",
|
||||
title: t("project_cycles.action.restore.failed.title"),
|
||||
message: t("project_cycles.action.restore.failed.description"),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -96,7 +98,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
|||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "edit",
|
||||
title: "Edit",
|
||||
title: t("edit"),
|
||||
icon: Pencil,
|
||||
action: handleEditCycle,
|
||||
shouldRender: isEditingAllowed && !isCompleted && !isArchived,
|
||||
|
|
@ -104,22 +106,22 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
|||
{
|
||||
key: "open-new-tab",
|
||||
action: handleOpenInNewTab,
|
||||
title: "Open in new tab",
|
||||
title: t("open_in_new_tab"),
|
||||
icon: ExternalLink,
|
||||
shouldRender: !isArchived,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
action: handleCopyText,
|
||||
title: "Copy link",
|
||||
title: t("copy_link"),
|
||||
icon: LinkIcon,
|
||||
shouldRender: !isArchived,
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
action: handleArchiveCycle,
|
||||
title: "Archive",
|
||||
description: isCompleted ? undefined : "Only completed cycles can\nbe archived.",
|
||||
title: t("archive"),
|
||||
description: isCompleted ? undefined : t("project_cycles.only_completed_cycles_can_be_archived"),
|
||||
icon: ArchiveIcon,
|
||||
className: "items-start",
|
||||
iconClassName: "mt-1",
|
||||
|
|
@ -129,14 +131,14 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
|||
{
|
||||
key: "restore",
|
||||
action: handleRestoreCycle,
|
||||
title: "Restore",
|
||||
title: t("restor"),
|
||||
icon: ArchiveRestoreIcon,
|
||||
shouldRender: isEditingAllowed && isArchived,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
action: handleDeleteCycle,
|
||||
title: "Delete",
|
||||
title: t("delete"),
|
||||
icon: Trash2,
|
||||
shouldRender: isEditingAllowed && !isCompleted && !isArchived,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export const TransferIssuesModal: React.FC<Props> = observer((props) => {
|
|||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Issues have been transferred successfully",
|
||||
message: "Work items have been transferred successfully",
|
||||
});
|
||||
await getCycleDetails(payload.new_cycle_id);
|
||||
})
|
||||
|
|
@ -49,7 +49,7 @@ export const TransferIssuesModal: React.FC<Props> = observer((props) => {
|
|||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Unable to transfer Issues. Please try again.",
|
||||
message: "Unable to transfer work items. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -114,7 +114,7 @@ export const TransferIssuesModal: React.FC<Props> = observer((props) => {
|
|||
<div className="flex items-center justify-between px-5">
|
||||
<div className="flex items-center gap-1">
|
||||
<TransferIcon className="w-5 fill-custom-text-100" />
|
||||
<h4 className="text-xl font-medium text-custom-text-100">Transfer Issues</h4>
|
||||
<h4 className="text-xl font-medium text-custom-text-100">Transfer work items</h4>
|
||||
</div>
|
||||
<button onClick={handleClose}>
|
||||
<X className="h-4 w-4" />
|
||||
|
|
@ -164,7 +164,7 @@ export const TransferIssuesModal: React.FC<Props> = observer((props) => {
|
|||
<div className="flex w-full items-center justify-center gap-4 p-5 text-sm">
|
||||
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<span className="text-center text-custom-text-200">
|
||||
You don’t have any current cycle. Please create one to transfer the issues.
|
||||
You don’t have any current cycle. Please create one to transfer the work items.
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const TransferIssues: React.FC<Props> = (props) => {
|
|||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
Transfer Issues
|
||||
Transfer work items
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
// ui
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
// assets
|
||||
import ProjectEmptyStateImage from "@/public/empty-state/onboarding/dashboard-light.webp";
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ export const DashboardProjectEmptyState = observer(() => {
|
|||
<div className="mx-auto flex h-full flex-col justify-center space-y-4 lg:w-3/5">
|
||||
<h4 className="text-xl font-semibold">Overview of your projects, activity, and metrics</h4>
|
||||
<p className="text-custom-text-300">
|
||||
Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this
|
||||
Welcome to Plane, we are excited to have you here. Create your first project and track your work items, and this
|
||||
page will transform into a space that helps you progress. Admins will also see items which help their team
|
||||
progress.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants";
|
||||
import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types";
|
||||
// hooks
|
||||
import { Card } from "@plane/ui";
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
WidgetLoader,
|
||||
WidgetProps,
|
||||
} from "@/components/dashboard/widgets";
|
||||
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard";
|
||||
import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper";
|
||||
import { useDashboard } from "@/hooks/store";
|
||||
// components
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants";
|
||||
import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types";
|
||||
// hooks
|
||||
import { Card } from "@plane/ui";
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
WidgetLoader,
|
||||
WidgetProps,
|
||||
} from "@/components/dashboard/widgets";
|
||||
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard";
|
||||
import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper";
|
||||
import { useDashboard } from "@/hooks/store";
|
||||
// components
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// components
|
||||
import { DURATION_FILTER_OPTIONS, EDurationFilters } from "@plane/constants";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { DateFilterModal } from "@/components/core";
|
||||
// ui
|
||||
// helpers
|
||||
import { DURATION_FILTER_OPTIONS, EDurationFilters } from "@/constants/dashboard";
|
||||
import { getDurationFilterDropdownLabel } from "@/helpers/dashboard.helper";
|
||||
// constants
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,35 @@
|
|||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { TIssuesListTypes } from "@plane/types";
|
||||
// types
|
||||
import { ASSIGNED_ISSUES_EMPTY_STATES } from "@/constants/dashboard";
|
||||
// constants
|
||||
import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg";
|
||||
import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg";
|
||||
import UpcomingIssuesDark from "@/public/empty-state/dashboard/dark/upcoming-issues.svg";
|
||||
import CompletedIssuesLight from "@/public/empty-state/dashboard/light/completed-issues.svg";
|
||||
import OverdueIssuesLight from "@/public/empty-state/dashboard/light/overdue-issues.svg";
|
||||
import UpcomingIssuesLight from "@/public/empty-state/dashboard/light/upcoming-issues.svg";
|
||||
|
||||
export const ASSIGNED_ISSUES_EMPTY_STATES = {
|
||||
pending: {
|
||||
title: "Work items assigned to you that are pending\nwill show up here.",
|
||||
darkImage: UpcomingIssuesDark,
|
||||
lightImage: UpcomingIssuesLight,
|
||||
},
|
||||
upcoming: {
|
||||
title: "Upcoming work items assigned to\nyou will show up here.",
|
||||
darkImage: UpcomingIssuesDark,
|
||||
lightImage: UpcomingIssuesLight,
|
||||
},
|
||||
overdue: {
|
||||
title: "Work items assigned to you that are past\ntheir due date will show up here.",
|
||||
darkImage: OverdueIssuesDark,
|
||||
lightImage: OverdueIssuesLight,
|
||||
},
|
||||
completed: {
|
||||
title: "Work items assigned to you that you have\nmarked Completed will show up here.",
|
||||
darkImage: CompletedIssuesDark,
|
||||
lightImage: CompletedIssuesLight,
|
||||
},
|
||||
};
|
||||
type Props = {
|
||||
type: TIssuesListTypes;
|
||||
};
|
||||
|
|
@ -22,7 +47,7 @@ export const AssignedIssuesEmptyState: React.FC<Props> = (props) => {
|
|||
return (
|
||||
<div className="text-center space-y-6 flex flex-col items-center">
|
||||
<div className="h-24 w-24">
|
||||
<Image src={image} className="w-full h-full" alt="Assigned issues" />
|
||||
<Image src={image} className="w-full h-full" alt="Assigned work items" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-custom-text-300 whitespace-pre-line">{typeDetails.title}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,35 @@
|
|||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { TIssuesListTypes } from "@plane/types";
|
||||
// types
|
||||
import { CREATED_ISSUES_EMPTY_STATES } from "@/constants/dashboard";
|
||||
// constants
|
||||
import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg";
|
||||
import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg";
|
||||
import UpcomingIssuesDark from "@/public/empty-state/dashboard/dark/upcoming-issues.svg";
|
||||
import CompletedIssuesLight from "@/public/empty-state/dashboard/light/completed-issues.svg";
|
||||
import OverdueIssuesLight from "@/public/empty-state/dashboard/light/overdue-issues.svg";
|
||||
import UpcomingIssuesLight from "@/public/empty-state/dashboard/light/upcoming-issues.svg";
|
||||
|
||||
export const CREATED_ISSUES_EMPTY_STATES = {
|
||||
pending: {
|
||||
title: "Work items created by you that are pending\nwill show up here.",
|
||||
darkImage: UpcomingIssuesDark,
|
||||
lightImage: UpcomingIssuesLight,
|
||||
},
|
||||
upcoming: {
|
||||
title: "Upcoming work items you created\nwill show up here.",
|
||||
darkImage: UpcomingIssuesDark,
|
||||
lightImage: UpcomingIssuesLight,
|
||||
},
|
||||
overdue: {
|
||||
title: "Work items created by you that are past their\ndue date will show up here.",
|
||||
darkImage: OverdueIssuesDark,
|
||||
lightImage: OverdueIssuesLight,
|
||||
},
|
||||
completed: {
|
||||
title: "Work items created by you that you have\nmarked completed will show up here.",
|
||||
darkImage: CompletedIssuesDark,
|
||||
lightImage: CompletedIssuesLight,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
type: TIssuesListTypes;
|
||||
|
|
@ -21,7 +47,7 @@ export const CreatedIssuesEmptyState: React.FC<Props> = (props) => {
|
|||
return (
|
||||
<div className="text-center space-y-6 flex flex-col items-center">
|
||||
<div className="h-24 w-24">
|
||||
<Image src={image} className="w-full h-full" alt="Assigned issues" />
|
||||
<Image src={image} className="w-full h-full" alt="Assigned work items" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-custom-text-300 whitespace-pre-line">{typeDetails.title}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ export const IssuesByPriorityEmptyState = () => {
|
|||
return (
|
||||
<div className="text-center space-y-6 flex flex-col items-center">
|
||||
<div className="h-24 w-24">
|
||||
<Image src={image} className="w-full h-full" alt="Issues by state group" />
|
||||
<Image src={image} className="w-full h-full" alt="Work items by state group" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-custom-text-300">
|
||||
Issues assigned to you, broken down by
|
||||
Work items assigned to you, broken down by
|
||||
<br />
|
||||
priority will show up here.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ export const IssuesByStateGroupEmptyState = () => {
|
|||
return (
|
||||
<div className="text-center space-y-6 flex flex-col items-center">
|
||||
<div className="h-24 w-24">
|
||||
<Image src={image} className="w-full h-full" alt="Issues by state group" />
|
||||
<Image src={image} className="w-full h-full" alt="Work items by state group" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-custom-text-300">
|
||||
Issue assigned to you, broken down by state,
|
||||
Work items assigned to you, broken down by state,
|
||||
<br />
|
||||
will show up here.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ export const RecentActivityEmptyState = () => {
|
|||
return (
|
||||
<div className="text-center space-y-6 flex flex-col items-center">
|
||||
<div className="h-24 w-24">
|
||||
<Image src={image} className="w-full h-full" alt="Issues by state group" />
|
||||
<Image src={image} className="w-full h-full" alt="Work items by state group" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-custom-text-300">
|
||||
All your issue activities across
|
||||
All your work items activities across
|
||||
<br />
|
||||
projects will show up here.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
|||
"col-span-9": type === "created" && tab === "completed",
|
||||
})}
|
||||
>
|
||||
Issues
|
||||
Work items
|
||||
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl px-2 flex items-center text-center justify-center">
|
||||
{widgetStats.count}
|
||||
</span>
|
||||
|
|
@ -126,7 +126,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
|||
"w-min my-3 mx-auto py-1 px-2 text-xs hover:bg-custom-primary-100/20"
|
||||
)}
|
||||
>
|
||||
View all issues
|
||||
View all work items
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { TIssuesListTypes } from "@plane/types";
|
||||
// helpers
|
||||
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard";
|
||||
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants";
|
||||
import { TIssuesListTypes } from "@plane/types";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// types
|
||||
// constants
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// types
|
||||
import { EDurationFilters } from "@plane/constants";
|
||||
import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types";
|
||||
// components
|
||||
import { Card } from "@plane/ui";
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
} from "@/components/dashboard/widgets";
|
||||
import { IssuesByPriorityGraph } from "@/components/graphs";
|
||||
// constants
|
||||
import { EDurationFilters } from "@/constants/dashboard";
|
||||
// helpers
|
||||
import { getCustomDates } from "@/helpers/dashboard.helper";
|
||||
// hooks
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { linearGradientDef } from "@nivo/core";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// types
|
||||
import { EDurationFilters, STATE_GROUPS } from "@plane/constants";
|
||||
import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types";
|
||||
// components
|
||||
import { Card } from "@plane/ui";
|
||||
|
|
@ -12,9 +14,6 @@ import {
|
|||
WidgetProps,
|
||||
} from "@/components/dashboard/widgets";
|
||||
import { PieGraph } from "@/components/ui";
|
||||
// constants
|
||||
import { EDurationFilters, STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "@/constants/dashboard";
|
||||
import { STATE_GROUPS } from "@/constants/state";
|
||||
// helpers
|
||||
import { getCustomDates } from "@/helpers/dashboard.helper";
|
||||
// hooks
|
||||
|
|
@ -23,6 +22,37 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
|||
|
||||
const WIDGET_KEY = "issues_by_state_groups";
|
||||
|
||||
export const STATE_GROUP_GRAPH_COLORS: Record<TStateGroups, string> = {
|
||||
backlog: "#CDCED6",
|
||||
unstarted: "#80838D",
|
||||
started: "#FFC53D",
|
||||
completed: "#3E9B4F",
|
||||
cancelled: "#E5484D",
|
||||
};
|
||||
// colors for work items by state group widget graph arcs
|
||||
export const STATE_GROUP_GRAPH_GRADIENTS = [
|
||||
linearGradientDef("gradientBacklog", [
|
||||
{ offset: 0, color: "#DEDEDE" },
|
||||
{ offset: 100, color: "#BABABE" },
|
||||
]),
|
||||
linearGradientDef("gradientUnstarted", [
|
||||
{ offset: 0, color: "#D4D4D4" },
|
||||
{ offset: 100, color: "#878796" },
|
||||
]),
|
||||
linearGradientDef("gradientStarted", [
|
||||
{ offset: 0, color: "#FFD300" },
|
||||
{ offset: 100, color: "#FAE270" },
|
||||
]),
|
||||
linearGradientDef("gradientCompleted", [
|
||||
{ offset: 0, color: "#0E8B1B" },
|
||||
{ offset: 100, color: "#37CB46" },
|
||||
]),
|
||||
linearGradientDef("gradientCanceled", [
|
||||
{ offset: 0, color: "#C90004" },
|
||||
{ offset: 100, color: "#FF7679" },
|
||||
]),
|
||||
];
|
||||
|
||||
export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
const { dashboardId, workspaceSlug } = props;
|
||||
// states
|
||||
|
|
|
|||
|
|
@ -30,25 +30,25 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
|
|||
const STATS_LIST = [
|
||||
{
|
||||
key: "assigned",
|
||||
title: "Issues assigned",
|
||||
title: "Work items assigned",
|
||||
count: widgetStats?.assigned_issues_count,
|
||||
link: `/${workspaceSlug}/workspace-views/assigned`,
|
||||
},
|
||||
{
|
||||
key: "overdue",
|
||||
title: "Issues overdue",
|
||||
title: "Work items overdue",
|
||||
count: widgetStats?.pending_issues_count,
|
||||
link: `/${workspaceSlug}/workspace-views/assigned/?state_group=backlog,unstarted,started&target_date=${today};before`,
|
||||
},
|
||||
{
|
||||
key: "created",
|
||||
title: "Issues created",
|
||||
title: "Work items created",
|
||||
count: widgetStats?.created_issues_count,
|
||||
link: `/${workspaceSlug}/workspace-views/created`,
|
||||
},
|
||||
{
|
||||
key: "completed",
|
||||
title: "Issues completed",
|
||||
title: "Work items completed",
|
||||
count: widgetStats?.completed_issues_count,
|
||||
link: `/${workspaceSlug}/workspace-views/assigned?state_group=completed`,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
|
|||
return (
|
||||
<Card>
|
||||
<Link href={redirectionLink} className="text-lg font-semibold text-custom-text-300 hover:underline mb-4">
|
||||
Your issue activities
|
||||
Your work item activities
|
||||
</Link>
|
||||
{widgetStats.length > 0 ? (
|
||||
<div className="mt-4 space-y-6">
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const CollaboratorListItem: React.FC<CollaboratorListItemProps> = observer((prop
|
|||
{isCurrentUser ? "You" : userDetails?.display_name}
|
||||
</h6>
|
||||
<p className="mt-2 text-sm">
|
||||
{issueCount} active issue{issueCount > 1 ? "s" : ""}
|
||||
{issueCount} active work items{issueCount > 1 ? "s" : ""}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { observer } from "mobx-react";
|
|||
import Link from "next/link";
|
||||
import { Plus } from "lucide-react";
|
||||
// plane types
|
||||
import { PROJECT_BACKGROUND_COLORS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TRecentProjectsWidgetResponse } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar, AvatarGroup, Card } from "@plane/ui";
|
||||
|
|
@ -12,7 +13,6 @@ import { Avatar, AvatarGroup, Card } from "@plane/ui";
|
|||
import { Logo } from "@/components/common";
|
||||
import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
|
||||
// constants
|
||||
import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
|
|
@ -25,7 +25,6 @@ import {
|
|||
useMember,
|
||||
} from "@/hooks/store";
|
||||
// plane web constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
const WIDGET_KEY = "recent_projects";
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { usePopper } from "react-popper";
|
|||
// components
|
||||
import { Check, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// icon
|
||||
import { TCycleGroups } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -36,6 +38,8 @@ type CycleOptionsProps = {
|
|||
|
||||
export const CycleOptions: FC<CycleOptionsProps> = observer((props) => {
|
||||
const { projectId, isOpen, referenceElement, placement, canRemoveCycle, currentCycleId } = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
//state hooks
|
||||
const [query, setQuery] = useState("");
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
|
@ -103,11 +107,11 @@ export const CycleOptions: FC<CycleOptionsProps> = observer((props) => {
|
|||
if (canRemoveCycle) {
|
||||
options?.unshift({
|
||||
value: null,
|
||||
query: "No cycle",
|
||||
query: t("cycle.no_cycle"),
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<ContrastIcon className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="flex-grow truncate">No cycle</span>
|
||||
<span className="flex-grow truncate">{t("cycle.no_cycle")}</span>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
|
@ -132,7 +136,7 @@ export const CycleOptions: FC<CycleOptionsProps> = observer((props) => {
|
|||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
placeholder={t("common.search.label")}
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
onKeyDown={searchInputKeyDown}
|
||||
/>
|
||||
|
|
@ -159,10 +163,10 @@ export const CycleOptions: FC<CycleOptionsProps> = observer((props) => {
|
|||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">No matches found</p>
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("common.search.no_matches_found")}</p>
|
||||
)
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">Loading...</p>
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("common.loading")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { ReactNode, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { ComboDropDown, ContrastIcon } from "@plane/ui";
|
||||
// helpers
|
||||
|
|
@ -52,6 +53,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||
renderByDefault = true,
|
||||
currentCycleId,
|
||||
} = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
|
@ -108,7 +111,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading="Cycle"
|
||||
tooltipHeading={t("common.cycle")}
|
||||
tooltipContent={selectedName ?? placeholder}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@ import { useCallback, useMemo } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes } from "@plane/constants";
|
||||
import { EIssueLayoutTypes, ISSUE_LAYOUT_MAP } from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane ui
|
||||
import { Dropdown } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { ISSUE_LAYOUT_MAP } from "@/constants/issue";
|
||||
// components
|
||||
import { IssueLayoutIcon } from "@/components/issues";
|
||||
|
||||
type TLayoutDropDown = {
|
||||
onChange: (value: EIssueLayoutTypes) => void;
|
||||
|
|
@ -18,6 +20,8 @@ type TLayoutDropDown = {
|
|||
|
||||
export const LayoutDropDown = observer((props: TLayoutDropDown) => {
|
||||
const { onChange, value = EIssueLayoutTypes.LIST, disabledLayouts = [] } = props;
|
||||
// plane i18n
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const availableLayouts = useMemo(
|
||||
() => Object.values(ISSUE_LAYOUT_MAP).filter((layout) => !disabledLayouts.includes(layout.key)),
|
||||
|
|
@ -35,11 +39,10 @@ export const LayoutDropDown = observer((props: TLayoutDropDown) => {
|
|||
|
||||
const buttonContent = useCallback((isOpen: boolean, buttonValue: string | string[] | undefined) => {
|
||||
const dropdownValue = ISSUE_LAYOUT_MAP[buttonValue as EIssueLayoutTypes];
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center text-custom-text-200">
|
||||
<dropdownValue.icon strokeWidth={2} className={`size-3.5 text-custom-text-200`} />
|
||||
<span className="font-medium text-xs">{dropdownValue.label}</span>
|
||||
<IssueLayoutIcon layout={dropdownValue.key} strokeWidth={2} className={`size-3.5 text-custom-text-200`} />
|
||||
<span className="font-medium text-xs">{t(dropdownValue.i18n_label)}</span>
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
|
|
@ -50,8 +53,8 @@ export const LayoutDropDown = observer((props: TLayoutDropDown) => {
|
|||
return (
|
||||
<div className={cn("flex gap-2 items-center text-custom-text-200 w-full justify-between")}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<dropdownValue.icon strokeWidth={2} className={`size-3 text-custom-text-200`} />
|
||||
<span className="font-medium text-xs">{dropdownValue.label}</span>
|
||||
<IssueLayoutIcon layout={dropdownValue.key} strokeWidth={2} className={`size-3 text-custom-text-200`} />
|
||||
<span className="font-medium text-xs">{t(dropdownValue.i18n_label)}</span>
|
||||
</div>
|
||||
{props.selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { createPortal } from "react-dom";
|
|||
import { usePopper } from "react-popper";
|
||||
import { Check, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { EUserPermissions } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
|
|
@ -17,7 +18,6 @@ import { getFileURL } from "@/helpers/file.helper";
|
|||
// hooks
|
||||
import { useUser, useMember } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { EUserPermissions } from "@/plane-web/constants";
|
||||
|
||||
interface Props {
|
||||
memberIds?: string[];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronDown, X } from "lucide-react";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { ComboDropDown, DiceIcon, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
|
|
@ -185,6 +187,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||
value,
|
||||
renderByDefault = true,
|
||||
} = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
|
|
@ -256,7 +260,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading="Module"
|
||||
tooltipHeading={t("common.module")}
|
||||
tooltipContent={
|
||||
Array.isArray(value)
|
||||
? `${value
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { useParams } from "next/navigation";
|
|||
import { usePopper } from "react-popper";
|
||||
import { Check, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
//components
|
||||
import { DiceIcon } from "@plane/ui";
|
||||
//store
|
||||
|
|
@ -35,6 +37,8 @@ interface Props {
|
|||
|
||||
export const ModuleOptions = observer((props: Props) => {
|
||||
const { projectId, isOpen, referenceElement, placement, multiple } = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
|
@ -97,11 +101,11 @@ export const ModuleOptions = observer((props: Props) => {
|
|||
if (!multiple)
|
||||
options?.unshift({
|
||||
value: null,
|
||||
query: "No module",
|
||||
query: t("module.no_module"),
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="flex-grow truncate">No module</span>
|
||||
<span className="flex-grow truncate">{t("module.no_module")}</span>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
|
@ -125,7 +129,7 @@ export const ModuleOptions = observer((props: Props) => {
|
|||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
placeholder={t("common.search.label")}
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
onKeyDown={searchInputKeyDown}
|
||||
/>
|
||||
|
|
@ -157,10 +161,10 @@ export const ModuleOptions = observer((props: Props) => {
|
|||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">No matching results</p>
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("common.search.no_matching_results")}</p>
|
||||
)
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">Loading...</p>
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("common.loading")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@ import { useTheme } from "next-themes";
|
|||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search, SignalHigh } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { ISSUE_PRIORITIES } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
// ui
|
||||
import { ComboDropDown, PriorityIcon, Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
|
|
@ -77,7 +76,7 @@ const BorderButton = (props: ButtonProps) => {
|
|||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={t("priority")}
|
||||
tooltipContent={t(priorityDetails?.key ?? "none")}
|
||||
tooltipContent={priorityDetails?.title ?? t("common.none")}
|
||||
disabled={!showTooltip}
|
||||
isMobile={isMobile}
|
||||
renderByDefault={renderToolTipByDefault}
|
||||
|
|
@ -121,7 +120,7 @@ const BorderButton = (props: ButtonProps) => {
|
|||
) : (
|
||||
<SignalHigh className="size-3" />
|
||||
))}
|
||||
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
|
|
@ -204,7 +203,9 @@ const BackgroundButton = (props: ButtonProps) => {
|
|||
) : (
|
||||
<SignalHigh className="size-3" />
|
||||
))}
|
||||
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{priorityDetails?.title ?? t("common.priority") ?? placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
|
|
@ -244,7 +245,7 @@ const TransparentButton = (props: ButtonProps) => {
|
|||
return (
|
||||
<Tooltip
|
||||
tooltipHeading={t("priority")}
|
||||
tooltipContent={t(priorityDetails?.key ?? "none")}
|
||||
tooltipContent={priorityDetails?.title ?? t("common.none")}
|
||||
disabled={!showTooltip}
|
||||
isMobile={isMobile}
|
||||
renderByDefault={renderToolTipByDefault}
|
||||
|
|
@ -289,7 +290,9 @@ const TransparentButton = (props: ButtonProps) => {
|
|||
) : (
|
||||
<SignalHigh className="size-3" />
|
||||
))}
|
||||
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{priorityDetails?.title ?? t("common.priority") ?? placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
|
|
@ -299,6 +302,8 @@ const TransparentButton = (props: ButtonProps) => {
|
|||
};
|
||||
|
||||
export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
//hooks
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
button,
|
||||
buttonClassName,
|
||||
|
|
@ -312,7 +317,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||
highlightUrgent = true,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Priority",
|
||||
placeholder = t("common.priority"),
|
||||
placement,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
|
|
@ -340,8 +345,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||
},
|
||||
],
|
||||
});
|
||||
//hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
// next-themes
|
||||
// TODO: remove this after new theming implementation
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
|
@ -352,7 +356,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<PriorityIcon priority={priority.key} size={14} withContainer />
|
||||
<span className="flex-grow truncate">{t(priority.key)}</span>
|
||||
<span className="flex-grow truncate">{priority.title}</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
placeholder={t("common.search.label")}
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
onKeyDown={searchInputKeyDown}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { observer } from "mobx-react";
|
|||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { usePopper } from "react-popper";
|
||||
// plane imports
|
||||
import { ROLE } from "@plane/constants";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// constants
|
||||
import { ROLE } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import React, { useState } from "react";
|
|||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
||||
// helpers
|
||||
|
|
@ -34,6 +36,7 @@ interface LiteTextEditorWrapperProps
|
|||
}
|
||||
|
||||
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
containerClassName,
|
||||
workspaceSlug,
|
||||
|
|
@ -46,7 +49,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||
showSubmitButton = true,
|
||||
isSubmitting = false,
|
||||
showToolbarInitially = true,
|
||||
placeholder = "Add comment...",
|
||||
placeholder = t("issue.comments.placeholder"),
|
||||
uploadFile,
|
||||
...rest
|
||||
} = props;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { Globe2, Lock, LucideIcon } from "lucide-react";
|
|||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Button, Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
|
|
@ -46,6 +48,7 @@ const COMMENT_ACCESS_SPECIFIERS: TCommentAccessType[] = [
|
|||
const toolbarItems = TOOLBAR_ITEMS.lite;
|
||||
|
||||
export const IssueCommentToolbar: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
accessSpecifier,
|
||||
executeCommand,
|
||||
|
|
@ -171,7 +174,7 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
|
|||
disabled={isSubmitButtonDisabled}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Comment
|
||||
{t("common.comment")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,194 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
// components
|
||||
import { Button, TButtonSizes, TButtonVariant } from "@plane/ui";
|
||||
// constant
|
||||
import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
import { ComicBoxButton } from "./comic-box-button";
|
||||
|
||||
export type EmptyStateProps = {
|
||||
size?: TButtonSizes;
|
||||
type: EmptyStateType;
|
||||
layout?: "screen-detailed" | "screen-simple";
|
||||
additionalPath?: string;
|
||||
primaryButtonConfig?: {
|
||||
size?: TButtonSizes;
|
||||
variant?: TButtonVariant;
|
||||
};
|
||||
primaryButtonOnClick?: () => void;
|
||||
primaryButtonLink?: string;
|
||||
secondaryButtonOnClick?: () => void;
|
||||
};
|
||||
|
||||
export const EmptyState: React.FC<EmptyStateProps> = observer((props) => {
|
||||
const {
|
||||
size = "lg",
|
||||
type,
|
||||
layout = "screen-detailed",
|
||||
additionalPath = "",
|
||||
primaryButtonConfig = {
|
||||
size: "lg",
|
||||
variant: "primary",
|
||||
},
|
||||
primaryButtonOnClick,
|
||||
primaryButtonLink,
|
||||
secondaryButtonOnClick,
|
||||
} = props;
|
||||
// store
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
// if empty state type is not found
|
||||
if (!EMPTY_STATE_DETAILS[type]) return null;
|
||||
|
||||
// current empty state details
|
||||
const { key, title, description, path, primaryButton, secondaryButton, accessType, access } =
|
||||
EMPTY_STATE_DETAILS[type];
|
||||
// resolved empty state path
|
||||
const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${
|
||||
resolvedTheme === "light" ? "light" : "dark"
|
||||
}.webp`;
|
||||
// permission
|
||||
const isEditingAllowed =
|
||||
access &&
|
||||
accessType &&
|
||||
allowPermissions(
|
||||
access,
|
||||
accessType === "workspace" ? EUserPermissionsLevel.WORKSPACE : EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
const anyButton = primaryButton || secondaryButton;
|
||||
|
||||
// primary button
|
||||
const renderPrimaryButton = () => {
|
||||
if (!primaryButton) return null;
|
||||
|
||||
const commonProps = {
|
||||
size: primaryButtonConfig.size,
|
||||
variant: primaryButtonConfig.variant,
|
||||
prependIcon: primaryButton.icon,
|
||||
onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined,
|
||||
disabled: !isEditingAllowed,
|
||||
};
|
||||
|
||||
if (primaryButton.comicBox) {
|
||||
return (
|
||||
<ComicBoxButton
|
||||
label={primaryButton.text}
|
||||
icon={primaryButton.icon}
|
||||
title={primaryButton.comicBox?.title}
|
||||
description={primaryButton.comicBox?.description}
|
||||
onClick={primaryButtonOnClick}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
);
|
||||
} else if (primaryButtonLink) {
|
||||
return (
|
||||
<Link href={primaryButtonLink}>
|
||||
<Button {...commonProps}>{primaryButton.text}</Button>
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return <Button {...commonProps}>{primaryButton.text}</Button>;
|
||||
}
|
||||
};
|
||||
// secondary button
|
||||
const renderSecondaryButton = () => {
|
||||
if (!secondaryButton) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
size={size}
|
||||
variant="neutral-primary"
|
||||
prependIcon={secondaryButton.icon}
|
||||
onClick={secondaryButtonOnClick}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
{secondaryButton.text}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{layout === "screen-detailed" && (
|
||||
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
|
||||
<div
|
||||
className={cn("flex flex-col gap-5", {
|
||||
"md:min-w-[24rem] max-w-[45rem]": size === "sm",
|
||||
"md:min-w-[30rem] max-w-[60rem]": size === "lg",
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5 flex-shrink">
|
||||
{description ? (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">{title}</h3>
|
||||
<p className="text-sm">{description}</p>
|
||||
</>
|
||||
) : (
|
||||
<h3 className="text-xl font-medium">{title}</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{path && (
|
||||
<Image
|
||||
src={resolvedEmptyStatePath}
|
||||
alt={key || "button image"}
|
||||
width={384}
|
||||
height={250}
|
||||
layout="responsive"
|
||||
lazyBoundary="100%"
|
||||
/>
|
||||
)}
|
||||
|
||||
{anyButton && (
|
||||
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
|
||||
{renderPrimaryButton()}
|
||||
{renderSecondaryButton()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{layout === "screen-simple" && (
|
||||
<div className="text-center flex flex-col gap-2.5 items-center">
|
||||
<div className={`${size === "sm" ? "h-24 w-24" : "h-28 w-28"}`}>
|
||||
<Image
|
||||
src={resolvedEmptyStatePath}
|
||||
alt={key || "button image"}
|
||||
width={size === "sm" ? 78 : 96}
|
||||
height={size === "sm" ? 78 : 96}
|
||||
layout="responsive"
|
||||
lazyBoundary="100%"
|
||||
/>
|
||||
</div>
|
||||
{description ? (
|
||||
<>
|
||||
<h3 className="text-lg font-medium text-custom-text-300 whitespace-pre-line">{title}</h3>
|
||||
<p className="text-base font-medium text-custom-text-400 whitespace-pre-line">{description}</p>
|
||||
</>
|
||||
) : (
|
||||
<h3 className="text-sm font-medium text-custom-text-400 whitespace-pre-line">{title}</h3>
|
||||
)}
|
||||
{anyButton && (
|
||||
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
|
||||
{renderPrimaryButton()}
|
||||
{renderSecondaryButton()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue