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:
Prateek Shourya 2025-02-06 20:41:31 +05:30 committed by GitHub
parent e244f48776
commit d36c3acbf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
693 changed files with 18182 additions and 10485 deletions

View file

@ -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}
</>
);
};
});

View file

@ -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();

View file

@ -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>
);
});
});

View file

@ -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>

View file

@ -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>
)}
</>

View file

@ -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>

View file

@ -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",
};
}

View file

@ -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 (

View file

@ -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;

View file

@ -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>
)

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"}`}
/>

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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")} </>}
/>
);
};

View file

@ -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>

View file

@ -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>

View file

@ -14,7 +14,7 @@ const ARCHIVES_TAB_LIST: {
}[] = [
{
key: "issues",
label: "Issues",
label: "Work items",
shouldRender: () => true,
},
{

View file

@ -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>

View file

@ -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) => {

View file

@ -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>

View file

@ -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

View file

@ -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>
))}

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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" />,

View file

@ -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>
);
};

View file

@ -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 {

View file

@ -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";

View file

@ -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";

View file

@ -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"
}`}

View file

@ -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" />,

View file

@ -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>

View file

@ -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 () => {

View file

@ -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>
)}

View file

@ -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>

View file

@ -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;
};

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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),

View file

@ -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 = {

View file

@ -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>
)
) : (

View file

@ -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>
</>
)}

View file

@ -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>

View file

@ -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>
)}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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"

View file

@ -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">

View file

@ -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}
>

View file

@ -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>

View file

@ -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,

View file

@ -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";

View file

@ -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);

View file

@ -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)}
/>
))
) : (

View file

@ -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>

View file

@ -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>
)}

View file

@ -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

View file

@ -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";

View file

@ -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,
},

View file

@ -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 dont have any current cycle. Please create one to transfer the issues.
You dont have any current cycle. Please create one to transfer the work items.
</span>
</div>
)

View file

@ -27,7 +27,7 @@ export const TransferIssues: React.FC<Props> = (props) => {
onClick={handleClick}
disabled={disabled}
>
Transfer Issues
Transfer work items
</Button>
</div>
)}

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>
)}
</>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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`,
},

View file

@ -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">

View file

@ -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>
);

View file

@ -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";

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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[];

View file

@ -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

View file

@ -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>

View file

@ -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>
),
}));

View file

@ -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}
/>

View file

@ -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";

View file

@ -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;

View file

@ -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>
)}

View file

@ -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