feat: language support (#6472)

* chore: ln support modules constants

* fix: translation key

* chore: empty state refactor (#6404)

* chore: asset path helper hook added

* chore: detailed and simple empty state component added

* chore: section empty state component added

* chore: language translation for all empty states

* chore: new empty state implementation

* improvement: add more translations

* improvement: user permissions and workspace draft empty state

* chore: update translation structure

* chore: inbox empty states

* chore: disabled project features empty state

* chore: active cycle progress empty state

* chore: notification empty state

* chore: connections translation

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

* chore: project pages empty state and translations

* chore: project module and view related empty state

* chore: remove project draft related empty state

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

* chore: project cycles related empty state

* chore: project settings empty state

* chore: profile issue and acitivity empty state

* chore: workspace settings realted constants

* chore: stickies and home widgets empty state

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

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

* chore: minor updates

* fix: build errors

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* fix: language support fo profile (#6461)

* fix: ln support fo profile

* fix: merge changes

* fix: merge changes

* [WEB-3165]feat: language support for issues (#6452)

* * chore: moved issue constants to packages
* chore: restructured issue constants
* improvement: added translations to issue constants

* chore: updated translation structure

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

* chore: updated spanish translation

* chore: removed translation for issue priorities

* fix: build errors

* chore: minor updates

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* chore: migrated filters.ts to packages (#6459)

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* chore: workspace drafts constant moved to plane constant package

* feat: home language support without stickies (#6443)

* feat: home language support without stickies

* fix: home sidebar

* fix: added missing keys

* fix: show all btn

* fix: recents empty state

* chore: translation update

* feat: workspace constant language support and refactor (#6462)

* chore: workspace constant language support and refactor

* chore: workspace constant language support and refactor

* chore: code refactor

* chore: code refactor

* merge conflict

* chore: code refactor

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* chore: tab indices constant moved to plane package (#6464)

* chore: notification language support and refactor

* chore: ln support for inbox constants (#6432)

* chore: ln support for inbox constants

* fix: snooze duration

* fix: enum

* fix: translation keys

* fix: inbox status icon

* fix: status icon

* fix: naming

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* fix: ln support for views constants (#6431)

* fix: ln support for views constants

* fix: added translation

* fix: translation keys

* fix: access

* chore: code refactor

* chore: ln support workspace projects constants (#6429)

* chore: ln support workspace projects constants

* fix: translation key

* fix: removed state translation

* fix: removed state translation

* fi: added translations

* Chore: theme language support and refactor (#6465)

* chore: themes language support and refactor

* chore: theme language support and refactor

* fix

* [WEB-3173] chore: language support for cycles constant file (#6415)

* chore: ln support for cycles constant file

* fix: added chinese

* fix: lint

* fix: translation key

* fix: build errors

* minor updates

* chore: minor translation update

* chore: minor translation update

* refactor: move labels contants to packages

* refactor: move swr, file and error related constants to packages

* chore: timezones constant moved to plane package

* chore: metadata constant code refactor

* chore: code refactor

* fix: dashboard constants moved

* chore: code refactor (#6478)

* refactor: spreadsheet constants

* chore: drafts language support (#6485)

* chore: workspace drafts language support

* chore: code refactor

* feat: ln support for notifications (#6486)

* feat: ln support for notifications

* fix: translations

* * refactor: moved page constants to packages (#6480)

* fix: removed use-client

* chore: removed unnecessary commnets

* chore: workspace draft language support (#6490)

* chore: workspace drafts language support

* chore: code refactor

* chore: draft language support

* Feat constant event tracker (#6479)

* fix: event tracjer constants

* fix: constants event tracker

* feat: language translation  - projects list (#6493)

* feat: added translation to projects list page

* chore: restructured translation file

* chore: module language support (#6499)

* chore: module language support added

* chore: code refactor

* chore: workspace views language support (#6492)

* chore: workspace views language support

* chore: code refactor

* feat: custom analytics language support (#6494)

* feat: custom analytics language support

* fix: key

* fix: refactoring

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* chore: minor improvements

* feat: language support for intake (#6498)

* feat: language support for intake

* fix: key name

* refactor: authentications related translations

* feat: language support issues  (#6501)

* enhancement: added translations for issue list view

* chore: added translations for issue detail widgets

* chore: added missing translations

* chore: modified issue to work items

* chore: updated translations

* Feat: workspace settings language support (#6508)

* feat: language support for workspace settings

* fix: lint

* fix: export title

* chore project settings language support (#6502)

* chore: project settings language support

* chore: code refactor

* refactor: workspace creation related translations

* chore: renamed issues to work items

* fix: build errors

* fix: lint

* chore: modified translations

* chore: remove duplicate

* improvement: french translation

* chore: chinese translation improvement

* fix: japanese translations

* chore: added spanish translation

* minor improvements

* fix: miscelleous language translations

* fix: clear_all key

* fix: moved user permission constants (#6516)

* feat: language support for  issues (#6513)

* chore: added language support to issue detail widgets

* improvement: added translation for issue detail

* enhancement: added language trasnlation to issue layouts

* chore: translation improvement (#6518)

* feat: language support description (#6519)

* enhancement: added language support for description

* fix: updated keys

* chore: renamed issue to work item (#6522)

* chore: replace missing issue occurances to work items

* fix: build errors

* minor improvements

* fix: profile links

* Feat ln cycles (#6528)

* feat: added language support for cycles

* feat: added language support for cycles

* chore: added core.json

* fix: translation keys

* fix: translation keys (#6530)

* fix: changed sidebar keys

* fix: removed extras

* fix: updated keys

* chore: optimize translation imports

* fix: updated keys (#6534)

* fix: updated keys

* fix-sub work items toasts

* chore: add missing translation and minor fixes

* chore: code refactor

* fix: language support keys (#6553)

* minor improvements

* minor fixes

* fix: remove lucide import from constants package

* chore: regenerate all translations

* chore: addded chinese and japanese translation files

* chore: remove all  from translations

* fix: added member

* fix: language support keys (#6558)

* fix: renamed keys

* fix: space app

* chore: renamed issues to work items

* chore: update site manifest

* chore: updated translations

* fix: lang keys

* chore: update translations

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
Co-authored-by: Vamsi krishna <matalav55@gmail.com>
Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com>
This commit is contained in:
Prateek Shourya
2025-02-06 20:41:31 +05:30
committed by GitHub
parent e244f48776
commit d36c3acbf7
693 changed files with 18182 additions and 10485 deletions

View File

@@ -7,15 +7,15 @@ import { DefaultLayout } from "@/layouts/default-layout";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Plane | Simple, extensible, open-source project management tool.", title: "Plane | Simple, extensible, open-source project management tool.",
description: description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.", "Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
openGraph: { openGraph: {
title: "Plane | Simple, extensible, open-source project management tool.", title: "Plane | Simple, extensible, open-source project management tool.",
description: description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.", "Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
url: "https://plane.so/", url: "https://plane.so/",
}, },
keywords: keywords:
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration", "software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
twitter: { twitter: {
site: "@planepowers", site: "@planepowers",
}, },

View File

@@ -2,8 +2,11 @@
import { TXAxisValues, TYAxisValues } from "@plane/types"; import { TXAxisValues, TYAxisValues } from "@plane/types";
export const ANALYTICS_TABS = [ export const ANALYTICS_TABS = [
{ key: "scope_and_demand", title: "Scope and Demand" }, {
{ key: "custom", title: "Custom Analytics" }, key: "scope_and_demand",
i18n_title: "workspace_analytics.tabs.scope_and_demand",
},
{ key: "custom", i18n_title: "workspace_analytics.tabs.custom" },
]; ];
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] = export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
@@ -62,7 +65,7 @@ export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
[ [
{ {
value: "issue_count", value: "issue_count",
label: "Issue Count", label: "Work item Count",
}, },
{ {
value: "estimate", value: "estimate",

View File

@@ -1,56 +1,40 @@
// types // types
import { TCycleLayoutOptions, TCycleTabOptions } from "@plane/types";
export const CYCLE_TABS_LIST: {
key: TCycleTabOptions;
name: string;
}[] = [
{
key: "active",
name: "Active",
},
{
key: "all",
name: "All",
},
];
export const CYCLE_STATUS: { export const CYCLE_STATUS: {
label: string; i18n_label: string;
value: "current" | "upcoming" | "completed" | "draft"; value: "current" | "upcoming" | "completed" | "draft";
title: string; i18n_title: string;
color: string; color: string;
textColor: string; textColor: string;
bgColor: string; bgColor: string;
}[] = [ }[] = [
{ {
label: "day left", i18n_label: "project_cycles.status.days_left",
value: "current", value: "current",
title: "In progress", i18n_title: "project_cycles.status.in_progress",
color: "#F59E0B", color: "#F59E0B",
textColor: "text-amber-500", textColor: "text-amber-500",
bgColor: "bg-amber-50", bgColor: "bg-amber-50",
}, },
{ {
label: "Yet to start", i18n_label: "project_cycles.status.yet_to_start",
value: "upcoming", value: "upcoming",
title: "Yet to start", i18n_title: "project_cycles.status.yet_to_start",
color: "#3F76FF", color: "#3F76FF",
textColor: "text-blue-500", textColor: "text-blue-500",
bgColor: "bg-indigo-50", bgColor: "bg-indigo-50",
}, },
{ {
label: "Completed", i18n_label: "project_cycles.status.completed",
value: "completed", value: "completed",
title: "Completed", i18n_title: "project_cycles.status.completed",
color: "#16A34A", color: "#16A34A",
textColor: "text-green-600", textColor: "text-green-600",
bgColor: "bg-green-50", bgColor: "bg-green-50",
}, },
{ {
label: "Draft", i18n_label: "project_cycles.status.draft",
value: "draft", value: "draft",
title: "Draft", i18n_title: "project_cycles.status.draft",
color: "#525252", color: "#525252",
textColor: "text-custom-text-300", textColor: "text-custom-text-300",
bgColor: "bg-custom-background-90", bgColor: "bg-custom-background-90",

View File

@@ -0,0 +1,92 @@
// types
import { TIssuesListTypes } from "@plane/types";
export enum EDurationFilters {
NONE = "none",
TODAY = "today",
THIS_WEEK = "this_week",
THIS_MONTH = "this_month",
THIS_YEAR = "this_year",
CUSTOM = "custom",
}
// filter duration options
export const DURATION_FILTER_OPTIONS: {
key: EDurationFilters;
label: string;
}[] = [
{
key: EDurationFilters.NONE,
label: "All time",
},
{
key: EDurationFilters.TODAY,
label: "Due today",
},
{
key: EDurationFilters.THIS_WEEK,
label: "Due this week",
},
{
key: EDurationFilters.THIS_MONTH,
label: "Due this month",
},
{
key: EDurationFilters.THIS_YEAR,
label: "Due this year",
},
{
key: EDurationFilters.CUSTOM,
label: "Custom",
},
];
// random background colors for project cards
export const PROJECT_BACKGROUND_COLORS = [
"bg-gray-500/20",
"bg-green-500/20",
"bg-red-500/20",
"bg-orange-500/20",
"bg-blue-500/20",
"bg-yellow-500/20",
"bg-pink-500/20",
"bg-purple-500/20",
];
// assigned and created issues widgets tabs list
export const FILTERED_ISSUES_TABS_LIST: {
key: TIssuesListTypes;
label: string;
}[] = [
{
key: "upcoming",
label: "Upcoming",
},
{
key: "overdue",
label: "Overdue",
},
{
key: "completed",
label: "Marked completed",
},
];
// assigned and created issues widgets tabs list
export const UNFILTERED_ISSUES_TABS_LIST: {
key: TIssuesListTypes;
label: string;
}[] = [
{
key: "pending",
label: "Pending",
},
{
key: "completed",
label: "Marked completed",
},
];
export type TLinkOptions = {
userId: string | undefined;
};

View File

@@ -1,5 +0,0 @@
export enum E_ARCHIVE_ERROR_CODES {
"INVALID_ARCHIVE_STATE_GROUP" = 4091,
"INVALID_ISSUE_START_DATE" = 4101,
"INVALID_ISSUE_TARGET_DATE" = 4102,
}

View File

@@ -104,7 +104,10 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
module_id: payload.module_id, module_id: payload.module_id,
archived_at: payload.archived_at, archived_at: payload.archived_at,
state: payload.state, state: payload.state,
view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "", view_id:
path?.includes("workspace-views") || path?.includes("views")
? path.split("/").pop()
: "",
}; };
if (eventName === ISSUE_UPDATED) { if (eventName === ISSUE_UPDATED) {
@@ -166,12 +169,12 @@ export const MODULE_LINK_CREATED = "Module link created";
export const MODULE_LINK_UPDATED = "Module link updated"; export const MODULE_LINK_UPDATED = "Module link updated";
export const MODULE_LINK_DELETED = "Module link deleted"; export const MODULE_LINK_DELETED = "Module link deleted";
// Issue Events // Issue Events
export const ISSUE_CREATED = "Issue created"; export const ISSUE_CREATED = "Work item created";
export const ISSUE_UPDATED = "Issue updated"; export const ISSUE_UPDATED = "Work item updated";
export const ISSUE_DELETED = "Issue deleted"; export const ISSUE_DELETED = "Work item deleted";
export const ISSUE_ARCHIVED = "Issue archived"; export const ISSUE_ARCHIVED = "Work item archived";
export const ISSUE_RESTORED = "Issue restored"; export const ISSUE_RESTORED = "Work item restored";
export const ISSUE_OPENED = "Issue opened"; export const ISSUE_OPENED = "Work item opened";
// Project State Events // Project State Events
export const STATE_CREATED = "State created"; export const STATE_CREATED = "State created";
export const STATE_UPDATED = "State updated"; export const STATE_UPDATED = "State updated";

View File

@@ -1 +0,0 @@
export const SIDEBAR_CLICKED = "Sidenav clicked";

View File

@@ -2,3 +2,56 @@ export enum E_SORT_ORDER {
ASC = "asc", ASC = "asc",
DESC = "desc", 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

@@ -2,15 +2,29 @@ export * from "./ai";
export * from "./analytics"; export * from "./analytics";
export * from "./auth"; export * from "./auth";
export * from "./endpoints"; export * from "./endpoints";
export * from "./event";
export * from "./file"; export * from "./file";
export * from "./filter"; export * from "./filter";
export * from "./graph"; export * from "./graph";
export * from "./instance"; export * from "./instance";
export * from "./issue"; export * from "./issue";
export * from "./metadata"; export * from "./metadata";
export * from "./notification";
export * from "./state"; export * from "./state";
export * from "./swr"; export * from "./swr";
export * from "./tab-indices";
export * from "./user"; export * from "./user";
export * from "./workspace"; export * from "./workspace";
export * from "./stickies"; export * from "./stickies";
export * from "./cycle";
export * from "./module";
export * from "./project";
export * from "./views";
export * from "./themes";
export * from "./inbox";
export * from "./profile";
export * from "./workspace-drafts";
export * from "./label";
export * from "./event-tracker";
export * from "./spreadsheet";
export * from "./dashboard";
export * from "./page";

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

@@ -3,9 +3,9 @@ export const SITE_NAME =
export const SITE_TITLE = export const SITE_TITLE =
"Plane | Simple, extensible, open-source project management tool."; "Plane | Simple, extensible, open-source project management tool.";
export const SITE_DESCRIPTION = export const SITE_DESCRIPTION =
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind."; "Open-source project management tool to manage work items, cycles, and product roadmaps easily";
export const SITE_KEYWORDS = export const SITE_KEYWORDS =
"software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration"; "software development, plan, ship, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration";
export const SITE_URL = "https://app.plane.so/"; export const SITE_URL = "https://app.plane.so/";
export const TWITTER_USER_NAME = export const TWITTER_USER_NAME =
"Plane | Simple, extensible, open-source project management tool."; "Plane | Simple, extensible, open-source project management tool.";
@@ -18,6 +18,6 @@ export const SPACE_SITE_TITLE =
export const SPACE_SITE_DESCRIPTION = export const SPACE_SITE_DESCRIPTION =
"Plane Publish is a customer feedback management tool built on top of plane.so"; "Plane Publish is a customer feedback management tool built on top of plane.so";
export const SPACE_SITE_KEYWORDS = export const SPACE_SITE_KEYWORDS =
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration"; "software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration";
export const SPACE_SITE_URL = "https://app.plane.so/"; export const SPACE_SITE_URL = "https://app.plane.so/";
export const SPACE_TWITTER_USER_NAME = "planepowers"; export const SPACE_TWITTER_USER_NAME = "planepowers";

View File

@@ -1,51 +1,54 @@
import { GanttChartSquare, LayoutGrid, List } from "lucide-react";
// types // types
import { TModuleLayoutOptions, TModuleOrderByOptions, TModuleStatus } from "@plane/types"; import {
TModuleLayoutOptions,
TModuleOrderByOptions,
TModuleStatus,
} from "@plane/types";
export const MODULE_STATUS: { export const MODULE_STATUS: {
label: string; i18n_label: string;
value: TModuleStatus; value: TModuleStatus;
color: string; color: string;
textColor: string; textColor: string;
bgColor: string; bgColor: string;
}[] = [ }[] = [
{ {
label: "Backlog", i18n_label: "project_modules.status.backlog",
value: "backlog", value: "backlog",
color: "#a3a3a2", color: "#a3a3a2",
textColor: "text-custom-text-400", textColor: "text-custom-text-400",
bgColor: "bg-custom-background-80", bgColor: "bg-custom-background-80",
}, },
{ {
label: "Planned", i18n_label: "project_modules.status.planned",
value: "planned", value: "planned",
color: "#3f76ff", color: "#3f76ff",
textColor: "text-blue-500", textColor: "text-blue-500",
bgColor: "bg-indigo-50", bgColor: "bg-indigo-50",
}, },
{ {
label: "In Progress", i18n_label: "project_modules.status.in_progress",
value: "in-progress", value: "in-progress",
color: "#f39e1f", color: "#f39e1f",
textColor: "text-amber-500", textColor: "text-amber-500",
bgColor: "bg-amber-50", bgColor: "bg-amber-50",
}, },
{ {
label: "Paused", i18n_label: "project_modules.status.paused",
value: "paused", value: "paused",
color: "#525252", color: "#525252",
textColor: "text-custom-text-300", textColor: "text-custom-text-300",
bgColor: "bg-custom-background-90", bgColor: "bg-custom-background-90",
}, },
{ {
label: "Completed", i18n_label: "project_modules.status.completed",
value: "completed", value: "completed",
color: "#16a34a", color: "#16a34a",
textColor: "text-green-600", textColor: "text-green-600",
bgColor: "bg-green-100", bgColor: "bg-green-100",
}, },
{ {
label: "Cancelled", i18n_label: "project_modules.status.cancelled",
value: "cancelled", value: "cancelled",
color: "#ef4444", color: "#ef4444",
textColor: "text-red-500", textColor: "text-red-500",
@@ -53,47 +56,50 @@ export const MODULE_STATUS: {
}, },
]; ];
export const MODULE_VIEW_LAYOUTS: { key: TModuleLayoutOptions; icon: any; title: string }[] = [ export const MODULE_VIEW_LAYOUTS: {
key: TModuleLayoutOptions;
i18n_title: string;
}[] = [
{ {
key: "list", key: "list",
icon: List, i18n_title: "project_modules.layout.list",
title: "List layout",
}, },
{ {
key: "board", key: "board",
icon: LayoutGrid, i18n_title: "project_modules.layout.board",
title: "Gallery layout",
}, },
{ {
key: "gantt", key: "gantt",
icon: GanttChartSquare, i18n_title: "project_modules.layout.timeline",
title: "Timeline layout",
}, },
]; ];
export const MODULE_ORDER_BY_OPTIONS: { key: TModuleOrderByOptions; label: string }[] = [ export const MODULE_ORDER_BY_OPTIONS: {
key: TModuleOrderByOptions;
i18n_label: string;
}[] = [
{ {
key: "name", key: "name",
label: "Name", i18n_label: "project_modules.order_by.name",
}, },
{ {
key: "progress", key: "progress",
label: "Progress", i18n_label: "project_modules.order_by.progress",
}, },
{ {
key: "issues_length", key: "issues_length",
label: "Number of issues", i18n_label: "project_modules.order_by.issues",
}, },
{ {
key: "target_date", key: "target_date",
label: "Due date", i18n_label: "project_modules.order_by.due_date",
}, },
{ {
key: "created_at", key: "created_at",
label: "Created date", i18n_label: "project_modules.order_by.created_at",
}, },
{ {
key: "sort_order", key: "sort_order",
label: "Manual", i18n_label: "project_modules.order_by.manual",
}, },
]; ];

View File

@@ -29,12 +29,13 @@ export type TNotificationTab = ENotificationTab.ALL | ENotificationTab.MENTIONS;
export const NOTIFICATION_TABS = [ export const NOTIFICATION_TABS = [
{ {
label: "All", i18n_label: "notification.tabs.all",
value: ENotificationTab.ALL, value: ENotificationTab.ALL,
count: (unReadNotification: TUnreadNotificationsCount) => unReadNotification?.total_unread_notifications_count || 0, count: (unReadNotification: TUnreadNotificationsCount) =>
unReadNotification?.total_unread_notifications_count || 0,
}, },
{ {
label: "Mentions", i18n_label: "notification.tabs.mentions",
value: ENotificationTab.MENTIONS, value: ENotificationTab.MENTIONS,
count: (unReadNotification: TUnreadNotificationsCount) => count: (unReadNotification: TUnreadNotificationsCount) =>
unReadNotification?.mention_unread_notifications_count || 0, unReadNotification?.mention_unread_notifications_count || 0,
@@ -43,15 +44,15 @@ export const NOTIFICATION_TABS = [
export const FILTER_TYPE_OPTIONS = [ export const FILTER_TYPE_OPTIONS = [
{ {
label: "Assigned to me", i18n_label: "notification.filter.assigned",
value: ENotificationFilterType.ASSIGNED, value: ENotificationFilterType.ASSIGNED,
}, },
{ {
label: "Created by me", i18n_label: "notification.filter.created",
value: ENotificationFilterType.CREATED, value: ENotificationFilterType.CREATED,
}, },
{ {
label: "Subscribed by me", i18n_label: "notification.filter.subscribed",
value: ENotificationFilterType.SUBSCRIBED, value: ENotificationFilterType.SUBSCRIBED,
}, },
]; ];
@@ -59,7 +60,7 @@ export const FILTER_TYPE_OPTIONS = [
export const NOTIFICATION_SNOOZE_OPTIONS = [ export const NOTIFICATION_SNOOZE_OPTIONS = [
{ {
key: "1_day", key: "1_day",
label: "1 day", i18n_label: "notification.snooze.1_day",
value: () => { value: () => {
const date = new Date(); const date = new Date();
return new Date(date.getTime() + 24 * 60 * 60 * 1000); return new Date(date.getTime() + 24 * 60 * 60 * 1000);
@@ -67,7 +68,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
}, },
{ {
key: "3_days", key: "3_days",
label: "3 days", i18n_label: "notification.snooze.3_days",
value: () => { value: () => {
const date = new Date(); const date = new Date();
return new Date(date.getTime() + 3 * 24 * 60 * 60 * 1000); return new Date(date.getTime() + 3 * 24 * 60 * 60 * 1000);
@@ -75,7 +76,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
}, },
{ {
key: "5_days", key: "5_days",
label: "5 days", i18n_label: "notification.snooze.5_days",
value: () => { value: () => {
const date = new Date(); const date = new Date();
return new Date(date.getTime() + 5 * 24 * 60 * 60 * 1000); return new Date(date.getTime() + 5 * 24 * 60 * 60 * 1000);
@@ -83,7 +84,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
}, },
{ {
key: "1_week", key: "1_week",
label: "1 week", i18n_label: "notification.snooze.1_week",
value: () => { value: () => {
const date = new Date(); const date = new Date();
return new Date(date.getTime() + 7 * 24 * 60 * 60 * 1000); return new Date(date.getTime() + 7 * 24 * 60 * 60 * 1000);
@@ -91,7 +92,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
}, },
{ {
key: "2_weeks", key: "2_weeks",
label: "2 weeks", i18n_label: "notification.snooze.2_weeks",
value: () => { value: () => {
const date = new Date(); const date = new Date();
return new Date(date.getTime() + 14 * 24 * 60 * 60 * 1000); return new Date(date.getTime() + 14 * 24 * 60 * 60 * 1000);
@@ -99,7 +100,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
}, },
{ {
key: "custom", key: "custom",
label: "Custom", i18n_label: "notification.snooze.custom",
value: undefined, value: undefined,
}, },
]; ];

View File

@@ -0,0 +1,14 @@
export enum EPageAccess {
PUBLIC = 0,
PRIVATE = 1,
}
export type TCreatePageModal = {
isOpen: boolean;
pageAccess?: EPageAccess;
};
export const DEFAULT_CREATE_PAGE_MODAL_DATA: TCreatePageModal = {
isOpen: false,
pageAccess: EPageAccess.PUBLIC,
};

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: { export const PROFILE_ACTION_LINKS: {
key: string; key: string;
label: string; i18n_label: string;
href: string; href: string;
highlight: (pathname: string) => boolean; highlight: (pathname: string) => boolean;
Icon: React.FC<LucideProps>;
}[] = [ }[] = [
{ {
key: "profile", key: "profile",
label: "Profile", i18n_label: "profile.actions.profile",
href: `/profile`, href: `/profile`,
highlight: (pathname: string) => pathname === "/profile/", highlight: (pathname: string) => pathname === "/profile/",
Icon: CircleUser,
}, },
{ {
key: "security", key: "security",
label: "Security", i18n_label: "profile.actions.security",
href: `/profile/security`, href: `/profile/security`,
highlight: (pathname: string) => pathname === "/profile/security/", highlight: (pathname: string) => pathname === "/profile/security/",
Icon: KeyRound,
}, },
{ {
key: "activity", key: "activity",
label: "Activity", i18n_label: "profile.actions.activity",
href: `/profile/activity`, href: `/profile/activity`,
highlight: (pathname: string) => pathname === "/profile/activity/", highlight: (pathname: string) => pathname === "/profile/activity/",
Icon: Activity,
}, },
{ {
key: "appearance", key: "appearance",
label: "Appearance", i18n_label: "profile.actions.appearance",
href: `/profile/appearance`, href: `/profile/appearance`,
highlight: (pathname: string) => pathname.includes("/profile/appearance"), highlight: (pathname: string) => pathname.includes("/profile/appearance"),
Icon: Settings2,
}, },
{ {
key: "notifications", key: "notifications",
label: "Notifications", i18n_label: "profile.actions.notifications",
href: `/profile/notifications`, href: `/profile/notifications`,
highlight: (pathname: string) => pathname === "/profile/notifications/", highlight: (pathname: string) => pathname === "/profile/notifications/",
Icon: Bell,
}, },
]; ];
@@ -50,7 +40,7 @@ export const PROFILE_VIEWER_TAB = [
{ {
key: "summary", key: "summary",
route: "", route: "",
label: "Summary", i18n_label: "profile.tabs.summary",
selected: "/", selected: "/",
}, },
]; ];
@@ -59,24 +49,25 @@ export const PROFILE_ADMINS_TAB = [
{ {
key: "assigned", key: "assigned",
route: "assigned", route: "assigned",
label: "Assigned", i18n_label: "profile.tabs.assigned",
selected: "/assigned/", selected: "/assigned/",
}, },
{ {
key: "created",
route: "created", route: "created",
label: "Created", i18n_label: "profile.tabs.created",
selected: "/created/", selected: "/created/",
}, },
{ {
key: "subscribed", key: "subscribed",
route: "subscribed", route: "subscribed",
label: "Subscribed", i18n_label: "profile.tabs.subscribed",
selected: "/subscribed/", selected: "/subscribed/",
}, },
{ {
key: "activity", key: "activity",
route: "activity", route: "activity",
label: "Activity", i18n_label: "profile.tabs.activity",
selected: "/activity/", selected: "/activity/",
}, },
]; ];

View File

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

View File

@@ -0,0 +1 @@
export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues";

View File

@@ -5,6 +5,11 @@ export type TStateGroups =
| "completed" | "completed"
| "cancelled"; | "cancelled";
export type TDraggableData = {
groupKey: TStateGroups;
id: string;
};
export const STATE_GROUPS: { export const STATE_GROUPS: {
[key in TStateGroups]: { [key in TStateGroups]: {
key: TStateGroups; key: TStateGroups;
@@ -43,6 +48,13 @@ export const ARCHIVABLE_STATE_GROUPS = [
STATE_GROUPS.completed.key, STATE_GROUPS.completed.key,
STATE_GROUPS.cancelled.key, STATE_GROUPS.cancelled.key,
]; ];
export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key];
export const PENDING_STATE_GROUPS = [
STATE_GROUPS.backlog.key,
STATE_GROUPS.unstarted.key,
STATE_GROUPS.started.key,
STATE_GROUPS.cancelled.key,
];
export const PROGRESS_STATE_GROUPS_DETAILS = [ export const PROGRESS_STATE_GROUPS_DETAILS = [
{ {
@@ -66,3 +78,5 @@ export const PROGRESS_STATE_GROUPS_DETAILS = [
color: "#A3A3A3", color: "#A3A3A3",
}, },
]; ];
export const DISPLAY_WORKFLOW_PRO_CTA = false;

View File

@@ -6,3 +6,11 @@ export const DEFAULT_SWR_CONFIG = {
refreshInterval: 600000, refreshInterval: 600000,
errorRetryCount: 3, errorRetryCount: 3,
}; };
export const WEB_SWR_CONFIG = {
refreshWhenHidden: false,
revalidateIfStale: true,
revalidateOnFocus: true,
revalidateOnMount: true,
errorRetryCount: 3,
};

View File

@@ -54,7 +54,14 @@ export const PROJECT_CREATE_TAB_INDICES = [
"logo_props", "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 = [ export const PROJECT_MODULE_TAB_INDICES = [
"name", "name",
@@ -67,9 +74,21 @@ export const PROJECT_MODULE_TAB_INDICES = [
"submit", "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 { export enum ETabIndices {
ISSUE_FORM = "issue-form", ISSUE_FORM = "issue-form",

View File

@@ -1,9 +1,15 @@
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"]; export const THEMES = [
"light",
"dark",
"light-contrast",
"dark-contrast",
"custom",
];
export interface I_THEME_OPTION { export interface I_THEME_OPTION {
key: string; key: string;
value: string; value: string;
label: string; i18n_label: string;
type: string; type: string;
icon: { icon: {
border: string; border: string;
@@ -16,7 +22,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{ {
key: "system_preference", key: "system_preference",
value: "system", value: "system",
label: "System preference", i18n_label: "System preference",
type: "light", type: "light",
icon: { icon: {
border: "#DEE2E6", border: "#DEE2E6",
@@ -27,7 +33,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{ {
key: "light", key: "light",
value: "light", value: "light",
label: "Light", i18n_label: "Light",
type: "light", type: "light",
icon: { icon: {
border: "#DEE2E6", border: "#DEE2E6",
@@ -38,7 +44,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{ {
key: "dark", key: "dark",
value: "dark", value: "dark",
label: "Dark", i18n_label: "Dark",
type: "dark", type: "dark",
icon: { icon: {
border: "#2E3234", border: "#2E3234",
@@ -49,7 +55,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{ {
key: "light_contrast", key: "light_contrast",
value: "light-contrast", value: "light-contrast",
label: "Light high contrast", i18n_label: "Light high contrast",
type: "light", type: "light",
icon: { icon: {
border: "#000000", border: "#000000",
@@ -60,7 +66,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{ {
key: "dark_contrast", key: "dark_contrast",
value: "dark-contrast", value: "dark-contrast",
label: "Dark high contrast", i18n_label: "Dark high contrast",
type: "dark", type: "dark",
icon: { icon: {
border: "#FFFFFF", border: "#FFFFFF",
@@ -71,7 +77,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{ {
key: "custom", key: "custom",
value: "custom", value: "custom",
label: "Custom theme", i18n_label: "Custom theme",
type: "light", type: "light",
icon: { icon: {
border: "#FFC9C9", border: "#FFC9C9",

View File

@@ -36,3 +36,40 @@ export enum EUserProjectRoles {
MEMBER = 15, MEMBER = 15,
GUEST = 5, GUEST = 5,
} }
export type TUserPermissionsLevel = EUserPermissionsLevel;
export enum EUserPermissions {
ADMIN = 20,
MEMBER = 15,
GUEST = 5,
}
export type TUserPermissions = EUserPermissions;
export type TUserAllowedPermissionsObject = {
create: TUserPermissions[];
update: TUserPermissions[];
delete: TUserPermissions[];
read: TUserPermissions[];
};
export type TUserAllowedPermissions = {
workspace: {
[key: string]: Partial<TUserAllowedPermissionsObject>;
};
project: {
[key: string]: Partial<TUserAllowedPermissionsObject>;
};
};
export const USER_ALLOWED_PERMISSIONS: TUserAllowedPermissions = {
workspace: {
dashboard: {
read: [
EUserPermissions.ADMIN,
EUserPermissions.MEMBER,
EUserPermissions.GUEST,
],
},
},
project: {},
};

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: "common.access.public" },
{ key: EViewAccess.PRIVATE, i18n_label: "common.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 = [ export const ORGANIZATION_SIZE = [
"Just myself", // TODO: translate "Just myself", // TODO: translate
"2-10", "2-10",
@@ -74,3 +77,182 @@ export const RESTRICTED_URLS = [
"instances", "instances",
"instance", "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.api_tokens.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",
},
];

View File

@@ -0,0 +1,171 @@
{
"sidebar": {
"projects": "Projects",
"pages": "Pages",
"new_work_item": "New work item",
"home": "Home",
"your_work": "Your work",
"inbox": "Inbox",
"workspace": "Workspace",
"views": "Views",
"analytics": "Analytics",
"work_items": "Work items",
"cycles": "Cycles",
"modules": "Modules",
"intake": "Intake",
"drafts": "Drafts",
"favorites": "Favorites",
"pro": "Pro",
"upgrade": "Upgrade"
},
"auth": {
"common": {
"email": {
"label": "Email",
"placeholder": "name@company.com",
"errors": {
"required": "Email is required",
"invalid": "Email is invalid"
}
},
"password": {
"label": "Password",
"set_password": "Set a password",
"placeholder": "Enter password",
"confirm_password": {
"label": "Confirm password",
"placeholder": "Confirm password"
},
"current_password": {
"label": "Current password"
},
"new_password": {
"label": "New password",
"placeholder": "Enter new password"
},
"change_password": {
"label": {
"default": "Change password",
"submitting": "Changing password"
}
},
"errors": {
"match": "Passwords don't match",
"empty": "Please enter your password",
"length": "Password length should me more than 8 characters",
"strength": {
"weak": "Password is weak",
"strong": "Password is strong"
}
},
"submit": "Set password",
"toast": {
"change_password": {
"success": {
"title": "Success!",
"message": "Password changed successfully."
},
"error": {
"title": "Error!",
"message": "Something went wrong. Please try again."
}
}
}
},
"unique_code": {
"label": "Unique code",
"placeholder": "gets-sets-flys",
"paste_code": "Paste the code sent to your email",
"requesting_new_code": "Requesting new code",
"sending_code": "Sending code"
},
"already_have_an_account": "Already have an account?",
"login": "Log in",
"create_account": "Create an account",
"new_to_plane": "New to Plane?",
"back_to_sign_in": "Back to sign in",
"resend_in": "Resend in {seconds} seconds",
"sign_in_with_unique_code": "Sign in with unique code",
"forgot_password": "Forgot your password?"
},
"sign_up": {
"header": {
"label": "Create an account to start managing work with your team.",
"step": {
"email": {
"header": "Sign up",
"sub_header": ""
},
"password": {
"header": "Sign up",
"sub_header": "Sign up using an email-password combination."
},
"unique_code": {
"header": "Sign up",
"sub_header": "Sign up using a unique code sent to the email address above."
}
}
},
"errors": {
"password": {
"strength": "Try setting-up a strong password to proceed"
}
}
},
"sign_in": {
"header": {
"label": "Log in to start managing work with your team.",
"step": {
"email": {
"header": "Log in or sign up",
"sub_header": ""
},
"password": {
"header": "Log in or sign up",
"sub_header": "Use your email-password combination to log in."
},
"unique_code": {
"header": "Log in or sign up",
"sub_header": "Log in using a unique code sent to the email address above."
}
}
}
},
"forgot_password": {
"title": "Reset your password",
"description": "Enter your user account's verified email address and we will send you a password reset link.",
"email_sent": "We sent the reset link to your email address",
"send_reset_link": "Send reset link",
"errors": {
"smtp_not_enabled": "We see that your god hasn't enabled SMTP, we will not be able to send a password reset link"
},
"toast": {
"success": {
"title": "Email sent",
"message": "Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder."
},
"error": {
"title": "Error!",
"message": "Something went wrong. Please try again."
}
}
},
"reset_password": {
"title": "Set new password",
"description": "Secure your account with a strong password"
},
"set_password": {
"title": "Secure your account",
"description": "Setting password helps you login securely"
},
"sign_out": {
"toast": {
"error": {
"title": "Error!",
"message": "Failed to sign out. Please try again."
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,11 @@
import IntlMessageFormat from "intl-messageformat"; import IntlMessageFormat from "intl-messageformat";
import get from "lodash/get"; import get from "lodash/get";
import { makeAutoObservable } from "mobx"; import merge from "lodash/merge";
import { makeAutoObservable, runInAction } from "mobx";
// constants // constants
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants"; import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
// core translations imports
import coreEn from "../locales/en/core.json";
// types // types
import { TLanguage, ILanguageOption, ITranslations } from "../types"; import { TLanguage, ILanguageOption, ITranslations } from "../types";
@@ -12,42 +15,35 @@ import { TLanguage, ILanguageOption, ITranslations } from "../types";
* Uses IntlMessageFormat to format the translations * Uses IntlMessageFormat to format the translations
*/ */
export class TranslationStore { export class TranslationStore {
// Core translations that are always loaded
private coreTranslations: ITranslations = {
en: coreEn,
};
// List of translations for each language // List of translations for each language
private translations: ITranslations = {}; private translations: ITranslations = {};
// Cache for IntlMessageFormat instances // Cache for IntlMessageFormat instances
private messageCache: Map<string, IntlMessageFormat> = new Map(); private messageCache: Map<string, IntlMessageFormat> = new Map();
// Current language // Current language
currentLocale: TLanguage = FALLBACK_LANGUAGE; currentLocale: TLanguage = FALLBACK_LANGUAGE;
// Loading state
isLoading: boolean = true;
isInitialized: boolean = false;
// Set of loaded languages
private loadedLanguages: Set<TLanguage> = new Set();
/** /**
* Constructor for the TranslationStore class * Constructor for the TranslationStore class
*/ */
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
// Initialize with core translations immediately
this.translations = this.coreTranslations;
// Initialize language
this.initializeLanguage(); this.initializeLanguage();
// Load all the translations
this.loadTranslations(); this.loadTranslations();
} }
/**
* Loads translations from JSON files and initializes the message cache
*/
private async loadTranslations() {
try {
// dynamic import of translations
const translations = {
en: (await import("../locales/en/translations.json")).default,
fr: (await import("../locales/fr/translations.json")).default,
es: (await import("../locales/es/translations.json")).default,
ja: (await import("../locales/ja/translations.json")).default,
"zh-CN": (await import("../locales/zh-CN/translations.json")).default,
};
this.translations = translations;
this.messageCache.clear(); // Clear cache when translations change
} catch (error) {
console.error("Failed to load translations:", error);
}
}
/** Initializes the language based on the local storage or browser language */ /** Initializes the language based on the local storage or browser language */
private initializeLanguage() { private initializeLanguage() {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
@@ -62,6 +58,100 @@ export class TranslationStore {
this.setLanguage(browserLang); this.setLanguage(browserLang);
} }
/** Loads the translations for the current language */
private async loadTranslations(): Promise<void> {
try {
// Set initialized to true (Core translations are already loaded)
runInAction(() => {
this.isInitialized = true;
});
// Load current and fallback languages in parallel
await this.loadPrimaryLanguages();
// Load all remaining languages in parallel
this.loadRemainingLanguages();
} catch (error) {
console.error("Failed in translation initialization:", error);
runInAction(() => {
this.isLoading = false;
});
}
}
private async loadPrimaryLanguages(): Promise<void> {
try {
// Load current and fallback languages in parallel
const languagesToLoad = new Set<TLanguage>([this.currentLocale]);
// Add fallback language only if different from current
if (this.currentLocale !== FALLBACK_LANGUAGE) {
languagesToLoad.add(FALLBACK_LANGUAGE);
}
// Load all primary languages in parallel
const loadPromises = Array.from(languagesToLoad).map((lang) => this.loadLanguageTranslations(lang));
await Promise.all(loadPromises);
// Update loading state
runInAction(() => {
this.isLoading = false;
});
} catch (error) {
console.error("Failed to load primary languages:", error);
runInAction(() => {
this.isLoading = false;
});
}
}
private loadRemainingLanguages(): void {
const remainingLanguages = SUPPORTED_LANGUAGES.map((lang) => lang.value).filter(
(lang) =>
!this.loadedLanguages.has(lang as TLanguage) && lang !== this.currentLocale && lang !== FALLBACK_LANGUAGE
);
// Load all remaining languages in parallel
Promise.all(remainingLanguages.map((lang) => this.loadLanguageTranslations(lang as TLanguage))).catch((error) => {
console.error("Failed to load some remaining languages:", error);
});
}
private async loadLanguageTranslations(language: TLanguage): Promise<void> {
// Skip if already loaded
if (this.loadedLanguages.has(language)) return;
try {
const translations = await this.importLanguageFile(language);
runInAction(() => {
// Use lodash merge for deep merging
this.translations[language] = merge({}, this.coreTranslations[language] || {}, translations.default);
// Add to loaded languages
this.loadedLanguages.add(language);
// Clear cache
this.messageCache.clear();
});
} catch (error) {
console.error(`Failed to load translations for ${language}:`, error);
}
}
/**
* Imports the translations for the given language
* @param language - The language to import the translations for
* @returns {Promise<any>}
*/
private importLanguageFile(language: TLanguage): Promise<any> {
switch (language) {
case "en":
return import("../locales/en/translations.json");
case "fr":
return import("../locales/fr/translations.json");
case "es":
return import("../locales/es/translations.json");
case "ja":
return import("../locales/ja/translations.json");
case "zh-CN":
return import("../locales/zh-CN/translations.json");
default:
throw new Error(`Unsupported language: ${language}`);
}
}
/** Checks if the language is valid based on the supported languages */ /** Checks if the language is valid based on the supported languages */
private isValidLanguage(lang: string | null): lang is TLanguage { private isValidLanguage(lang: string | null): lang is TLanguage {
return lang !== null && this.availableLanguages.some((l) => l.value === lang); return lang !== null && this.availableLanguages.some((l) => l.value === lang);
@@ -173,20 +263,26 @@ export class TranslationStore {
* Sets the current language and updates the translations * Sets the current language and updates the translations
* @param lng - The new language * @param lng - The new language
*/ */
setLanguage(lng: TLanguage): void { async setLanguage(lng: TLanguage): Promise<void> {
try { try {
if (!this.isValidLanguage(lng)) { if (!this.isValidLanguage(lng)) {
throw new Error(`Invalid language: ${lng}`); throw new Error(`Invalid language: ${lng}`);
} }
// Safeguard in case background loading failed
if (!this.loadedLanguages.has(lng)) {
await this.loadLanguageTranslations(lng);
}
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, lng); localStorage.setItem(STORAGE_KEY, lng);
}
this.currentLocale = lng;
this.messageCache.clear(); // Clear cache when language changes
if (typeof window !== "undefined") {
document.documentElement.lang = lng; document.documentElement.lang = lng;
} }
runInAction(() => {
this.currentLocale = lng;
this.messageCache.clear(); // Clear cache when language changes
});
} catch (error) { } catch (error) {
console.error("Failed to set language:", error); console.error("Failed to set language:", error);
} }

View File

@@ -2,23 +2,6 @@ import { TPaginationInfo } from "./common";
import { TIssuePriorities } from "./issues"; import { TIssuePriorities } from "./issues";
import { TIssue } from "./issues/base"; 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 // filters
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by"; export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
@@ -38,10 +21,7 @@ export type TInboxIssueFilter = {
// sorting filters // sorting filters
export type TInboxIssueSortingKeys = "order_by" | "sort_by"; export type TInboxIssueSortingKeys = "order_by" | "sort_by";
export type TInboxIssueSortingOrderByKeys = export type TInboxIssueSortingOrderByKeys = "issue__created_at" | "issue__updated_at" | "issue__sequence_id";
| "issue__created_at"
| "issue__updated_at"
| "issue__sequence_id";
export type TInboxIssueSortingSortByKeys = "asc" | "desc"; export type TInboxIssueSortingSortByKeys = "asc" | "desc";
@@ -78,17 +58,6 @@ export type TInboxDuplicateIssueDetails = {
name: string; 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 & { export type TInboxIssuePaginationInfo = TPaginationInfo & {
total_results: number; total_results: number;
}; };

View File

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

View File

@@ -1,3 +1,4 @@
import { EUserProjectRoles } from "@plane/constants";
import type { import type {
IProjectViewProps, IProjectViewProps,
IUser, IUser,
@@ -16,7 +17,7 @@ export interface IPartialProject {
identifier: string; identifier: string;
sort_order: number | null; sort_order: number | null;
logo_props: TLogoProps; logo_props: TLogoProps;
member_role: TUserPermissions | null; member_role?: TUserPermissions | EUserProjectRoles | null;
archived_at: string | null; archived_at: string | null;
workspace: IWorkspace | string; workspace: IWorkspace | string;
cycle_view: boolean; cycle_view: boolean;
@@ -118,7 +119,7 @@ export interface IProjectMembership {
} }
export interface IProjectBulkAddFormData { export interface IProjectBulkAddFormData {
members: { role: TUserPermissions; member_id: string }[]; members: { role: TUserPermissions | EUserProjectRoles; member_id: string }[];
} }
export interface IGithubRepository { export interface IGithubRepository {

View File

@@ -1,4 +1,4 @@
import { EViewAccess } from "@/constants/views"; import { EViewAccess } from "@plane/constants";
import { TLogoProps } from "./common"; import { TLogoProps } from "./common";
import { import {
IIssueDisplayFilterOptions, IIssueDisplayFilterOptions,

View File

@@ -1,4 +1,4 @@
import { EViewAccess } from "@/constants/views"; import { EViewAccess } from "@plane/constants";
import { import {
IWorkspaceViewProps, IWorkspaceViewProps,
IIssueDisplayFilterOptions, IIssueDisplayFilterOptions,

View File

@@ -5,6 +5,7 @@ import type {
IUserLite, IUserLite,
IWorkspaceViewProps, IWorkspaceViewProps,
} from "@plane/types"; } from "@plane/types";
import { EUserWorkspaceRoles } from "@plane/constants"; // TODO: check if importing this over here causes circular dependency
import { TUserPermissions } from "./enums"; import { TUserPermissions } from "./enums";
export interface IWorkspace { export interface IWorkspace {
@@ -69,7 +70,7 @@ export type Properties = {
export interface IWorkspaceMember { export interface IWorkspaceMember {
id: string; id: string;
member: IUserLite; member: IUserLite;
role: TUserPermissions; role: TUserPermissions | EUserWorkspaceRoles;
created_at?: string; created_at?: string;
avatar_url?: string; avatar_url?: string;
email?: string; email?: string;
@@ -87,7 +88,7 @@ export interface IWorkspaceMemberMe {
default_props: IWorkspaceViewProps; default_props: IWorkspaceViewProps;
id: string; id: string;
member: string; member: string;
role: TUserPermissions; role: TUserPermissions | EUserWorkspaceRoles;
updated_at: Date; updated_at: Date;
updated_by: string; updated_by: string;
view_props: IWorkspaceViewProps; view_props: IWorkspaceViewProps;

View File

@@ -15,7 +15,7 @@ export const metadata: Metadata = {
url: "https://sites.plane.so/", url: "https://sites.plane.so/",
}, },
keywords: keywords:
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration", "software development, customer feedback, software, accelerate, code management, release management, project management, work item tracking, agile, scrum, kanban, collaboration",
twitter: { twitter: {
site: "@planepowers", site: "@planepowers",
}, },
@@ -32,7 +32,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} /> <link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
</head> </head>
<body> <body>
<AppProvider>{children}</AppProvider> <AppProvider>
<>{children}</>
</AppProvider>
</body> </body>
</html> </html>
); );

View File

@@ -2,6 +2,7 @@
import { FC, ReactNode } from "react"; import { FC, ReactNode } from "react";
// components // components
import { TranslationProvider } from "@plane/i18n";
import { InstanceProvider } from "@/lib/instance-provider"; import { InstanceProvider } from "@/lib/instance-provider";
import { StoreProvider } from "@/lib/store-provider"; import { StoreProvider } from "@/lib/store-provider";
import { ToastProvider } from "@/lib/toast-provider"; import { ToastProvider } from "@/lib/toast-provider";
@@ -15,9 +16,11 @@ export const AppProvider: FC<IAppProvider> = (props) => {
return ( return (
<StoreProvider> <StoreProvider>
<ToastProvider> <TranslationProvider>
<InstanceProvider>{children}</InstanceProvider> <ToastProvider>
</ToastProvider> <InstanceProvider>{children}</InstanceProvider>
</ToastProvider>
</TranslationProvider>
</StoreProvider> </StoreProvider>
); );
}; };

View File

@@ -25,7 +25,7 @@ const Titles: TAuthHeaderDetails = {
}, },
[EAuthModes.SIGN_UP]: { [EAuthModes.SIGN_UP]: {
header: "View, comment, and do more", header: "View, comment, and do more",
subHeader: "Sign up or log in to work with Plane Issues and Pages.", subHeader: "Sign up or log in to work with Plane work items and Pages.",
}, },
}; };
@@ -38,7 +38,7 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
} }
return { return {
header: "Comment or react to issues", header: "Comment or react to work itemss",
subHeader: "Use plane to add your valuable inputs to features.", subHeader: "Use plane to add your valuable inputs to features.",
}; };
}; };

View File

@@ -25,7 +25,7 @@ export const InstanceFailureView: FC = () => {
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3> <h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
<p className="font-medium text-base text-center"> <p className="font-medium text-base text-center">
We were unable to fetch the details of the instance. <br /> We were unable to fetch the details of the instance. <br />
Fret not, it might just be a connectivity issue. Fret not, it might just be a connectivity work items.
</p> </p>
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center">

View File

@@ -3,6 +3,7 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { X } from "lucide-react"; import { X } from "lucide-react";
// types // types
import { useTranslation } from "@plane/i18n";
import { TFilters } from "@/types/issue"; import { TFilters } from "@/types/issue";
// components // components
import { AppliedPriorityFilters } from "./priority"; import { AppliedPriorityFilters } from "./priority";
@@ -18,6 +19,7 @@ export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, "
export const AppliedFiltersList: React.FC<Props> = observer((props) => { export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter } = props; const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter } = props;
const { t } = useTranslation();
return ( return (
<div className="flex flex-wrap items-stretch gap-2"> <div className="flex flex-wrap items-stretch gap-2">
@@ -72,7 +74,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
onClick={handleRemoveAllFilters} onClick={handleRemoveAllFilters}
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200" className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
> >
Clear all {t("common.clear_all")}
<X size={12} strokeWidth={2} /> <X size={12} strokeWidth={2} />
</button> </button>
</div> </div>

View File

@@ -2,8 +2,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// ui
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants"; import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { PriorityIcon } from "@plane/ui"; import { PriorityIcon } from "@plane/ui";
// components // components
import { FilterHeader, FilterOption } from "./helpers"; import { FilterHeader, FilterOption } from "./helpers";
@@ -18,6 +19,9 @@ type Props = {
export const FilterPriority: React.FC<Props> = observer((props) => { export const FilterPriority: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props; const { appliedFilters, handleUpdate, searchQuery } = props;
// hooks
const { t } = useTranslation();
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
@@ -40,11 +44,11 @@ export const FilterPriority: React.FC<Props> = observer((props) => {
isChecked={appliedFilters?.includes(priority.key) ? true : false} isChecked={appliedFilters?.includes(priority.key) ? true : false}
onClick={() => handleUpdate(priority.key)} onClick={() => handleUpdate(priority.key)}
icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />} 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.search.no_matches_found")}</p>
)} )}
</div> </div>
)} )}

View File

@@ -26,7 +26,7 @@ export const IssueLayoutHOC = observer((props: Props) => {
} }
if (getGroupIssueCount(undefined, undefined, false) === 0) { if (getGroupIssueCount(undefined, undefined, false) === 0) {
return <div className="flex w-full h-full items-center justify-center">No Issues Found</div>; return <div className="flex w-full h-full items-center justify-center">No work items Found</div>;
} }
return <>{props.children}</>; return <>{props.children}</>;

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"> <div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<HeaderGroupByCard <HeaderGroupByCard
groupBy={groupBy} groupBy={groupBy}
icon={subList.icon} icon={subList.icon as any}
title={subList.name} title={subList.name}
count={getGroupIssueCount(subList.id, undefined, false) ?? 0} count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
/> />

View File

@@ -131,10 +131,14 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup); const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
if (subGroupByVisibilityToggle === false) return <></>; if (subGroupByVisibilityToggle === false) return <></>;
return ( return (
<div key={`${subGroupBy}_${group.id}`} className="flex w-[350px] flex-shrink-0 flex-col"> <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> </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 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"> <div className="sticky left-0 flex-shrink-0">
<HeaderSubGroupByCard <HeaderSubGroupByCard
icon={group.icon} icon={group.icon as any}
title={group.name || ""} title={group.name || ""}
count={issueCount} count={issueCount}
isExpanded={isExpanded} isExpanded={isExpanded}

View File

@@ -2,6 +2,7 @@
import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react"; import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
// plane types // plane types
import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types"; import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types";
// plane utils // plane utils
@@ -62,6 +63,8 @@ export const ListGroup = observer((props: Props) => {
} = props; } = props;
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(true);
const groupRef = useRef<HTMLDivElement | null>(null); const groupRef = useRef<HTMLDivElement | null>(null);
// hooks
const { t } = useTranslation();
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null); const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
@@ -85,7 +88,7 @@ export const ListGroup = observer((props: Props) => {
} }
onClick={() => loadMoreIssues(group.id)} onClick={() => loadMoreIssues(group.id)}
> >
Load More &darr; {t("common.load_more")} &darr;
</div> </div>
); );

View File

@@ -137,7 +137,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
displayPropertyKey="sub_issue_count" displayPropertyKey="sub_issue_count"
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count} shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count}
> >
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}> <Tooltip tooltipHeading="Sub-work items" tooltipContent={`${issue.sub_issues_count}`}>
<div <div
className={cn( className={cn(
"flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1", "flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1",

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslation } from "@plane/i18n";
// types // types
import { TIssuePriorities } from "@plane/types"; import { TIssuePriorities } from "@plane/types";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
@@ -13,17 +14,19 @@ export const IssueBlockPriority = ({
priority: TIssuePriorities | null; priority: TIssuePriorities | null;
shouldShowName?: boolean; shouldShowName?: boolean;
}) => { }) => {
// hooks
const { t } = useTranslation();
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null; const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
if (priority_detail === null) return <></>; if (priority_detail === null) return <></>;
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="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}`}> <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> <span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
</div> </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> </div>
</Tooltip> </Tooltip>
); );

View File

@@ -50,7 +50,7 @@ export const getGroupByColumns = (
case "created_by": case "created_by":
return getCreatedByColumns(member) as any; return getCreatedByColumns(member) as any;
default: default:
if (includeNone) return [{ id: `All Issues`, name: `All Issues`, payload: {}, icon: undefined }]; if (includeNone) return [{ id: `All Issues`, name: `All work items`, payload: {}, icon: undefined }];
} }
}; };

View File

@@ -5,6 +5,8 @@ import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
// ui // ui
import { SITES_ISSUE_LAYOUTS } from "@plane/constants"; import { SITES_ISSUE_LAYOUTS } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// helpers // helpers
import { queryParamGenerator } from "@/helpers/query-param-generator"; import { queryParamGenerator } from "@/helpers/query-param-generator";
@@ -19,6 +21,8 @@ type Props = {
export const IssuesLayoutSelection: FC<Props> = observer((props) => { export const IssuesLayoutSelection: FC<Props> = observer((props) => {
const { anchor } = props; const { anchor } = props;
// hooks
const { t } = useTranslation();
// router // router
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -45,7 +49,7 @@ export const IssuesLayoutSelection: FC<Props> = observer((props) => {
if (!layoutOptions[layout.key]) return; if (!layoutOptions[layout.key]) return;
return ( return (
<Tooltip key={layout.key} tooltipContent={layout.title}> <Tooltip key={layout.key} tooltipContent={t(layout.titleTranslationKey)}>
<button <button
type="button" type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${ className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${

View File

@@ -50,7 +50,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
title: "Link copied!", title: "Link copied!",
message: "Issue link copied to clipboard.", message: "Work item link copied to clipboard.",
}); });
}); });
}; };

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"name": "Plane Space", "name": "Plane Space",
"short_name": "Plane Space", "short_name": "Plane Space",
"description": "Plane helps you plan your issues, cycles, and product modules.", "description": "Plane helps you plan your work items, cycles, and product modules.",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#f9fafb", "background_color": "#f9fafb",

View File

@@ -41,7 +41,12 @@ export const WorkspaceAnalyticsHeader = observer(() => {
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={<BreadcrumbLink label={t("analytics")} icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />} />} link={
<BreadcrumbLink
label={t("workspace_analytics.label")}
icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />}
/>
}
/> />
</Breadcrumbs> </Breadcrumbs>
{analytics_tab === "custom" ? ( {analytics_tab === "custom" ? (

View File

@@ -5,27 +5,40 @@ import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// plane package imports // plane package imports
import { ANALYTICS_TABS } from "@plane/constants"; import { ANALYTICS_TABS, EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Header, EHeaderVariant } from "@plane/ui"; import { Header, EHeaderVariant } from "@plane/ui";
// components // components
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics"; import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state"; import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks // 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";
const AnalyticsPage = observer(() => { const AnalyticsPage = observer(() => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const analytics_tab = searchParams.get("analytics_tab"); const analytics_tab = searchParams.get("analytics_tab");
// plane imports
const { t } = useTranslation();
// store hooks // store hooks
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, loader } = useProject(); const { workspaceProjectIds, loader } = useProject();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
// helper hooks
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" });
// derived values // derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined; const pageTitle = currentWorkspace?.name
? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name })
: undefined;
// permissions
const canPerformEmptyStateActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
// TODO: refactor loader implementation // TODO: refactor loader implementation
return ( return (
@@ -46,7 +59,7 @@ const AnalyticsPage = observer(() => {
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200" selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
}`} }`}
> >
{tab.title} {t(tab.i18n_title)}
<div <div
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`} className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
/> />
@@ -67,12 +80,22 @@ const AnalyticsPage = observer(() => {
</Tab.Group> </Tab.Group>
</div> </div>
) : ( ) : (
<EmptyState <DetailedEmptyState
type={EmptyStateType.WORKSPACE_ANALYTICS} title={t("workspace_analytics.empty_state.general.title")}
primaryButtonOnClick={() => { description={t("workspace_analytics.empty_state.general.description")}
setTrackElement("Analytics empty state"); assetPath={resolvedPath}
toggleCreateProjectModal(true); 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

@@ -3,7 +3,8 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { PenSquare } from "lucide-react"; import { PenSquare } from "lucide-react";
import { EIssuesStoreType } from "@plane/constants"; import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui // ui
import { Breadcrumbs, Button, Header } from "@plane/ui"; import { Breadcrumbs, Button, Header } from "@plane/ui";
// components // components
@@ -12,8 +13,6 @@ import { CreateUpdateIssueModal } from "@/components/issues";
// hooks // hooks
import { useProject, useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store"; import { useProject, useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store";
// plane-web
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const WorkspaceDraftHeader = observer(() => { export const WorkspaceDraftHeader = observer(() => {
// state // state
@@ -22,7 +21,9 @@ export const WorkspaceDraftHeader = observer(() => {
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
const { paginationInfo } = useWorkspaceDraftIssues(); const { paginationInfo } = useWorkspaceDraftIssues();
const { joinedProjectIds } = useProject(); const { joinedProjectIds } = useProject();
// check if user is authorized to create draft issue
const { t } = useTranslation();
// check if user is authorized to create draft work item
const isAuthorizedUser = allowPermissions( const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
@@ -42,7 +43,9 @@ export const WorkspaceDraftHeader = observer(() => {
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={<BreadcrumbLink label={`Drafts`} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />} link={
<BreadcrumbLink label={t("drafts")} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />
}
/> />
</Breadcrumbs> </Breadcrumbs>
{paginationInfo?.total_count && paginationInfo?.total_count > 0 ? ( {paginationInfo?.total_count && paginationInfo?.total_count > 0 ? (
@@ -62,7 +65,7 @@ export const WorkspaceDraftHeader = observer(() => {
onClick={() => setIsDraftIssueModalOpen(true)} onClick={() => setIsDraftIssueModalOpen(true)}
disabled={!isAuthorizedUser} disabled={!isAuthorizedUser}
> >
Draft<span className="hidden sm:inline-block"> an issue</span> {t("workspace_draft_issues.draft_an_issue")}
</Button> </Button>
)} )}
</Header.RightItem> </Header.RightItem>

View File

@@ -7,11 +7,12 @@ import { Home } from "lucide-react";
import githubBlackImage from "/public/logos/github-black.png"; import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png"; import githubWhiteImage from "/public/logos/github-white.png";
// ui // ui
import { GITHUB_REDIRECTED } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Header } from "@plane/ui"; import { Breadcrumbs, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
// constants // constants
import { GITHUB_REDIRECTED } from "@/constants/event-tracker";
// hooks // hooks
import { useEventTracker } from "@/hooks/store"; import { useEventTracker } from "@/hooks/store";
@@ -19,6 +20,7 @@ export const WorkspaceDashboardHeader = () => {
// hooks // hooks
const { captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const { t } = useTranslation();
return ( return (
<> <>
@@ -28,7 +30,9 @@ export const WorkspaceDashboardHeader = () => {
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" 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> </Breadcrumbs>
</div> </div>
@@ -51,7 +55,7 @@ export const WorkspaceDashboardHeader = () => {
width={16} width={16}
alt="GitHub Logo" 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> </a>
</Header.RightItem> </Header.RightItem>
</Header> </Header>

View File

@@ -4,21 +4,24 @@ import { useCallback, useEffect } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
// plane imports
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components // components
import { LogoSpinner } from "@/components/common"; import { LogoSpinner } from "@/components/common";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state"; import { SimpleEmptyState } from "@/components/empty-state";
import { InboxContentRoot } from "@/components/inbox"; import { InboxContentRoot } from "@/components/inbox";
import { IssuePeekOverview } from "@/components/issues"; import { IssuePeekOverview } from "@/components/issues";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
// hooks // hooks
import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
const WorkspaceDashboardPage = observer(() => { const WorkspaceDashboardPage = observer(() => {
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// plane hooks
const { t } = useTranslation();
// hooks // hooks
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { const {
@@ -31,11 +34,14 @@ const WorkspaceDashboardPage = observer(() => {
const { fetchUserProjectInfo } = useUserPermissions(); const { fetchUserProjectInfo } = useUserPermissions();
const { setPeekIssue } = useIssueDetail(); const { setPeekIssue } = useIssueDetail();
// derived values // derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined; const pageTitle = currentWorkspace?.name
? t("notification.page_label", { workspace: currentWorkspace?.name })
: undefined;
const { workspace_slug, project_id, issue_id, is_inbox_issue } = const { workspace_slug, project_id, issue_id, is_inbox_issue } =
notificationLiteByNotificationId(currentSelectedNotificationId); notificationLiteByNotificationId(currentSelectedNotificationId);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" });
// fetching workspace issue properties // fetching workspace work item properties
useWorkspaceIssueProperties(workspaceSlug); useWorkspaceIssueProperties(workspaceSlug);
// fetch workspace notifications // fetch workspace notifications
@@ -82,7 +88,7 @@ const WorkspaceDashboardPage = observer(() => {
<div className="w-full h-full overflow-hidden overflow-y-auto"> <div className="w-full h-full overflow-hidden overflow-y-auto">
{!currentSelectedNotificationId ? ( {!currentSelectedNotificationId ? (
<div className="w-full h-screen flex justify-center items-center"> <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> </div>
) : ( ) : (
<> <>

View File

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

View File

@@ -3,6 +3,8 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// ui // ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
@@ -10,7 +12,6 @@ import { DownloadActivityButton, WorkspaceActivityListPage } from "@/components/
// hooks // hooks
import { useUserPermissions } from "@/hooks/store"; import { useUserPermissions } from "@/hooks/store";
// plane-web constants // plane-web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const PER_PAGE = 100; const PER_PAGE = 100;
@@ -21,6 +22,8 @@ const ProfileActivityPage = observer(() => {
const [resultsCount, setResultsCount] = useState(0); const [resultsCount, setResultsCount] = useState(0);
// router // router
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
//hooks
const { t } = useTranslation();
const updateTotalPages = (count: number) => setTotalPages(count); const updateTotalPages = (count: number) => setTotalPages(count);
@@ -50,7 +53,7 @@ const ProfileActivityPage = observer(() => {
<PageHead title="Profile - Activity" /> <PageHead title="Profile - Activity" />
<div className="flex h-full w-full flex-col overflow-hidden py-5"> <div className="flex h-full w-full flex-col overflow-hidden py-5">
<div className="flex items-center justify-between gap-2 px-5 md:px-9"> <div className="flex items-center justify-between gap-2 px-5 md:px-9">
<h3 className="text-lg font-medium">Recent activity</h3> <h3 className="text-lg font-medium">{t("profile.stats.recent_activity.title")}</h3>
{canDownloadActivity && <DownloadActivityButton />} {canDownloadActivity && <DownloadActivityButton />}
</div> </div>
<div className="vertical-scrollbar scrollbar-md flex h-full flex-col overflow-y-auto px-5 md:px-9"> <div className="vertical-scrollbar scrollbar-md flex h-full flex-col overflow-y-auto px-5 md:px-9">
@@ -58,7 +61,7 @@ const ProfileActivityPage = observer(() => {
{pageCount < totalPages && resultsCount !== 0 && ( {pageCount < totalPages && resultsCount !== 0 && (
<div className="flex w-full items-center justify-center text-xs"> <div className="flex w-full items-center justify-center text-xs">
<Button variant="accent-primary" size="sm" onClick={handleLoadMore}> <Button variant="accent-primary" size="sm" onClick={handleLoadMore}>
Load more {t("common.load_more")}
</Button> </Button>
</div> </div>
)} )}

View File

@@ -6,15 +6,15 @@ import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { ChevronDown, PanelRight } from "lucide-react"; import { ChevronDown, PanelRight } from "lucide-react";
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IUserProfileProjectSegregation } from "@plane/types"; import { IUserProfileProjectSegregation } from "@plane/types";
import { Breadcrumbs, Header, CustomMenu, UserActivityIcon } from "@plane/ui"; import { Breadcrumbs, Header, CustomMenu, UserActivityIcon } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
// components // components
import { ProfileIssuesFilter } from "@/components/profile"; import { ProfileIssuesFilter } from "@/components/profile";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { useAppTheme, useUser, useUserPermissions } from "@/hooks/store"; import { useAppTheme, useUser, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type TUserProfileHeader = { type TUserProfileHeader = {
userProjectsData: IUserProfileProjectSegregation | undefined; userProjectsData: IUserProfileProjectSegregation | undefined;
@@ -30,6 +30,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme(); const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { workspaceUserInfo, allowPermissions } = useUserPermissions(); const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { t } = useTranslation();
// derived values // derived values
const isAuthorized = allowPermissions( const isAuthorized = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@@ -44,7 +45,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const isCurrentUser = currentUser?.id === userId; const isCurrentUser = currentUser?.id === userId;
const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Work`; const breadcrumbLabel = isCurrentUser ? t("profile.page_label") : `${userName} ${t("profile.work")}`;
return ( return (
<Header> <Header>
@@ -86,7 +87,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
className="w-full text-custom-text-300" className="w-full text-custom-text-300"
> >
{tab.label} {t(tab.i18n_label)}
</Link> </Link>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
))} ))}

View File

@@ -4,16 +4,15 @@ import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
// components // components
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { AppHeader, ContentWrapper } from "@/components/core"; import { AppHeader, ContentWrapper } from "@/components/core";
import { ProfileSidebar } from "@/components/profile"; import { ProfileSidebar } from "@/components/profile";
// constants // constants
import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys"; import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
// hooks // hooks
import { useUserPermissions } from "@/hooks/store"; import { useUserPermissions } from "@/hooks/store";
import useSize from "@/hooks/use-window-size"; import useSize from "@/hooks/use-window-size";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// local components // local components
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
import { UserProfileHeader } from "./header"; import { UserProfileHeader } from "./header";
@@ -66,7 +65,7 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
<AppHeader <AppHeader
header={ header={
<UserProfileHeader <UserProfileHeader
type={currentTab?.label} type={currentTab?.i18n_label}
userProjectsData={userProjectsData} userProjectsData={userProjectsData}
showProfileIssuesFilter={isIssuesTab} showProfileIssuesFilter={isIssuesTab}
/> />

View File

@@ -6,21 +6,30 @@ import { useParams } from "next/navigation";
// icons // icons
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
// plane constants // 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 // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
// components // components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
// helpers // helpers
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks // hooks
import { useIssues, useLabel } from "@/hooks/store"; import { useIssues, useLabel } from "@/hooks/store";
export const ProfileIssuesMobileHeader = observer(() => { export const ProfileIssuesMobileHeader = observer(() => {
// plane i18n
const { t } = useTranslation();
// router // router
const { workspaceSlug, userId } = useParams(); const { workspaceSlug, userId } = useParams();
// store hook // store hook
@@ -112,7 +121,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
placement="bottom-start" placement="bottom-start"
customButton={ customButton={
<div className="flex flex-center text-sm text-custom-text-200"> <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} /> <ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div> </div>
} }
@@ -129,19 +138,19 @@ export const ProfileIssuesMobileHeader = observer(() => {
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<layout.icon className="h-3 w-3" /> <IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div> <div className="text-custom-text-300">{t(layout.i18n_title)}</div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
); );
})} })}
</CustomMenu> </CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200"> <div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown <FiltersDropdown
title="Filters" title={t("common.filters")}
placement="bottom-end" placement="bottom-end"
menuButton={ menuButton={
<div className="flex flex-center text-sm text-custom-text-200"> <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} /> <ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
</div> </div>
} }
@@ -149,7 +158,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
> >
<FilterSelection <FilterSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
} }
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
@@ -163,18 +172,18 @@ export const ProfileIssuesMobileHeader = observer(() => {
</div> </div>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200"> <div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown <FiltersDropdown
title="Display" title={t("common.display")}
placement="bottom-end" placement="bottom-end"
menuButton={ menuButton={
<div className="flex flex-center text-sm text-custom-text-200"> <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} /> <ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
</div> </div>
} }
> >
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}

View File

@@ -2,12 +2,12 @@ import React from "react";
import Link from "next/link"; import Link from "next/link";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// components // components
// constants // constants
import { Header, EHeaderVariant } from "@plane/ui"; import { Header, EHeaderVariant } from "@plane/ui";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
type Props = { type Props = {
isAuthorized: boolean; isAuthorized: boolean;
@@ -33,7 +33,7 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
: "border-transparent" : "border-transparent"
}`} }`}
> >
{t(tab.label)} {t(tab.i18n_label)}
</span> </span>
</Link> </Link>
))} ))}

View File

@@ -3,6 +3,8 @@
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
// types // types
import { GROUP_CHOICES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IUserStateDistribution, TStateGroups } from "@plane/types"; import { IUserStateDistribution, TStateGroups } from "@plane/types";
// components // components
import { ContentWrapper } from "@plane/ui"; import { ContentWrapper } from "@plane/ui";
@@ -16,7 +18,6 @@ import {
} from "@/components/profile"; } from "@/components/profile";
// constants // constants
import { USER_PROFILE_DATA } from "@/constants/fetch-keys"; import { USER_PROFILE_DATA } from "@/constants/fetch-keys";
import { GROUP_CHOICES } from "@/constants/project";
// services // services
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
@@ -26,6 +27,7 @@ const userService = new UserService();
export default function ProfileOverviewPage() { export default function ProfileOverviewPage() {
const { workspaceSlug, userId } = useParams(); const { workspaceSlug, userId } = useParams();
const { t } = useTranslation();
const { data: userProfile } = useSWR( const { data: userProfile } = useSWR(
workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null, workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null,
workspaceSlug && userId ? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString()) : null workspaceSlug && userId ? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString()) : null
@@ -40,7 +42,7 @@ export default function ProfileOverviewPage() {
return ( return (
<> <>
<PageHead title="Your work" /> <PageHead title={t("profile.page_label")} />
<ContentWrapper className="space-y-7"> <ContentWrapper className="space-y-7">
<ProfileStats userProfile={userProfile} /> <ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} /> <ProfileWorkload stateDistribution={stateDistribution} />

View File

@@ -27,7 +27,7 @@ const PROJECT_ARCHIVES_BREADCRUMB_LIST: {
}; };
} = { } = {
issues: { issues: {
label: "Issues", label: "Work items",
href: "/issues", href: "/issues",
icon: LayersIcon, icon: LayersIcon,
}, },
@@ -92,7 +92,7 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
{activeTab === "issues" && issueCount && issueCount > 0 ? ( {activeTab === "issues" && issueCount && issueCount > 0 ? (
<Tooltip <Tooltip
isMobile={isMobile} isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's archived`} tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "work items" : "work item"} in project's archived`}
position="bottom" position="bottom"
> >
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl"> <span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">

View File

@@ -52,7 +52,7 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
link={ link={
<BreadcrumbLink <BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`} href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Issues" label="Work items"
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/> />
} }

View File

@@ -15,7 +15,7 @@ const ProjectArchivedIssuesPage = observer(() => {
const { getProjectById } = useProject(); const { getProjectById } = useProject();
// derived values // derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined; const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name && `${project?.name} - Archived issues`; const pageTitle = project?.name && `${project?.name} - Archived work items`;
return ( return (
<> <>

View File

@@ -7,7 +7,16 @@ import { useParams } from "next/navigation";
// icons // icons
import { ArrowRight, PanelRight } from "lucide-react"; import { ArrowRight, PanelRight } from "lucide-react";
// plane constants // plane constants
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants"; import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui // ui
@@ -16,8 +25,6 @@ import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "
import { ProjectAnalyticsModal } from "@/components/analytics"; import { ProjectAnalyticsModal } from "@/components/analytics";
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
@@ -39,7 +46,6 @@ import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
// router // router
@@ -72,6 +78,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
projectId: string; projectId: string;
cycleId: string; cycleId: string;
}; };
// i18n
const { t } = useTranslation();
// store hooks // store hooks
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
@@ -184,7 +192,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
type="text" type="text"
link={ link={
<BreadcrumbLink <BreadcrumbLink
label="Cycles" label={t("common.cycles")}
href={`/${workspaceSlug}/projects/${projectId}/cycles`} href={`/${workspaceSlug}/projects/${projectId}/cycles`}
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />} icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />}
/> />
@@ -203,7 +211,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
<Tooltip <Tooltip
isMobile={isMobile} isMobile={isMobile}
tooltipContent={`There are ${issuesCount} ${ tooltipContent={`There are ${issuesCount} ${
issuesCount > 1 ? "issues" : "issue" issuesCount > 1 ? "work items" : "work item"
} in this cycle`} } in this cycle`}
position="bottom" position="bottom"
> >
@@ -239,7 +247,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />
<FiltersDropdown <FiltersDropdown
title="Filters" title={t("common.filters")}
placement="bottom-end" placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)} isFiltersApplied={isIssueFilterActive(issueFilters)}
> >
@@ -247,7 +255,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -258,10 +266,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view} moduleViewDisabled={!currentProjectDetails?.module_view}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -276,18 +284,18 @@ export const CycleIssuesHeader: React.FC = observer(() => {
{canUserCreateIssue && ( {canUserCreateIssue && (
<> <>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics {t("common.analytics")}
</Button> </Button>
{!isCompletedCycle && ( {!isCompletedCycle && (
<Button <Button
className="h-full self-start" className="h-full self-start"
onClick={() => { onClick={() => {
setTrackElement("Cycle issues page"); setTrackElement("Cycle work items page");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
}} }}
size="sm" size="sm"
> >
Add issue {t("issue.add.label")}
</Button> </Button>
)} )}
</> </>

View File

@@ -5,32 +5,42 @@ import { useParams } from "next/navigation";
// icons // icons
import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
// plane constants // 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 // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
// components // components
import { ProjectAnalyticsModal } from "@/components/analytics"; import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
// helpers // helpers
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks // hooks
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store"; import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
export const CycleIssuesMobileHeader = () => { export const CycleIssuesMobileHeader = () => {
// i18n
const { t } = useTranslation();
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
const { getCycleById } = useCycle(); const { getCycleById } = useCycle();
const layouts = [ const layouts = [
{ key: "list", title: "List", icon: List }, { key: "list", titleTranslationKey: "issue.layouts.list", icon: List },
{ key: "kanban", title: "Board", icon: Kanban }, { key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar }, { key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar },
]; ];
const { workspaceSlug, projectId, cycleId } = useParams(); const { workspaceSlug, projectId, cycleId } = useParams();
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
// store hooks // store hooks
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { const {
@@ -123,7 +133,9 @@ export const CycleIssuesMobileHeader = () => {
maxHeight={"md"} maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm" className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start" 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" customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect closeOnSelect
> >
@@ -135,18 +147,18 @@ export const CycleIssuesMobileHeader = () => {
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<layout.icon className="w-3 h-3" /> <IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div> <div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
))} ))}
</CustomMenu> </CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm"> <div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown <FiltersDropdown
title="Filters" title={t("common.filters")}
placement="bottom-end" placement="bottom-end"
menuButton={ menuButton={
<span className="flex items-center text-custom-text-200 text-sm"> <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" /> <ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span> </span>
} }
@@ -156,7 +168,7 @@ export const CycleIssuesMobileHeader = () => {
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -170,18 +182,18 @@ export const CycleIssuesMobileHeader = () => {
</div> </div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm"> <div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown <FiltersDropdown
title="Display" title={t("common.display")}
placement="bottom-end" placement="bottom-end"
menuButton={ menuButton={
<span className="flex items-center text-custom-text-200 text-sm"> <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" /> <ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span> </span>
} }
> >
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -198,7 +210,7 @@ export const CycleIssuesMobileHeader = () => {
onClick={() => setAnalyticsModal(true)} onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200" className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
> >
Analytics {t("common.analytics")}
</span> </span>
</div> </div>
</> </>

View File

@@ -3,6 +3,8 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// ui // ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Button, ContrastIcon, Header } from "@plane/ui"; import { Breadcrumbs, Button, ContrastIcon, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
@@ -13,7 +15,6 @@ import { useAppRouter } from "@/hooks/use-app-router";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
// constants // constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const CyclesListHeader: FC = observer(() => { export const CyclesListHeader: FC = observer(() => {
// router // router
@@ -23,6 +24,7 @@ export const CyclesListHeader: FC = observer(() => {
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
const { t } = useTranslation();
const canUserCreateCycle = allowPermissions( const canUserCreateCycle = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@@ -36,7 +38,12 @@ export const CyclesListHeader: FC = observer(() => {
<ProjectBreadcrumb /> <ProjectBreadcrumb />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={<BreadcrumbLink label="Cycles" icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />} />} link={
<BreadcrumbLink
label={t("cycle.label", { count: 2 })}
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/> />
</Breadcrumbs> </Breadcrumbs>
</Header.LeftItem> </Header.LeftItem>
@@ -51,7 +58,8 @@ export const CyclesListHeader: FC = observer(() => {
toggleCreateCycleModal(true); toggleCreateCycleModal(true);
}} }}
> >
<div className="hidden sm:block">Add</div> Cycle <div className="sm:hidden block">{t("add")}</div>
<div className="hidden sm:block">{t("project_cycles.add_cycle")}</div>
</Button> </Button>
</Header.RightItem> </Header.RightItem>
) : ( ) : (

View File

@@ -3,20 +3,22 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// types // plane imports
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TCycleFilters } from "@plane/types"; import { TCycleFilters } from "@plane/types";
// components // components
import { Header, EHeaderVariant } from "@plane/ui"; import { Header, EHeaderVariant } from "@plane/ui";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles"; 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"; import { CycleModuleListLayout } from "@/components/ui";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers // helpers
import { calculateTotalFilters } from "@/helpers/filter.helper"; import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks // 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(() => { const ProjectCyclesPage = observer(() => {
// states // states
@@ -26,13 +28,23 @@ const ProjectCyclesPage = observer(() => {
const { currentProjectCycleIds, loader } = useCycle(); const { currentProjectCycleIds, loader } = useCycle();
const { getProjectById, currentProjectDetails } = useProject(); const { getProjectById, currentProjectDetails } = useProject();
// router // router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// cycle filters hook // cycle filters hook
const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter(); const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter();
const { allowPermissions } = useUserPermissions();
// derived values // derived values
const totalCycles = currentProjectCycleIds?.length ?? 0; const totalCycles = currentProjectCycleIds?.length ?? 0;
const project = projectId ? getProjectById(projectId?.toString()) : undefined; const project = projectId ? getProjectById(projectId?.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; const pageTitle = project?.name ? `${project?.name} - ${t("cycles.label", { count: 2 })}` : 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) => { const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => {
if (!projectId) return; if (!projectId) return;
@@ -50,9 +62,17 @@ const ProjectCyclesPage = observer(() => {
if (currentProjectDetails?.cycle_view === false) if (currentProjectDetails?.cycle_view === false)
return ( return (
<div className="flex items-center justify-center h-full w-full"> <div className="flex items-center justify-center h-full w-full">
<EmptyState <DetailedEmptyState
type={EmptyStateType.DISABLED_PROJECT_CYCLE} title={t("disabled_project.empty_state.cycle.title")}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`} 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> </div>
); );
@@ -71,12 +91,22 @@ const ProjectCyclesPage = observer(() => {
/> />
{totalCycles === 0 ? ( {totalCycles === 0 ? (
<div className="h-full place-items-center"> <div className="h-full place-items-center">
<EmptyState <DetailedEmptyState
type={EmptyStateType.PROJECT_CYCLES} title={t("project_cycles.empty_state.general.title")}
primaryButtonOnClick={() => { description={t("project_cycles.empty_state.general.description")}
setTrackElement("Cycle empty state"); assetPath={resolvedPath}
setCreateModal(true); 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> </div>
) : ( ) : (

View File

@@ -4,7 +4,9 @@ import { FC, useCallback } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// plane constants // 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 // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui // ui
@@ -12,8 +14,6 @@ import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers // helpers
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks // hooks
@@ -24,6 +24,8 @@ import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
// FIXME: Deprecated. Remove it // FIXME: Deprecated. Remove it
export const ProjectDraftIssueHeader: FC = observer(() => { export const ProjectDraftIssueHeader: FC = observer(() => {
// i18n
const { t } = useTranslation();
// router // router
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string }; const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks // store hooks
@@ -96,14 +98,17 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={ link={
<BreadcrumbLink label="Draft Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} /> <BreadcrumbLink
label="Draft work items"
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/>
} }
/> />
</Breadcrumbs> </Breadcrumbs>
{issueCount && issueCount > 0 ? ( {issueCount && issueCount > 0 ? (
<Tooltip <Tooltip
isMobile={isMobile} isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's draft`} tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "work items" : "work item"} in project's draft`}
position="bottom" position="bottom"
> >
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl"> <span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
@@ -119,14 +124,18 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}> <FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection <FilterSelection
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
labels={projectLabels} labels={projectLabels}
memberIds={projectMemberIds ?? undefined} memberIds={projectMemberIds ?? undefined}
@@ -135,10 +144,10 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view} moduleViewDisabled={!currentProjectDetails?.module_view}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}

View File

@@ -17,7 +17,7 @@ const ProjectDraftIssuesPage = observer(() => {
const { getProjectById } = useProject(); const { getProjectById } = useProject();
// derived values // derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined; const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Draft Issues` : undefined; const pageTitle = project?.name ? `${project?.name} - Draft work items` : undefined;
return ( return (
<> <>
@@ -30,7 +30,7 @@ const ProjectDraftIssuesPage = observer(() => {
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs" className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
> >
<PenSquare className="h-4 w-4" /> <PenSquare className="h-4 w-4" />
<span>Draft Issues</span> <span>Draft work items</span>
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</button> </button>
</div> </div>

View File

@@ -2,41 +2,62 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// components // components
import { useParams, useSearchParams } from "next/navigation"; import { useParams, useSearchParams } from "next/navigation";
import { EUserPermissionsLevel } from "@plane/constants";
import { EUserProjectRoles } from "@plane/constants/src/user";
import { useTranslation } from "@plane/i18n";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state"; import { DetailedEmptyState } from "@/components/empty-state";
import { InboxIssueRoot } from "@/components/inbox"; import { InboxIssueRoot } from "@/components/inbox";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers // helpers
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
// hooks // 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 ProjectInboxPage = observer(() => { const ProjectInboxPage = observer(() => {
/// router /// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const navigationTab = searchParams.get("currentTab"); const navigationTab = searchParams.get("currentTab");
const inboxIssueId = searchParams.get("inboxIssueId"); const inboxIssueId = searchParams.get("inboxIssueId");
// plane hooks
const { t } = useTranslation();
// hooks // hooks
const { currentProjectDetails } = useProject(); 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 // No access to inbox
if (currentProjectDetails?.inbox_view === false) if (currentProjectDetails?.inbox_view === false)
return ( return (
<div className="flex items-center justify-center h-full w-full"> <div className="flex items-center justify-center h-full w-full">
<EmptyState <DetailedEmptyState
type={EmptyStateType.DISABLED_PROJECT_INBOX} title={t("disabled_project.empty_state.inbox.title")}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`} 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> </div>
); );
// derived values // derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Intake` : "Plane - Intake"; const pageTitle = currentProjectDetails?.name
? t("inbox_issue.page_label", {
workspace: currentProjectDetails?.name,
})
: t("inbox_issue.page_label", {
workspace: "Plane",
});
const currentNavigationTab = navigationTab const currentNavigationTab = navigationTab
? navigationTab === "open" ? navigationTab === "open"

View File

@@ -5,6 +5,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
// i18n
import { useTranslation } from "@plane/i18n";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
@@ -19,6 +21,8 @@ import emptyIssueDark from "@/public/empty-state/search/issues-dark.webp";
import emptyIssueLight from "@/public/empty-state/search/issues-light.webp"; import emptyIssueLight from "@/public/empty-state/search/issues-light.webp";
const IssueDetailsPage = observer(() => { const IssueDetailsPage = observer(() => {
// i18n
const { t } = useTranslation();
// router // router
const router = useAppRouter(); const router = useAppRouter();
const { workspaceSlug, projectId, issueId } = useParams(); const { workspaceSlug, projectId, issueId } = useParams();
@@ -31,7 +35,7 @@ const IssueDetailsPage = observer(() => {
} = useIssueDetail(); } = useIssueDetail();
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { toggleIssueDetailSidebar, issueDetailSidebarCollapsed } = useAppTheme(); const { toggleIssueDetailSidebar, issueDetailSidebarCollapsed } = useAppTheme();
// fetching issue details // fetching work item details
const { isLoading, error } = useSWR( const { isLoading, error } = useSWR(
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null, workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
workspaceSlug && projectId && issueId workspaceSlug && projectId && issueId
@@ -64,10 +68,10 @@ const IssueDetailsPage = observer(() => {
{error ? ( {error ? (
<EmptyState <EmptyState
image={resolvedTheme === "dark" ? emptyIssueDark : emptyIssueLight} image={resolvedTheme === "dark" ? emptyIssueDark : emptyIssueLight}
title="Issue does not exist" title={t("issue.empty_state.issue_detail.title")}
description="The issue you are looking for does not exist, has been archived, or has been deleted." description={t("issue.empty_state.issue_detail.description")}
primaryButton={{ primaryButton={{
text: "View other issues", text: t("issue.empty_state.issue_detail.primary_button.text"),
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`), onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`),
}} }}
/> />

View File

@@ -2,6 +2,8 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// i18n
import { useTranslation } from "@plane/i18n";
// ui // ui
import { Breadcrumbs, LayersIcon, Header } from "@plane/ui"; import { Breadcrumbs, LayersIcon, Header } from "@plane/ui";
// components // components
@@ -14,6 +16,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
export const ProjectIssueDetailsHeader = observer(() => { export const ProjectIssueDetailsHeader = observer(() => {
const { t } = useTranslation();
// router // router
const router = useAppRouter(); const router = useAppRouter();
const { workspaceSlug, projectId, issueId } = useParams(); const { workspaceSlug, projectId, issueId } = useParams();
@@ -37,7 +40,7 @@ export const ProjectIssueDetailsHeader = observer(() => {
link={ link={
<BreadcrumbLink <BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/issues`} href={`/${workspaceSlug}/projects/${projectId}/issues`}
label="Issues" label={t("issue.label", { count: 2 })} // count is for pluralization
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/> />
} }

View File

@@ -6,26 +6,39 @@ import { useParams } from "next/navigation";
// icons // icons
import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
// plane constants // 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 // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
// components // components
import { ProjectAnalyticsModal } from "@/components/analytics"; import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts"; import {
// constants DisplayFiltersSelection,
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue"; FilterSelection,
FiltersDropdown,
IssueLayoutIcon,
} from "@/components/issues/issue-layouts";
// helpers // helpers
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks // hooks
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
export const ProjectIssuesMobileHeader = observer(() => { export const ProjectIssuesMobileHeader = observer(() => {
// i18n
const { t } = useTranslation();
const layouts = [ const layouts = [
{ key: "list", title: "List", icon: List }, { key: "list", titleTranslationKey: "issue.layouts.list", icon: List },
{ key: "kanban", title: "Board", icon: Kanban }, { key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar }, { key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar },
]; ];
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
const { workspaceSlug, projectId } = useParams() as { const { workspaceSlug, projectId } = useParams() as {
@@ -104,7 +117,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
placement="bottom-start" placement="bottom-start"
customButton={ customButton={
<div className="flex flex-start text-sm text-custom-text-200"> <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} /> <ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div> </div>
} }
@@ -119,18 +132,18 @@ export const ProjectIssuesMobileHeader = observer(() => {
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<layout.icon className="h-3 w-3" /> <IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div> <div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
))} ))}
</CustomMenu> </CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200"> <div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown <FiltersDropdown
title="Filters" title={t("common.filters")}
placement="bottom-end" placement="bottom-end"
menuButton={ menuButton={
<span className="flex items-center text-sm text-custom-text-200"> <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" /> <ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span> </span>
} }
@@ -142,7 +155,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
labels={projectLabels} labels={projectLabels}
memberIds={projectMemberIds ?? undefined} memberIds={projectMemberIds ?? undefined}
@@ -154,18 +167,18 @@ export const ProjectIssuesMobileHeader = observer(() => {
</div> </div>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200"> <div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown <FiltersDropdown
title="Display" title={t("common.display")}
placement="bottom-end" placement="bottom-end"
menuButton={ menuButton={
<span className="flex items-center text-sm text-custom-text-200"> <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" /> <ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span> </span>
} }
> >
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -181,7 +194,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
onClick={() => setAnalyticsModal(true)} onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center border-l border-custom-border-200 text-sm text-custom-text-200" className="flex flex-grow justify-center border-l border-custom-border-200 text-sm text-custom-text-200"
> >
Analytics {t("common.analytics")}
</button> </button>
</div> </div>
</> </>

View File

@@ -3,6 +3,8 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Head from "next/head"; import Head from "next/head";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// i18n
import { useTranslation } from "@plane/i18n";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { ProjectLayoutRoot } from "@/components/issues"; import { ProjectLayoutRoot } from "@/components/issues";
@@ -11,6 +13,8 @@ import { useProject } from "@/hooks/store";
const ProjectIssuesPage = observer(() => { const ProjectIssuesPage = observer(() => {
const { projectId } = useParams(); const { projectId } = useParams();
// i18n
const { t } = useTranslation();
// store // store
const { getProjectById } = useProject(); const { getProjectById } = useProject();
@@ -20,13 +24,15 @@ const ProjectIssuesPage = observer(() => {
// derived values // derived values
const project = getProjectById(projectId.toString()); const project = getProjectById(projectId.toString());
const pageTitle = project?.name ? `${project?.name} - Issues` : undefined; const pageTitle = project?.name ? `${project?.name} - ${t("issue.label", { count: 2 })}` : undefined; // Count is for pluralization
return ( return (
<> <>
<PageHead title={pageTitle} /> <PageHead title={pageTitle} />
<Head> <Head>
<title>{project?.name} - Issues</title> <title>
{project?.name} - {t("issue.label", { count: 2 })}
</title>
</Head> </Head>
<div className="h-full w-full"> <div className="h-full w-full">
<ProjectLayoutRoot /> <ProjectLayoutRoot />

View File

@@ -7,7 +7,14 @@ import { useParams } from "next/navigation";
// icons // icons
import { ArrowRight, PanelRight } from "lucide-react"; import { ArrowRight, PanelRight } from "lucide-react";
// plane constants // plane constants
import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType } from "@plane/constants"; import {
EIssueLayoutTypes,
EIssuesStoreType,
EIssueFilterType,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui // ui
@@ -16,8 +23,6 @@ import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header } from "@pla
import { ProjectAnalyticsModal } from "@/components/analytics"; import { ProjectAnalyticsModal } from "@/components/analytics";
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
@@ -40,7 +45,6 @@ import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
// router // router
@@ -202,7 +206,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<Tooltip <Tooltip
isMobile={isMobile} isMobile={isMobile}
tooltipContent={`There are ${issuesCount} ${ tooltipContent={`There are ${issuesCount} ${
issuesCount > 1 ? "issues" : "issue" issuesCount > 1 ? "work items" : "work item"
} in this module`} } in this module`}
position="bottom" position="bottom"
> >
@@ -247,7 +251,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
labels={projectLabels} labels={projectLabels}
memberIds={projectMemberIds ?? undefined} memberIds={projectMemberIds ?? undefined}
@@ -259,7 +263,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -285,12 +289,12 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<Button <Button
className="hidden sm:flex" className="hidden sm:flex"
onClick={() => { onClick={() => {
setTrackElement("Module issues page"); setTrackElement("Module work items page");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE); toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
}} }}
size="sm" size="sm"
> >
Add issue Add work item
</Button> </Button>
</> </>
) : ( ) : (

View File

@@ -6,16 +6,27 @@ import { useParams } from "next/navigation";
// icons // icons
import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
// plane constants // 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 // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
// components // components
import { ProjectAnalyticsModal } from "@/components/analytics"; import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts"; import {
// constants DisplayFiltersSelection,
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue"; FilterSelection,
FiltersDropdown,
IssueLayoutIcon,
} from "@/components/issues/issue-layouts";
// helpers // helpers
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks // hooks
@@ -25,10 +36,11 @@ export const ModuleIssuesMobileHeader = observer(() => {
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { getModuleById } = useModule(); const { getModuleById } = useModule();
const { t } = useTranslation();
const layouts = [ const layouts = [
{ key: "list", title: "List", icon: List }, { key: "list", i18n_title: "issue.layouts.list", icon: List },
{ key: "kanban", title: "Board", icon: Kanban }, { key: "kanban", i18n_title: "issue.layouts.kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar }, { key: "calendar", i18n_title: "issue.layouts.calendar", icon: Calendar },
]; ];
const { workspaceSlug, projectId, moduleId } = useParams() as { const { workspaceSlug, projectId, moduleId } = useParams() as {
workspaceSlug: string; workspaceSlug: string;
@@ -116,8 +128,8 @@ export const ModuleIssuesMobileHeader = observer(() => {
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<layout.icon className="h-3 w-3" /> <IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div> <div className="text-custom-text-300">{t(layout.i18n_title)}</div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
))} ))}
</CustomMenu> </CustomMenu>
@@ -139,7 +151,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
labels={projectLabels} labels={projectLabels}
memberIds={projectMemberIds ?? undefined} memberIds={projectMemberIds ?? undefined}
@@ -162,7 +174,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
> >
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}

View File

@@ -1,6 +1,9 @@
"use client"; "use client";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui // ui
import { Breadcrumbs, Button, DiceIcon, Header } from "@plane/ui"; import { Breadcrumbs, Button, DiceIcon, Header } from "@plane/ui";
// components // components
@@ -12,7 +15,6 @@ import { useAppRouter } from "@/hooks/use-app-router";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
// constants // constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ModulesListHeader: React.FC = observer(() => { export const ModulesListHeader: React.FC = observer(() => {
// router // router
@@ -24,6 +26,8 @@ export const ModulesListHeader: React.FC = observer(() => {
const { loader } = useProject(); const { loader } = useProject();
const { t } = useTranslation();
// auth // auth
const canUserCreateModule = allowPermissions( const canUserCreateModule = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@@ -38,7 +42,9 @@ export const ModulesListHeader: React.FC = observer(() => {
<ProjectBreadcrumb /> <ProjectBreadcrumb />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={<BreadcrumbLink label="Modules" icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />} link={
<BreadcrumbLink label={t("modules")} icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />
}
/> />
</Breadcrumbs> </Breadcrumbs>
</div> </div>
@@ -54,7 +60,8 @@ export const ModulesListHeader: React.FC = observer(() => {
toggleCreateModuleModal(true); toggleCreateModuleModal(true);
}} }}
> >
<div className="hidden sm:block">Add</div> Module <div className="sm:hidden block">{t("add")}</div>
<div className="hidden sm:block">{t("project_module.add_module")}</div>
</Button> </Button>
) : ( ) : (
<></> <></>

View File

@@ -2,13 +2,16 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { MODULE_VIEW_LAYOUTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { CustomMenu, Row } from "@plane/ui"; import { CustomMenu, Row } from "@plane/ui";
import { MODULE_VIEW_LAYOUTS } from "@/constants/module"; import { ModuleLayoutIcon } from "@/components/modules";
import { useModuleFilter, useProject } from "@/hooks/store"; import { useModuleFilter, useProject } from "@/hooks/store";
export const ModulesListMobileHeader = observer(() => { export const ModulesListMobileHeader = observer(() => {
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { updateDisplayFilters } = useModuleFilter(); const { updateDisplayFilters } = useModuleFilter();
const { t } = useTranslation();
return ( return (
<div className="flex justify-start md:hidden"> <div className="flex justify-start md:hidden">
@@ -34,8 +37,8 @@ export const ModulesListMobileHeader = observer(() => {
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<layout.icon className="w-3 h-3" /> <ModuleLayoutIcon layoutType={layout.key} />
<div className="text-custom-text-300">{layout.title}</div> <div className="text-custom-text-300">{t(layout.i18n_title)}</div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
); );
})} })}

View File

@@ -4,27 +4,36 @@ import { useCallback } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// types // types
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TModuleFilters } from "@plane/types"; import { TModuleFilters } from "@plane/types";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state"; import { DetailedEmptyState } from "@/components/empty-state";
import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules"; import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers // helpers
import { calculateTotalFilters } from "@/helpers/filter.helper"; import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks // 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(() => { const ProjectModulesPage = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// store // store
const { getProjectById, currentProjectDetails } = useProject(); const { getProjectById, currentProjectDetails } = useProject();
const { currentProjectFilters, currentProjectDisplayFilters, clearAllFilters, updateFilters, updateDisplayFilters } = const { currentProjectFilters, currentProjectDisplayFilters, clearAllFilters, updateFilters, updateDisplayFilters } =
useModuleFilter(); useModuleFilter();
const { allowPermissions } = useUserPermissions();
// derived values // derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined; const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Modules` : 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( const handleRemoveFilter = useCallback(
(key: keyof TModuleFilters, value: string | null) => { (key: keyof TModuleFilters, value: string | null) => {
@@ -45,9 +54,17 @@ const ProjectModulesPage = observer(() => {
if (currentProjectDetails?.module_view === false) if (currentProjectDetails?.module_view === false)
return ( return (
<div className="flex items-center justify-center h-full w-full"> <div className="flex items-center justify-center h-full w-full">
<EmptyState <DetailedEmptyState
type={EmptyStateType.DISABLED_PROJECT_MODULE} title={t("disabled_project.empty_state.module.title")}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`} 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> </div>
); );

View File

@@ -4,14 +4,14 @@ import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useParams, useRouter, useSearchParams } from "next/navigation";
import { FileText } from "lucide-react"; import { FileText } from "lucide-react";
// constants
import { EPageAccess } from "@plane/constants";
// plane types // plane types
import { TPage } from "@plane/types"; import { TPage } from "@plane/types";
// plane ui // plane ui
import { Breadcrumbs, Button, Header, setToast, TOAST_TYPE } from "@plane/ui"; import { Breadcrumbs, Button, Header, setToast, TOAST_TYPE } from "@plane/ui";
// helpers // helpers
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
// constants
import { EPageAccess } from "@/constants/page";
// hooks // hooks
import { useEventTracker, useProject, useProjectPages } from "@/hooks/store"; import { useEventTracker, useProject, useProjectPages } from "@/hooks/store";
// plane web // plane web

View File

@@ -2,27 +2,35 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, useSearchParams } from "next/navigation"; 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"; import { TPageNavigationTabs } from "@plane/types";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state"; import { DetailedEmptyState } from "@/components/empty-state";
import { PagesListRoot, PagesListView } from "@/components/pages"; import { PagesListRoot, PagesListView } from "@/components/pages";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks // 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(() => { const ProjectPagesPage = observer(() => {
// router // router
const router = useAppRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const type = searchParams.get("type"); const type = searchParams.get("type");
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// store hooks // store hooks
const { getProjectById, currentProjectDetails } = useProject(); const { getProjectById, currentProjectDetails } = useProject();
const { allowPermissions } = useUserPermissions();
// derived values // derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined; const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Pages` : 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 currentPageType = (): TPageNavigationTabs => {
const pageType = type?.toString(); const pageType = type?.toString();
@@ -37,9 +45,17 @@ const ProjectPagesPage = observer(() => {
if (currentProjectDetails?.page_view === false) if (currentProjectDetails?.page_view === false)
return ( return (
<div className="flex items-center justify-center h-full w-full"> <div className="flex items-center justify-center h-full w-full">
<EmptyState <DetailedEmptyState
type={EmptyStateType.DISABLED_PROJECT_PAGE} title={t("disabled_project.empty_state.page.title")}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`} 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> </div>
); );

View File

@@ -3,6 +3,8 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IProject } from "@plane/types"; import { IProject } from "@plane/types";
// ui // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
@@ -12,7 +14,6 @@ import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automat
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
// hooks // hooks
import { useProject, useUserPermissions } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const AutomationSettingsPage = observer(() => { const AutomationSettingsPage = observer(() => {
// router // router
@@ -21,6 +22,8 @@ const AutomationSettingsPage = observer(() => {
const { workspaceUserInfo, allowPermissions } = useUserPermissions(); const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentProjectDetails: projectDetails, updateProject } = useProject(); const { currentProjectDetails: projectDetails, updateProject } = useProject();
const { t } = useTranslation();
// derived values // derived values
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
@@ -48,7 +51,7 @@ const AutomationSettingsPage = observer(() => {
<PageHead title={pageTitle} /> <PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "opacity-60"}`}> <section className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<div className="flex flex-col items-start border-b border-custom-border-100 pb-3.5"> <div className="flex flex-col items-start border-b border-custom-border-100 pb-3.5">
<h3 className="text-xl font-medium leading-normal">Automations</h3> <h3 className="text-xl font-medium leading-normal">{t("project_settings.automations.label")}</h3>
</div> </div>
<AutoArchiveAutomation handleChange={handleChange} /> <AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} /> <AutoCloseAutomation handleChange={handleChange} />

View File

@@ -3,12 +3,12 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// components // components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens"; import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { EstimateRoot } from "@/components/estimates"; import { EstimateRoot } from "@/components/estimates";
// hooks // hooks
import { useProject, useUserPermissions } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const EstimatesSettingsPage = observer(() => { const EstimatesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();

View File

@@ -3,12 +3,12 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// components // components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens"; import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { ProjectFeaturesList } from "@/components/project"; import { ProjectFeaturesList } from "@/components/project";
// hooks // hooks
import { useProject, useUserPermissions } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const FeaturesSettingsPage = observer(() => { const FeaturesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();

View File

@@ -5,6 +5,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// ui // ui
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, CustomMenu, Header } from "@plane/ui"; import { Breadcrumbs, CustomMenu, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
@@ -14,7 +16,6 @@ import { useAppRouter } from "@/hooks/use-app-router";
// plane web // plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project"; import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ProjectSettingHeader: FC = observer(() => { export const ProjectSettingHeader: FC = observer(() => {
// router // router
@@ -24,6 +25,8 @@ export const ProjectSettingHeader: FC = observer(() => {
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
const { loader } = useProject(); const { loader } = useProject();
const { t } = useTranslation();
return ( return (
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
@@ -65,7 +68,7 @@ export const ProjectSettingHeader: FC = observer(() => {
key={item.key} key={item.key}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)} onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}
> >
{item.label} {t(item.i18n_label)}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
) )
)} )}

View File

@@ -5,12 +5,12 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// components // components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens"; import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { ProjectSettingsLabelList } from "@/components/labels"; import { ProjectSettingsLabelList } from "@/components/labels";
// hooks // hooks
import { useProject, useUserPermissions } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const LabelsSettingsPage = observer(() => { const LabelsSettingsPage = observer(() => {
// store hooks // store hooks

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