feat: language support (#6472)
* chore: ln support modules constants * fix: translation key * chore: empty state refactor (#6404) * chore: asset path helper hook added * chore: detailed and simple empty state component added * chore: section empty state component added * chore: language translation for all empty states * chore: new empty state implementation * improvement: add more translations * improvement: user permissions and workspace draft empty state * chore: update translation structure * chore: inbox empty states * chore: disabled project features empty state * chore: active cycle progress empty state * chore: notification empty state * chore: connections translation * chore: issue comment, relation, bulk delete, and command k empty state translation * chore: project pages empty state and translations * chore: project module and view related empty state * chore: remove project draft related empty state * chore: project cycle, views and archived issues empty state * chore: project cycles related empty state * chore: project settings empty state * chore: profile issue and acitivity empty state * chore: workspace settings realted constants * chore: stickies and home widgets empty state * chore: remove all reference to deprecated empty state component and constnats * chore: add support to ignore theme in resolved asset path hook * chore: minor updates * fix: build errors --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * fix: language support fo profile (#6461) * fix: ln support fo profile * fix: merge changes * fix: merge changes * [WEB-3165]feat: language support for issues (#6452) * * chore: moved issue constants to packages * chore: restructured issue constants * improvement: added translations to issue constants * chore: updated translation structure * * chore: updated chinese, spanish and french translation * chore: updated translation for issues mobile header * chore: updated spanish translation * chore: removed translation for issue priorities * fix: build errors * chore: minor updates --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: migrated filters.ts to packages (#6459) Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: workspace drafts constant moved to plane constant package * feat: home language support without stickies (#6443) * feat: home language support without stickies * fix: home sidebar * fix: added missing keys * fix: show all btn * fix: recents empty state * chore: translation update * feat: workspace constant language support and refactor (#6462) * chore: workspace constant language support and refactor * chore: workspace constant language support and refactor * chore: code refactor * chore: code refactor * merge conflict * chore: code refactor --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: tab indices constant moved to plane package (#6464) * chore: notification language support and refactor * chore: ln support for inbox constants (#6432) * chore: ln support for inbox constants * fix: snooze duration * fix: enum * fix: translation keys * fix: inbox status icon * fix: status icon * fix: naming --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * fix: ln support for views constants (#6431) * fix: ln support for views constants * fix: added translation * fix: translation keys * fix: access * chore: code refactor * chore: ln support workspace projects constants (#6429) * chore: ln support workspace projects constants * fix: translation key * fix: removed state translation * fix: removed state translation * fi: added translations * Chore: theme language support and refactor (#6465) * chore: themes language support and refactor * chore: theme language support and refactor * fix * [WEB-3173] chore: language support for cycles constant file (#6415) * chore: ln support for cycles constant file * fix: added chinese * fix: lint * fix: translation key * fix: build errors * minor updates * chore: minor translation update * chore: minor translation update * refactor: move labels contants to packages * refactor: move swr, file and error related constants to packages * chore: timezones constant moved to plane package * chore: metadata constant code refactor * chore: code refactor * fix: dashboard constants moved * chore: code refactor (#6478) * refactor: spreadsheet constants * chore: drafts language support (#6485) * chore: workspace drafts language support * chore: code refactor * feat: ln support for notifications (#6486) * feat: ln support for notifications * fix: translations * * refactor: moved page constants to packages (#6480) * fix: removed use-client * chore: removed unnecessary commnets * chore: workspace draft language support (#6490) * chore: workspace drafts language support * chore: code refactor * chore: draft language support * Feat constant event tracker (#6479) * fix: event tracjer constants * fix: constants event tracker * feat: language translation - projects list (#6493) * feat: added translation to projects list page * chore: restructured translation file * chore: module language support (#6499) * chore: module language support added * chore: code refactor * chore: workspace views language support (#6492) * chore: workspace views language support * chore: code refactor * feat: custom analytics language support (#6494) * feat: custom analytics language support * fix: key * fix: refactoring --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: minor improvements * feat: language support for intake (#6498) * feat: language support for intake * fix: key name * refactor: authentications related translations * feat: language support issues (#6501) * enhancement: added translations for issue list view * chore: added translations for issue detail widgets * chore: added missing translations * chore: modified issue to work items * chore: updated translations * Feat: workspace settings language support (#6508) * feat: language support for workspace settings * fix: lint * fix: export title * chore project settings language support (#6502) * chore: project settings language support * chore: code refactor * refactor: workspace creation related translations * chore: renamed issues to work items * fix: build errors * fix: lint * chore: modified translations * chore: remove duplicate * improvement: french translation * chore: chinese translation improvement * fix: japanese translations * chore: added spanish translation * minor improvements * fix: miscelleous language translations * fix: clear_all key * fix: moved user permission constants (#6516) * feat: language support for issues (#6513) * chore: added language support to issue detail widgets * improvement: added translation for issue detail * enhancement: added language trasnlation to issue layouts * chore: translation improvement (#6518) * feat: language support description (#6519) * enhancement: added language support for description * fix: updated keys * chore: renamed issue to work item (#6522) * chore: replace missing issue occurances to work items * fix: build errors * minor improvements * fix: profile links * Feat ln cycles (#6528) * feat: added language support for cycles * feat: added language support for cycles * chore: added core.json * fix: translation keys * fix: translation keys (#6530) * fix: changed sidebar keys * fix: removed extras * fix: updated keys * chore: optimize translation imports * fix: updated keys (#6534) * fix: updated keys * fix-sub work items toasts * chore: add missing translation and minor fixes * chore: code refactor * fix: language support keys (#6553) * minor improvements * minor fixes * fix: remove lucide import from constants package * chore: regenerate all translations * chore: addded chinese and japanese translation files * chore: remove all from translations * fix: added member * fix: language support keys (#6558) * fix: renamed keys * fix: space app * chore: renamed issues to work items * chore: update site manifest * chore: updated translations * fix: lang keys * chore: update translations --------- Co-authored-by: gakshita <akshitagoyal1516@gmail.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Co-authored-by: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com> Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so> Co-authored-by: Vamsi krishna <matalav55@gmail.com> Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com>
This commit is contained in:
parent
e244f48776
commit
d36c3acbf7
693 changed files with 18182 additions and 10485 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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.`}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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", "");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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">"{query}"</span> to labels
|
||||
{/* TODO: Translate here */}+ Add{" "}
|
||||
<span className="text-custom-text-100">"{query}"</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`),
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 = () => <></>;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ${
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 ↓
|
||||
{t("common.load_more")} ↓
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
19
web/core/components/issues/issue-layouts/layout-icon.tsx
Normal file
19
web/core/components/issues/issue-layouts/layout-icon.tsx
Normal 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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 ↓
|
||||
{t("common.load_more")} ↓
|
||||
</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}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue