From 3072cedeba28bd3755819d4851b0de0a122ef513 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 20 Jan 2025 12:34:03 +0530 Subject: [PATCH 01/93] chore: ln support modules constants --- packages/constants/src/index.ts | 1 + .../constants/src}/module.ts | 47 ++++++---- .../i18n/src/locales/en/translations.json | 26 +++++- .../modules/(list)/mobile-header.tsx | 6 +- .../modules/analytics-sidebar/root.tsx | 10 ++- .../modules/applied-filters/status.tsx | 6 +- .../modules/dropdowns/filters/status.tsx | 9 +- .../components/modules/dropdowns/order-by.tsx | 9 +- .../components/modules/gantt-chart/blocks.tsx | 2 +- .../components/modules/module-card-item.tsx | 3 +- .../modules/module-list-item-action.tsx | 2 +- .../modules/module-status-dropdown.tsx | 86 +++++++++--------- .../components/modules/module-view-header.tsx | 7 +- web/core/components/modules/select/status.tsx | 81 +++++++++-------- .../modules/sidebar-select/select-status.tsx | 88 ++++++++++--------- .../stickies/layout/stickies-list.tsx | 2 +- web/core/constants/empty-state.tsx | 2 +- 17 files changed, 223 insertions(+), 164 deletions(-) rename {web/core/constants => packages/constants/src}/module.ts (56%) diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 2b3964ae9fd..ea735740051 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -14,3 +14,4 @@ export * from "./swr"; export * from "./user"; export * from "./workspace"; export * from "./stickies"; +export * from "./module"; diff --git a/web/core/constants/module.ts b/packages/constants/src/module.ts similarity index 56% rename from web/core/constants/module.ts rename to packages/constants/src/module.ts index 8d26ab5f4e7..45c28223e70 100644 --- a/web/core/constants/module.ts +++ b/packages/constants/src/module.ts @@ -1,6 +1,10 @@ import { GanttChartSquare, LayoutGrid, List } from "lucide-react"; // types -import { TModuleLayoutOptions, TModuleOrderByOptions, TModuleStatus } from "@plane/types"; +import { + TModuleLayoutOptions, + TModuleOrderByOptions, + TModuleStatus, +} from "@plane/types"; export const MODULE_STATUS: { label: string; @@ -10,42 +14,42 @@ export const MODULE_STATUS: { bgColor: string; }[] = [ { - label: "Backlog", + label: "project_modules.status.backlog", value: "backlog", color: "#a3a3a2", textColor: "text-custom-text-400", bgColor: "bg-custom-background-80", }, { - label: "Planned", + label: "project_modules.status.planned", value: "planned", color: "#3f76ff", textColor: "text-blue-500", bgColor: "bg-indigo-50", }, { - label: "In Progress", + label: "project_modules.status.in_progress", value: "in-progress", color: "#f39e1f", textColor: "text-amber-500", bgColor: "bg-amber-50", }, { - label: "Paused", + label: "project_modules.status.paused", value: "paused", color: "#525252", textColor: "text-custom-text-300", bgColor: "bg-custom-background-90", }, { - label: "Completed", + label: "project_modules.status.completed", value: "completed", color: "#16a34a", textColor: "text-green-600", bgColor: "bg-green-100", }, { - label: "Cancelled", + label: "project_modules.status.cancelled", value: "cancelled", color: "#ef4444", textColor: "text-red-500", @@ -53,47 +57,54 @@ export const MODULE_STATUS: { }, ]; -export const MODULE_VIEW_LAYOUTS: { key: TModuleLayoutOptions; icon: any; title: string }[] = [ +export const MODULE_VIEW_LAYOUTS: { + key: TModuleLayoutOptions; + icon: any; + title: string; +}[] = [ { key: "list", icon: List, - title: "List layout", + title: "project_modules.layout.list", }, { key: "board", icon: LayoutGrid, - title: "Gallery layout", + title: "project_modules.layout.board", }, { key: "gantt", icon: GanttChartSquare, - title: "Timeline layout", + title: "project_modules.layout.timeline", }, ]; -export const MODULE_ORDER_BY_OPTIONS: { key: TModuleOrderByOptions; label: string }[] = [ +export const MODULE_ORDER_BY_OPTIONS: { + key: TModuleOrderByOptions; + label: string; +}[] = [ { key: "name", - label: "Name", + label: "project_modules.order_by.name", }, { key: "progress", - label: "Progress", + label: "project_modules.order_by.progress", }, { key: "issues_length", - label: "Number of issues", + label: "project_modules.order_by.issues", }, { key: "target_date", - label: "Due date", + label: "project_modules.order_by.due_date", }, { key: "created_at", - label: "Created date", + label: "project_modules.order_by.created_at", }, { key: "sort_order", - label: "Manual", + label: "project_modules.order_by.manual", }, ]; diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 596c3093fa9..0ba00a266d2 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -316,5 +316,29 @@ "change_parent_issue": "Change parent issue", "remove_parent_issue": "Remove parent issue", "add_parent": "Add parent", - "loading_members": "Loading members..." + "loading_members": "Loading members...", + "project_modules": { + "status": { + "backlog": "Backlog", + "planned": "Planned", + "in_progress": "In Progress", + "paused": "Paused", + "completed": "Completed", + "cancelled": "Cancelled" + }, + "layout": { + "list": "List layout", + "board": "Gallery layout", + "timeline": "Timeline layout" + }, + "order_by": { + "name": "Name", + "progress": "Progress", + "issues": "Number of issues", + "due_date": "Due date", + "created_at": "Created date", + "manual": "Manual" + } + } + } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx index 12301c9d44d..4172e97ce33 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx @@ -2,13 +2,15 @@ import { observer } from "mobx-react"; import { ChevronDown } from "lucide-react"; +import { MODULE_VIEW_LAYOUTS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { CustomMenu, Row } from "@plane/ui"; -import { MODULE_VIEW_LAYOUTS } from "@/constants/module"; import { useModuleFilter, useProject } from "@/hooks/store"; export const ModulesListMobileHeader = observer(() => { const { currentProjectDetails } = useProject(); const { updateDisplayFilters } = useModuleFilter(); + const { t } = useTranslation(); return (
@@ -35,7 +37,7 @@ export const ModulesListMobileHeader = observer(() => { className="flex items-center gap-2" > -
{layout.title}
+
{t(layout.title)}
); })} diff --git a/web/core/components/modules/analytics-sidebar/root.tsx b/web/core/components/modules/analytics-sidebar/root.tsx index cf3ae40c85b..118b62cb2c7 100644 --- a/web/core/components/modules/analytics-sidebar/root.tsx +++ b/web/core/components/modules/analytics-sidebar/root.tsx @@ -18,6 +18,8 @@ import { } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // plane types +import { MODULE_STATUS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; // plane ui import { @@ -46,7 +48,7 @@ import { MODULE_LINK_UPDATED, MODULE_UPDATED, } from "@/constants/event-tracker"; -import { MODULE_STATUS } from "@/constants/module"; + // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; @@ -84,7 +86,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { const { workspaceSlug, projectId } = useParams(); // store hooks - + const { t } = useTranslation(); const { allowPermissions } = useUserPermissions(); const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, restoreModule } = @@ -374,7 +376,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220", }} > - {moduleStatus?.label ?? "Backlog"} + {(moduleStatus && t(moduleStatus?.label)) ?? t("project_modules.status.backlog")} } value={value} @@ -387,7 +389,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => {
- {status.label} + {t(status.label)}
))} diff --git a/web/core/components/modules/applied-filters/status.tsx b/web/core/components/modules/applied-filters/status.tsx index 442dcfadcc8..b4b6ae8625b 100644 --- a/web/core/components/modules/applied-filters/status.tsx +++ b/web/core/components/modules/applied-filters/status.tsx @@ -3,9 +3,10 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; // ui +import { MODULE_STATUS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ModuleStatusIcon } from "@plane/ui"; // constants -import { MODULE_STATUS } from "@/constants/module"; type Props = { handleRemove: (val: string) => void; @@ -15,6 +16,7 @@ type Props = { export const AppliedStatusFilters: React.FC = observer((props) => { const { handleRemove, values, editable } = props; + const { t } = useTranslation(); return ( <> @@ -25,7 +27,7 @@ export const AppliedStatusFilters: React.FC = observer((props) => { return (
- {statusDetails.label} + {t(statusDetails.label)} {editable && (
- } - onChange={onChange} - tabIndex={tabIndex} - noChevron - > - {MODULE_STATUS.map((status) => ( - -
- - {status.label} -
-
- ))} - - )} - /> -); +export const ModuleStatusSelect: React.FC = ({ control, error, tabIndex }) => { + const { t } = useTranslation(); + return ( + { + const selectedValue = MODULE_STATUS.find((s) => s.value === value); + return ( + + {value ? ( + + ) : ( + + )} + {(selectedValue && t(selectedValue?.label)) ?? ( + Status + )} +
+ } + onChange={onChange} + tabIndex={tabIndex} + noChevron + > + {MODULE_STATUS.map((status) => ( + +
+ + {t(status.label)} +
+
+ ))} + + ); + }} + /> + ); +}; diff --git a/web/core/components/modules/sidebar-select/select-status.tsx b/web/core/components/modules/sidebar-select/select-status.tsx index fcac3e01fae..75c5863894f 100644 --- a/web/core/components/modules/sidebar-select/select-status.tsx +++ b/web/core/components/modules/sidebar-select/select-status.tsx @@ -4,11 +4,12 @@ import React from "react"; // react-hook-form import { Control, Controller, UseFormWatch } from "react-hook-form"; +import { MODULE_STATUS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { IModule } from "@plane/types"; // ui import { CustomSelect, DoubleCircleIcon } from "@plane/ui"; // types -import { MODULE_STATUS } from "@/constants/module"; // common // constants @@ -18,45 +19,48 @@ type Props = { watch: UseFormWatch>; }; -export const SidebarStatusSelect: React.FC = ({ control, submitChanges, watch }) => ( -
-
- -

Status

+export const SidebarStatusSelect: React.FC = ({ control, submitChanges, watch }) => { + const { t } = useTranslation(); + return ( +
+
+ +

Status

+
+
+ ( + + option.value === value)?.color, + }} + /> + {watch("status")} + + } + value={value} + onChange={(value: any) => { + submitChanges({ status: value }); + }} + > + {MODULE_STATUS.map((option) => ( + +
+ + {t(option.label)} +
+
+ ))} +
+ )} + /> +
-
- ( - - option.value === value)?.color, - }} - /> - {watch("status")} - - } - value={value} - onChange={(value: any) => { - submitChanges({ status: value }); - }} - > - {MODULE_STATUS.map((option) => ( - -
- - {option.label} -
-
- ))} -
- )} - /> -
-
-); + ); +}; diff --git a/web/core/components/stickies/layout/stickies-list.tsx b/web/core/components/stickies/layout/stickies-list.tsx index 4da6efe7b03..2a743d0cc02 100644 --- a/web/core/components/stickies/layout/stickies-list.tsx +++ b/web/core/components/stickies/layout/stickies-list.tsx @@ -12,13 +12,13 @@ import { Loader } from "@plane/ui"; // components import { EmptyState } from "@/components/empty-state"; // constants +import { StickiesEmptyState } from "@/components/home/widgets/empty-states/stickies"; import { EmptyStateType } from "@/constants/empty-state"; // hooks import { useSticky } from "@/hooks/use-stickies"; import { useStickyOperations } from "../sticky/use-operations"; import { StickyDNDWrapper } from "./sticky-dnd-wrapper"; import { getInstructionFromPayload } from "./sticky.helpers"; -import { StickiesEmptyState } from "@/components/home/widgets/empty-states/stickies"; type TStickiesLayout = { workspaceSlug: string; diff --git a/web/core/constants/empty-state.tsx b/web/core/constants/empty-state.tsx index ef1b9e91829..c2ae08784d7 100644 --- a/web/core/constants/empty-state.tsx +++ b/web/core/constants/empty-state.tsx @@ -1,5 +1,5 @@ -import { EUserPermissions } from "ee/constants/user-permissions"; import { Plus, Shapes } from "lucide-react"; +import { EUserPermissions } from "ee/constants/user-permissions"; export interface EmptyStateDetails { key: EmptyStateType; From f8dc1bdac612526aa9f206f48d9dbb02c9c17056 Mon Sep 17 00:00:00 2001 From: gakshita Date: Wed, 22 Jan 2025 16:32:57 +0530 Subject: [PATCH 02/93] fix: translation key --- packages/constants/src/module.ts | 41 ++++++++----------- .../modules/(list)/mobile-header.tsx | 5 ++- .../modules/analytics-sidebar/root.tsx | 4 +- .../modules/applied-filters/status.tsx | 2 +- .../modules/dropdowns/filters/status.tsx | 2 +- .../components/modules/dropdowns/order-by.tsx | 4 +- web/core/components/modules/index.ts | 2 +- .../components/modules/module-layout-icon.tsx | 38 +++++++++++++++++ .../modules/module-status-dropdown.tsx | 4 +- .../components/modules/module-view-header.tsx | 10 ++--- web/core/components/modules/select/status.tsx | 4 +- .../modules/sidebar-select/select-status.tsx | 2 +- 12 files changed, 74 insertions(+), 44 deletions(-) create mode 100644 web/core/components/modules/module-layout-icon.tsx diff --git a/packages/constants/src/module.ts b/packages/constants/src/module.ts index 45c28223e70..6ce30f0dcf1 100644 --- a/packages/constants/src/module.ts +++ b/packages/constants/src/module.ts @@ -1,4 +1,3 @@ -import { GanttChartSquare, LayoutGrid, List } from "lucide-react"; // types import { TModuleLayoutOptions, @@ -7,49 +6,49 @@ import { } from "@plane/types"; export const MODULE_STATUS: { - label: string; + i18n_label: string; value: TModuleStatus; color: string; textColor: string; bgColor: string; }[] = [ { - label: "project_modules.status.backlog", + i18n_label: "project_modules.status.backlog", value: "backlog", color: "#a3a3a2", textColor: "text-custom-text-400", bgColor: "bg-custom-background-80", }, { - label: "project_modules.status.planned", + i18n_label: "project_modules.status.planned", value: "planned", color: "#3f76ff", textColor: "text-blue-500", bgColor: "bg-indigo-50", }, { - label: "project_modules.status.in_progress", + i18n_label: "project_modules.status.in_progress", value: "in-progress", color: "#f39e1f", textColor: "text-amber-500", bgColor: "bg-amber-50", }, { - label: "project_modules.status.paused", + i18n_label: "project_modules.status.paused", value: "paused", color: "#525252", textColor: "text-custom-text-300", bgColor: "bg-custom-background-90", }, { - label: "project_modules.status.completed", + i18n_label: "project_modules.status.completed", value: "completed", color: "#16a34a", textColor: "text-green-600", bgColor: "bg-green-100", }, { - label: "project_modules.status.cancelled", + i18n_label: "project_modules.status.cancelled", value: "cancelled", color: "#ef4444", textColor: "text-red-500", @@ -59,52 +58,48 @@ export const MODULE_STATUS: { export const MODULE_VIEW_LAYOUTS: { key: TModuleLayoutOptions; - icon: any; - title: string; + i18n_title: string; }[] = [ { key: "list", - icon: List, - title: "project_modules.layout.list", + i18n_title: "project_modules.layout.list", }, { key: "board", - icon: LayoutGrid, - title: "project_modules.layout.board", + i18n_title: "project_modules.layout.board", }, { key: "gantt", - icon: GanttChartSquare, - title: "project_modules.layout.timeline", + i18n_title: "project_modules.layout.timeline", }, ]; export const MODULE_ORDER_BY_OPTIONS: { key: TModuleOrderByOptions; - label: string; + i18n_label: string; }[] = [ { key: "name", - label: "project_modules.order_by.name", + i18n_label: "project_modules.order_by.name", }, { key: "progress", - label: "project_modules.order_by.progress", + i18n_label: "project_modules.order_by.progress", }, { key: "issues_length", - label: "project_modules.order_by.issues", + i18n_label: "project_modules.order_by.issues", }, { key: "target_date", - label: "project_modules.order_by.due_date", + i18n_label: "project_modules.order_by.due_date", }, { key: "created_at", - label: "project_modules.order_by.created_at", + i18n_label: "project_modules.order_by.created_at", }, { key: "sort_order", - label: "project_modules.order_by.manual", + i18n_label: "project_modules.order_by.manual", }, ]; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx index 4172e97ce33..629dca36a1c 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx @@ -5,6 +5,7 @@ import { ChevronDown } from "lucide-react"; import { MODULE_VIEW_LAYOUTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { CustomMenu, Row } from "@plane/ui"; +import { ModuleLayoutIcon } from "@/components/modules"; import { useModuleFilter, useProject } from "@/hooks/store"; export const ModulesListMobileHeader = observer(() => { @@ -36,8 +37,8 @@ export const ModulesListMobileHeader = observer(() => { }} className="flex items-center gap-2" > - -
{t(layout.title)}
+ +
{t(layout.i18n_title)}
); })} diff --git a/web/core/components/modules/analytics-sidebar/root.tsx b/web/core/components/modules/analytics-sidebar/root.tsx index 118b62cb2c7..227619623ed 100644 --- a/web/core/components/modules/analytics-sidebar/root.tsx +++ b/web/core/components/modules/analytics-sidebar/root.tsx @@ -376,7 +376,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220", }} > - {(moduleStatus && t(moduleStatus?.label)) ?? t("project_modules.status.backlog")} + {(moduleStatus && t(moduleStatus?.i18n_label)) ?? t("project_modules.status.backlog")} } value={value} @@ -389,7 +389,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => {
- {t(status.label)} + {t(status.i18n_label)}
))} diff --git a/web/core/components/modules/applied-filters/status.tsx b/web/core/components/modules/applied-filters/status.tsx index b4b6ae8625b..15a2e8b6ee7 100644 --- a/web/core/components/modules/applied-filters/status.tsx +++ b/web/core/components/modules/applied-filters/status.tsx @@ -27,7 +27,7 @@ export const AppliedStatusFilters: React.FC = observer((props) => { return (
- {t(statusDetails.label)} + {t(statusDetails.i18n_label)} {editable && ( ))} diff --git a/web/core/components/modules/select/status.tsx b/web/core/components/modules/select/status.tsx index 79f0b2a93c9..7da437946c1 100644 --- a/web/core/components/modules/select/status.tsx +++ b/web/core/components/modules/select/status.tsx @@ -37,7 +37,7 @@ export const ModuleStatusSelect: React.FC = ({ control, error, tabIndex } ) : ( )} - {(selectedValue && t(selectedValue?.label)) ?? ( + {(selectedValue && t(selectedValue?.i18n_label)) ?? ( Status )}
@@ -50,7 +50,7 @@ export const ModuleStatusSelect: React.FC = ({ control, error, tabIndex }
- {t(status.label)} + {t(status.i18n_label)}
))} diff --git a/web/core/components/modules/sidebar-select/select-status.tsx b/web/core/components/modules/sidebar-select/select-status.tsx index 75c5863894f..9d7a7fda93d 100644 --- a/web/core/components/modules/sidebar-select/select-status.tsx +++ b/web/core/components/modules/sidebar-select/select-status.tsx @@ -53,7 +53,7 @@ export const SidebarStatusSelect: React.FC = ({ control, submitChanges, w
- {t(option.label)} + {t(option.i18n_label)}
))} From ad2dc6759a31c7b86f3b522cb2ba89c1db21c8c0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Sat, 25 Jan 2025 18:54:01 +0530 Subject: [PATCH 03/93] 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 Co-authored-by: sriram veeraghanta --- .../i18n/src/locales/en/translations.json | 469 ++++++++- .../i18n/src/locales/es/translations.json | 468 ++++++++- .../i18n/src/locales/fr/translations.json | 467 ++++++++- .../i18n/src/locales/ja/translations.json | 467 ++++++++- space/app/layout.tsx | 4 +- .../issues/issue-layouts/kanban/default.tsx | 2 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 4 +- .../(projects)/analytics/page.tsx | 45 +- .../(projects)/notifications/page.tsx | 12 +- .../[projectId]/cycles/(list)/page.tsx | 58 +- .../(detail)/[projectId]/inbox/page.tsx | 35 +- .../[projectId]/modules/(list)/page.tsx | 31 +- .../[projectId]/pages/(list)/page.tsx | 32 +- .../[projectId]/views/(list)/page.tsx | 29 +- .../(projects)/settings/api-tokens/page.tsx | 17 +- .../(projects)/settings/webhooks/page.tsx | 20 +- web/app/profile/activity/page.tsx | 19 +- web/app/profile/notifications/page.tsx | 2 +- web/app/profile/page.tsx | 2 +- .../components/cycles/active-cycle/root.tsx | 19 +- .../command-palette/command-modal.tsx | 37 +- .../core/modals/bulk-delete-issues-modal.tsx | 33 +- .../modals/issue-search-modal-empty-state.tsx | 44 +- .../cycles/active-cycle/cycle-stats.tsx | 35 +- .../cycles/active-cycle/productivity.tsx | 15 +- .../cycles/active-cycle/progress.tsx | 12 +- .../cycles/archived-cycles/root.tsx | 17 +- .../components/empty-state/empty-state.tsx | 194 ---- web/core/components/empty-state/index.ts | 1 - .../empty-state/simple-empty-state-root.tsx | 8 +- web/core/components/exporter/guide.tsx | 18 +- .../home/home-dashboard-widgets.tsx | 24 +- .../inbox/modals/select-duplicate.tsx | 40 +- web/core/components/inbox/root.tsx | 13 +- web/core/components/inbox/sidebar/root.tsx | 47 +- web/core/components/integration/guide.tsx | 5 +- .../issue-activity/comments/root.tsx | 19 +- .../empty-states/archived-issues.tsx | 69 +- .../issue-layouts/empty-states/cycle.tsx | 110 +- .../empty-states/draft-issues.tsx | 55 +- .../empty-states/global-view.tsx | 74 +- .../issue-layouts/empty-states/module.tsx | 90 +- .../empty-states/profile-view.tsx | 25 +- .../empty-states/project-epic.tsx | 13 +- .../empty-states/project-issues.tsx | 72 +- .../issues/workspace-draft/empty-state.tsx | 36 +- .../issues/workspace-draft/root.tsx | 38 +- .../labels/project-setting-label-list.tsx | 31 +- .../modules/archived-modules/root.tsx | 18 +- .../components/modules/modules-list-view.tsx | 41 +- .../page-views/workspace-dashboard.tsx | 52 +- .../pages/pages-list-main-content.tsx | 81 +- web/core/components/project/card-list.tsx | 49 +- .../components/project/multi-select-modal.tsx | 17 +- .../stickies/layout/stickies-list.tsx | 61 +- web/core/components/views/views-list.tsx | 47 +- .../sidebar/empty-state.tsx | 29 +- .../components/workspace/sidebar/dropdown.tsx | 2 +- .../workspace/sidebar/user-menu.tsx | 2 +- web/core/constants/empty-state.tsx | 960 ------------------ web/core/constants/profile.ts | 2 +- web/core/hooks/use-resolved-asset-path.tsx | 13 +- .../layouts/auth-layout/project-wrapper.tsx | 43 +- web/core/store/user/permissions.store.ts | 9 +- 64 files changed, 3046 insertions(+), 1757 deletions(-) delete mode 100644 web/core/components/empty-state/empty-state.tsx delete mode 100644 web/core/constants/empty-state.tsx diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 596c3093fa9..d52d125529b 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -38,12 +38,10 @@ "deactivate_account_description": "When deactivating an account, all of the data and resources within that account will be permanently removed and cannot be recovered.", "profile_settings": "Profile settings", "your_account": "Your account", - "profile": "Profile", "security": "Security", "activity": "Activity", "appearance": "Appearance", "notifications": "Notifications", - "inbox": "Inbox", "workspaces": "Workspaces", "create_workspace": "Create workspace", "invitations": "Invitations", @@ -155,7 +153,6 @@ "stay_ahead_of_blockers_description": "Spot challenges from one project to another and see inter-cycle dependencies that aren't obvious from any other view.", "analytics": "Analytics", "workspace_invites": "Workspace invites", - "workspace_settings": "Workspace settings", "enter_god_mode": "Enter god mode", "workspace_logo": "Workspace logo", "new_issue": "New issue", @@ -316,5 +313,469 @@ "change_parent_issue": "Change parent issue", "remove_parent_issue": "Remove parent issue", "add_parent": "Add parent", - "loading_members": "Loading members..." + "loading_members": "Loading members...", + "connections": "Connections", + + "workspace_dashboard": { + "empty_state": { + "general": { + "title": "Overview of your projects, activity, and metrics", + "description": "Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", + "primary_button": { + "text": "Build your first project", + "comic": { + "title": "Everything starts with a project in Plane", + "description": "A project could be a product's roadmap, a marketing campaign, or launching a new car." + } + } + } + } + }, + + "workspace_analytics": { + "empty_state": { + "general": { + "title": "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", + "description": "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", + "primary_button": { + "text": "Start your first project", + "comic": { + "title": "Analytics works best with Cycles + Modules", + "description": "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav." + } + } + } + } + }, + + "workspace_projects": { + "empty_state": { + "general": { + "title": "No active projects", + "description": "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal. Create a new project or filter for archived projects.", + "primary_button": { + "text": "Start your first project", + "comic": { + "title": "Everything starts with a project in Plane", + "description": "A project could be a product's roadmap, a marketing campaign, or launching a new car." + } + } + }, + "no_projects": { + "title": "No project", + "description": "To create issues or manage your work, you need to create a project or be a part of one.", + "primary_button": { + "text": "Start your first project", + "comic": { + "title": "Everything starts with a project in Plane", + "description": "A project could be a product's roadmap, a marketing campaign, or launching a new car." + } + } + }, + "filter": { + "title": "No matching projects", + "description": "No projects detected with the matching criteria. \n Create a new project instead." + } + } + }, + + "workspace_issues": { + "empty_state": { + "all-issues": { + "title": "No issues in the project", + "description": "First project done! Now, slice your work into trackable pieces with issues. Let's go!", + "primary_button": { + "text": "Create new issue" + } + }, + "assigned": { + "title": "No issues yet", + "description": "Issues assigned to you can be tracked from here.", + "primary_button": { + "text": "Create new issue" + } + }, + "created": { + "title": "No issues yet", + "description": "All issues created by you come here, track them here directly.", + "primary_button": { + "text": "Create new issue" + } + }, + "subscribed": { + "title": "No issues yet", + "description": "Subscribe to issues you are interested in, track all of them here." + }, + "custom-view": { + "title": "No issues yet", + "description": "Issues that applies to the filters, track all of them here." + } + } + }, + + "workspace_settings": { + "label": "Workspace settings", + "empty_state": { + "api_tokens": { + "title": "No API tokens created", + "description": "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started." + }, + "webhooks": { + "title": "No webhooks added", + "description": "Create webhooks to receive real-time updates and automate actions." + }, + "exports": { + "title": "No exports yet", + "description": "Anytime you export, you will also have a copy here for reference." + }, + "imports": { + "title": "No imports yet", + "description": "Find all your previous imports here and download them." + } + } + }, + + "profile": { + "label": "Profile", + "empty_state": { + "activity": { + "title": "No activities yet", + "description": "Get started by creating a new issue! Add details and properties to it. Explore more in Plane to see your activity." + }, + "assigned": { + "title": "No issues are assigned to you", + "description": "Issues assigned to you can be tracked from here." + }, + "created": { + "title": "No issues yet", + "description": "All issues created by you come here, track them here directly." + }, + "subscribed": { + "title": "No issues yet", + "description": "Subscribe to issues you are interested in, track all of them here." + } + } + }, + + "project_settings": { + "empty_state": { + "labels": { + "title": "No labels yet", + "description": "Create labels to help organize and filter issues in you project." + } + } + }, + + "project_cycles": { + "empty_state": { + "general": { + "title": "Group and timebox your work in Cycles.", + "description": "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", + "primary_button": { + "text": "Set your first cycle", + "comic": { + "title": "Cycles are repetitive time-boxes.", + "description": "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle." + } + } + }, + "no_issues": { + "title": "No issues added to the cycle", + "description": "Add or create issues you wish to timebox and deliver within this cycle", + "primary_button": { + "text": "Create new issue" + }, + "secondary_button": { + "text": "Add existing issue" + } + }, + "completed_no_issues": { + "title": "No issues in the cycle", + "description": "No issues in the cycle. Issues are either transferred or hidden. To see hidden issues if any, update your display properties accordingly." + }, + "active": { + "title": "No active cycle", + "description": "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here." + }, + "archived": { + "title": "No archived cycles yet", + "description": "To tidy up your project, archive completed cycles. Find them here once archived." + } + } + }, + + "project_issues": { + "empty_state": { + "no_issues": { + "title": "Create an issue and assign it to someone, even yourself", + "description": "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", + "primary_button": { + "text": "Create your first issue", + "comic": { + "title": "Issues are building blocks in Plane.", + "description": "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues." + } + } + }, + "no_archived_issues": { + "title": "No archived issues yet", + "description": "Manually or through automation, you can archive issues that are completed or cancelled. Find them here once archived.", + "primary_button": { + "text": "Set automation" + } + }, + "issues_empty_filter": { + "title": "No issues found matching the filters applied", + "secondary_button": { + "text": "Clear all filters" + } + } + } + }, + + "project_module": { + "empty_state": { + "general": { + "title": "Map your project milestones to Modules and track aggregated work easily.", + "description": "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", + "primary_button": { + "text": "Build your first module", + "comic": { + "title": "Modules help group work by hierarchy.", + "description": "A cart module, a chassis module, and a warehouse module are all good example of this grouping." + } + } + }, + "no_issues": { + "title": "No issues in the module", + "description": "Create or add issues which you want to accomplish as part of this module", + "primary_button": { + "text": "Create new issue" + }, + "secondary_button": { + "text": "Add an existing issue" + } + }, + "archived": { + "title": "No archived Modules yet", + "description": "To tidy up your project, archive completed or cancelled modules. Find them here once archived." + } + } + }, + + "project_views": { + "empty_state": { + "general": { + "title": "Save filtered views for your project. Create as many as you need", + "description": "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", + "primary_button": { + "text": "Create your first view", + "comic": { + "title": "Views work atop Issue properties.", + "description": "You can create a view from here with as many properties as filters as you see fit." + } + } + }, + "filter": { + "title": "No matching views", + "description": "No views match the search criteria. \n Create a new view instead." + } + } + }, + + "project_page": { + "empty_state": { + "general": { + "title": "Write a note, a doc, or a full knowledge base. Get Galileo, Plane's AI assistant, to help you get started", + "description": "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project's context. To make short work of any doc, invoke Galileo, Plane's AI, with a shortcut or the click of a button.", + "primary_button": { + "text": "Create your first page" + } + }, + "private": { + "title": "No private pages yet", + "description": "Keep your private thoughts here. When you're ready to share, the team's just a click away.", + "primary_button": { + "text": "Create your first page" + } + }, + "public": { + "title": "No public pages yet", + "description": "See pages shared with everyone in your project right here.", + "primary_button": { + "text": "Create your first page" + } + }, + "archived": { + "title": "No archived pages yet", + "description": "Archive pages not on your radar. Access them here when needed." + } + } + }, + + "command_k": { + "empty_state": { + "search": { + "title": "No results found" + } + } + }, + + "issue_relation": { + "empty_state": { + "search": { + "title": "No matching issues found" + }, + "no_issues": { + "title": "No issues found" + } + } + }, + + "issue_comment": { + "empty_state": { + "general": { + "title": "No comments yet", + "description": "Comments can be used as a discussion and follow-up space for the issues" + } + } + }, + + "notification": { + "empty_state": { + "detail": { + "title": "Select to view details." + }, + "all": { + "title": "No issues assigned", + "description": "Updates for issues assigned to you can be \n seen here" + }, + "mentions": { + "title": "No issues assigned", + "description": "Updates for issues assigned to you can be \n seen here" + } + } + }, + + "active_cycle": { + "empty_state": { + "progress": { + "title": "Add issues to the cycle to view it's progress" + }, + "chart": { + "title": "Add issues to the cycle to view the burndown chart." + }, + "priority_issue": { + "title": "Observe high priority issues tackled in the cycle at a glance." + }, + "assignee": { + "title": "Add assignees to issues to see a breakdown of work by assignees." + }, + "label": { + "title": "Add labels to issues to see the breakdown of work by labels." + } + } + }, + + "disabled_project": { + "empty_state": { + "inbox": { + "title": "Intake is not enabled for the project.", + "description": "Intake helps you manage incoming requests to your project and add them as issues in your workflow. Enable intake from project settings to manage requests.", + "primary_button": { + "text": "Manage features" + } + }, + "cycle": { + "title": "Cycles is not enabled for this project.", + "description": "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team. Enable the cycles feature for your project to start using them.", + "primary_button": { + "text": "Manage features" + } + }, + "module": { + "title": "Modules are not enabled for the project.", + "description": "Modules are the building blocks of your project. Enable modules from project settings to start using them.", + "primary_button": { + "text": "Manage features" + } + }, + "page": { + "title": "Pages are not enabled for the project.", + "description": "Pages are the building blocks of your project. Enable pages from project settings to start using them.", + "primary_button": { + "text": "Manage features" + } + }, + "view": { + "title": "Views are not enabled for the project.", + "description": "Views are the building blocks of your project. Enable views from project settings to start using them.", + "primary_button": { + "text": "Manage features" + } + } + } + }, + + "inbox": { + "label": "Inbox", + "empty_state": { + "sidebar_open_tab": { + "title": "No open issues", + "description": "Find open issues here. Create new issue." + }, + "sidebar_closed_tab": { + "title": "No closed issues", + "description": "All the issues whether accepted or declined can be found here." + }, + "sidebar_filter": { + "title": "No matching issues", + "description": "No issue matches filter applied in intake. Create a new issue." + }, + "detail": { + "title": "Select an issue to view its details." + } + } + }, + + "workspace_draft_issues": { + "empty_state": { + "title": "Half-written issues, and soon, comments will show up here.", + "description": "To try this out, start adding an issue and leave it mid-way or create your first draft below. 😉", + "primary_button": { + "text": "Create your first draft" + } + } + }, + + "stickies": { + "empty_state": { + "general": { + "title": "Stickies are quick notes and to-dos you take down on the fly.", + "description": "Capture your thoughts and ideas effortlessly by creating stickies that you can access anytime and from anywhere.", + "primary_button": { + "text": "Add sticky" + } + }, + "search": { + "title": "That doesn't match any of your stickies.", + "description": "Try a different term or let us know\nif you are sure your search is right. ", + "primary_button": { + "text": "Add sticky" + } + } + } + }, + + "home_widgets": { + "empty_state": { + "general": { + "title": "It's Quiet Without Widgets, Turn Them On", + "description": "It looks like all your widgets are turned off. Enable them\nnow to enhance your experience!", + "primary_button": { + "text": "Manage widgets" + } + } + } + } } diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 9f2b98792c2..f516cf152d6 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -38,7 +38,6 @@ "deactivate_account_description": "Al desactivar una cuenta, todos los datos y recursos dentro de esa cuenta se eliminarán permanentemente y no se podrán recuperar.", "profile_settings": "Configuración de perfil", "your_account": "Tu cuenta", - "profile": "Perfil", "security": "Seguridad", "activity": "Actividad", "appearance": "Apariencia", @@ -114,7 +113,6 @@ "plane_logo": "Logo de Plane", "workspace_creation_disabled": "Creación de espacio de trabajo deshabilitada", "workspace_request_subject": "Solicitando un nuevo espacio de trabajo", - "workspace_request_body": "Hola administrador(es) de instancia,\n\nPor favor, crea un nuevo espacio de trabajo con la URL [/nombre-del-espacio-de-trabajo] para [propósito de crear el espacio de trabajo].\n\nGracias,\n{{firstName}} {{lastName}}\n{{email}}", "creating_workspace": "Creando espacio de trabajo", "workspace_created_successfully": "Espacio de trabajo creado con éxito", "create_workspace_page": "Página de creación de espacio de trabajo", @@ -154,7 +152,6 @@ "stay_ahead_of_blockers_description": "Detecta desafíos de un proyecto a otro y ve dependencias entre ciclos que no son obvias desde ninguna otra vista.", "analytics": "Analítica", "workspace_invites": "Invitaciones al espacio de trabajo", - "workspace_settings": "Configuración del espacio de trabajo", "enter_god_mode": "Entrar en modo dios", "workspace_logo": "Logo del espacio de trabajo", "new_issue": "Nuevo problema", @@ -316,5 +313,468 @@ "remove_parent_issue": "Eliminar problema padre", "add_parent": "Agregar padre", "loading_members": "Cargando miembros...", - "inbox": "bandeja de entrada" + "connections": "Conexiones", + + "workspace_dashboard": { + "empty_state": { + "general": { + "title": "Resumen de tus proyectos, actividad y métricas", + "description": "Bienvenido a Plane, estamos emocionados de tenerte aquí. Crea tu primer proyecto y rastrea tus problemas, y esta página se transformará en un espacio que te ayudará a avanzar. Los administradores también verán elementos que ayudan a su equipo a progresar.", + "primary_button": { + "text": "Construye tu primer proyecto", + "comic": { + "title": "Todo comienza con un proyecto en Plane", + "description": "Un proyecto podría ser la hoja de ruta de un producto, una campaña de marketing o el lanzamiento de un nuevo coche." + } + } + } + } + }, + + "workspace_analytics": { + "empty_state": { + "general": { + "title": "Rastrea el progreso, cargas de trabajo y asignaciones. Detecta tendencias, elimina bloqueos y acelera el trabajo", + "description": "Consulta el alcance frente a la demanda, estimaciones y desbordamiento del alcance. Obtén el rendimiento por miembros del equipo y equipos, y asegúrate de que tu proyecto termine a tiempo.", + "primary_button": { + "text": "Comienza tu primer proyecto", + "comic": { + "title": "La analítica funciona mejor con Ciclos + Módulos", + "description": "Primero, organiza tus problemas en ciclos y, si puedes, agrupa problemas que abarquen más de un ciclo en módulos. Consulta ambos en el menú de navegación a la izquierda." + } + } + } + } + }, + + "workspace_projects": { + "empty_state": { + "general": { + "title": "No hay proyectos activos", + "description": "Piensa en cada proyecto como el contenedor para trabajo orientado a objetivos. Los proyectos son donde viven Trabajos, Ciclos y Módulos y, junto con tus colegas, te ayudan a lograr ese objetivo. Crea un nuevo proyecto o filtra por proyectos archivados.", + "primary_button": { + "text": "Comienza tu primer proyecto", + "comic": { + "title": "Todo comienza con un proyecto en Plane", + "description": "Un proyecto podría ser la hoja de ruta de un producto, una campaña de marketing o el lanzamiento de un nuevo coche." + } + } + }, + "no_projects": { + "title": "Sin proyectos", + "description": "Para crear problemas o gestionar tu trabajo, necesitas crear un proyecto o ser parte de uno.", + "primary_button": { + "text": "Comienza tu primer proyecto", + "comic": { + "title": "Todo comienza con un proyecto en Plane", + "description": "Un proyecto podría ser la hoja de ruta de un producto, una campaña de marketing o el lanzamiento de un nuevo coche." + } + } + }, + "filter": { + "title": "No hay proyectos coincidentes", + "description": "No se detectaron proyectos con los criterios coincidentes. \n Crea un nuevo proyecto en su lugar." + } + } + }, + + "workspace_issues": { + "empty_state": { + "all-issues": { + "title": "No hay problemas en el proyecto", + "description": "¡Primer proyecto terminado! Ahora, divide tu trabajo en piezas rastreables con problemas. ¡Vamos!", + "primary_button": { + "text": "Crear nuevo problema" + } + }, + "assigned": { + "title": "Aún no hay problemas", + "description": "Los problemas asignados a ti se pueden rastrear desde aquí.", + "primary_button": { + "text": "Crear nuevo problema" + } + }, + "created": { + "title": "Aún no hay problemas", + "description": "Todos los problemas creados por ti aparecen aquí. Rastréalos directamente.", + "primary_button": { + "text": "Crear nuevo problema" + } + }, + "subscribed": { + "title": "Aún no hay problemas", + "description": "Suscríbete a los problemas que te interesan y rastrea todos ellos aquí." + }, + "custom-view": { + "title": "Aún no hay problemas", + "description": "Los problemas que se aplican a los filtros se rastrean aquí." + } + } + }, + + "workspace_settings": { + "label": "Configuración del espacio de trabajo", + "empty_state": { + "api_tokens": { + "title": "No se han creado tokens API", + "description": "Las APIs de Plane pueden ser usadas para integrar tus datos en Plane con cualquier sistema externo. Crea un token para comenzar." + }, + "webhooks": { + "title": "No se han agregado webhooks", + "description": "Crea webhooks para recibir actualizaciones en tiempo real y automatizar acciones." + }, + "exports": { + "title": "Aún no hay exportaciones", + "description": "Cada vez que exportes, también tendrás una copia aquí para referencia." + }, + "imports": { + "title": "Aún no hay importaciones", + "description": "Encuentra todas tus importaciones previas aquí y descárgalas." + } + } + }, + + "profile": { + "label": "Perfil", + "empty_state": { + "activity": { + "title": "Aún no hay actividades", + "description": "¡Comienza creando un nuevo problema! Añade detalles y propiedades a él. Explora más en Plane para ver tu actividad." + }, + "assigned": { + "title": "No tienes problemas asignados", + "description": "Los problemas asignados a ti se pueden rastrear desde aquí." + }, + "created": { + "title": "Aún no hay problemas", + "description": "Todos los problemas creados por ti aparecen aquí. Rastréalos directamente." + }, + "subscribed": { + "title": "Aún no hay problemas", + "description": "Suscríbete a los problemas que te interesan y rastrea todos ellos aquí." + } + } + }, + + "project_settings": { + "empty_state": { + "labels": { + "title": "Aún no hay etiquetas", + "description": "Crea etiquetas para ayudar a organizar y filtrar problemas en tu proyecto." + } + } + }, + + "project_cycles": { + "empty_state": { + "general": { + "title": "Agrupa y organiza tu trabajo en Ciclos.", + "description": "Divide el trabajo en partes organizadas por plazos, retrocede desde la fecha límite de tu proyecto para establecer fechas y haz un progreso tangible como equipo.", + "primary_button": { + "text": "Configura tu primer ciclo", + "comic": { + "title": "Los ciclos son bloques de tiempo repetitivos.", + "description": "Un sprint, una iteración o cualquier otro término que uses para el seguimiento semanal o quincenal del trabajo es un ciclo." + } + } + }, + "no_issues": { + "title": "No hay problemas añadidos al ciclo", + "description": "Añade o crea problemas que desees organizar y entregar dentro de este ciclo", + "primary_button": { + "text": "Crear nuevo problema" + }, + "secondary_button": { + "text": "Añadir problema existente" + } + }, + "completed_no_issues": { + "title": "No hay problemas en el ciclo", + "description": "No hay problemas en el ciclo. Los problemas se han transferido o están ocultos. Para ver problemas ocultos, si los hay, actualiza las propiedades de visualización en consecuencia." + }, + "active": { + "title": "No hay ciclo activo", + "description": "Un ciclo activo incluye cualquier período que abarque la fecha de hoy dentro de su rango. Encuentra el progreso y los detalles del ciclo activo aquí." + }, + "archived": { + "title": "No hay ciclos archivados todavía", + "description": "Para organizar tu proyecto, archiva ciclos completados. Encuéntralos aquí una vez archivados." + } + } + }, + + "project_issues": { + "empty_state": { + "no_issues": { + "title": "Crea un problema y asígnalo a alguien, incluso a ti mismo", + "description": "Piensa en los problemas como trabajos, tareas, acciones, o JTBD (trabajos por hacer). Nos gusta eso. Un problema y sus subproblemas suelen ser acciones basadas en el tiempo asignadas a los miembros de tu equipo. Tu equipo crea, asigna y completa problemas para avanzar el proyecto hacia su objetivo.", + "primary_button": { + "text": "Crea tu primer problema", + "comic": { + "title": "Los problemas son bloques de construcción en Plane.", + "description": "Rediseñar la interfaz de Plane, Rebrandear la empresa o Lanzar el nuevo sistema de inyección de combustible son ejemplos de problemas que probablemente tengan subproblemas." + } + } + }, + "no_archived_issues": { + "title": "Aún no hay problemas archivados", + "description": "De forma manual o mediante automatización, puedes archivar problemas que estén completados o cancelados. Encuéntralos aquí una vez archivados.", + "primary_button": { + "text": "Configurar automatización" + } + }, + "issues_empty_filter": { + "title": "No se encontraron problemas que coincidan con los filtros aplicados", + "secondary_button": { + "text": "Borrar todos los filtros" + } + } + } + }, + + "project_module": { + "empty_state": { + "no_issues": { + "title": "No hay problemas en el módulo", + "description": "Crea o agrega problemas que deseas lograr como parte de este módulo", + "primary_button": { + "text": "Crear nuevo problema" + }, + "secondary_button": { + "text": "Agregar un problema existente" + } + }, + "general": { + "title": "Mapea los hitos de tu proyecto a Módulos y sigue el trabajo agregado fácilmente.", + "description": "Un grupo de problemas que pertenecen a un padre lógico y jerárquico forma un módulo. Piénsalos como una forma de rastrear el trabajo por hitos del proyecto. Tienen sus propios períodos y plazos, así como análisis para ayudarte a ver qué tan cerca o lejos estás de un hito.", + "primary_button": { + "text": "Construye tu primer módulo", + "comic": { + "title": "Los módulos ayudan a agrupar el trabajo por jerarquía.", + "description": "Un módulo de carrito, un módulo de chasis y un módulo de almacén son buenos ejemplos de esta agrupación." + } + } + }, + "archived": { + "title": "Aún no hay módulos archivados", + "description": "Para organizar tu proyecto, archiva módulos completados o cancelados. Encuéntralos aquí una vez archivados." + } + } + }, + + "project_views": { + "empty_state": { + "general": { + "title": "Guarda vistas filtradas para tu proyecto. Crea tantas como necesites", + "description": "Las vistas son un conjunto de filtros guardados que usas con frecuencia o a los que deseas tener acceso fácil. Todos tus colegas en un proyecto pueden ver las vistas de todos y elegir la que mejor se adapte a sus necesidades.", + "primary_button": { + "text": "Crea tu primera vista", + "comic": { + "title": "Las vistas funcionan sobre las propiedades de los problemas.", + "description": "Puedes crear una vista desde aquí con tantas propiedades como filtros que consideres necesarios." + } + } + }, + "filter": { + "title": "No hay vistas coincidentes", + "description": "Ninguna vista coincide con los criterios de búsqueda. \n Crea una nueva vista en su lugar." + } + } + }, + + "project_page": { + "empty_state": { + "general": { + "title": "Escribe una nota, un documento o una base de conocimiento completa. Deja que Galileo, el asistente de IA de Plane, te ayude a empezar", + "description": "Las páginas son un espacio para capturar ideas en Plane. Toma notas de reuniones, formatearlas fácilmente, incrusta problemas, organiza usando una biblioteca de componentes y mantén todo en el contexto de tu proyecto. Para simplificar cualquier documento, invoca a Galileo, la IA de Plane, con un atajo o un clic.", + "primary_button": { + "text": "Crea tu primera página" + } + }, + "private": { + "title": "Aún no hay páginas privadas", + "description": "Guarda aquí tus pensamientos privados. Cuando estés listo para compartirlos, el equipo está a un clic de distancia.", + "primary_button": { + "text": "Crea tu primera página" + } + }, + "public": { + "title": "Aún no hay páginas públicas", + "description": "Consulta aquí las páginas compartidas con todos en tu proyecto.", + "primary_button": { + "text": "Crea tu primera página" + } + }, + "archived": { + "title": "Aún no hay páginas archivadas", + "description": "Archiva las páginas que no están en tu radar. Accede a ellas aquí cuando las necesites." + } + } + }, + + "command_k": { + "empty_state": { + "search": { + "title": "No se encontraron resultados" + } + } + }, + + "issue_relation": { + "empty_state": { + "search": { + "title": "No se encontraron problemas coincidentes" + }, + "general": { + "title": "No se encontraron problemas" + } + } + }, + + "issue_comment": { + "empty_state": { + "general": { + "title": "Aún no hay comentarios", + "description": "Los comentarios pueden usarse como un espacio de discusión y seguimiento para los problemas" + } + } + }, + + "notification": { + "empty_state": { + "detail": { + "title": "Selecciona para ver los detalles." + }, + "all": { + "title": "No hay problemas asignados", + "description": "Las actualizaciones de los problemas asignados a ti se \n pueden ver aquí" + }, + "mentions": { + "title": "No hay problemas asignados", + "description": "Las actualizaciones de los problemas asignados a ti se \n pueden ver aquí" + } + } + }, + + "active_cycle": { + "empty_state": { + "progress": { + "title": "Agrega problemas al ciclo para ver su progreso." + }, + "chart": { + "title": "Agrega problemas al ciclo para ver el gráfico de burndown." + }, + "priority_issue": { + "title": "Observa de un vistazo los problemas de alta prioridad abordados en el ciclo." + }, + "assignee": { + "title": "Asigna responsables a los problemas para ver un desglose del trabajo por responsables." + }, + "label": { + "title": "Agrega etiquetas a los problemas para ver el desglose del trabajo por etiquetas." + } + } + }, + + "disabled_project": { + "empty_state": { + "inbox": { + "title": "La entrada no está habilitada para el proyecto.", + "description": "La entrada te ayuda a gestionar las solicitudes entrantes para tu proyecto y agregarlas como problemas en tu flujo de trabajo. Habilita la entrada desde la configuración del proyecto para gestionar solicitudes.", + "primary_button": { + "text": "Gestionar funciones" + } + }, + "cycle": { + "title": "Los ciclos no están habilitados para este proyecto.", + "description": "Divide el trabajo en bloques de tiempo, trabaja hacia atrás desde la fecha límite de tu proyecto para establecer fechas y logra avances tangibles como equipo. Habilita la función de ciclos para tu proyecto para comenzar a usarlos.", + "primary_button": { + "text": "Gestionar funciones" + } + }, + "module": { + "title": "Los módulos no están habilitados para el proyecto.", + "description": "Los módulos son los bloques de construcción de tu proyecto. Habilita módulos desde la configuración del proyecto para comenzar a usarlos.", + "primary_button": { + "text": "Gestionar funciones" + } + }, + "page": { + "title": "Las páginas no están habilitadas para el proyecto.", + "description": "Las páginas son los bloques de construcción de tu proyecto. Habilita páginas desde la configuración del proyecto para comenzar a usarlas.", + "primary_button": { + "text": "Gestionar funciones" + } + }, + "view": { + "title": "Las vistas no están habilitadas para el proyecto.", + "description": "Las vistas son los bloques de construcción de tu proyecto. Habilita vistas desde la configuración del proyecto para comenzar a usarlas.", + "primary_button": { + "text": "Gestionar funciones" + } + } + } + }, + + "inbox": { + "label": "Bandeja de entrada", + "empty_state": { + "sidebar_open_tab": { + "title": "No hay problemas abiertos", + "description": "Encuentra los problemas abiertos aquí. Crea un nuevo problema." + }, + "sidebar_closed_tab": { + "title": "No hay problemas cerrados", + "description": "Todos los problemas, ya sean aceptados o rechazados, se pueden encontrar aquí." + }, + "sidebar_filter": { + "title": "No hay problemas coincidentes", + "description": "Ningún problema coincide con el filtro aplicado en la entrada. Crea un nuevo problema." + }, + "detail": { + "title": "Selecciona un problema para ver sus detalles." + } + } + }, + + "workspace_draft_issues": { + "empty_state": { + "title": "Los problemas a medio escribir, y pronto, los comentarios aparecerán aquí.", + "description": "Para probar esto, comienza a agregar un problema y déjalo a medias o crea tu primer borrador abajo. 😉", + "primary_button": { + "text": "Crea tu primer borrador" + } + } + }, + + "stickies": { + "empty_state": { + "general": { + "title": "Los stickies son notas rápidas y tareas que tomas al vuelo.", + "description": "Captura tus pensamientos e ideas sin esfuerzo creando stickies a los que puedes acceder en cualquier momento y lugar.", + "primary_button": { + "text": "Agregar sticky" + } + }, + "search": { + "title": "Eso no coincide con ninguno de tus stickies.", + "description": "Prueba con un término diferente o avísanos si estás seguro de que tu búsqueda es correcta.", + "primary_button": { + "text": "Agregar sticky" + } + } + } + }, + + "home_widgets": { + "empty_state": { + "general": { + "title": "Está tranquilo sin widgets, actívalos", + "description": "Parece que todos tus widgets están desactivados. ¡Enciéndelos ahora para mejorar tu experiencia!", + "primary_button": { + "text": "Gestionar widgets" + } + } + } + } } diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 0eee868e174..4d5d7512efd 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -38,7 +38,6 @@ "deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées.", "profile_settings": "Paramètres du profil", "your_account": "Votre compte", - "profile": "Profil", "security": " Sécurité", "activity": "Activité", "appearance": "Apparence", @@ -154,7 +153,6 @@ "stay_ahead_of_blockers_description": "Repérez les défis d'un projet à l'autre et identifiez les dépendances entre cycles qui ne sont pas évidentes depuis d'autres vues.", "analytics": "Analyse", "workspace_invites": "Invitations de l'espace de travail", - "workspace_settings": "Paramètres de l'espace de travail", "enter_god_mode": "Entrer en mode dieu", "workspace_logo": "Logo de l'espace de travail", "new_issue": "Nouveau problème", @@ -316,5 +314,468 @@ "remove_parent_issue": "Supprimer le problème parent", "add_parent": "Ajouter un parent", "loading_members": "Chargement des membres...", - "inbox": "Boîte de réception" + "connections": "Connexions", + + "workspace_dashboard": { + "empty_state": { + "general": { + "title": "Vue d'ensemble de vos projets, activités et métriques", + "description": "Bienvenue sur Plane, nous sommes ravis de vous accueillir. Créez votre premier projet et suivez vos tâches, et cette page se transformera en un espace qui vous aidera à progresser. Les administrateurs verront également des éléments pour aider leur équipe à progresser.", + "primary_button": { + "text": "Créez votre premier projet", + "comic": { + "title": "Tout commence par un projet sur Plane", + "description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture." + } + } + } + } + }, + + "workspace_analytics": { + "empty_state": { + "general": { + "title": "Suivez les progrès, les charges de travail et les allocations. Repérez les tendances, éliminez les obstacles et accélérez le travail", + "description": "Visualisez l'étendue par rapport à la demande, les estimations et l'expansion des objectifs. Obtenez des performances par membre et par équipe, et assurez-vous que votre projet respecte les délais.", + "primary_button": { + "text": "Commencez votre premier projet", + "comic": { + "title": "Les analyses fonctionnent mieux avec Cycles + Modules", + "description": "D'abord, cadrez vos tâches dans des Cycles et, si possible, regroupez les tâches qui s'étendent sur plus d'un cycle dans des Modules. Consultez-les dans la navigation à gauche." + } + } + } + } + }, + + "workspace_projects": { + "empty_state": { + "general": { + "title": "Aucun projet actif", + "description": "Considérez chaque projet comme le parent des travaux orientés vers un objectif. Les projets sont l'endroit où vivent les tâches, les Cycles et les Modules, et avec vos collègues, ils vous aident à atteindre cet objectif. Créez un nouveau projet ou filtrez les projets archivés.", + "primary_button": { + "text": "Commencez votre premier projet", + "comic": { + "title": "Tout commence par un projet sur Plane", + "description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture." + } + } + }, + "no_projects": { + "title": "Aucun projet", + "description": "Pour créer des tâches ou gérer votre travail, vous devez créer un projet ou en faire partie.", + "primary_button": { + "text": "Commencez votre premier projet", + "comic": { + "title": "Tout commence par un projet sur Plane", + "description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture." + } + } + }, + "filter": { + "title": "Aucun projet correspondant", + "description": "Aucun projet détecté avec les critères correspondants. \n Créez un nouveau projet à la place." + } + } + }, + + "workspace_issues": { + "empty_state": { + "all-issues": { + "title": "Aucune tâche dans le projet", + "description": "Premier projet terminé ! Maintenant, divisez votre travail en morceaux traçables avec des tâches. Allons-y !", + "primary_button": { + "text": "Créer une nouvelle tâche" + } + }, + "assigned": { + "title": "Aucune tâche assignée", + "description": "Les tâches qui vous sont assignées peuvent être suivies ici.", + "primary_button": { + "text": "Créer une nouvelle tâche" + } + }, + "created": { + "title": "Aucune tâche créée", + "description": "Toutes les tâches que vous avez créées se trouvent ici. Suivez-les directement ici.", + "primary_button": { + "text": "Créer une nouvelle tâche" + } + }, + "subscribed": { + "title": "Aucune tâche suivie", + "description": "Abonnez-vous aux tâches qui vous intéressent, suivez-les toutes ici." + }, + "custom-view": { + "title": "Aucune tâche trouvée", + "description": "Les tâches correspondant aux filtres sont affichées ici. Suivez-les toutes ici." + } + } + }, + + "workspace_settings": { + "label": "Paramètres de l'espace de travail", + "empty_state": { + "api_tokens": { + "title": "Aucun jeton API créé", + "description": "Les API Plane peuvent être utilisées pour intégrer vos données dans Plane avec n'importe quel système externe. Créez un jeton pour commencer." + }, + "webhooks": { + "title": "Aucun webhook ajouté", + "description": "Créez des webhooks pour recevoir des mises à jour en temps réel et automatiser des actions." + }, + "exports": { + "title": "Aucune exportation pour le moment", + "description": "Chaque fois que vous exportez, une copie sera également disponible ici pour référence." + }, + "imports": { + "title": "Aucune importation pour le moment", + "description": "Retrouvez toutes vos importations précédentes ici et téléchargez-les." + } + } + }, + + "profile": { + "label": "Profil", + "empty_state": { + "activity": { + "title": "Aucune activité pour le moment", + "description": "Commencez par créer une nouvelle tâche ! Ajoutez des détails et des propriétés à celle-ci. Explorez davantage Plane pour voir votre activité." + }, + "assigned": { + "title": "Aucune tâche assignée", + "description": "Les tâches qui vous sont assignées peuvent être suivies ici." + }, + "created": { + "title": "Aucune tâche créée", + "description": "Toutes les tâches que vous avez créées se trouvent ici. Suivez-les directement ici." + }, + "subscribed": { + "title": "Aucune tâche suivie", + "description": "Abonnez-vous aux tâches qui vous intéressent, suivez-les toutes ici." + } + } + }, + + "project_settings": { + "empty_state": { + "labels": { + "title": "Aucune étiquette pour le moment", + "description": "Créez des étiquettes pour organiser et filtrer les tâches de votre projet." + } + } + }, + + "project_cycles": { + "empty_state": { + "general": { + "title": "Groupez et cadrez votre travail en cycles.", + "description": "Décomposez le travail en périodes définies, travaillez à rebours à partir de votre date limite pour fixer des échéances, et réalisez des progrès tangibles en équipe.", + "primary_button": { + "text": "Définir votre premier cycle", + "comic": { + "title": "Les cycles sont des périodes répétitives.", + "description": "Un sprint, une itération ou tout autre terme que vous utilisez pour le suivi hebdomadaire ou bi-hebdomadaire du travail est un cycle." + } + } + }, + "no_issues": { + "title": "Aucune tâche ajoutée au cycle", + "description": "Ajoutez ou créez des tâches que vous souhaitez cadencer et livrer dans ce cycle", + "primary_button": { + "text": "Créer une nouvelle tâche" + }, + "secondary_button": { + "text": "Ajouter une tâche existante" + } + }, + "completed_no_issues": { + "title": "Aucun problème dans le cycle", + "description": "Aucun problème dans le cycle. Les problèmes ont été transférés ou sont cachés. Pour voir les problèmes cachés, le cas échéant, mettez à jour les propriétés d'affichage en conséquence." + }, + "active": { + "title": "Aucun cycle actif", + "description": "Un cycle actif inclut toute période englobant la date d'aujourd'hui dans sa plage. Retrouvez ici les progrès et les détails du cycle actif." + }, + "archived": { + "title": "Aucun cycle archivé pour le moment", + "description": "Pour organiser votre projet, archivez les cycles terminés. Retrouvez-les ici une fois archivés." + } + } + }, + + "project_issues": { + "empty_state": { + "no_issues": { + "title": "Créez un problème et assignez-le à quelqu’un, même à vous-même", + "description": "Considérez les problèmes comme des emplois, des tâches, des actions ou JTBD (travaux à accomplir). Nous aimons ça. Un problème et ses sous-problèmes sont généralement des actions basées sur le temps assignées aux membres de votre équipe. Votre équipe crée, assigne et termine des problèmes pour faire avancer votre projet vers son objectif.", + "primary_button": { + "text": "Créez votre premier problème", + "comic": { + "title": "Les problèmes sont les éléments constitutifs de Plane.", + "description": "Redessiner l’interface utilisateur de Plane, Rebrander l’entreprise ou Lancer le nouveau système d’injection de carburant sont des exemples de problèmes qui comportent probablement des sous-problèmes." + } + } + }, + "no_archived_issues": { + "title": "Aucun problème archivé pour l'instant", + "description": "Manuellement ou via une automatisation, vous pouvez archiver les problèmes terminés ou annulés. Retrouvez-les ici une fois archivés.", + "primary_button": { + "text": "Configurer l'automatisation" + } + }, + "issues_empty_filter": { + "title": "Aucun problème trouvé correspondant aux filtres appliqués", + "secondary_button": { + "text": "Effacer tous les filtres" + } + } + } + }, + + "project_module": { + "empty_state": { + "no_issues": { + "title": "Aucun problème dans le module", + "description": "Créez ou ajoutez des problèmes que vous souhaitez réaliser dans le cadre de ce module", + "primary_button": { + "text": "Créer un nouveau problème" + }, + "secondary_button": { + "text": "Ajouter un problème existant" + } + }, + "general": { + "title": "Associez les jalons de votre projet aux modules et suivez facilement le travail global.", + "description": "Un groupe de problèmes appartenant à un parent logique et hiérarchique forme un module. Pensez-y comme un moyen de suivre le travail par jalons de projet. Ils ont leurs propres périodes, échéances et analyses pour vous aider à voir votre progression vers un jalon.", + "primary_button": { + "text": "Construisez votre premier module", + "comic": { + "title": "Les modules aident à regrouper le travail par hiérarchie.", + "description": "Un module de panier, un module de châssis et un module d'entrepôt sont de bons exemples de ce regroupement." + } + } + }, + "archived": { + "title": "Aucun module archivé pour le moment", + "description": "Pour organiser votre projet, archivez les modules terminés ou annulés. Retrouvez-les ici une fois archivés." + } + } + }, + + "project_views": { + "empty_state": { + "general": { + "title": "Enregistrez des vues filtrées pour votre projet. Créez-en autant que nécessaire", + "description": "Les vues sont un ensemble de filtres enregistrés que vous utilisez fréquemment ou auxquels vous souhaitez accéder facilement. Tous vos collègues dans un projet peuvent voir les vues de chacun et choisir celle qui correspond le mieux à leurs besoins.", + "primary_button": { + "text": "Créez votre première vue", + "comic": { + "title": "Les vues fonctionnent au-dessus des propriétés des problèmes.", + "description": "Vous pouvez créer une vue à partir d'ici avec autant de propriétés ou de filtres que vous le souhaitez." + } + } + }, + "filter": { + "title": "Aucune vue correspondante", + "description": "Aucune vue ne correspond aux critères de recherche. \n Créez une nouvelle vue à la place." + } + } + }, + + "project_page": { + "empty_state": { + "general": { + "title": "Écrivez une note, un document ou une base de connaissances complète. Laissez Galileo, l'assistant IA de Plane, vous aider à démarrer", + "description": "Les pages sont un espace pour capturer des idées dans Plane. Prenez des notes de réunion, formatez-les facilement, intégrez des problèmes, organisez-les en utilisant une bibliothèque de composants et gardez-les toutes dans le contexte de votre projet. Pour simplifier tout document, invoquez Galileo, l'IA de Plane, avec un raccourci ou en cliquant sur un bouton.", + "primary_button": { + "text": "Créez votre première page" + } + }, + "private": { + "title": "Aucune page privée pour le moment", + "description": "Conservez vos pensées privées ici. Lorsque vous êtes prêt à les partager, votre équipe n'est qu'à un clic.", + "primary_button": { + "text": "Créez votre première page" + } + }, + "public": { + "title": "Aucune page publique pour le moment", + "description": "Consultez ici les pages partagées avec tout le monde dans votre projet.", + "primary_button": { + "text": "Créez votre première page" + } + }, + "archived": { + "title": "Aucune page archivée pour le moment", + "description": "Archivez les pages qui ne sont pas sur votre radar. Accédez-y ici en cas de besoin." + } + } + }, + + "command_k": { + "empty_state": { + "search": { + "title": "Aucun résultat trouvé" + } + } + }, + + "issue_relation": { + "empty_state": { + "search": { + "title": "Aucun problème correspondant trouvé" + }, + "general": { + "title": "Aucun problème trouvé" + } + } + }, + + "issue_comment": { + "empty_state": { + "general": { + "title": "Pas encore de commentaires", + "description": "Les commentaires peuvent être utilisés comme un espace de discussion et de suivi pour les problèmes" + } + } + }, + + "notification": { + "empty_state": { + "detail": { + "title": "Sélectionnez pour voir les détails." + }, + "all": { + "title": "Aucune tâche assignée", + "description": "Les mises à jour des problèmes qui vous sont assignés peuvent être \n vues ici." + }, + "mentions": { + "title": "Aucune tâche assignée", + "description": "Les mises à jour des problèmes qui vous sont assignés peuvent être \n vues ici." + } + } + }, + + "active_cycle": { + "empty_state": { + "progress": { + "title": "Ajoutez des problèmes au cycle pour voir sa progression." + }, + "chart": { + "title": "Ajoutez des problèmes au cycle pour voir le graphique d'avancement." + }, + "priority_issue": { + "title": "Observez d'un coup d'œil les problèmes prioritaires traités dans le cycle." + }, + "assignee": { + "title": "Ajoutez des responsables aux problèmes pour voir la répartition du travail par responsable." + }, + "label": { + "title": "Ajoutez des étiquettes aux problèmes pour voir la répartition du travail par étiquette." + } + } + }, + + "disabled_project": { + "empty_state": { + "inbox": { + "title": "La collecte n'est pas activée pour le projet.", + "description": "La collecte vous aide à gérer les demandes entrantes pour votre projet et à les ajouter en tant que problèmes dans votre flux de travail. Activez la collecte dans les paramètres du projet pour gérer les demandes.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + }, + "cycle": { + "title": "Les cycles ne sont pas activés pour ce projet.", + "description": "Divisez le travail en segments limités dans le temps, travaillez à rebours depuis la date limite de votre projet pour fixer des dates et progressez concrètement en équipe. Activez la fonctionnalité des cycles pour votre projet afin de commencer à les utiliser.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + }, + "module": { + "title": "Les modules ne sont pas activés pour le projet.", + "description": "Les modules sont les éléments constitutifs de votre projet. Activez les modules dans les paramètres du projet pour commencer à les utiliser.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + }, + "page": { + "title": "Les pages ne sont pas activées pour le projet.", + "description": "Les pages sont les éléments constitutifs de votre projet. Activez les pages dans les paramètres du projet pour commencer à les utiliser.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + }, + "view": { + "title": "Les vues ne sont pas activées pour le projet.", + "description": "Les vues sont les éléments constitutifs de votre projet. Activez les vues dans les paramètres du projet pour commencer à les utiliser.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + } + } + }, + + "inbox": { + "label": "Boîte de réception", + "empty_state": { + "sidebar_open_tab": { + "title": "Aucun problème ouvert", + "description": "Trouvez ici les problèmes ouverts. Créez un nouveau problème." + }, + "sidebar_closed_tab": { + "title": "Aucun problème fermé", + "description": "Tous les problèmes, qu'ils soient acceptés ou refusés, se trouvent ici." + }, + "sidebar_filter": { + "title": "Aucun problème correspondant", + "description": "Aucun problème ne correspond au filtre appliqué dans l'entrée. Créez un nouveau problème." + }, + "detail": { + "title": "Sélectionnez un problème pour voir ses détails." + } + } + }, + + "workspace_draft_issues": { + "empty_state": { + "title": "Les problèmes à moitié rédigés, et bientôt, les commentaires apparaîtront ici.", + "description": "Pour essayer cela, commencez à rédiger un problème et laissez-le en suspens ou créez votre premier brouillon ci-dessous. 😉", + "primary_button": { + "text": "Créez votre premier brouillon" + } + } + }, + + "stickies": { + "empty_state": { + "general": { + "title": "Les stickies sont des notes rapides et des tâches que vous notez à la volée.", + "description": "Capturez vos pensées et vos idées facilement en créant des stickies accessibles à tout moment et de n'importe où.", + "primary_button": { + "text": "Ajouter un sticky" + } + }, + "search": { + "title": "Aucun sticky ne correspond à votre recherche.", + "description": "Essayez un autre terme ou faites-le nous savoir si vous êtes sûr que votre recherche est correcte.", + "primary_button": { + "text": "Ajouter un sticky" + } + } + } + }, + + "home_widgets": { + "empty_state": { + "general": { + "title": "C'est calme sans widgets, activez-les", + "description": "Il semble que tous vos widgets soient désactivés. Activez-les maintenant pour améliorer votre expérience !", + "primary_button": { + "text": "Gérer les widgets" + } + } + } + } } diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index fa2b244cc5e..d7c8e76322d 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -38,7 +38,6 @@ "deactivate_account_description": "アカウントを無効化すると、そのアカウント内のすべてのデータとリソースが完全に削除され、復元することはできません。", "profile_settings": "プロフィール設定", "your_account": "アカウント", - "profile": "プロフィール", "security": "セキュリティ", "activity": "アクティビティ", "appearance": "アピアンス", @@ -154,7 +153,6 @@ "stay_ahead_of_blockers_description": "プロジェクト間の課題を見つけ、他のビューからは明らかでないサイクル間の依存関係を確認します。", "analytics": "分析", "workspace_invites": "ワークスペースの招待", - "workspace_settings": "ワークスペース設定", "enter_god_mode": "ゴッドモードに入る", "workspace_logo": "ワークスペースロゴ", "new_issue": "新しい問題", @@ -316,5 +314,468 @@ "remove_parent_issue": "親問題を削除", "add_parent": "親問題を追加", "loading_members": "メンバーを読み込んでいます...", - "inbox": "受信箱" + "connections": "接続", + + "workspace_dashboard": { + "empty_state": { + "general": { + "title": "プロジェクト、アクティビティ、指標の概要", + "description": "Planeへようこそ!私たちはあなたを迎えることができて嬉しいです。最初のプロジェクトを作成してタスクを追跡し、このページが進捗を助けるスペースに変わります。管理者は、チームを前進させるための要素も見ることができます。", + "primary_button": { + "text": "最初のプロジェクトを作成", + "comic": { + "title": "すべてはPlaneでのプロジェクトから始まります", + "description": "プロジェクトは製品ロードマップ、マーケティングキャンペーン、新車の発売など、さまざまなものになります。" + } + } + } + } + }, + + "workspace_analytics": { + "empty_state": { + "general": { + "title": "進捗、作業負荷、割り当てを追跡します。傾向を見つけ、障害を取り除き、作業を加速させます", + "description": "需要に対するスコープ、見積もり、目標の進展を視覚化します。メンバーやチームごとのパフォーマンスを確認し、プロジェクトが期限内に進むことを確認します。", + "primary_button": { + "text": "最初のプロジェクトを開始", + "comic": { + "title": "分析はサイクル+モジュールでより効果的に機能します", + "description": "まず、タスクをサイクルにフレーム化し、可能であれば複数のサイクルにまたがるタスクをモジュールにグループ化します。左側のナビゲーションで確認してください。" + } + } + } + } + }, + + "workspace_projects": { + "empty_state": { + "general": { + "title": "アクティブなプロジェクトがありません", + "description": "各プロジェクトは目標志向の作業の親と見なされます。プロジェクトはタスク、サイクル、モジュールが存在する場所であり、同僚とともに目標を達成するのに役立ちます。新しいプロジェクトを作成するか、アーカイブされたプロジェクトをフィルタリングしてください。", + "primary_button": { + "text": "最初のプロジェクトを開始", + "comic": { + "title": "すべてはPlaneでのプロジェクトから始まります", + "description": "プロジェクトは製品ロードマップ、マーケティングキャンペーン、新車の発売など、さまざまなものになります。" + } + } + }, + "no_projects": { + "title": "プロジェクトがありません", + "description": "タスクを作成したり作業を管理するには、プロジェクトを作成するか、その一部である必要があります。", + "primary_button": { + "text": "最初のプロジェクトを開始", + "comic": { + "title": "すべてはPlaneでのプロジェクトから始まります", + "description": "プロジェクトは製品ロードマップ、マーケティングキャンペーン、新車の発売など、さまざまなものになります。" + } + } + }, + "filter": { + "title": "一致するプロジェクトがありません", + "description": "一致する条件のプロジェクトが見つかりませんでした。\n 代わりに新しいプロジェクトを作成してください。" + } + } + }, + + "workspace_issues": { + "empty_state": { + "all-issues": { + "title": "プロジェクトにタスクがありません", + "description": "最初のプロジェクトが完了しました!次に、追跡可能なチャンクに分割してタスクを作成します。始めましょう!", + "primary_button": { + "text": "新しいタスクを作成" + } + }, + "assigned": { + "title": "割り当てられたタスクがありません", + "description": "割り当てられたタスクをここで追跡できます。", + "primary_button": { + "text": "新しいタスクを作成" + } + }, + "created": { + "title": "作成されたタスクがありません", + "description": "作成したすべてのタスクはここにあります。直接ここで追跡してください。", + "primary_button": { + "text": "新しいタスクを作成" + } + }, + "subscribed": { + "title": "フォローしているタスクがありません", + "description": "興味のあるタスクをフォローして、すべてここで追跡してください。" + }, + "custom-view": { + "title": "タスクが見つかりません", + "description": "フィルタに一致するタスクがここに表示されます。すべてここで追跡してください。" + } + } + }, + + "workspace_settings": { + "label": "ワークスペース設定", + "empty_state": { + "api_tokens": { + "title": "APIトークンが作成されていません", + "description": "Plane APIは、外部システムでPlaneデータを統合するために使用できます。トークンを作成して始めましょう。" + }, + "webhooks": { + "title": "Webhookが追加されていません", + "description": "リアルタイム更新を受け取り、アクションを自動化するためにWebhookを作成します。" + }, + "exports": { + "title": "エクスポートはまだありません", + "description": "エクスポートするたびに、コピーがここにも参照用として利用可能になります。" + }, + "imports": { + "title": "インポートはまだありません", + "description": "過去のインポート履歴をすべてここで確認し、ダウンロードできます。" + } + } + }, + + "profile": { + "label": "プロフィール", + "empty_state": { + "activity": { + "title": "まだアクティビティがありません", + "description": "新しいタスクを作成することから始めましょう!詳細やプロパティを追加してください。Planeをさらに探索して、アクティビティを確認してください。" + }, + "assigned": { + "title": "割り当てられたタスクがありません", + "description": "割り当てられたタスクをここで追跡できます。" + }, + "created": { + "title": "作成されたタスクがありません", + "description": "作成したすべてのタスクはここにあります。直接ここで追跡してください。" + }, + "subscribed": { + "title": "フォローしているタスクがありません", + "description": "興味のあるタスクをフォローして、すべてここで追跡してください。" + } + } + }, + + "project_settings": { + "empty_state": { + "labels": { + "title": "ラベルがありません", + "description": "ラベルを作成して、プロジェクトのタスクを整理し、フィルタリングします。" + } + } + }, + + "project_cycles": { + "empty_state": { + "general": { + "title": "作業をサイクルにまとめ、フレーム化します。", + "description": "作業を定義された期間に分割し、期限から逆算して締め切りを設定し、チームで具体的な進捗を達成します。", + "primary_button": { + "text": "最初のサイクルを設定", + "comic": { + "title": "サイクルは繰り返し期間です。", + "description": "スプリント、イテレーション、または週次・隔週の作業追跡に使用するその他の用語がサイクルです。" + } + } + }, + "no_issues": { + "title": "サイクルに追加されたタスクがありません", + "description": "このサイクル内でタイムフレーム化し、配信したいタスクを追加または作成してください。", + "primary_button": { + "text": "新しいタスクを作成" + }, + "secondary_button": { + "text": "既存のタスクを追加" + } + }, + "completed_no_issues": { + "title": "サイクルに課題がありません", + "description": "サイクルに課題がありません。課題は転送されるか非表示になっています。非表示の課題がある場合は、表示プロパティを更新して確認してください。" + }, + "active": { + "title": "アクティブなサイクルがありません", + "description": "アクティブなサイクルは、今日の日付を含む範囲がある期間を指します。ここでアクティブなサイクルの進捗と詳細を確認してください。" + }, + "archived": { + "title": "まだアーカイブされたサイクルはありません", + "description": "プロジェクトを整理するには、完了したサイクルをアーカイブしてください。アーカイブされたサイクルはここで見つけることができます。" + } + } + }, + + "project_issues": { + "empty_state": { + "no_issues": { + "title": "課題を作成し、誰かに、または自分に割り当てましょう", + "description": "課題を仕事、タスク、行動、またはJTBD(やるべき仕事)と考えてください。それが私たちの好む方法です。課題とそのサブ課題は通常、チームメンバーに割り当てられる時間ベースのアクションです。チームは課題を作成し、割り当て、完了させることで、プロジェクトの目標に向かって進みます。", + "primary_button": { + "text": "最初の課題を作成", + "comic": { + "title": "課題はPlaneの構成要素です。", + "description": "Plane UIの再設計、会社のリブランド、新しい燃料噴射システムの開始は、サブ課題がある可能性のある課題の例です。" + } + } + }, + "no_archived_issues": { + "title": "まだアーカイブされた課題はありません", + "description": "手動または自動化を使用して、完了またはキャンセルされた課題をアーカイブできます。アーカイブされた課題はここで見つけることができます。", + "primary_button": { + "text": "自動化を設定" + } + }, + "issues_empty_filter": { + "title": "適用されたフィルターに一致する課題は見つかりませんでした", + "secondary_button": { + "text": "すべてのフィルターをクリア" + } + } + } + }, + + "project_module": { + "empty_state": { + "no_issues": { + "title": "モジュールに課題がありません", + "description": "このモジュールの一部として達成したい課題を作成または追加してください", + "primary_button": { + "text": "新しい課題を作成" + }, + "secondary_button": { + "text": "既存の課題を追加" + } + }, + "general": { + "title": "プロジェクトのマイルストーンをモジュールにマッピングし、集計された作業を簡単に追跡します。", + "description": "論理的で階層的な親に属する課題のグループがモジュールを形成します。それをプロジェクトのマイルストーンで作業を追跡する方法と考えてください。モジュールには独自の期間と締切があり、分析を通じてマイルストーンへの進捗状況を確認できます。", + "primary_button": { + "text": "最初のモジュールを構築", + "comic": { + "title": "モジュールは階層によって作業をグループ化します。", + "description": "カートモジュール、シャーシモジュール、倉庫モジュールは、このグループ化の良い例です。" + } + } + }, + "archived": { + "title": "まだアーカイブされたモジュールはありません", + "description": "プロジェクトを整理するために、完了またはキャンセルされたモジュールをアーカイブします。アーカイブされたモジュールはここで見つけることができます。" + } + } + }, + + "project_views": { + "empty_state": { + "general": { + "title": "プロジェクト用にフィルタされたビューを保存します。必要なだけ作成してください", + "description": "ビューは、頻繁に使用するか、簡単にアクセスしたい保存されたフィルターのセットです。プロジェクト内のすべての同僚が他の人のビューを確認し、自分のニーズに最適なものを選ぶことができます。", + "primary_button": { + "text": "最初のビューを作成", + "comic": { + "title": "ビューは課題プロパティの上に機能します。", + "description": "ここから必要に応じてプロパティをフィルターとして使用してビューを作成できます。" + } + } + }, + "filter": { + "title": "一致するビューがありません", + "description": "検索条件に一致するビューはありません。 \n 代わりに新しいビューを作成してください。" + } + } + }, + + "project_page": { + "empty_state": { + "general": { + "title": "メモ、ドキュメント、または完全なナレッジベースを書きましょう。PlaneのAIアシスタントGalileoがスタートをサポートします", + "description": "ページはPlaneでアイデアをまとめるスペースです。会議のメモを取る、簡単にフォーマットする、課題を埋め込む、コンポーネントライブラリを使用してレイアウトするなど、すべてをプロジェクトのコンテキスト内に保ちます。どんなドキュメントでも簡単にするために、ショートカットやボタンのクリックでPlaneのAIであるGalileoを呼び出してください。", + "primary_button": { + "text": "最初のページを作成" + } + }, + "private": { + "title": "まだプライベートページはありません", + "description": "プライベートな考えをここに保存します。共有する準備ができたら、チームはすぐそばにいます。", + "primary_button": { + "text": "最初のページを作成" + } + }, + "public": { + "title": "まだ公開ページはありません", + "description": "プロジェクト内のすべての人と共有されているページをここで確認してください。", + "primary_button": { + "text": "最初のページを作成" + } + }, + "archived": { + "title": "まだアーカイブされたページはありません", + "description": "レーダーに載っていないページをアーカイブします。必要なときにここからアクセスしてください。" + } + } + }, + + "command_k": { + "empty_state": { + "search": { + "title": "結果が見つかりません" + } + } + }, + + "issue_relation": { + "empty_state": { + "search": { + "title": "一致する課題が見つかりませんでした" + }, + "general": { + "title": "課題が見つかりません" + } + } + }, + + "issue_comment": { + "empty_state": { + "general": { + "title": "まだコメントはありません", + "description": "コメントは課題の議論やフォローアップスペースとして使用できます" + } + } + }, + + "notification": { + "empty_state": { + "detail": { + "title": "詳細を表示するには選択してください。" + }, + "all": { + "title": "割り当てられた問題はありません", + "description": "あなたに割り当てられた問題の更新情報はここで確認できます。" + }, + "mentions": { + "title": "割り当てられた問題はありません", + "description": "あなたに割り当てられた問題の更新情報はここで確認できます。" + } + } + }, + + "active_cycle": { + "empty_state": { + "progress": { + "title": "サイクルに課題を追加して進捗を確認してください。" + }, + "chart": { + "title": "サイクルに課題を追加してバーンダウンチャートを確認してください。" + }, + "priority_issue": { + "title": "サイクル内で処理された高優先度の課題を一目で把握できます。" + }, + "assignee": { + "title": "課題に担当者を追加して、担当者ごとの作業の内訳を確認してください。" + }, + "label": { + "title": "課題にラベルを追加して、ラベルごとの作業の内訳を確認してください。" + } + } + }, + + "disabled_project": { + "empty_state": { + "inbox": { + "title": "プロジェクトにインテークが有効化されていません。", + "description": "インテークは、プロジェクトへのリクエストを管理し、それらをワークフローの課題として追加するのに役立ちます。プロジェクト設定からインテークを有効化してリクエストを管理してください。", + "primary_button": { + "text": "機能を管理" + } + }, + "cycle": { + "title": "このプロジェクトでサイクルが有効化されていません。", + "description": "作業をタイムボックス化されたチャンクに分割し、プロジェクトの期限から逆算して日程を設定し、チームとして具体的な進捗を実現します。プロジェクトでサイクル機能を有効化して利用を開始してください。", + "primary_button": { + "text": "機能を管理" + } + }, + "module": { + "title": "プロジェクトにモジュールが有効化されていません。", + "description": "モジュールはプロジェクトの構成要素です。プロジェクト設定からモジュールを有効化して利用を開始してください。", + "primary_button": { + "text": "機能を管理" + } + }, + "page": { + "title": "プロジェクトにページが有効化されていません。", + "description": "ページはプロジェクトの構成要素です。プロジェクト設定からページを有効化して利用を開始してください。", + "primary_button": { + "text": "機能を管理" + } + }, + "view": { + "title": "プロジェクトにビューが有効化されていません。", + "description": "ビューはプロジェクトの構成要素です。プロジェクト設定からビューを有効化して利用を開始してください。", + "primary_button": { + "text": "機能を管理" + } + } + } + }, + + "inbox": { + "label": "受信トレイ", + "empty_state": { + "sidebar_open_tab": { + "title": "未解決の課題はありません", + "description": "ここに未解決の課題が表示されます。新しい課題を作成してください。" + }, + "sidebar_closed_tab": { + "title": "完了した課題はありません", + "description": "受け入れ済みまたは却下済みのすべての課題がここに表示されます。" + }, + "sidebar_filter": { + "title": "一致する課題はありません", + "description": "入力で適用されたフィルターに一致する課題はありません。新しい課題を作成してください。" + }, + "detail": { + "title": "詳細を表示するには課題を選択してください。" + } + } + }, + + "workspace_draft_issues": { + "empty_state": { + "title": "書きかけの課題や、もうすぐコメントもここに表示されます。", + "description": "これを試すには、課題の作成を始めて途中で中断するか、下に最初の下書きを作成してください。😉", + "primary_button": { + "text": "最初の下書きを作成" + } + } + }, + + "stickies": { + "empty_state": { + "general": { + "title": "スティッキーは、その場で素早く取るメモやタスクです。", + "description": "いつでもどこでもアクセスできるスティッキーを作成して、思いついたアイデアを簡単にキャプチャしましょう。", + "primary_button": { + "text": "スティッキーを追加" + } + }, + "search": { + "title": "一致するスティッキーはありません。", + "description": "別の用語を試すか、検索が正しい場合はお知らせください。", + "primary_button": { + "text": "スティッキーを追加" + } + } + } + }, + + "home_widgets": { + "empty_state": { + "general": { + "title": "ウィジェットがないと静かですね、オンにしましょう", + "description": "すべてのウィジェットがオフになっているようです。 今すぐ有効にして、体験を向上させましょう!", + "primary_button": { + "text": "ウィジェットを管理" + } + } + } + } } diff --git a/space/app/layout.tsx b/space/app/layout.tsx index e457ae5d1d1..488fb551b42 100644 --- a/space/app/layout.tsx +++ b/space/app/layout.tsx @@ -32,7 +32,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - {children} + + <>{children} + ); diff --git a/space/core/components/issues/issue-layouts/kanban/default.tsx b/space/core/components/issues/issue-layouts/kanban/default.tsx index 50f7b0f7138..6ad7b2e0f17 100644 --- a/space/core/components/issues/issue-layouts/kanban/default.tsx +++ b/space/core/components/issues/issue-layouts/kanban/default.tsx @@ -94,7 +94,7 @@ export const KanBan: React.FC = observer((props) => {
diff --git a/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx index 902dff670d0..bc61c54af6e 100644 --- a/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -134,7 +134,7 @@ const SubGroupSwimlaneHeader: React.FC = observer( return (
- +
); })} @@ -262,7 +262,7 @@ const SubGroup: React.FC = observer((props) => {
{ const searchParams = useSearchParams(); const analytics_tab = searchParams.get("analytics_tab"); + // plane imports + const { t } = useTranslation(); // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); const { workspaceProjectIds, loader } = useProject(); const { currentWorkspace } = useWorkspace(); + const { allowPermissions } = useUserPermissions(); + // helper hooks + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" }); // derived values const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined; + // permissions + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + // TODO: refactor loader implementation return ( <> @@ -67,12 +80,22 @@ const AnalyticsPage = observer(() => {
) : ( - { - setTrackElement("Analytics empty state"); - toggleCreateProjectModal(true); - }} + { + setTrackElement("Analytics empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } /> )} diff --git a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx index 4ea0c8e4258..3074acbf8ce 100644 --- a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx @@ -4,21 +4,24 @@ import { useCallback, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; +// plane imports +import { useTranslation } from "@plane/i18n"; // components import { LogoSpinner } from "@/components/common"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; import { InboxContentRoot } from "@/components/inbox"; import { IssuePeekOverview } from "@/components/issues"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification"; // hooks import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; const WorkspaceDashboardPage = observer(() => { const { workspaceSlug } = useParams(); + // plane hooks + const { t } = useTranslation(); // hooks const { currentWorkspace } = useWorkspace(); const { @@ -34,6 +37,7 @@ const WorkspaceDashboardPage = observer(() => { const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined; const { workspace_slug, project_id, issue_id, is_inbox_issue } = notificationLiteByNotificationId(currentSelectedNotificationId); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" }); // fetching workspace issue properties useWorkspaceIssueProperties(workspaceSlug); @@ -82,7 +86,7 @@ const WorkspaceDashboardPage = observer(() => {
{!currentSelectedNotificationId ? (
- +
) : ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx index 5b1793d5d1f..b72b46dbeaf 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx @@ -3,20 +3,22 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// types +// plane imports +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TCycleFilters } from "@plane/types"; // components import { Header, EHeaderVariant } from "@plane/ui"; import { PageHead } from "@/components/core"; import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles"; -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { CycleModuleListLayout } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useEventTracker, useCycle, useProject, useCycleFilter } from "@/hooks/store"; +import { useEventTracker, useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const ProjectCyclesPage = observer(() => { // states @@ -26,13 +28,23 @@ const ProjectCyclesPage = observer(() => { const { currentProjectCycleIds, loader } = useCycle(); const { getProjectById, currentProjectDetails } = useProject(); // router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // cycle filters hook const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter(); + const { allowPermissions } = useUserPermissions(); // derived values const totalCycles = currentProjectCycleIds?.length ?? 0; const project = projectId ? getProjectById(projectId?.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; + const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const hasMemberLevelPermission = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/cycles" }); const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => { if (!projectId) return; @@ -50,9 +62,17 @@ const ProjectCyclesPage = observer(() => { if (currentProjectDetails?.cycle_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !hasAdminLevelPermission, + }} />
); @@ -71,12 +91,22 @@ const ProjectCyclesPage = observer(() => { /> {totalCycles === 0 ? (
- { - setTrackElement("Cycle empty state"); - setCreateModal(true); - }} + { + setTrackElement("Cycle empty state"); + setCreateModal(true); + }} + disabled={!hasMemberLevelPermission} + /> + } />
) : ( diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/page.tsx index 36aa37e302c..92d01aa25a9 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/page.tsx @@ -2,35 +2,50 @@ import { observer } from "mobx-react"; // components import { useParams, useSearchParams } from "next/navigation"; +import { EUserProjectRoles } from "@plane/constants/src/user"; +import { useTranslation } from "@plane/i18n"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { InboxIssueRoot } from "@/components/inbox"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; // hooks -import { useProject } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const ProjectInboxPage = observer(() => { /// router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); - const searchParams = useSearchParams(); - const navigationTab = searchParams.get("currentTab"); const inboxIssueId = searchParams.get("inboxIssueId"); - + // plane hooks + const { t } = useTranslation(); // hooks const { currentProjectDetails } = useProject(); + const { allowPermissions } = useUserPermissions(); + // derived values + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/intake" }); // No access to inbox if (currentProjectDetails?.inbox_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !canPerformEmptyStateActions, + }} />
); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx index 9417016e385..572cb3862f7 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx @@ -4,27 +4,36 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // types +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TModuleFilters } from "@plane/types"; // components import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useModuleFilter, useProject } from "@/hooks/store"; +import { useModuleFilter, useProject, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const ProjectModulesPage = observer(() => { + // router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store const { getProjectById, currentProjectDetails } = useProject(); const { currentProjectFilters, currentProjectDisplayFilters, clearAllFilters, updateFilters, updateDisplayFilters } = useModuleFilter(); + const { allowPermissions } = useUserPermissions(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Modules` : undefined; + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/modules" }); const handleRemoveFilter = useCallback( (key: keyof TModuleFilters, value: string | null) => { @@ -45,9 +54,17 @@ const ProjectModulesPage = observer(() => { if (currentProjectDetails?.module_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !canPerformEmptyStateActions, + }} />
); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx index 93f37ea83b0..547584f51a1 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx @@ -2,27 +2,35 @@ import { observer } from "mobx-react"; import { useParams, useSearchParams } from "next/navigation"; -// types +// plane imports +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TPageNavigationTabs } from "@plane/types"; // components import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { PagesListRoot, PagesListView } from "@/components/pages"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useProject } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const ProjectPagesPage = observer(() => { // router + const router = useAppRouter(); const searchParams = useSearchParams(); const type = searchParams.get("type"); const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { getProjectById, currentProjectDetails } = useProject(); + const { allowPermissions } = useUserPermissions(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/pages" }); const currentPageType = (): TPageNavigationTabs => { const pageType = type?.toString(); @@ -37,9 +45,17 @@ const ProjectPagesPage = observer(() => { if (currentProjectDetails?.page_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !canPerformEmptyStateActions, + }} />
); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx index 387f6e5b423..73e7e0d85e4 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx @@ -4,29 +4,38 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TViewFilterProps } from "@plane/types"; import { Header, EHeaderVariant } from "@plane/ui"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { ProjectViewsList } from "@/components/views"; import { ViewAppliedFiltersList } from "@/components/views/applied-filters"; -import { EmptyStateType } from "@/constants/empty-state"; // constants import { EViewAccess } from "@/constants/views"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useProject, useProjectView } from "@/hooks/store"; +import { useProject, useProjectView, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const ProjectViewsPage = observer(() => { // router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store const { getProjectById, currentProjectDetails } = useProject(); const { filters, updateFilters, clearAllFilters } = useProjectView(); + const { allowPermissions } = useUserPermissions(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Views` : undefined; + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/views" }); const handleRemoveFilter = useCallback( (key: keyof TViewFilterProps, value: string | EViewAccess | null) => { @@ -53,9 +62,17 @@ const ProjectViewsPage = observer(() => { if (currentProjectDetails?.issue_views_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !canPerformEmptyStateActions, + }} />
); diff --git a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx index fd2ed9669b2..03bb1fa6cbe 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx @@ -4,19 +4,19 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // component import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token"; import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { APITokenSettingsLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // store hooks import { useUserPermissions, useWorkspace } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services import { APITokenService } from "@/services/api_token.service"; @@ -28,11 +28,14 @@ const ApiTokensPage = observer(() => { const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false); // router const { workspaceSlug } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { currentWorkspace } = useWorkspace(); const { workspaceUserInfo, allowPermissions } = useUserPermissions(); // derived values const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" }); const { data: tokens } = useSWR( workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null, @@ -78,7 +81,11 @@ const ApiTokensPage = observer(() => {
- +
)} diff --git a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx index 86c922f07fb..412c29f5dd2 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx @@ -4,18 +4,18 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // components import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { WebhookSettingsLoader } from "@/components/ui"; import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const WebhooksListPage = observer(() => { @@ -23,13 +23,15 @@ const WebhooksListPage = observer(() => { const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false); // router const { workspaceSlug } = useParams(); + // plane hooks + const { t } = useTranslation(); // mobx store const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); - + // derived values const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/webhooks" }); useSWR( workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null, @@ -81,7 +83,11 @@ const WebhooksListPage = observer(() => {
- +
)} diff --git a/web/app/profile/activity/page.tsx b/web/app/profile/activity/page.tsx index a2a8cad851f..ca6c9511d06 100644 --- a/web/app/profile/activity/page.tsx +++ b/web/app/profile/activity/page.tsx @@ -7,24 +7,27 @@ import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // components import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { ProfileActivityListPage, ProfileSettingContentHeader, ProfileSettingContentWrapper, } from "@/components/profile"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +// hooks +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const PER_PAGE = 100; const ProfileActivityPage = observer(() => { - const { t } = useTranslation(); // states const [pageCount, setPageCount] = useState(1); const [totalPages, setTotalPages] = useState(0); const [resultsCount, setResultsCount] = useState(0); const [isEmpty, setIsEmpty] = useState(false); + // plane hooks + const { t } = useTranslation(); + // derived values + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/profile/activity" }); const updateTotalPages = (count: number) => setTotalPages(count); @@ -50,7 +53,13 @@ const ProfileActivityPage = observer(() => { const isLoadMoreVisible = pageCount < totalPages && resultsCount !== 0; if (isEmpty) { - return ; + return ( + + ); } return ( diff --git a/web/app/profile/notifications/page.tsx b/web/app/profile/notifications/page.tsx index cbdcd147d73..5e154fdffc1 100644 --- a/web/app/profile/notifications/page.tsx +++ b/web/app/profile/notifications/page.tsx @@ -25,7 +25,7 @@ export default function ProfileNotificationPage() { return ( <> - + { return ( <> - + diff --git a/web/ce/components/cycles/active-cycle/root.tsx b/web/ce/components/cycles/active-cycle/root.tsx index 5ebddc63f23..a13b7f64cfa 100644 --- a/web/ce/components/cycles/active-cycle/root.tsx +++ b/web/ce/components/cycles/active-cycle/root.tsx @@ -3,7 +3,8 @@ import { useMemo } from "react"; import { observer } from "mobx-react"; import { Disclosure } from "@headlessui/react"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Row } from "@plane/ui"; // components import { @@ -14,10 +15,10 @@ import { CyclesListItem, } from "@/components/cycles"; import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; +// hooks import { useCycle } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; interface IActiveCycleDetails { @@ -29,9 +30,13 @@ interface IActiveCycleDetails { export const ActiveCycleRoot: React.FC = observer((props) => { const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props; + // plane hooks + const { t } = useTranslation(); + // store hooks const { currentProjectActiveCycleId } = useCycle(); // derived values const cycleId = propsCycleId ?? currentProjectActiveCycleId; + const activeCycleResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/cycle/active" }); // fetch cycle details const { handleFiltersUpdate, @@ -43,7 +48,11 @@ export const ActiveCycleRoot: React.FC = observer((props) = () => ( <> {!cycleId || !activeCycle ? ( - + ) : (
{cycleId && ( diff --git a/web/core/components/command-palette/command-modal.tsx b/web/core/components/command-palette/command-modal.tsx index b80b0dfe145..8422288cf51 100644 --- a/web/core/components/command-palette/command-modal.tsx +++ b/web/core/components/command-palette/command-modal.tsx @@ -2,14 +2,15 @@ import React, { useEffect, useState } from "react"; import { Command } from "cmdk"; +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; import { FolderPlus, Search, Settings } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; -// types +// plane imports +import { useTranslation } from "@plane/i18n"; import { IWorkspaceSearchResults } from "@plane/types"; -// ui import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // components import { @@ -23,9 +24,7 @@ import { CommandPaletteThemeActions, CommandPaletteWorkspaceSettingsActions, } from "@/components/command-palette"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // fetch-keys import { ISSUE_DETAILS } from "@/constants/fetch-keys"; // helpers @@ -36,21 +35,20 @@ import { useAppRouter } from "@/hooks/use-app-router"; import useDebounce from "@/hooks/use-debounce"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { IssueIdentifier } from "@/plane-web/components/issues"; // plane web services import { WorkspaceService } from "@/plane-web/services"; // services import { IssueService } from "@/services/issue"; -import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; const workspaceService = new WorkspaceService(); const issueService = new IssueService(); export const CommandModal: React.FC = observer(() => { - // hooks - const { workspaceProjectIds } = useProject(); - const { isMobile } = usePlatformOS(); - const { canPerformAnyCreateAction } = useUser(); + // router + const router = useAppRouter(); + const { workspaceSlug, projectId, issueId } = useParams(); // states const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [resultsCount, setResultsCount] = useState(0); @@ -70,26 +68,25 @@ export const CommandModal: React.FC = observer(() => { }); const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [pages, setPages] = useState([]); + // plane hooks + const { t } = useTranslation(); + // hooks + const { workspaceProjectIds } = useProject(); + const { isMobile } = usePlatformOS(); + const { canPerformAnyCreateAction } = useUser(); const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); const { setTrackElement } = useEventTracker(); - - // router - const router = useAppRouter(); - // router params - const { workspaceSlug, projectId, issueId } = useParams(); - + // derived values const page = pages[pages.length - 1]; - const debouncedSearchTerm = useDebounce(searchTerm, 500); - const { baseTabIndex } = getTabIndex(undefined, isMobile); - const canPerformWorkspaceActions = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE ); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); // TODO: update this to mobx store const { data: issueDetails } = useSWR( @@ -268,7 +265,7 @@ export const CommandModal: React.FC = observer(() => { {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
- +
)} diff --git a/web/core/components/core/modals/bulk-delete-issues-modal.tsx b/web/core/components/core/modals/bulk-delete-issues-modal.tsx index aec70262ab1..73dd37a231a 100644 --- a/web/core/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/core/components/core/modals/bulk-delete-issues-modal.tsx @@ -6,19 +6,18 @@ import { useParams } from "next/navigation"; import { SubmitHandler, useForm } from "react-hook-form"; import { Search } from "lucide-react"; import { Combobox, Dialog, Transition } from "@headlessui/react"; +// plane imports import { EIssuesStoreType } from "@plane/constants"; -// types +import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse, IUser } from "@plane/types"; -// ui import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useIssues } from "@/hooks/store"; import useDebounce from "@/hooks/use-debounce"; // services +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { ProjectService } from "@/services/project"; // local components import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item"; @@ -39,16 +38,19 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { const { isOpen, onClose } = props; // router params const { workspaceSlug, projectId } = useParams(); - // hooks - const { - issues: { removeBulkIssues }, - } = useIssues(EIssuesStoreType.PROJECT); // states const [query, setQuery] = useState(""); const [issues, setIssues] = useState([]); const [isSearching, setIsSearching] = useState(false); - + // hooks + const { + issues: { removeBulkIssues }, + } = useIssues(EIssuesStoreType.PROJECT); + const { t } = useTranslation(); + // derived values const debouncedSearchTerm: string = useDebounce(query, 500); + const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); + const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" }); useEffect(() => { if (!isOpen || !workspaceSlug || !projectId) return; @@ -131,12 +133,11 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { ) : (
- + {query === "" ? ( + + ) : ( + + )}
); diff --git a/web/core/components/core/modals/issue-search-modal-empty-state.tsx b/web/core/components/core/modals/issue-search-modal-empty-state.tsx index 578d39a60fc..20646efb182 100644 --- a/web/core/components/core/modals/issue-search-modal-empty-state.tsx +++ b/web/core/components/core/modals/issue-search-modal-empty-state.tsx @@ -1,10 +1,10 @@ import React from "react"; -// components +// plane imports +import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse } from "@plane/types"; -import { EmptyState } from "@/components/empty-state"; -// types -import { EmptyStateType } from "@/constants/empty-state"; -// constants +// components +import { SimpleEmptyState } from "@/components/empty-state"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; interface EmptyStateProps { issues: ISearchIssueResponse[]; @@ -19,18 +19,28 @@ export const IssueSearchModalEmptyState: React.FC = ({ debouncedSearchTerm, isSearching, }) => { - const renderEmptyState = (type: EmptyStateType) => ( -
- -
- ); + // plane hooks + const { t } = useTranslation(); + // derived values + const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); + const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" }); - const emptyState = - issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching - ? renderEmptyState(EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE) - : issues.length === 0 - ? renderEmptyState(EmptyStateType.ISSUE_RELATION_EMPTY_STATE) - : null; + const EmptyStateContainer = ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ); - return emptyState; + if (issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching) { + return ( + + + + ); + } else if (issues.length === 0) { + return ( + + + + ); + } + return null; }; diff --git a/web/core/components/cycles/active-cycle/cycle-stats.tsx b/web/core/components/cycles/active-cycle/cycle-stats.tsx index 44d7b595f48..b9cf267a62f 100644 --- a/web/core/components/cycles/active-cycle/cycle-stats.tsx +++ b/web/core/components/cycles/active-cycle/cycle-stats.tsx @@ -6,17 +6,16 @@ import { observer } from "mobx-react"; import { CalendarCheck } from "lucide-react"; // headless ui import { Tab } from "@headlessui/react"; -// types +// plane imports import { EIssuesStoreType } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ICycle, IIssueFilterOptions } from "@plane/types"; // ui import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui"; // components import { SingleProgressStats } from "@/components/core"; import { StateDropdown } from "@/components/dropdowns"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; @@ -26,6 +25,7 @@ import { useIssueDetail, useIssues } from "@/hooks/store"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import useLocalStorage from "@/hooks/use-local-storage"; // plane web components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { IssueIdentifier } from "@/plane-web/components/issues"; // store import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; @@ -41,11 +41,18 @@ export type ActiveCycleStatsProps = { export const ActiveCycleStats: FC = observer((props) => { const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props; - + // local storage const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees"); - + // refs const issuesContainerRef = useRef(null); + // states const [issuesLoaderElement, setIssueLoaderElement] = useState(null); + // plane hooks + const { t } = useTranslation(); + // derived values + const priorityResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/priority" }); + const assigneesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/assignee" }); + const labelsResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/label" }); const currentValue = (tab: string | null) => { switch (tab) { @@ -231,10 +238,9 @@ export const ActiveCycleStats: FC = observer((props) => { ) : (
-
) @@ -293,10 +299,9 @@ export const ActiveCycleStats: FC = observer((props) => { }) ) : (
-
) @@ -336,7 +341,7 @@ export const ActiveCycleStats: FC = observer((props) => { )) ) : (
- +
) ) : ( diff --git a/web/core/components/cycles/active-cycle/productivity.tsx b/web/core/components/cycles/active-cycle/productivity.tsx index 74957af03c7..859c3015435 100644 --- a/web/core/components/cycles/active-cycle/productivity.tsx +++ b/web/core/components/cycles/active-cycle/productivity.tsx @@ -1,16 +1,17 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { ICycle, TCycleEstimateType } from "@plane/types"; import { Loader } from "@plane/ui"; // components import ProgressChart from "@/components/core/sidebar/progress-chart"; -import { EmptyState } from "@/components/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // constants -import { EmptyStateType } from "@/constants/empty-state"; -import { useCycle, useProjectEstimates } from "@/hooks/store"; +import { useCycle } from "@/hooks/store"; // plane web constants -import { EEstimateSystem } from "@/plane-web/constants/estimates"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { EstimateTypeDropdown } from "../dropdowns/estimate-type-dropdown"; export type ActiveCycleProductivityProps = { @@ -21,11 +22,13 @@ export type ActiveCycleProductivityProps = { export const ActiveCycleProductivity: FC = observer((props) => { const { workspaceSlug, projectId, cycle } = props; + // plane hooks + const { t } = useTranslation(); // hooks const { getEstimateTypeByCycleId, setEstimateType } = useCycle(); - // derived values const estimateType: TCycleEstimateType = (cycle && getEstimateTypeByCycleId(cycle.id)) || "issues"; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/chart" }); const onChange = async (value: TCycleEstimateType) => { if (!workspaceSlug || !projectId || !cycle || !cycle.id) return; @@ -95,7 +98,7 @@ export const ActiveCycleProductivity: FC = observe ) : ( <>
- +
)} diff --git a/web/core/components/cycles/active-cycle/progress.tsx b/web/core/components/cycles/active-cycle/progress.tsx index 0dffff5c757..0f2c5a419aa 100644 --- a/web/core/components/cycles/active-cycle/progress.tsx +++ b/web/core/components/cycles/active-cycle/progress.tsx @@ -4,14 +4,14 @@ import { FC } from "react"; import { observer } from "mobx-react"; // plane package imports import { PROGRESS_STATE_GROUPS_DETAILS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ICycle, IIssueFilterOptions } from "@plane/types"; import { LinearProgressIndicator, Loader } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useProjectState } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export type ActiveCycleProgressProps = { cycle: ICycle | null; @@ -22,9 +22,10 @@ export type ActiveCycleProgressProps = { export const ActiveCycleProgress: FC = observer((props) => { const { handleFiltersUpdate, cycle } = props; + // plane hooks + const { t } = useTranslation(); // store hooks const { groupedProjectStates } = useProjectState(); - // derived values const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({ id: index, @@ -40,6 +41,7 @@ export const ActiveCycleProgress: FC = observer((props backlog: cycle?.backlog_issues, } : {}; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/progress" }); return cycle && cycle.hasOwnProperty("started_issues") ? (
@@ -101,7 +103,7 @@ export const ActiveCycleProgress: FC = observer((props
) : (
- +
)}
diff --git a/web/core/components/cycles/archived-cycles/root.tsx b/web/core/components/cycles/archived-cycles/root.tsx index 06f61b66f51..165b5e4aa34 100644 --- a/web/core/components/cycles/archived-cycles/root.tsx +++ b/web/core/components/cycles/archived-cycles/root.tsx @@ -2,28 +2,31 @@ import React from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -// types +// plane imports +import { useTranslation } from "@plane/i18n"; import { TCycleFilters } from "@plane/types"; // components import { ArchivedCyclesView, CycleAppliedFiltersList } from "@/components/cycles"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { CycleModuleListLayout } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useCycle, useCycleFilter } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ArchivedCycleLayoutRoot: React.FC = observer(() => { // router const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // hooks const { fetchArchivedCycles, currentProjectArchivedCycleIds, loader } = useCycle(); // cycle filters hook const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useCycleFilter(); // derived values const totalArchivedCycles = currentProjectArchivedCycleIds?.length ?? 0; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-cycles" }); useSWR( workspaceSlug && projectId ? `ARCHIVED_CYCLES_${workspaceSlug.toString()}_${projectId.toString()}` : null, @@ -64,7 +67,11 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => { )} {totalArchivedCycles === 0 ? (
- +
) : (
diff --git a/web/core/components/empty-state/empty-state.tsx b/web/core/components/empty-state/empty-state.tsx deleted file mode 100644 index faab4ebc290..00000000000 --- a/web/core/components/empty-state/empty-state.tsx +++ /dev/null @@ -1,194 +0,0 @@ -"use client"; - -import React from "react"; -import { observer } from "mobx-react"; -import Image from "next/image"; -import Link from "next/link"; - -import { useTheme } from "next-themes"; -// hooks -// components -import { Button, TButtonSizes, TButtonVariant } from "@plane/ui"; -// constant -import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { useUserPermissions } from "@/hooks/store"; -import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; -import { ComicBoxButton } from "./comic-box-button"; - -export type EmptyStateProps = { - size?: TButtonSizes; - type: EmptyStateType; - layout?: "screen-detailed" | "screen-simple"; - additionalPath?: string; - primaryButtonConfig?: { - size?: TButtonSizes; - variant?: TButtonVariant; - }; - primaryButtonOnClick?: () => void; - primaryButtonLink?: string; - secondaryButtonOnClick?: () => void; -}; - -export const EmptyState: React.FC = observer((props) => { - const { - size = "lg", - type, - layout = "screen-detailed", - additionalPath = "", - primaryButtonConfig = { - size: "lg", - variant: "primary", - }, - primaryButtonOnClick, - primaryButtonLink, - secondaryButtonOnClick, - } = props; - // store - const { allowPermissions } = useUserPermissions(); - // theme - const { resolvedTheme } = useTheme(); - - // if empty state type is not found - if (!EMPTY_STATE_DETAILS[type]) return null; - - // current empty state details - const { key, title, description, path, primaryButton, secondaryButton, accessType, access } = - EMPTY_STATE_DETAILS[type]; - // resolved empty state path - const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${ - resolvedTheme === "light" ? "light" : "dark" - }.webp`; - // permission - const isEditingAllowed = - access && - accessType && - allowPermissions( - access, - accessType === "workspace" ? EUserPermissionsLevel.WORKSPACE : EUserPermissionsLevel.PROJECT - ); - const anyButton = primaryButton || secondaryButton; - - // primary button - const renderPrimaryButton = () => { - if (!primaryButton) return null; - - const commonProps = { - size: primaryButtonConfig.size, - variant: primaryButtonConfig.variant, - prependIcon: primaryButton.icon, - onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined, - disabled: !isEditingAllowed, - }; - - if (primaryButton.comicBox) { - return ( - - ); - } else if (primaryButtonLink) { - return ( - - - - ); - } else { - return ; - } - }; - // secondary button - const renderSecondaryButton = () => { - if (!secondaryButton) return null; - - return ( - - ); - }; - - return ( - <> - {layout === "screen-detailed" && ( -
-
-
- {description ? ( - <> -

{title}

-

{description}

- - ) : ( -

{title}

- )} -
- - {path && ( - {key - )} - - {anyButton && ( -
- {renderPrimaryButton()} - {renderSecondaryButton()} -
- )} -
-
- )} - {layout === "screen-simple" && ( -
-
- {key -
- {description ? ( - <> -

{title}

-

{description}

- - ) : ( -

{title}

- )} - {anyButton && ( -
- {renderPrimaryButton()} - {renderSecondaryButton()} -
- )} -
- )} - - ); -}); diff --git a/web/core/components/empty-state/index.ts b/web/core/components/empty-state/index.ts index afa892f2712..3ba1c3ad302 100644 --- a/web/core/components/empty-state/index.ts +++ b/web/core/components/empty-state/index.ts @@ -1,4 +1,3 @@ -export * from "./empty-state"; export * from "./helper"; export * from "./comic-box-button"; export * from "./detailed-empty-state-root"; diff --git a/web/core/components/empty-state/simple-empty-state-root.tsx b/web/core/components/empty-state/simple-empty-state-root.tsx index d0ab95076a7..ce00a143f9d 100644 --- a/web/core/components/empty-state/simple-empty-state-root.tsx +++ b/web/core/components/empty-state/simple-empty-state-root.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; // utils import { cn } from "@plane/utils"; -type EmptyStateSize = "sm" | "md" | "lg"; +type EmptyStateSize = "sm" | "lg"; type Props = { title: string; @@ -17,12 +17,8 @@ type Props = { const sizeConfig = { sm: { - container: "size-20", - dimensions: 78, - }, - md: { container: "size-24", - dimensions: 80, + dimensions: 78, }, lg: { container: "size-28", diff --git a/web/core/components/exporter/guide.tsx b/web/core/components/exporter/guide.tsx index 7ee2899956a..65e77a6f200 100644 --- a/web/core/components/exporter/guide.tsx +++ b/web/core/components/exporter/guide.tsx @@ -8,19 +8,20 @@ import { useParams, useSearchParams } from "next/navigation"; import useSWR, { mutate } from "swr"; // icons import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { Exporter, SingleExport } from "@/components/exporter"; import { ImportExportSettingsLoader } from "@/components/ui"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; import { EXPORTERS_LIST } from "@/constants/workspace"; // hooks import { useProject, useUser, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services import { IntegrationService } from "@/services/integrations"; @@ -37,11 +38,14 @@ const IntegrationGuide = observer(() => { const { workspaceSlug } = useParams(); const searchParams = useSearchParams(); const provider = searchParams.get("provider"); + // plane hooks + const { t } = useTranslation(); // store hooks const { data: currentUser, canPerformAnyCreateAction } = useUser(); const { allowPermissions } = useUserPermissions(); - const { workspaceProjectIds } = useProject(); + // derived values + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/exports" }); const { data: exporterServices } = useSWR( workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, @@ -164,7 +168,11 @@ const IntegrationGuide = observer(() => {
) : (
- +
) ) : ( diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx index 8ebdb70664e..1d0be0c5b03 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// plane types +// plane imports +import { useTranslation } from "@plane/i18n"; import { THomeWidgetKeys, THomeWidgetProps } from "@plane/types"; // components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useHome } from "@/hooks/store/use-home"; // plane web components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { HomePageHeader } from "@/plane-web/components/home/header"; import { StickiesWidget } from "../stickies"; import { RecentActivityWidget } from "./widgets"; @@ -52,8 +52,12 @@ export const HOME_WIDGETS_LIST: { export const DashboardWidgets = observer(() => { // router const { workspaceSlug } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets, isAnyWidgetEnabled } = useHome(); + // derived values + const noWidgetsResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/dashboard/widgets" }); if (!workspaceSlug) return null; @@ -80,14 +84,10 @@ export const DashboardWidgets = observer(() => { ) : (
- toggleWidgetSettings(true)} - primaryButtonConfig={{ - size: "sm", - variant: "neutral-primary", - }} +
)} diff --git a/web/core/components/inbox/modals/select-duplicate.tsx b/web/core/components/inbox/modals/select-duplicate.tsx index 9bcefad490a..ccd95593148 100644 --- a/web/core/components/inbox/modals/select-duplicate.tsx +++ b/web/core/components/inbox/modals/select-duplicate.tsx @@ -4,18 +4,16 @@ import React, { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { Search } from "lucide-react"; import { Combobox, Dialog, Transition } from "@headlessui/react"; -// icons -// components -// types +// plane imports +import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse } from "@plane/types"; -// ui import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +// components +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useProject } from "@/hooks/store"; import useDebounce from "@/hooks/use-debounce"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // services import { ProjectService } from "@/services/project"; @@ -30,18 +28,19 @@ const projectService = new ProjectService(); export const SelectDuplicateInboxIssueModal: React.FC = (props) => { const { isOpen, onClose, onSubmit, value } = props; - - const [query, setQuery] = useState(""); - + // router const { workspaceSlug, projectId, issueId } = useParams(); - - // hooks - const { getProjectById } = useProject(); - + // states + const [query, setQuery] = useState(""); const [issues, setIssues] = useState([]); const [isSearching, setIsSearching] = useState(false); - + // hooks + const { getProjectById } = useProject(); + const { t } = useTranslation(); + // derived values const debouncedSearchTerm: string = useDebounce(query, 500); + const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); + const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" }); useEffect(() => { if (!isOpen || !workspaceSlug || !projectId) return; @@ -110,12 +109,11 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { ) : (
- + {query === "" ? ( + + ) : ( + + )}
); diff --git a/web/core/components/inbox/root.tsx b/web/core/components/inbox/root.tsx index 5d5617bb394..cea33743e6a 100644 --- a/web/core/components/inbox/root.tsx +++ b/web/core/components/inbox/root.tsx @@ -1,18 +1,19 @@ import { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; import { PanelLeft } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { Intake } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; import { InboxSidebar, InboxContentRoot } from "@/components/inbox"; import { InboxLayoutLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; // hooks import { useProjectInbox } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; type TInboxIssueRoot = { workspaceSlug: string; @@ -26,8 +27,12 @@ export const InboxIssueRoot: FC = observer((props) => { const { workspaceSlug, projectId, inboxIssueId, inboxAccessible, navigationTab } = props; // states const [isMobileSidebar, setIsMobileSidebar] = useState(true); + // plane hooks + const { t } = useTranslation(); // hooks const { loader, error, currentTab, handleCurrentTab, fetchInboxIssues } = useProjectInbox(); + // derived values + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" }); useEffect(() => { if (!inboxAccessible || !workspaceSlug || !projectId) return; @@ -96,7 +101,7 @@ export const InboxIssueRoot: FC = observer((props) => { /> ) : (
- +
)} diff --git a/web/core/components/inbox/sidebar/root.tsx b/web/core/components/inbox/sidebar/root.tsx index b8c86e0dee5..ea85b5dd4b6 100644 --- a/web/core/components/inbox/sidebar/root.tsx +++ b/web/core/components/inbox/sidebar/root.tsx @@ -2,14 +2,14 @@ import { FC, useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { TInboxIssueCurrentTab } from "@plane/types"; import { Header, Loader, EHeaderVariant } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; import { FiltersRoot, InboxIssueAppliedFilters, InboxIssueList } from "@/components/inbox"; import { InboxSidebarLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; @@ -17,6 +17,7 @@ import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; import { useProject, useProjectInbox } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; type IInboxSidebarProps = { workspaceSlug: string; @@ -38,9 +39,13 @@ const tabNavigationOptions: { key: TInboxIssueCurrentTab; label: string }[] = [ export const InboxSidebar: FC = observer((props) => { const { workspaceSlug, projectId, inboxIssueId, setIsMobileSidebar } = props; + // router + const router = useAppRouter(); // ref const containerRef = useRef(null); const [elementRef, setElementRef] = useState(null); + // plane hooks + const { t } = useTranslation(); // store const { currentProjectDetails } = useProject(); const { @@ -52,8 +57,11 @@ export const InboxSidebar: FC = observer((props) => { fetchInboxPaginationIssues, getAppliedFiltersCount, } = useProjectInbox(); - - const router = useAppRouter(); + // derived values + const sidebarAssetPath = useResolvedAssetPath({ basePath: "/empty-state/intake/intake-issue" }); + const sidebarFilterAssetPath = useResolvedAssetPath({ + basePath: "/empty-state/intake/filter-issue", + }); const fetchNextPages = useCallback(() => { if (!workspaceSlug || !projectId) return; @@ -128,16 +136,25 @@ export const InboxSidebar: FC = observer((props) => { /> ) : (
- 0 - ? EmptyStateType.INBOX_SIDEBAR_FILTER_EMPTY_STATE - : currentTab === EInboxIssueCurrentTab.OPEN - ? EmptyStateType.INBOX_SIDEBAR_OPEN_TAB - : EmptyStateType.INBOX_SIDEBAR_CLOSED_TAB - } - layout="screen-simple" - /> + {getAppliedFiltersCount > 0 ? ( + + ) : currentTab === EInboxIssueCurrentTab.OPEN ? ( + + ) : ( + + )}
)}
diff --git a/web/core/components/integration/guide.tsx b/web/core/components/integration/guide.tsx index da751cde8cf..e40085fce14 100644 --- a/web/core/components/integration/guide.tsx +++ b/web/core/components/integration/guide.tsx @@ -13,11 +13,9 @@ import { IImporterService } from "@plane/types"; // ui import { Button } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "@/components/integration"; import { ImportExportSettingsLoader } from "@/components/ui"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys"; import { IMPORTERS_LIST } from "@/constants/workspace"; // hooks @@ -28,6 +26,7 @@ import { IntegrationService } from "@/services/integrations"; // services const integrationService = new IntegrationService(); +// FIXME: [Deprecated] Remove this component const IntegrationGuide = observer(() => { // states const [refreshing, setRefreshing] = useState(false); @@ -137,7 +136,7 @@ const IntegrationGuide = observer(() => {
) : (
- + {/* */}
) ) : ( diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx index 7b481af355b..8a7e9210845 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx @@ -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 = 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 = observer((props) => { )) ) : (
- +
)} diff --git a/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx index 833a2d6054e..b4c3f7ae55c 100644 --- a/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -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 (
- 0 ? undefined : `/${workspaceSlug}/projects/${projectId}/settings/automations` - } - secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} - /> + {issueFilterCount > 0 ? ( + + ) : ( + router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`), + disabled: !canPerformEmptyStateActions, + }} + /> + )}
); }); diff --git a/web/core/components/issues/issue-layouts/empty-states/cycle.tsx b/web/core/components/issues/issue-layouts/empty-states/cycle.tsx index 274aa85883b..3a2ac22ef40 100644 --- a/web/core/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/cycle.tsx @@ -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; @@ -54,13 +80,6 @@ export const CycleEmptyState: React.FC = observer(() => { }) ); }; - 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 (
{ handleOnSubmit={handleAddIssuesToCycle} />
- { - setTrackElement("Cycle issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); - } - : undefined - } - secondaryButtonOnClick={ - !isCompletedAndEmpty && isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true) - } - /> + {isCompletedAndEmpty ? ( + + ) : isEmptyFilters ? ( + + ) : ( + { + 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, + }} + /> + )}
); diff --git a/web/core/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/core/components/issues/issue-layouts/empty-states/draft-issues.tsx index 1af3ca5a5c4..bb10176ae9f 100644 --- a/web/core/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -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 ( -
- 0 ? handleClearAllFilters : undefined} - /> -
- ); -}); +// FIXME: Project drafts is deprecated. Remove this component and all the related code. +export const ProjectDraftEmptyState: React.FC = observer(() => ( +
+)); diff --git a/web/core/components/issues/issue-layouts/empty-states/global-view.tsx b/web/core/components/issues/issue-layouts/empty-states/global-view.tsx index 8850fe22677..6eb8543d9e7 100644 --- a/web/core/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/global-view.tsx @@ -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; + if (workspaceProjectIds?.length === 0) { + return ( + { + setTrackElement("All issues empty state"); + toggleCreateProjectModal(true); + }} + disabled={!hasMemberLevelPermission} + /> + } + /> + ); + } return ( - 0 - ? currentView !== "custom-view" && currentView !== "subscribed" - ? () => { + title={t(`workspace_issues.empty_state.${resolvedCurrentView}.title`)} + description={t(`workspace_issues.empty_state.${resolvedCurrentView}.description`)} + assetPath={globalViewsResolvedPath} + primaryButton={ + ["subscribed", "custom-view"].includes(resolvedCurrentView) === false + ? { + text: t(`workspace_issues.empty_state.${resolvedCurrentView}.primary_button.text`), + onClick: () => { setTrackElement("All issues empty state"); toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - } - : undefined - : () => { - setTrackElement("All issues empty state"); - toggleCreateProjectModal(true); + }, + disabled: !hasMemberLevelPermission, } + : undefined } /> ); diff --git a/web/core/components/issues/issue-layouts/empty-states/module.tsx b/web/core/components/issues/issue-layouts/empty-states/module.tsx index 9e8a26955e6..517542b44ec 100644 --- a/web/core/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/module.tsx @@ -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; @@ -52,12 +73,6 @@ export const ModuleEmptyState: React.FC = observer(() => { ); }; - 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 (
{ handleOnSubmit={handleAddIssuesToModule} />
- { - setTrackElement("Module issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.MODULE); - } - } - secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} - /> + {isEmptyFilters ? ( + + ) : ( + { + 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, + }} + /> + )}
); diff --git a/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx b/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx index 12a31df838f..d67aad491f9 100644 --- a/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx @@ -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 ; + return ( + + ); }); diff --git a/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx b/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx index 213e5ac405c..e8bf3a9d565 100644 --- a/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx @@ -1,12 +1 @@ -// types -// components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; -// hooks - -export const ProjectEpicsEmptyState: React.FC = () => ( -
- {}} /> -
-); +export const ProjectEpicsEmptyState: React.FC = () => <>; diff --git a/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx index 5e437357a89..109b67c28d8 100644 --- a/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -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 (
- 0 - ? undefined - : () => { + {issueFilterCount > 0 ? ( + + ) : ( + { setTrackElement("Project issue empty state"); toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - } - } - secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} - /> + }} + disabled={!canPerformEmptyStateActions} + /> + } + /> + )}
); }); diff --git a/web/core/components/issues/workspace-draft/empty-state.tsx b/web/core/components/issues/workspace-draft/empty-state.tsx index 7a2893abd98..1c0b51fbb1d 100644 --- a/web/core/components/issues/workspace-draft/empty-state.tsx +++ b/web/core/components/issues/workspace-draft/empty-state.tsx @@ -2,15 +2,27 @@ import { FC, Fragment, useState } from "react"; // components -import { EIssuesStoreType } from "@plane/constants"; -import { EmptyState } from "@/components/empty-state"; +import { observer } from "mobx-react"; +import { EIssuesStoreType, EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { DetailedEmptyState } from "@/components/empty-state"; import { CreateUpdateIssueModal } from "@/components/issues"; // constants -import { EmptyStateType } from "@/constants/empty-state"; +import { useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -export const WorkspaceDraftEmptyState: FC = () => { +export const WorkspaceDraftEmptyState: FC = observer(() => { // state const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); + // store hooks + const { t } = useTranslation(); + const { allowPermissions } = useUserPermissions(); + // derived values + const canPerformEmptyStateActions = allowPermissions( + [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/cycles" }); return ( @@ -21,13 +33,19 @@ export const WorkspaceDraftEmptyState: FC = () => { isDraft />
- { - setIsDraftIssueModalOpen(true); + { + setIsDraftIssueModalOpen(true); + }, + disabled: !canPerformEmptyStateActions }} />
); -}; +}); diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx index 633fdead729..30f99bde671 100644 --- a/web/core/components/issues/workspace-draft/root.tsx +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -3,15 +3,18 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +// plane imports +import { EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants/src/user"; +import { useTranslation } from "@plane/i18n"; // components -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { EDraftIssuePaginationType } from "@/constants/workspace-drafts"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useCommandPalette, useProject, useWorkspaceDraftIssues } from "@/hooks/store"; +import { useCommandPalette, useProject, useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; // components import { DraftIssueBlock } from "./draft-issue-block"; @@ -24,10 +27,19 @@ type TWorkspaceDraftIssuesRoot = { export const WorkspaceDraftIssuesRoot: FC = observer((props) => { const { workspaceSlug } = props; + // plane hooks + const { t } = useTranslation(); // hooks const { loader, paginationInfo, fetchIssues, issueIds } = useWorkspaceDraftIssues(); const { workspaceProjectIds } = useProject(); const { toggleCreateProjectModal } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); + // derived values + const hasMemberLevelPermission = allowPermissions( + [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + const noProjectResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); //swr hook for fetching issue properties useWorkspaceIssueProperties(workspaceSlug); @@ -51,12 +63,22 @@ export const WorkspaceDraftIssuesRoot: FC = observer( if (workspaceProjectIds?.length === 0) return ( - { - toggleCreateProjectModal(true); - }} + title={t("workspace_projects.empty_state.no_projects.title")} + description={t("workspace_projects.empty_state.no_projects.description")} + assetPath={noProjectResolvedPath} + customPrimaryButton={ + { + toggleCreateProjectModal(true); + }} + disabled={!hasMemberLevelPermission} + /> + } /> ); diff --git a/web/core/components/labels/project-setting-label-list.tsx b/web/core/components/labels/project-setting-label-list.tsx index 2fc9c838277..e0943da3769 100644 --- a/web/core/components/labels/project-setting-label-list.tsx +++ b/web/core/components/labels/project-setting-label-list.tsx @@ -3,39 +3,40 @@ import React, { useState, useRef } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { IIssueLabel } from "@plane/types"; -// hooks import { Button, Loader } from "@plane/ui"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup, ProjectSettingLabelItem, } from "@/components/labels"; -import { EmptyStateType } from "@/constants/empty-state"; +// hooks import { useLabel, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +// plane web imports import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; -// components -// ui -// types -// constants export const ProjectSettingsLabelList: React.FC = observer(() => { + // router + const { workspaceSlug, projectId } = useParams(); + // refs + const scrollToRef = useRef(null); // states const [showLabelForm, setLabelForm] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [selectDeleteLabel, setSelectDeleteLabel] = useState(null); - // refs - const scrollToRef = useRef(null); - // router - const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel(); const { allowPermissions } = useUserPermissions(); - // derived values const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/project-settings/labels" }); const newLabel = () => { setIsUpdating(false); @@ -94,7 +95,11 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? (
- +
) : ( projectLabelsTree && ( diff --git a/web/core/components/modules/archived-modules/root.tsx b/web/core/components/modules/archived-modules/root.tsx index b74814add67..30ae74a5ce8 100644 --- a/web/core/components/modules/archived-modules/root.tsx +++ b/web/core/components/modules/archived-modules/root.tsx @@ -2,28 +2,30 @@ import React, { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -// types +// plane imports +import { useTranslation } from "@plane/i18n"; import { TModuleFilters } from "@plane/types"; // components -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { ArchivedModulesView, ModuleAppliedFiltersList } from "@/components/modules"; import { CycleModuleListLayout } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useModule, useModuleFilter } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ArchivedModuleLayoutRoot: React.FC = observer(() => { // router const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // hooks const { fetchArchivedModules, projectArchivedModuleIds, loader } = useModule(); - // module filters hook const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useModuleFilter(); // derived values const totalArchivedModules = projectArchivedModuleIds?.length ?? 0; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-modules" }); useSWR( workspaceSlug && projectId ? `ARCHIVED_MODULES_${workspaceSlug.toString()}_${projectId.toString()}` : null, @@ -69,7 +71,11 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => { )} {totalArchivedModules === 0 ? (
- +
) : (
diff --git a/web/core/components/modules/modules-list-view.tsx b/web/core/components/modules/modules-list-view.tsx index 4b45879f7a2..4d8eac8d3c1 100644 --- a/web/core/components/modules/modules-list-view.tsx +++ b/web/core/components/modules/modules-list-view.tsx @@ -2,15 +2,16 @@ import { observer } from "mobx-react"; import Image from "next/image"; import { useParams, useSearchParams } from "next/navigation"; // components +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ContentWrapper, Row, ERowVariant } from "@plane/ui"; import { ListLayout } from "@/components/core/list"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState, ComicBoxButton } from "@/components/empty-state"; import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules"; import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useCommandPalette, useEventTracker, useModule, useModuleFilter } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useModule, useModuleFilter, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import AllFiltersImage from "@/public/empty-state/module/all-filters.svg"; import NameFilterImage from "@/public/empty-state/module/name-filter.svg"; @@ -19,14 +20,24 @@ export const ModulesListView: React.FC = observer(() => { const { workspaceSlug, projectId } = useParams(); const searchParams = useSearchParams(); const peekModule = searchParams.get("peekModule"); + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleCreateModuleModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); const { getProjectModuleIds, getFilteredModuleIds, loader } = useModule(); const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter(); + const { allowPermissions } = useUserPermissions(); // derived values const projectModuleIds = projectId ? getProjectModuleIds(projectId.toString()) : undefined; const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined; + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const generalViewResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/onboarding/modules", + }); if (loader || !projectModuleIds || !filteredModuleIds) return ( @@ -39,12 +50,22 @@ export const ModulesListView: React.FC = observer(() => { if (projectModuleIds.length === 0) return ( - { - setTrackElement("Module empty state"); - toggleCreateModuleModal(true); - }} + { + setTrackElement("Module empty state"); + toggleCreateModuleModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } /> ); diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx index f4df757cd8b..b20ed914b5a 100644 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ b/web/core/components/page-views/workspace-dashboard.tsx @@ -1,23 +1,37 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// components +// plane imports +import { EUserPermissionsLevel } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ContentWrapper } from "@plane/ui"; +// components import { DashboardWidgets } from "@/components/dashboard"; -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { IssuePeekOverview } from "@/components/issues"; import { TourRoot } from "@/components/onboarding"; import { UserGreetingsView } from "@/components/user"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { PRODUCT_TOUR_COMPLETED } from "@/constants/event-tracker"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useCommandPalette, useUserProfile, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store"; +import { + useCommandPalette, + useUserProfile, + useEventTracker, + useDashboard, + useProject, + useUser, + useUserPermissions, +} from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import useSize from "@/hooks/use-window-size"; +import { EUserPermissions } from "@/plane-web/constants"; export const WorkspaceDashboardView = observer(() => { + // plane hooks + const { t } = useTranslation(); // store hooks const { // captureEvent, @@ -30,8 +44,11 @@ export const WorkspaceDashboardView = observer(() => { const { captureEvent } = useEventTracker(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { joinedProjectIds, loader } = useProject(); + const { allowPermissions } = useUserPermissions(); + // helper hooks const [windowWidth] = useSize(); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/dashboard" }); const handleTourCompleted = () => { updateTourCompleted() @@ -53,6 +70,11 @@ export const WorkspaceDashboardView = observer(() => { fetchHomeDashboardWidgets(workspaceSlug?.toString()); }, [fetchHomeDashboardWidgets, workspaceSlug]); + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + // TODO: refactor loader implementation return ( <> @@ -77,12 +99,22 @@ export const WorkspaceDashboardView = observer(() => { ) : ( - { - setTrackElement("Dashboard empty state"); - toggleCreateProjectModal(true); - }} + { + setTrackElement("Dashboard empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } /> )} diff --git a/web/core/components/pages/pages-list-main-content.tsx b/web/core/components/pages/pages-list-main-content.tsx index a0b4b356591..ec5ac519333 100644 --- a/web/core/components/pages/pages-list-main-content.tsx +++ b/web/core/components/pages/pages-list-main-content.tsx @@ -1,15 +1,16 @@ import { observer } from "mobx-react"; import Image from "next/image"; -// types +// plane imports +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TPageNavigationTabs } from "@plane/types"; // components -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { PageLoader } from "@/components/pages"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks import { EPageAccess } from "@/constants/page"; -import { useCommandPalette, useProjectPages } from "@/hooks/store"; +import { useCommandPalette, useProjectPages, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // assets import AllFiltersImage from "@/public/empty-state/pages/all-filters.svg"; import NameFilterImage from "@/public/empty-state/pages/name-filter.svg"; @@ -21,46 +22,90 @@ type Props = { export const PagesListMainContent: React.FC = observer((props) => { const { children, pageType } = props; + // plane hooks + const { t } = useTranslation(); // store hooks const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } = useProjectPages(); const { toggleCreatePageModal } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); // derived values const pageIds = getCurrentProjectPageIds(pageType); const filteredPageIds = getCurrentProjectFilteredPageIds(pageType); + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const generalPageResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/onboarding/pages", + }); + const publicPageResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/pages/public", + }); + const privatePageResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/pages/private", + }); + const archivedPageResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/pages/archived", + }); if (loader === "init-loader") return ; // if no pages exist in the active page type if (!isAnyPageAvailable || pageIds?.length === 0) { if (!isAnyPageAvailable) { return ( - { - toggleCreatePageModal({ isOpen: true }); + { + toggleCreatePageModal({ isOpen: true }); + }, + disabled: !canPerformEmptyStateActions, }} /> ); } if (pageType === "public") return ( - { - toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC }); + { + toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC }); + }, + disabled: !canPerformEmptyStateActions, }} /> ); if (pageType === "private") return ( - { - toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE }); + { + toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE }); + }, + disabled: !canPerformEmptyStateActions, }} /> ); - if (pageType === "archived") return ; + if (pageType === "archived") + return ( + + ); } // if no pages match the filter criteria if (filteredPageIds?.length === 0) diff --git a/web/core/components/project/card-list.tsx b/web/core/components/project/card-list.tsx index ff98c188c55..3d0fa1fae74 100644 --- a/web/core/components/project/card-list.tsx +++ b/web/core/components/project/card-list.tsx @@ -1,14 +1,18 @@ import { observer } from "mobx-react"; import Image from "next/image"; -// components +// plane imports +import { EUserPermissionsLevel } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ContentWrapper } from "@plane/ui"; -import { EmptyState } from "@/components/empty-state"; +// components +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { ProjectCard } from "@/components/project"; import { ProjectsLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useCommandPalette, useEventTracker, useProject, useProjectFilter } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useProject, useProjectFilter, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +// plane-web +import { EUserPermissions } from "@/plane-web/constants"; // assets import AllFiltersImage from "@/public/empty-state/project/all-filters.svg"; import NameFilterImage from "@/public/empty-state/project/name-filter.svg"; @@ -20,6 +24,8 @@ type TProjectCardListProps = { export const ProjectCardList = observer((props: TProjectCardListProps) => { const { totalProjectIds: totalProjectIdsProps, filteredProjectIds: filteredProjectIdsProps } = props; + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); @@ -30,20 +36,41 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => { loader, } = useProject(); const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter(); + const { allowPermissions } = useUserPermissions(); + + // helper hooks + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); + // derived values const workspaceProjectIds = totalProjectIdsProps ?? storeWorkspaceProjectIds; const filteredProjectIds = filteredProjectIdsProps ?? storeFilteredProjectIds; + // permissions + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + if (!filteredProjectIds || !workspaceProjectIds || loader) return ; if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects) return ( - { - setTrackElement("Project empty state"); - toggleCreateProjectModal(true); - }} + { + setTrackElement("Project empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } /> ); diff --git a/web/core/components/project/multi-select-modal.tsx b/web/core/components/project/multi-select-modal.tsx index e866f2595e6..013cd1248bf 100644 --- a/web/core/components/project/multi-select-modal.tsx +++ b/web/core/components/project/multi-select-modal.tsx @@ -4,16 +4,16 @@ import { observer } from "mobx-react"; import { Search, X } from "lucide-react"; import { Combobox } from "@headlessui/react"; // plane ui +import { useTranslation } from "@plane/i18n"; import { Button, Checkbox, EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; // components import { Logo } from "@/components/common"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; // hooks import { useProject } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; type Props = { isOpen: boolean; @@ -31,6 +31,8 @@ export const ProjectMultiSelectModal: React.FC = observer((props) => { const [isSubmitting, setIsSubmitting] = useState(false); // refs const moveButtonRef = useRef(null); + // plane hooks + const { t } = useTranslation(); // store hooks const { getProjectById } = useProject(); // derived values @@ -44,6 +46,9 @@ export const ProjectMultiSelectModal: React.FC = observer((props) => { const projectQuery = `${project?.identifier} ${project?.name}`.toLowerCase(); return projectQuery.includes(searchTerm.toLowerCase()); }); + const filteredProjectResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/search/project", + }); useEffect(() => { if (isOpen) setSelectedProjectIds(selectedProjectIdsProp); @@ -114,7 +119,11 @@ export const ProjectMultiSelectModal: React.FC = observer((props) => { > {filteredProjectIds.length === 0 ? (
- +
) : (
    { const { workspaceSlug, intersectionElement, columnCount } = props; // navigation const pathname = usePathname(); + // plane hooks + const { t } = useTranslation(); // store hooks const { getWorkspaceStickyIds, toggleShowNewSticky, searchQuery, loader } = useSticky(); + const { allowPermissions } = useUserPermissions(); // sticky operations const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); // derived values @@ -41,6 +49,14 @@ export const StickiesList = observer((props: TProps) => { const itemWidth = `${100 / columnCount}%`; const totalRows = Math.ceil(workspaceStickyIds.length / columnCount); const isStickiesPage = pathname?.includes("stickies"); + const hasGuestLevelPermissions = allowPermissions( + [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST], + EUserPermissionsLevel.WORKSPACE + ); + const stickiesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/stickies/stickies" }); + const stickiesSearchResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/stickies/stickies-search", + }); const masonryRef = useRef(null); const handleLayout = () => { @@ -84,19 +100,32 @@ export const StickiesList = observer((props: TProps) => { if (loader === "loaded" && workspaceStickyIds.length === 0) { return ( -
    - {isStickiesPage || searchQuery ? ( - { - toggleShowNewSticky(true); - stickyOperations.create(); - }} - primaryButtonConfig={{ - size: "sm", - }} - /> +
    + {isStickiesPage ? ( + <> + {searchQuery ? ( + + ) : ( + , + text: t("stickies.empty_state.general.primary_button.text"), + onClick: () => { + toggleShowNewSticky(true); + stickyOperations.create(); + }, + disabled: !hasGuestLevelPermissions, + }} + /> + )} + ) : ( )} diff --git a/web/core/components/views/views-list.tsx b/web/core/components/views/views-list.tsx index 6b7d4117241..29a0c4cb08f 100644 --- a/web/core/components/views/views-list.tsx +++ b/web/core/components/views/views-list.tsx @@ -1,31 +1,49 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +// plane imports +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; // components import { ListLayout } from "@/components/core/list"; -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState, SimpleEmptyState } from "@/components/empty-state"; import { ViewListLoader } from "@/components/ui"; import { ProjectViewListItem } from "@/components/views"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useCommandPalette, useProjectView } from "@/hooks/store"; -// assets +import { useCommandPalette, useProjectView, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ProjectViewsList = observer(() => { const { projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleCreateViewModal } = useCommandPalette(); const { getProjectViews, getFilteredProjectViews, loader } = useProjectView(); - + const { allowPermissions } = useUserPermissions(); + // derived values const projectViews = getProjectViews(projectId?.toString()); const filteredProjectViews = getFilteredProjectViews(projectId?.toString()); + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER, EUserProjectRoles.GUEST], + EUserPermissionsLevel.PROJECT + ); + const generalViewResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/onboarding/views", + }); + const filteredViewResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/search/views", + }); if (loader || !projectViews || !filteredProjectViews) return ; if (filteredProjectViews.length === 0 && projectViews.length > 0) { return (
    - +
    ); } @@ -43,7 +61,20 @@ export const ProjectViewsList = observer(() => {
    ) : ( - toggleCreateViewModal(true)} /> + toggleCreateViewModal(true)} + disabled={!canPerformEmptyStateActions} + /> + } + /> )} ); diff --git a/web/core/components/workspace-notifications/sidebar/empty-state.tsx b/web/core/components/workspace-notifications/sidebar/empty-state.tsx index 3b2b0c0f546..687e0c38c73 100644 --- a/web/core/components/workspace-notifications/sidebar/empty-state.tsx +++ b/web/core/components/workspace-notifications/sidebar/empty-state.tsx @@ -3,16 +3,33 @@ import { FC } from "react"; import { observer } from "mobx-react"; // components -import { EmptyState } from "@/components/empty-state"; +import { useTranslation } from "@plane/i18n"; +import { SimpleEmptyState } from "@/components/empty-state"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { ENotificationTab } from "@/constants/notification"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const NotificationEmptyState: FC = observer(() => { + // plane imports + const { t } = useTranslation(); // derived values - const currentTabEmptyState = ENotificationTab.ALL - ? EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE - : EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/notification" }); - return ; + return ( + <> + {ENotificationTab.ALL ? ( + + ) : ( + + )} + + ); }); diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index 93eb92bc08e..901dfcd65af 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -41,7 +41,7 @@ export const SidebarDropdown = observer(() => { }, { key: "settings", - name: t("workspace_settings"), + name: t("workspace_settings.label"), href: `/${workspaceSlug}/settings`, icon: Settings, access: [EUserPermissions.ADMIN], diff --git a/web/core/components/workspace/sidebar/user-menu.tsx b/web/core/components/workspace/sidebar/user-menu.tsx index e412fc7b65a..89e9cff3258 100644 --- a/web/core/components/workspace/sidebar/user-menu.tsx +++ b/web/core/components/workspace/sidebar/user-menu.tsx @@ -37,7 +37,7 @@ export const SidebarUserMenu = observer(() => { }, { key: "notifications", - labelTranslationKey: "inbox", + labelTranslationKey: "inbox.label", href: `/${workspaceSlug.toString()}/notifications/`, access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST], Icon: Inbox, diff --git a/web/core/constants/empty-state.tsx b/web/core/constants/empty-state.tsx deleted file mode 100644 index b502035de7f..00000000000 --- a/web/core/constants/empty-state.tsx +++ /dev/null @@ -1,960 +0,0 @@ -import { EUserPermissions } from "ee/constants/user-permissions"; -import { Plus, Shapes } from "lucide-react"; - -export interface EmptyStateDetails { - key: EmptyStateType; - title?: string; - description?: string; - path?: string; - primaryButton?: { - icon?: React.ReactNode; - text: string; - comicBox?: { - title?: string; - description?: string; - }; - }; - secondaryButton?: { - icon?: React.ReactNode; - text: string; - comicBox?: { - title?: string; - description?: string; - }; - }; - accessType?: "workspace" | "project"; - access?: any; -} - -export enum EmptyStateType { - WORKSPACE_DASHBOARD = "workspace-dashboard", - WORKSPACE_ANALYTICS = "workspace-analytics", - WORKSPACE_PROJECTS = "workspace-projects", - WORKSPACE_TEAMS = "workspace-teams", - WORKSPACE_INITIATIVES = "workspace-initiatives", - WORKSPACE_INITIATIVES_EMPTY_SEARCH = "workspace-initiatives-empty-search", - WORKSPACE_ALL_ISSUES = "workspace-all-issues", - WORKSPACE_ASSIGNED = "workspace-assigned", - WORKSPACE_CREATED = "workspace-created", - WORKSPACE_SUBSCRIBED = "workspace-subscribed", - WORKSPACE_CUSTOM_VIEW = "workspace-custom-view", - WORKSPACE_NO_PROJECTS = "workspace-no-projects", - WORKSPACE_PROJECT_NOT_FOUND = "workspace-project-not-found", - WORKSPACE_SETTINGS_API_TOKENS = "workspace-settings-api-tokens", - WORKSPACE_SETTINGS_WEBHOOKS = "workspace-settings-webhooks", - WORKSPACE_SETTINGS_EXPORT = "workspace-settings-export", - WORKSPACE_SETTINGS_IMPORT = "workspace-settings-import", - PROFILE_ACTIVITY = "profile-activity", - PROFILE_ASSIGNED = "profile-assigned", - PROFILE_CREATED = "profile-created", - PROFILE_SUBSCRIBED = "profile-subscribed", - PROJECT_SETTINGS_LABELS = "project-settings-labels", - PROJECT_SETTINGS_INTEGRATIONS = "project-settings-integrations", - PROJECT_SETTINGS_ESTIMATE = "project-settings-estimate", - PROJECT_CYCLES = "project-cycles", - PROJECT_CYCLE_NO_ISSUES = "project-cycle-no-issues", - PROJECT_CYCLE_ACTIVE = "project-cycle-active", - PROJECT_CYCLE_ALL = "project-cycle-all", - PROJECT_CYCLE_COMPLETED_NO_ISSUES = "project-cycle-completed-no-issues", - PROJECT_ARCHIVED_NO_CYCLES = "project-archived-no-cycles", - PROJECT_EMPTY_FILTER = "project-empty-filter", - PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter", - PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter", - PROJECT_NO_ISSUES = "project-no-issues", - PROJECT_ARCHIVED_NO_ISSUES = "project-archived-no-issues", - PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues", - VIEWS_EMPTY_SEARCH = "views-empty-search", - PROJECTS_EMPTY_SEARCH = "projects-empty-search", - MEMBERS_EMPTY_SEARCH = "members-empty-search", - PROJECT_MODULE_ISSUES = "project-module-issues", - PROJECT_MODULE = "project-module", - PROJECT_ARCHIVED_NO_MODULES = "project-archived-no-modules", - PROJECT_VIEW = "project-view", - PROJECT_PAGE = "project-page", - PROJECT_PAGE_PRIVATE = "project-page-private", - PROJECT_PAGE_PUBLIC = "project-page-public", - PROJECT_PAGE_ARCHIVED = "project-page-archived", - WORKSPACE_PAGE = "workspace-page", - WORKSPACE_PAGE_PRIVATE = "workspace-page-private", - WORKSPACE_PAGE_PUBLIC = "workspace-page-public", - WORKSPACE_PAGE_ARCHIVED = "workspace-page-archived", - - COMMAND_K_SEARCH_EMPTY_STATE = "command-k-search-empty-state", - ISSUE_RELATION_SEARCH_EMPTY_STATE = "issue-relation-search-empty-state", - ISSUE_RELATION_EMPTY_STATE = "issue-relation-empty-state", - ISSUE_COMMENT_EMPTY_STATE = "issue-comment-empty-state", - - EPIC_RELATION_SEARCH_EMPTY_STATE = "epic-relation-search-empty-state", - EPIC_RELATION_EMPTY_STATE = "epic-relation-empty-state", - - NOTIFICATION_DETAIL_EMPTY_STATE = "notification-detail-empty-state", - NOTIFICATION_ALL_EMPTY_STATE = "notification-all-empty-state", - NOTIFICATION_MENTIONS_EMPTY_STATE = "notification-mentions-empty-state", - NOTIFICATION_MY_ISSUE_EMPTY_STATE = "notification-my-issues-empty-state", - NOTIFICATION_CREATED_EMPTY_STATE = "notification-created-empty-state", - NOTIFICATION_SUBSCRIBED_EMPTY_STATE = "notification-subscribed-empty-state", - NOTIFICATION_ARCHIVED_EMPTY_STATE = "notification-archived-empty-state", - NOTIFICATION_SNOOZED_EMPTY_STATE = "notification-snoozed-empty-state", - NOTIFICATION_UNREAD_EMPTY_STATE = "notification-unread-empty-state", - - ACTIVE_CYCLE_PROGRESS_EMPTY_STATE = "active-cycle-progress-empty-state", - ACTIVE_CYCLE_CHART_EMPTY_STATE = "active-cycle-chart-empty-state", - ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE = "active-cycle-priority-issue-empty-state", - ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE = "active-cycle-assignee-empty-state", - ACTIVE_CYCLE_LABEL_EMPTY_STATE = "active-cycle-label-empty-state", - - WORKSPACE_ACTIVE_CYCLES = "workspace-active-cycles", - DISABLED_PROJECT_INBOX = "disabled-project-inbox", - DISABLED_PROJECT_CYCLE = "disabled-project-cycle", - DISABLED_PROJECT_MODULE = "disabled-project-module", - DISABLED_PROJECT_VIEW = "disabled-project-view", - DISABLED_PROJECT_PAGE = "disabled-project-page", - - INBOX_SIDEBAR_OPEN_TAB = "inbox-sidebar-open-tab", - INBOX_SIDEBAR_CLOSED_TAB = "inbox-sidebar-closed-tab", - INBOX_SIDEBAR_FILTER_EMPTY_STATE = "inbox-sidebar-filter-empty-state", - INBOX_DETAIL_EMPTY_STATE = "inbox-detail-empty-state", - - WORKSPACE_DRAFT_ISSUES = "workspace-draft-issues", - - PROJECT_NO_EPICS = "project-no-epics", - // Teams - TEAM_NO_ISSUES = "team-no-issues", - TEAM_EMPTY_FILTER = "team-empty-filter", - TEAM_VIEW = "team-view", - TEAM_PAGE = "team-page", - // stickies - STICKIES = "stickies", - STICKIES_SEARCH = "stickies-search", - // home widgets - HOME_WIDGETS = "home-widgets", -} - -const emptyStateDetails: Record = { - // workspace - [EmptyStateType.WORKSPACE_DASHBOARD]: { - key: EmptyStateType.WORKSPACE_DASHBOARD, - title: "Overview of your projects, activity, and metrics", - description: - " Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", - path: "/empty-state/onboarding/dashboard", - // path: "/empty-state/onboarding/", - primaryButton: { - text: "Build your first project", - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_ANALYTICS]: { - key: EmptyStateType.WORKSPACE_ANALYTICS, - title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", - description: - "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", - path: "/empty-state/onboarding/analytics", - primaryButton: { - text: "Start your first project", - comicBox: { - title: "Analytics works best with Cycles + Modules", - description: - "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_PROJECTS]: { - key: EmptyStateType.WORKSPACE_PROJECTS, - title: "No active projects", - description: - "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal. Create a new project or filter for archived projects.", - path: "/empty-state/onboarding/projects", - primaryButton: { - text: "Start your first project", - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_TEAMS]: { - key: EmptyStateType.WORKSPACE_TEAMS, - title: "Teams", - description: "Teams are groups of people who collaborate on projects. Create a team to get started.", - path: "/empty-state/teams/teams", - primaryButton: { - text: "Create new team", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN], - }, - [EmptyStateType.WORKSPACE_INITIATIVES]: { - key: EmptyStateType.WORKSPACE_INITIATIVES, - title: "Organize work at the highest level with Initiatives", - description: - "When you need to organize work spanning several projects and teams, Initiatives come in handy. Connect projects and epics to initiatives, see automatically rolled up updates, and see the forests before you get to the trees.", - path: "/empty-state/initiatives/initiatives", - primaryButton: { - text: "Create an initiative", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_INITIATIVES_EMPTY_SEARCH]: { - key: EmptyStateType.WORKSPACE_INITIATIVES_EMPTY_SEARCH, - title: "No matching initiatives", - description: "No initiatives detected with the matching criteria. \n Create a new initiative instead.", - path: "/empty-state/search/project", - }, - // all-issues - [EmptyStateType.WORKSPACE_ALL_ISSUES]: { - key: EmptyStateType.WORKSPACE_ALL_ISSUES, - title: "No issues in the project", - description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", - path: "/empty-state/all-issues/all-issues", - primaryButton: { - text: "Create new issue", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_ASSIGNED]: { - key: EmptyStateType.WORKSPACE_ASSIGNED, - title: "No issues yet", - description: "Issues assigned to you can be tracked from here.", - path: "/empty-state/all-issues/assigned", - primaryButton: { - text: "Create new issue", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_CREATED]: { - key: EmptyStateType.WORKSPACE_CREATED, - title: "No issues yet", - description: "All issues created by you come here, track them here directly.", - path: "/empty-state/all-issues/created", - primaryButton: { - text: "Create new issue", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_SUBSCRIBED]: { - key: EmptyStateType.WORKSPACE_SUBSCRIBED, - title: "No issues yet", - description: "Subscribe to issues you are interested in, track all of them here.", - path: "/empty-state/all-issues/subscribed", - }, - [EmptyStateType.WORKSPACE_CUSTOM_VIEW]: { - key: EmptyStateType.WORKSPACE_CUSTOM_VIEW, - title: "No issues yet", - description: "Issues that applies to the filters, track all of them here.", - path: "/empty-state/all-issues/custom-view", - }, - [EmptyStateType.WORKSPACE_PROJECT_NOT_FOUND]: { - key: EmptyStateType.WORKSPACE_PROJECT_NOT_FOUND, - title: "No such project exists", - description: "To create issues or manage your work, you need to create a project or be a part of one.", - path: "/empty-state/onboarding/projects", - primaryButton: { - text: "Create Project", - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_NO_PROJECTS]: { - key: EmptyStateType.WORKSPACE_NO_PROJECTS, - title: "No project", - description: "To create issues or manage your work, you need to create a project or be a part of one.", - path: "/empty-state/onboarding/projects", - primaryButton: { - text: "Start your first project", - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - // workspace settings - [EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS]: { - key: EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS, - title: "No API tokens created", - description: - "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.", - path: "/empty-state/workspace-settings/api-tokens", - }, - [EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS]: { - key: EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS, - title: "No webhooks added", - description: "Create webhooks to receive real-time updates and automate actions.", - path: "/empty-state/workspace-settings/webhooks", - }, - [EmptyStateType.WORKSPACE_SETTINGS_EXPORT]: { - key: EmptyStateType.WORKSPACE_SETTINGS_EXPORT, - title: "No previous exports yet", - description: "Anytime you export, you will also have a copy here for reference.", - path: "/empty-state/workspace-settings/exports", - }, - [EmptyStateType.WORKSPACE_SETTINGS_IMPORT]: { - key: EmptyStateType.WORKSPACE_SETTINGS_IMPORT, - title: "No previous imports yet", - description: "Find all your previous imports here and download them.", - path: "/empty-state/workspace-settings/imports", - }, - // profile - [EmptyStateType.PROFILE_ACTIVITY]: { - key: EmptyStateType.PROFILE_ASSIGNED, - title: "No activities yet", - description: - "Get started by creating a new issue! Add details and properties to it. Explore more in Plane to see your activity.", - path: "/empty-state/profile/activity", - }, - [EmptyStateType.PROFILE_ASSIGNED]: { - key: EmptyStateType.PROFILE_ASSIGNED, - title: "No issues are assigned to you", - description: "Issues assigned to you can be tracked from here.", - path: "/empty-state/profile/assigned", - }, - [EmptyStateType.PROFILE_CREATED]: { - key: EmptyStateType.PROFILE_CREATED, - title: "No issues yet", - description: "All issues created by you come here, track them here directly.", - path: "/empty-state/profile/created", - }, - [EmptyStateType.PROFILE_SUBSCRIBED]: { - key: EmptyStateType.PROFILE_SUBSCRIBED, - title: "No issues yet", - description: "Subscribe to issues you are interested in, track all of them here.", - path: "/empty-state/profile/subscribed", - }, - // project settings - [EmptyStateType.PROJECT_SETTINGS_LABELS]: { - key: EmptyStateType.PROJECT_SETTINGS_LABELS, - title: "No labels yet", - description: "Create labels to help organize and filter issues in you project.", - path: "/empty-state/project-settings/labels", - }, - [EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS]: { - key: EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS, - title: "No integrations configured", - description: "Configure GitHub and other integrations to sync your project issues.", - path: "/empty-state/project-settings/integrations", - }, - [EmptyStateType.PROJECT_SETTINGS_ESTIMATE]: { - key: EmptyStateType.PROJECT_SETTINGS_ESTIMATE, - title: "No estimates added", - description: "Create a set of estimates to communicate the amount of work per issue.", - path: "/empty-state/project-settings/estimates", - }, - // project cycles - [EmptyStateType.PROJECT_CYCLES]: { - key: EmptyStateType.PROJECT_CYCLES, - title: "Group and timebox your work in Cycles.", - description: - "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", - path: "/empty-state/onboarding/cycles", - primaryButton: { - text: "Set your first cycle", - comicBox: { - title: "Cycles are repetitive time-boxes.", - description: - "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", - }, - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_CYCLE_NO_ISSUES]: { - key: EmptyStateType.PROJECT_CYCLE_NO_ISSUES, - title: "No issues added to the cycle", - description: "Add or create issues you wish to timebox and deliver within this cycle", - path: "/empty-state/cycle-issues/", - primaryButton: { - text: "Create new issue ", - }, - secondaryButton: { - text: "Add an existing issue", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_CYCLE_ACTIVE]: { - key: EmptyStateType.PROJECT_CYCLE_ACTIVE, - title: "No active cycle", - description: - "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", - path: "/empty-state/cycle/active", - }, - [EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES]: { - key: EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES, - title: "No issues in the cycle", - description: - "No issues in the cycle. Issues are either transferred or hidden. To see hidden issues if any, update your display properties accordingly.", - path: "/empty-state/cycle/completed-no-issues", - }, - [EmptyStateType.PROJECT_ARCHIVED_NO_CYCLES]: { - key: EmptyStateType.PROJECT_ARCHIVED_NO_CYCLES, - title: "No archived cycles yet", - description: "To tidy up your project, archive completed cycles. Find them here once archived.", - path: "/empty-state/archived/empty-cycles", - }, - [EmptyStateType.PROJECT_CYCLE_ALL]: { - key: EmptyStateType.PROJECT_CYCLE_ALL, - title: "No cycles", - description: - "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", - path: "/empty-state/cycle/active", - }, - // empty filters - [EmptyStateType.PROJECT_EMPTY_FILTER]: { - key: EmptyStateType.PROJECT_EMPTY_FILTER, - title: "No issues found matching the filters applied", - path: "/empty-state/empty-filters/", - secondaryButton: { - text: "Clear all filters", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER]: { - key: EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER, - title: "No issues found matching the filters applied", - path: "/empty-state/empty-filters/", - secondaryButton: { - text: "Clear all filters", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER]: { - key: EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER, - title: "No issues found matching the filters applied", - path: "/empty-state/empty-filters/", - secondaryButton: { - text: "Clear all filters", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - // project issues - [EmptyStateType.PROJECT_NO_ISSUES]: { - key: EmptyStateType.PROJECT_NO_ISSUES, - title: "Create an issue and assign it to someone, even yourself", - description: - "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", - path: "/empty-state/onboarding/issues", - primaryButton: { - text: "Create your first issue", - comicBox: { - title: "Issues are building blocks in Plane.", - description: - "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", - }, - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES]: { - key: EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES, - title: "No archived issues yet", - description: - "Manually or through automation, you can archive issues that are completed or cancelled. Find them here once archived.", - path: "/empty-state/archived/empty-issues", - primaryButton: { - text: "Set automation", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_DRAFT_NO_ISSUES]: { - key: EmptyStateType.PROJECT_DRAFT_NO_ISSUES, - title: "No draft issues yet", - description: - "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", - path: "/empty-state/draft/draft-issues-empty", - }, - [EmptyStateType.VIEWS_EMPTY_SEARCH]: { - key: EmptyStateType.VIEWS_EMPTY_SEARCH, - title: "No matching views", - description: "No views match the search criteria. \n Create a new view instead.", - path: "/empty-state/search/views", - }, - [EmptyStateType.PROJECTS_EMPTY_SEARCH]: { - key: EmptyStateType.PROJECTS_EMPTY_SEARCH, - title: "No matching projects", - description: "No projects detected with the matching criteria. Create a new project instead.", - path: "/empty-state/search/project", - }, - [EmptyStateType.MEMBERS_EMPTY_SEARCH]: { - key: EmptyStateType.MEMBERS_EMPTY_SEARCH, - title: "No matching members", - description: "Add them to the project if they are already a part of the workspace", - path: "/empty-state/search/member", - }, - // project module - [EmptyStateType.PROJECT_MODULE_ISSUES]: { - key: EmptyStateType.PROJECT_MODULE_ISSUES, - title: "No issues in the module", - description: "Create or add issues which you want to accomplish as part of this module", - path: "/empty-state/module-issues/", - primaryButton: { - text: "Create new issue ", - }, - secondaryButton: { - text: "Add an existing issue", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_MODULE]: { - key: EmptyStateType.PROJECT_MODULE, - title: "Map your project milestones to Modules and track aggregated work easily.", - description: - "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", - path: "/empty-state/onboarding/modules", - primaryButton: { - text: "Build your first module", - comicBox: { - title: "Modules help group work by hierarchy.", - description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", - }, - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_ARCHIVED_NO_MODULES]: { - key: EmptyStateType.PROJECT_ARCHIVED_NO_MODULES, - title: "No archived Modules yet", - description: "To tidy up your project, archive completed or cancelled modules. Find them here once archived.", - path: "/empty-state/archived/empty-modules", - }, - // project views - [EmptyStateType.PROJECT_VIEW]: { - key: EmptyStateType.PROJECT_VIEW, - title: "Save filtered views for your project. Create as many as you need", - description: - "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", - path: "/empty-state/onboarding/views", - primaryButton: { - text: "Create your first view", - comicBox: { - title: "Views work atop Issue properties.", - description: "You can create a view from here with as many properties as filters as you see fit.", - }, - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - }, - // project pages - [EmptyStateType.PROJECT_PAGE]: { - key: EmptyStateType.PROJECT_PAGE, - title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", - description: - "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", - path: "/empty-state/onboarding/pages", - primaryButton: { - text: "Create your first page", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_PAGE_PRIVATE]: { - key: EmptyStateType.PROJECT_PAGE_PRIVATE, - title: "No private pages yet", - description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", - path: "/empty-state/pages/private", - primaryButton: { - text: "Create your first page", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_PAGE_PUBLIC]: { - key: EmptyStateType.PROJECT_PAGE_PUBLIC, - title: "No public pages yet", - description: "See pages shared with everyone in your project right here.", - path: "/empty-state/pages/public", - primaryButton: { - text: "Create your first page", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_PAGE_ARCHIVED]: { - key: EmptyStateType.PROJECT_PAGE_ARCHIVED, - title: "No archived pages yet", - description: "Archive pages not on your radar. Access them here when needed.", - path: "/empty-state/pages/archived", - }, - [EmptyStateType.WORKSPACE_PAGE]: { - key: EmptyStateType.WORKSPACE_PAGE, - title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", - description: - "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", - path: "/empty-state/onboarding/pages", - primaryButton: { - text: "Create your first page", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_PAGE_PRIVATE]: { - key: EmptyStateType.WORKSPACE_PAGE_PRIVATE, - title: "No private pages yet", - description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", - path: "/empty-state/pages/private", - primaryButton: { - text: "Create your first page", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_PAGE_PUBLIC]: { - key: EmptyStateType.WORKSPACE_PAGE_PUBLIC, - title: "No public pages yet", - description: "See pages shared with everyone in your workspace right here.", - path: "/empty-state/pages/public", - primaryButton: { - text: "Create your first page", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_PAGE_ARCHIVED]: { - key: EmptyStateType.WORKSPACE_PAGE_ARCHIVED, - title: "No archived pages yet", - description: "Archive pages not on your radar. Access them here when needed.", - path: "/empty-state/pages/archived", - }, - - [EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE]: { - key: EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE, - title: "No results found", - path: "/empty-state/search/search", - }, - [EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE]: { - key: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE, - title: "No matching issues found", - path: "/empty-state/search/search", - }, - [EmptyStateType.ISSUE_RELATION_EMPTY_STATE]: { - key: EmptyStateType.ISSUE_RELATION_EMPTY_STATE, - title: "No issues found", - path: "/empty-state/search/issues", - }, - - [EmptyStateType.EPIC_RELATION_SEARCH_EMPTY_STATE]: { - key: EmptyStateType.EPIC_RELATION_SEARCH_EMPTY_STATE, - title: "No matching epics found", - path: "/empty-state/search/search", - }, - [EmptyStateType.EPIC_RELATION_EMPTY_STATE]: { - key: EmptyStateType.EPIC_RELATION_EMPTY_STATE, - title: "No epics found", - path: "/empty-state/search/issues", - }, - - [EmptyStateType.ISSUE_COMMENT_EMPTY_STATE]: { - key: EmptyStateType.ISSUE_COMMENT_EMPTY_STATE, - title: "No comments yet", - description: "Comments can be used as a discussion and \n follow-up space for the issues", - path: "/empty-state/search/comments", - }, - - [EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE]: { - key: EmptyStateType.INBOX_DETAIL_EMPTY_STATE, - title: "Select to view details.", - path: "/empty-state/intake/issue-detail", - }, - [EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE, - title: "No issues assigned", - description: "Updates for issues assigned to you can be \n seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE, - title: "No issues assigned", - description: "Updates for issues assigned to you can be \n seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE, - title: "No issues assigned", - description: "Updates for issues assigned to you can be \n seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE, - title: "No updates to issues", - description: "Updates to issues created by you can be \n seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE, - title: "No updates to issues", - description: "Updates to any issue you are \n subscribed to can be seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE, - title: "No unread notifications", - description: "Congratulations, you are up-to-date \n with everything happening in the issues \n you care about", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE, - title: "No snoozed notifications yet", - description: "Any notification you snooze for later will \n be available here to act upon", - path: "/empty-state/search/snooze", - }, - [EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE, - title: "No archived notifications yet", - description: "Any notification you archive will be \n available here to help you focus", - path: "/empty-state/search/archive", - }, - - [EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE, - title: "Add issues to the cycle to view it's \n progress", - path: "/empty-state/active-cycle/progress", - }, - [EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE, - title: "Add issues to the cycle to view the \n burndown chart.", - path: "/empty-state/active-cycle/chart", - }, - [EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE, - title: "Observe high priority issues tackled in \n the cycle at a glance.", - path: "/empty-state/active-cycle/priority", - }, - [EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE, - title: "Add assignees to issues to see a \n breakdown of work by assignees.", - path: "/empty-state/active-cycle/assignee", - }, - [EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE, - title: "Add labels to issues to see the \n breakdown of work by labels.", - path: "/empty-state/active-cycle/label", - }, - [EmptyStateType.WORKSPACE_ACTIVE_CYCLES]: { - key: EmptyStateType.WORKSPACE_ACTIVE_CYCLES, - title: "No active cycles", - description: - "Cycles of your projects that includes any period that encompasses today's date within its range. Find the progress and details of all your active cycle here.", - path: "/empty-state/onboarding/workspace-active-cycles", - }, - [EmptyStateType.DISABLED_PROJECT_INBOX]: { - key: EmptyStateType.DISABLED_PROJECT_INBOX, - title: "Intake is not enabled for the project.", - description: - "Intake helps you manage incoming requests to your project and add them as issues in your workflow. Enable intake \n from project settings to manage requests.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/intake", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.DISABLED_PROJECT_CYCLE]: { - key: EmptyStateType.DISABLED_PROJECT_CYCLE, - title: "Cycles is not enabled for this project.", - description: - "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team. Enable the cycles feature for your project to start using them.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/cycles", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.DISABLED_PROJECT_MODULE]: { - key: EmptyStateType.DISABLED_PROJECT_MODULE, - title: "Modules are not enabled for the project.", - description: - "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. Enable modules from project settings.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/modules", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.DISABLED_PROJECT_PAGE]: { - key: EmptyStateType.DISABLED_PROJECT_PAGE, - title: "Pages are not enabled for the project.", - description: - "Pages are thought spotting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. Enable the pages feature to start creating them in your project.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/pages", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.DISABLED_PROJECT_VIEW]: { - key: EmptyStateType.DISABLED_PROJECT_VIEW, - title: "Views is not enabled for this project.", - description: - "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best. Enable views in the project settings to start using them.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/views", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.INBOX_SIDEBAR_OPEN_TAB]: { - key: EmptyStateType.INBOX_SIDEBAR_OPEN_TAB, - title: "No open issues", - description: "Find open issues here. Create new issue.", - path: "/empty-state/intake/intake-issue", - }, - [EmptyStateType.INBOX_SIDEBAR_CLOSED_TAB]: { - key: EmptyStateType.INBOX_SIDEBAR_CLOSED_TAB, - title: "No closed issues", - description: "All the issues whether accepted or \n declined can be found here.", - path: "/empty-state/intake/intake-issue", - }, - [EmptyStateType.INBOX_SIDEBAR_FILTER_EMPTY_STATE]: { - key: EmptyStateType.INBOX_SIDEBAR_FILTER_EMPTY_STATE, - title: "No matching issues", - description: "No issue matches filter applied in intake. \n Create a new issue.", - path: "/empty-state/intake/filter-issue", - }, - [EmptyStateType.INBOX_DETAIL_EMPTY_STATE]: { - key: EmptyStateType.INBOX_DETAIL_EMPTY_STATE, - title: "Select an issue to view its details.", - path: "/empty-state/intake/issue-detail", - }, - [EmptyStateType.WORKSPACE_DRAFT_ISSUES]: { - key: EmptyStateType.WORKSPACE_DRAFT_ISSUES, - title: "Half-written issues, and soon, comments will show up here.", - description: "To try this out, start adding an issue and leave it mid-way or create your first draft below. 😉", - path: "/empty-state/workspace-draft/issue", - primaryButton: { - text: "Create your first draft", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_NO_EPICS]: { - key: EmptyStateType.PROJECT_NO_EPICS, - title: "Create an epic and assign it to someone, even yourself", - description: - "For larger bodies of work that span several cycles and can live across modules, create an epic. Link issues and sub-issues in a project to an epic and jump into an issue from the overview.", - path: "/empty-state/onboarding/issues", - primaryButton: { - text: "Create an Epic", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - // Teams - [EmptyStateType.TEAM_NO_ISSUES]: { - key: EmptyStateType.TEAM_NO_ISSUES, - title: "Create an issue in your team projects and assign it to someone, even yourself", - description: - "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", - path: "/empty-state/onboarding/issues", - primaryButton: { - text: "Create your first issue", - comicBox: { - title: "Issues are building blocks in Plane.", - description: - "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.TEAM_EMPTY_FILTER]: { - key: EmptyStateType.TEAM_EMPTY_FILTER, - title: "No issues found matching the filters applied", - path: "/empty-state/empty-filters/", - secondaryButton: { - text: "Clear all filters", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.TEAM_VIEW]: { - key: EmptyStateType.TEAM_VIEW, - title: "Save filtered views for your team. Create as many as you need", - description: - "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a team can see everyone’s views and choose whichever suits their needs best.", - path: "/empty-state/onboarding/views", - primaryButton: { - text: "Create your first view", - comicBox: { - title: "Views work atop Issue properties.", - description: "You can create a view from here with as many properties as filters as you see fit.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.TEAM_PAGE]: { - key: EmptyStateType.TEAM_PAGE, - title: "Team pages are coming soon!", - description: - "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started. Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", - path: "/empty-state/onboarding/pages", - }, - [EmptyStateType.STICKIES]: { - key: EmptyStateType.STICKIES, - title: "Stickies are quick notes and to-dos you take down on the fly.", - description: - "Capture ideas, ahas, brainwaves, light-bulb moments without scrambling for a pen and paper, hunting for the recorder app on your phone, or firing up a notes app only to forget all about it later. Keep them all right next to your work so you can easily come back, build them up, or well, discard them.", - path: "/empty-state/stickies/stickies", - primaryButton: { - icon: , - text: "Add sticky", - }, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - accessType: "workspace", - }, - [EmptyStateType.STICKIES_SEARCH]: { - key: EmptyStateType.STICKIES_SEARCH, - title: "That doesn't match any of your stickies.", - description: "Try a different term or let us know\nif you are sure your search is right. ", - path: "/empty-state/stickies/stickies-search", - primaryButton: { - icon: , - text: "Add sticky", - }, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - accessType: "workspace", - }, - [EmptyStateType.HOME_WIDGETS]: { - key: EmptyStateType.HOME_WIDGETS, - title: "So much to add, yet such empty", - description: "You have turned off all your widgets. Turn some or\nall of them back on to see delightful things.", - path: "/empty-state/dashboard/widgets", - primaryButton: { - icon: , - text: "Manage widgets", - }, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - accessType: "workspace", - }, -} as const; - -export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/core/constants/profile.ts b/web/core/constants/profile.ts index d6ccf658324..14fc08638de 100644 --- a/web/core/constants/profile.ts +++ b/web/core/constants/profile.ts @@ -10,7 +10,7 @@ export const PROFILE_ACTION_LINKS: { Icon: React.FC; }[] = [ { - key: "profile", + key: "profile.label", label: "Profile", href: `/profile`, highlight: (pathname: string) => pathname === "/profile/", diff --git a/web/core/hooks/use-resolved-asset-path.tsx b/web/core/hooks/use-resolved-asset-path.tsx index f5543d2006e..4233dc6469c 100644 --- a/web/core/hooks/use-resolved-asset-path.tsx +++ b/web/core/hooks/use-resolved-asset-path.tsx @@ -4,14 +4,23 @@ type AssetPathConfig = { basePath: string; additionalPath?: string; extension?: string; + includeThemeInPath?: boolean; }; -export const useResolvedAssetPath = ({ basePath, additionalPath = "", extension = "webp" }: AssetPathConfig) => { +export const useResolvedAssetPath = ({ + basePath, + additionalPath = "", + extension = "webp", + includeThemeInPath = true, +}: AssetPathConfig) => { // hooks const { resolvedTheme } = useTheme(); - // resolved theme const theme = resolvedTheme === "light" ? "light" : "dark"; + if (!includeThemeInPath) { + return `${additionalPath && additionalPath !== "" ? `${basePath}${additionalPath}` : basePath}.${extension}`; + } + return `${additionalPath && additionalPath !== "" ? `${basePath}${additionalPath}` : basePath}-${theme}.${extension}`; }; diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index dcba2a8fef2..61d8f89d4b3 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -4,14 +4,13 @@ import { FC, ReactNode, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; - +// plane imports +import { useTranslation } from "@plane/i18n"; // components import { JoinProject } from "@/components/auth-screens"; import { LogoSpinner } from "@/components/common"; -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { ETimeLineTypeType } from "@/components/gantt-chart/contexts"; -//constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks import { useCommandPalette, @@ -26,6 +25,7 @@ import { useProjectView, useUserPermissions, } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { useTimeLineChart } from "@/hooks/use-timeline-chart"; // local import { persistence } from "@/local-db/storage.sqlite"; @@ -41,6 +41,8 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { children, isLoading: isParentLoading = false } = props; // router const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); @@ -56,6 +58,10 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { fetchProjectStates, fetchProjectStateTransitions } = useProjectState(); const { fetchProjectLabels } = useLabel(); const { getProjectEstimates } = useProjectEstimates(); + + // helper hooks + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); + // derived values const projectExists = projectId ? getProjectById(projectId.toString()) : null; const projectMemberInfo = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]; @@ -151,6 +157,12 @@ export const ProjectAuthWrapper: FC = observer((props) => { { revalidateIfStale: false, revalidateOnFocus: false } ); + // permissions + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + // check if the project member apis is loading if (isParentLoading || (!projectMemberInfo && projectId && hasPermissionToCurrentProject === null)) return ( @@ -168,13 +180,22 @@ export const ProjectAuthWrapper: FC = observer((props) => { if (!loader && !projectExists && projectId && !!hasPermissionToCurrentProject === false) return (
    - { - setTrackElement("Projects page empty state"); - toggleCreateProjectModal(true); - }} + { + setTrackElement("Project empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } />
    ); diff --git a/web/core/store/user/permissions.store.ts b/web/core/store/user/permissions.store.ts index ba1211b0f1a..2c745e6bc2d 100644 --- a/web/core/store/user/permissions.store.ts +++ b/web/core/store/user/permissions.store.ts @@ -3,6 +3,7 @@ import unset from "lodash/unset"; import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types +import { EUserProjectRoles, EUserWorkspaceRoles } from "@plane/constants"; import { IProjectMember, IUserProjectsRole, IWorkspaceMemberMe } from "@plane/types"; // plane web types import { @@ -22,6 +23,8 @@ import { CoreRootStore } from "@/store/root.store"; // derived services const workspaceService = new WorkspaceService(); +type ETempUserRole = TUserPermissions | EUserWorkspaceRoles | EUserProjectRoles; // TODO: Remove this once we have migrated user permissions to enums to plane constants package + export interface IUserPermissionStore { loader: boolean; // observables @@ -36,7 +39,7 @@ export interface IUserPermissionStore { projectId: string ) => TUserPermissions | undefined; allowPermissions: ( - allowPermissions: TUserPermissions[], + allowPermissions: ETempUserRole[], level: TUserPermissionsLevel, workspaceSlug?: string, projectId?: string, @@ -115,7 +118,7 @@ export class UserPermissionStore implements IUserPermissionStore { * @returns { boolean } */ allowPermissions = ( - allowPermissions: TUserPermissions[], + allowPermissions: ETempUserRole[], level: TUserPermissionsLevel, workspaceSlug?: string, projectId?: string, @@ -140,7 +143,7 @@ export class UserPermissionStore implements IUserPermissionStore { this.projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId)) as EUserPermissions | undefined; } - if (currentUserRole && allowPermissions.includes(currentUserRole)) { + if (currentUserRole && allowPermissions.includes(currentUserRole as TUserPermissions)) { if (onPermissionAllowed) { return onPermissionAllowed(); } else { From 9093b411325cd509359d0c073ad09a3146d2abcc Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:31:16 +0530 Subject: [PATCH 04/93] fix: language support fo profile (#6461) * fix: ln support fo profile * fix: merge changes * fix: merge changes --- packages/constants/src/index.ts | 1 + .../constants/src}/profile.ts | 35 ++--- .../i18n/src/locales/en/translations.json | 40 +++++ .../i18n/src/locales/es/translations.json | 41 +++++ .../i18n/src/locales/fr/translations.json | 41 +++++ .../i18n/src/locales/ja/translations.json | 50 +++++- .../i18n/src/locales/zh-CN/translations.json | 44 +++++- .../(projects)/profile/[userId]/header.tsx | 8 +- .../(projects)/profile/[userId]/layout.tsx | 4 +- .../(projects)/profile/[userId]/navbar.tsx | 4 +- .../(projects)/profile/[userId]/page.tsx | 4 +- web/app/profile/sidebar.tsx | 31 +++- .../components/profile/overview/activity.tsx | 8 +- .../overview/priority-distribution.tsx | 142 +++++++++--------- .../profile/overview/state-distribution.tsx | 8 +- .../components/profile/overview/stats.tsx | 13 +- .../components/profile/overview/workload.tsx | 65 ++++---- web/core/components/profile/sidebar.tsx | 26 +++- 18 files changed, 408 insertions(+), 157 deletions(-) rename {web/core/constants => packages/constants/src}/profile.ts (69%) diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 2b3964ae9fd..673a76e7b6f 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -14,3 +14,4 @@ export * from "./swr"; export * from "./user"; export * from "./workspace"; export * from "./stickies"; +export * from "./profile"; diff --git a/web/core/constants/profile.ts b/packages/constants/src/profile.ts similarity index 69% rename from web/core/constants/profile.ts rename to packages/constants/src/profile.ts index 14fc08638de..f7765a0cfc0 100644 --- a/web/core/constants/profile.ts +++ b/packages/constants/src/profile.ts @@ -1,48 +1,38 @@ -import React from "react"; -// icons -import { Activity, Bell, CircleUser, KeyRound, LucideProps, Settings2 } from "lucide-react"; - export const PROFILE_ACTION_LINKS: { key: string; - label: string; + i18n_label: string; href: string; highlight: (pathname: string) => boolean; - Icon: React.FC; }[] = [ { - key: "profile.label", - label: "Profile", + key: "profile", + i18n_label: "profile.actions.profile", href: `/profile`, highlight: (pathname: string) => pathname === "/profile/", - Icon: CircleUser, }, { key: "security", - label: "Security", + i18n_label: "profile.actions.security", href: `/profile/security`, highlight: (pathname: string) => pathname === "/profile/security/", - Icon: KeyRound, }, { key: "activity", - label: "Activity", + i18n_label: "profile.actions.activity", href: `/profile/activity`, highlight: (pathname: string) => pathname === "/profile/activity/", - Icon: Activity, }, { key: "appearance", - label: "Appearance", + i18n_label: "profile.actions.appearance", href: `/profile/appearance`, highlight: (pathname: string) => pathname.includes("/profile/appearance"), - Icon: Settings2, }, { key: "notifications", - label: "Notifications", + i18n_label: "profile.actions.notifications", href: `/profile/notifications`, highlight: (pathname: string) => pathname === "/profile/notifications/", - Icon: Bell, }, ]; @@ -50,7 +40,7 @@ export const PROFILE_VIEWER_TAB = [ { key: "summary", route: "", - label: "Summary", + i18n_label: "profile.tabs.summary", selected: "/", }, ]; @@ -59,24 +49,25 @@ export const PROFILE_ADMINS_TAB = [ { key: "assigned", route: "assigned", - label: "Assigned", + i18n_label: "profile.tabs.assigned", selected: "/assigned/", }, { + key: "created", route: "created", - label: "Created", + i18n_label: "profile.tabs.created", selected: "/created/", }, { key: "subscribed", route: "subscribed", - label: "Subscribed", + i18n_label: "profile.tabs.subscribed", selected: "/subscribed/", }, { key: "activity", route: "activity", - label: "Activity", + i18n_label: "profile.tabs.activity", selected: "/activity/", }, ]; diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index d52d125529b..276384a0b4b 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -314,6 +314,7 @@ "remove_parent_issue": "Remove parent issue", "add_parent": "Add parent", "loading_members": "Loading members...", + "no_data_yet": "No Data yet", "connections": "Connections", "workspace_dashboard": { @@ -437,6 +438,45 @@ "profile": { "label": "Profile", + "page_label": "Your work", + "work": "Work", + "details": { + "joined_on": "Joined on", + "time_zone": "Timezone" + }, + "stats": { + "workload": "Workload", + "overview": "Overview", + "created": "Issues created", + "assigned": "Issues assigned", + "subscribed": "Issues subscribed", + "state_distribution": { + "title": "Issues by state", + "empty": "Create issues to view the them by states in the graph for better analysis." + }, + "priority_distribution": { + "title": "Issues by Priority", + "empty": "Create issues to view the them by priority in the graph for better analysis." + }, + "recent_activity": { + "title": "Recent activity", + "empty": "We couldn't find data. Kindly view your inputs" + } + }, + "actions": { + "profile": "Profile", + "security": "Security", + "activity": "Activity", + "appearance": "Appearance", + "notifications": "Notifications" + }, + "tabs": { + "summary": "Summary", + "assigned": "Assigned", + "created": "Created", + "subscribed": "Subscribed", + "activity": "Activity" + }, "empty_state": { "activity": { "title": "No activities yet", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index f516cf152d6..5fe8916cb5c 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -313,6 +313,8 @@ "remove_parent_issue": "Eliminar problema padre", "add_parent": "Agregar padre", "loading_members": "Cargando miembros...", + "inbox": "bandeja de entrada", + "no_data_yet": "Sin datos aún", "connections": "Conexiones", "workspace_dashboard": { @@ -436,6 +438,45 @@ "profile": { "label": "Perfil", + "page_label": "Tu trabajo", + "work": "Trabajo", + "details": { + "joined_on": "Se unió el", + "time_zone": "Zona horaria" + }, + "stats": { + "workload": "Carga de trabajo", + "overview": "Resumen", + "created": "Problemas creados", + "assigned": "Problemas asignados", + "subscribed": "Problemas suscritos", + "state_distribution": { + "title": "Problemas por estado", + "empty": "Crea problemas para verlos por estados en el gráfico para un mejor análisis." + }, + "priority_distribution": { + "title": "Problemas por prioridad", + "empty": "Crea problemas para verlos por prioridad en el gráfico para un mejor análisis." + }, + "recent_activity": { + "title": "Actividad reciente", + "empty": "No pudimos encontrar datos. Por favor revisa tus entradas" + } + }, + "actions": { + "profile": "Perfil", + "security": "Seguridad", + "activity": "Actividad", + "appearance": "Apariencia", + "notifications": "Notificaciones" + }, + "tabs": { + "summary": "Resumen", + "assigned": "Asignados", + "created": "Creados", + "subscribed": "Suscritos", + "activity": "Actividad" + }, "empty_state": { "activity": { "title": "Aún no hay actividades", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 4d5d7512efd..e4490819d4a 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -314,6 +314,8 @@ "remove_parent_issue": "Supprimer le problème parent", "add_parent": "Ajouter un parent", "loading_members": "Chargement des membres...", + "inbox": "Boîte de réception", + "no_data_yet": "Pas encore de données", "connections": "Connexions", "workspace_dashboard": { @@ -437,6 +439,45 @@ "profile": { "label": "Profil", + "page_label": "Votre travail", + "work": "Travail", + "details": { + "joined_on": "Inscrit le", + "time_zone": "Fuseau horaire" + }, + "stats": { + "workload": "Charge de travail", + "overview": "Aperçu", + "created": "Problèmes créés", + "assigned": "Problèmes assignés", + "subscribed": "Problèmes suivis", + "state_distribution": { + "title": "Problèmes par état", + "empty": "Créez des problèmes pour les visualiser par état dans le graphique pour une meilleure analyse." + }, + "priority_distribution": { + "title": "Problèmes par priorité", + "empty": "Créez des problèmes pour les visualiser par priorité dans le graphique pour une meilleure analyse." + }, + "recent_activity": { + "title": "Activité récente", + "empty": "Nous n'avons pas trouvé de données. Veuillez consulter vos entrées" + } + }, + "actions": { + "profile": "Profil", + "security": "Sécurité", + "activity": "Activité", + "appearance": "Apparence", + "notifications": "Notifications" + }, + "tabs": { + "summary": "Résumé", + "assigned": "Assignés", + "created": "Créés", + "subscribed": "Suivis", + "activity": "Activité" + }, "empty_state": { "activity": { "title": "Aucune activité pour le moment", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index d7c8e76322d..361cda4753a 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -314,14 +314,15 @@ "remove_parent_issue": "親問題を削除", "add_parent": "親問題を追加", "loading_members": "メンバーを読み込んでいます...", + "no_data_yet": "まだデータがありません", "connections": "接続", "workspace_dashboard": { - "empty_state": { - "general": { - "title": "プロジェクト、アクティビティ、指標の概要", - "description": "Planeへようこそ!私たちはあなたを迎えることができて嬉しいです。最初のプロジェクトを作成してタスクを追跡し、このページが進捗を助けるスペースに変わります。管理者は、チームを前進させるための要素も見ることができます。", - "primary_button": { + "empty_state": { + "general": { + "title": "プロジェクト、アクティビティ、指標の概要", + "description": "Planeへようこそ!私たちはあなたを迎えることができて嬉しいです。最初のプロジェクトを作成してタスクを追跡し、このページが進捗を助けるスペースに変わります。管理者は、チームを前進させるための要素も見ることができます。", + "primary_button": { "text": "最初のプロジェクトを作成", "comic": { "title": "すべてはPlaneでのプロジェクトから始まります", @@ -437,6 +438,45 @@ "profile": { "label": "プロフィール", + "page_label": "あなたの作業", + "work": "作業", + "details": { + "joined_on": "参加日", + "time_zone": "タイムゾーン" + }, + "stats": { + "workload": "作業負荷", + "overview": "概要", + "created": "作成した課題", + "assigned": "割り当てられた課題", + "subscribed": "購読中の課題", + "state_distribution": { + "title": "状態別の課題", + "empty": "より良い分析のために、グラフで状態別に表示する課題を作成してください。" + }, + "priority_distribution": { + "title": "優先度別の課題", + "empty": "より良い分析のために、グラフで優先度別に表示する課題を作成してください。" + }, + "recent_activity": { + "title": "最近の活動", + "empty": "データが見つかりませんでした。入力内容を確認してください" + } + }, + "actions": { + "profile": "プロフィール", + "security": "セキュリティ", + "activity": "アクティビティ", + "appearance": "外観", + "notifications": "通知" + }, + "tabs": { + "summary": "サマリー", + "assigned": "割り当て済み", + "created": "作成済み", + "subscribed": "購読中", + "activity": "アクティビティ" + }, "empty_state": { "activity": { "title": "まだアクティビティがありません", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 093027c16b1..0da2835d89f 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -315,5 +315,47 @@ "remove_parent_issue": "移除父问题", "add_parent": "添加父问题", "loading_members": "正在加载成员...", - "inbox": "收件箱" + "inbox": "收件箱", + "no_data_yet": "暂无数据", + "user_profile": { + "title": "你的工作", + "work": "工作", + "details": { + "joined_on": "加入时间", + "time_zone": "时区" + }, + "stats": { + "workload": "工作量", + "overview": "概述", + "created": "已创建问题", + "assigned": "已分配问题", + "subscribed": "已订阅问题", + "state_distribution": { + "title": "按状态显示的问题", + "empty": "创建问题以在图表中按状态查看它们,以便更好地分析。" + }, + "priority_distribution": { + "title": "按优先级显示的问题", + "empty": "创建问题以在图表中按优先级查看它们,以便更好地分析。" + }, + "recent_activity": { + "title": "最近活动", + "empty": "我们找不到数据。请查看您的输入" + } + }, + "actions": { + "profile": "个人资料", + "security": "安全", + "activity": "活动", + "appearance": "外观", + "notifications": "通知" + }, + "tabs": { + "summary": "摘要", + "assigned": "已分配", + "created": "已创建", + "subscribed": "已订阅", + "activity": "活动" + } + } } diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx index 13a944c8819..fdffd0a0864 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx @@ -6,12 +6,13 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { ChevronDown, PanelRight } from "lucide-react"; +import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { IUserProfileProjectSegregation } from "@plane/types"; import { Breadcrumbs, Header, CustomMenu, UserActivityIcon } from "@plane/ui"; import { BreadcrumbLink } from "@/components/common"; // components import { ProfileIssuesFilter } from "@/components/profile"; -import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile"; import { cn } from "@/helpers/common.helper"; import { useAppTheme, useUser, useUserPermissions } from "@/hooks/store"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; @@ -30,6 +31,7 @@ export const UserProfileHeader: FC = observer((props) => { const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme(); const { data: currentUser } = useUser(); const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const { t } = useTranslation(); // derived values const isAuthorized = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], @@ -44,7 +46,7 @@ export const UserProfileHeader: FC = observer((props) => { const isCurrentUser = currentUser?.id === userId; - const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Work`; + const breadcrumbLabel = isCurrentUser ? t("profile.page_label") : `${userName} ${t("profile.work")}`; return (
    @@ -86,7 +88,7 @@ export const UserProfileHeader: FC = observer((props) => { href={`/${workspaceSlug}/profile/${userId}/${tab.route}`} className="w-full text-custom-text-300" > - {tab.label} + {t(tab.i18n_label)} ))} diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx index d6743e8f2ba..3209586d4db 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx @@ -4,12 +4,12 @@ import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; import useSWR from "swr"; // components +import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { AppHeader, ContentWrapper } from "@/components/core"; import { ProfileSidebar } from "@/components/profile"; // constants import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys"; -import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile"; // hooks import { useUserPermissions } from "@/hooks/store"; import useSize from "@/hooks/use-window-size"; @@ -66,7 +66,7 @@ const UseProfileLayout: React.FC = observer((props) => { diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx index e002f8f6641..a91836518a7 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx @@ -2,12 +2,12 @@ import React from "react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; +import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components // constants import { Header, EHeaderVariant } from "@plane/ui"; -import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile"; type Props = { isAuthorized: boolean; @@ -33,7 +33,7 @@ export const ProfileNavbar: React.FC = (props) => { : "border-transparent" }`} > - {t(tab.label)} + {t(tab.i18n_label)} ))} diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/page.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/page.tsx index 480e30aed37..aaacff0d7ed 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/page.tsx @@ -3,6 +3,7 @@ import { useParams } from "next/navigation"; import useSWR from "swr"; // types +import { useTranslation } from "@plane/i18n"; import { IUserStateDistribution, TStateGroups } from "@plane/types"; // components import { ContentWrapper } from "@plane/ui"; @@ -26,6 +27,7 @@ const userService = new UserService(); export default function ProfileOverviewPage() { const { workspaceSlug, userId } = useParams(); + const { t } = useTranslation(); const { data: userProfile } = useSWR( workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null, workspaceSlug && userId ? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString()) : null @@ -40,7 +42,7 @@ export default function ProfileOverviewPage() { return ( <> - + diff --git a/web/app/profile/sidebar.tsx b/web/app/profile/sidebar.tsx index d3b98642161..77fdfb25448 100644 --- a/web/app/profile/sidebar.tsx +++ b/web/app/profile/sidebar.tsx @@ -5,15 +5,26 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; // icons -import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react"; +import { + ChevronLeft, + LogOut, + MoveLeft, + Plus, + UserPlus, + Activity, + Bell, + CircleUser, + KeyRound, + Settings2, +} from "lucide-react"; // plane imports +import { PROFILE_ACTION_LINKS } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; // components import { SidebarNavItem } from "@/components/sidebar"; // constants -import { PROFILE_ACTION_LINKS } from "@/constants/profile"; // helpers import { cn } from "@/helpers/common.helper"; import { getFileURL } from "@/helpers/file.helper"; @@ -36,6 +47,19 @@ const WORKSPACE_ACTION_LINKS = [ }, ]; +export const ProjectActionIcons = ({ type, size, className }: { type: string; size?: number; className?: string }) => { + const icons = { + profile: CircleUser, + security: KeyRound, + activity: Activity, + appearance: Settings2, + notifications: Bell, + }; + + if (type === undefined) return null; + const Icon = icons[type as keyof typeof icons]; + return ; +}; export const ProfileLayoutSidebar = observer(() => { // states const [isSigningOut, setIsSigningOut] = useState(false); @@ -145,7 +169,8 @@ export const ProfileLayoutSidebar = observer(() => { isActive={link.highlight(pathname)} >
    - + + {!sidebarCollapsed &&

    {t(link.key)}

    }
    diff --git a/web/core/components/profile/overview/activity.tsx b/web/core/components/profile/overview/activity.tsx index 7e5e6062132..9ac14b2aafd 100644 --- a/web/core/components/profile/overview/activity.tsx +++ b/web/core/components/profile/overview/activity.tsx @@ -4,6 +4,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; // ui +import { useTranslation } from "@plane/i18n"; import { Loader, Card } from "@plane/ui"; // components import { ActivityMessage, IssueLink } from "@/components/core"; @@ -26,6 +27,7 @@ export const ProfileActivity = observer(() => { const { workspaceSlug, userId } = useParams(); // store hooks const { data: currentUser } = useUser(); + const { t } = useTranslation(); const { data: userProfileActivity } = useSWR( workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), {}) : null, @@ -39,7 +41,7 @@ export const ProfileActivity = observer(() => { return (
    -

    Recent activity

    +

    {t("profile.stats.recent_activity.title")}

    {userProfileActivity ? ( userProfileActivity.results.length > 0 ? ( @@ -81,8 +83,8 @@ export const ProfileActivity = observer(() => {
    ) : ( ) diff --git a/web/core/components/profile/overview/priority-distribution.tsx b/web/core/components/profile/overview/priority-distribution.tsx index 66f0e02e718..856f7259157 100644 --- a/web/core/components/profile/overview/priority-distribution.tsx +++ b/web/core/components/profile/overview/priority-distribution.tsx @@ -1,6 +1,7 @@ "use client"; // ui +import { useTranslation } from "@plane/i18n"; import { IUserProfileData } from "@plane/types"; import { Loader, Card } from "@plane/ui"; import { BarGraph, ProfileEmptyState } from "@/components/ui"; @@ -14,77 +15,80 @@ type Props = { userProfile: IUserProfileData | undefined; }; -export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => ( -
    -

    Issues by Priority

    - {userProfile ? ( - - {userProfile.priority_distribution.length > 0 ? ( - ({ - priority: capitalizeFirstLetter(priority.priority ?? "None"), - value: priority.priority_count, - }))} - height="300px" - indexBy="priority" - keys={["value"]} - borderRadius={4} - padding={0.7} - customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)} - tooltip={(datum) => ( -
    - - {datum.data.priority}: - {datum.value} -
    - )} - colors={(datum) => { - if (datum.data.priority === "Urgent") return "#991b1b"; - else if (datum.data.priority === "High") return "#ef4444"; - else if (datum.data.priority === "Medium") return "#f59e0b"; - else if (datum.data.priority === "Low") return "#16a34a"; - else return "#e5e5e5"; - }} - theme={{ - axis: { - domain: { +export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => { + const { t } = useTranslation(); + return ( +
    +

    {t("profile.stats.priority_distribution.title")}

    + {userProfile ? ( + + {userProfile.priority_distribution.length > 0 ? ( + ({ + priority: capitalizeFirstLetter(priority.priority ?? "None"), + value: priority.priority_count, + }))} + height="300px" + indexBy="priority" + keys={["value"]} + borderRadius={4} + padding={0.7} + customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)} + tooltip={(datum) => ( +
    + + {datum.data.priority}: + {datum.value} +
    + )} + colors={(datum) => { + if (datum.data.priority === "Urgent") return "#991b1b"; + else if (datum.data.priority === "High") return "#ef4444"; + else if (datum.data.priority === "Medium") return "#f59e0b"; + else if (datum.data.priority === "Low") return "#16a34a"; + else return "#e5e5e5"; + }} + theme={{ + axis: { + domain: { + line: { + stroke: "transparent", + }, + }, + }, + grid: { line: { stroke: "transparent", }, }, - }, - grid: { - line: { - stroke: "transparent", - }, - }, - }} - /> - ) : ( -
    - -
    - )} -
    - ) : ( -
    - - - - - - - -
    - )} -
    -); + ) : ( +
    + +
    + )} +
    + ) : ( +
    + + + + + + + +
    + )} +
    + ); +}; diff --git a/web/core/components/profile/overview/state-distribution.tsx b/web/core/components/profile/overview/state-distribution.tsx index c3533018cb3..ca73fa46592 100644 --- a/web/core/components/profile/overview/state-distribution.tsx +++ b/web/core/components/profile/overview/state-distribution.tsx @@ -1,4 +1,5 @@ // ui +import { useTranslation } from "@plane/i18n"; import { IUserProfileData, IUserStateDistribution } from "@plane/types"; import { Card } from "@plane/ui"; import { ProfileEmptyState, PieGraph } from "@/components/ui"; @@ -15,11 +16,12 @@ type Props = { }; export const ProfileStateDistribution: React.FC = ({ stateDistribution, userProfile }) => { + const { t } = useTranslation(); if (!userProfile) return null; return (
    -

    Issues by state

    +

    {t("profile.stats.state_distribution.title")}

    {userProfile.state_distribution.length > 0 ? (
    @@ -77,8 +79,8 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u
    ) : ( )} diff --git a/web/core/components/profile/overview/stats.tsx b/web/core/components/profile/overview/stats.tsx index 50f6b0ae02f..cf0f15d3e1a 100644 --- a/web/core/components/profile/overview/stats.tsx +++ b/web/core/components/profile/overview/stats.tsx @@ -5,6 +5,7 @@ import { useParams } from "next/navigation"; // ui import { UserCircle2 } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; import { IUserProfileData } from "@plane/types"; import { CreateIcon, LayerStackIcon, Loader, Card, ECardSpacing, ECardDirection } from "@plane/ui"; // types @@ -16,30 +17,32 @@ type Props = { export const ProfileStats: React.FC = ({ userProfile }) => { const { workspaceSlug, userId } = useParams(); + const { t } = useTranslation(); + const overviewCards = [ { icon: CreateIcon, route: "created", - title: "Issues created", + i18n_title: "profile.stats.created", value: userProfile?.created_issues ?? "...", }, { icon: UserCircle2, route: "assigned", - title: "Issues assigned", + i18n_title: "profile.stats.assigned", value: userProfile?.assigned_issues ?? "...", }, { icon: LayerStackIcon, route: "subscribed", - title: "Issues subscribed", + i18n_title: "profile.stats.subscribed", value: userProfile?.subscribed_issues ?? "...", }, ]; return (
    -

    Overview

    +

    {t("profile.stats.overview")}

    {userProfile ? (
    {overviewCards.map((card) => ( @@ -49,7 +52,7 @@ export const ProfileStats: React.FC = ({ userProfile }) => {
    -

    {card.title}

    +

    {t(card.i18n_title)}

    {card.value}

    diff --git a/web/core/components/profile/overview/workload.tsx b/web/core/components/profile/overview/workload.tsx index 94ebc2fe6d8..9f1283cdf90 100644 --- a/web/core/components/profile/overview/workload.tsx +++ b/web/core/components/profile/overview/workload.tsx @@ -1,4 +1,5 @@ // types +import { useTranslation } from "@plane/i18n"; import { IUserStateDistribution } from "@plane/types"; import { Card, ECardDirection, ECardSpacing } from "@plane/ui"; import { STATE_GROUPS } from "@/constants/state"; @@ -8,34 +9,38 @@ type Props = { stateDistribution: IUserStateDistribution[]; }; -export const ProfileWorkload: React.FC = ({ stateDistribution }) => ( -
    -

    Workload

    -
    - {stateDistribution.map((group) => ( -
    - - - - ))} +export const ProfileWorkload: React.FC = ({ stateDistribution }) => { + const { t } = useTranslation(); + + return ( + -); + ); +}; diff --git a/web/core/components/profile/sidebar.tsx b/web/core/components/profile/sidebar.tsx index 11e10184add..d3e007ea370 100644 --- a/web/core/components/profile/sidebar.tsx +++ b/web/core/components/profile/sidebar.tsx @@ -11,6 +11,7 @@ import { Disclosure, Transition } from "@headlessui/react"; // plane helpers import { useOutsideClickDetector } from "@plane/hooks"; // types +import { useTranslation } from "@plane/i18n"; import { IUserProfileProjectSegregation } from "@plane/types"; // plane ui import { Loader, Tooltip } from "@plane/ui"; @@ -42,6 +43,7 @@ export const ProfileSidebar: FC = observer((props) => { const { profileSidebarCollapsed, toggleProfileSidebar } = useAppTheme(); const { getProjectById } = useProject(); const { isMobile } = usePlatformOS(); + const { t } = useTranslation(); // derived values const userData = userProjectsData?.user_data; @@ -55,11 +57,11 @@ export const ProfileSidebar: FC = observer((props) => { const userDetails = [ { - label: "Joined on", + i18n_label: "profile.details.joined_on", value: renderFormattedDate(userData?.date_joined ?? ""), }, { - label: "Timezone", + i18n_label: "profile.details.time_zone", value: , }, ]; @@ -131,8 +133,8 @@ export const ProfileSidebar: FC = observer((props) => {
    {userDetails.map((detail) => ( -
    -
    {detail.label}
    +
    +
    {t(detail.i18n_label)}
    {detail.value}
    ))} @@ -229,28 +231,36 @@ export const ProfileSidebar: FC = observer((props) => {
    Created
    -
    {project.created_issues} Issues
    +
    + {project.created_issues} {t("issues")} +
    Assigned
    -
    {project.assigned_issues} Issues
    +
    + {project.assigned_issues} {t("issues")} +
    Due
    -
    {project.pending_issues} Issues
    +
    + {project.pending_issues} {t("issues")} +
    Completed
    -
    {project.completed_issues} Issues
    +
    + {project.completed_issues} {t("issues")} +
    From 0e110f3fd80f790d9443d35ca3284571bd02d3d3 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:14:17 +0530 Subject: [PATCH 05/93] [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 --- packages/constants/src/issue.ts | 185 ------ packages/constants/src/issue/common.ts | 217 +++++++ packages/constants/src/issue/filter.ts | 530 ++++++++++++++++++ packages/constants/src/issue/index.ts | 3 + packages/constants/src/issue/layout.ts | 76 +++ .../i18n/src/locales/en/translations.json | 153 ++++- .../i18n/src/locales/es/translations.json | 146 ++++- .../i18n/src/locales/fr/translations.json | 141 ++++- .../i18n/src/locales/ja/translations.json | 155 ++++- .../i18n/src/locales/zh-CN/translations.json | 142 ++++- packages/types/src/issues.d.ts | 2 +- .../components/issues/filters/priority.tsx | 10 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 8 +- .../issues/issue-layouts/list/list-group.tsx | 5 +- .../issue-layouts/properties/priority.tsx | 7 +- .../issues/navbar/layout-selection.tsx | 6 +- .../issues/peek-overview/issue-properties.tsx | 5 +- space/package.json | 1 + .../profile/[userId]/mobile-header.tsx | 35 +- .../[projectId]/cycles/(detail)/header.tsx | 22 +- .../cycles/(detail)/mobile-header.tsx | 46 +- .../[projectId]/draft-issues/header.tsx | 20 +- .../issues/(list)/mobile-header.tsx | 47 +- .../[projectId]/modules/(detail)/header.tsx | 8 +- .../modules/(detail)/mobile-header.tsx | 34 +- .../views/(detail)/[viewId]/header.tsx | 7 +- .../(projects)/workspace-views/header.tsx | 8 +- .../issues/worklog/activity/filter-root.tsx | 4 +- web/ce/constants/index.ts | 1 - web/ce/constants/issues.ts | 38 -- .../issue/issue-details/activity.store.ts | 3 +- .../actions/issue-actions/change-priority.tsx | 3 +- web/core/components/dropdowns/layout.tsx | 19 +- web/core/components/dropdowns/priority.tsx | 26 +- .../components/graphs/issues-by-priority.tsx | 4 +- .../inbox-filter/applied-filters/priority.tsx | 7 +- .../inbox/inbox-filter/filters/priority.tsx | 10 +- .../issues/archived-issues-header.tsx | 18 +- web/core/components/issues/filters.tsx | 18 +- .../issue-activity/activity-comment-root.tsx | 4 +- .../issue-activity/activity-filter.tsx | 10 +- .../issue-detail/issue-activity/root.tsx | 37 +- .../display-filters/display-properties.tsx | 12 +- .../header/display-filters/extra-options.tsx | 23 +- .../header/display-filters/group-by.tsx | 12 +- .../header/display-filters/issue-grouping.tsx | 23 +- .../header/display-filters/order-by.tsx | 11 +- .../header/display-filters/sub-group-by.tsx | 9 +- .../header/filters/filters-selection.tsx | 8 +- .../filters/header/filters/priority.tsx | 16 +- .../filters/header/layout-selection.tsx | 14 +- .../issue-layouts/group-drag-overlay.tsx | 15 +- .../components/issues/issue-layouts/index.ts | 2 +- .../issues/issue-layouts/kanban/default.tsx | 4 + .../issue-layouts/kanban/kanban-group.tsx | 12 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 8 +- .../issues/issue-layouts/layout-icon.tsx | 19 + .../issues/issue-layouts/list/list-group.tsx | 16 +- .../roots/all-issue-layout-root.tsx | 16 +- .../components/issues/issue-layouts/utils.tsx | 8 +- .../profile/profile-issues-filter.tsx | 16 +- .../project/project-feature-update.tsx | 2 +- web/core/components/views/form.tsx | 38 +- web/core/components/workspace/views/form.tsx | 38 +- web/core/constants/dashboard.ts | 10 +- web/core/constants/issue.ts | 527 ----------------- web/core/store/base-command-palette.store.ts | 3 +- .../store/issue/helpers/base-issues.store.ts | 4 +- .../helpers/issue-filter-helper.store.ts | 3 +- .../store/issue/issue_kanban_view.store.ts | 2 +- web/ee/constants/issues.ts | 1 - web/helpers/issue.helper.ts | 6 +- 72 files changed, 2027 insertions(+), 1072 deletions(-) delete mode 100644 packages/constants/src/issue.ts create mode 100644 packages/constants/src/issue/common.ts create mode 100644 packages/constants/src/issue/filter.ts create mode 100644 packages/constants/src/issue/index.ts create mode 100644 packages/constants/src/issue/layout.ts delete mode 100644 web/ce/constants/issues.ts create mode 100644 web/core/components/issues/issue-layouts/layout-icon.tsx delete mode 100644 web/core/constants/issue.ts delete mode 100644 web/ee/constants/issues.ts diff --git a/packages/constants/src/issue.ts b/packages/constants/src/issue.ts deleted file mode 100644 index 19cfe60f3a6..00000000000 --- a/packages/constants/src/issue.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { List, Kanban } from "lucide-react"; - -export const ALL_ISSUES = "All Issues"; - -export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; - -export type TIssueFilterKeys = "priority" | "state" | "labels"; - -export type TIssueLayout = - | "list" - | "kanban" - | "calendar" - | "spreadsheet" - | "gantt"; - -export type TIssueFilterPriorityObject = { - key: TIssuePriorities; - title: string; - className: string; - icon: string; -}; - -export enum EIssueGroupByToServerOptions { - "state" = "state_id", - "priority" = "priority", - "labels" = "labels__id", - "state_detail.group" = "state__group", - "assignees" = "assignees__id", - "cycle" = "cycle_id", - "module" = "issue_module__module_id", - "target_date" = "target_date", - "project" = "project_id", - "created_by" = "created_by", - "team_project" = "project_id", -} - -export enum EIssueGroupBYServerToProperty { - "state_id" = "state_id", - "priority" = "priority", - "labels__id" = "label_ids", - "state__group" = "state__group", - "assignees__id" = "assignee_ids", - "cycle_id" = "cycle_id", - "issue_module__module_id" = "module_ids", - "target_date" = "target_date", - "project_id" = "project_id", - "created_by" = "created_by", -} - -export enum EServerGroupByToFilterOptions { - "state_id" = "state", - "priority" = "priority", - "labels__id" = "labels", - "state__group" = "state_group", - "assignees__id" = "assignees", - "cycle_id" = "cycle", - "issue_module__module_id" = "module", - "target_date" = "target_date", - "project_id" = "project", - "created_by" = "created_by", -} - -export enum EIssueServiceType { - ISSUES = "issues", - EPICS = "epics", -} - -export enum EIssueLayoutTypes { - LIST = "list", - KANBAN = "kanban", - CALENDAR = "calendar", - GANTT = "gantt_chart", - SPREADSHEET = "spreadsheet", -} - -export enum EIssuesStoreType { - GLOBAL = "GLOBAL", - PROFILE = "PROFILE", - TEAM = "TEAM", - PROJECT = "PROJECT", - CYCLE = "CYCLE", - MODULE = "MODULE", - TEAM_VIEW = "TEAM_VIEW", - PROJECT_VIEW = "PROJECT_VIEW", - ARCHIVED = "ARCHIVED", - DRAFT = "DRAFT", - DEFAULT = "DEFAULT", - WORKSPACE_DRAFT = "WORKSPACE_DRAFT", - EPIC = "EPIC", -} - -export enum EIssueFilterType { - FILTERS = "filters", - DISPLAY_FILTERS = "display_filters", - DISPLAY_PROPERTIES = "display_properties", - KANBAN_FILTERS = "kanban_filters", -} - -export enum EIssueCommentAccessSpecifier { - EXTERNAL = "EXTERNAL", - INTERNAL = "INTERNAL", -} - -export enum EIssueListRow { - HEADER = "HEADER", - ISSUE = "ISSUE", - NO_ISSUES = "NO_ISSUES", - QUICK_ADD = "QUICK_ADD", -} - -export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { - [key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>; -} = { - list: { - filters: ["priority", "state", "labels"], - }, - kanban: { - filters: ["priority", "state", "labels"], - }, - calendar: { - filters: ["priority", "state", "labels"], - }, - spreadsheet: { - filters: ["priority", "state", "labels"], - }, - gantt: { - filters: ["priority", "state", "labels"], - }, -}; - -export const ISSUE_PRIORITIES: { - key: TIssuePriorities; - title: string; -}[] = [ - { key: "urgent", title: "Urgent" }, - { key: "high", title: "High" }, - { key: "medium", title: "Medium" }, - { key: "low", title: "Low" }, - { key: "none", title: "None" }, -]; - -export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [ - { - key: "urgent", - title: "Urgent", - className: "bg-red-500 border-red-500 text-white", - icon: "error", - }, - { - key: "high", - title: "High", - className: "text-orange-500 border-custom-border-300", - icon: "signal_cellular_alt", - }, - { - key: "medium", - title: "Medium", - className: "text-yellow-500 border-custom-border-300", - icon: "signal_cellular_alt_2_bar", - }, - { - key: "low", - title: "Low", - className: "text-green-500 border-custom-border-300", - icon: "signal_cellular_alt_1_bar", - }, - { - key: "none", - title: "None", - className: "text-gray-500 border-custom-border-300", - icon: "block", - }, -]; - -export const SITES_ISSUE_LAYOUTS: { - key: TIssueLayout; - title: string; - icon: any; -}[] = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, - // { key: "calendar", title: "Calendar", icon: Calendar }, - // { key: "spreadsheet", title: "Spreadsheet", icon: Sheet }, - // { key: "gantt", title: "Gantt chart", icon: GanttChartSquare }, -]; diff --git a/packages/constants/src/issue/common.ts b/packages/constants/src/issue/common.ts new file mode 100644 index 00000000000..a63dd110d39 --- /dev/null +++ b/packages/constants/src/issue/common.ts @@ -0,0 +1,217 @@ +import { + TIssueGroupByOptions, + TIssueOrderByOptions, + IIssueDisplayProperties, +} from "@plane/types"; + +export const ALL_ISSUES = "All Issues"; + +export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; + +export type TIssueFilterPriorityObject = { + key: TIssuePriorities; + titleTranslationKey: string; + className: string; + icon: string; +}; + +export enum EIssueGroupByToServerOptions { + "state" = "state_id", + "priority" = "priority", + "labels" = "labels__id", + "state_detail.group" = "state__group", + "assignees" = "assignees__id", + "cycle" = "cycle_id", + "module" = "issue_module__module_id", + "target_date" = "target_date", + "project" = "project_id", + "created_by" = "created_by", + "team_project" = "project_id", +} + +export enum EIssueGroupBYServerToProperty { + "state_id" = "state_id", + "priority" = "priority", + "labels__id" = "label_ids", + "state__group" = "state__group", + "assignees__id" = "assignee_ids", + "cycle_id" = "cycle_id", + "issue_module__module_id" = "module_ids", + "target_date" = "target_date", + "project_id" = "project_id", + "created_by" = "created_by", +} + +export enum EIssueServiceType { + ISSUES = "issues", + EPICS = "epics", +} + +export enum EIssuesStoreType { + GLOBAL = "GLOBAL", + PROFILE = "PROFILE", + TEAM = "TEAM", + PROJECT = "PROJECT", + CYCLE = "CYCLE", + MODULE = "MODULE", + TEAM_VIEW = "TEAM_VIEW", + PROJECT_VIEW = "PROJECT_VIEW", + ARCHIVED = "ARCHIVED", + DRAFT = "DRAFT", + DEFAULT = "DEFAULT", + WORKSPACE_DRAFT = "WORKSPACE_DRAFT", + EPIC = "EPIC", +} + +export enum EIssueCommentAccessSpecifier { + EXTERNAL = "EXTERNAL", + INTERNAL = "INTERNAL", +} + +export enum EIssueListRow { + HEADER = "HEADER", + ISSUE = "ISSUE", + NO_ISSUES = "NO_ISSUES", + QUICK_ADD = "QUICK_ADD", +} + +export const ISSUE_PRIORITIES: { + key: TIssuePriorities; + title: string; +}[] = [ + { + key: "urgent", + title: "Urgent", + }, + { + key: "high", + title: "High", + }, + { + key: "medium", + title: "Medium", + }, + { + key: "low", + title: "Low", + }, + { + key: "none", + title: "None", + }, +]; + +export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [ + "state", + "priority", + "assignees", + "labels", + "module", + "cycle", +]; + +export type TCreateModalStoreTypes = + | EIssuesStoreType.TEAM + | EIssuesStoreType.PROJECT + | EIssuesStoreType.TEAM_VIEW + | EIssuesStoreType.PROJECT_VIEW + | EIssuesStoreType.PROFILE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.MODULE + | EIssuesStoreType.EPIC; + +export const ISSUE_GROUP_BY_OPTIONS: { + key: TIssueGroupByOptions; + titleTranslationKey: string; +}[] = [ + { key: "state", titleTranslationKey: "common.states" }, + { key: "state_detail.group", titleTranslationKey: "common.state_groups" }, + { key: "priority", titleTranslationKey: "common.priority" }, + { key: "team_project", titleTranslationKey: "common.team_project" }, // required this on team issues + { key: "project", titleTranslationKey: "common.project" }, // required this on my issues + { key: "cycle", titleTranslationKey: "common.cycle" }, // required this on my issues + { key: "module", titleTranslationKey: "common.module" }, // required this on my issues + { key: "labels", titleTranslationKey: "common.labels" }, + { key: "assignees", titleTranslationKey: "common.assignees" }, + { key: "created_by", titleTranslationKey: "common.created_by" }, + { key: null, titleTranslationKey: "common.none" }, +]; + +export const ISSUE_ORDER_BY_OPTIONS: { + key: TIssueOrderByOptions; + titleTranslationKey: string; +}[] = [ + { key: "sort_order", titleTranslationKey: "common.order_by.manual" }, + { key: "-created_at", titleTranslationKey: "common.order_by.last_created" }, + { key: "-updated_at", titleTranslationKey: "common.order_by.last_updated" }, + { key: "start_date", titleTranslationKey: "common.order_by.start_date" }, + { key: "target_date", titleTranslationKey: "common.order_by.due_date" }, + { key: "-priority", titleTranslationKey: "common.priority" }, +]; + +export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = + [ + "assignee", + "start_date", + "due_date", + "labels", + "key", + "priority", + "state", + "sub_issue_count", + "link", + "attachment_count", + "estimate", + "created_on", + "updated_on", + "modules", + "cycle", + "issue_type", + ]; + +export const ISSUE_DISPLAY_PROPERTIES: { + key: keyof IIssueDisplayProperties; + titleTranslationKey: string; +}[] = [ + { + key: "key", + titleTranslationKey: "issue.display.properties.id", + }, + { + key: "issue_type", + titleTranslationKey: "issue.display.properties.issue_type", + }, + { + key: "assignee", + titleTranslationKey: "common.assignee", + }, + { + key: "start_date", + titleTranslationKey: "common.order_by.start_date", + }, + { + key: "due_date", + titleTranslationKey: "common.order_by.due_date", + }, + { key: "labels", titleTranslationKey: "common.labels" }, + { + key: "priority", + titleTranslationKey: "common.priority", + }, + { key: "state", titleTranslationKey: "common.state" }, + { + key: "sub_issue_count", + titleTranslationKey: "issue.display.properties.sub_issue_count", + }, + { + key: "attachment_count", + titleTranslationKey: "issue.display.properties.attachment_count", + }, + { key: "link", titleTranslationKey: "common.link" }, + { + key: "estimate", + titleTranslationKey: "common.estimate", + }, + { key: "modules", titleTranslationKey: "common.module" }, + { key: "cycle", titleTranslationKey: "common.cycle" }, +]; diff --git a/packages/constants/src/issue/filter.ts b/packages/constants/src/issue/filter.ts new file mode 100644 index 00000000000..5d1d694b722 --- /dev/null +++ b/packages/constants/src/issue/filter.ts @@ -0,0 +1,530 @@ +import { + ILayoutDisplayFiltersOptions, + TIssueActivityComment, +} from "@plane/types"; +import { + TIssueFilterPriorityObject, + ISSUE_DISPLAY_PROPERTIES_KEYS, + EIssuesStoreType, +} from "./common"; + +import { TIssueLayout } from "./layout"; + +export type TIssueFilterKeys = "priority" | "state" | "labels"; + +export enum EServerGroupByToFilterOptions { + "state_id" = "state", + "priority" = "priority", + "labels__id" = "labels", + "state__group" = "state_group", + "assignees__id" = "assignees", + "cycle_id" = "cycle", + "issue_module__module_id" = "module", + "target_date" = "target_date", + "project_id" = "project", + "created_by" = "created_by", +} + +export enum EIssueFilterType { + FILTERS = "filters", + DISPLAY_FILTERS = "display_filters", + DISPLAY_PROPERTIES = "display_properties", + KANBAN_FILTERS = "kanban_filters", +} + +export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { + [key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>; +} = { + list: { + filters: ["priority", "state", "labels"], + }, + kanban: { + filters: ["priority", "state", "labels"], + }, + calendar: { + filters: ["priority", "state", "labels"], + }, + spreadsheet: { + filters: ["priority", "state", "labels"], + }, + gantt: { + filters: ["priority", "state", "labels"], + }, +}; + +export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [ + { + key: "urgent", + titleTranslationKey: "issue.priority.urgent", + className: "bg-red-500 border-red-500 text-white", + icon: "error", + }, + { + key: "high", + titleTranslationKey: "issue.priority.high", + className: "text-orange-500 border-custom-border-300", + icon: "signal_cellular_alt", + }, + { + key: "medium", + titleTranslationKey: "issue.priority.medium", + className: "text-yellow-500 border-custom-border-300", + icon: "signal_cellular_alt_2_bar", + }, + { + key: "low", + titleTranslationKey: "issue.priority.low", + className: "text-green-500 border-custom-border-300", + icon: "signal_cellular_alt_1_bar", + }, + { + key: "none", + titleTranslationKey: "common.none", + className: "text-gray-500 border-custom-border-300", + icon: "block", + }, +]; + +export type TFiltersByLayout = { + [layoutType: string]: ILayoutDisplayFiltersOptions; +}; + +export type TIssueFiltersToDisplayByPageType = { + [pageType: string]: TFiltersByLayout; +}; + +export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = { + profile_issues: { + list: { + filters: [ + "priority", + "state_group", + "labels", + "start_date", + "target_date", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: ["state_detail.group", "priority", "project", "labels", null], + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups", "sub_issue"], + }, + }, + kanban: { + filters: [ + "priority", + "state_group", + "labels", + "start_date", + "target_date", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: ["state_detail.group", "priority", "project", "labels"], + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + }, + archived_issues: { + list: { + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "created_by", + "labels", + "start_date", + "target_date", + "issue_type", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: [ + "state", + "cycle", + "module", + "state_detail.group", + "priority", + "labels", + "assignees", + "created_by", + null, + ], + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + }, + draft_issues: { + list: { + filters: [ + "priority", + "state_group", + "cycle", + "module", + "labels", + "start_date", + "target_date", + "issue_type", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: [ + "state_detail.group", + "cycle", + "module", + "priority", + "project", + "labels", + null, + ], + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + kanban: { + filters: [ + "priority", + "state_group", + "cycle", + "module", + "labels", + "start_date", + "target_date", + "issue_type", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: [ + "state_detail.group", + "cycle", + "module", + "priority", + "project", + "labels", + ], + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + }, + my_issues: { + spreadsheet: { + filters: [ + "priority", + "state_group", + "labels", + "assignees", + "created_by", + "subscriber", + "project", + "start_date", + "target_date", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + order_by: [], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, + list: { + filters: [ + "priority", + "state_group", + "labels", + "assignees", + "created_by", + "subscriber", + "project", + "start_date", + "target_date", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + type: [null, "active", "backlog"], + }, + extra_options: { + access: false, + values: [], + }, + }, + }, + issues: { + list: { + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + "issue_type", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: [ + "state", + "priority", + "cycle", + "module", + "labels", + "assignees", + "created_by", + null, + ], + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups", "sub_issue"], + }, + }, + kanban: { + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + "issue_type", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: [ + "state", + "priority", + "cycle", + "module", + "labels", + "assignees", + "created_by", + ], + sub_group_by: [ + "state", + "priority", + "cycle", + "module", + "labels", + "assignees", + "created_by", + null, + ], + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + "target_date", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups", "sub_issue"], + }, + }, + calendar: { + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "issue_type", + ], + display_properties: ["key", "issue_type"], + display_filters: { + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, + spreadsheet: { + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + "issue_type", + ], + display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, + display_filters: { + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, + gantt_chart: { + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + "issue_type", + ], + display_properties: ["key", "issue_type"], + display_filters: { + order_by: [ + "sort_order", + "-created_at", + "-updated_at", + "start_date", + "-priority", + ], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, + }, +}; + +export const ISSUE_STORE_TO_FILTERS_MAP: Partial< + Record +> = { + [EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_PAGE.issues, +}; + +export enum EActivityFilterType { + ACTIVITY = "ACTIVITY", + COMMENT = "COMMENT", +} + +export type TActivityFilters = EActivityFilterType; + +export const ACTIVITY_FILTER_TYPE_OPTIONS: Record< + TActivityFilters, + { labelTranslationKey: string } +> = { + [EActivityFilterType.ACTIVITY]: { + labelTranslationKey: "common.updates", + }, + [EActivityFilterType.COMMENT]: { + labelTranslationKey: "common.comments", + }, +}; + +export type TActivityFilterOption = { + key: TActivityFilters; + labelTranslationKey: string; + isSelected: boolean; + onClick: () => void; +}; + +export const defaultActivityFilters: TActivityFilters[] = [ + EActivityFilterType.ACTIVITY, + EActivityFilterType.COMMENT, +]; + +export const filterActivityOnSelectedFilters = ( + activity: TIssueActivityComment[], + filters: TActivityFilters[] +): TIssueActivityComment[] => + activity.filter((activity) => + filters.includes(activity.activity_type as TActivityFilters) + ); + +export const ENABLE_ISSUE_DEPENDENCIES = false; diff --git a/packages/constants/src/issue/index.ts b/packages/constants/src/issue/index.ts new file mode 100644 index 00000000000..63320322340 --- /dev/null +++ b/packages/constants/src/issue/index.ts @@ -0,0 +1,3 @@ +export * from "./common"; +export * from "./filter"; +export * from "./layout"; diff --git a/packages/constants/src/issue/layout.ts b/packages/constants/src/issue/layout.ts new file mode 100644 index 00000000000..6dd62fd177d --- /dev/null +++ b/packages/constants/src/issue/layout.ts @@ -0,0 +1,76 @@ +export type TIssueLayout = + | "list" + | "kanban" + | "calendar" + | "spreadsheet" + | "gantt"; + +export enum EIssueLayoutTypes { + LIST = "list", + KANBAN = "kanban", + CALENDAR = "calendar", + GANTT = "gantt_chart", + SPREADSHEET = "spreadsheet", +} + +export type TIssueLayoutMap = Record< + EIssueLayoutTypes, + { + key: EIssueLayoutTypes; + i18n_title: string; + i18n_label: string; + } +>; + +export const SITES_ISSUE_LAYOUTS: { + key: TIssueLayout; + titleTranslationKey: string; + icon: any; +}[] = [ + { + key: "list", + icon: "List", + titleTranslationKey: "issue.layouts.list", + }, + { + key: "kanban", + icon: "Kanban", + titleTranslationKey: "issue.layouts.kanban", + }, + // { key: "calendar", title: "Calendar", icon: Calendar }, + // { key: "spreadsheet", title: "Spreadsheet", icon: Sheet }, + // { key: "gantt", title: "Gantt chart", icon: GanttChartSquare }, +]; + +export const ISSUE_LAYOUT_MAP: TIssueLayoutMap = { + [EIssueLayoutTypes.LIST]: { + key: EIssueLayoutTypes.LIST, + i18n_title: "issue.layouts.title.list", + i18n_label: "issue.layouts.list", + }, + [EIssueLayoutTypes.KANBAN]: { + key: EIssueLayoutTypes.KANBAN, + i18n_title: "issue.layouts.title.kanban", + i18n_label: "issue.layouts.kanban", + }, + [EIssueLayoutTypes.CALENDAR]: { + key: EIssueLayoutTypes.CALENDAR, + i18n_title: "issue.layouts.title.calendar", + i18n_label: "issue.layouts.calendar", + }, + [EIssueLayoutTypes.SPREADSHEET]: { + key: EIssueLayoutTypes.SPREADSHEET, + i18n_title: "issue.layouts.title.spreadsheet", + i18n_label: "issue.layouts.spreadsheet", + }, + [EIssueLayoutTypes.GANTT]: { + key: EIssueLayoutTypes.GANTT, + i18n_title: "issue.layouts.title.gantt", + i18n_label: "issue.layouts.gantt", + }, +}; + +export const ISSUE_LAYOUTS: { + key: EIssueLayoutTypes; + i18n_title: string; +}[] = Object.values(ISSUE_LAYOUT_MAP); diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 276384a0b4b..e115a99ec67 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -220,7 +220,6 @@ "join_the_project_to_rearrange": "Join the project to rearrange", "drag_to_rearrange": "Drag to rearrange", "congrats": "Congrats!", - "project": "Project", "open_project": "Open project", "issues": "Issues", "cycles": "Cycles", @@ -314,9 +313,153 @@ "remove_parent_issue": "Remove parent issue", "add_parent": "Add parent", "loading_members": "Loading members...", - "no_data_yet": "No Data yet", + "no_data_yet": "No Data yet", "connections": "Connections", + "common": { + "all": "All", + "states": "States", + "state": "State", + "state_groups": "State groups", + "priority": "Priority", + "team_project": "Team project", + "project": "Project", + "cycle": "Cycle", + "cycles": "Cycles", + "module": "Module", + "modules": "Modules", + "labels": "Labels", + "assignees": "Assignees", + "assignee": "Assignee", + "created_by": "Created by", + "none": "None", + "link": "Link", + "estimate": "Estimate", + "layout": "Layout", + "filters": "Filters", + "display": "Display", + "load_more": "Load more", + "no_matches_found": "No matches found", + "activity": "Activity", + "analytics": "Analytics", + "success": "Success", + "error": "Error", + "group_by": "Group by", + "search": "Search", + "epic": "Epic", + "issue": "Issue", + "warning": "Warning", + "updating": "Updating", + "update": "Update", + "creating": "Creating", + "cancel": "Cancel", + "description": "Description", + "title": "Title", + "order_by": { + "label": "Order by", + "manual": "Manual", + "last_created": "Last created", + "last_updated": "Last updated", + "start_date": "Start date", + "due_date": "Due date" + }, + "comments": "Comments", + "updates": "Updates" + }, + + "form": { + "title": { + "required": "Title is required", + "max_length": "Title should be less than {length} characters" + } + }, + + "entity": { + "grouping_title": "{entity} Grouping", + "priority": "{entity} ", + "all": "All {entity}", + "drop_here_to_move": "Drop here to move the {entity}" + }, + + "epic": { + "all": "All Epics", + "label": "{count, plural, one {Epic} other {Epics}}" + }, + + "issue": { + "label": "{count, plural, one {Issue} other {Issues}}", + "all": "All Issues", + "add": "Add Issue", + "priority": { + "urgent": "Urgent", + "high": "High", + "medium": "Medium", + "low": "Low" + }, + "display": { + "properties": { + "label": "Display Properties", + "id": "ID", + "issue_type": "Issue Type", + "sub_issue_count": "Sub issue count", + "attachment_count": "Attachment count" + }, + "extra": { + "show_sub_issues": "Show sub-issues", + "show_empty_groups": "Show empty groups" + } + }, + "layouts": { + "ordered_by_label": "This layout is ordered by", + "list": "List", + "kanban": "Board", + "calendar": "Calendar", + "spreadsheet": "Table", + "gantt": "Timeline", + "title": { + "list": "List Layout", + "kanban": "Board Layout", + "calendar": "Calendar Layout", + "spreadsheet": "Table Layout", + "gantt": "Timeline Layout" + } + }, + "states": { + "active": "Active", + "backlog": "Backlog" + }, + "comments": { + "create": { + "success": "Comment created successfully", + "error": "Comment creation failed. Please try again later." + }, + "update": { + "success": "Comment updated successfully", + "error": "Comment update failed. Please try again later." + }, + "remove": { + "success": "Comment removed successfully", + "error": "Comment remove failed. Please try again later." + }, + "upload": { + "error": "Asset upload failed. Please try again later." + } + } + }, + + "project": { + "label": "{count, plural, one {Project} other {Projects}}" + }, + + "view": { + "create": { + "label": "Create View" + }, + "update": { + "label": "Update View" + } + }, + "workspace_dashboard": { "empty_state": { "general": { @@ -441,9 +584,9 @@ "page_label": "Your work", "work": "Work", "details": { - "joined_on": "Joined on", - "time_zone": "Timezone" - }, + "joined_on": "Joined on", + "time_zone": "Timezone" + }, "stats": { "workload": "Workload", "overview": "Overview", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 5fe8916cb5c..557abd6bb51 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -219,7 +219,6 @@ "join_the_project_to_rearrange": "Únete al proyecto para reordenar", "drag_to_rearrange": "Arrastra para reordenar", "congrats": "¡Felicitaciones!", - "project": "Proyecto", "open_project": "Abrir proyecto", "issues": "Problemas", "cycles": "Ciclos", @@ -313,10 +312,153 @@ "remove_parent_issue": "Eliminar problema padre", "add_parent": "Agregar padre", "loading_members": "Cargando miembros...", - "inbox": "bandeja de entrada", "no_data_yet": "Sin datos aún", "connections": "Conexiones", + "common": { + "all": "Todos", + "states": "Estados", + "state": "Estado", + "state_groups": "Grupos de estado", + "priority": "Prioridad", + "team_project": "Proyecto de equipo", + "project": "Proyecto", + "cycle": "Ciclo", + "cycles": "Ciclos", + "module": "Módulo", + "modules": "Módulos", + "labels": "Etiquetas", + "assignees": "Asignados", + "assignee": "Asignado", + "created_by": "Creado por", + "none": "Ninguno", + "link": "Enlace", + "estimate": "Estimación", + "layout": "Diseño", + "filters": "Filtros", + "display": "Mostrar", + "load_more": "Cargar más", + "no_matches_found": "No se encontraron coincidencias", + "activity": "Actividad", + "analytics": "Analítica", + "success": "Éxito", + "error": "Error", + "group_by": "Agrupar por", + "search": "Buscar", + "epic": "Épica", + "issue": "Problema", + "warning": "Advertencia", + "updating": "Actualizando", + "update": "Actualizar", + "creating": "Creando", + "cancel": "Cancelar", + "description": "Descripción", + "title": "Título", + "order_by": { + "label": "Ordenar por", + "manual": "Manual", + "last_created": "Último creado", + "last_updated": "Última actualización", + "start_date": "Fecha de inicio", + "due_date": "Fecha de vencimiento" + }, + "comments": "Comentarios", + "updates": "Actualizaciones" + }, + + "form": { + "title": { + "required": "El título es obligatorio", + "max_length": "El título debe tener menos de {length} caracteres" + } + }, + + "entity": { + "grouping_title": "Agrupación de {entity}", + "priority": "{entity}", + "all": "Todos los {entity}", + "drop_here_to_move": "Suelte aquí para mover el {entity}" + }, + + "epic": { + "all": "Todas las épicas", + "label": "{count, plural, one {Épica} other {Épicas}}" + }, + + "issue": { + "label": "{count, plural, one {Problema} other {Problemas}}", + "all": "Todos los problemas", + "add": "Agregar problema", + "priority": { + "urgent": "Urgente", + "high": "Alta", + "medium": "Media", + "low": "Baja" + }, + "display": { + "properties": { + "label": "Propiedades de visualización", + "id": "ID", + "issue_type": "Tipo de problema", + "sub_issue_count": "Cantidad de subproblemas", + "attachment_count": "Cantidad de archivos adjuntos" + }, + "extra": { + "show_sub_issues": "Mostrar subproblemas", + "show_empty_groups": "Mostrar grupos vacíos" + } + }, + "layouts": { + "ordered_by_label": "Este diseño está ordenado por", + "list": "Lista", + "kanban": "Tablero", + "calendar": "Calendario", + "spreadsheet": "Hoja de cálculo", + "gantt": "Línea de tiempo", + "title": { + "list": "Diseño de lista", + "kanban": "Diseño de tablero", + "calendar": "Diseño de calendario", + "spreadsheet": "Diseño de hoja de cálculo", + "gantt": "Diseño de línea de tiempo" + } + }, + "states": { + "active": "Activo", + "backlog": "Pendientes" + }, + "comments": { + "create": { + "success": "Comentario creado con éxito", + "error": "Error al crear el comentario. Por favor, inténtalo de nuevo más tarde." + }, + "update": { + "success": "Comentario actualizado con éxito", + "error": "Error al actualizar el comentario. Por favor, inténtalo más tarde." + }, + "remove": { + "success": "Comentario eliminado con éxito", + "error": "Error al eliminar el comentario. Por favor, inténtalo de nuevo más tarde." + }, + "upload": { + "error": "Error al subir el archivo. Por favor, inténtalo de nuevo más tarde." + } + } + }, + + "project": { + "label": "{count, plural, one {Proyecto} other {Proyectos}}" + }, + + "view": { + "create": { + "label": "Crear vista" + }, + "update": { + "label": "Actualizar vista" + } + }, + "workspace_dashboard": { "empty_state": { "general": { diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index e4490819d4a..4a5594a81d9 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -314,10 +314,149 @@ "remove_parent_issue": "Supprimer le problème parent", "add_parent": "Ajouter un parent", "loading_members": "Chargement des membres...", - "inbox": "Boîte de réception", "no_data_yet": "Pas encore de données", "connections": "Connexions", + "common": { + "all": "Tout", + "states": "États", + "state": "État", + "state_groups": "Groupes d'états", + "priority": "Priorité", + "team_project": "Projet d'équipe", + "project": "Projet", + "cycle": "Cycle", + "cycles": "Cycles", + "module": "Module", + "modules": "Modules", + "labels": "Étiquettes", + "assignees": "Assignés", + "assignee": "Assigné", + "created_by": "Créé par", + "none": "Aucun", + "link": "Lien", + "estimate": "Estimation", + "layout": "Disposition", + "filters": "Filtres", + "display": "Affichage", + "load_more": "Charger plus", + "no_matches_found": "Aucun résultat trouvé", + "activity": "Activité", + "analytics": "Analyses", + "success": "Succès", + "error": "Erreur", + "group_by": "Grouper par", + "search": "Rechercher", + "epic": "Épopée", + "issue": "Problème", + "warning": "Avertissement", + "updating": "Mise à jour", + "update": "Mettre à jour", + "creating": "Création", + "cancel": "Annuler", + "description": "Description", + "title": "Titre", + "order_by": { + "label": "Trier par", + "manual": "Manuel", + "last_created": "Dernière création", + "last_updated": "Dernière mise à jour", + "start_date": "Date de début", + "due_date": "Date d'échéance" + }, + "comments": "Commentaires", + "updates": "Mises à jour" + }, + + "form": { + "title": { + "required": "Le titre est requis", + "max_length": "Le titre ne doit pas dépasser {length} caractères" + } + }, + + "entity": { + "grouping_title": "Groupement de {entity}", + "priority": "{entity}", + "all": "Tous les {entity}", + "drop_here_to_move": "Déposer ici pour déplacer {entity}" + }, + + "epic": { + "all": "Toutes les épopées", + "label": "{count, plural, one {Épopée} other {Épopées}}" + }, + + "issue": { + "label": "{count, plural, one {Problème} other {Problèmes}}", + "all": "Tous les problèmes", + "add": "Ajouter un problème", + "priority": { + "urgent": "Urgent", + "high": "Haute", + "medium": "Moyenne", + "low": "Basse" + }, + "display": { + "properties": { + "label": "Propriétés d'affichage", + "id": "ID", + "issue_type": "Type de problème", + "sub_issue_count": "Nombre de sous-problèmes", + "attachment_count": "Nombre de pièces jointes" + }, + "extra": { + "show_sub_issues": "Afficher les sous-problèmes", + "show_empty_groups": "Afficher les groupes vides" + } + }, + "layouts": { + "ordered_by_label": "Cette disposition est triée par", + "list": "Liste", + "kanban": "Tableau", + "calendar": "Calendrier", + "spreadsheet": "Feuille de calcul", + "gantt": "Chronologie", + "title": { + "list": "Disposition en liste", + "kanban": "Disposition en tableau", + "calendar": "Disposition en calendrier", + "spreadsheet": "Disposition en feuille de calcul", + "gantt": "Disposition en chronologie" + } + }, + "states": { + "active": "Actif", + "backlog": "Backlog" + }, + "comments": { + "create": { + "success": "Commentaire créé avec succès", + "error": "Échec de la création du commentaire. Veuillez réessayer plus tard." + }, + "update": { + "success": "Commentaire mis à jour avec succès", + "error": "Échec de la mise à jour du commentaire. Veuillez réessayer plus tard." + }, + "remove": { + "success": "Commentaire supprimé avec succès", + "error": "Échec de la suppression du commentaire. Veuillez réessayer plus tard." + }, + "upload": { + "error": "Échec du téléchargement du fichier. Veuillez réessayer plus tard." + } + } + }, + + "view": { + "create": { + "label": "Créer une vue" + }, + "update": { + "label": "Mettre à jour la vue" + } + }, + "workspace_dashboard": { "empty_state": { "general": { diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 361cda4753a..4f53184200a 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -220,7 +220,6 @@ "join_the_project_to_rearrange": "プロジェクトに参加して並べ替え", "drag_to_rearrange": "ドラッグして並べ替え", "congrats": "おめでとうございます!", - "project": "プロジェクト", "open_project": "プロジェクトを開く", "issues": "問題", "cycles": "サイクル", @@ -317,12 +316,156 @@ "no_data_yet": "まだデータがありません", "connections": "接続", + "common": { + "all": "すべて", + "states": "ステータス", + "state": "ステータス", + "state_groups": "ステータスグループ", + "priority": "優先度", + "team_project": "チームプロジェクト", + "project": "プロジェクト", + "cycle": "サイクル", + "cycles": "サイクル", + "module": "モジュール", + "modules": "モジュール", + "labels": "ラベル", + "assignees": "アサイン者", + "assignee": "アサイン者", + "created_by": "作成者", + "none": "なし", + "link": "リンク", + "estimate": "見積もり", + "layout": "レイアウト", + "filters": "フィルター", + "display": "表示", + "load_more": "もっと見る", + "no_matches_found": "一致する結果はありません", + "activity": "アクティビティ", + "analytics": "分析", + "success": "成功", + "error": "エラー", + "group_by": "グループ化", + "search": "検索", + "epic": "エピック", + "issue": "問題", + "warning": "警告", + "updating": "更新中", + "update": "更新", + "creating": "作成中", + "cancel": "キャンセル", + "description": "説明", + "title": "タイトル", + "order_by": { + "label": "オーダー", + "manual": "手動", + "last_created": "最後に作成", + "last_updated": "最後に更新", + "start_date": "開始日", + "due_date": "期限日" + }, + "comments": "コメント", + "updates": "更新" + }, + + "form": { + "title": { + "required": "タイトルは必須です", + "max_length": "タイトルは{length}文字未満である必要があります" + } + }, + + "entity": { + "grouping_title": "{entity}グループ化", + "priority": "", + "all": "すべての{entity}", + "drop_here_to_move": "ここにドロップして{entity}を移動" + }, + + "epic": { + "all": "すべてのエピック", + "label": "{count, plural, one {エピック} other {エピック}}" + }, + + "issue": { + "label": "{count, plural, one {問題} other {問題}}", + "all": "すべての問題", + "add": "問題を追加", + "priority": { + "urgent": "緊急", + "high": "高", + "medium": "中", + "low": "低" + }, + "display": { + "properties": { + "label": "表示プロパティ", + "id": "ID", + "issue_type": "課題タイプ", + "sub_issue_count": "サブ課題数", + "attachment_count": "添付ファイル数" + }, + "extra": { + "show_sub_issues": "サブ課題を表示", + "show_empty_groups": "空のグループを表示" + } + }, + "layouts": { + "ordered_by_label": "このレイアウトは", + "list": "リスト", + "kanban": "ボード", + "calendar": "カレンダー", + "spreadsheet": "表", + "gantt": "ガントチャート", + "title": { + "list": "リストレイアウト", + "kanban": "ボードレイアウト", + "calendar": "カレンダーレイアウト", + "spreadsheet": "テーブルレイアウト", + "gantt": "タイムラインレイアウト" + } + }, + "states": { + "active": "アクティブ", + "backlog": "バックログ" + }, + "comments": { + "create": { + "success": "コメントが正常に作成されました。", + "error": "コメントの作成に失敗しました。後で再試行してください。" + }, + "update": { + "success": "コメントが正常に更新されました。", + "error": "コメントの更新に失敗しました。後で再試行してください。" + }, + "remove": { + "success": "コメントが正常に削除されました。", + "error": "コメントの削除に失敗しました。後で再試行してください。" + }, + "upload": { + "error": "コメントのアセットのアップロードに失敗しました。後で再試行してください。" + } + } + }, + + "project": { + "label": "{count, plural, one {プロジェクト} other {プロジェクト}}" + }, + + "view": { + "create": { + "label": "ビューを作成" + }, + "update": { + "label": "ビューを更新" + } + }, + "workspace_dashboard": { - "empty_state": { - "general": { - "title": "プロジェクト、アクティビティ、指標の概要", - "description": "Planeへようこそ!私たちはあなたを迎えることができて嬉しいです。最初のプロジェクトを作成してタスクを追跡し、このページが進捗を助けるスペースに変わります。管理者は、チームを前進させるための要素も見ることができます。", - "primary_button": { + "empty_state": { + "general": { + "title": "プロジェクト、アクティビティ、指標の概要", + "description": "Planeへようこそ!私たちはあなたを迎えることができて嬉しいです。最初のプロジェクトを作成してタスクを追跡し、このページが進捗を助けるスペースに変わります。管理者は、チームを前進させるための要素も見ることができます。", + "primary_button": { "text": "最初のプロジェクトを作成", "comic": { "title": "すべてはPlaneでのプロジェクトから始まります", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 0da2835d89f..12c85b57f7b 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -222,7 +222,6 @@ "join_the_project_to_rearrange": "加入项目以重新排序", "drag_to_rearrange": "拖动以重新排序", "congrats": "恭喜!", - "project": "项目", "open_project": "打开项目", "issues": "问题", "cycles": "周期", @@ -316,7 +315,146 @@ "add_parent": "添加父问题", "loading_members": "正在加载成员...", "inbox": "收件箱", - "no_data_yet": "暂无数据", + "common": { + "all": "全部", + "states": "状态", + "state": "状态", + "state_groups": "状态组", + "priority": "优先级", + "team_project": "团队项目", + "project": "项目", + "cycle": "周期", + "cycles": "周期", + "module": "模块", + "modules": "模块", + "labels": "标签", + "assignees": "分配人", + "assignee": "分配人", + "created_by": "创建者", + "none": "无", + "link": "链接", + "estimate": "估算", + "layout": "布局", + "filters": "筛选", + "display": "显示", + "load_more": "加载更多", + "no_matches_found": "未找到匹配项", + "activity": "活动", + "analytics": "分析", + "success": "成功", + "error": "错误", + "group_by": "分组方式", + "search": "搜索", + "epic": "史诗", + "issue": "问题", + "warning": "警告", + "updating": "更新中", + "update": "更新", + "creating": "创建中", + "cancel": "取消", + "description": "描述", + "title": "标题", + "order_by": { + "label": "排序方式", + "manual": "手动", + "last_created": "最近创建", + "last_updated": "最近更新", + "start_date": "开始日期", + "due_date": "截止日期" + }, + "comments": "评论", + "updates": "更新" + }, + "form": { + "title": { + "required": "标题为必填项", + "max_length": "标题不能超过{length}个字符" + } + }, + "entity": { + "grouping_title": "{entity}分组", + "priority": "{entity}", + "all": "所有{entity}", + "drop_here_to_move": "拖放至此以移动{entity}" + }, + "epic": { + "all": "所有史诗", + "label": "{count, plural, one {史诗} other {史诗}}" + }, + "issue": { + "label": "{count, plural, one {问题} other {问题}}", + "all": "所有问题", + "add": "添加问题", + "priority": { + "urgent": "紧急", + "high": "高", + "medium": "中", + "low": "低" + }, + "display": { + "properties": { + "label": "显示属性", + "id": "ID", + "issue_type": "问题类型", + "sub_issue_count": "子问题数量", + "attachment_count": "附件数量" + }, + "extra": { + "show_sub_issues": "显示子问题", + "show_empty_groups": "显示空分组" + } + }, + "layouts": { + "ordered_by_label": "当前排序方式", + "list": "列表", + "kanban": "看板", + "calendar": "日历", + "spreadsheet": "表格", + "gantt": "时间线", + "title": { + "list": "列表视图", + "kanban": "看板视图", + "calendar": "日历视图", + "spreadsheet": "表格视图", + "gantt": "时间线视图" + } + }, + "states": { + "active": "进行中", + "backlog": "待办" + }, + "comments": { + "create": { + "success": "评论创建成功", + "error": "评论创建失败,请稍后重试" + }, + "update": { + "success": "评论更新成功", + "error": "评论更新失败,请稍后重试" + }, + "remove": { + "success": "评论删除成功", + "error": "评论删除失败,请稍后重试" + }, + "upload": { + "error": "资源上传失败,请稍后重试" + } + } + }, + + "project": { + "label": "{count, plural, one {项目} other {项目}}" + }, + + "view": { + "create": { + "label": "创建视图" + }, + "update": { + "label": "更新视图" + } + }, + "user_profile": { "title": "你的工作", "work": "工作", diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts index f77408fd6c0..a630d0ba20a 100644 --- a/packages/types/src/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -15,7 +15,7 @@ import type { TIssueGroupByOptions, TIssueOrderByOptions, TIssueGroupingFilters, - TIssueExtraOptions + TIssueExtraOptions, } from "@plane/types"; export interface IIssueCycle { diff --git a/space/core/components/issues/filters/priority.tsx b/space/core/components/issues/filters/priority.tsx index 4f16f89ba83..916c17abb86 100644 --- a/space/core/components/issues/filters/priority.tsx +++ b/space/core/components/issues/filters/priority.tsx @@ -2,8 +2,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; -// ui import { ISSUE_PRIORITY_FILTERS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +// ui import { PriorityIcon } from "@plane/ui"; // components import { FilterHeader, FilterOption } from "./helpers"; @@ -18,6 +19,9 @@ type Props = { export const FilterPriority: React.FC = observer((props) => { const { appliedFilters, handleUpdate, searchQuery } = props; + // hooks + const { t } = useTranslation(); + const [previewEnabled, setPreviewEnabled] = useState(true); const appliedFiltersCount = appliedFilters?.length ?? 0; @@ -40,11 +44,11 @@ export const FilterPriority: React.FC = observer((props) => { isChecked={appliedFilters?.includes(priority.key) ? true : false} onClick={() => handleUpdate(priority.key)} icon={} - title={priority.title} + title={t(priority.titleTranslationKey)} /> )) ) : ( -

    No matches found

    +

    {t("common.no_matches_found")}

    )}
    )} diff --git a/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx index bc61c54af6e..48dd4047c34 100644 --- a/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -131,10 +131,14 @@ const SubGroupSwimlaneHeader: React.FC = observer( const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup); if (subGroupByVisibilityToggle === false) return <>; - return (
    - +
    ); })} diff --git a/space/core/components/issues/issue-layouts/list/list-group.tsx b/space/core/components/issues/issue-layouts/list/list-group.tsx index 75d280d0d07..0a8c5ebb0d2 100644 --- a/space/core/components/issues/issue-layouts/list/list-group.tsx +++ b/space/core/components/issues/issue-layouts/list/list-group.tsx @@ -2,6 +2,7 @@ import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; // plane types import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types"; // plane utils @@ -62,6 +63,8 @@ export const ListGroup = observer((props: Props) => { } = props; const [isExpanded, setIsExpanded] = useState(true); const groupRef = useRef(null); + // hooks + const { t } = useTranslation(); const [intersectionElement, setIntersectionElement] = useState(null); @@ -85,7 +88,7 @@ export const ListGroup = observer((props: Props) => { } onClick={() => loadMoreIssues(group.id)} > - Load More ↓ + {t("common.load_more")} ↓
    ); diff --git a/space/core/components/issues/issue-layouts/properties/priority.tsx b/space/core/components/issues/issue-layouts/properties/priority.tsx index 7b4bbda7714..e3bb6e178e6 100644 --- a/space/core/components/issues/issue-layouts/properties/priority.tsx +++ b/space/core/components/issues/issue-layouts/properties/priority.tsx @@ -1,5 +1,6 @@ "use client"; +import { useTranslation } from "@plane/i18n"; // types import { TIssuePriorities } from "@plane/types"; import { Tooltip } from "@plane/ui"; @@ -13,17 +14,19 @@ export const IssueBlockPriority = ({ priority: TIssuePriorities | null; shouldShowName?: boolean; }) => { + // hooks + const { t } = useTranslation(); const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null; if (priority_detail === null) return <>; return ( - +
    {priority_detail?.icon}
    - {shouldShowName && {priority_detail?.title}} + {shouldShowName && {t(priority_detail?.titleTranslationKey || "")}}
    ); diff --git a/space/core/components/issues/navbar/layout-selection.tsx b/space/core/components/issues/navbar/layout-selection.tsx index 1b8344d0242..36c8f8e243d 100644 --- a/space/core/components/issues/navbar/layout-selection.tsx +++ b/space/core/components/issues/navbar/layout-selection.tsx @@ -5,6 +5,8 @@ import { observer } from "mobx-react"; import { useRouter, useSearchParams } from "next/navigation"; // ui import { SITES_ISSUE_LAYOUTS } from "@plane/constants"; +// plane i18n +import { useTranslation } from "@plane/i18n"; import { Tooltip } from "@plane/ui"; // helpers import { queryParamGenerator } from "@/helpers/query-param-generator"; @@ -19,6 +21,8 @@ type Props = { export const IssuesLayoutSelection: FC = observer((props) => { const { anchor } = props; + // hooks + const { t } = useTranslation(); // router const router = useRouter(); const searchParams = useSearchParams(); @@ -45,7 +49,7 @@ export const IssuesLayoutSelection: FC = observer((props) => { if (!layoutOptions[layout.key]) return; return ( - +
    diff --git a/space/package.json b/space/package.json index ff174a1d333..207f95938d1 100644 --- a/space/package.json +++ b/space/package.json @@ -20,6 +20,7 @@ "@mui/material": "^5.14.1", "@plane/constants": "*", "@plane/editor": "*", + "@plane/i18n": "*", "@plane/types": "*", "@plane/ui": "*", "@plane/services": "*", diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx index 21d230347c9..49fa5d2e5a1 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx @@ -6,21 +6,30 @@ import { useParams } from "next/navigation"; // icons import { ChevronDown } from "lucide-react"; // plane constants -import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants"; +import { + EIssueLayoutTypes, + EIssueFilterType, + EIssuesStoreType, + ISSUE_LAYOUTS, + ISSUE_DISPLAY_FILTERS_BY_PAGE, +} from "@plane/constants"; +// plane i18n +import { useTranslation } from "@plane/i18n"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // ui import { CustomMenu } from "@plane/ui"; // components -import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue"; +import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues"; + // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel } from "@/hooks/store"; export const ProfileIssuesMobileHeader = observer(() => { + // plane i18n + const { t } = useTranslation(); // router const { workspaceSlug, userId } = useParams(); // store hook @@ -112,7 +121,7 @@ export const ProfileIssuesMobileHeader = observer(() => { placement="bottom-start" customButton={
    - Layout + {t("common.layout")}
    } @@ -129,19 +138,19 @@ export const ProfileIssuesMobileHeader = observer(() => { }} className="flex items-center gap-2" > - -
    {layout.title}
    + +
    {t(layout.i18n_title)}
    ); })}
    - Filters + {t("common.filters")}
    } @@ -149,7 +158,7 @@ export const ProfileIssuesMobileHeader = observer(() => { > {
    - Display + {t("common.display")}
    } > { projectId: string; cycleId: string; }; + // i18n + const { t } = useTranslation(); // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -184,7 +186,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { type="text" link={ } /> @@ -239,7 +241,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { selectedLayout={activeLayout} /> @@ -247,7 +249,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { filters={issueFilters?.filters ?? {}} handleFiltersUpdate={handleFiltersUpdate} layoutDisplayFiltersOptions={ - activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined + activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined } displayFilters={issueFilters?.displayFilters ?? {}} handleDisplayFiltersUpdate={handleDisplayFilters} @@ -258,10 +260,10 @@ export const CycleIssuesHeader: React.FC = observer(() => { moduleViewDisabled={!currentProjectDetails?.module_view} /> - + { {canUserCreateIssue && ( <> {!isCompletedCycle && ( )} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx index 1f85665fa86..31eb5b249fd 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx @@ -5,32 +5,42 @@ import { useParams } from "next/navigation"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; // plane constants -import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants"; +import { + EIssueLayoutTypes, + EIssueFilterType, + EIssuesStoreType, + ISSUE_LAYOUTS, + ISSUE_DISPLAY_FILTERS_BY_PAGE, +} from "@plane/constants"; +// i18n +import { useTranslation } from "@plane/i18n"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // ui import { CustomMenu } from "@plane/ui"; // components import { ProjectAnalyticsModal } from "@/components/analytics"; -import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue"; +import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues"; // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store"; export const CycleIssuesMobileHeader = () => { + // i18n + const { t } = useTranslation(); + const [analyticsModal, setAnalyticsModal] = useState(false); const { getCycleById } = useCycle(); const layouts = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Board", icon: Kanban }, - { key: "calendar", title: "Calendar", icon: Calendar }, + { key: "list", titleTranslationKey: "issue.layouts.list", icon: List }, + { key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban }, + { key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar }, ]; const { workspaceSlug, projectId, cycleId } = useParams(); const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; + // store hooks const { currentProjectDetails } = useProject(); const { @@ -123,7 +133,9 @@ export const CycleIssuesMobileHeader = () => { maxHeight={"md"} className="flex flex-grow justify-center text-custom-text-200 text-sm" placement="bottom-start" - customButton={Layout} + customButton={ + {t("common.layout")} + } customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" closeOnSelect > @@ -135,18 +147,18 @@ export const CycleIssuesMobileHeader = () => { }} className="flex items-center gap-2" > - -
    {layout.title}
    + +
    {t(layout.titleTranslationKey)}
    ))}
    - Filters + {t("common.filters")} } @@ -156,7 +168,7 @@ export const CycleIssuesMobileHeader = () => { filters={issueFilters?.filters ?? {}} handleFiltersUpdate={handleFiltersUpdate} layoutDisplayFiltersOptions={ - activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined + activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined } displayFilters={issueFilters?.displayFilters ?? {}} handleDisplayFiltersUpdate={handleDisplayFilters} @@ -170,18 +182,18 @@ export const CycleIssuesMobileHeader = () => {
    - Display + {t("common.display")} } > { onClick={() => setAnalyticsModal(true)} className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200" > - Analytics + {t("common.analytics")}
    diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx index 3c996a8d14a..7ad851e86f7 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx @@ -4,7 +4,9 @@ import { FC, useCallback } 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, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; +// i18n +import { useTranslation } from "@plane/i18n"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // ui @@ -12,8 +14,6 @@ import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks @@ -23,6 +23,8 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; export const ProjectDraftIssueHeader: FC = observer(() => { + // i18n + const { t } = useTranslation(); // router const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string }; // store hooks @@ -122,14 +124,18 @@ export const ProjectDraftIssueHeader: FC = observer(() => { onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} /> - + { moduleViewDisabled={!currentProjectDetails?.module_view} /> - + { + // i18n + const { t } = useTranslation(); const layouts = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Board", icon: Kanban }, - { key: "calendar", title: "Calendar", icon: Calendar }, + { key: "list", titleTranslationKey: "issue.layouts.list", icon: List }, + { key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban }, + { key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar }, ]; const [analyticsModal, setAnalyticsModal] = useState(false); const { workspaceSlug, projectId } = useParams() as { @@ -104,7 +117,7 @@ export const ProjectIssuesMobileHeader = observer(() => { placement="bottom-start" customButton={
    - Layout + {t("common.layout")}
    } @@ -119,18 +132,18 @@ export const ProjectIssuesMobileHeader = observer(() => { }} className="flex items-center gap-2" > - -
    {layout.title}
    + +
    {t(layout.titleTranslationKey)}
    ))}
    - Filters + {t("common.filters")} } @@ -142,7 +155,7 @@ export const ProjectIssuesMobileHeader = observer(() => { displayFilters={issueFilters?.displayFilters ?? {}} handleDisplayFiltersUpdate={handleDisplayFilters} layoutDisplayFiltersOptions={ - activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined + activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined } labels={projectLabels} memberIds={projectMemberIds ?? undefined} @@ -154,18 +167,18 @@ export const ProjectIssuesMobileHeader = observer(() => {
    - Display + {t("common.display")} } > { onClick={() => setAnalyticsModal(true)} className="flex flex-grow justify-center border-l border-custom-border-200 text-sm text-custom-text-200" > - Analytics + {t("common.analytics")}
    diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx index e167509f221..8e9f49f173b 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx @@ -7,7 +7,7 @@ import { useParams } from "next/navigation"; // icons import { ArrowRight, PanelRight } from "lucide-react"; // plane constants -import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType } from "@plane/constants"; +import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // ui @@ -16,8 +16,6 @@ import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header } from "@pla import { ProjectAnalyticsModal } from "@/components/analytics"; import { BreadcrumbLink } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; // helpers import { cn } from "@/helpers/common.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper"; @@ -247,7 +245,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { displayFilters={issueFilters?.displayFilters ?? {}} handleDisplayFiltersUpdate={handleDisplayFilters} layoutDisplayFiltersOptions={ - activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined + activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined } labels={projectLabels} memberIds={projectMemberIds ?? undefined} @@ -259,7 +257,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { { const [analyticsModal, setAnalyticsModal] = useState(false); const { currentProjectDetails } = useProject(); const { getModuleById } = useModule(); + const { t } = useTranslation(); const layouts = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Board", icon: Kanban }, - { key: "calendar", title: "Calendar", icon: Calendar }, + { key: "list", i18n_title: "issue.layouts.list", icon: List }, + { key: "kanban", i18n_title: "issue.layouts.kanban", icon: Kanban }, + { key: "calendar", i18n_title: "issue.layouts.calendar", icon: Calendar }, ]; const { workspaceSlug, projectId, moduleId } = useParams() as { workspaceSlug: string; @@ -116,8 +128,8 @@ export const ModuleIssuesMobileHeader = observer(() => { }} className="flex items-center gap-2" > - -
    {layout.title}
    + +
    {t(layout.i18n_title)}
    ))} @@ -139,7 +151,7 @@ export const ModuleIssuesMobileHeader = observer(() => { displayFilters={issueFilters?.displayFilters ?? {}} handleDisplayFiltersUpdate={handleDisplayFilters} layoutDisplayFiltersOptions={ - activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined + activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined } labels={projectLabels} memberIds={projectMemberIds ?? undefined} @@ -162,7 +174,7 @@ export const ModuleIssuesMobileHeader = observer(() => { > { displayFilters={issueFilters?.displayFilters ?? {}} handleDisplayFiltersUpdate={handleDisplayFilters} layoutDisplayFiltersOptions={ - activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined + activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined } projectId={projectId.toString()} labels={projectLabels} @@ -255,7 +254,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { { isFiltersApplied={isIssueFilterActive(issueFilters)} > { = (props) => { const filterKey = key as TActivityFilters; return { key: filterKey, - label: value.label, + labelTranslationKey: value.labelTranslationKey, isSelected: selectedFilters.includes(filterKey), onClick: () => toggleFilter(filterKey), }; diff --git a/web/ce/constants/index.ts b/web/ce/constants/index.ts index 123db122c8a..329f796ceb9 100644 --- a/web/ce/constants/index.ts +++ b/web/ce/constants/index.ts @@ -1,7 +1,6 @@ export * from "./ai"; export * from "./estimates"; export * from "./gantt-chart"; -export * from "./issues"; export * from "./project"; export * from "./user-permissions"; export * from "./workspace"; diff --git a/web/ce/constants/issues.ts b/web/ce/constants/issues.ts deleted file mode 100644 index 70a34577f73..00000000000 --- a/web/ce/constants/issues.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ILayoutDisplayFiltersOptions, TIssueActivityComment } from "@plane/types"; - -export enum EActivityFilterType { - ACTIVITY = "ACTIVITY", - COMMENT = "COMMENT", -} - -export type TActivityFilters = EActivityFilterType; - -export const ACTIVITY_FILTER_TYPE_OPTIONS: Record = { - [EActivityFilterType.ACTIVITY]: { - label: "Updates", - }, - [EActivityFilterType.COMMENT]: { - label: "Comments", - }, -}; - -export const defaultActivityFilters: TActivityFilters[] = [EActivityFilterType.ACTIVITY, EActivityFilterType.COMMENT]; - -export type TActivityFilterOption = { - key: TActivityFilters; - label: string; - isSelected: boolean; - onClick: () => void; -}; - -export const filterActivityOnSelectedFilters = ( - activity: TIssueActivityComment[], - filter: TActivityFilters[] -): TIssueActivityComment[] => - activity.filter((activity) => filter.includes(activity.activity_type as TActivityFilters)); - -export const ENABLE_ISSUE_DEPENDENCIES = false; - -export const ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { - [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; -} = {}; diff --git a/web/ce/store/issue/issue-details/activity.store.ts b/web/ce/store/issue/issue-details/activity.store.ts index 72c725010b8..93b925aba9d 100644 --- a/web/ce/store/issue/issue-details/activity.store.ts +++ b/web/ce/store/issue/issue-details/activity.store.ts @@ -8,7 +8,7 @@ import update from "lodash/update"; import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // plane package imports -import { EIssueServiceType, E_SORT_ORDER } from "@plane/constants"; +import { EIssueServiceType, E_SORT_ORDER, EActivityFilterType } from "@plane/constants"; import { TIssueActivityComment, TIssueActivity, @@ -17,7 +17,6 @@ import { TIssueServiceType, } from "@plane/types"; // plane web constants -import { EActivityFilterType } from "@/plane-web/constants/issues"; // services import { IssueActivityService } from "@/services/issue"; // store diff --git a/web/core/components/command-palette/actions/issue-actions/change-priority.tsx b/web/core/components/command-palette/actions/issue-actions/change-priority.tsx index 2f004e8eae0..d5fd09a55a5 100644 --- a/web/core/components/command-palette/actions/issue-actions/change-priority.tsx +++ b/web/core/components/command-palette/actions/issue-actions/change-priority.tsx @@ -5,12 +5,11 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Check } from "lucide-react"; // plane constants -import { EIssuesStoreType } from "@plane/constants"; +import { EIssuesStoreType, ISSUE_PRIORITIES } from "@plane/constants"; // plane types import { TIssue, TIssuePriorities } from "@plane/types"; // mobx store import { PriorityIcon } from "@plane/ui"; -import { ISSUE_PRIORITIES } from "@/constants/issue"; import { useIssues } from "@/hooks/store"; // ui // types diff --git a/web/core/components/dropdowns/layout.tsx b/web/core/components/dropdowns/layout.tsx index 9907ab1406f..5864f4a2933 100644 --- a/web/core/components/dropdowns/layout.tsx +++ b/web/core/components/dropdowns/layout.tsx @@ -2,13 +2,15 @@ import { useCallback, useMemo } from "react"; import { observer } from "mobx-react"; import { Check } from "lucide-react"; // plane constants -import { EIssueLayoutTypes } from "@plane/constants"; +import { EIssueLayoutTypes, ISSUE_LAYOUT_MAP } from "@plane/constants"; +// plane i18n +import { useTranslation } from "@plane/i18n"; // plane ui import { Dropdown } from "@plane/ui"; // plane utils import { cn } from "@plane/utils"; -// constants -import { ISSUE_LAYOUT_MAP } from "@/constants/issue"; +// components +import { IssueLayoutIcon } from "@/components/issues"; type TLayoutDropDown = { onChange: (value: EIssueLayoutTypes) => void; @@ -18,6 +20,8 @@ type TLayoutDropDown = { export const LayoutDropDown = observer((props: TLayoutDropDown) => { const { onChange, value = EIssueLayoutTypes.LIST, disabledLayouts = [] } = props; + // plane i18n + const { t } = useTranslation(); // derived values const availableLayouts = useMemo( () => Object.values(ISSUE_LAYOUT_MAP).filter((layout) => !disabledLayouts.includes(layout.key)), @@ -35,11 +39,10 @@ export const LayoutDropDown = observer((props: TLayoutDropDown) => { const buttonContent = useCallback((isOpen: boolean, buttonValue: string | string[] | undefined) => { const dropdownValue = ISSUE_LAYOUT_MAP[buttonValue as EIssueLayoutTypes]; - return (
    - - {dropdownValue.label} + + {t(dropdownValue.i18n_label)}
    ); }, []); @@ -50,8 +53,8 @@ export const LayoutDropDown = observer((props: TLayoutDropDown) => { return (
    - - {dropdownValue.label} + + {t(dropdownValue.i18n_label)}
    {props.selected && }
    diff --git a/web/core/components/dropdowns/priority.tsx b/web/core/components/dropdowns/priority.tsx index acf6dc26b12..4009405571d 100644 --- a/web/core/components/dropdowns/priority.tsx +++ b/web/core/components/dropdowns/priority.tsx @@ -5,13 +5,12 @@ import { useTheme } from "next-themes"; import { usePopper } from "react-popper"; import { Check, ChevronDown, Search, SignalHigh } from "lucide-react"; import { Combobox } from "@headlessui/react"; +import { ISSUE_PRIORITIES } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // types import { TIssuePriorities } from "@plane/types"; // ui import { ComboDropDown, PriorityIcon, Tooltip } from "@plane/ui"; -// constants -import { ISSUE_PRIORITIES } from "@/constants/issue"; // helpers import { cn } from "@/helpers/common.helper"; // hooks @@ -77,7 +76,7 @@ const BorderButton = (props: ButtonProps) => { return ( { ) : ( ))} - {!hideText && {t(priorityDetails?.key ?? "priority") ?? placeholder}} + {!hideText && {priorityDetails?.title ?? placeholder}} {dropdownArrow && (
    )} diff --git a/web/core/components/issues/archived-issues-header.tsx b/web/core/components/issues/archived-issues-header.tsx index e64c651b744..4e2eaebd9d4 100644 --- a/web/core/components/issues/archived-issues-header.tsx +++ b/web/core/components/issues/archived-issues-header.tsx @@ -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(() => {
    {/* filter options */}
    - + - + diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index e09b5434857..346c960b769 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -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} /> - + { isEpic={storeType === EIssuesStoreType.EPIC} /> - + { {canUserCreateIssue ? ( ) : ( <> diff --git a/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index c2b81310256..daf0115b18b 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -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"; diff --git a/web/core/components/issues/issue-detail/issue-activity/activity-filter.tsx b/web/core/components/issues/issue-detail/issue-activity/activity-filter.tsx index 3733e7f72b2..40162dc15d9 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity-filter.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity-filter.tsx @@ -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 = observer((props) => { const { selectedFilters = [], filterOptions } = props; + // hooks + const { t } = useTranslation(); + return ( = observer((props) => { prependIcon={} className="relative" > - Filters + {t("common.filters")} {selectedFilters.length < filterOptions.length && ( )} @@ -53,7 +57,7 @@ export const ActivityFilter: FC = observer((props) => { {item.isSelected && }
    - {item.label} + {t(item.labelTranslationKey)}
)} diff --git a/web/core/components/issues/issue-detail/issue-activity/root.tsx b/web/core/components/issues/issue-detail/issue-activity/root.tsx index 90bc93f1c0e..da54174ccc6 100644 --- a/web/core/components/issues/issue-detail/issue-activity/root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/root.tsx @@ -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 } 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,8 +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"; @@ -40,6 +41,8 @@ export type TActivityOperations = { export const IssueActivity: FC = 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 +90,16 @@ export const IssueActivity: FC = 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"), type: TOAST_TYPE.ERROR, - message: "Comment creation failed. Please try again later.", + message: t("issue.comments.create.error"), }); } }, @@ -105,15 +108,15 @@ export const IssueActivity: FC = 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"), type: TOAST_TYPE.ERROR, - message: "Comment update failed. Please try again later.", + message: t("issue.comments.update.error"), }); } }, @@ -122,15 +125,15 @@ export const IssueActivity: FC = 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"), type: TOAST_TYPE.ERROR, - message: "Comment remove failed. Please try again later.", + message: t("issue.comments.remove.error"), }); } }, @@ -149,7 +152,7 @@ export const IssueActivity: FC = 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 +166,7 @@ export const IssueActivity: FC = observer((props) => {
{/* header */}
-
Activity
+
{t("common.activity")}
{isWorklogButtonEnabled && ( = observer((props) => { moduleViewDisabled = false, isEpic = false, } = props; + // hooks + const { t } = useTranslation(); // router const { workspaceSlug, projectId: routerProjectId } = useParams(); // states @@ -57,7 +61,7 @@ export const FilterDisplayProperties: React.FC = observer((props) => { return ( <> setPreviewEnabled(!previewEnabled)} /> @@ -79,7 +83,7 @@ export const FilterDisplayProperties: React.FC = observer((props) => { }) } > - {displayProperty.title} + {t(displayProperty.titleTranslationKey)} ))} diff --git a/web/core/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx b/web/core/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx index a5067377d17..51780d2277e 100644 --- a/web/core/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx @@ -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 = 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 = 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)} /> ); })} diff --git a/web/core/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/core/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index c5adff75b9c..2622eae9d03 100644 --- a/web/core/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -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 = 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 = observer((props) => { return ( <> setPreviewEnabled(!previewEnabled)} /> @@ -45,7 +45,7 @@ export const FilterGroupBy: React.FC = observer((props) => { key={groupBy?.key} isChecked={selectedGroupBy === groupBy?.key ? true : false} onClick={() => handleUpdate(groupBy.key)} - title={groupBy.title} + title={t(groupBy.titleTranslationKey)} multiple={false} /> ); diff --git a/web/core/components/issues/issue-layouts/filters/header/display-filters/issue-grouping.tsx b/web/core/components/issues/issue-layouts/filters/header/display-filters/issue-grouping.tsx index 0de60f625be..89cdabb1513 100644 --- a/web/core/components/issues/issue-layouts/filters/header/display-filters/issue-grouping.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/display-filters/issue-grouping.tsx @@ -1,11 +1,9 @@ import React from "react"; import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; 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 +12,16 @@ type Props = { isEpic?: boolean; }; +const ISSUE_FILTER_OPTIONS: { + key: TIssueGroupingFilters; + i18n_title: string; +}[] = [ + { key: null, i18n_title: "common.all" }, + { key: "active", i18n_title: "issue.states.active" }, + { key: "backlog", i18n_title: "issue.states.backlog" }, + // { key: "draft", title: "Draft Issues" }, +]; + export const FilterIssueGrouping: React.FC = observer((props) => { const { selectedIssueType, handleUpdate, isEpic = false } = props; @@ -21,10 +29,15 @@ export const FilterIssueGrouping: React.FC = observer((props) => { const activeIssueType = selectedIssueType ?? null; + // hooks + const { t } = useTranslation(); + return ( <> setPreviewEnabled(!previewEnabled)} /> @@ -35,7 +48,7 @@ export const FilterIssueGrouping: React.FC = observer((props) => { key={issueType?.key} isChecked={activeIssueType === issueType?.key ? true : false} onClick={() => handleUpdate(issueType?.key)} - title={`${issueType.title} ${isEpic ? "Epics" : "Issues"}`} + title={`${t(issueType.i18n_title)} ${isEpic ? t("epic.label", { count: 2 }) : t("issue.label", { count: 2 })}`} multiple={false} /> ))} diff --git a/web/core/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx b/web/core/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx index a9ff00b3d0e..5c4a6ba50b3 100644 --- a/web/core/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx @@ -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 = 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 = observer((props) => { return ( <> setPreviewEnabled(!previewEnabled)} /> @@ -35,7 +36,7 @@ export const FilterOrderBy: React.FC = observer((props) => { key={orderBy?.key} isChecked={activeOrderBy === orderBy?.key ? true : false} onClick={() => handleUpdate(orderBy.key)} - title={orderBy.title} + title={t(orderBy.titleTranslationKey)} multiple={false} /> ))} diff --git a/web/core/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx b/web/core/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx index 56dd86f36e5..e19f6d0112d 100644 --- a/web/core/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx @@ -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 = 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 = observer((props) => { key={subGroupBy?.key} isChecked={selectedSubGroupBy === subGroupBy?.key ? true : false} onClick={() => handleUpdate(subGroupBy.key)} - title={subGroupBy.title} + title={t(subGroupBy.titleTranslationKey)} multiple={false} /> ); diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index 37eee9e936f..c9f463222b1 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -2,6 +2,8 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Search, X } from "lucide-react"; +// i18n +import { useTranslation } from "@plane/i18n"; // types import { IIssueDisplayFilterOptions, @@ -49,6 +51,7 @@ type Props = { }; export const FilterSelection: React.FC = observer((props) => { + const { filters, displayFilters, @@ -63,6 +66,9 @@ export const FilterSelection: React.FC = observer((props) => { moduleViewDisabled = false, isEpic = false, } = props; + + // i18n + const { t } = useTranslation(); // hooks const { isMobile } = usePlatformOS(); const { moduleId, cycleId } = useParams(); @@ -95,7 +101,7 @@ export const FilterSelection: React.FC = observer((props) => { setFiltersSearchQuery(e.target.value)} autoFocus={!isMobile} diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/priority.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/priority.tsx index 609bb37f473..8d763899590 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/priority.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/priority.tsx @@ -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 = 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 ( <> 0 ? ` (${appliedFiltersCount})` : ""}`} + title={`${t("common.priority")}${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`} isPreviewEnabled={previewEnabled} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} /> @@ -47,7 +45,7 @@ export const FilterPriority: React.FC = observer((props) => { /> )) ) : ( -

No matches found

+

{t("common.no_matches_found")}

)}
)} diff --git a/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx b/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx index 9439eea5dcf..53d32aaeae2 100644 --- a/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx @@ -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) => { 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) => { return (
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => ( - +
); diff --git a/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx index e2d52e7ca59..f2a6adf276f 100644 --- a/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -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 = observer( const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup); if (subGroupByVisibilityToggle === false) return <>; - + const { t } = useTranslation(); return (
= observer( collapsedGroups={collapsedGroups} handleCollapsedGroups={handleCollapsedGroups} issuePayload={_list.payload} - /> + />{" "}
); })} @@ -155,6 +156,7 @@ const SubGroupSwimlane: React.FC = 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 = observer((props) => { { + switch (layout) { + case EIssueLayoutTypes.LIST: + return ; + case EIssueLayoutTypes.KANBAN: + return ; + case EIssueLayoutTypes.CALENDAR: + return ; + case EIssueLayoutTypes.SPREADSHEET: + return ; + case EIssueLayoutTypes.GANTT: + return ; + default: + return null; + } +}; diff --git a/web/core/components/issues/issue-layouts/list/list-group.tsx b/web/core/components/issues/issue-layouts/list/list-group.tsx index 22faef8438f..8e766d05ec3 100644 --- a/web/core/components/issues/issue-layouts/list/list-group.tsx +++ b/web/core/components/issues/issue-layouts/list/list-group.tsx @@ -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(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")} ↓
); @@ -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} diff --git a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index c93a36316b9..8b55be56571 100644 --- a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -4,7 +4,13 @@ import { observer } from "mobx-react"; import { useParams, useSearchParams } from "next/navigation"; import useSWR from "swr"; // plane constants -import { ALL_ISSUES, EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants"; +import { + ALL_ISSUES, + EIssueLayoutTypes, + EIssueFilterType, + EIssuesStoreType, + ISSUE_DISPLAY_FILTERS_BY_PAGE, +} from "@plane/constants"; import { IIssueDisplayFilterOptions } from "@plane/types"; // hooks // components @@ -12,8 +18,6 @@ import { EmptyState } from "@/components/common"; import { SpreadsheetView } from "@/components/issues/issue-layouts"; import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns"; import { SpreadsheetLayoutLoader } from "@/components/ui"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; // hooks import { useGlobalView, useIssues, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; @@ -71,11 +75,7 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { Object.keys(routeFilters).forEach((key) => { const filterKey: any = key; const filterValue = routeFilters[key]?.toString() || undefined; - if ( - ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet.filters.includes(filterKey) && - filterKey && - filterValue - ) + if (ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet.filters.includes(filterKey) && filterKey && filterValue) issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") }; }); diff --git a/web/core/components/issues/issue-layouts/utils.tsx b/web/core/components/issues/issue-layouts/utils.tsx index 77d8ae19221..59b3dc9b257 100644 --- a/web/core/components/issues/issue-layouts/utils.tsx +++ b/web/core/components/issues/issue-layouts/utils.tsx @@ -11,7 +11,7 @@ import uniq from "lodash/uniq"; import scrollIntoView from "smooth-scroll-into-view-if-needed"; import { ContrastIcon } from "lucide-react"; // plane types -import { EIssuesStoreType } from "@plane/constants"; +import { EIssuesStoreType, ISSUE_PRIORITIES } from "@plane/constants"; import { GroupByColumnTypes, IGroupByColumn, @@ -30,8 +30,6 @@ import { import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; // components import { Logo } from "@/components/common"; -// constants -import { ISSUE_PRIORITIES } from "@/constants/issue"; import { STATE_GROUPS } from "@/constants/state"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; @@ -85,7 +83,7 @@ export const getGroupByColumns = ({ return [ { id: "All Issues", - name: isEpic ? "All Epics" : "All Issues", + name: `All ${isEpic ? "Epics" : "Issues"}`, payload: {}, icon: undefined, }, @@ -710,4 +708,4 @@ export const getBlockViewDetails = ( message, blockStyle, }; -}; \ No newline at end of file +}; diff --git a/web/core/components/profile/profile-issues-filter.tsx b/web/core/components/profile/profile-issues-filter.tsx index 4b20324ea2f..baf85c2108e 100644 --- a/web/core/components/profile/profile-issues-filter.tsx +++ b/web/core/components/profile/profile-issues-filter.tsx @@ -2,19 +2,21 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane constants -import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType } from "@plane/constants"; +import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; +// i18n +import { useTranslation } from "@plane/i18n"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // components import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "@/components/issues"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel } from "@/hooks/store"; export const ProfileIssuesFilter = observer(() => { + // i18n + const { t } = useTranslation(); // router const { workspaceSlug, userId } = useParams(); // store hook @@ -105,10 +107,10 @@ export const ProfileIssuesFilter = observer(() => { selectedLayout={activeLayout} /> - + { /> - + = observer((props) => {
- {t("congrats")}! {t("project")} {" "} + {t("congrats")}! {t("project.label", { count: 1 })} {" "}

{currentProjectDetails.name}

{t("created").toLowerCase()}.
diff --git a/web/core/components/views/form.tsx b/web/core/components/views/form.tsx index a39f98fc4e8..f543966a2f1 100644 --- a/web/core/components/views/form.tsx +++ b/web/core/components/views/form.tsx @@ -5,7 +5,9 @@ import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { Layers } from "lucide-react"; // plane constants -import { EIssueLayoutTypes } from "@plane/constants"; +import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; +// i18n +import { useTranslation } from "@plane/i18n"; // types import { IProjectView, IIssueFilterOptions, IIssueDisplayProperties, IIssueDisplayFilterOptions } from "@plane/types"; // ui @@ -13,8 +15,6 @@ import { Button, EmojiIconPicker, EmojiIconPickerTypes, Input, TextArea } from " // components import { Logo } from "@/components/common"; import { AppliedFiltersList, DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { ETabIndices } from "@/constants/tab-indices"; import { EViewAccess } from "@/constants/views"; // helpers @@ -45,6 +45,8 @@ const defaultValues: Partial = { export const ProjectViewForm: React.FC = observer((props) => { const { handleFormSubmit, handleClose, data, preLoadedData } = props; + // i18n + const { t } = useTranslation(); // state const [isOpen, setIsOpen] = useState(false); // store hooks @@ -140,7 +142,9 @@ export const ProjectViewForm: React.FC = observer((props) => { return (
-

{data ? "Update" : "Create"} view

+

+ {data ? t("view.update.label") : t("view.create.label")} +

= observer((props) => { control={control} name="name" rules={{ - required: "Title is required", + required: t("form.title.required"), maxLength: { value: 255, - message: "Title should be less than 255 characters", + message: t("form.title.max_length", { length: 255 }), }, }} render={({ field: { value, onChange } }) => ( @@ -201,7 +205,7 @@ export const ProjectViewForm: React.FC = observer((props) => { value={value} onChange={onChange} hasError={Boolean(errors.name)} - placeholder="Title" + placeholder={t("common.title")} className="w-full text-base" tabIndex={getIndex("name")} autoFocus @@ -219,7 +223,7 @@ export const ProjectViewForm: React.FC = observer((props) => {