Compare commits

...

13 Commits

Author SHA1 Message Date
Akshita Goyal
2f56caa414 chore: ln support workspace projects constants (#6429)
* chore: ln support workspace projects constants

* fix: translation key

* fix: removed state translation

* fix: removed state translation
2025-01-27 20:14:36 +05:30
Akshita Goyal
ca02b0d10e fix: ln support for views constants (#6431)
* fix: ln support for views constants

* fix: added translation

* fix: translation keys

* fix: access
2025-01-27 19:59:23 +05:30
Akshita Goyal
eaf8ce0ed2 chore: ln support for inbox constants (#6432)
* chore: ln support for inbox constants

* fix: snooze duration

* fix: enum

* fix: translation keys

* fix: inbox status icon

* fix: status icon

* fix: naming

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-01-27 19:47:52 +05:30
Anmol Singh Bhatia
a4450d34b4 chore: tab indices constant moved to plane package (#6464) 2025-01-27 19:06:16 +05:30
Anmol Singh Bhatia
7f19caf5dc feat: workspace constant language support and refactor (#6462)
* chore: workspace constant language support and refactor

* chore: workspace constant language support and refactor

* chore: code refactor

* chore: code refactor

* merge conflict

* chore: code refactor

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-01-27 18:01:22 +05:30
Prateek Shourya
3ec71d6607 chore: translation update 2025-01-27 17:58:49 +05:30
Akshita Goyal
571c3f6212 feat: home language support without stickies (#6443)
* feat: home language support without stickies

* fix: home sidebar

* fix: added missing keys

* fix: show all btn

* fix: recents empty state
2025-01-27 17:55:13 +05:30
Akshita Goyal
9a3fb8b9ee Merge pull request #6463 from makeplane/chore-workspace-drafts-constant-language-support-and-refactor
chore workspace drafts constant refactor
2025-01-27 17:31:29 +05:30
Anmol Singh Bhatia
385be31ff1 chore: workspace drafts constant moved to plane constant package 2025-01-27 17:26:35 +05:30
Vamsi Krishna
2e022a4ed7 chore: migrated filters.ts to packages (#6459)
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-01-27 16:54:11 +05:30
Vamsi Krishna
0e110f3fd8 [WEB-3165]feat: language support for issues (#6452)
* * chore: moved issue constants to packages
* chore: restructured issue constants
* improvement: added translations to issue constants

* chore: updated translation structure

* * chore: updated chinese, spanish and french translation
* chore: updated translation for issues mobile header

* chore: updated spanish translation

* chore: removed translation for issue priorities

* fix: build errors

* chore: minor updates

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-01-27 16:14:17 +05:30
Akshita Goyal
9093b41132 fix: language support fo profile (#6461)
* fix: ln support fo profile

* fix: merge changes

* fix: merge changes
2025-01-27 14:31:16 +05:30
Anmol Singh Bhatia
ad2dc6759a chore: empty state refactor (#6404)
* chore: asset path helper hook added

* chore: detailed and simple empty state component added

* chore: section empty state component added

* chore: language translation for all empty states

* chore: new empty state implementation

* improvement: add more translations

* improvement: user permissions and workspace draft empty state

* chore: update translation structure

* chore: inbox empty states

* chore: disabled project features empty state

* chore: active cycle progress empty state

* chore: notification empty state

* chore: connections translation

* chore: issue comment, relation, bulk delete, and command k empty state translation

* chore: project pages empty state and translations

* chore: project module and view related empty state

* chore: remove project draft related empty state

* chore: project cycle, views and archived issues empty state

* chore: project cycles related empty state

* chore: project settings empty state

* chore: profile issue and acitivity empty state

* chore: workspace settings realted constants

* chore: stickies and home widgets empty state

* chore: remove all reference to deprecated empty state component and constnats

* chore: add support to ignore theme in resolved asset path hook

* chore: minor updates

* fix: build errors

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-01-25 18:54:01 +05:30
272 changed files with 8211 additions and 3867 deletions

View File

@@ -2,3 +2,56 @@ export enum E_SORT_ORDER {
ASC = "asc",
DESC = "desc",
}
export const DATE_AFTER_FILTER_OPTIONS = [
{
name: "1 week from now",
value: "1_weeks;after;fromnow",
},
{
name: "2 weeks from now",
value: "2_weeks;after;fromnow",
},
{
name: "1 month from now",
value: "1_months;after;fromnow",
},
{
name: "2 months from now",
value: "2_months;after;fromnow",
},
];
export const DATE_BEFORE_FILTER_OPTIONS = [
{
name: "1 week ago",
value: "1_weeks;before;fromnow",
},
{
name: "2 weeks ago",
value: "2_weeks;before;fromnow",
},
{
name: "1 month ago",
i18n_name: "date_filters.1_month_ago",
value: "1_months;before;fromnow",
},
];
export const PROJECT_CREATED_AT_FILTER_OPTIONS = [
{
name: "Today",
value: "today;custom;custom",
},
{
name: "Yesterday",
value: "yesterday;custom;custom",
},
{
name: "Last 7 days",
value: "last_7_days;custom;custom",
},
{
name: "Last 30 days",
value: "last_30_days;custom;custom",
},
];

View File

@@ -0,0 +1,91 @@
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
export enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
}
export enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export const INBOX_STATUS: {
key: string;
status: TInboxIssueStatus;
i18n_title: string;
i18n_description: () => string;
}[] = [
{
key: "pending",
i18n_title: "inbox_issue.status.pending.title",
status: EInboxIssueStatus.PENDING,
i18n_description: () => `inbox_issue.status.pending.description`,
},
{
key: "declined",
i18n_title: "inbox_issue.status.declined.title",
status: EInboxIssueStatus.DECLINED,
i18n_description: () => `inbox_issue.status.declined.description`,
},
{
key: "snoozed",
i18n_title: "inbox_issue.status.snoozed.title",
status: EInboxIssueStatus.SNOOZED,
i18n_description: () => `inbox_issue.status.snoozed.description`,
},
{
key: "accepted",
i18n_title: "inbox_issue.status.accepted.title",
status: EInboxIssueStatus.ACCEPTED,
i18n_description: () => `inbox_issue.status.accepted.description`,
},
{
key: "duplicate",
i18n_title: "inbox_issue.status.duplicate.title",
status: EInboxIssueStatus.DUPLICATE,
i18n_description: () => `inbox_issue.status.duplicate.description`,
},
];
export const INBOX_ISSUE_ORDER_BY_OPTIONS = [
{
key: "issue__created_at",
i18n_label: "inbox_issue.order_by.created_at",
},
{
key: "issue__updated_at",
i18n_label: "inbox_issue.order_by.updated_at",
},
{
key: "issue__sequence_id",
i18n_label: "inbox_issue.order_by.id",
},
];
export const INBOX_ISSUE_SORT_BY_OPTIONS = [
{
key: "asc",
i18n_label: "common.sort.asc",
},
{
key: "desc",
i18n_label: "common.sort.desc",
},
];

View File

@@ -11,6 +11,12 @@ export * from "./issue";
export * from "./metadata";
export * from "./state";
export * from "./swr";
export * from "./tab-indices";
export * from "./user";
export * from "./workspace";
export * from "./stickies";
export * from "./project";
export * from "./views";
export * from "./inbox";
export * from "./profile";
export * from "./workspace-drafts";

View File

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

View File

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

View File

@@ -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, TFiltersByLayout>
> = {
[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;

View File

@@ -0,0 +1,3 @@
export * from "./common";
export * from "./filter";
export * from "./layout";

View File

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

View File

@@ -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<LucideProps>;
}[] = [
{
key: "profile",
label: "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/",
},
];

View File

@@ -1,41 +1,59 @@
// icons
import { Globe2, Lock, LucideIcon } from "lucide-react";
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
import {
TProjectAppliedDisplayFilterKeys,
TProjectOrderByOptions,
} from "@plane/types";
export const NETWORK_CHOICES: {
key: 0 | 2;
label: string;
i18n_label: string;
description: string;
icon: LucideIcon;
}[] = [
{
key: 0,
label: "Private",
description: "Accessible only by invite",
i18n_label: "workspace_projects.network.private.title",
description: "workspace_projects.network.private.description", //"Accessible only by invite",
icon: Lock,
},
{
key: 2,
label: "Public",
description: "Anyone in the workspace except Guests can join",
i18n_label: "workspace_projects.network.public.title",
description: "workspace_projects.network.public.description", //"Anyone in the workspace except Guests can join",
icon: Globe2,
},
];
export const GROUP_CHOICES = {
backlog: "Backlog",
unstarted: "Unstarted",
started: "Started",
completed: "Completed",
cancelled: "Cancelled",
backlog: {
key: "backlog",
i18n_label: "workspace_projects.state.backlog",
},
unstarted: {
key: "unstarted",
i18n_label: "workspace_projects.state.unstarted",
},
started: {
key: "started",
i18n_label: "workspace_projects.state.started",
},
completed: {
key: "completed",
i18n_label: "workspace_projects.state.completed",
},
cancelled: {
key: "cancelled",
i18n_label: "workspace_projects.state.cancelled",
},
};
export const PROJECT_AUTOMATION_MONTHS = [
{ label: "1 month", value: 1 },
{ label: "3 months", value: 3 },
{ label: "6 months", value: 6 },
{ label: "9 months", value: 9 },
{ label: "12 months", value: 12 },
{ i18n_label: "common.months_count", value: 1 },
{ i18n_label: "common.months_count", value: 3 },
{ i18n_label: "common.months_count", value: 6 },
{ i18n_label: "common.months_count", value: 9 },
{ i18n_label: "common.months_count", value: 12 },
];
export const PROJECT_UNSPLASH_COVERS = [
@@ -59,55 +77,55 @@ export const PROJECT_UNSPLASH_COVERS = [
export const PROJECT_ORDER_BY_OPTIONS: {
key: TProjectOrderByOptions;
label: string;
i18n_label: string;
}[] = [
{
key: "sort_order",
label: "Manual",
i18n_label: "workspace_projects.sort.manual",
},
{
key: "name",
label: "Name",
i18n_label: "workspace_projects.sort.name",
},
{
key: "created_at",
label: "Created date",
i18n_label: "workspace_projects.sort.created_at",
},
{
key: "members_length",
label: "Number of members",
i18n_label: "workspace_projects.sort.members_length",
},
];
export const PROJECT_DISPLAY_FILTER_OPTIONS: {
key: TProjectAppliedDisplayFilterKeys;
label: string;
i18n_label: string;
}[] = [
{
key: "my_projects",
label: "My projects",
i18n_label: "workspace_projects.scope.my_projects",
},
{
key: "archived_projects",
label: "Archived",
i18n_label: "workspace_projects.scope.archived_projects",
},
];
export const PROJECT_ERROR_MESSAGES = {
permissionError: {
title: "You don't have permission to perform this action.",
message: undefined,
i18n_title: "workspace_projects.error.permission",
i18n_message: undefined,
},
cycleDeleteError: {
title: "Error",
message: "Failed to delete cycle",
i18n_title: "error",
i18n_message: "workspace_projects.error.cycle_delete",
},
moduleDeleteError: {
title: "Error",
message: "Failed to delete module",
i18n_title: "error",
i18n_message: "workspace_projects.error.module_delete",
},
issueDeleteError: {
title: "Error",
message: "Failed to delete issue",
i18n_title: "error",
i18n_message: "workspace_projects.error.issue_delete",
},
};

View File

@@ -54,7 +54,14 @@ export const PROJECT_CREATE_TAB_INDICES = [
"logo_props",
];
export const PROJECT_CYCLE_TAB_INDICES = ["name", "description", "date_range", "cancel", "submit", "project_id"];
export const PROJECT_CYCLE_TAB_INDICES = [
"name",
"description",
"date_range",
"cancel",
"submit",
"project_id",
];
export const PROJECT_MODULE_TAB_INDICES = [
"name",
@@ -67,9 +74,21 @@ export const PROJECT_MODULE_TAB_INDICES = [
"submit",
];
export const PROJECT_VIEW_TAB_INDICES = ["name", "description", "filters", "cancel", "submit"];
export const PROJECT_VIEW_TAB_INDICES = [
"name",
"description",
"filters",
"cancel",
"submit",
];
export const PROJECT_PAGE_TAB_INDICES = ["name", "public", "private", "cancel", "submit"];
export const PROJECT_PAGE_TAB_INDICES = [
"name",
"public",
"private",
"cancel",
"submit",
];
export enum ETabIndices {
ISSUE_FORM = "issue-form",

View File

@@ -0,0 +1,23 @@
export enum EViewAccess {
PRIVATE,
PUBLIC,
}
export const VIEW_ACCESS_SPECIFIERS: {
key: EViewAccess;
i18n_label: string;
}[] = [
{ key: EViewAccess.PUBLIC, i18n_label: "project_view.access.public" },
{ key: EViewAccess.PRIVATE, i18n_label: "project_view.access.private" },
];
export const VIEW_SORTING_KEY_OPTIONS = [
{ key: "name", i18n_label: "project_view.sort_by.name" },
{ key: "created_at", i18n_label: "project_view.sort_by.created_at" },
{ key: "updated_at", i18n_label: "project_view.sort_by.updated_at" },
];
export const VIEW_SORT_BY_OPTIONS = [
{ key: "asc", i18n_label: "common.order_by.asc" },
{ key: "desc", i18n_label: "common.order_by.desc" },
];

View File

@@ -1,3 +1,6 @@
import { TStaticViewTypes } from "@plane/types";
import { EUserWorkspaceRoles } from "./user";
export const ORGANIZATION_SIZE = [
"Just myself", // TODO: translate
"2-10",
@@ -74,3 +77,182 @@ export const RESTRICTED_URLS = [
"instances",
"instance",
];
export const WORKSPACE_SETTINGS = {
general: {
key: "general",
i18n_label: "workspace_settings.settings.general.title",
href: `/settings`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/`,
},
members: {
key: "members",
i18n_label: "workspace_settings.settings.members.title",
href: `/settings/members`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/members/`,
},
"billing-and-plans": {
key: "billing-and-plans",
i18n_label: "workspace_settings.settings.billing_and_plans.title",
href: `/settings/billing`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/billing/`,
},
export: {
key: "export",
i18n_label: "workspace_settings.settings.exports.title",
href: `/settings/exports`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/exports/`,
},
webhooks: {
key: "webhooks",
i18n_label: "workspace_settings.settings.webhooks.title",
href: `/settings/webhooks`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/webhooks/`,
},
"api-tokens": {
key: "api-tokens",
i18n_label: "workspace_settings.settings.webhooks.title",
href: `/settings/api-tokens`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/api-tokens/`,
},
};
export const WORKSPACE_SETTINGS_LINKS: {
key: string;
i18n_label: string;
href: string;
access: EUserWorkspaceRoles[];
highlight: (pathname: string, baseUrl: string) => boolean;
}[] = [
WORKSPACE_SETTINGS["general"],
WORKSPACE_SETTINGS["members"],
WORKSPACE_SETTINGS["billing-and-plans"],
WORKSPACE_SETTINGS["export"],
WORKSPACE_SETTINGS["webhooks"],
WORKSPACE_SETTINGS["api-tokens"],
];
export const ROLE = {
[EUserWorkspaceRoles.GUEST]: "Guest",
[EUserWorkspaceRoles.MEMBER]: "Member",
[EUserWorkspaceRoles.ADMIN]: "Admin",
};
export const ROLE_DETAILS = {
[EUserWorkspaceRoles.GUEST]: {
i18n_title: "role_details.guest.title",
i18n_description: "role_details.guest.description",
},
[EUserWorkspaceRoles.MEMBER]: {
i18n_title: "role_details.member.title",
i18n_description: "role_details.member.description",
},
[EUserWorkspaceRoles.ADMIN]: {
i18n_title: "role_details.admin.title",
i18n_description: "role_details.admin.description",
},
};
export const USER_ROLES = [
{
value: "Product / Project Manager",
i18n_label: "user_roles.product_or_project_manager",
},
{
value: "Development / Engineering",
i18n_label: "user_roles.development_or_engineering",
},
{
value: "Founder / Executive",
i18n_label: "user_roles.founder_or_executive",
},
{
value: "Freelancer / Consultant",
i18n_label: "user_roles.freelancer_or_consultant",
},
{ value: "Marketing / Growth", i18n_label: "user_roles.marketing_or_growth" },
{
value: "Sales / Business Development",
i18n_label: "user_roles.sales_or_business_development",
},
{
value: "Support / Operations",
i18n_label: "user_roles.support_or_operations",
},
{
value: "Student / Professor",
i18n_label: "user_roles.student_or_professor",
},
{ value: "Human Resources", i18n_label: "user_roles.human_resources" },
{ value: "Other", i18n_label: "user_roles.other" },
];
export const IMPORTERS_LIST = [
{
provider: "github",
type: "import",
i18n_title: "importer.github.title",
i18n_description: "importer.github.description",
},
{
provider: "jira",
type: "import",
i18n_title: "importer.jira.title",
i18n_description: "importer.jira.description",
},
];
export const EXPORTERS_LIST = [
{
provider: "csv",
type: "export",
i18n_title: "exporter.csv.title",
i18n_description: "exporter.csv.description",
},
{
provider: "xlsx",
type: "export",
i18n_title: "exporter.excel.title",
i18n_description: "exporter.csv.description",
},
{
provider: "json",
type: "export",
i18n_title: "exporter.json.title",
i18n_description: "exporter.csv.description",
},
];
export const DEFAULT_GLOBAL_VIEWS_LIST: {
key: TStaticViewTypes;
i18n_label: string;
}[] = [
{
key: "all-issues",
i18n_label: "default_global_view.all_issues",
},
{
key: "assigned",
i18n_label: "default_global_view.assigned",
},
{
key: "created",
i18n_label: "default_global_view.created",
},
{
key: "subscribed",
i18n_label: "default_global_view.subscribed",
},
];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
"add_members": "Ajouter des membres",
"remove_members": "Supprimer des membres",
"add": "Ajouter",
"adding": "Ajout",
"remove": "Supprimer",
"add_new": "Ajouter un nouveau",
"remove_selected": "Supprimer la sélection",
@@ -38,7 +39,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,11 +154,9 @@
"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",
"home": "Accueil",
"your_work": "Votre travail",
"drafts": "Brouillons",
"projects": "Projets",
@@ -316,5 +314,969 @@
"remove_parent_issue": "Supprimer le problème parent",
"add_parent": "Ajouter un parent",
"loading_members": "Chargement des membres...",
"inbox": "Boîte de réception"
"view_link_copied_to_clipboard": "Lien de vue copié dans le presse-papiers.",
"required": "Requis",
"optional": "Optionnel",
"Cancel": "Annuler",
"edit": "Modifier",
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
"delete": "Supprimer",
"good": "Bonjour",
"morning": "matin",
"afternoon": "après-midi",
"evening": "soir",
"show_all": "Tout afficher",
"show_less": "Afficher moins",
"no_data_yet": "Pas encore de données",
"connections": "Connexions",
"project_view": {
"access": {
"public": "Public",
"private": "Privé"
},
"sort_by": {
"created_at": "Date de création",
"updated_at": "Date de modification",
"name": "Nom"
}
},
"toast": {
"success": "Succès !",
"error": "Erreur !"
},
"home": {
"empty": {
"create_project": {
"title": "Créer un projet",
"description": "La plupart des choses commencent par un projet dans Plane.",
"cta": "Commencer"
},
"invite_team": {
"title": "Inviter votre équipe",
"description": "Construisez, déployez et gérez avec vos collègues.",
"cta": "Les faire entrer"
},
"configure_workspace": {
"title": "Configurez votre espace de travail",
"description": "Activez ou désactivez des fonctionnalités ou allez plus loin.",
"cta": "Configurer cet espace"
},
"personalize_account": {
"title": "Faites de Plane le vôtre",
"description": "Choisissez votre photo, vos couleurs et plus encore.",
"cta": "Personnaliser maintenant"
},
"widgets": {
"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"
}
}
},
"quick_links": {
"empty": "Enregistrez des liens vers des éléments de travail que vous souhaitez avoir à portée de main.",
"add": "Ajouter un lien rapide",
"title": "Lien rapide",
"title_plural": "Liens rapides",
"toasts": {
"created": {
"title": "Lien créé",
"message": "Le lien a été créé avec succès"
},
"not_created": {
"title": "Lien non créé",
"message": "Le lien n'a pas pu être créé"
},
"updated": {
"title": "Lien mis à jour",
"message": "Le lien a été mis à jour avec succès"
},
"not_updated": {
"title": "Lien non mis à jour",
"message": "Le lien n'a pas pu être mis à jour"
},
"removed": {
"title": "Lien supprimé",
"message": "Le lien a été supprimé avec succès"
},
"not_removed": {
"title": "Lien non supprimé",
"message": "Le lien n'a pas pu être supprimé"
}
}
},
"recents": {
"title": "Récents",
"empty": {
"project": "Vos projets récents apparaîtront ici une fois que vous en aurez visité un.",
"page": "Vos pages récentes apparaîtront ici une fois que vous en aurez visité une.",
"issue": "Vos problèmes récents apparaîtront ici une fois que vous en aurez visité un.",
"default": "Vous n'avez pas encore d'éléments récents."
},
"filters": {
"all": "Tous les éléments",
"projects": "Projets",
"pages": "Pages",
"issues": "Problèmes"
}
},
"my_stickies": {
"title": "Vos notes"
},
"new_at_plane": {
"title": "Nouveau chez Plane"
},
"quick_tutorial": {
"title": "Tutoriel rapide"
},
"widget": {
"reordered_successfully": "Widget réorganisé avec succès.",
"reordering_failed": "Une erreur s'est produite lors de la réorganisation du widget."
},
"manage_widgets": "Gérer les widgets",
"title": "Accueil",
"star_us_on_github": "Étoilez-nous sur GitHub"
},
"link": {
"modal": {
"url": {
"text": "URL",
"required": "L'URL n'est pas valide",
"placeholder": "Tapez ou collez une URL"
},
"title": {
"text": "Titre d'affichage",
"placeholder": "Comment souhaitez-vous voir ce lien"
}
}
},
"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",
"asc": "Croissant",
"desc": "Décroissant"
},
"sort": {
"asc": "Croissant",
"desc": "Décroissant"
},
"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"
}
},
"inbox_issue": {
"status": {
"pending": {
"title": "En attente",
"description": "En attente"
},
"declined": {
"title": "Refusé",
"description": "Refusé"
},
"snoozed": {
"title": "Reporté",
"description": "{days, plural, one{# jour} other{# jours}} restants"
},
"accepted": {
"title": "Accepté",
"description": "Accepté"
},
"duplicate": {
"title": "Doublon",
"description": "Doublon"
}
},
"source": {
"in-app": "dans l'application"
},
"order_by": {
"created_at": "Créé le",
"updated_at": "Mis à jour le",
"id": "Identifiant"
}
},
"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": {
"network": {
"private": {
"title": "Privé",
"description": "Accessible uniquement sur invitation"
},
"public": {
"title": "Public",
"description": "Tout le monde dans l'espace de travail sauf les invités peut rejoindre"
}
},
"error": {
"permission": "Vous n'avez pas la permission d'effectuer cette action.",
"cycle_delete": "Échec de la suppression du cycle",
"module_delete": "Échec de la suppression du module",
"issue_delete": "Échec de la suppression du problème"
},
"state": {
"backlog": "Backlog",
"unstarted": "Non démarré",
"started": "Démarré",
"completed": "Terminé",
"cancelled": "Annulé"
},
"sort": {
"manual": "Manuel",
"name": "Nom",
"created_at": "Date de création",
"members_length": "Nombre de membres"
},
"scope": {
"my_projects": "Mes projets",
"archived_projects": "Archivés"
},
"common": {
"months_count": "{months, plural, one{# mois} other{# mois}}"
},
"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",
"settings": {
"general": {
"title": "Général"
},
"members": {
"title": "Membres"
},
"billing-and-plans": {
"title": "Facturation et plans"
},
"exports": {
"title": "Exportations"
},
"webhooks": {
"title": "Webhooks"
},
"api-tokens": {
"title": "Jetons API"
}
},
"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",
"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",
"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": {
"title": "Vos pense-bêtes",
"placeholder": "cliquez pour écrire ici",
"all": "Tous les pense-bêtes",
"no-data": "Notez une idée, capturez un éclair de génie ou enregistrez une réflexion. Ajoutez un pense-bête pour commencer.",
"add": "Ajouter un pense-bête",
"search_placeholder": "Rechercher par titre",
"delete": "Supprimer le pense-bête",
"delete_confirmation": "Êtes-vous sûr de vouloir supprimer ce pense-bête ?",
"empty_state": {
"simple": "Notez une idée, capturez un éclair de génie ou enregistrez une réflexion. Ajoutez un pense-bête pour commencer.",
"general": {
"title": "Les pense-bêtes sont des notes rapides et des tâches que vous prenez à la volée.",
"description": "Capturez vos pensées et vos idées sans effort en créant des pense-bêtes accessibles à tout moment et de n'importe où.",
"primary_button": {
"text": "Ajouter un pense-bête"
}
},
"search": {
"title": "Cela ne correspond à aucun de vos pense-bêtes.",
"description": "Essayez un terme différent ou informez-nous\nsi vous êtes sûr que votre recherche est correcte.",
"primary_button": {
"text": "Ajouter un pense-bête"
}
}
},
"toasts": {
"errors": {
"wrong_name": "Le nom du pense-bête ne peut pas dépasser 100 caractères.",
"already_exists": "Il existe déjà un pense-bête sans description"
},
"created": {
"title": "Pense-bête créé",
"message": "Le pense-bête a été créé avec succès"
},
"not_created": {
"title": "Pense-bête non créé",
"message": "Le pense-bête n'a pas pu être créé"
},
"updated": {
"title": "Pense-bête mis à jour",
"message": "Le pense-bête a été mis à jour avec succès"
},
"not_updated": {
"title": "Pense-bête non mis à jour",
"message": "Le pense-bête n'a pas pu être mis à jour"
},
"removed": {
"title": "Pense-bête supprimé",
"message": "Le pense-bête a été supprimé avec succès"
},
"not_removed": {
"title": "Pense-bête non supprimé",
"message": "Le pense-bête n'a pas pu être supprimé"
}
}
},
"role_details": {
"guest": {
"title": "Invité",
"description": "Les membres externes des organisations peuvent être invités en tant qu'invités."
},
"member": {
"title": "Membre",
"description": "Capacité à lire, écrire, modifier et supprimer des entités dans les projets, cycles et modules"
},
"admin": {
"title": "Administrateur",
"description": "Toutes les autorisations définies sur vrai dans l'espace de travail."
}
},
"user_roles": {
"product_or_project_manager": "Chef de Produit / Projet",
"development_or_engineering": "Développement / Ingénierie",
"founder_or_executive": "Fondateur / Dirigeant",
"freelancer_or_consultant": "Freelance / Consultant",
"marketing_or_growth": "Marketing / Croissance",
"sales_or_business_development": "Ventes / Développement Commercial",
"support_or_operations": "Support / Opérations",
"student_or_professor": "Étudiant / Professeur",
"human_or_resources": "Ressources Humaines",
"other": "Autre"
},
"importer": {
"github": {
"title": "Github",
"description": "Importer des problèmes depuis les dépôts GitHub et les synchroniser."
},
"jira": {
"title": "Jira",
"description": "Importer des problèmes et des épopées depuis les projets Jira."
}
},
"exporter": {
"csv": {
"title": "CSV",
"description": "Exporter les problèmes vers un fichier CSV."
},
"xlsx": {
"title": "Excel",
"description": "Exporter les problèmes vers un fichier Excel."
},
"json": {
"title": "JSON",
"description": "Exporter les problèmes vers un fichier JSON."
}
},
"default_global_view": {
"all_issues": "Tous les problèmes",
"assigned": "Assignés",
"created": "Créés",
"subscribed": "Abonnés"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -154,11 +154,50 @@
"stay_ahead_of_blockers_description": "发现项目间的挑战,查看其他视图中不明显的周期依赖关系。",
"analytics": "分析",
"workspace_invites": "工作区邀请",
"workspace_settings": "工作区设置",
"workspace_settings": {
"label": "工作区设置",
"settings": {
"general": {
"title": "常规"
},
"members": {
"title": "会员"
},
"billing-and-plans": {
"title": "账单与计划"
},
"exports": {
"title": "导出"
},
"webhooks": {
"title": "网络钩子"
},
"api-tokens": {
"title": "API 令牌"
}
},
"empty_state": {
"api_tokens": {
"title": "尚未创建 API 令牌",
"description": "Plane API 可用于将您在 Plane 中的数据与任何外部系统集成。创建令牌即可开始使用。"
},
"webhooks": {
"title": "尚未添加网络钩子",
"description": "创建网络钩子以接收实时更新并自动执行操作。"
},
"exports": {
"title": "尚无导出记录",
"description": "每次导出时,您都可以在此处找到参考副本。"
},
"imports": {
"title": "尚无导入记录",
"description": "在此处查找所有以前的导入记录并下载。"
}
}
},
"enter_god_mode": "进入上帝模式",
"workspace_logo": "工作区徽标",
"new_issue": "新建问题",
"home": "首页",
"your_work": "您的工作",
"drafts": "草稿",
"projects": "项目",
@@ -222,7 +261,6 @@
"join_the_project_to_rearrange": "加入项目以重新排序",
"drag_to_rearrange": "拖动以重新排序",
"congrats": "恭喜!",
"project": "项目",
"open_project": "打开项目",
"issues": "问题",
"cycles": "周期",
@@ -315,5 +353,504 @@
"remove_parent_issue": "移除父问题",
"add_parent": "添加父问题",
"loading_members": "正在加载成员...",
"inbox": "收件箱"
"inbox": "收件箱",
"show_all": "显示全部",
"show_less": "显示较少",
"no_data_yet": "暂无数据",
"project_view": {
"access": {
"public": "公开",
"private": "私密"
},
"sort_by": {
"created_at": "创建时间",
"updated_at": "更新时间",
"name": "名称"
}
},
"toast": {
"success": "成功!",
"error": "错误!"
},
"home": {
"empty": {
"create_project": {
"title": "创建项目",
"description": "在Plane中大多数事情都从项目开始。",
"cta": "开始使用"
},
"invite_team": {
"title": "邀请团队",
"description": "与同事一起构建、部署和管理。",
"cta": "邀请成员"
},
"configure_workspace": {
"title": "设置工作空间",
"description": "开启或关闭功能,或进行更多设置。",
"cta": "配置此工作空间"
},
"personalize_account": {
"title": "让Plane更个性化",
"description": "选择您的头像、颜色等更多设置。",
"cta": "立即个性化"
}
},
"quick_links": {
"empty": "保存您想随手可得的工作相关链接。",
"add": "添加快捷链接",
"title": "快捷链接",
"title_plural": "快捷链接",
"toasts": {
"created": {
"title": "链接已创建",
"message": "链接已成功创建"
},
"not_created": {
"title": "链接未创建",
"message": "无法创建链接"
},
"updated": {
"title": "链接已更新",
"message": "链接已成功更新"
},
"not_updated": {
"title": "链接未更新",
"message": "无法更新链接"
},
"removed": {
"title": "链接已删除",
"message": "链接已成功删除"
},
"not_removed": {
"title": "链接未删除",
"message": "无法删除链接"
}
}
},
"recents": {
"title": "最近",
"empty": {
"project": "访问项目后,您的最近项目将显示在这里。",
"page": "访问页面后,您的最近页面将显示在这里。",
"issue": "访问问题后,您的最近问题将显示在这里。",
"default": "您还没有任何最近项目。"
},
"filters": {
"all": "所有项目",
"projects": "项目",
"pages": "页面",
"issues": "问题"
}
},
"my_stickies": {
"title": "您的便签"
},
"new_at_plane": {
"title": "Plane 新功能"
},
"quick_tutorial": {
"title": "快速教程"
},
"widget": {
"reordered_successfully": "小部件重新排序成功。",
"reordering_failed": "重新排序小部件时发生错误。"
},
"manage_widgets": "管理小部件",
"title": "首页",
"star_us_on_github": "在 GitHub 上为我们加星"
},
"link": {
"modal": {
"url": {
"text": "网址",
"required": "网址无效",
"placeholder": "输入或粘贴网址"
},
"title": {
"text": "显示标题",
"placeholder": "您希望将此链接显示为什么"
}
}
},
"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": "截止日期",
"asc": "升序",
"desc": "降序"
},
"sort": {
"asc": "升序",
"desc": "降序"
},
"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": "更新视图"
}
},
"inbox_issue": {
"status": {
"pending": {
"title": "待处理",
"description": "待处理"
},
"declined": {
"title": "已拒绝",
"description": "已拒绝"
},
"snoozed": {
"title": "已暂停",
"description": "还剩{days}天"
},
"accepted": {
"title": "已接受",
"description": "已接受"
},
"duplicate": {
"title": "重复",
"description": "重复"
}
},
"source": {
"in-app": "应用内"
},
"order_by": {
"created_at": "创建时间",
"updated_at": "更新时间",
"id": "编号"
}
},
"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": "活动"
}
},
"role_details": {
"guest": {
"title": "访客",
"description": "组织的外部成员可以被邀请为访客。"
},
"member": {
"title": "成员",
"description": "可以在项目、周期和模块内读取、写入、编辑和删除实体"
},
"admin": {
"title": "管理员",
"description": "工作区内的所有权限都设置为允许。"
}
},
"user_roles": {
"product_or_project_manager": "产品/项目经理",
"development_or_engineering": "开发/工程",
"founder_or_executive": "创始人/高管",
"freelancer_or_consultant": "自由职业者/顾问",
"marketing_or_growth": "市场营销/增长",
"sales_or_business_development": "销售/业务发展",
"support_or_operations": "支持/运营",
"student_or_professor": "学生/教授",
"human_or_resources": "人力资源",
"other": "其他"
},
"importer": {
"github": {
"title": "Github",
"description": "从 GitHub 仓库导入问题并同步。"
},
"jira": {
"title": "Jira",
"description": "从 Jira 项目导入问题和史诗。"
}
},
"exporter": {
"csv": {
"title": "CSV",
"description": "将问题导出为 CSV 文件。"
},
"xlsx": {
"title": "Excel",
"description": "将问题导出为 Excel 文件。"
},
"json": {
"title": "JSON",
"description": "将问题导出为 JSON 文件。"
}
},
"default_global_view": {
"all_issues": "所有问题",
"assigned": "已分配",
"created": "已创建",
"subscribed": "已订阅"
},
"stickies": {
"title": "便签",
"placeholder": "点击此处输入",
"all": "所有便签",
"no-data": "记下想法,捕捉灵感,或记录突发奇想。添加便签即可开始。",
"add": "添加便签",
"search_placeholder": "按标题搜索",
"delete": "删除便签",
"delete_confirmation": "确定要删除这个便签吗?",
"empty_state": {
"simple": "记下想法,捕捉灵感,或记录突发奇想。添加便签即可开始。",
"general": {
"title": "便签是您随手记下的快速笔记和待办事项。",
"description": "通过创建便签轻松捕捉您的想法和创意,随时随地都可以访问。",
"primary_button": {
"text": "添加便签"
}
},
"search": {
"title": "没有找到匹配的便签。",
"description": "尝试使用不同的搜索词,或如果您确定搜索正确,\n请告诉我们。",
"primary_button": {
"text": "添加便签"
}
}
},
"toasts": {
"errors": {
"wrong_name": "便签名称不能超过100个字符。",
"already_exists": "已存在一个没有描述的便签"
},
"created": {
"title": "便签已创建",
"message": "便签已成功创建"
},
"not_created": {
"title": "便签未创建",
"message": "无法创建便签"
},
"updated": {
"title": "便签已更新",
"message": "便签已成功更新"
},
"not_updated": {
"title": "便签未更新",
"message": "无法更新便签"
},
"removed": {
"title": "便签已删除",
"message": "便签已成功删除"
},
"not_removed": {
"title": "便签未删除",
"message": "无法删除便签"
}
}
},
"workspace_projects": {
"network": {
"private": {
"title": "私有",
"description": "仅限邀请访问"
},
"public": {
"title": "公开",
"description": "除访客外的工作区所有成员都可加入"
}
},
"error": {
"permission": "您没有执行此操作的权限。",
"cycle_delete": "删除周期失败",
"module_delete": "删除模块失败",
"issue_delete": "删除问题失败"
},
"state": {
"backlog": "待办",
"unstarted": "未开始",
"started": "进行中",
"completed": "已完成",
"cancelled": "已取消"
},
"sort": {
"manual": "手动",
"name": "名称",
"created_at": "创建日期",
"members_length": "成员数量"
},
"scope": {
"my_projects": "我的项目",
"archived_projects": "已归档"
},
"common": {
"months_count": "{months, plural, one{#个月} other{#个月}}"
}
}
}

View File

@@ -2,23 +2,6 @@ import { TPaginationInfo } from "./common";
import { TIssuePriorities } from "./issues";
import { TIssue } from "./issues/base";
enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
}
enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
// filters
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
@@ -38,10 +21,7 @@ export type TInboxIssueFilter = {
// sorting filters
export type TInboxIssueSortingKeys = "order_by" | "sort_by";
export type TInboxIssueSortingOrderByKeys =
| "issue__created_at"
| "issue__updated_at"
| "issue__sequence_id";
export type TInboxIssueSortingOrderByKeys = "issue__created_at" | "issue__updated_at" | "issue__sequence_id";
export type TInboxIssueSortingSortByKeys = "asc" | "desc";
@@ -78,17 +58,6 @@ export type TInboxDuplicateIssueDetails = {
name: string;
};
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export type TInboxIssuePaginationInfo = TPaginationInfo & {
total_results: number;
};

View File

@@ -15,7 +15,7 @@ import type {
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueGroupingFilters,
TIssueExtraOptions
TIssueExtraOptions,
} from "@plane/types";
export interface IIssueCycle {

View File

@@ -32,7 +32,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
</head>
<body>
<AppProvider>{children}</AppProvider>
<AppProvider>
<>{children}</>
</AppProvider>
</body>
</html>
);

View File

@@ -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<Props> = 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<Props> = observer((props) => {
isChecked={appliedFilters?.includes(priority.key) ? true : false}
onClick={() => handleUpdate(priority.key)}
icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />}
title={priority.title}
title={t(priority.titleTranslationKey)}
/>
))
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
<p className="text-xs italic text-custom-text-400">{t("common.no_matches_found")}</p>
)}
</div>
)}

View File

@@ -94,7 +94,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<HeaderGroupByCard
groupBy={groupBy}
icon={subList.icon}
icon={subList.icon as any}
title={subList.name}
count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
/>

View File

@@ -131,10 +131,14 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
if (subGroupByVisibilityToggle === false) return <></>;
return (
<div key={`${subGroupBy}_${group.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard groupBy={groupBy} icon={group.icon} title={group.name} count={groupCount} />
<HeaderGroupByCard
groupBy={groupBy}
icon={group.icon}
title={group.name}
count={groupCount}
/>
</div>
);
})}
@@ -262,7 +266,7 @@ const SubGroup: React.FC<ISubGroup> = observer((props) => {
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
<div className="sticky left-0 flex-shrink-0">
<HeaderSubGroupByCard
icon={group.icon}
icon={group.icon as any}
title={group.name || ""}
count={issueCount}
isExpanded={isExpanded}

View File

@@ -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<HTMLDivElement | null>(null);
// hooks
const { t } = useTranslation();
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
@@ -85,7 +88,7 @@ export const ListGroup = observer((props: Props) => {
}
onClick={() => loadMoreIssues(group.id)}
>
Load More &darr;
{t("common.load_more")} &darr;
</div>
);

View File

@@ -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 (
<Tooltip tooltipHeading="Priority" tooltipContent={priority_detail?.title}>
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
<div className="flex items-center relative w-full h-full">
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}>
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
</div>
{shouldShowName && <span className="pl-2 text-sm">{priority_detail?.title}</span>}
{shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
</div>
</Tooltip>
);

View File

@@ -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<Props> = observer((props) => {
const { anchor } = props;
// hooks
const { t } = useTranslation();
// router
const router = useRouter();
const searchParams = useSearchParams();
@@ -45,7 +49,7 @@ export const IssuesLayoutSelection: FC<Props> = observer((props) => {
if (!layoutOptions[layout.key]) return;
return (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<Tooltip key={layout.key} tooltipContent={t(layout.titleTranslationKey)}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${

View File

@@ -3,6 +3,7 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { CalendarCheck2, Signal } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// ui
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
import { getIssuePriorityFilters } from "@plane/utils";
@@ -24,6 +25,8 @@ type Props = {
};
export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDetails, mode }) => {
// hooks
const { t } = useTranslation();
const { getStateById } = useStates();
const state = getStateById(issueDetails?.state_id ?? undefined);
@@ -95,7 +98,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDet
<Icon iconName={priority?.icon} />
</span>
)}
<span>{priority?.title ?? "None"}</span>
<span>{t(priority?.titleTranslationKey || "common.none")}</span>
</div>
</div>
</div>

View File

@@ -20,6 +20,7 @@
"@mui/material": "^5.14.1",
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/i18n": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/services": "*",

View File

@@ -5,28 +5,41 @@ import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
import { Tab } from "@headlessui/react";
// plane package imports
import { ANALYTICS_TABS } from "@plane/constants";
import { ANALYTICS_TABS, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Header, EHeaderVariant } from "@plane/ui";
// components
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useProject, useWorkspace } from "@/hooks/store";
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// plane web
import { EUserPermissions } from "@/plane-web/constants";
const AnalyticsPage = observer(() => {
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(() => {
</Tab.Group>
</div>
) : (
<EmptyState
type={EmptyStateType.WORKSPACE_ANALYTICS}
primaryButtonOnClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
}}
<DetailedEmptyState
title={t("workspace_analytics.empty_state.general.title")}
description={t("workspace_analytics.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_analytics.empty_state.general.primary_button.text")}
title={t("workspace_analytics.empty_state.general.primary_button.comic.title")}
description={t("workspace_analytics.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
}}
disabled={!canPerformEmptyStateActions}
/>
}
/>
)}
</>

View File

@@ -7,6 +7,7 @@ import { Home } from "lucide-react";
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// ui
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
@@ -19,6 +20,7 @@ export const WorkspaceDashboardHeader = () => {
// hooks
const { captureEvent } = useEventTracker();
const { resolvedTheme } = useTheme();
const { t } = useTranslation();
return (
<>
@@ -28,7 +30,9 @@ export const WorkspaceDashboardHeader = () => {
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Home" icon={<Home className="h-4 w-4 text-custom-text-300" />} />}
link={
<BreadcrumbLink label={t("home.title")} icon={<Home className="h-4 w-4 text-custom-text-300" />} />
}
/>
</Breadcrumbs>
</div>
@@ -51,7 +55,7 @@ export const WorkspaceDashboardHeader = () => {
width={16}
alt="GitHub Logo"
/>
<span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
<span className="hidden text-xs font-medium sm:hidden md:block">{t("home.star_us_on_github")}</span>
</a>
</Header.RightItem>
</Header>

View File

@@ -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(() => {
<div className="w-full h-full overflow-hidden overflow-y-auto">
{!currentSelectedNotificationId ? (
<div className="w-full h-screen flex justify-center items-center">
<EmptyState type={EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE} layout="screen-simple" />
<SimpleEmptyState title={t("notification.empty_state.detail.title")} assetPath={resolvedPath} />
</div>
) : (
<>

View File

@@ -2,6 +2,7 @@
import { observer } from "mobx-react";
// components
import { useTranslation } from "@plane/i18n";
import { PageHead, AppHeader, ContentWrapper } from "@/components/core";
import { WorkspaceHomeView } from "@/components/home";
// hooks
@@ -11,8 +12,9 @@ import { WorkspaceDashboardHeader } from "./header";
const WorkspaceDashboardPage = observer(() => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Home` : undefined;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - ${t("home.title")}` : undefined;
return (
<>

View File

@@ -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<TUserProfileHeader> = 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<TUserProfileHeader> = 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 (
<Header>
@@ -86,7 +88,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
className="w-full text-custom-text-300"
>
{tab.label}
{t(tab.i18n_label)}
</Link>
</CustomMenu.MenuItem>
))}

View File

@@ -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<Props> = observer((props) => {
<AppHeader
header={
<UserProfileHeader
type={currentTab?.label}
type={currentTab?.i18n_label}
userProjectsData={userProjectsData}
showProfileIssuesFilter={isIssuesTab}
/>

View File

@@ -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={
<div className="flex flex-center text-sm text-custom-text-200">
Layout
{t("common.layout")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
}
@@ -129,19 +138,19 @@ export const ProfileIssuesMobileHeader = observer(() => {
}}
className="flex items-center gap-2"
>
<layout.icon className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title="Filters"
title={t("common.filters")}
placement="bottom-end"
menuButton={
<div className="flex flex-center text-sm text-custom-text-200">
Filters
{t("common.filters")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
</div>
}
@@ -149,7 +158,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
>
<FilterSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@@ -163,18 +172,18 @@ export const ProfileIssuesMobileHeader = observer(() => {
</div>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title="Display"
title={t("common.display")}
placement="bottom-end"
menuButton={
<div className="flex flex-center text-sm text-custom-text-200">
Display
{t("common.display")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
</div>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}

View File

@@ -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> = (props) => {
: "border-transparent"
}`}
>
{t(tab.label)}
{t(tab.i18n_label)}
</span>
</Link>
))}

View File

@@ -3,6 +3,8 @@
import { useParams } from "next/navigation";
import useSWR from "swr";
// types
import { GROUP_CHOICES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IUserStateDistribution, TStateGroups } from "@plane/types";
// components
import { ContentWrapper } from "@plane/ui";
@@ -16,7 +18,6 @@ import {
} from "@/components/profile";
// constants
import { USER_PROFILE_DATA } from "@/constants/fetch-keys";
import { GROUP_CHOICES } from "@/constants/project";
// services
import { UserService } from "@/services/user.service";
@@ -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 (
<>
<PageHead title="Your work" />
<PageHead title={t("profile.page_label")} />
<ContentWrapper className="space-y-7">
<ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} />

View File

@@ -7,7 +7,9 @@ import { useParams } from "next/navigation";
// icons
import { ArrowRight, PanelRight } from "lucide-react";
// 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
@@ -16,8 +18,6 @@ import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "
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";
@@ -72,6 +72,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
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={
<BreadcrumbLink
label="Cycles"
label={t("common.cycles")}
href={`/${workspaceSlug}/projects/${projectId}/cycles`}
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />}
/>
@@ -239,7 +241,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
selectedLayout={activeLayout}
/>
<FiltersDropdown
title="Filters"
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
@@ -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}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -276,7 +278,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
{canUserCreateIssue && (
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
{t("common.analytics")}
</Button>
{!isCompletedCycle && (
<Button
@@ -287,7 +289,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
}}
size="sm"
>
Add issue
{t("issue.add")}
</Button>
)}
</>

View File

@@ -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={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButton={
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{t("common.layout")}</span>
}
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.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="w-3 h-3" />
<div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Filters"
title={t("common.filters")}
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Filters
{t("common.filters")}
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
@@ -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 = () => {
</div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Display"
title={t("common.display")}
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Display
{t("common.display")}
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -198,7 +210,7 @@ export const CycleIssuesMobileHeader = () => {
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")}
</span>
</div>
</>

View File

@@ -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 (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_CYCLE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
<DetailedEmptyState
title={t("disabled_project.empty_state.cycle.title")}
description={t("disabled_project.empty_state.cycle.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.cycle.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !hasAdminLevelPermission,
}}
/>
</div>
);
@@ -71,12 +91,22 @@ const ProjectCyclesPage = observer(() => {
/>
{totalCycles === 0 ? (
<div className="h-full place-items-center">
<EmptyState
type={EmptyStateType.PROJECT_CYCLES}
primaryButtonOnClick={() => {
setTrackElement("Cycle empty state");
setCreateModal(true);
}}
<DetailedEmptyState
title={t("project_cycles.empty_state.general.title")}
description={t("project_cycles.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("project_cycles.empty_state.general.primary_button.text")}
title={t("project_cycles.empty_state.general.primary_button.comic.title")}
description={t("project_cycles.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Cycle empty state");
setCreateModal(true);
}}
disabled={!hasMemberLevelPermission}
/>
}
/>
</div>
) : (

View File

@@ -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}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={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}
@@ -138,10 +144,10 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}

View File

@@ -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 (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_INBOX}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
<DetailedEmptyState
title={t("disabled_project.empty_state.inbox.title")}
description={t("disabled_project.empty_state.inbox.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.inbox.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
/>
</div>
);

View File

@@ -6,26 +6,39 @@ 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/issue-layouts";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
import {
DisplayFiltersSelection,
FilterSelection,
FiltersDropdown,
IssueLayoutIcon,
} from "@/components/issues/issue-layouts";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
export const ProjectIssuesMobileHeader = observer(() => {
// 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={
<div className="flex flex-start text-sm text-custom-text-200">
Layout
{t("common.layout")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
}
@@ -119,18 +132,18 @@ export const ProjectIssuesMobileHeader = observer(() => {
}}
className="flex items-center gap-2"
>
<layout.icon className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title="Filters"
title={t("common.filters")}
placement="bottom-end"
menuButton={
<span className="flex items-center text-sm text-custom-text-200">
Filters
{t("common.filters")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
@@ -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(() => {
</div>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title="Display"
title={t("common.display")}
placement="bottom-end"
menuButton={
<span className="flex items-center text-sm text-custom-text-200">
Display
{t("common.display")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -181,7 +194,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
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")}
</button>
</div>
</>

View File

@@ -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(() => {
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}

View File

@@ -6,16 +6,27 @@ 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";
// plane 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/issue-layouts";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
import {
DisplayFiltersSelection,
FilterSelection,
FiltersDropdown,
IssueLayoutIcon,
} from "@/components/issues/issue-layouts";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@@ -25,10 +36,11 @@ export const ModuleIssuesMobileHeader = 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.icon className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@@ -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(() => {
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}

View File

@@ -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 (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_MODULE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
<DetailedEmptyState
title={t("disabled_project.empty_state.module.title")}
description={t("disabled_project.empty_state.module.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.module.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
/>
</div>
);

View File

@@ -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 (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_PAGE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
<DetailedEmptyState
title={t("disabled_project.empty_state.page.title")}
description={t("disabled_project.empty_state.page.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.page.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
/>
</div>
);

View File

@@ -43,8 +43,6 @@ const GeneralSettingsPage = observer(() => {
);
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
return (
<>

View File

@@ -6,7 +6,13 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { Layers, Lock } from "lucide-react";
// plane constants
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
EViewAccess,
} from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@@ -16,8 +22,6 @@ import { BreadcrumbLink, Logo } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ViewQuickActions } from "@/components/views";
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EViewAccess } from "@/constants/views";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
import { truncateText } from "@/helpers/string.helper";
@@ -242,7 +246,7 @@ export const ProjectViewIssuesHeader: 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
}
projectId={projectId.toString()}
labels={projectLabels}
@@ -255,7 +259,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}

View File

@@ -4,29 +4,37 @@ import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { EUserPermissionsLevel, EUserProjectRoles, EViewAccess } 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 +61,17 @@ const ProjectViewsPage = observer(() => {
if (currentProjectDetails?.issue_views_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_VIEW}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
<DetailedEmptyState
title={t("disabled_project.empty_state.view.title")}
description={t("disabled_project.empty_state.view.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.view.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
/>
</div>
);

View File

@@ -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(() => {
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS} />
<DetailedEmptyState
title={t("workspace_settings.empty_state.api_tokens.title")}
description={t("workspace_settings.empty_state.api_tokens.description")}
assetPath={resolvedPath}
/>
</div>
</div>
)}

View File

@@ -1,11 +1,12 @@
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { WORKSPACE_SETTINGS_LINKS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// hooks
import { useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@@ -13,6 +14,7 @@ export const MobileWorkspaceSettingsTabs = observer(() => {
const router = useAppRouter();
const { workspaceSlug } = useParams();
const pathname = usePathname();
const { t } = useTranslation();
// mobx store
const { allowPermissions } = useUserPermissions();
@@ -31,7 +33,7 @@ export const MobileWorkspaceSettingsTabs = observer(() => {
key={index}
onClick={() => router.push(`/${workspaceSlug}${item.href}`)}
>
{item.label}
{t(item.i18n_label)}
</div>
)
)}

View File

@@ -4,13 +4,14 @@ import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { WORKSPACE_SETTINGS_LINKS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { SidebarNavItem } from "@/components/sidebar";
// hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@@ -19,6 +20,7 @@ export const WorkspaceSettingsSidebar = observer(() => {
const { workspaceSlug } = useParams();
const pathname = usePathname();
// mobx store
const { t } = useTranslation();
const { allowPermissions } = useUserPermissions();
return (
@@ -36,7 +38,7 @@ export const WorkspaceSettingsSidebar = observer(() => {
isActive={link.highlight(pathname, `/${workspaceSlug}`)}
className="text-sm font-medium px-4 py-2"
>
{link.label}
{t(link.i18n_label)}
</SidebarNavItem>
</Link>
)

View File

@@ -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(() => {
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS} />
<DetailedEmptyState
title={t("workspace_settings.empty_state.webhooks.title")}
description={t("workspace_settings.empty_state.webhooks.description")}
assetPath={resolvedPath}
/>
</div>
</div>
)}

View File

@@ -3,12 +3,13 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
// components
import { PageHead } from "@/components/core";
import { AllIssueLayoutRoot, GlobalViewsAppliedFiltersRoot } from "@/components/issues";
import { GlobalViewsHeader } from "@/components/workspace";
// constants
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@/constants/workspace";
// hooks
import { useWorkspace } from "@/hooks/store";

View File

@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Layers } from "lucide-react";
// plane constants
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@@ -14,8 +14,6 @@ import { Breadcrumbs, Button, Header } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@@ -119,7 +117,7 @@ export const GlobalIssuesHeader = observer(() => {
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
@@ -130,7 +128,7 @@ export const GlobalIssuesHeader = observer(() => {
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}

View File

@@ -4,13 +4,15 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
// icons
import { Search } from "lucide-react";
// plane imports
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Input } from "@plane/ui";
// components
import { PageHead } from "@/components/core";
import { GlobalDefaultViewListItem, GlobalViewsList } from "@/components/workspace";
// constants
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@/constants/workspace";
// hooks
import { useWorkspace } from "@/hooks/store";
@@ -18,6 +20,7 @@ const WorkspaceViewsPage = observer(() => {
const [query, setQuery] = useState("");
// store
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined;
@@ -36,7 +39,7 @@ const WorkspaceViewsPage = observer(() => {
/>
</div>
<div className="flex flex-col h-full w-full vertical-scrollbar scrollbar-lg">
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map(
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => t(v.i18n_label).toLowerCase().includes(query.toLowerCase())).map(
(option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
)

View File

@@ -8,6 +8,8 @@ import Link from "next/link";
import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr";
import { CheckCircle2 } from "lucide-react";
// plane imports
import { ROLE } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import type { IWorkspaceMemberInvitation } from "@plane/types";
@@ -18,7 +20,6 @@ import { EmptyState } from "@/components/common";
// constants
import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
import { ROLE } from "@/constants/workspace";
// helpers
import { truncateText } from "@/helpers/string.helper";
import { getUserRole } from "@/helpers/user.helper";

View File

@@ -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 <EmptyState type={EmptyStateType.PROFILE_ACTIVITY} layout="screen-detailed" />;
return (
<DetailedEmptyState
title={t("profile.empty_state.activity.title")}
description={t("profile.empty_state.activity.description")}
assetPath={resolvedPath}
/>
);
}
return (

View File

@@ -25,7 +25,7 @@ export default function ProfileNotificationPage() {
return (
<>
<PageHead title={`${t("profile")} - ${t("notifications")}`} />
<PageHead title={`${t("profile.label")} - ${t("notifications")}`} />
<ProfileSettingContentWrapper>
<ProfileSettingContentHeader
title={t("email_notifications")}

View File

@@ -23,7 +23,7 @@ const ProfileSettingsPage = observer(() => {
return (
<>
<PageHead title={`${t("profile")} - ${t("general_settings")}`} />
<PageHead title={`${t("profile.label")} - ${t("general_settings")}`} />
<ProfileSettingContentWrapper>
<ProfileForm user={currentUser} profile={userProfile.data} />
</ProfileSettingContentWrapper>

View File

@@ -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 <Icon size={size} className={className} />;
};
export const ProfileLayoutSidebar = observer(() => {
// states
const [isSigningOut, setIsSigningOut] = useState(false);
@@ -145,7 +169,8 @@ export const ProfileLayoutSidebar = observer(() => {
isActive={link.highlight(pathname)}
>
<div className="flex items-center gap-1.5 py-[1px]">
<link.Icon className="size-4" />
<ProjectActionIcons type={link.key} size={16} />
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(link.key)}</p>}
</div>
</SidebarNavItem>

View File

@@ -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<IActiveCycleDetails> = 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<IActiveCycleDetails> = observer((props) =
() => (
<>
{!cycleId || !activeCycle ? (
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
<DetailedEmptyState
title={t("project_cycles.empty_state.active.title")}
description={t("project_cycles.empty_state.active.description")}
assetPath={activeCycleResolvedPath}
/>
) : (
<div className="flex flex-col border-b border-custom-border-200">
{cycleId && (

View File

@@ -2,9 +2,9 @@
import { FC } from "react";
// components
import { TActivityFilters, ACTIVITY_FILTER_TYPE_OPTIONS, TActivityFilterOption } from "@plane/constants";
import { ActivityFilter } from "@/components/issues";
// plane web constants
import { TActivityFilters, ACTIVITY_FILTER_TYPE_OPTIONS, TActivityFilterOption } from "@/plane-web/constants/issues";
export type TActivityFilterRoot = {
selectedFilters: TActivityFilters[];
@@ -20,7 +20,7 @@ export const ActivityFilterRoot: FC<TActivityFilterRoot> = (props) => {
const filterKey = key as TActivityFilters;
return {
key: filterKey,
label: value.label,
labelTranslationKey: value.labelTranslationKey,
isSelected: selectedFilters.includes(filterKey),
onClick: () => toggleFilter(filterKey),
};

View File

@@ -1,15 +1,13 @@
"use client";
import { FC } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { NETWORK_CHOICES, ETabIndices } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IProject } from "@plane/types";
// ui
import { CustomSelect } from "@plane/ui";
// components
import { MemberDropdown } from "@/components/dropdowns";
// constants
import { NETWORK_CHOICES } from "@/constants/project";
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
@@ -40,7 +38,7 @@ const ProjectAttributes: FC<Props> = (props) => {
{currentNetwork ? (
<>
<currentNetwork.icon className="h-3 w-3" />
{currentNetwork.label}
{t(currentNetwork.i18n_label)}
</>
) : (
<span className="text-custom-text-400">{t("select_network")}</span>
@@ -58,8 +56,8 @@ const ProjectAttributes: FC<Props> = (props) => {
<div className="flex items-start gap-2">
<network.icon className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{network.label}</p>
<p className="text-xs text-custom-text-400">{network.description}</p>
<p>{t(network.i18n_label)}</p>
<p className="text-xs text-custom-text-400">{t(network.description)}</p>
</div>
</div>
</CustomSelect.Option>

View File

@@ -3,6 +3,7 @@
import { useState, FC } from "react";
import { observer } from "mobx-react";
import { FormProvider, useForm } from "react-hook-form";
import { PROJECT_UNSPLASH_COVERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { setToast, TOAST_TYPE } from "@plane/ui";
@@ -11,7 +12,6 @@ import ProjectCommonAttributes from "@/components/project/create/common-attribut
import ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
import { PROJECT_CREATED } from "@/constants/event-tracker";
import { PROJECT_UNSPLASH_COVERS } from "@/constants/project";
// helpers
import { getRandomEmoji } from "@/helpers/emoji.helper";
// hooks

View File

@@ -1,7 +1,5 @@
export * from "./ai";
export * from "./estimates";
export * from "./gantt-chart";
export * from "./issues";
export * from "./project";
export * from "./user-permissions";
export * from "./workspace";
export * from "./user-permissions";

View File

@@ -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, { label: string }> = {
[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 };
} = {};

View File

@@ -1,72 +0,0 @@
// icons
import { SettingIcon } from "@/components/icons/attachment";
import { Props } from "@/components/icons/types";
import { EUserPermissions } from "./user-permissions";
// constants
export const WORKSPACE_SETTINGS = {
general: {
key: "general",
label: "General",
href: `/settings`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
Icon: SettingIcon,
},
members: {
key: "members",
label: "Members",
href: `/settings/members`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
Icon: SettingIcon,
},
"billing-and-plans": {
key: "billing-and-plans",
label: "Billing and plans",
href: `/settings/billing`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`,
Icon: SettingIcon,
},
export: {
key: "export",
label: "Exports",
href: `/settings/exports`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`,
Icon: SettingIcon,
},
webhooks: {
key: "webhooks",
label: "Webhooks",
href: `/settings/webhooks`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`,
Icon: SettingIcon,
},
"api-tokens": {
key: "api-tokens",
label: "API tokens",
href: `/settings/api-tokens`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`,
Icon: SettingIcon,
},
};
export const WORKSPACE_SETTINGS_LINKS: {
key: string;
label: string;
href: string;
access: EUserPermissions[];
highlight: (pathname: string, baseUrl: string) => boolean;
Icon: React.FC<Props>;
}[] = [
WORKSPACE_SETTINGS["general"],
WORKSPACE_SETTINGS["members"],
WORKSPACE_SETTINGS["billing-and-plans"],
WORKSPACE_SETTINGS["export"],
WORKSPACE_SETTINGS["webhooks"],
WORKSPACE_SETTINGS["api-tokens"],
];

View File

@@ -1,5 +1,5 @@
import { EViewAccess } from "@plane/constants";
import { TPublishViewSettings } from "@plane/types";
import { EViewAccess } from "@/constants/views";
import { API_BASE_URL } from "@/helpers/common.helper";
import { ViewService as CoreViewService } from "@/services/view.service";

View File

@@ -1,4 +1,4 @@
import { EViewAccess } from "@/constants/views";
import { EViewAccess } from "@plane/constants";
import { API_BASE_URL } from "@/helpers/common.helper";
import { WorkspaceService as CoreWorkspaceService } from "@/services/workspace.service";

View File

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

View File

@@ -1,9 +1,10 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { NETWORK_CHOICES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Logo } from "@/components/common";
// constants
import { NETWORK_CHOICES } from "@/constants/project";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
@@ -16,6 +17,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
const { getCycleById } = useCycle();
const { getModuleById } = useModule();
const { getUserDetails } = useMember();
const { t } = useTranslation();
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
@@ -91,7 +93,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
<div className="mt-4 space-y-4">
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Network</h6>
<span>{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ?? ""}</span>
<span>{t(NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.i18n_label ?? "")}</span>
</div>
</div>
</div>

View File

@@ -4,13 +4,14 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { ArchiveRestore } from "lucide-react";
// types
import { PROJECT_AUTOMATION_MONTHS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IProject } from "@plane/types";
// ui
import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui";
// component
import { SelectMonthModal } from "@/components/automation";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "@/constants/project";
// hooks
import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
@@ -27,6 +28,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
const [monthModal, setmonthModal] = useState(false);
// store hooks
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
const { currentProjectDetails } = useProject();
@@ -90,8 +92,8 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
<span className="text-sm">{month.label}</span>
<CustomSelect.Option key={month.i18n_label} value={month.value}>
<span className="text-sm">{t(month.i18n_label, { month: month.value })}</span>
</CustomSelect.Option>
))}

View File

@@ -5,13 +5,14 @@ import { observer } from "mobx-react";
// icons
import { ArchiveX } from "lucide-react";
// types
import { PROJECT_AUTOMATION_MONTHS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IProject } from "@plane/types";
// ui
import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui";
// component
import { SelectMonthModal } from "@/components/automation";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "@/constants/project";
// hooks
import { useProject, useProjectState, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
@@ -28,6 +29,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
// const stateGroups = projectStateStore.groupedProjectStates ?? undefined;
@@ -117,8 +119,8 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
{month.label}
<CustomSelect.Option key={month.i18n_label} value={month.value}>
{t(month.i18n_label, { month: month.value })}
</CustomSelect.Option>
))}
<button

View File

@@ -5,12 +5,11 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Check } from "lucide-react";
// plane constants
import { EIssuesStoreType } from "@plane/constants";
import { EIssuesStoreType, ISSUE_PRIORITIES } from "@plane/constants";
// plane types
import { TIssue, TIssuePriorities } from "@plane/types";
// mobx store
import { PriorityIcon } from "@plane/ui";
import { ISSUE_PRIORITIES } from "@/constants/issue";
import { useIssues } from "@/hooks/store";
// ui
// types

View File

@@ -4,13 +4,15 @@ import { Command } from "cmdk";
// hooks
import Link from "next/link";
import { useParams } from "next/navigation";
import { WORKSPACE_SETTINGS_LINKS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { SettingIcon } from "@/components/icons";
// hooks
import { useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// plane wev constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@@ -25,6 +27,7 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
// router params
const { workspaceSlug } = useParams();
// mobx store
const { t } = useTranslation();
const { allowPermissions } = useUserPermissions();
// derived values
@@ -46,8 +49,8 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
>
<Link href={`/${workspaceSlug}${setting.href}`}>
<div className="flex items-center gap-2 text-custom-text-200">
<setting.Icon className="h-4 w-4 text-custom-text-200" />
{setting.label}
<SettingIcon className="h-4 w-4 text-custom-text-200" />
{t(setting.i18n_label)}
</div>
</Link>
</Command.Item>

View File

@@ -7,9 +7,9 @@ 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 +23,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,6 +34,7 @@ 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";
@@ -47,10 +46,9 @@ const workspaceService = new WorkspaceService();
const issueService = new IssueService();
export const CommandModal: React.FC = observer(() => {
// hooks
const { workspaceProjectIds } = useProject();
const { isMobile } = usePlatformOS();
const { canPerformAnyCreateAction } = useUser();
// router
const router = useAppRouter();
const { workspaceSlug, projectId, issueId } = useParams();
// states
const [placeholder, setPlaceholder] = useState("Type a command or search...");
const [resultsCount, setResultsCount] = useState(0);
@@ -70,26 +68,25 @@ export const CommandModal: React.FC = observer(() => {
});
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const [pages, setPages] = useState<string[]>([]);
// plane hooks
const { t } = useTranslation();
// hooks
const { workspaceProjectIds } = useProject();
const { isMobile } = usePlatformOS();
const { canPerformAnyCreateAction } = useUser();
const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } =
useCommandPalette();
const { allowPermissions } = useUserPermissions();
const { setTrackElement } = useEventTracker();
// router
const router = useAppRouter();
// router params
const { workspaceSlug, projectId, issueId } = useParams();
// derived values
const page = pages[pages.length - 1];
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { baseTabIndex } = getTabIndex(undefined, isMobile);
const canPerformWorkspaceActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
// TODO: update this to mobx store
const { data: issueDetails } = useSWR(
@@ -268,7 +265,7 @@ export const CommandModal: React.FC = observer(() => {
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
<EmptyState type={EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE} layout="screen-simple" />
<SimpleEmptyState title={t("command_k.empty_state.search.title")} assetPath={resolvedPath} />
</div>
)}

View File

@@ -1,8 +1,8 @@
import { observer } from "mobx-react";
// icons
import { X } from "lucide-react";
// constants
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
// plane constants
import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { capitalizeFirstLetter } from "@/helpers/string.helper";

View File

@@ -1,10 +1,9 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
// components
// plane constants
import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants";
import { DateFilterModal } from "@/components/core";
import { FilterHeader, FilterOption } from "@/components/issues";
// constants
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
// helpers
import { isInDateFormat } from "@/helpers/date-time.helper";

View File

@@ -1,5 +1,6 @@
import { ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { cn } from "@plane/utils";
interface IContentOverflowWrapper {
@@ -31,6 +32,9 @@ export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper)
const contentRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// hooks
const { t } = useTranslation();
useEffect(() => {
if (!contentRef?.current) return;
@@ -142,7 +146,7 @@ export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper)
onClick={handleToggle}
disabled={isTransitioning}
>
{showAll ? "Show less" : "Show all"}
{showAll ? t("show_less") : t("show_all")}
</button>
)}
</div>

View File

@@ -6,19 +6,18 @@ import { useParams } from "next/navigation";
import { SubmitHandler, useForm } from "react-hook-form";
import { Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react";
// plane imports
import { EIssuesStoreType } from "@plane/constants";
// types
import { useTranslation } from "@plane/i18n";
import { ISearchIssueResponse, IUser } from "@plane/types";
// ui
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { SimpleEmptyState } from "@/components/empty-state";
// hooks
import { useIssues } from "@/hooks/store";
import useDebounce from "@/hooks/use-debounce";
// services
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { ProjectService } from "@/services/project";
// local components
import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item";
@@ -39,16 +38,19 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
const { isOpen, onClose } = props;
// router params
const { workspaceSlug, projectId } = useParams();
// hooks
const {
issues: { removeBulkIssues },
} = useIssues(EIssuesStoreType.PROJECT);
// states
const [query, setQuery] = useState("");
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
const [isSearching, setIsSearching] = useState(false);
// hooks
const {
issues: { removeBulkIssues },
} = useIssues(EIssuesStoreType.PROJECT);
const { t } = useTranslation();
// derived values
const debouncedSearchTerm: string = useDebounce(query, 500);
const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" });
useEffect(() => {
if (!isOpen || !workspaceSlug || !projectId) return;
@@ -131,12 +133,11 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
</li>
) : (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
<EmptyState
type={
query === "" ? EmptyStateType.ISSUE_RELATION_EMPTY_STATE : EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
}
layout="screen-simple"
/>
{query === "" ? (
<SimpleEmptyState title={t("issue_relation.empty_state.no_issues.title")} assetPath={issuesResolvedPath} />
) : (
<SimpleEmptyState title={t("issue_relation.empty_state.search.title")} assetPath={searchResolvedPath} />
)}
</div>
);

View File

@@ -1,10 +1,10 @@
import React from "react";
// components
// plane imports
import { useTranslation } from "@plane/i18n";
import { ISearchIssueResponse } from "@plane/types";
import { EmptyState } from "@/components/empty-state";
// types
import { EmptyStateType } from "@/constants/empty-state";
// constants
// components
import { SimpleEmptyState } from "@/components/empty-state";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
interface EmptyStateProps {
issues: ISearchIssueResponse[];
@@ -19,18 +19,28 @@ export const IssueSearchModalEmptyState: React.FC<EmptyStateProps> = ({
debouncedSearchTerm,
isSearching,
}) => {
const renderEmptyState = (type: EmptyStateType) => (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
<EmptyState type={type} layout="screen-simple" />
</div>
// plane hooks
const { t } = useTranslation();
// derived values
const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" });
const EmptyStateContainer = ({ children }: { children: React.ReactNode }) => (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">{children}</div>
);
const emptyState =
issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching
? renderEmptyState(EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE)
: issues.length === 0
? renderEmptyState(EmptyStateType.ISSUE_RELATION_EMPTY_STATE)
: null;
return emptyState;
if (issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching) {
return (
<EmptyStateContainer>
<SimpleEmptyState title={t("issue_relation.empty_state.no_issues.title")} assetPath={issuesResolvedPath} />
</EmptyStateContainer>
);
} else if (issues.length === 0) {
return (
<EmptyStateContainer>
<SimpleEmptyState title={t("issue_relation.empty_state.search.title")} assetPath={searchResolvedPath} />
</EmptyStateContainer>
);
}
return null;
};

View File

@@ -6,17 +6,16 @@ import { observer } from "mobx-react";
import { CalendarCheck } from "lucide-react";
// headless ui
import { Tab } from "@headlessui/react";
// types
// plane imports
import { EIssuesStoreType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ICycle, IIssueFilterOptions } from "@plane/types";
// ui
import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui";
// components
import { SingleProgressStats } from "@/components/core";
import { StateDropdown } from "@/components/dropdowns";
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { SimpleEmptyState } from "@/components/empty-state";
// helpers
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper";
@@ -26,6 +25,7 @@ import { useIssueDetail, useIssues } from "@/hooks/store";
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
import useLocalStorage from "@/hooks/use-local-storage";
// plane web components
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { IssueIdentifier } from "@/plane-web/components/issues";
// store
import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
@@ -41,11 +41,18 @@ export type ActiveCycleStatsProps = {
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props;
// local storage
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
// refs
const issuesContainerRef = useRef<HTMLDivElement | null>(null);
// states
const [issuesLoaderElement, setIssueLoaderElement] = useState<HTMLDivElement | null>(null);
// plane hooks
const { t } = useTranslation();
// derived values
const priorityResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/priority" });
const assigneesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/assignee" });
const labelsResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/label" });
const currentValue = (tab: string | null) => {
switch (tab) {
@@ -231,10 +238,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
</>
) : (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE}
layout="screen-simple"
size="sm"
<SimpleEmptyState
title={t("active_cycle.empty_state.priority_issue.title")}
assetPath={priorityResolvedPath}
/>
</div>
)
@@ -293,10 +299,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
})
) : (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE}
layout="screen-simple"
size="sm"
<SimpleEmptyState
title={t("active_cycle.empty_state.assignee.title")}
assetPath={assigneesResolvedPath}
/>
</div>
)
@@ -336,7 +341,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
))
) : (
<div className="flex items-center justify-center h-full w-full">
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE} layout="screen-simple" size="sm" />
<SimpleEmptyState title={t("active_cycle.empty_state.label.title")} assetPath={labelsResolvedPath} />
</div>
)
) : (

View File

@@ -1,16 +1,17 @@
import { FC, Fragment } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// plane imports
import { useTranslation } from "@plane/i18n";
import { ICycle, TCycleEstimateType } from "@plane/types";
import { Loader } from "@plane/ui";
// components
import ProgressChart from "@/components/core/sidebar/progress-chart";
import { EmptyState } from "@/components/empty-state";
import { SimpleEmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { useCycle, useProjectEstimates } from "@/hooks/store";
import { useCycle } from "@/hooks/store";
// plane web constants
import { EEstimateSystem } from "@/plane-web/constants/estimates";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { EstimateTypeDropdown } from "../dropdowns/estimate-type-dropdown";
export type ActiveCycleProductivityProps = {
@@ -21,11 +22,13 @@ export type ActiveCycleProductivityProps = {
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observer((props) => {
const { workspaceSlug, projectId, cycle } = props;
// plane hooks
const { t } = useTranslation();
// hooks
const { getEstimateTypeByCycleId, setEstimateType } = useCycle();
// derived values
const estimateType: TCycleEstimateType = (cycle && getEstimateTypeByCycleId(cycle.id)) || "issues";
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/chart" });
const onChange = async (value: TCycleEstimateType) => {
if (!workspaceSlug || !projectId || !cycle || !cycle.id) return;
@@ -95,7 +98,7 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = observe
) : (
<>
<div className="flex items-center justify-center h-full w-full">
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE} layout="screen-simple" size="sm" />
<SimpleEmptyState title={t("active_cycle.empty_state.chart.title")} assetPath={resolvedPath} />
</div>
</>
)}

View File

@@ -4,14 +4,14 @@ import { FC } from "react";
import { observer } from "mobx-react";
// plane package imports
import { PROGRESS_STATE_GROUPS_DETAILS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ICycle, IIssueFilterOptions } from "@plane/types";
import { LinearProgressIndicator, Loader } from "@plane/ui";
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { SimpleEmptyState } from "@/components/empty-state";
// hooks
import { useProjectState } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export type ActiveCycleProgressProps = {
cycle: ICycle | null;
@@ -22,9 +22,10 @@ export type ActiveCycleProgressProps = {
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props) => {
const { handleFiltersUpdate, cycle } = props;
// plane hooks
const { t } = useTranslation();
// store hooks
const { groupedProjectStates } = useProjectState();
// derived values
const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({
id: index,
@@ -40,6 +41,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props
backlog: cycle?.backlog_issues,
}
: {};
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/progress" });
return cycle && cycle.hasOwnProperty("started_issues") ? (
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
@@ -101,7 +103,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props
</div>
) : (
<div className="flex items-center justify-center h-full w-full">
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" />
<SimpleEmptyState title={t("active_cycle.empty_state.progress.title")} assetPath={resolvedPath} />
</div>
)}
</div>

View File

@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import { X } from "lucide-react";
// helpers
import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters";
import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants";
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { capitalizeFirstLetter } from "@/helpers/string.helper";
// constants

View File

@@ -2,28 +2,31 @@ import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// types
// plane imports
import { useTranslation } from "@plane/i18n";
import { TCycleFilters } from "@plane/types";
// components
import { ArchivedCyclesView, CycleAppliedFiltersList } from "@/components/cycles";
import { EmptyState } from "@/components/empty-state";
import { DetailedEmptyState } from "@/components/empty-state";
import { CycleModuleListLayout } from "@/components/ui";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useCycle, useCycleFilter } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ArchivedCycleLayoutRoot: React.FC = observer(() => {
// router
const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// hooks
const { fetchArchivedCycles, currentProjectArchivedCycleIds, loader } = useCycle();
// cycle filters hook
const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useCycleFilter();
// derived values
const totalArchivedCycles = currentProjectArchivedCycleIds?.length ?? 0;
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-cycles" });
useSWR(
workspaceSlug && projectId ? `ARCHIVED_CYCLES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
@@ -64,7 +67,11 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => {
)}
{totalArchivedCycles === 0 ? (
<div className="h-full place-items-center">
<EmptyState type={EmptyStateType.PROJECT_ARCHIVED_NO_CYCLES} />
<DetailedEmptyState
title={t("project_cycles.empty_state.archived.title")}
description={t("project_cycles.empty_state.archived.description")}
assetPath={resolvedPath}
/>
</div>
) : (
<div className="relative h-full w-full overflow-auto">

View File

@@ -4,12 +4,13 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams, useSearchParams } from "next/navigation";
// types
import { PROJECT_ERROR_MESSAGES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ICycle } from "@plane/types";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
import { CYCLE_DELETED } from "@/constants/event-tracker";
import { PROJECT_ERROR_MESSAGES } from "@/constants/project";
// hooks
import { useEventTracker, useCycle } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -29,6 +30,7 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
// store hooks
const { captureCycleEvent } = useEventTracker();
const { deleteCycle } = useCycle();
const { t } = useTranslation();
// router
const router = useAppRouter();
const { cycleId } = useParams();
@@ -59,9 +61,9 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
? PROJECT_ERROR_MESSAGES.permissionError
: PROJECT_ERROR_MESSAGES.cycleDeleteError;
setToast({
title: currentError.title,
title: t(currentError.i18n_title),
type: TOAST_TYPE.ERROR,
message: currentError.message,
message: currentError.i18n_message && t(currentError.i18n_message),
});
captureCycleEvent({
eventName: CYCLE_DELETED,

View File

@@ -1,11 +1,11 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
// constants
import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants";
// components
import { DateFilterModal } from "@/components/core";
import { FilterHeader, FilterOption } from "@/components/issues";
// constants
import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters";
// helpers
import { isInDateFormat } from "@/helpers/date-time.helper";

View File

@@ -1,11 +1,11 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
// constants
import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants";
// components
import { DateFilterModal } from "@/components/core";
import { FilterHeader, FilterOption } from "@/components/issues";
// constants
import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters";
// helpers
import { isInDateFormat } from "@/helpers/date-time.helper";
@@ -17,7 +17,6 @@ type Props = {
export const FilterStartDate: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);

View File

@@ -2,6 +2,8 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// plane imports
import { ETabIndices } from "@plane/constants";
// types
import { ICycle } from "@plane/types";
// ui
@@ -9,7 +11,6 @@ import { Button, Input, TextArea } from "@plane/ui";
// components
import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldRenderProject } from "@/helpers/project.helper";

View File

@@ -2,13 +2,15 @@ import { useCallback, useMemo } from "react";
import { observer } from "mobx-react";
import { Check } from "lucide-react";
// plane constants
import { EIssueLayoutTypes } from "@plane/constants";
import { EIssueLayoutTypes, ISSUE_LAYOUT_MAP } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
// plane ui
import { Dropdown } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// constants
import { ISSUE_LAYOUT_MAP } from "@/constants/issue";
// components
import { IssueLayoutIcon } from "@/components/issues";
type TLayoutDropDown = {
onChange: (value: EIssueLayoutTypes) => void;
@@ -18,6 +20,8 @@ type TLayoutDropDown = {
export const LayoutDropDown = observer((props: TLayoutDropDown) => {
const { onChange, value = EIssueLayoutTypes.LIST, disabledLayouts = [] } = props;
// plane i18n
const { t } = useTranslation();
// derived values
const availableLayouts = useMemo(
() => Object.values(ISSUE_LAYOUT_MAP).filter((layout) => !disabledLayouts.includes(layout.key)),
@@ -35,11 +39,10 @@ export const LayoutDropDown = observer((props: TLayoutDropDown) => {
const buttonContent = useCallback((isOpen: boolean, buttonValue: string | string[] | undefined) => {
const dropdownValue = ISSUE_LAYOUT_MAP[buttonValue as EIssueLayoutTypes];
return (
<div className="flex gap-2 items-center text-custom-text-200">
<dropdownValue.icon strokeWidth={2} className={`size-3.5 text-custom-text-200`} />
<span className="font-medium text-xs">{dropdownValue.label}</span>
<IssueLayoutIcon layout={dropdownValue.key} strokeWidth={2} className={`size-3.5 text-custom-text-200`} />
<span className="font-medium text-xs">{t(dropdownValue.i18n_label)}</span>
</div>
);
}, []);
@@ -50,8 +53,8 @@ export const LayoutDropDown = observer((props: TLayoutDropDown) => {
return (
<div className={cn("flex gap-2 items-center text-custom-text-200 w-full justify-between")}>
<div className="flex gap-2 items-center">
<dropdownValue.icon strokeWidth={2} className={`size-3 text-custom-text-200`} />
<span className="font-medium text-xs">{dropdownValue.label}</span>
<IssueLayoutIcon layout={dropdownValue.key} strokeWidth={2} className={`size-3 text-custom-text-200`} />
<span className="font-medium text-xs">{t(dropdownValue.i18n_label)}</span>
</div>
{props.selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</div>

View File

@@ -5,13 +5,12 @@ import { useTheme } from "next-themes";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search, SignalHigh } from "lucide-react";
import { Combobox } from "@headlessui/react";
import { ISSUE_PRIORITIES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { TIssuePriorities } from "@plane/types";
// ui
import { ComboDropDown, PriorityIcon, Tooltip } from "@plane/ui";
// constants
import { ISSUE_PRIORITIES } from "@/constants/issue";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@@ -77,7 +76,7 @@ const BorderButton = (props: ButtonProps) => {
return (
<Tooltip
tooltipHeading={t("priority")}
tooltipContent={t(priorityDetails?.key ?? "none")}
tooltipContent={priorityDetails?.title ?? t("common.none")}
disabled={!showTooltip}
isMobile={isMobile}
renderByDefault={renderToolTipByDefault}
@@ -121,7 +120,7 @@ const BorderButton = (props: ButtonProps) => {
) : (
<SignalHigh className="size-3" />
))}
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
@@ -204,7 +203,9 @@ const BackgroundButton = (props: ButtonProps) => {
) : (
<SignalHigh className="size-3" />
))}
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
{!hideText && (
<span className="flex-grow truncate">{priorityDetails?.title ?? t("common.priority") ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
@@ -244,7 +245,7 @@ const TransparentButton = (props: ButtonProps) => {
return (
<Tooltip
tooltipHeading={t("priority")}
tooltipContent={t(priorityDetails?.key ?? "none")}
tooltipContent={priorityDetails?.title ?? t("common.none")}
disabled={!showTooltip}
isMobile={isMobile}
renderByDefault={renderToolTipByDefault}
@@ -289,7 +290,9 @@ const TransparentButton = (props: ButtonProps) => {
) : (
<SignalHigh className="size-3" />
))}
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
{!hideText && (
<span className="flex-grow truncate">{priorityDetails?.title ?? t("common.priority") ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
@@ -299,6 +302,8 @@ const TransparentButton = (props: ButtonProps) => {
};
export const PriorityDropdown: React.FC<Props> = (props) => {
//hooks
const { t } = useTranslation();
const {
button,
buttonClassName,
@@ -312,7 +317,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
highlightUrgent = true,
onChange,
onClose,
placeholder = "Priority",
placeholder = t("common.priority"),
placement,
showTooltip = false,
tabIndex,
@@ -340,8 +345,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
},
],
});
//hooks
const { t } = useTranslation();
// next-themes
// TODO: remove this after new theming implementation
const { resolvedTheme } = useTheme();
@@ -352,7 +356,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
content: (
<div className="flex items-center gap-2">
<PriorityIcon priority={priority.key} size={14} withContainer />
<span className="flex-grow truncate">{t(priority.key)}</span>
<span className="flex-grow truncate">{priority.title}</span>
</div>
),
}));

View File

@@ -3,10 +3,11 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
// plane imports
import { ROLE } from "@plane/constants";
// plane ui
import { Avatar } from "@plane/ui";
// constants
import { ROLE } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
import { getFileURL } from "@/helpers/file.helper";

View File

@@ -1,194 +0,0 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
// hooks
// components
import { Button, TButtonSizes, TButtonVariant } from "@plane/ui";
// constant
import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state";
// helpers
import { cn } from "@/helpers/common.helper";
import { useUserPermissions } from "@/hooks/store";
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { ComicBoxButton } from "./comic-box-button";
export type EmptyStateProps = {
size?: TButtonSizes;
type: EmptyStateType;
layout?: "screen-detailed" | "screen-simple";
additionalPath?: string;
primaryButtonConfig?: {
size?: TButtonSizes;
variant?: TButtonVariant;
};
primaryButtonOnClick?: () => void;
primaryButtonLink?: string;
secondaryButtonOnClick?: () => void;
};
export const EmptyState: React.FC<EmptyStateProps> = observer((props) => {
const {
size = "lg",
type,
layout = "screen-detailed",
additionalPath = "",
primaryButtonConfig = {
size: "lg",
variant: "primary",
},
primaryButtonOnClick,
primaryButtonLink,
secondaryButtonOnClick,
} = props;
// store
const { allowPermissions } = useUserPermissions();
// theme
const { resolvedTheme } = useTheme();
// if empty state type is not found
if (!EMPTY_STATE_DETAILS[type]) return null;
// current empty state details
const { key, title, description, path, primaryButton, secondaryButton, accessType, access } =
EMPTY_STATE_DETAILS[type];
// resolved empty state path
const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${
resolvedTheme === "light" ? "light" : "dark"
}.webp`;
// permission
const isEditingAllowed =
access &&
accessType &&
allowPermissions(
access,
accessType === "workspace" ? EUserPermissionsLevel.WORKSPACE : EUserPermissionsLevel.PROJECT
);
const anyButton = primaryButton || secondaryButton;
// primary button
const renderPrimaryButton = () => {
if (!primaryButton) return null;
const commonProps = {
size: primaryButtonConfig.size,
variant: primaryButtonConfig.variant,
prependIcon: primaryButton.icon,
onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined,
disabled: !isEditingAllowed,
};
if (primaryButton.comicBox) {
return (
<ComicBoxButton
label={primaryButton.text}
icon={primaryButton.icon}
title={primaryButton.comicBox?.title}
description={primaryButton.comicBox?.description}
onClick={primaryButtonOnClick}
disabled={!isEditingAllowed}
/>
);
} else if (primaryButtonLink) {
return (
<Link href={primaryButtonLink}>
<Button {...commonProps}>{primaryButton.text}</Button>
</Link>
);
} else {
return <Button {...commonProps}>{primaryButton.text}</Button>;
}
};
// secondary button
const renderSecondaryButton = () => {
if (!secondaryButton) return null;
return (
<Button
size={size}
variant="neutral-primary"
prependIcon={secondaryButton.icon}
onClick={secondaryButtonOnClick}
disabled={!isEditingAllowed}
>
{secondaryButton.text}
</Button>
);
};
return (
<>
{layout === "screen-detailed" && (
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
<div
className={cn("flex flex-col gap-5", {
"md:min-w-[24rem] max-w-[45rem]": size === "sm",
"md:min-w-[30rem] max-w-[60rem]": size === "lg",
})}
>
<div className="flex flex-col gap-1.5 flex-shrink">
{description ? (
<>
<h3 className="text-xl font-semibold">{title}</h3>
<p className="text-sm">{description}</p>
</>
) : (
<h3 className="text-xl font-medium">{title}</h3>
)}
</div>
{path && (
<Image
src={resolvedEmptyStatePath}
alt={key || "button image"}
width={384}
height={250}
layout="responsive"
lazyBoundary="100%"
/>
)}
{anyButton && (
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
{renderPrimaryButton()}
{renderSecondaryButton()}
</div>
)}
</div>
</div>
)}
{layout === "screen-simple" && (
<div className="text-center flex flex-col gap-2.5 items-center">
<div className={`${size === "sm" ? "h-24 w-24" : "h-28 w-28"}`}>
<Image
src={resolvedEmptyStatePath}
alt={key || "button image"}
width={size === "sm" ? 78 : 96}
height={size === "sm" ? 78 : 96}
layout="responsive"
lazyBoundary="100%"
/>
</div>
{description ? (
<>
<h3 className="text-lg font-medium text-custom-text-300 whitespace-pre-line">{title}</h3>
<p className="text-base font-medium text-custom-text-400 whitespace-pre-line">{description}</p>
</>
) : (
<h3 className="text-sm font-medium text-custom-text-400 whitespace-pre-line">{title}</h3>
)}
{anyButton && (
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
{renderPrimaryButton()}
{renderSecondaryButton()}
</div>
)}
</div>
)}
</>
);
});

View File

@@ -1,4 +1,3 @@
export * from "./empty-state";
export * from "./helper";
export * from "./comic-box-button";
export * from "./detailed-empty-state-root";

View File

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

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