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

@ -2,6 +2,8 @@
import { useState, Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { TDeDupeIssue, TIssue } from "@plane/types";
// ui
@ -20,6 +22,7 @@ type Props = {
export const ArchiveIssueModal: React.FC<Props> = (props) => {
const { dataId, data, isOpen, handleClose, onSubmit } = props;
const { t } = useTranslation();
// states
const [isArchiving, setIsArchiving] = useState(false);
// store hooks
@ -44,16 +47,16 @@ export const ArchiveIssueModal: React.FC<Props> = (props) => {
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Archive success",
message: "Your archives can be found in project archives.",
title: t("issue.archive.success.label"),
message: t("issue.archive.success.message"),
});
onClose();
})
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be archived. Please try again.",
title: t("common.error.label"),
message: t("issue.archive.failed.message"),
})
)
.finally(() => setIsArchiving(false));
@ -88,17 +91,15 @@ export const ArchiveIssueModal: React.FC<Props> = (props) => {
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="px-5 py-4">
<h3 className="text-xl font-medium 2xl:text-2xl">
Archive issue {projectDetails?.identifier} {issue.sequence_id}
{t("issue.archive.label")} {projectDetails?.identifier} {issue.sequence_id}
</h3>
<p className="mt-3 text-sm text-custom-text-200">
Are you sure you want to archive the issue? All your archived issues can be restored later.
</p>
<p className="mt-3 text-sm text-custom-text-200">{t("issue.archive.confirm_message")}</p>
<div className="mt-3 flex justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Cancel
{t("common.cancel")}
</Button>
<Button size="sm" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
{isArchiving ? "Archiving" : "Archive"}
{isArchiving ? t("common.archiving") : t("common.archive")}
</Button>
</div>
</div>

View file

@ -2,14 +2,14 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane constants
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// components
import { ArchiveTabsList } from "@/components/archives";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@ -28,6 +28,8 @@ export const ArchivedIssuesHeader: FC = observer(() => {
const {
project: { projectMemberIds },
} = useMember();
// i18n
const { t } = useTranslation();
// for archived issues list layout is the only option
const activeLayout = "list";
// hooks
@ -72,29 +74,27 @@ export const ArchivedIssuesHeader: FC = observer(() => {
</div>
{/* filter options */}
<div className="flex items-center gap-2 px-8">
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FiltersDropdown title={t("common.filters")} placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FilterSelection
filters={issueFilters?.filters || {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.archived_issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection
displayFilters={issueFilters?.displayFilters || {}}
displayProperties={issueFilters?.displayProperties || {}}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>

View file

@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import { FileRejection, useDropzone } from "react-dropzone";
import { UploadCloud } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueServiceType } from "@plane/types";
// hooks
import { TOAST_TYPE, setToast } from "@plane/ui";
@ -35,6 +36,7 @@ export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((p
disabled,
issueServiceType = EIssueServiceType.ISSUES,
} = props;
const { t } = useTranslation();
// states
const [isUploading, setIsUploading] = useState(false);
// store hooks
@ -70,8 +72,8 @@ export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((p
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "File could not be attached. Try uploading again.",
title: t("toast.error"),
message: t("attachment.error"),
});
})
.finally(() => {
@ -83,11 +85,11 @@ export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((p
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
title: t("toast.error"),
message:
totalAttachedFiles > 1
? "Only one file can be uploaded at a time."
: `File must be of ${maxFileSize / 1024 / 1024}MB or less in size.`,
? t("attachment.only_one_file_allowed")
: t("attachment.file_size_limit", { size: maxFileSize / 1024 / 1024 }),
});
return;
},
@ -127,7 +129,7 @@ export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((p
<div className="flex items-center justify-center p-1 rounded-md bg-custom-background-100">
<div className="flex flex-col justify-center items-center px-5 py-6 rounded-md border border-dashed border-custom-border-300">
<UploadCloud className="size-7" />
<span className="text-sm text-custom-text-300">Drag and drop anywhere to upload</span>
<span className="text-sm text-custom-text-300">{t("attachment.drag_and_drop")}</span>
</div>
</div>
</div>

View file

@ -4,6 +4,7 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { Trash } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueServiceType } from "@plane/types";
// ui
import { CustomMenu, Tooltip } from "@plane/ui";
@ -25,6 +26,7 @@ type TIssueAttachmentsListItem = {
};
export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer((props) => {
const { t } = useTranslation();
// props
const { attachmentId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks
@ -89,7 +91,7 @@ export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer(
>
<div className="flex items-center gap-2">
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
<span>Delete</span>
<span>{t("common.actions.delete")}</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>

View file

@ -2,6 +2,8 @@ import { FC, useState } from "react";
import { observer } from "mobx-react";
// constants
import { EIssueServiceType } from "@plane/constants";
// plane-i18n
import { useTranslation } from "@plane/i18n";
// types
import { TIssueServiceType } from "@plane/types";
// ui
@ -24,6 +26,7 @@ type Props = {
};
export const IssueAttachmentDeleteModal: FC<Props> = observer((props) => {
const { t } = useTranslation();
const { isOpen, onClose, attachmentId, attachmentOperations, issueServiceType = EIssueServiceType.ISSUES } = props;
// states
const [loader, setLoader] = useState(false);
@ -54,9 +57,10 @@ export const IssueAttachmentDeleteModal: FC<Props> = observer((props) => {
handleSubmit={() => handleDeletion(attachment.id)}
isSubmitting={loader}
isOpen={isOpen}
title="Delete attachment"
title={t("attachment.delete")}
content={
<>
{/* TODO: Translate here */}
Are you sure you want to delete attachment-{" "}
<span className="font-bold">{getFileName(attachment.attributes.name)}</span>? This attachment will be
permanently removed. This action cannot be undone.

View file

@ -15,7 +15,8 @@ export const BulkOperationsUpgradeBanner: React.FC<Props> = (props) => {
<div className={cn("sticky bottom-0 left-0 h-20 z-[2] px-3.5 grid place-items-center", className)}>
<div className="h-14 w-full bg-custom-primary-100/10 border-[0.5px] border-custom-primary-100/50 py-4 px-3.5 flex items-center justify-between gap-2 rounded-md">
<p className="text-custom-primary-100 font-medium">
Change state, priority, and more for several issues at once. Save three minutes on an average per operation.
Change state, priority, and more for several work items at once. Save three minutes on an average per
operation.
</p>
<a
href={MARKETING_PLANE_ONE_PAGE_LINK}

View file

@ -65,7 +65,7 @@ export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
You can save this issue to Drafts so you can come back to it later.{" "}
You can save this work item to Drafts so you can come back to it later.{" "}
</p>
</div>
</div>

View file

@ -49,7 +49,7 @@ export const CreateIssueToastActionItems: FC<TCreateIssueToastActionItems> = obs
rel="noopener noreferrer"
className="text-custom-primary px-2 py-1 hover:bg-custom-background-90 font-medium rounded"
>
{`View ${isEpic ? "epic" : "issue"}`}
{`View ${isEpic ? "epic" : "work item"}`}
</a>
{copied ? (

View file

@ -2,15 +2,16 @@
import { useEffect, useState } from "react";
// types
import { PROJECT_ERROR_MESSAGES ,EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TDeDupeIssue, TIssue } from "@plane/types";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
import { PROJECT_ERROR_MESSAGES } from "@/constants/project";
// hooks
import { useIssues, useProject, useUser, useUserPermissions } from "@/hooks/store";
// plane-web
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type Props = {
isOpen: boolean;
handleClose: () => void;
@ -29,6 +30,7 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
const { issueMap } = useIssues();
const { getProjectById } = useProject();
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
const { data: currentUser } = useUser();
@ -57,9 +59,10 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
if (!authorized) {
setToast({
title: PROJECT_ERROR_MESSAGES.permissionError.title,
title: t(PROJECT_ERROR_MESSAGES.permissionError.i18n_title),
type: TOAST_TYPE.ERROR,
message: PROJECT_ERROR_MESSAGES.permissionError.message,
message:
PROJECT_ERROR_MESSAGES.permissionError.i18n_message && t(PROJECT_ERROR_MESSAGES.permissionError.i18n_message),
});
onClose();
return;
@ -69,22 +72,24 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: `${isSubIssue ? "Sub-issue" : isEpic ? "Epic" : "Issue"} deleted successfully`,
title: t("common.success"),
message: t("entity.delete.success", {
entity: isSubIssue ? t("common.sub_work_item") : isEpic ? t("common.epic") : t("common.work_item"),
}),
});
onClose();
})
.catch((errors) => {
const isPermissionError =
errors?.error ===
`Only admin or creator can delete the ${isSubIssue ? "sub-issue" : isEpic ? "epic" : "issue"}`;
`Only admin or creator can delete the ${isSubIssue ? "sub-work item" : isEpic ? "epic" : "work item"}`;
const currentError = isPermissionError
? PROJECT_ERROR_MESSAGES.permissionError
: PROJECT_ERROR_MESSAGES.issueDeleteError;
setToast({
title: currentError.title,
title: t(currentError.i18n_title),
type: TOAST_TYPE.ERROR,
message: currentError.message,
message: currentError.i18n_message && t(currentError.i18n_message),
});
})
.finally(() => onClose());
@ -96,14 +101,15 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
handleSubmit={handleIssueDelete}
isSubmitting={isDeleting}
isOpen={isOpen}
title={`Delete ${isEpic ? "epic" : "issue"}`}
title={t("entity.delete.label", { entity: isEpic ? t("common.epic") : t("common.work_item") })}
content={
<>
{`Are you sure you want to delete ${isEpic ? "epic" : "issue"} `}
{/* TODO: Translate here */}
{`Are you sure you want to delete ${isEpic ? "epic" : "work item"} `}
<span className="break-words font-medium text-custom-text-100">
{projectDetails?.identifier}-{issue?.sequence_id}
</span>
{` ? All of the data related to the ${isEpic ? "epic" : "issue"} will be permanently removed. This action cannot be undone.`}
{` ? All of the data related to the ${isEpic ? "epic" : "work item"} will be permanently removed. This action cannot be undone.`}
</>
}
/>

View file

@ -4,6 +4,8 @@ import { FC, useCallback, useEffect, useState } from "react";
import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { TIssue, TNameDescriptionLoader } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums";
@ -13,7 +15,7 @@ import { Loader } from "@plane/ui";
import { RichTextEditor, RichTextReadOnlyEditor } from "@/components/editor";
import { TIssueOperations } from "@/components/issues/issue-detail";
// helpers
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
// hooks
import { useWorkspace } from "@/hooks/store";
// plane web services
@ -50,6 +52,9 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
placeholder,
} = props;
// i18n
const { t } = useTranslation();
const { handleSubmit, reset, control } = useForm<TIssue>({
defaultValues: {
description_html: initialValue,
@ -119,7 +124,9 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
debouncedFormSave();
}}
placeholder={
placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)
placeholder
? placeholder
: (isFocused, value) => t(`${getDescriptionPlaceholderI18n(isFocused, value)}`)
}
searchMentionCallback={async (payload) =>
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
@ -142,7 +149,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
);
return asset_id;
} catch (error) {
console.log("Error in uploading issue asset:", error);
console.log("Error in uploading work item asset:", error);
throw new Error("Asset upload failed. Please try again later.");
}
}}

View file

@ -3,14 +3,14 @@
import { useCallback, useState } from "react";
import { observer } from "mobx-react";
// plane constants
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType, ISSUE_STORE_TO_FILTERS_MAP } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import { Button } from "@plane/ui";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ISSUE_STORE_TO_FILTERS_MAP } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@ -34,6 +34,8 @@ const HeaderFilters = observer((props: Props) => {
canUserCreateIssue,
storeType = EIssuesStoreType.PROJECT,
} = props;
// i18n
const { t } = useTranslation();
// states
const [analyticsModal, setAnalyticsModal] = useState(false);
// store hooks
@ -111,7 +113,11 @@ const HeaderFilters = observer((props: Props) => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@ -127,7 +133,7 @@ const HeaderFilters = observer((props: Props) => {
isEpic={storeType === EIssuesStoreType.EPIC}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
displayFilters={issueFilters?.displayFilters ?? {}}
@ -141,7 +147,7 @@ const HeaderFilters = observer((props: Props) => {
</FiltersDropdown>
{canUserCreateIssue ? (
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
{t("common.analytics")}
</Button>
) : (
<></>

View file

@ -1,6 +1,8 @@
"use client";
import React, { FC } from "react";
import { Layers, Link, Paperclip, Waypoints } from "lucide-react";
//i18n
import { useTranslation } from "@plane/i18n";
// components
import {
IssueAttachmentActionButton,
@ -19,13 +21,14 @@ type Props = {
export const IssueDetailWidgetActionButtons: FC<Props> = (props) => {
const { workspaceSlug, projectId, issueId, disabled } = props;
const { t } = useTranslation();
return (
<div className="flex items-center flex-wrap gap-2">
<SubIssuesActionButton
issueId={issueId}
customButton={
<IssueDetailWidgetButton
title="Add sub-issue"
title={t("issue.add.sub_issue")}
icon={<Layers className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />}
disabled={disabled}
/>
@ -36,7 +39,7 @@ export const IssueDetailWidgetActionButtons: FC<Props> = (props) => {
issueId={issueId}
customButton={
<IssueDetailWidgetButton
title="Add relation"
title={t("issue.add.relation")}
icon={<Waypoints className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />}
disabled={disabled}
/>
@ -46,7 +49,7 @@ export const IssueDetailWidgetActionButtons: FC<Props> = (props) => {
<IssueLinksActionButton
customButton={
<IssueDetailWidgetButton
title="Add link"
title={t("issue.add.link")}
icon={<Link className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />}
disabled={disabled}
/>
@ -59,7 +62,7 @@ export const IssueDetailWidgetActionButtons: FC<Props> = (props) => {
issueId={issueId}
customButton={
<IssueDetailWidgetButton
title="Attach"
title={t("common.attach")}
icon={<Paperclip className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />}
disabled={disabled}
/>

View file

@ -2,6 +2,7 @@
import React, { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueServiceType } from "@plane/types";
import { CollapsibleButton } from "@plane/ui";
// components
@ -20,6 +21,7 @@ type Props = {
export const IssueAttachmentsCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, workspaceSlug, projectId, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
const { t } = useTranslation();
// store hooks
const {
issue: { getIssueById },
@ -42,7 +44,7 @@ export const IssueAttachmentsCollapsibleTitle: FC<Props> = observer((props) => {
return (
<CollapsibleButton
isOpen={isOpen}
title="Attachments"
title={t("common.attachments")}
indicatorElement={indicatorElement}
actionItemElement={
!disabled && (

View file

@ -104,7 +104,7 @@ export const IssueDetailWidgetModals: 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;
}

View file

@ -1,6 +1,7 @@
"use client";
import { useMemo } from "react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueLink, TIssueServiceType } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// hooks
@ -15,6 +16,8 @@ export const useLinkOperations = (
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
): TLinkOperations => {
const { createLink, updateLink, removeLink } = useIssueDetail(issueServiceType);
// i18n
const { t } = useTranslation();
const handleLinkOperations: TLinkOperations = useMemo(
() => ({
@ -23,15 +26,15 @@ export const useLinkOperations = (
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await createLink(workspaceSlug, projectId, issueId, data);
setToast({
message: "The link has been successfully created",
message: t("links.toasts.created.message"),
type: TOAST_TYPE.SUCCESS,
title: "Link created",
title: t("links.toasts.created.title"),
});
} catch (error: any) {
setToast({
message: error?.data?.url?.error ?? "The link could not be created",
message: error?.data?.url?.error ?? t("links.toasts.not_created.message"),
type: TOAST_TYPE.ERROR,
title: "Link not created",
title: t("links.toasts.not_created.title"),
});
throw error;
}
@ -41,15 +44,15 @@ export const useLinkOperations = (
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await updateLink(workspaceSlug, projectId, issueId, linkId, data);
setToast({
message: "The link has been successfully updated",
message: t("links.toasts.updated.message"),
type: TOAST_TYPE.SUCCESS,
title: "Link updated",
title: t("links.toasts.updated.title"),
});
} catch (error: any) {
setToast({
message: error?.data?.url?.error ?? "The link could not be updated",
message: error?.data?.url?.error ?? t("links.toasts.not_updated.message"),
type: TOAST_TYPE.ERROR,
title: "Link not updated",
title: t("links.toasts.not_updated.title"),
});
throw error;
}
@ -59,15 +62,15 @@ export const useLinkOperations = (
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await removeLink(workspaceSlug, projectId, issueId, linkId);
setToast({
message: "The link has been successfully removed",
message: t("links.toasts.removed.message"),
type: TOAST_TYPE.SUCCESS,
title: "Link removed",
title: t("links.toasts.removed.title"),
});
} catch (error) {
setToast({
message: "The link could not be removed",
message: t("links.toasts.not_removed.message"),
type: TOAST_TYPE.ERROR,
title: "Link not removed",
title: t("links.toasts.not_removed.title"),
});
}
},

View file

@ -2,6 +2,7 @@
import React, { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueServiceType } from "@plane/types";
import { CollapsibleButton } from "@plane/ui";
// components
@ -18,6 +19,7 @@ type Props = {
export const IssueLinksCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
const { t } = useTranslation();
// store hooks
const {
issue: { getIssueById },
@ -41,7 +43,7 @@ export const IssueLinksCollapsibleTitle: FC<Props> = observer((props) => {
return (
<CollapsibleButton
isOpen={isOpen}
title="Links"
title={t("common.links")}
indicatorElement={indicatorElement}
actionItemElement={
!disabled && <IssueLinksActionButton issueServiceType={issueServiceType} disabled={disabled} />

View file

@ -3,6 +3,7 @@ import { FC, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssue, TIssueServiceType } from "@plane/types";
import { Collapsible } from "@plane/ui";
// components
@ -29,7 +30,7 @@ type TIssueCrudState = { toggle: boolean; issueId: string | undefined; issue: TI
export type TRelationObject = {
key: TIssueRelationTypes;
label: string;
i18n_label: string;
className: string;
icon: (size: number) => React.ReactElement;
placeholder: string;
@ -37,6 +38,8 @@ export type TRelationObject = {
export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
const { workspaceSlug, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// plane hooks
const { t } = useTranslation();
// state
const [issueCrudState, setIssueCrudState] = useState<{
update: TIssueCrudState;
@ -93,7 +96,7 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
relationKey: relationKey,
issueIds: issueIds,
icon: issueRelationOption?.icon,
label: issueRelationOption?.label,
label: issueRelationOption?.i18n_label ? t(issueRelationOption?.i18n_label) : "",
className: issueRelationOption?.className,
};
});

View file

@ -1,11 +1,11 @@
"use client";
import { useMemo } from "react";
import { usePathname } from "next/navigation";
import { EIssueServiceType } from "@plane/constants";
import { EIssueServiceType, ISSUE_DELETED, ISSUE_UPDATED } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssue, TIssueServiceType } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// constants
import { ISSUE_DELETED, ISSUE_UPDATED } from "@/constants/event-tracker";
// helper
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
@ -23,8 +23,9 @@ export const useRelationOperations = (
const { updateIssue, removeIssue } = useIssueDetail(issueServiceType);
const { captureIssueEvent } = useEventTracker();
const pathname = usePathname();
const { t } = useTranslation();
// derived values
const entityName = issueServiceType === EIssueServiceType.ISSUES ? "Issue" : "Epic";
const entityName = issueServiceType === EIssueServiceType.ISSUES ? "Work item" : "Epic";
const issueOperations: TRelationIssueOperations = useMemo(
() => ({
@ -33,8 +34,8 @@ export const useRelationOperations = (
copyTextToClipboard(`${originURL}/${text}`).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Link Copied!",
message: `${entityName} link copied to clipboard.`,
title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", { entity: entityName }),
});
});
},
@ -51,9 +52,9 @@ export const useRelationOperations = (
path: pathname,
});
setToast({
title: "Success!",
title: t("toast.success"),
type: TOAST_TYPE.SUCCESS,
message: `${entityName} updated successfully`,
message: t("entity.update.success", { entity: entityName }),
});
} catch (error) {
captureIssueEvent({
@ -66,9 +67,9 @@ export const useRelationOperations = (
path: pathname,
});
setToast({
title: "Error!",
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: `${entityName} update failed`,
message: t("entity.update.failed", { entity: entityName }),
});
}
},

View file

@ -3,6 +3,7 @@ import React, { FC } from "react";
import { observer } from "mobx-react";
import { Plus } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueServiceType } from "@plane/types";
import { CustomMenu } from "@plane/ui";
// hooks
@ -20,6 +21,7 @@ type Props = {
export const RelationActionButton: FC<Props> = observer((props) => {
const { customButton, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
const { t } = useTranslation();
// store hooks
const { toggleRelationModal, setRelationKey } = useIssueDetail(issueServiceType);
@ -56,7 +58,7 @@ export const RelationActionButton: FC<Props> = observer((props) => {
>
<div className="flex items-center gap-2">
{item.icon(12)}
<span>{item.label}</span>
<span>{t(item.i18n_label)}</span>
</div>
</CustomMenu.MenuItem>
);

View file

@ -2,6 +2,7 @@
import React, { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueServiceType } from "@plane/types";
import { CollapsibleButton } from "@plane/ui";
// components
@ -20,6 +21,7 @@ type Props = {
export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
const { t } = useTranslation();
// store hook
const {
relation: { getRelationCountByIssueId },
@ -42,7 +44,7 @@ export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
return (
<CollapsibleButton
isOpen={isOpen}
title="Relations"
title={t("common.relations")}
indicatorElement={indicatorElement}
actionItemElement={
!disabled && <RelationActionButton issueId={issueId} disabled={disabled} issueServiceType={issueServiceType} />

View file

@ -85,7 +85,7 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, parentIssueId);
setSubIssueHelpers(`${parentIssueId}_root`, "issue_visibility", parentIssueId);
} catch (error) {
console.error("Error fetching sub-issues:", error);
console.error("Error fetching sub-work items:", error);
} finally {
setSubIssueHelpers(`${parentIssueId}_root`, "preview_loader", "");
}

View file

@ -2,6 +2,7 @@
import { useMemo } from "react";
import { useParams, usePathname } from "next/navigation";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssue, TIssueServiceType } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// helper
@ -25,6 +26,7 @@ export const useSubIssueOperations = (
// router
const { epicId: epicIdParam } = useParams();
const pathname = usePathname();
const { t } = useTranslation();
// store hooks
const {
issue: { getIssueById },
@ -51,8 +53,13 @@ export const useSubIssueOperations = (
copyTextToClipboard(`${originURL}/${text}`).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Link Copied!",
message: `${issueServiceType === EIssueServiceType.ISSUES ? "Issue" : "Epic"} link copied to clipboard`,
title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", {
entity:
issueServiceType === EIssueServiceType.ISSUES
? t("issue.label", { count: 1 })
: t("epic.label", { count: 1 }),
}),
});
});
},
@ -62,8 +69,13 @@ export const useSubIssueOperations = (
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: `Error fetching ${issueServiceType === EIssueServiceType.ISSUES ? "sub-issues" : "issues"}`,
title: t("toast.error"),
message: t("entity.fetch.failed", {
entity:
issueServiceType === EIssueServiceType.ISSUES
? t("common.sub_work_items", { count: 2 })
: t("issue.label", { count: 2 }),
}),
});
}
},
@ -72,14 +84,25 @@ export const useSubIssueOperations = (
await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: `${issueServiceType === EIssueServiceType.ISSUES ? "Sub-issues" : "Issues"} added successfully`,
title: t("toast.success"),
message: t("entity.add.success", {
entity:
issueServiceType === EIssueServiceType.ISSUES
? t("common.sub_work_items")
: t("issue.label", { count: 2 }),
}),
});
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: `Error adding ${issueServiceType === EIssueServiceType.ISSUES ? "sub-issues" : "issues"}`,
title: t("toast.error"),
// message: `Error adding ${issueServiceType === EIssueServiceType.ISSUES ? "sub-issues" : "issues"}`,
message: t("entity.add.failed", {
entity:
issueServiceType === EIssueServiceType.ISSUES
? t("common.sub_work_items")
: t("issue.label", { count: 2 }),
}),
});
}
},
@ -130,8 +153,8 @@ export const useSubIssueOperations = (
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Sub-issue updated successfully",
title: t("toast.success"),
message: t("sub_work_item.update.success"),
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch (error) {
@ -146,8 +169,8 @@ export const useSubIssueOperations = (
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Error updating sub-issue",
title: t("toast.error"),
message: t("sub_work_item.update.error"),
});
}
},
@ -167,8 +190,8 @@ export const useSubIssueOperations = (
}
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Sub-issue removed successfully",
title: t("toast.success"),
message: t("sub_work_item.remove.success"),
});
captureIssueEvent({
eventName: "Sub-issue removed",
@ -192,8 +215,8 @@ export const useSubIssueOperations = (
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Error removing sub-issue",
title: t("toast.error"),
message: t("sub_work_item.remove.error"),
});
}
},
@ -216,8 +239,8 @@ export const useSubIssueOperations = (
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Error deleting issue",
title: t("toast.error"),
message: t("issue.delete.error"),
});
}
},

View file

@ -3,6 +3,7 @@ import React, { FC } from "react";
import { observer } from "mobx-react";
import { LayersIcon, Plus } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssue, TIssueServiceType } from "@plane/types";
import { CustomMenu } from "@plane/ui";
// hooks
@ -17,6 +18,7 @@ type Props = {
export const SubIssuesActionButton: FC<Props> = observer((props) => {
const { issueId, customButton, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
const { t } = useTranslation();
// store hooks
const {
issue: { getIssueById },
@ -63,12 +65,12 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
// options
const optionItems = [
{
label: "Create new",
i18n_label: "common.create_new",
icon: <Plus className="h-3 w-3" />,
onClick: handleCreateNew,
},
{
label: "Add existing",
i18n_label: "common.add_existing",
icon: <LayersIcon className="h-3 w-3" />,
onClick: handleAddExisting,
},
@ -90,7 +92,7 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
>
<div className="flex items-center gap-2">
{item.icon}
<span>{item.label}</span>
<span>{t(item.i18n_label)}</span>
</div>
</CustomMenu.MenuItem>
))}

View file

@ -2,6 +2,7 @@
import React, { FC } from "react";
import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueServiceType } from "@plane/types";
import { CircularProgressIndicator, CollapsibleButton } from "@plane/ui";
// components
@ -18,6 +19,7 @@ type Props = {
export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, parentIssueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
const { t } = useTranslation();
// store hooks
const {
subIssues: { subIssuesByIssueId, stateDistributionByIssueId },
@ -38,12 +40,12 @@ export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
return (
<CollapsibleButton
isOpen={isOpen}
title={`${issueServiceType === EIssueServiceType.EPICS ? "Issues" : "Sub-issues"}`}
title={`${issueServiceType === EIssueServiceType.EPICS ? t("issue.label", { count: 1 }) : t("common.sub_work_items")}`}
indicatorElement={
<div className="flex items-center gap-1.5 text-custom-text-300 text-sm">
<CircularProgressIndicator size={18} percentage={percentage} strokeWidth={3} />
<span>
{completedCount}/{totalCount} Done
{completedCount}/{totalCount} {t("common.done")}
</span>
</div>
}

View file

@ -1,5 +1,6 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
// hooks
// components
import { CycleDropdown } from "@/components/dropdowns";
@ -21,6 +22,7 @@ type TIssueCycleSelect = {
export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) => {
const { className = "", workspaceSlug, projectId, issueId, issueOperations, disabled = false } = props;
const { t } = useTranslation();
// states
const [isUpdating, setIsUpdating] = useState(false);
// store hooks
@ -50,7 +52,7 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
className="group w-full"
buttonContainerClassName="w-full text-left rounded"
buttonClassName={`text-sm justify-between ${issue?.cycle_id ? "" : "text-custom-text-400"}`}
placeholder="No cycle"
placeholder={t("cycle.no_cycle")}
hideIcon
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"

View file

@ -1,14 +1,12 @@
import { FC } from "react";
import { observer } from "mobx-react";
// constants
import { E_SORT_ORDER } from "@plane/constants";
import { E_SORT_ORDER, TActivityFilters, filterActivityOnSelectedFilters } from "@plane/constants";
// hooks
import { useIssueDetail } from "@/hooks/store";
// plane web components
import { IssueAdditionalPropertiesActivity } from "@/plane-web/components/issues";
import { IssueActivityWorklog } from "@/plane-web/components/issues/worklog/activity/root";
// plane web constants
import { TActivityFilters, filterActivityOnSelectedFilters } from "@/plane-web/constants/issues";
// components
import { IssueActivityItem } from "./activity/activity-list";
import { IssueCommentCard } from "./comments/comment-card";

View file

@ -1,11 +1,12 @@
import React, { FC } from "react";
import { observer } from "mobx-react";
import { Check, ListFilter } from "lucide-react";
import { TActivityFilters, TActivityFilterOption } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, PopoverMenu } from "@plane/ui";
// helper
import { cn } from "@/helpers/common.helper";
// constants
import { TActivityFilterOption, TActivityFilters } from "@/plane-web/constants/issues";
type TActivityFilter = {
selectedFilters: TActivityFilters[];
@ -15,6 +16,9 @@ type TActivityFilter = {
export const ActivityFilter: FC<TActivityFilter> = observer((props) => {
const { selectedFilters = [], filterOptions } = props;
// hooks
const { t } = useTranslation();
return (
<PopoverMenu
buttonClassName="outline-none"
@ -25,7 +29,7 @@ export const ActivityFilter: FC<TActivityFilter> = observer((props) => {
prependIcon={<ListFilter className="h-3 w-3" />}
className="relative"
>
<span className="text-custom-text-200">Filters</span>
<span className="text-custom-text-200">{t("common.filters")}</span>
{selectedFilters.length < filterOptions.length && (
<span className="absolute h-2 w-2 -right-0.5 -top-0.5 bg-custom-primary-100 rounded-full" />
)}
@ -53,7 +57,7 @@ export const ActivityFilter: FC<TActivityFilter> = observer((props) => {
{item.isSelected && <Check className="h-2.5 w-2.5" />}
</div>
<div className={cn("whitespace-nowrap", item.isSelected ? "text-custom-text-100" : "text-custom-text-200")}>
{item.label}
{t(item.labelTranslationKey)}
</div>
</div>
)}

View file

@ -36,7 +36,7 @@ export const IssueArchivedAtActivity: FC<TIssueArchivedAtActivity> = observer((p
ends={ends}
customUserName={activity.new_value === "archive" ? "Plane" : undefined}
>
{activity.new_value === "restore" ? "restored the issue" : "archived the issue"}.
{activity.new_value === "restore" ? "restored the work item" : "archived the work item"}.
</IssueActivityBlockComponent>
);
});

View file

@ -30,7 +30,7 @@ export const IssueCycleActivity: FC<TIssueCycleActivity> = observer((props) => {
<>
{activity.verb === "created" ? (
<>
<span>added this issue to the cycle </span>
<span>added this work item to the cycle </span>
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
@ -54,7 +54,7 @@ export const IssueCycleActivity: FC<TIssueCycleActivity> = observer((props) => {
</>
) : (
<>
<span>removed the issue from the cycle </span>
<span>removed the work item from the cycle </span>
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
target="_blank"

View file

@ -27,7 +27,7 @@ export const IssueDefaultActivity: FC<TIssueDefaultActivity> = observer((props)
icon={<LayersIcon width={14} height={14} className="text-custom-text-200" aria-hidden="true" />}
ends={ends}
>
<>{activity.verb === "created" ? " created the issue." : " deleted an issue."}</>
<>{activity.verb === "created" ? " created the work item." : " deleted an work item."}</>
</IssueActivityBlockComponent>
);
});

View file

@ -23,7 +23,7 @@ export const IssueLink: FC<TIssueLink> = (props) => {
if (!activity) return <></>;
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}
>
<a
@ -37,7 +37,9 @@ export const IssueLink: FC<TIssueLink> = (props) => {
rel={activity.issue === null ? "" : "noopener noreferrer"}
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
>
{activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}{" "}
{activity.issue_detail
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
: "Work items"}{" "}
<span className="font-normal">{activity.issue_detail?.name}</span>
</a>
</Tooltip>

View file

@ -21,15 +21,15 @@ export const IssueInboxActivity: FC<TIssueInboxActivity> = observer((props) => {
const getInboxActivityMessage = () => {
switch (activity?.verb) {
case "-1":
return "declined this issue from intake.";
return "declined this work item from intake.";
case "0":
return "snoozed this issue.";
return "snoozed this work item.";
case "1":
return "accepted this issue from intake.";
return "accepted this work item from intake.";
case "2":
return "declined this issue from intake by marking a duplicate issue.";
return "declined this work item from intake by marking a duplicate work item.";
default:
return "updated intake issue status.";
return "updated intake work item status.";
}
};

View file

@ -30,7 +30,7 @@ export const IssueModuleActivity: FC<TIssueModuleActivity> = observer((props) =>
<>
{activity.verb === "created" ? (
<>
<span>added this issue to the module </span>
<span>added this work item to the module </span>
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank"
@ -54,7 +54,7 @@ export const IssueModuleActivity: FC<TIssueModuleActivity> = observer((props) =>
</>
) : (
<>
<span>removed the issue from the module </span>
<span>removed the work item from the module </span>
<a
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/modules/${activity.old_identifier}`}
target="_blank"

View file

@ -8,6 +8,8 @@ import { Check, Globe2, Lock, Pencil, Trash2, X } from "lucide-react";
import { EIssueCommentAccessSpecifier } from "@plane/constants";
// plane editor
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
// plane i18n
import { useTranslation } from "@plane/i18n";
// plane types
import { TIssueComment } from "@plane/types";
// plane ui
@ -47,6 +49,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
showAccessSpecifier = false,
disabled = false,
} = props;
const { t } = useTranslation();
// states
const [isEditing, setIsEditing] = useState(false);
// refs
@ -104,7 +107,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
<CustomMenu ellipsis closeOnSelect>
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
<Pencil className="flex-shrink-0 size-3" />
Edit
{t("common.actions.edit")}
</CustomMenu.MenuItem>
{showAccessSpecifier && (
<>
@ -116,7 +119,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
className="flex items-center gap-1"
>
<Globe2 className="flex-shrink-0 size-3" />
Switch to public comment
{t("issue.comments.switch.public")}
</CustomMenu.MenuItem>
) : (
<CustomMenu.MenuItem
@ -126,7 +129,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
className="flex items-center gap-1"
>
<Lock className="flex-shrink-0 size-3" />
Switch to private comment
{t("issue.comments.switch.private")}
</CustomMenu.MenuItem>
)}
</>
@ -136,7 +139,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
className="flex items-center gap-1"
>
<Trash2 className="flex-shrink-0 size-3" />
Delete
{t("common.actions.delete")}
</CustomMenu.MenuItem>
</CustomMenu>
)}

View file

@ -1,15 +1,15 @@
import { FC } from "react";
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { EmptyState } from "@/components/empty-state";
// hooks
import { EmptyStateType } from "@/constants/empty-state";
import { SimpleEmptyState } from "@/components/empty-state";
// hooks
import { useIssueDetail } from "@/hooks/store";
// components
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// local components
import { TActivityOperations } from "../root";
import { IssueCommentCard } from "./comment-card";
// types
type TIssueCommentRoot = {
projectId: string;
@ -26,6 +26,9 @@ export const IssueCommentRoot: FC<TIssueCommentRoot> = observer((props) => {
const {
comment: { getCommentsByIssueId },
} = useIssueDetail();
const { t } = useTranslation();
// derived values
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/comments" });
const commentIds = getCommentsByIssueId(issueId);
if (!commentIds) return <></>;
@ -48,7 +51,11 @@ export const IssueCommentRoot: FC<TIssueCommentRoot> = observer((props) => {
))
) : (
<div className="flex items-center justify-center py-9">
<EmptyState type={EmptyStateType.ISSUE_COMMENT_EMPTY_STATE} layout="screen-simple" />
<SimpleEmptyState
title={t("issue_comment.empty_state.general.title")}
description={t("issue_comment.empty_state.general.description")}
assetPath={resolvedPath}
/>
</div>
)}
</div>

View file

@ -3,8 +3,11 @@
import { FC, useMemo } from "react";
import { observer } from "mobx-react";
// plane package imports
import { E_SORT_ORDER } from "@plane/constants";
import { E_SORT_ORDER, TActivityFilters, defaultActivityFilters,EUserPermissions } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
// i18n
import { useTranslation } from "@plane/i18n";
//types
import { TFileSignedURLResponse, TIssueComment } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums";
import { TOAST_TYPE, setToast } from "@plane/ui";
@ -16,9 +19,6 @@ import { ActivitySortRoot, IssueActivityCommentRoot } from "@/components/issues/
import { useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
// plane web components
import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog";
// plane web constants
import { TActivityFilters, defaultActivityFilters } from "@/plane-web/constants/issues";
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
@ -40,6 +40,8 @@ export type TActivityOperations = {
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false, isIntakeIssue = false } = props;
// i18n
const { t } = useTranslation();
// hooks
const { setValue: setFilterValue, storedValue: selectedFilters } = useLocalStorage(
"issue_activity_filters",
@ -87,16 +89,16 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
const comment = await createComment(workspaceSlug, projectId, issueId, data);
setToast({
title: "Success!",
title: t("common.success"),
type: TOAST_TYPE.SUCCESS,
message: "Comment created successfully.",
message: t("issue.comments.create.success"),
});
return comment;
} catch (error) {
setToast({
title: "Error!",
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
message: "Comment creation failed. Please try again later.",
message: t("issue.comments.create.error"),
});
}
},
@ -105,15 +107,15 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
await updateComment(workspaceSlug, projectId, issueId, commentId, data);
setToast({
title: "Success!",
title: t("common.success"),
type: TOAST_TYPE.SUCCESS,
message: "Comment updated successfully.",
message: t("issue.comments.update.success"),
});
} catch (error) {
setToast({
title: "Error!",
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
message: "Comment update failed. Please try again later.",
message: t("issue.comments.update.error"),
});
}
},
@ -122,15 +124,15 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
await removeComment(workspaceSlug, projectId, issueId, commentId);
setToast({
title: "Success!",
title: t("common.success"),
type: TOAST_TYPE.SUCCESS,
message: "Comment removed successfully.",
message: t("issue.comments.remove.success"),
});
} catch (error) {
setToast({
title: "Error!",
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
message: "Comment remove failed. Please try again later.",
message: t("issue.comments.remove.error"),
});
}
},
@ -149,7 +151,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
return res;
} catch (error) {
console.log("Error in uploading comment asset:", error);
throw new Error("Asset upload failed. Please try again later.");
throw new Error(t("issue.comments.upload.error"));
}
},
}),
@ -163,7 +165,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
<div className="space-y-4 pt-3">
{/* header */}
<div className="flex items-center justify-between">
<div className="text-lg text-custom-text-100">Activity</div>
<div className="text-lg text-custom-text-100">{t("common.activity")}</div>
<div className="flex items-center gap-2">
{isWorklogButtonEnabled && (
<IssueActivityWorklogCreateButton

View file

@ -4,13 +4,18 @@ import React, { FC, useState } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { ArchiveIcon, ArchiveRestoreIcon, LinkIcon, Trash2 } from "lucide-react";
import { EIssuesStoreType } from "@plane/constants";
import {
ISSUE_ARCHIVED,
ISSUE_DELETED,
ARCHIVABLE_STATE_GROUPS,
EIssuesStoreType,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// components
import { ArchiveIssueModal, DeleteIssueModal, IssueSubscription } from "@/components/issues";
// constants
import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker";
import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
// helpers
import { cn } from "@/helpers/common.helper";
import { copyTextToClipboard } from "@/helpers/string.helper";
@ -25,7 +30,6 @@ import {
} from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type Props = {
workspaceSlug: string;
@ -35,7 +39,7 @@ type Props = {
export const IssueDetailQuickActions: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId } = props;
const { t } = useTranslation();
// states
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
@ -75,8 +79,8 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Link Copied!",
message: "Issue link copied to clipboard.",
title: t("common.link_copied"),
message: t("common.copied_to_clipboard"),
});
});
};
@ -92,19 +96,19 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
router.push(redirectionPath);
captureIssueEvent({
eventName: ISSUE_DELETED,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
payload: { id: issueId, state: "SUCCESS", element: "Work item detail page" },
path: pathname,
});
});
} catch (error) {
setToast({
title: "Error!",
title: t("toast.error "),
type: TOAST_TYPE.ERROR,
message: "Issue delete failed",
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
captureIssueEvent({
eventName: ISSUE_DELETED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
payload: { id: issueId, state: "FAILED", element: "Work item detail page" },
path: pathname,
});
}
@ -138,16 +142,16 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Restore success",
message: "Your issue can be found in project issues.",
title: t("issue.restore.success.title"),
message: t("issue.restore.success.message"),
});
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`);
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be restored. Please try again.",
title: t("toast.error"),
message: t("issue.restore.failed.message"),
});
})
.finally(() => setIsRestoring(false));
@ -182,7 +186,7 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
)}
<div className="flex flex-wrap items-center gap-2.5 text-custom-text-300">
<Tooltip tooltipContent="Copy link" isMobile={isMobile}>
<Tooltip tooltipContent={t("common.actions.copy_link")} isMobile={isMobile}>
<button
type="button"
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"
@ -215,9 +219,7 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
{isArchivingAllowed && (
<Tooltip
isMobile={isMobile}
tooltipContent={
isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"
}
tooltipContent={isInArchivableGroup ? t("common.actions.archive") : t("issue.archive.description")}
>
<button
type="button"
@ -241,7 +243,7 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
)}
{isEditable && (
<Tooltip tooltipContent="Delete" isMobile={isMobile}>
<Tooltip tooltipContent={t("common.actions.delete")} isMobile={isMobile}>
<button
type="button"
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"

View file

@ -3,6 +3,7 @@
import { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueLabel, TIssue, TIssueServiceType } from "@plane/types";
// components
import { TOAST_TYPE, setToast } from "@plane/ui";
@ -39,6 +40,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
onLabelUpdate,
issueServiceType = EIssueServiceType.ISSUES,
} = props;
const { t } = useTranslation();
// hooks
const { updateIssue } = useIssueDetail(issueServiceType);
const { createLabel } = useLabel();
@ -57,9 +59,9 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
else await updateIssue(workspaceSlug, projectId, issueId, data);
} catch (error) {
setToast({
title: "Error!",
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: "Issue update failed",
message: t("entity.update.failed", { entity: t("issue.label", { count: 1 }) }),
});
}
},
@ -68,18 +70,18 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
const labelResponse = await createLabel(workspaceSlug, projectId, data);
if (!isInboxIssue)
setToast({
title: "Success!",
title: t("toast.success"),
type: TOAST_TYPE.SUCCESS,
message: "Label created successfully",
message: t("label.create.success"),
});
return labelResponse;
} catch (error) {
let errMessage = "Label creation failed";
let errMessage = t("label.create.failed");
if (error && (error as any).error === "Label with the same name already exists in the project")
errMessage = "Label already exists";
errMessage = t("label.create.already_exists");
setToast({
title: "Error!",
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: errMessage,
});

View file

@ -3,14 +3,15 @@ import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Check, Loader, Search, Tag } from "lucide-react";
import { Combobox } from "@headlessui/react";
// helpers
// plane imports
import { EUserPermissionsLevel, EUserProjectRoles, getRandomLabelColor } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueLabel } from "@plane/types";
import { getRandomLabelColor } from "@/constants/label";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useLabel, useUserPermissions } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
//constants
export interface IIssueLabelSelect {
workspaceSlug: string;
@ -23,6 +24,7 @@ export interface IIssueLabelSelect {
export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) => {
const { workspaceSlug, projectId, issueId, values, onSelect, onAddLabel } = props;
const { t } = useTranslation();
// store hooks
const { isMobile } = usePlatformOS();
const { fetchProjectLabels, getProjectLabels } = useLabel();
@ -34,7 +36,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
const [query, setQuery] = useState("");
const [submitting, setSubmitting] = useState<boolean>(false);
const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
const canCreateLabel = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const projectLabels = getProjectLabels(projectId);
@ -86,7 +88,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
<div className="flex-shrink-0">
<Tag className="h-2.5 w-2.5" />
</div>
<div className="flex-shrink-0">Select Label</div>
<div className="flex-shrink-0">{t("label.select")}</div>
</div>
);
@ -147,7 +149,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
className="w-full bg-transparent px-2 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}
tabIndex={baseTabIndex}
@ -156,7 +158,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
</div>
<div className={`vertical-scrollbar scrollbar-sm mt-2 max-h-48 space-y-1 overflow-y-scroll px-2 pr-0`}>
{isLoading ? (
<p className="text-center text-custom-text-200">Loading...</p>
<p className="text-center text-custom-text-200">{t("common.loading")}</p>
) : filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
@ -195,14 +197,15 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
>
{query.length ? (
<>
+ Add <span className="text-custom-text-100">&quot;{query}&quot;</span> to labels
{/* TODO: Translate here */}+ Add{" "}
<span className="text-custom-text-100">&quot;{query}&quot;</span> to labels
</>
) : (
"Type to add a new label"
t("label.create.type")
)}
</Combobox.Option>
) : (
<p className="text-left text-custom-text-200 ">No matching results.</p>
<p className="text-left text-custom-text-200 ">{t("common.search.no_matching_results")}</p>
)}
</div>
</div>

View file

@ -4,6 +4,7 @@ import { FC, useEffect } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// plane types
import type { TIssueLinkEditableFields, TIssueServiceType } from "@plane/types";
// plane ui
@ -32,6 +33,8 @@ const defaultValues: TIssueLinkCreateFormFieldOptions = {
};
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observer((props) => {
// i18n
const { t } = useTranslation();
// props
const { isModalOpen, handleOnClose, linkOperations, issueServiceType = EIssueServiceType.ISSUES } = props;
// react hook form
@ -70,11 +73,13 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
<ModalCore isOpen={isModalOpen} handleClose={onClose}>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-5 p-5">
<h3 className="text-xl font-medium text-custom-text-200">{preloadedData?.id ? "Update" : "Add"} link</h3>
<h3 className="text-xl font-medium text-custom-text-200">
{preloadedData?.id ? t("common.update_link") : t("common.add_link")}
</h3>
<div className="mt-2 space-y-3">
<div>
<label htmlFor="url" className="mb-2 text-custom-text-200">
URL
{t("common.url")}
</label>
<Controller
control={control}
@ -90,17 +95,17 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
onChange={onChange}
ref={ref}
hasError={Boolean(errors.url)}
placeholder="Type or paste a URL"
placeholder={t("common.type_or_paste_a_url")}
className="w-full"
/>
)}
/>
{errors.url && <span className="text-xs text-red-500">URL is invalid</span>}
{errors.url && <span className="text-xs text-red-500">{t("common.url_is_invalid")}</span>}
</div>
<div>
<label htmlFor="title" className="mb-2 text-custom-text-200">
Display title
<span className="text-[10px] block">Optional</span>
{t("common.display_title")}
<span className="text-[10px] block">{t("common.optional")}</span>
</label>
<Controller
control={control}
@ -113,7 +118,7 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
onChange={onChange}
ref={ref}
hasError={Boolean(errors.title)}
placeholder="What you'd like to see this link as"
placeholder={t("common.link_title_placeholder")}
className="w-full"
/>
)}
@ -123,10 +128,18 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
</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={onClose}>
Cancel
{t("common.cancel")}
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{preloadedData?.id ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"} link
{`${
preloadedData?.id
? isSubmitting
? t("common.updating")
: t("common.update")
: isSubmitting
? t("common.adding")
: t("common.add")
} ${t("common.link")}`}
</Button>
</div>
</form>

View file

@ -4,6 +4,7 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { Pencil, Trash2, LinkIcon, Copy } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueServiceType } from "@plane/types";
// ui
import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
@ -26,6 +27,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
// props
const { linkId, linkOperations, isNotAllowed, issueServiceType = EIssueServiceType.ISSUES } = props;
// hooks
const { t } = useTranslation();
const {
toggleIssueLinkModal: toggleIssueLinkModalStore,
setIssueLinkData,
@ -67,8 +69,8 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
copyTextToClipboard(linkDetail.url);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Link copied!",
message: "Link copied to clipboard",
title: t("common.link_copied"),
message: t("common.link_copied_to_clipboard"),
});
}}
className="relative grid place-items-center rounded p-1 text-custom-text-400 outline-none group-hover:text-custom-text-200 cursor-pointer hover:bg-custom-background-80"
@ -91,7 +93,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
}}
>
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
Edit
{t("common.actions.edit")}
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
className="flex items-center gap-2"
@ -102,7 +104,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
}}
>
<Trash2 className="h-3 w-3" />
Delete
{t("common.actions.delete")}
</CustomMenu.MenuItem>
</CustomMenu>
</div>

View file

@ -1,6 +1,7 @@
import React, { useState } from "react";
import xor from "lodash/xor";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
// hooks
// components
import { ModuleDropdown } from "@/components/dropdowns";
@ -22,6 +23,7 @@ type TIssueModuleSelect = {
export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props) => {
const { className = "", workspaceSlug, projectId, issueId, issueOperations, disabled = false } = props;
const { t } = useTranslation();
// states
const [isUpdating, setIsUpdating] = useState(false);
// store hooks
@ -59,7 +61,7 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
projectId={projectId}
value={issue?.module_ids ?? []}
onChange={handleIssueModuleChange}
placeholder="No module"
placeholder={t("module.no_module")}
disabled={disableSelect}
className="group h-full w-full"
buttonContainerClassName="w-full rounded"

View file

@ -4,6 +4,7 @@ import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { Pencil, X } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// ui
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// components
@ -29,6 +30,7 @@ type TIssueParentSelect = {
export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props) => {
const { className = "", disabled = false, issueId, issueOperations, projectId, workspaceSlug } = props;
const { t } = useTranslation();
// store hooks
const { getProjectById } = useProject();
const {
@ -72,8 +74,8 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Something went wrong",
title: t("common.error.label"),
message: t("common.something_went_wrong"),
});
}
};
@ -126,7 +128,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
</Tooltip>
{!disabled && (
<Tooltip tooltipContent="Remove" position="bottom" isMobile={isMobile}>
<Tooltip tooltipContent={t("common.remove")} position="bottom" isMobile={isMobile}>
<span
onClick={(e) => {
e.preventDefault();
@ -140,7 +142,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
)}
</div>
) : (
<span className="text-sm text-custom-text-400">Add parent issue</span>
<span className="text-sm text-custom-text-400">{t("issue.add.parent")}</span>
)}
{!disabled && (
<span

View file

@ -3,6 +3,7 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { MinusCircle } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { TIssue } from "@plane/types";
// component
// ui
@ -27,6 +28,7 @@ export type TIssueParentDetail = {
export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
const { workspaceSlug, projectId, issueId, issue, issueOperations } = props;
const { t } = useTranslation();
// hooks
const { issueMap } = useIssues();
const { getProjectStates } = useProjectState();
@ -66,7 +68,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
<CustomMenu ellipsis optionsClassName="p-1.5">
<div className="border-b border-custom-border-300 text-xs font-medium text-custom-text-200">
Sibling issues
{t("issue.sibling.label")}
</div>
<IssueParentSiblings workspaceSlug={workspaceSlug} currentIssue={issue} parentIssue={parentIssue} />
@ -76,7 +78,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
className="flex items-center gap-2 py-2 text-red-500"
>
<MinusCircle className="h-4 w-4" />
<span> Remove Parent Issue</span>
<span>{t("issue.remove.parent.label")}</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>

View file

@ -48,7 +48,7 @@ export const IssueParentSiblings: FC<TIssueParentSiblings> = observer((props) =>
)
) : (
<div className="flex items-center gap-2 whitespace-nowrap px-1 py-1 text-left text-xs text-custom-text-200">
No sibling issues
No sibling work items
</div>
)}
</div>

View file

@ -50,7 +50,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Please select at least one issue.",
message: "Please select at least one work item.",
});
return;
}

View file

@ -4,7 +4,8 @@ import { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
// types
import { EIssuesStoreType } from "@plane/constants";
import { EIssuesStoreType, ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED,EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssue } from "@plane/types";
// ui
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
@ -12,11 +13,9 @@ import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
import { EmptyState } from "@/components/common";
import { IssueDetailsSidebar, IssuePeekOverview } from "@/components/issues";
// constants
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "@/constants/event-tracker";
// hooks
import { useAppTheme, useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// images
import emptyIssue from "@/public/empty-state/issue.svg";
// local components
@ -54,6 +53,7 @@ export type TIssueDetailRoot = {
};
export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
const { t } = useTranslation();
const { workspaceSlug, projectId, issueId, is_archived = false } = props;
// router
const router = useAppRouter();
@ -111,9 +111,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
path: pathname,
});
setToast({
title: "Error!",
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
message: "Issue update failed",
message: t("entity.update.failed", { entity: t("issue.label") }),
});
}
},
@ -122,9 +122,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId);
else await removeIssue(workspaceSlug, projectId, issueId);
setToast({
title: "Success!",
title: t("common.success"),
type: TOAST_TYPE.SUCCESS,
message: "Issue deleted successfully",
message: t("entity.delete.success", { entity: t("issue.label") }),
});
captureIssueEvent({
eventName: ISSUE_DELETED,
@ -134,9 +134,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
} catch (error) {
console.log("Error in deleting issue:", error);
setToast({
title: "Error!",
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
message: "Issue delete failed",
message: t("entity.delete.failed", { entity: t("issue.label") }),
});
captureIssueEvent({
eventName: ISSUE_DELETED,
@ -177,8 +177,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be added to the cycle. Please try again.",
title: t("common.error.label"),
message: t("issue.add.cycle.failed"),
});
captureIssueEvent({
eventName: ISSUE_UPDATED,
@ -206,8 +206,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be added to the cycle. Please try again.",
title: t("common.error.label"),
message: t("issue.add.cycle.failed"),
});
captureIssueEvent({
eventName: ISSUE_UPDATED,
@ -224,14 +224,14 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
try {
const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
setPromiseToast(removeFromCyclePromise, {
loading: "Removing issue from the cycle...",
loading: t("issue.remove.cycle.loading"),
success: {
title: "Success!",
message: () => "Issue removed from the cycle successfully.",
title: t("common.success"),
message: () => t("issue.remove.cycle.success"),
},
error: {
title: "Error!",
message: () => "Issue could not be removed from the cycle. Please try again.",
title: t("common.error.label"),
message: () => t("issue.remove.cycle.failed"),
},
});
await removeFromCyclePromise;
@ -260,14 +260,14 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
try {
const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
setPromiseToast(removeFromModulePromise, {
loading: "Removing issue from the module...",
loading: t("issue.remove.module.loading"),
success: {
title: "Success!",
message: () => "Issue removed from the module successfully.",
title: t("common.success"),
message: () => t("issue.remove.module.success"),
},
error: {
title: "Error!",
message: () => "Issue could not be removed from the module. Please try again.",
title: t("common.error.label"),
message: () => t("issue.remove.module.failed"),
},
});
await removeFromModulePromise;
@ -339,10 +339,10 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
{!issue ? (
<EmptyState
image={emptyIssue}
title="Issue does not exist"
description="The issue you are looking for does not exist, has been archived, or has been deleted."
title={t("issue.empty_state.issue_detail.title")}
description={t("issue.empty_state.issue_detail.description")}
primaryButton={{
text: "View other issues",
text: t("issue.empty_state.issue_detail.primary_button.text"),
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`),
}}
/>

View file

@ -3,6 +3,8 @@
import React from "react";
import { observer } from "mobx-react";
import { CalendarCheck2, CalendarClock, LayoutPanelTop, Signal, Tag, Triangle, UserCircle2, Users } from "lucide-react";
// i18n
import { useTranslation } from "@plane/i18n";
// ui
import { ContrastIcon, DiceIcon, DoubleCircleIcon } from "@plane/ui";
// components
@ -36,6 +38,7 @@ type Props = {
};
export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const { t } = useTranslation();
const { workspaceSlug, projectId, issueId, issueOperations, isEditable } = props;
// store hooks
const { getProjectById } = useProject();
@ -64,13 +67,13 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<>
<div className="flex items-center h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
<div className="h-full w-full overflow-y-auto px-6">
<h5 className="mt-6 text-sm font-medium">Properties</h5>
<h5 className="mt-6 text-sm font-medium">{t("common.properties")}</h5>
{/* TODO: render properties using a common component */}
<div className={`mb-2 mt-3 space-y-2.5 ${!isEditable ? "opacity-60" : ""}`}>
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
<span>State</span>
<span>{t("common.state")}</span>
</div>
<StateDropdown
value={issue?.state_id}
@ -89,14 +92,14 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<Users className="h-4 w-4 flex-shrink-0" />
<span>Assignees</span>
<span>{t("common.assignees")}</span>
</div>
<MemberDropdown
value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={!isEditable}
projectId={projectId?.toString() ?? ""}
placeholder="Add assignees"
placeholder={t("issue.add.assignee")}
multiple
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
className="group w-3/5 flex-grow"
@ -113,7 +116,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<Signal className="h-4 w-4 flex-shrink-0" />
<span>Priority</span>
<span>{t("common.priority")}</span>
</div>
<PriorityDropdown
value={issue?.priority}
@ -130,7 +133,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
<span>Created by</span>
<span>{t("common.created_by")}</span>
</div>
<div className="w-full h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-not-allowed">
<ButtonAvatars showTooltip userIds={createdByDetails.id} />
@ -142,10 +145,10 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<CalendarClock className="h-4 w-4 flex-shrink-0" />
<span>Start date</span>
<span>{t("common.order_by.start_date")}</span>
</div>
<DateDropdown
placeholder="Add start date"
placeholder={t("issue.add.start_date")}
value={issue.start_date}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, {
@ -168,10 +171,10 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
<span>Due date</span>
<span>{t("common.order_by.due_date")}</span>
</div>
<DateDropdown
placeholder="Add due date"
placeholder={t("issue.add.due_date")}
value={issue.target_date}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, {
@ -198,7 +201,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<Triangle className="h-4 w-4 flex-shrink-0" />
<span>Estimate</span>
<span>{t("common.estimate")}</span>
</div>
<EstimateDropdown
value={issue?.estimate_point ?? undefined}
@ -211,7 +214,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
className="group w-3/5 flex-grow"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`}
placeholder="None"
placeholder={t("common.none")}
hideIcon
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
@ -223,7 +226,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex min-h-8 gap-2">
<div className="flex w-2/5 flex-shrink-0 gap-1 pt-2 text-sm text-custom-text-300">
<DiceIcon className="h-4 w-4 flex-shrink-0" />
<span>Modules</span>
<span>{t("common.modules")}</span>
</div>
<IssueModuleSelect
className="w-3/5 flex-grow"
@ -240,7 +243,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<ContrastIcon className="h-4 w-4 flex-shrink-0" />
<span>Cycle</span>
<span>{t("common.cycle")}</span>
</div>
<IssueCycleSelect
className="w-3/5 flex-grow"
@ -256,7 +259,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<LayoutPanelTop className="h-4 w-4 flex-shrink-0" />
<span>Parent</span>
<span>{t("common.parent")}</span>
</div>
<IssueParentSelect
className="h-full w-3/5 flex-grow"
@ -271,7 +274,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex min-h-8 gap-2">
<div className="flex w-2/5 flex-shrink-0 gap-1 pt-2 text-sm text-custom-text-300">
<Tag className="h-4 w-4 flex-shrink-0" />
<span>Labels</span>
<span>{t("common.labels")}</span>
</div>
<div className="h-full min-h-8 w-3/5 flex-grow">
<IssueLabel

View file

@ -4,11 +4,13 @@ import { FC, useState } from "react";
import isNil from "lodash/isNil";
import { observer } from "mobx-react";
import { Bell, BellOff } from "lucide-react";
// plane-i18n
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// UI
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useIssueDetail, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export type TIssueSubscription = {
workspaceSlug: string;
@ -18,6 +20,7 @@ export type TIssueSubscription = {
export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
const { workspaceSlug, projectId, issueId } = props;
const { t } = useTranslation();
// hooks
const {
subscription: { getSubscriptionByIssueId },
@ -44,16 +47,18 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
else await createSubscription(workspaceSlug, projectId, issueId);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`,
title: t("toast.success"),
message: isSubscribed
? t("issue.subscription.actions.unsubscribed")
: t("issue.subscription.actions.subscribed"),
});
setLoading(false);
} catch (error) {
setLoading(false);
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Something went wrong. Please try again later.",
title: t("toast.error"),
message: t("commons.error.message"),
});
}
};
@ -77,12 +82,12 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
>
{loading ? (
<span>
<span className="hidden sm:block">Loading...</span>
<span className="hidden sm:block">{t("common.loading")}</span>
</span>
) : isSubscribed ? (
<div className="hidden sm:block">Unsubscribe</div>
<div className="hidden sm:block">{t("common.actions.unsubscribe")}</div>
) : (
<div className="hidden sm:block">Subscribe</div>
<div className="hidden sm:block">{t("common.actions.subscribe")}</div>
)}
</Button>
</div>

View file

@ -3,7 +3,7 @@
import { FC, useCallback, useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { EIssueGroupByToServerOptions, EIssuesStoreType } from "@plane/constants";
import { EIssueGroupByToServerOptions, EIssuesStoreType,EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { TGroupedIssues } from "@plane/types";
// components
import { TOAST_TYPE, setToast } from "@plane/ui";
@ -13,7 +13,6 @@ import { CalendarChart } from "@/components/issues";
import { useIssues, useCalendarView, useUserPermissions } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// types
import { IQuickActionProps } from "../list/list-view-types";
import { handleDragDrop } from "./utils";

View file

@ -116,7 +116,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Due date cannot be before the start date of the issue.",
message: "Due date cannot be before the start date of the work item.",
});
return;
}

View file

@ -10,6 +10,7 @@ import { Popover, Transition } from "@headlessui/react";
// ui
// icons
import { EIssueFilterType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
@ -46,6 +47,8 @@ interface ICalendarHeader {
export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((props) => {
const { issuesFilterStore, updateFilters } = props;
const { t } = useTranslation();
const { projectId } = useParams();
const issueCalendarView = useCalendarView();
@ -111,7 +114,7 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
<div className="font-medium">Options</div>
<div className="font-medium">{t("common.options")}</div>
<div
className={`flex h-3.5 w-3.5 items-center justify-center transition-all ${open ? "" : "rotate-180"}`}
>
@ -156,7 +159,7 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
className="flex w-full items-center justify-between gap-2 rounded px-1 py-1.5 text-left text-xs hover:bg-custom-background-80"
onClick={handleToggleWeekends}
>
Show weekends
{t("common.actions.show_weekends")}
<ToggleSwitch
value={showWeekends}
onChange={() => {

View file

@ -3,6 +3,7 @@ import { observer } from "mobx-react";
// components
import { ChevronLeft, ChevronRight } from "lucide-react";
import { EIssueFilterType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
@ -37,6 +38,8 @@ interface ICalendarHeader {
export const CalendarHeader: React.FC<ICalendarHeader> = observer((props) => {
const { issuesFilterStore, updateFilters, setSelectedDate } = props;
const { t } = useTranslation();
const issueCalendarView = useCalendarView();
const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month";
@ -119,7 +122,7 @@ export const CalendarHeader: React.FC<ICalendarHeader> = observer((props) => {
className="rounded bg-custom-background-80 px-2.5 py-1 text-xs font-medium text-custom-text-200 hover:text-custom-text-100"
onClick={handleToday}
>
Today
{t("common.today")}
</button>
<CalendarOptionsDropdown issuesFilterStore={issuesFilterStore} updateFilters={updateFilters} />
</div>

View file

@ -1,4 +1,5 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { TIssue, TPaginationData } from "@plane/types";
// components
import { CalendarQuickAddIssueActions, CalendarIssueBlockRoot } from "@/components/issues";
@ -43,6 +44,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
isEpic = false,
} = props;
const formattedDatePayload = renderFormattedPayloadDate(date);
const { t } = useTranslation();
const {
issues: { getGroupIssueCount, getPaginationData, getIssueLoader },
@ -99,7 +101,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 font-medium hover:bg-custom-background-80 text-custom-primary-100 hover:text-custom-primary-200"
onClick={() => loadMoreIssues(formattedDatePayload)}
>
Load More
{t("common.load_more")}
</button>
</div>
)}

View file

@ -7,6 +7,8 @@ import { useParams } from "next/navigation";
import { PlusIcon } from "lucide-react";
// plane constants
import { EIssueLayoutTypes } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { ISearchIssueResponse, TIssue } from "@plane/types";
// ui
@ -29,6 +31,7 @@ type TCalendarQuickAddIssueActions = {
export const CalendarQuickAddIssueActions: FC<TCalendarQuickAddIssueActions> = observer((props) => {
const { prePopulatedData, quickAddCallback, addIssuesToView, onOpen, isEpic = false } = props;
const { t } = useTranslation();
// router
const { workspaceSlug, projectId, moduleId } = useParams();
// states
@ -52,14 +55,14 @@ export const CalendarQuickAddIssueActions: FC<TCalendarQuickAddIssueActions> = o
).then(() => addIssuesToView?.(issueIds));
setPromiseToast(addExistingIssuesPromise, {
loading: `Adding ${issueIds.length > 1 ? "issues" : "issue"} to cycle...`,
loading: t("issue.adding", { count: issueIds.length }),
success: {
title: "Success!",
message: () => `${issueIds.length > 1 ? "Issues" : "Issue"} added to cycle successfully.`,
title: t("toast.success"),
message: () => t("entity.add.success", { entity: t("issue.label", { count: 2 }) }),
},
error: {
title: "Error!",
message: (err) => err?.message || "Something went wrong. Please try again.",
title: t("toast.error"),
message: (err) => err?.message || t("common.errors.default.message"),
},
});
};
@ -119,12 +122,18 @@ export const CalendarQuickAddIssueActions: FC<TCalendarQuickAddIssueActions> = o
customButton={
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-text-350 hover:text-custom-text-300">
<PlusIcon className="h-3.5 w-3.5 stroke-2 flex-shrink-0" />
<span className="text-sm font-medium flex-shrink-0">{`New ${isEpic ? "Epic" : "Issue"}`}</span>
<span className="text-sm font-medium flex-shrink-0">
{isEpic ? t("epic.add.label") : t("issue.add.label")}
</span>
</div>
}
>
<CustomMenu.MenuItem onClick={handleNewIssue}>{`New ${isEpic ? "Epic" : "Issue"}`}</CustomMenu.MenuItem>
{!isEpic && <CustomMenu.MenuItem onClick={handleExistingIssue}>Add existing issue</CustomMenu.MenuItem>}
<CustomMenu.MenuItem onClick={handleNewIssue}>
{isEpic ? t("epic.add.label") : t("issue.add.label")}
</CustomMenu.MenuItem>
{!isEpic && (
<CustomMenu.MenuItem onClick={handleExistingIssue}>{t("issue.add.existing")}</CustomMenu.MenuItem>
)}
</CustomMenu>
</div>
}

View file

@ -1,11 +1,11 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// hooks
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { ProjectIssueQuickActions } from "@/components/issues";
// hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// components
import { BaseCalendarRoot } from "../base-calendar-root";

View file

@ -1,31 +1,46 @@
import size from "lodash/size";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// plane imports
import { EIssueFilterType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueFilterOptions } from "@plane/types";
// hooks
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { useIssues } from "@/hooks/store";
// types
import { DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useIssues, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ProjectArchivedEmptyState: React.FC = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
// theme
// plane hooks
const { t } = useTranslation();
// store hooks
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
const { allowPermissions } = useUserPermissions();
// derived values
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const additionalPath = issueFilterCount > 0 ? (activeLayout ?? "list") : undefined;
const canPerformEmptyStateActions = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const emptyFilterResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/empty-filters/",
additionalPath: additionalPath,
});
const archivedIssuesResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/archived/empty-issues",
});
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;
@ -38,20 +53,30 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
});
};
const emptyStateType =
issueFilterCount > 0 ? EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER : EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES;
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
return (
<div className="relative h-full w-full overflow-y-auto">
<EmptyState
type={emptyStateType}
additionalPath={additionalPath}
primaryButtonLink={
issueFilterCount > 0 ? undefined : `/${workspaceSlug}/projects/${projectId}/settings/automations`
}
secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined}
/>
{issueFilterCount > 0 ? (
<DetailedEmptyState
title={t("project_issues.empty_state.issues_empty_filter.title")}
assetPath={emptyFilterResolvedPath}
secondaryButton={{
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
onClick: handleClearAllFilters,
disabled: !canPerformEmptyStateActions,
}}
/>
) : (
<DetailedEmptyState
title={t("project_issues.empty_state.no_archived_issues.title")}
description={t("project_issues.empty_state.no_archived_issues.description")}
assetPath={archivedIssuesResolvedPath}
primaryButton={{
text: t("project_issues.empty_state.no_archived_issues.primary_button.text"),
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`),
disabled: !canPerformEmptyStateActions,
}}
/>
)}
</div>
);
});

View file

@ -5,32 +5,58 @@ import isEmpty from "lodash/isEmpty";
import size from "lodash/size";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// plane imports
import { EIssueFilterType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ExistingIssuesListModal } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { useCommandPalette, useCycle, useEventTracker, useIssues } from "@/hooks/store";
import { DetailedEmptyState } from "@/components/empty-state";
import { useCommandPalette, useCycle, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const CycleEmptyState: React.FC = observer(() => {
// router
const { workspaceSlug, projectId, cycleId } = useParams();
// states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
// plane hooks
const { t } = useTranslation();
// store hooks
const { getCycleById } = useCycle();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// derived values
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
const isEmptyFilters = issueFilterCount > 0;
const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status?.toLowerCase() === "completed";
const additionalPath = activeLayout ?? "list";
const canPerformEmptyStateActions = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const emptyFilterResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/empty-filters/",
additionalPath: additionalPath,
});
const noIssueResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/cycle-issues/",
additionalPath: additionalPath,
});
const completedNoIssuesResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/cycle/completed-no-issues",
});
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !cycleId) return;
@ -43,24 +69,17 @@ export const CycleEmptyState: React.FC = observer(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Issues added to the cycle successfully.",
message: "Work items added to the cycle successfully.",
})
)
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.",
message: "Selected work items could not be added to the cycle. Please try again.",
})
);
};
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !cycleId) return;
@ -79,16 +98,6 @@ export const CycleEmptyState: React.FC = observer(() => {
);
};
const isEmptyFilters = issueFilterCount > 0;
const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status?.toLowerCase() === "completed";
const emptyStateType = isCompletedAndEmpty
? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES
: isEmptyFilters
? EmptyStateType.PROJECT_EMPTY_FILTER
: EmptyStateType.PROJECT_CYCLE_NO_ISSUES;
const additionalPath = isCompletedAndEmpty ? undefined : activeLayout ?? "list";
return (
<div className="relative h-full w-full overflow-y-auto">
<ExistingIssuesListModal
@ -100,21 +109,42 @@ export const CycleEmptyState: React.FC = observer(() => {
handleOnSubmit={handleAddIssuesToCycle}
/>
<div className="grid h-full w-full place-items-center">
<EmptyState
type={emptyStateType}
additionalPath={additionalPath}
primaryButtonOnClick={
!isCompletedAndEmpty && !isEmptyFilters
? () => {
setTrackElement("Cycle issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
}
: undefined
}
secondaryButtonOnClick={
!isCompletedAndEmpty && isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)
}
/>
{isCompletedAndEmpty ? (
<DetailedEmptyState
title={t("project_cycles.empty_state.completed_no_issues.title")}
description={t("project_cycles.empty_state.completed_no_issues.description")}
assetPath={completedNoIssuesResolvedPath}
/>
) : isEmptyFilters ? (
<DetailedEmptyState
title={t("project_issues.empty_state.issues_empty_filter.title")}
assetPath={emptyFilterResolvedPath}
secondaryButton={{
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
onClick: handleClearAllFilters,
disabled: !canPerformEmptyStateActions,
}}
/>
) : (
<DetailedEmptyState
title={t("project_cycles.empty_state.no_issues.title")}
description={t("project_cycles.empty_state.no_issues.description")}
assetPath={noIssueResolvedPath}
primaryButton={{
text: t("project_cycles.empty_state.no_issues.primary_button.text"),
onClick: () => {
setTrackElement("Cycle issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
},
disabled: !canPerformEmptyStateActions,
}}
secondaryButton={{
text: t("project_cycles.empty_state.no_issues.secondary_button.text"),
onClick: () => setCycleIssuesListModal(true),
disabled: !canPerformEmptyStateActions,
}}
/>
)}
</div>
</div>
);

View file

@ -1,53 +1,6 @@
import size from "lodash/size";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import { IIssueFilterOptions } from "@plane/types";
// hooks
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { useIssues } from "@/hooks/store";
// types
export const ProjectDraftEmptyState: React.FC = observer(() => {
// router
const { workspaceSlug, projectId } = useParams();
// store hooks
const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = [];
});
issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
...newFilters,
});
};
const emptyStateType =
issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES;
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
return (
<div className="relative h-full w-full overflow-y-auto">
<EmptyState
type={emptyStateType}
additionalPath={additionalPath}
secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined}
/>
</div>
);
});
// FIXME: Project drafts is deprecated. Remove this component and all the related code.
export const ProjectDraftEmptyState: React.FC = observer(() => (
<div className="relative h-full w-full overflow-y-auto" />
));

View file

@ -1,43 +1,77 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EIssuesStoreType, EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { EIssuesStoreType } from "@plane/constants";
import { EmptyState } from "@/components/empty-state";
// constants
import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useProject } from "@/hooks/store";
// assets
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const GlobalViewEmptyState: React.FC = observer(() => {
const { globalViewId } = useParams();
// plane imports
const { t } = useTranslation();
// store hooks
const { workspaceProjectIds } = useProject();
const { toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// derived values
const hasMemberLevelPermission = allowPermissions(
[EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId?.toString() ?? "");
const currentView = isDefaultView && globalViewId ? globalViewId : "custom-view";
const resolvedCurrentView = currentView?.toString();
const noProjectResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" });
const globalViewsResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/all-issues/",
additionalPath: resolvedCurrentView,
});
const emptyStateType =
(workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS;
return (
<EmptyState
type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS}
size="sm"
primaryButtonOnClick={
(workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed"
? () => {
setTrackElement("All issues empty state");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}
: undefined
: () => {
if (workspaceProjectIds?.length === 0) {
return (
<DetailedEmptyState
size="sm"
title={t("workspace_projects.empty_state.no_projects.title")}
description={t("workspace_projects.empty_state.no_projects.description")}
assetPath={noProjectResolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_projects.empty_state.no_projects.primary_button.text")}
title={t("workspace_projects.empty_state.no_projects.primary_button.comic.title")}
description={t("workspace_projects.empty_state.no_projects.primary_button.comic.description")}
onClick={() => {
setTrackElement("All issues empty state");
toggleCreateProjectModal(true);
}}
disabled={!hasMemberLevelPermission}
/>
}
/>
);
}
return (
<DetailedEmptyState
size="sm"
title={t(`workspace_views.empty_state.${resolvedCurrentView}.title`)}
description={t(`workspace_views.empty_state.${resolvedCurrentView}.description`)}
assetPath={globalViewsResolvedPath}
primaryButton={
["subscribed", "custom-view"].includes(resolvedCurrentView) === false
? {
text: t(`workspace_views.empty_state.${resolvedCurrentView}.primary_button.text`),
onClick: () => {
setTrackElement("All issues empty state");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
disabled: !hasMemberLevelPermission,
}
: undefined
}
/>
);

View file

@ -4,31 +4,52 @@ import { useState } from "react";
import size from "lodash/size";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// plane imports
import { EIssueFilterType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ExistingIssuesListModal } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useIssues } from "@/hooks/store";
import { useCommandPalette, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ModuleEmptyState: React.FC = observer(() => {
// router
const { workspaceSlug, projectId, moduleId } = useParams();
// states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
// plane hooks
const { t } = useTranslation();
// store hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// derived values
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const isEmptyFilters = issueFilterCount > 0;
const additionalPath = activeLayout ?? "list";
const canPerformEmptyStateActions = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const emptyFilterResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/empty-filters/",
additionalPath: additionalPath,
});
const moduleIssuesResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/module-issues/",
additionalPath: additionalPath,
});
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !moduleId) return;
@ -40,24 +61,18 @@ export const ModuleEmptyState: React.FC = observer(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Issues added to the module successfully.",
message: "Work items added to the module successfully.",
})
)
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Selected issues could not be added to the module. Please try again.",
message: "Selected work items could not be added to the module. Please try again.",
})
);
};
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !moduleId) return;
const newFilters: IIssueFilterOptions = {};
@ -75,10 +90,6 @@ export const ModuleEmptyState: React.FC = observer(() => {
);
};
const isEmptyFilters = issueFilterCount > 0;
const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES;
const additionalPath = activeLayout ?? "list";
return (
<div className="relative h-full w-full overflow-y-auto">
<ExistingIssuesListModal
@ -90,19 +101,36 @@ export const ModuleEmptyState: React.FC = observer(() => {
handleOnSubmit={handleAddIssuesToModule}
/>
<div className="grid h-full w-full place-items-center">
<EmptyState
type={emptyStateType}
additionalPath={additionalPath}
primaryButtonOnClick={
isEmptyFilters
? undefined
: () => {
setTrackElement("Module issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
}
}
secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)}
/>
{isEmptyFilters ? (
<DetailedEmptyState
title={t("project_issues.empty_state.issues_empty_filter.title")}
assetPath={emptyFilterResolvedPath}
secondaryButton={{
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
onClick: handleClearAllFilters,
disabled: !canPerformEmptyStateActions,
}}
/>
) : (
<DetailedEmptyState
title={t("project_module.empty_state.no_issues.title")}
description={t("project_module.empty_state.no_issues.description")}
assetPath={moduleIssuesResolvedPath}
primaryButton={{
text: t("project_module.empty_state.no_issues.primary_button.text"),
onClick: () => {
setTrackElement("Module issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
},
disabled: !canPerformEmptyStateActions,
}}
secondaryButton={{
text: t("project_module.empty_state.no_issues.secondary_button.text"),
onClick: () => setModuleIssuesListModal(true),
disabled: !canPerformEmptyStateActions,
}}
/>
)}
</div>
</div>
);

View file

@ -1,19 +1,30 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { EmptyState } from "@/components/empty-state";
import { useTranslation } from "@plane/i18n";
import { DetailedEmptyState } from "@/components/empty-state";
// constants
import { EMPTY_STATE_DETAILS } from "@/constants/empty-state";
// assets
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// TODO: If projectViewId changes, everything breaks. Figure out a better way to handle this.
export const ProfileViewEmptyState: React.FC = observer(() => {
// plane hooks
const { t } = useTranslation();
// store hooks
const { profileViewId } = useParams();
// derived values
const resolvedPath = useResolvedAssetPath({
basePath: "/empty-state/profile/",
additionalPath: profileViewId?.toString(),
});
if (!profileViewId) return null;
const emptyStateType = `profile-${profileViewId.toString()}`;
return <EmptyState type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} size="sm" />;
return (
<DetailedEmptyState
title={t(`profile.empty_state.${profileViewId.toString()}.title`)}
description={t(`profile.empty_state.${profileViewId.toString()}.description`)}
assetPath={resolvedPath}
/>
);
});

View file

@ -1,12 +1 @@
// types
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
export const ProjectEpicsEmptyState: React.FC = () => (
<div className="relative h-full w-full overflow-y-auto">
<EmptyState type={EmptyStateType.PROJECT_NO_EPICS} primaryButtonOnClick={() => {}} />
</div>
);
export const ProjectEpicsEmptyState: React.FC = () => <></>;

View file

@ -1,33 +1,46 @@
import size from "lodash/size";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// plane imports
import { EIssueFilterType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueFilterOptions } from "@plane/types";
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useIssues } from "@/hooks/store";
import { useCommandPalette, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ProjectEmptyState: React.FC = observer(() => {
// router
const { workspaceSlug, projectId } = useParams();
// plane imports
const { t } = useTranslation();
// store hooks
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const { allowPermissions } = useUserPermissions();
// derived values
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const additionalPath = issueFilterCount > 0 ? (activeLayout ?? "list") : undefined;
const canPerformEmptyStateActions = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const emptyFilterResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/empty-filters/",
additionalPath: additionalPath,
});
const projectIssuesResolvedPath = useResolvedAssetPath({
basePath: "/empty-state/onboarding/issues",
});
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;
@ -40,24 +53,37 @@ export const ProjectEmptyState: React.FC = observer(() => {
});
};
const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES;
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
return (
<div className="relative h-full w-full overflow-y-auto">
<EmptyState
type={emptyStateType}
additionalPath={additionalPath}
primaryButtonOnClick={
issueFilterCount > 0
? undefined
: () => {
{issueFilterCount > 0 ? (
<DetailedEmptyState
title={t("project_issues.empty_state.issues_empty_filter.title")}
assetPath={emptyFilterResolvedPath}
secondaryButton={{
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
onClick: handleClearAllFilters,
disabled: !canPerformEmptyStateActions,
}}
/>
) : (
<DetailedEmptyState
title={t("project_issues.empty_state.no_issues.title")}
description={t("project_issues.empty_state.no_issues.description")}
assetPath={projectIssuesResolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("project_issues.empty_state.no_issues.primary_button.text")}
title={t("project_issues.empty_state.no_issues.primary_button.comic.title")}
description={t("project_issues.empty_state.no_issues.primary_button.comic.description")}
onClick={() => {
setTrackElement("Project issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}
}
secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined}
/>
}}
disabled={!canPerformEmptyStateActions}
/>
}
/>
)}
</div>
);
});

View file

@ -1,12 +1,11 @@
import { observer } from "mobx-react";
import { PlusIcon } from "lucide-react";
// components
import { EIssuesStoreType } from "@plane/constants";
import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EmptyState } from "@/components/common";
// constants
// hooks
import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// assets
import emptyIssue from "@/public/empty-state/issue.svg";
@ -25,16 +24,16 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
return (
<div className="grid h-full w-full place-items-center">
<EmptyState
title="View issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
title="View work items will appear here"
description="Work items help you track individual pieces of work. With work items, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={
isCreatingIssueAllowed
? {
text: "New issue",
text: "New work item",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("View issue empty state");
setTrackElement("View work item empty state");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
}

View file

@ -2,7 +2,7 @@ import { observer } from "mobx-react";
// icons
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,6 +1,8 @@
import { observer } from "mobx-react";
import { X } from "lucide-react";
// types
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types";
// components
import { Tag } from "@plane/ui";
@ -22,7 +24,6 @@ import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
import { useUserPermissions } from "@/hooks/store";
// plane web components
import { AppliedIssueTypeFilters } from "@/plane-web/components/issues";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type Props = {
appliedFilters: IIssueFilterOptions;
@ -49,6 +50,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
} = props;
// store hooks
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
if (!appliedFilters) return null;
@ -156,7 +158,7 @@ export const AppliedFiltersList: 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

@ -6,7 +6,13 @@ import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import {
DEFAULT_GLOBAL_VIEWS_LIST,
EIssueFilterType,
EIssuesStoreType,
EViewAccess,
GLOBAL_VIEW_UPDATED,
EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
//ui
// components
@ -15,14 +21,10 @@ import { AppliedFiltersList } from "@/components/issues";
import { UpdateViewComponent } from "@/components/views/update-view-component";
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// constants
import { GLOBAL_VIEW_UPDATED } from "@/constants/event-tracker";
import { EViewAccess } from "@/constants/views";
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useEventTracker, useGlobalView, useIssues, useLabel, useUser, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { getAreFiltersEqual } from "../../../utils";
type Props = {

View file

@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import { EIssueFilterType, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { IIssueFilterOptions } from "@plane/types";
// ui
import { Header, EHeaderVariant } from "@plane/ui";
@ -12,7 +12,6 @@ import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
import { useLabel, useProjectState, useUserPermissions } from "@/hooks/store";
import { useIssues } from "@/hooks/store/use-issues";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type TProjectAppliedFiltersRootProps = {
storeType?: EIssuesStoreType.PROJECT | EIssuesStoreType.EPIC;

View file

@ -6,7 +6,13 @@ import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import {
EIssueFilterType,
EIssuesStoreType,
EViewAccess,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
import { IIssueFilterOptions } from "@plane/types";
// components
import { Header, EHeaderVariant } from "@plane/ui";
@ -14,10 +20,8 @@ import { AppliedFiltersList } from "@/components/issues";
import { CreateUpdateProjectViewModal } from "@/components/views";
import { UpdateViewComponent } from "@/components/views/update-view-component";
// constants
import { EViewAccess } from "@/constants/views";
// hooks
import { useIssues, useLabel, useProjectState, useProjectView, useUser, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { getAreFiltersEqual } from "../../../utils";
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {

View file

@ -1,10 +1,12 @@
import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane constants
import { ISSUE_DISPLAY_PROPERTIES } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
// types
import { IIssueDisplayProperties } from "@plane/types";
// constants
import { ISSUE_DISPLAY_PROPERTIES } from "@/constants/issue";
// plane web helpers
import { shouldRenderDisplayProperty } from "@/plane-web/helpers/issue-filter.helper";
// components
@ -28,6 +30,8 @@ export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
moduleViewDisabled = false,
isEpic = false,
} = props;
// hooks
const { t } = useTranslation();
// router
const { workspaceSlug, projectId: routerProjectId } = useParams();
// states
@ -49,7 +53,7 @@ export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
}
}).map((property) => {
if (isEpic && property.key === "sub_issue_count") {
return { ...property, title: "Issue count" };
return { ...property, title: "Work item count" };
}
return property;
});
@ -57,7 +61,7 @@ export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
return (
<>
<FilterHeader
title="Display Properties"
title={t("issue.display.properties.label")}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
@ -79,7 +83,7 @@ export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
})
}
>
{displayProperty.title}
{t(displayProperty.titleTranslationKey)}
</button>
</>
))}

View file

@ -1,12 +1,24 @@
import React from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "@plane/types";
// components
import { FilterOption } from "@/components/issues";
// types
import { ISSUE_EXTRA_OPTIONS } from "@/constants/issue";
// constants
const ISSUE_EXTRA_OPTIONS: {
key: TIssueExtraOptions;
titleTranslationKey: string;
}[] = [
{
key: "sub_issue",
titleTranslationKey: "issue.display.extra.show_sub_issues",
}, // in spreadsheet its always false
{
key: "show_empty_groups",
titleTranslationKey: "issue.display.extra.show_empty_groups",
}, // filter on front-end
];
type Props = {
selectedExtraOptions: {
@ -19,7 +31,8 @@ type Props = {
export const FilterExtraOptions: React.FC<Props> = observer((props) => {
const { selectedExtraOptions, handleUpdate, enabledExtraOptions } = props;
// hooks
const { t } = useTranslation();
const isExtraOptionEnabled = (option: TIssueExtraOptions) => enabledExtraOptions.includes(option);
return (
@ -32,7 +45,7 @@ export const FilterExtraOptions: React.FC<Props> = observer((props) => {
key={option.key}
isChecked={selectedExtraOptions?.[option.key] ? true : false}
onClick={() => handleUpdate(option.key, !selectedExtraOptions?.[option.key])}
title={option.title}
title={t(option.titleTranslationKey)}
/>
);
})}

View file

@ -1,11 +1,10 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { ISSUE_GROUP_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types";
// components
import { FilterHeader, FilterOption } from "@/components/issues";
// types
import { ISSUE_GROUP_BY_OPTIONS } from "@/constants/issue";
// constants
type Props = {
displayFilters: IIssueDisplayFilterOptions | undefined;
@ -16,7 +15,8 @@ type Props = {
export const FilterGroupBy: React.FC<Props> = observer((props) => {
const { displayFilters, groupByOptions, handleUpdate, ignoreGroupedFilters } = props;
// hooks
const { t } = useTranslation();
const [previewEnabled, setPreviewEnabled] = useState(true);
const selectedGroupBy = displayFilters?.group_by ?? null;
@ -25,7 +25,7 @@ export const FilterGroupBy: React.FC<Props> = observer((props) => {
return (
<>
<FilterHeader
title="Group by"
title={t("common.group_by")}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
@ -45,7 +45,7 @@ export const FilterGroupBy: React.FC<Props> = observer((props) => {
key={groupBy?.key}
isChecked={selectedGroupBy === groupBy?.key ? true : false}
onClick={() => handleUpdate(groupBy.key)}
title={groupBy.title}
title={t(groupBy.titleTranslationKey)}
multiple={false}
/>
);

View file

@ -1,11 +1,8 @@
import React from "react";
import { observer } from "mobx-react";
import { TIssueGroupingFilters } from "@plane/types";
// components
import { FilterHeader, FilterOption } from "@/components/issues";
// types
import { ISSUE_FILTER_OPTIONS } from "@/constants/issue";
// constants
type Props = {
@ -14,6 +11,15 @@ type Props = {
isEpic?: boolean;
};
const ISSUE_FILTER_OPTIONS: {
key: TIssueGroupingFilters;
title: string;
}[] = [
{ key: null, title: "All" },
{ key: "active", title: "Active" },
{ key: "backlog", title: "Backlog" },
];
export const FilterIssueGrouping: React.FC<Props> = observer((props) => {
const { selectedIssueType, handleUpdate, isEpic = false } = props;
@ -24,7 +30,7 @@ export const FilterIssueGrouping: React.FC<Props> = observer((props) => {
return (
<>
<FilterHeader
title={`${isEpic ? "Epic" : "Issue"} Grouping`}
title={`${isEpic ? "Epic" : "Work item"} Grouping`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
@ -35,7 +41,7 @@ export const FilterIssueGrouping: React.FC<Props> = observer((props) => {
key={issueType?.key}
isChecked={activeIssueType === issueType?.key ? true : false}
onClick={() => handleUpdate(issueType?.key)}
title={`${issueType.title} ${isEpic ? "Epics" : "Issues"}`}
title={`${issueType.title} ${isEpic ? "Epics" : "Work items"}`}
multiple={false}
/>
))}

View file

@ -1,12 +1,11 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { ISSUE_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueOrderByOptions } from "@plane/types";
// components
import { FilterHeader, FilterOption } from "@/components/issues";
// types
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
// constants
type Props = {
selectedOrderBy: TIssueOrderByOptions | undefined;
@ -16,6 +15,8 @@ type Props = {
export const FilterOrderBy: React.FC<Props> = observer((props) => {
const { selectedOrderBy, handleUpdate, orderByOptions } = props;
// hooks
const { t } = useTranslation();
const [previewEnabled, setPreviewEnabled] = useState(true);
@ -24,7 +25,7 @@ export const FilterOrderBy: React.FC<Props> = observer((props) => {
return (
<>
<FilterHeader
title="Order by"
title={t("common.order_by.label")}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
@ -35,7 +36,7 @@ export const FilterOrderBy: React.FC<Props> = observer((props) => {
key={orderBy?.key}
isChecked={activeOrderBy === orderBy?.key ? true : false}
onClick={() => handleUpdate(orderBy.key)}
title={orderBy.title}
title={t(orderBy.titleTranslationKey)}
multiple={false}
/>
))}

View file

@ -1,10 +1,10 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { ISSUE_GROUP_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types";
// components
import { FilterHeader, FilterOption } from "@/components/issues";
// types
import { ISSUE_GROUP_BY_OPTIONS } from "@/constants/issue";
// constants
type Props = {
@ -15,6 +15,9 @@ type Props = {
};
export const FilterSubGroupBy: React.FC<Props> = observer((props) => {
// hooks
const { t } = useTranslation();
const { displayFilters, handleUpdate, subGroupByOptions, ignoreGroupedFilters } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
@ -40,7 +43,7 @@ export const FilterSubGroupBy: React.FC<Props> = observer((props) => {
key={subGroupBy?.key}
isChecked={selectedSubGroupBy === subGroupBy?.key ? true : false}
onClick={() => handleUpdate(subGroupBy.key)}
title={subGroupBy.title}
title={t(subGroupBy.titleTranslationKey)}
multiple={false}
/>
);

View file

@ -2,6 +2,9 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Search, X } from "lucide-react";
// i18n
import { EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import {
IIssueDisplayFilterOptions,
@ -31,7 +34,6 @@ import { useMember } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { FilterIssueTypes, FilterTeamProjects } from "@/plane-web/components/issues";
import { EUserPermissions } from "@/plane-web/constants";
type Props = {
filters: IIssueFilterOptions;
@ -63,6 +65,9 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
moduleViewDisabled = false,
isEpic = false,
} = props;
// i18n
const { t } = useTranslation();
// hooks
const { isMobile } = usePlatformOS();
const { moduleId, cycleId } = useParams();
@ -95,7 +100,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
placeholder="Search"
placeholder={t("common.search.label")}
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus={!isMobile}

View file

@ -2,16 +2,14 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
// plane constants
import { ISSUE_PRIORITIES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { PriorityIcon } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "@/components/issues";
// constants
import { ISSUE_PRIORITIES } from "@/constants/issue";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
@ -20,17 +18,17 @@ type Props = {
export const FilterPriority: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
// hooks
const { t } = useTranslation();
const [previewEnabled, setPreviewEnabled] = useState(true);
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = ISSUE_PRIORITIES.filter((p) => p.key.includes(searchQuery.toLowerCase()));
return (
<>
<FilterHeader
title={`Priority${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
title={`Priority ${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
@ -47,7 +45,7 @@ export const FilterPriority: React.FC<Props> = observer((props) => {
/>
))
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
<p className="text-xs italic text-custom-text-400">{t("common.search.no_matches_found")}</p>
)}
</div>
)}

View file

@ -1,11 +1,10 @@
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";
type Props = {
appliedFilters: string[] | null;
@ -15,7 +14,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

@ -2,12 +2,11 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
// components
// plane imports
import { STATE_GROUPS } from "@plane/constants";
import { StateGroupIcon } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "@/components/issues";
// icons
import { STATE_GROUPS } from "@/constants/state";
// constants
type Props = {
appliedFilters: string[] | null;

View file

@ -1,11 +1,10 @@
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";
type Props = {
appliedFilters: string[] | null;

View file

@ -2,12 +2,13 @@
import React from "react";
// plane constants
import { EIssueLayoutTypes } from "@plane/constants";
import { EIssueLayoutTypes, ISSUE_LAYOUTS } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
// ui
import { Tooltip } from "@plane/ui";
// types
// constants
import { ISSUE_LAYOUTS } from "@/constants/issue";
import { IssueLayoutIcon } from "@/components/issues";
import { usePlatformOS } from "@/hooks/use-platform-os";
// hooks
@ -20,7 +21,7 @@ type Props = {
export const LayoutSelection: React.FC<Props> = (props) => {
const { layouts, onChange, selectedLayout } = props;
const { isMobile } = usePlatformOS();
const { t } = useTranslation();
const handleOnChange = (layoutKey: EIssueLayoutTypes) => {
if (selectedLayout !== layoutKey) {
onChange(layoutKey);
@ -30,7 +31,7 @@ export const LayoutSelection: React.FC<Props> = (props) => {
return (
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<Tooltip key={layout.key} tooltipContent={t(layout.i18n_title)} isMobile={isMobile}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
@ -38,7 +39,8 @@ export const LayoutSelection: React.FC<Props> = (props) => {
}`}
onClick={() => handleOnChange(layout.key)}
>
<layout.icon
<IssueLayoutIcon
layout={layout.key}
size={14}
strokeWidth={2}
className={`h-3.5 w-3.5 ${

View file

@ -2,7 +2,14 @@ import React, { useCallback, useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane constants
import { ALL_ISSUES, EIssueLayoutTypes, EIssuesStoreType } from "@plane/constants";
import {
ALL_ISSUES,
EIssueLayoutTypes,
EIssuesStoreType,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssue } from "@plane/types";
import { setToast, TOAST_TYPE } from "@plane/ui";
// hooks
@ -18,7 +25,6 @@ import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
// plane web hooks
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
import { IssueLayoutHOC } from "../issue-layout-HOC";
@ -38,6 +44,7 @@ export type GanttStoreType =
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
const { viewId, isCompletedCycle = false, isEpic = false } = props;
const { t } = useTranslation();
// router
const { workspaceSlug, projectId } = useParams();
@ -93,8 +100,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
issues.updateIssueDates(workspaceSlug.toString(), projectId.toString(), updates).catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Error while updating Issue Dates, Please try again Later",
title: t("toast.error"),
message: "Error while updating work item dates, Please try again Later",
});
}),
[issues]
@ -121,8 +128,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
<div className="h-full w-full">
<GanttChartRoot
border={false}
title={isEpic ? "Epics" : "Issues"}
loaderTitle={isEpic ? "Epics" : "Issues"}
title={isEpic ? t("epic.label", { count: 2 }) : t("issue.label", { count: 2 })}
loaderTitle={isEpic ? t("epic.label", { count: 2 }) : t("issue.label", { count: 2 })}
blockIds={issuesIds}
blockUpdateHandler={updateIssueBlockStructure}
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} isEpic={isEpic} />}

View file

@ -1,8 +1,8 @@
import { AlertCircle } from "lucide-react";
// Plane
import { ISSUE_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueOrderByOptions } from "@plane/types";
// constants
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
// helpers
import { cn } from "@/helpers/common.helper";
// Plane-web
@ -31,8 +31,13 @@ export const GroupDragOverlay = (props: Props) => {
isEpic = false,
} = props;
// hooks
const { t } = useTranslation();
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
const readableOrderBy = ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.title;
const readableOrderBy = t(
ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.titleTranslationKey || ""
);
return (
<div
@ -67,10 +72,10 @@ export const GroupDragOverlay = (props: Props) => {
<>
{readableOrderBy && (
<span>
The layout is ordered by <span className="font-semibold">{readableOrderBy}</span>.
{t("issue.layouts.ordered_by_label")} <span className="font-semibold">{t(readableOrderBy)}</span>.
</span>
)}
<span>{`Drop here to move the ${isEpic ? "epic" : "issue"}.`}</span>
<span>{t("entity.drop_here_to_move", { entity: isEpic ? "epic" : "work item" })}</span>
</>
)}
</div>

View file

@ -12,6 +12,7 @@ export * from "./calendar";
export * from "./gantt";
export * from "./kanban";
export * from "./spreadsheet";
export * from "./layout-icon";
// properties
export * from "./properties";
@ -21,4 +22,3 @@ export * from "./save-filter-view";
// quick add
export * from "./quick-add";

View file

@ -6,16 +6,22 @@ import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { EIssueLayoutTypes, EIssueServiceType, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import {
EIssueLayoutTypes,
EIssueServiceType,
EIssueFilterType,
EIssuesStoreType,
ISSUE_DELETED,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
import { DeleteIssueModal } from "@/components/issues";
//constants
import { ISSUE_DELETED } from "@/constants/event-tracker";
//hooks
import { useEventTracker, useIssueDetail, useIssues, useKanbanView, useUserPermissions } from "@/hooks/store";
import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// store
// ui
// types
@ -258,7 +264,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
isDragOverDelete ? "bg-red-500 opacity-70 blur-2xl" : ""
} transition duration-300`}
>
Drop here to delete the issue.
Drop here to delete the work item.
</div>
</div>

View file

@ -205,9 +205,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
else {
setToast({
type: TOAST_TYPE.WARNING,
title: "Cannot move issue",
title: "Cannot move work item",
message: !canEditIssueProperties
? "You are not allowed to move this issue"
? "You are not allowed to move this work item"
: "Drag and drop is disabled for the current grouping",
});
}
@ -215,7 +215,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
>
<ControlLink
id={getIssueBlockId(issueId, groupId, subGroupId)}
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${isEpic ? "epics" : "issues"}/${
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${isEpic ? "epics" : "work items"}/${
issue.id
}`}
ref={cardRef}

View file

@ -1,5 +1,7 @@
import { MutableRefObject } from "react";
import { observer } from "mobx-react";
// i18n
import { useTranslation } from "@plane/i18n";
import {
GroupByColumnTypes,
IGroupByColumn,
@ -89,6 +91,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
subGroupIndex = 0,
isEpic = false,
} = props;
// i18n
const { t } = useTranslation();
// store hooks
const storeType = useIssueStoreType();
const issueKanBanView = useKanbanView();

View file

@ -77,13 +77,13 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Issues added to the cycle successfully.",
message: "Work items added to the cycle successfully.",
});
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.",
message: "Selected work items could not be added to the cycle. Please try again.",
});
}
};
@ -171,7 +171,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
setIsOpen(true);
}}
>
<span className="flex items-center justify-start gap-2">Create issue</span>
<span className="flex items-center justify-start gap-2">Create work item</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
@ -179,7 +179,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
setOpenExistingIssueListModal(true);
}}
>
<span className="flex items-center justify-start gap-2">Add an existing issue</span>
<span className="flex items-center justify-start gap-2">Add an existing work item</span>
</CustomMenu.MenuItem>
</CustomMenu>
) : (

View file

@ -6,7 +6,9 @@ import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react";
// plane constants
import { EIssueLayoutTypes } from "@plane/constants";
import { EIssueLayoutTypes, DRAG_ALLOWED_GROUPS } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
//types
import {
TGroupedIssues,
@ -22,7 +24,6 @@ import { KanbanQuickAddIssueButton, QuickAddIssueRoot } from "@/components/issue
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
import { KanbanIssueBlockLoader } from "@/components/ui";
// helpers
import { DRAG_ALLOWED_GROUPS } from "@/constants/issue";
import { cn } from "@/helpers/common.helper";
// hooks
import { useProjectState } from "@/hooks/store";
@ -84,6 +85,8 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
handleOnDrop,
isEpic = false,
} = props;
// i18n
const { t } = useTranslation();
// hooks
const projectState = useProjectState();
@ -155,7 +158,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
dropErrorMessage &&
setToast({
type: TOAST_TYPE.WARNING,
title: "Warning!",
title: t("common.warning"),
message: dropErrorMessage,
});
return;
@ -254,8 +257,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
className="w-full sticky bottom-0 p-3 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 hover:underline cursor-pointer"
onClick={loadMoreIssuesInThisGroup}
>
{" "}
Load More &darr;
{t("common.load_more")} &darr;
</div>
);

View file

@ -2,12 +2,11 @@ import React, { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { EIssuesStoreType } from "@plane/constants";
import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { CycleIssueQuickActions } from "@/components/issues";
// constants
// hooks
import { useCycle, useIssues, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// components
import { BaseKanBanRoot } from "../base-kanban-root";

View file

@ -1,9 +1,9 @@
import { observer } from "mobx-react";
// hooks
import { useParams } from "next/navigation";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { ProjectIssueQuickActions } from "@/components/issues";
import { useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// components
// types

View file

@ -1,11 +1,11 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { ProjectIssueQuickActions } from "@/components/issues";
// hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// local components
import { BaseKanBanRoot } from "../base-kanban-root";

View file

@ -1,5 +1,6 @@
import { MutableRefObject } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import {
GroupByColumnTypes,
IGroupByColumn,
@ -62,7 +63,7 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
if (subGroupByVisibilityToggle === false) return <></>;
const { t } = useTranslation();
return (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
@ -75,7 +76,7 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
collapsedGroups={collapsedGroups}
handleCollapsedGroups={handleCollapsedGroups}
issuePayload={_list.payload}
/>
/>{" "}
</div>
);
})}
@ -155,6 +156,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn, subGroupIndex) => {
const { t } = useTranslation();
const issueCount = getGroupIssueCount(undefined, _list.id, true) ?? 0;
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list, issueCount);
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
@ -165,7 +167,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.icon}
title={_list.name || ""}
title={_list.name}
count={issueCount}
collapsedGroups={collapsedGroups}
handleCollapsedGroups={handleCollapsedGroups}

View file

@ -0,0 +1,19 @@
import { List, Kanban, Calendar, Sheet, GanttChartSquare, LucideProps } from "lucide-react";
import { EIssueLayoutTypes } from "@plane/constants";
export const IssueLayoutIcon = ({ layout, ...props }: { layout: EIssueLayoutTypes } & LucideProps) => {
switch (layout) {
case EIssueLayoutTypes.LIST:
return <List {...props} />;
case EIssueLayoutTypes.KANBAN:
return <Kanban {...props} />;
case EIssueLayoutTypes.CALENDAR:
return <Calendar {...props} />;
case EIssueLayoutTypes.SPREADSHEET:
return <Sheet {...props} />;
case EIssueLayoutTypes.GANTT:
return <GanttChartSquare {...props} />;
default:
return null;
}
};

View file

@ -2,7 +2,13 @@ import { FC, useCallback, useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane constants
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
// types
import { GroupByColumnTypes, TGroupedIssues, TIssueKanbanFilters } from "@plane/types";
// constants
@ -12,7 +18,6 @@ import { useIssues, useUserPermissions } from "@/hooks/store";
import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// components
import { IssueLayoutHOC } from "../issue-layout-HOC";
import { List } from "./default";

View file

@ -175,9 +175,9 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
if (!isDraggingAllowed) {
setToast({
type: TOAST_TYPE.WARNING,
title: "Cannot move issue",
title: "Cannot move work item",
message: !canEditIssueProperties
? "You are not allowed to move this issue"
? "You are not allowed to move this work item"
: "Drag and drop is disabled for the current grouping",
});
}
@ -191,7 +191,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
<Tooltip
tooltipContent={
<>
Only issues within the current
Only work items within the current
<br />
project can be selected.
</>

View file

@ -81,13 +81,13 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Issues added to the cycle successfully.",
message: "Work items added to the cycle successfully.",
});
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.",
message: "Selected work items could not be added to the cycle. Please try again.",
});
}
};
@ -138,7 +138,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
setIsOpen(true);
}}
>
<span className="flex items-center justify-start gap-2">Create issue</span>
<span className="flex items-center justify-start gap-2">Create work item</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
@ -146,7 +146,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
setOpenExistingIssueListModal(true);
}}
>
<span className="flex items-center justify-start gap-2">Add an existing issue</span>
<span className="flex items-center justify-start gap-2">Add an existing work item</span>
</CustomMenu.MenuItem>
</CustomMenu>
) : (

View file

@ -5,7 +5,9 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { observer } from "mobx-react";
// plane constants
import { EIssueLayoutTypes } from "@plane/constants";
import { EIssueLayoutTypes, DRAG_ALLOWED_GROUPS } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
// plane ui
import {
IGroupByColumn,
@ -21,8 +23,6 @@ import { Row, setToast, TOAST_TYPE } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { ListLoaderItemRow } from "@/components/ui";
// constants
import { DRAG_ALLOWED_GROUPS } from "@/constants/issue";
// hooks
import { useProjectState } from "@/hooks/store";
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
@ -101,7 +101,7 @@ export const ListGroup = observer((props: Props) => {
const [dragColumnOrientation, setDragColumnOrientation] = useState<"justify-start" | "justify-end">("justify-start");
const isExpanded = !collapsedGroups?.group_by.includes(group.id);
const groupRef = useRef<HTMLDivElement | null>(null);
const { t } = useTranslation();
const projectState = useProjectState();
const {
@ -132,7 +132,7 @@ export const ListGroup = observer((props: Props) => {
}
onClick={() => loadMoreIssues(group.id)}
>
Load More &darr;
{t("common.load_more")} &darr;
</div>
);
@ -211,10 +211,10 @@ export const ListGroup = observer((props: Props) => {
if (!source || !destination) return;
if (isWorkflowDropDisabled || group.isDropDisabled) {
group.dropErrorMessage &&
if (group.dropErrorMessage)
setToast({
type: TOAST_TYPE.WARNING,
title: "Warning!",
title: t("common.warning"),
message: group.dropErrorMessage,
});
return;
@ -263,7 +263,7 @@ export const ListGroup = observer((props: Props) => {
groupID={group.id}
groupBy={group_by}
icon={group.icon}
title={group.name || ""}
title={group.name}
count={groupIssueCount}
issuePayload={group.payload}
canEditProperties={canEditProperties}

View file

@ -2,12 +2,11 @@ import React, { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { EIssuesStoreType } from "@plane/constants";
import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { CycleIssueQuickActions } from "@/components/issues";
// constants
// hooks
import { useCycle, useIssues, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// types
import { BaseListRoot } from "../base-list-root";

Some files were not shown because too many files have changed in this diff Show more