feat: language support (#6472)

* chore: ln support modules constants

* fix: translation key

* chore: empty state refactor (#6404)

* chore: asset path helper hook added

* chore: detailed and simple empty state component added

* chore: section empty state component added

* chore: language translation for all empty states

* chore: new empty state implementation

* improvement: add more translations

* improvement: user permissions and workspace draft empty state

* chore: update translation structure

* chore: inbox empty states

* chore: disabled project features empty state

* chore: active cycle progress empty state

* chore: notification empty state

* chore: connections translation

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

* chore: project pages empty state and translations

* chore: project module and view related empty state

* chore: remove project draft related empty state

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

* chore: project cycles related empty state

* chore: project settings empty state

* chore: profile issue and acitivity empty state

* chore: workspace settings realted constants

* chore: stickies and home widgets empty state

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

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

* chore: minor updates

* fix: build errors

---------

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

* fix: language support fo profile (#6461)

* fix: ln support fo profile

* fix: merge changes

* fix: merge changes

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

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

* chore: updated translation structure

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

* chore: updated spanish translation

* chore: removed translation for issue priorities

* fix: build errors

* chore: minor updates

---------

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

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

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

* chore: workspace drafts constant moved to plane constant package

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

* feat: home language support without stickies

* fix: home sidebar

* fix: added missing keys

* fix: show all btn

* fix: recents empty state

* chore: translation update

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

* chore: workspace constant language support and refactor

* chore: workspace constant language support and refactor

* chore: code refactor

* chore: code refactor

* merge conflict

* chore: code refactor

---------

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

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

* chore: notification language support and refactor

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

* chore: ln support for inbox constants

* fix: snooze duration

* fix: enum

* fix: translation keys

* fix: inbox status icon

* fix: status icon

* fix: naming

---------

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

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

* fix: ln support for views constants

* fix: added translation

* fix: translation keys

* fix: access

* chore: code refactor

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

* chore: ln support workspace projects constants

* fix: translation key

* fix: removed state translation

* fix: removed state translation

* fi: added translations

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

* chore: themes language support and refactor

* chore: theme language support and refactor

* fix

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

* chore: ln support for cycles constant file

* fix: added chinese

* fix: lint

* fix: translation key

* fix: build errors

* minor updates

* chore: minor translation update

* chore: minor translation update

* refactor: move labels contants to packages

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

* chore: timezones constant moved to plane package

* chore: metadata constant code refactor

* chore: code refactor

* fix: dashboard constants moved

* chore: code refactor (#6478)

* refactor: spreadsheet constants

* chore: drafts language support (#6485)

* chore: workspace drafts language support

* chore: code refactor

* feat: ln support for notifications (#6486)

* feat: ln support for notifications

* fix: translations

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

* fix: removed use-client

* chore: removed unnecessary commnets

* chore: workspace draft language support (#6490)

* chore: workspace drafts language support

* chore: code refactor

* chore: draft language support

* Feat constant event tracker (#6479)

* fix: event tracjer constants

* fix: constants event tracker

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

* feat: added translation to projects list page

* chore: restructured translation file

* chore: module language support (#6499)

* chore: module language support added

* chore: code refactor

* chore: workspace views language support (#6492)

* chore: workspace views language support

* chore: code refactor

* feat: custom analytics language support (#6494)

* feat: custom analytics language support

* fix: key

* fix: refactoring

---------

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

* chore: minor improvements

* feat: language support for intake (#6498)

* feat: language support for intake

* fix: key name

* refactor: authentications related translations

* feat: language support issues  (#6501)

* enhancement: added translations for issue list view

* chore: added translations for issue detail widgets

* chore: added missing translations

* chore: modified issue to work items

* chore: updated translations

* Feat: workspace settings language support (#6508)

* feat: language support for workspace settings

* fix: lint

* fix: export title

* chore project settings language support (#6502)

* chore: project settings language support

* chore: code refactor

* refactor: workspace creation related translations

* chore: renamed issues to work items

* fix: build errors

* fix: lint

* chore: modified translations

* chore: remove duplicate

* improvement: french translation

* chore: chinese translation improvement

* fix: japanese translations

* chore: added spanish translation

* minor improvements

* fix: miscelleous language translations

* fix: clear_all key

* fix: moved user permission constants (#6516)

* feat: language support for  issues (#6513)

* chore: added language support to issue detail widgets

* improvement: added translation for issue detail

* enhancement: added language trasnlation to issue layouts

* chore: translation improvement (#6518)

* feat: language support description (#6519)

* enhancement: added language support for description

* fix: updated keys

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

* chore: replace missing issue occurances to work items

* fix: build errors

* minor improvements

* fix: profile links

* Feat ln cycles (#6528)

* feat: added language support for cycles

* feat: added language support for cycles

* chore: added core.json

* fix: translation keys

* fix: translation keys (#6530)

* fix: changed sidebar keys

* fix: removed extras

* fix: updated keys

* chore: optimize translation imports

* fix: updated keys (#6534)

* fix: updated keys

* fix-sub work items toasts

* chore: add missing translation and minor fixes

* chore: code refactor

* fix: language support keys (#6553)

* minor improvements

* minor fixes

* fix: remove lucide import from constants package

* chore: regenerate all translations

* chore: addded chinese and japanese translation files

* chore: remove all  from translations

* fix: added member

* fix: language support keys (#6558)

* fix: renamed keys

* fix: space app

* chore: renamed issues to work items

* chore: update site manifest

* chore: updated translations

* fix: lang keys

* chore: update translations

---------

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

View file

@ -41,7 +41,12 @@ export const WorkspaceAnalyticsHeader = observer(() => {
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
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>
{analytics_tab === "custom" ? (

View file

@ -5,27 +5,40 @@ import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
import { Tab } from "@headlessui/react";
// plane package imports
import { ANALYTICS_TABS } from "@plane/constants";
import { ANALYTICS_TABS, EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Header, EHeaderVariant } from "@plane/ui";
// components
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useProject, useWorkspace } from "@/hooks/store";
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const AnalyticsPage = observer(() => {
const searchParams = useSearchParams();
const analytics_tab = searchParams.get("analytics_tab");
// plane imports
const { t } = useTranslation();
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, loader } = useProject();
const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
// helper hooks
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" });
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined;
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
return (
@ -46,7 +59,7 @@ const AnalyticsPage = observer(() => {
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
}`}
>
{tab.title}
{t(tab.i18n_title)}
<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"}`}
/>
@ -67,12 +80,22 @@ const AnalyticsPage = observer(() => {
</Tab.Group>
</div>
) : (
<EmptyState
type={EmptyStateType.WORKSPACE_ANALYTICS}
primaryButtonOnClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
}}
<DetailedEmptyState
title={t("workspace_analytics.empty_state.general.title")}
description={t("workspace_analytics.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_analytics.empty_state.general.primary_button.text")}
title={t("workspace_analytics.empty_state.general.primary_button.comic.title")}
description={t("workspace_analytics.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
}}
disabled={!canPerformEmptyStateActions}
/>
}
/>
)}
</>

View file

@ -3,7 +3,8 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { PenSquare } from "lucide-react";
import { EIssuesStoreType } from "@plane/constants";
import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
@ -12,8 +13,6 @@ import { CreateUpdateIssueModal } from "@/components/issues";
// hooks
import { useProject, useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store";
// plane-web
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const WorkspaceDraftHeader = observer(() => {
// state
@ -22,7 +21,9 @@ export const WorkspaceDraftHeader = observer(() => {
const { allowPermissions } = useUserPermissions();
const { paginationInfo } = useWorkspaceDraftIssues();
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(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
@ -42,7 +43,9 @@ export const WorkspaceDraftHeader = observer(() => {
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
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>
{paginationInfo?.total_count && paginationInfo?.total_count > 0 ? (
@ -62,7 +65,7 @@ export const WorkspaceDraftHeader = observer(() => {
onClick={() => setIsDraftIssueModalOpen(true)}
disabled={!isAuthorizedUser}
>
Draft<span className="hidden sm:inline-block"> an issue</span>
{t("workspace_draft_issues.draft_an_issue")}
</Button>
)}
</Header.RightItem>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@ const PROJECT_ARCHIVES_BREADCRUMB_LIST: {
};
} = {
issues: {
label: "Issues",
label: "Work items",
href: "/issues",
icon: LayersIcon,
},
@ -92,7 +92,7 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
{activeTab === "issues" && issueCount && issueCount > 0 ? (
<Tooltip
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"
>
<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={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Issues"
label="Work items"
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/>
}

View file

@ -15,7 +15,7 @@ const ProjectArchivedIssuesPage = observer(() => {
const { getProjectById } = useProject();
// derived values
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 (
<>

View file

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

View file

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

View file

@ -3,6 +3,8 @@
import { FC } from "react";
import { observer } from "mobx-react";
// ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Button, ContrastIcon, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
@ -13,7 +15,6 @@ import { useAppRouter } from "@/hooks/use-app-router";
// plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
// constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const CyclesListHeader: FC = observer(() => {
// router
@ -23,6 +24,7 @@ export const CyclesListHeader: FC = observer(() => {
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { currentProjectDetails, loader } = useProject();
const { t } = useTranslation();
const canUserCreateCycle = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@ -36,7 +38,12 @@ export const CyclesListHeader: FC = observer(() => {
<ProjectBreadcrumb />
<Breadcrumbs.BreadcrumbItem
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>
</Header.LeftItem>
@ -51,7 +58,8 @@ export const CyclesListHeader: FC = observer(() => {
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>
</Header.RightItem>
) : (

View file

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

View file

@ -4,7 +4,9 @@ import { FC, useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane constants
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@ -12,8 +14,6 @@ import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@ -24,6 +24,8 @@ import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
// FIXME: Deprecated. Remove it
export const ProjectDraftIssueHeader: FC = observer(() => {
// i18n
const { t } = useTranslation();
// router
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
@ -96,14 +98,17 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
<Breadcrumbs.BreadcrumbItem
type="text"
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>
{issueCount && issueCount > 0 ? (
<Tooltip
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"
>
<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)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
@ -135,10 +144,10 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}

View file

@ -17,7 +17,7 @@ const ProjectDraftIssuesPage = observer(() => {
const { getProjectById } = useProject();
// derived values
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 (
<>
@ -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"
>
<PenSquare className="h-4 w-4" />
<span>Draft Issues</span>
<span>Draft work items</span>
<X className="h-3 w-3" />
</button>
</div>
@ -40,4 +40,4 @@ const ProjectDraftIssuesPage = observer(() => {
);
});
export default ProjectDraftIssuesPage;
export default ProjectDraftIssuesPage;

View file

@ -2,41 +2,62 @@
import { observer } from "mobx-react";
// components
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 { EmptyState } from "@/components/empty-state";
import { DetailedEmptyState } from "@/components/empty-state";
import { InboxIssueRoot } from "@/components/inbox";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
// hooks
import { useProject } from "@/hooks/store";
import { useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const ProjectInboxPage = observer(() => {
/// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
const searchParams = useSearchParams();
const navigationTab = searchParams.get("currentTab");
const inboxIssueId = searchParams.get("inboxIssueId");
// plane hooks
const { t } = useTranslation();
// hooks
const { currentProjectDetails } = useProject();
const { allowPermissions } = useUserPermissions();
// derived values
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/intake" });
// No access to inbox
if (currentProjectDetails?.inbox_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_INBOX}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
<DetailedEmptyState
title={t("disabled_project.empty_state.inbox.title")}
description={t("disabled_project.empty_state.inbox.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.inbox.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
/>
</div>
);
// 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
? navigationTab === "open"

View file

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

View file

@ -2,6 +2,8 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// i18n
import { useTranslation } from "@plane/i18n";
// ui
import { Breadcrumbs, LayersIcon, Header } from "@plane/ui";
// components
@ -14,6 +16,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
export const ProjectIssueDetailsHeader = observer(() => {
const { t } = useTranslation();
// router
const router = useAppRouter();
const { workspaceSlug, projectId, issueId } = useParams();
@ -37,7 +40,7 @@ export const ProjectIssueDetailsHeader = observer(() => {
link={
<BreadcrumbLink
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" />}
/>
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,9 @@
"use client";
import { observer } from "mobx-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Breadcrumbs, Button, DiceIcon, Header } from "@plane/ui";
// components
@ -12,7 +15,6 @@ import { useAppRouter } from "@/hooks/use-app-router";
// plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
// constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ModulesListHeader: React.FC = observer(() => {
// router
@ -24,6 +26,8 @@ export const ModulesListHeader: React.FC = observer(() => {
const { loader } = useProject();
const { t } = useTranslation();
// auth
const canUserCreateModule = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@ -38,7 +42,9 @@ export const ModulesListHeader: React.FC = observer(() => {
<ProjectBreadcrumb />
<Breadcrumbs.BreadcrumbItem
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>
</div>
@ -54,7 +60,8 @@ export const ModulesListHeader: React.FC = observer(() => {
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>
) : (
<></>

View file

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

View file

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

View file

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

View file

@ -2,27 +2,35 @@
import { observer } from "mobx-react";
import { useParams, useSearchParams } from "next/navigation";
// types
// plane imports
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TPageNavigationTabs } from "@plane/types";
// components
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { DetailedEmptyState } from "@/components/empty-state";
import { PagesListRoot, PagesListView } from "@/components/pages";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useProject } from "@/hooks/store";
import { useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const ProjectPagesPage = observer(() => {
// router
const router = useAppRouter();
const searchParams = useSearchParams();
const type = searchParams.get("type");
const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// store hooks
const { getProjectById, currentProjectDetails } = useProject();
const { allowPermissions } = useUserPermissions();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/pages" });
const currentPageType = (): TPageNavigationTabs => {
const pageType = type?.toString();
@ -37,9 +45,17 @@ const ProjectPagesPage = observer(() => {
if (currentProjectDetails?.page_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_PAGE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
<DetailedEmptyState
title={t("disabled_project.empty_state.page.title")}
description={t("disabled_project.empty_state.page.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.page.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
/>
</div>
);

View file

@ -3,6 +3,8 @@
import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IProject } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
@ -12,7 +14,6 @@ import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automat
import { PageHead } from "@/components/core";
// hooks
import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const AutomationSettingsPage = observer(() => {
// router
@ -21,6 +22,8 @@ const AutomationSettingsPage = observer(() => {
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentProjectDetails: projectDetails, updateProject } = useProject();
const { t } = useTranslation();
// derived values
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
@ -48,7 +51,7 @@ const AutomationSettingsPage = observer(() => {
<PageHead title={pageTitle} />
<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">
<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>
<AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} />

View file

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

View file

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

View file

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

View file

@ -2,12 +2,12 @@
import { observer } from "mobx-react";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project";
// hooks
import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const MembersSettingsPage = observer(() => {
// store

View file

@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { PageHead } from "@/components/core";
import {
ArchiveRestoreProjectModal,
@ -16,7 +17,6 @@ import {
} from "@/components/project";
// hooks
import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const GeneralSettingsPage = observer(() => {
// states
@ -43,8 +43,6 @@ const GeneralSettingsPage = observer(() => {
);
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
return (
<>

View file

@ -5,6 +5,8 @@ import range from "lodash/range";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Loader } from "@plane/ui";
// components
@ -13,7 +15,6 @@ import { SidebarNavItem } from "@/components/sidebar";
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ProjectSettingsSidebar = observer(() => {
const { workspaceSlug, projectId } = useParams();
@ -21,6 +22,8 @@ export const ProjectSettingsSidebar = observer(() => {
// mobx store
const { allowPermissions, projectUserInfo } = useUserPermissions();
const { t } = useTranslation();
// derived values
const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role;
@ -58,7 +61,7 @@ export const ProjectSettingsSidebar = observer(() => {
isActive={link.highlight(pathname, `/${workspaceSlug}/projects/${projectId}`)}
className="text-sm font-medium px-4 py-2"
>
{link.label}
{t(link.i18n_label)}
</SidebarNavItem>
</Link>
)

View file

@ -2,13 +2,14 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { ProjectStateRoot } from "@/components/project-states";
// hook
import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const StatesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams();
@ -16,6 +17,8 @@ const StatesSettingsPage = observer(() => {
const { currentProjectDetails } = useProject();
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { t } = useTranslation();
// derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined;
// derived values
@ -32,7 +35,7 @@ const StatesSettingsPage = observer(() => {
<>
<PageHead title={pageTitle} />
<div className="flex items-center border-b border-custom-border-100">
<h3 className="text-xl font-medium">States</h3>
<h3 className="text-xl font-medium">{t("common.states")}</h3>
</div>
{workspaceSlug && projectId && (
<ProjectStateRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />

View file

@ -6,7 +6,15 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { Layers, Lock } from "lucide-react";
// plane constants
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
EViewAccess,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@ -16,8 +24,6 @@ import { BreadcrumbLink, Logo } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ViewQuickActions } from "@/components/views";
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EViewAccess } from "@/constants/views";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
import { truncateText } from "@/helpers/string.helper";
@ -35,7 +41,6 @@ import {
} from "@/hooks/store";
// plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ProjectViewIssuesHeader: React.FC = observer(() => {
// refs
@ -242,7 +247,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
projectId={projectId.toString()}
labels={projectLabels}
@ -255,7 +260,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@ -277,7 +282,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
}}
size="sm"
>
Add issue
Add work item
</Button>
) : (
<></>

View file

@ -4,29 +4,37 @@ import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { EUserPermissionsLevel, EUserProjectRoles, EViewAccess } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TViewFilterProps } from "@plane/types";
import { Header, EHeaderVariant } from "@plane/ui";
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { DetailedEmptyState } from "@/components/empty-state";
import { ProjectViewsList } from "@/components/views";
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
import { EmptyStateType } from "@/constants/empty-state";
// constants
import { EViewAccess } from "@/constants/views";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useProject, useProjectView } from "@/hooks/store";
import { useProject, useProjectView, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const ProjectViewsPage = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// store
const { getProjectById, currentProjectDetails } = useProject();
const { filters, updateFilters, clearAllFilters } = useProjectView();
const { allowPermissions } = useUserPermissions();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Views` : undefined;
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/views" });
const handleRemoveFilter = useCallback(
(key: keyof TViewFilterProps, value: string | EViewAccess | null) => {
@ -53,9 +61,17 @@ const ProjectViewsPage = observer(() => {
if (currentProjectDetails?.issue_views_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_VIEW}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
<DetailedEmptyState
title={t("disabled_project.empty_state.view.title")}
description={t("disabled_project.empty_state.view.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.view.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
/>
</div>
);

View file

@ -4,20 +4,20 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/ui";
// component
import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token";
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { DetailedEmptyState } from "@/components/empty-state";
import { APITokenSettingsLoader } from "@/components/ui";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
// store hooks
import { useUserPermissions, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// services
import { APITokenService } from "@/services/api_token.service";
@ -28,11 +28,14 @@ const ApiTokensPage = observer(() => {
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
// router
const { workspaceSlug } = useParams();
// plane hooks
const { t } = useTranslation();
// store hooks
const { currentWorkspace } = useWorkspace();
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
// derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" });
const { data: tokens } = useSWR(
workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null,
@ -40,7 +43,9 @@ const ApiTokensPage = observer(() => {
workspaceSlug && canPerformWorkspaceAdminActions ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
const pageTitle = currentWorkspace?.name
? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}`
: undefined;
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" />;
@ -58,9 +63,9 @@ const ApiTokensPage = observer(() => {
{tokens.length > 0 ? (
<>
<div className="flex items-center justify-between border-b border-custom-border-200 pb-3.5">
<h3 className="text-xl font-medium">API tokens</h3>
<h3 className="text-xl font-medium">{t("workspace_settings.settings.api_tokens.title")}</h3>
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
Add API token
{t("workspace_settings.settings.api_tokens.add_token")}
</Button>
</div>
<div>
@ -72,13 +77,17 @@ const ApiTokensPage = observer(() => {
) : (
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<h3 className="text-xl font-medium">API tokens</h3>
<h3 className="text-xl font-medium">{t("workspace_settings.settings.api_tokens.title")}</h3>
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
Add API token
{t("workspace_settings.settings.api_tokens.add_token")}
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS} />
<DetailedEmptyState
title={t("workspace_settings.empty_state.api_tokens.title")}
description={t("workspace_settings.empty_state.api_tokens.description")}
assetPath={resolvedPath}
/>
</div>
</div>
)}

View file

@ -2,13 +2,13 @@
import { observer } from "mobx-react";
// component
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
// hooks
import { useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web components
import { BillingRoot } from "@/plane-web/components/workspace";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const BillingSettingsPage = observer(() => {
// store hooks

View file

@ -2,6 +2,8 @@
import { observer } from "mobx-react";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import ExportGuide from "@/components/exporter/guide";
@ -9,19 +11,21 @@ import ExportGuide from "@/components/exporter/guide";
import { cn } from "@/helpers/common.helper";
// hooks
import { useUserPermissions, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const ExportsPage = observer(() => {
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const canPerformWorkspaceMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined;
const pageTitle = currentWorkspace?.name
? `${currentWorkspace.name} - ${t("workspace_settings.settings.exports.title")}`
: undefined;
// if user is not authorized to view this page
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
@ -37,7 +41,7 @@ const ExportsPage = observer(() => {
})}
>
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
<h3 className="text-xl font-medium">Exports</h3>
<h3 className="text-xl font-medium">{t("workspace_settings.settings.exports.title")}</h3>
</div>
<ExportGuide />
</div>

View file

@ -4,6 +4,7 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { Settings } from "lucide-react";
// ui
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
@ -12,6 +13,7 @@ import { useWorkspace } from "@/hooks/store";
export const WorkspaceSettingHeader: FC = observer(() => {
const { currentWorkspace, loader } = useWorkspace();
const { t } = useTranslation();
return (
<Header>
@ -27,7 +29,7 @@ export const WorkspaceSettingHeader: FC = observer(() => {
/>
}
/>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Settings" />} />
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={t("settings")} />} />
</Breadcrumbs>
</Header.LeftItem>
</Header>

View file

@ -2,11 +2,11 @@
import { observer } from "mobx-react";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { PageHead } from "@/components/core";
import IntegrationGuide from "@/components/integration/guide";
// hooks
import { useUserPermissions, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const ImportsPage = observer(() => {
// store hooks

View file

@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { PageHead } from "@/components/core";
import { SingleIntegrationCard } from "@/components/integration";
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "@/components/ui";
@ -10,7 +11,6 @@ import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "@/
import { APP_INTEGRATIONS } from "@/constants/fetch-keys";
// hooks
import { useUserPermissions, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services
import { IntegrationService } from "@/services/integrations";

View file

@ -3,12 +3,12 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens";
import { AppHeader } from "@/components/core";
// hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// local components
import { WorkspaceSettingHeader } from "./header";
import { MobileWorkspaceSettingsTabs } from "./mobile-header-tabs";

View file

@ -5,6 +5,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Search } from "lucide-react";
// types
import { MEMBER_INVITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspaceBulkInviteFormData } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
@ -13,13 +15,11 @@ import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "@/components/workspace";
// constants
import { MEMBER_INVITED } from "@/constants/event-tracker";
// helpers
import { cn } from "@/helpers/common.helper";
import { getUserRole } from "@/helpers/user.helper";
// hooks
import { useEventTracker, useMember, useUserPermissions, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const WorkspaceMembersSettingsPage = observer(() => {
// states
@ -34,6 +34,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
workspace: { inviteMembersToWorkspace },
} = useMember();
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
@ -62,7 +63,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Invitations sent successfully.",
message: t("workspace_settings.settings.members.invitations_sent_successfully"),
});
})
.catch((err) => {
@ -80,7 +81,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: `${err.error ?? "Something went wrong. Please try again."}`,
message: `${err.error ?? t("something_went_wrong_please_try_again")}`,
});
});
};
@ -107,12 +108,12 @@ const WorkspaceMembersSettingsPage = observer(() => {
})}
>
<div className="flex justify-between gap-4 pb-3.5 items-start ">
<h4 className="text-xl font-medium">Members</h4>
<h4 className="text-xl font-medium">{t("workspace_settings.settings.members.title")}</h4>
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
<Search className="h-3.5 w-3.5 text-custom-text-400" />
<input
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
placeholder="Search..."
placeholder={`${t("search")}...`}
value={searchQuery}
autoFocus
onChange={(e) => setSearchQuery(e.target.value)}
@ -120,7 +121,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
</div>
{canPerformWorkspaceAdminActions && (
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
Add member
{t("workspace_settings.settings.members.add_member")}
</Button>
)}
</div>

View file

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

View file

@ -2,6 +2,7 @@
import { observer } from "mobx-react";
// components
import { useTranslation } from "@plane/i18n";
import { PageHead } from "@/components/core";
import { WorkspaceDetails } from "@/components/workspace";
// hooks
@ -10,8 +11,11 @@ import { useWorkspace } from "@/hooks/store";
const WorkspaceSettingsPage = observer(() => {
// store hooks
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - General Settings` : undefined;
const pageTitle = currentWorkspace?.name
? t("workspace_settings.page_label", { workspace: currentWorkspace.name })
: undefined;
return (
<>

View file

@ -4,13 +4,12 @@ import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { WORKSPACE_SETTINGS_LINKS, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { SidebarNavItem } from "@/components/sidebar";
// hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@ -19,12 +18,13 @@ export const WorkspaceSettingsSidebar = observer(() => {
const { workspaceSlug } = useParams();
const pathname = usePathname();
// mobx store
const { t } = useTranslation();
const { allowPermissions } = useUserPermissions();
return (
<div className="flex w-[280px] flex-col gap-6">
<div className="flex flex-col gap-2">
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
<span className="text-xs font-semibold text-custom-sidebar-text-400 uppercase">{t("settings")}</span>
<div className="flex w-full flex-col gap-1">
{WORKSPACE_SETTINGS_LINKS.map(
(link) =>
@ -36,7 +36,7 @@ export const WorkspaceSettingsSidebar = observer(() => {
isActive={link.highlight(pathname, `/${workspaceSlug}`)}
className="text-sm font-medium px-4 py-2"
>
{link.label}
{t(link.i18n_label)}
</SidebarNavItem>
</Link>
)

View file

@ -4,6 +4,7 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { IWebhook } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
@ -13,7 +14,6 @@ import { PageHead } from "@/components/core";
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks";
// hooks
import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const WebhookDetailsPage = observer(() => {
// states

View file

@ -4,39 +4,43 @@ import React, { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/ui";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { DetailedEmptyState } from "@/components/empty-state";
import { WebhookSettingsLoader } from "@/components/ui";
import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const WebhooksListPage = observer(() => {
// states
const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false);
// router
const { workspaceSlug } = useParams();
// plane hooks
const { t } = useTranslation();
// mobx store
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace();
// derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/webhooks" });
useSWR(
workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null
);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
const pageTitle = currentWorkspace?.name
? `${currentWorkspace.name} - ${t("workspace_settings.settings.webhooks.title")}`
: undefined;
// clear secret key when modal is closed.
useEffect(() => {
@ -65,9 +69,9 @@ const WebhooksListPage = observer(() => {
{Object.keys(webhooks).length > 0 ? (
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<div className="text-xl font-medium">Webhooks</div>
<div className="text-xl font-medium">{t("workspace_settings.settings.webhooks.title")}</div>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
{t("workspace_settings.settings.webhooks.add_webhook")}
</Button>
</div>
<WebhooksList />
@ -75,13 +79,17 @@ const WebhooksListPage = observer(() => {
) : (
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<div className="text-xl font-medium">Webhooks</div>
<div className="text-xl font-medium">{t("workspace_settings.settings.webhooks.title")}</div>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
{t("workspace_settings.settings.webhooks.add_webhook")}
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS} />
<DetailedEmptyState
title={t("workspace_settings.empty_state.webhooks.title")}
description={t("workspace_settings.empty_state.webhooks.description")}
assetPath={resolvedPath}
/>
</div>
</div>
)}

View file

@ -2,6 +2,7 @@ import { FC, useEffect, useRef } from "react";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react";
// plane helpers
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
// components
import {
@ -22,7 +23,6 @@ import useSize from "@/hooks/use-window-size";
// plane web components
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const AppSidebar: FC = observer(() => {
// store hooks

View file

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

View file

@ -5,7 +5,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Layers } from "lucide-react";
// plane constants
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@ -14,8 +15,6 @@ import { Breadcrumbs, Button, Header } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@ -35,6 +34,7 @@ export const GlobalIssuesHeader = observer(() => {
const {
workspace: { workspaceMemberIds },
} = useMember();
const { t } = useTranslation();
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
@ -105,7 +105,7 @@ export const GlobalIssuesHeader = observer(() => {
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label={`Views`} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
link={<BreadcrumbLink label={t("views")} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</Header.LeftItem>
@ -114,12 +114,12 @@ export const GlobalIssuesHeader = observer(() => {
{!isLocked ? (
<>
<FiltersDropdown
title="Filters"
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
@ -128,9 +128,9 @@ export const GlobalIssuesHeader = observer(() => {
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
@ -143,7 +143,7 @@ export const GlobalIssuesHeader = observer(() => {
)}
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
Add view
{t("workspace_views.add_view")}
</Button>
</Header.RightItem>
</Header>

View file

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

View file

@ -1,5 +1,6 @@
"use client";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
@ -7,10 +8,10 @@ import { useTheme } from "next-themes";
import { Controller, useForm } from "react-hook-form";
// icons
import { CircleCheck } from "lucide-react";
// ui
// plane imports
import { FORGOT_PASS_LINK, NAVIGATE_TO_SIGNUP } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// constants
import { FORGOT_PASS_LINK, NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
import { cn } from "@/helpers/common.helper";
@ -20,12 +21,12 @@ import { useEventTracker } from "@/hooks/store";
import useTimer from "@/hooks/use-timer";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
// services
// images
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
// services
import { AuthService } from "@/services/auth.service";
type TForgotPasswordFormValues = {
@ -39,10 +40,12 @@ const defaultValues: TForgotPasswordFormValues = {
// services
const authService = new AuthService();
export default function ForgotPasswordPage() {
const ForgotPasswordPage = observer(() => {
// search params
const searchParams = useSearchParams();
const email = searchParams.get("email");
// plane hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
// hooks
@ -73,9 +76,8 @@ export default function ForgotPasswordPage() {
});
setToast({
type: TOAST_TYPE.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.",
title: t("auth.forgot_password.toast.success.title"),
message: t("auth.forgot_password.toast.success.message"),
});
setResendCodeTimer(30);
})
@ -85,8 +87,8 @@ export default function ForgotPasswordPage() {
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
title: t("auth.forgot_password.toast.error.title"),
message: err?.error ?? t("auth.forgot_password.toast.error.message"),
});
});
};
@ -111,13 +113,13 @@ export default function ForgotPasswordPage() {
</Link>
</div>
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
New to Plane?{" "}
{t("auth.common.new_to_plane")}
<Link
href="/"
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
className="font-semibold text-custom-primary-100 hover:underline"
>
Create an account
{t("auth.common.create_account")}
</Link>
</div>
</div>
@ -125,23 +127,21 @@ export default function ForgotPasswordPage() {
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1 py-4">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Reset your password
{t("auth.forgot_password.title")}
</h3>
<p className="font-medium text-onboarding-text-400">
Enter your user account{"'"}s verified email address and we will send you a password reset link.
</p>
<p className="font-medium text-onboarding-text-400">{t("auth.forgot_password.description")}</p>
</div>
<form onSubmit={handleSubmit(handleForgotPassword)} className="mt-5 space-y-4">
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
{t("auth.common.email.label")}
</label>
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
required: t("auth.common.email.errors.required"),
validate: (value) => checkEmailValidity(value) || t("auth.common.email.errors.invalid"),
}}
render={({ field: { value, onChange, ref } }) => (
<Input
@ -152,7 +152,7 @@ export default function ForgotPasswordPage() {
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="name@company.com"
placeholder={t("auth.common.email.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
autoComplete="on"
disabled={resendTimerCode > 0}
@ -162,7 +162,7 @@ export default function ForgotPasswordPage() {
{resendTimerCode > 0 && (
<p className="flex w-full items-start px-1 gap-1 text-xs font-medium text-green-700">
<CircleCheck height={12} width={12} className="mt-0.5" />
We sent the reset link to your email address
{t("auth.forgot_password.email_sent")}
</p>
)}
</div>
@ -174,10 +174,12 @@ export default function ForgotPasswordPage() {
disabled={!isValid}
loading={isSubmitting || resendTimerCode > 0}
>
{resendTimerCode > 0 ? `Resend in ${resendTimerCode} seconds` : "Send reset link"}
{resendTimerCode > 0
? t("auth.common.resend_in", { seconds: resendTimerCode })
: t("auth.forgot_password.send_reset_link")}
</Button>
<Link href="/" className={cn("w-full", getButtonStyling("link-neutral", "lg"))}>
Back to sign in
{t("auth.common.back_to_sign_in")}
</Link>
</form>
</div>
@ -186,4 +188,6 @@ export default function ForgotPasswordPage() {
</div>
</AuthenticationWrapper>
);
}
});
export default ForgotPasswordPage;

View file

@ -1,6 +1,7 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
@ -8,6 +9,7 @@ import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
import { Eye, EyeOff } from "lucide-react";
// ui
import { useTranslation } from "@plane/i18n";
import { Button, Input } from "@plane/ui";
// components
import { AuthBanner, PasswordStrengthMeter } from "@/components/account";
@ -45,7 +47,7 @@ const defaultValues: TResetPasswordFormValues = {
// services
const authService = new AuthService();
export default function ResetPasswordPage() {
const ResetPasswordPage = observer(() => {
// search params
const searchParams = useSearchParams();
const uidb64 = searchParams.get("uidb64");
@ -65,7 +67,8 @@ export default function ResetPasswordPage() {
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false);
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
// plane hooks
const { t } = useTranslation();
// hooks
const { resolvedTheme } = useTheme();
@ -127,9 +130,9 @@ export default function ResetPasswordPage() {
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1 py-4">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Set new password
{t("auth.reset_password.title")}
</h3>
<p className="font-medium text-onboarding-text-400">Secure your account with a strong password</p>
<p className="font-medium text-onboarding-text-400">{t("auth.reset_password.description")}</p>
</div>
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
@ -142,7 +145,7 @@ export default function ResetPasswordPage() {
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
{t("auth.common.email.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
@ -151,7 +154,7 @@ export default function ResetPasswordPage() {
type="email"
value={resetFormData.email}
//hasError={Boolean(errors.email)}
placeholder="name@company.com"
placeholder={t("auth.common.email.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
autoComplete="on"
disabled
@ -160,7 +163,7 @@ export default function ResetPasswordPage() {
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Password
{t("auth.common.password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
@ -169,7 +172,7 @@ export default function ResetPasswordPage() {
value={resetFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder="Enter password"
placeholder={t("auth.common.password.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
@ -193,7 +196,7 @@ export default function ResetPasswordPage() {
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
{t("auth.common.password.confirm_password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
@ -201,7 +204,7 @@ export default function ResetPasswordPage() {
name="confirm_password"
value={resetFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
placeholder={t("auth.common.password.confirm_password.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
@ -220,10 +223,12 @@ export default function ResetPasswordPage() {
</div>
{!!resetFormData.confirm_password &&
resetFormData.password !== resetFormData.confirm_password &&
renderPasswordMatchError && <span className="text-sm text-red-500">Passwords don{"'"}t match</span>}
renderPasswordMatchError && (
<span className="text-sm text-red-500">{t("auth.common.password.errors.match")}</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Set password
{t("auth.common.password.submit")}
</Button>
</form>
</div>
@ -232,4 +237,6 @@ export default function ResetPasswordPage() {
</div>
</AuthenticationWrapper>
);
}
});
export default ResetPasswordPage;

View file

@ -8,7 +8,8 @@ import { useSearchParams } from "next/navigation";
// icons
import { useTheme } from "next-themes";
import { Eye, EyeOff } from "lucide-react";
// ui
// plane imports
import { useTranslation } from "@plane/i18n";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { PasswordStrengthMeter } from "@/components/account";
@ -60,9 +61,10 @@ const SetPasswordPage = observer(() => {
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false);
// plane hooks
const { t } = useTranslation();
// hooks
const { resolvedTheme } = useTheme();
// hooks
const { data: user, handleSetPassword } = useUser();
useEffect(() => {
@ -95,8 +97,8 @@ const SetPasswordPage = observer(() => {
} catch (err: any) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
title: t("common.errors.default.title"),
message: err?.error ?? t("common.errors.default.message"),
});
}
};
@ -108,7 +110,8 @@ const SetPasswordPage = observer(() => {
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<AuthenticationWrapper pageType={EPageTypes.SET_PASSWORD}>
// TODO: change to EPageTypes.SET_PASSWORD
<AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
<div className="relative w-screen h-screen overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
@ -129,14 +132,14 @@ const SetPasswordPage = observer(() => {
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1 py-4">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Secure your account
{t("auth.set_password.title")}
</h3>
<p className="font-medium text-onboarding-text-400">Setting password helps you login securely</p>
<p className="font-medium text-onboarding-text-400">{t("auth.set_password.description")}</p>
</div>
<form className="mt-5 space-y-4" onSubmit={(e) => handleSubmit(e)}>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
{t("auth.common.email.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
@ -145,7 +148,7 @@ const SetPasswordPage = observer(() => {
type="email"
value={user?.email}
//hasError={Boolean(errors.email)}
placeholder="name@company.com"
placeholder={t("auth.common.email.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
autoComplete="on"
disabled
@ -154,7 +157,7 @@ const SetPasswordPage = observer(() => {
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Set a password
{t("auth.common.password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
@ -163,7 +166,7 @@ const SetPasswordPage = observer(() => {
value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder="Enter password"
placeholder={t("auth.common.password.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
@ -187,7 +190,7 @@ const SetPasswordPage = observer(() => {
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
{t("auth.common.password.confirm_password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
@ -195,7 +198,7 @@ const SetPasswordPage = observer(() => {
name="confirm_password"
value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
placeholder={t("auth.common.password.confirm_password.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
@ -214,10 +217,12 @@ const SetPasswordPage = observer(() => {
</div>
{!!passwordFormData.confirm_password &&
passwordFormData.password !== passwordFormData.confirm_password &&
renderPasswordMatchError && <span className="text-sm text-red-500">Passwords don{"'"}t match</span>}
renderPasswordMatchError && (
<span className="text-sm text-red-500">{t("auth.common.password.errors.match")}</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Continue
{t("common.continue")}
</Button>
</form>
</div>

View file

@ -42,11 +42,12 @@ const CreateWorkspacePage = observer(() => {
// methods
const getMailtoHref = () => {
const subject = t("workspace_request_subject");
const body = t("workspace_request_body")
.replace("{{firstName}}", currentUser?.first_name || "")
.replace("{{lastName}}", currentUser?.last_name || "")
.replace("{{email}}", currentUser?.email || "");
const subject = t("workspace_creation.request_email.subject");
const body = t("workspace_creation.request_email.body", {
firstName: currentUser?.first_name || "",
lastName: currentUser?.last_name || "",
email: currentUser?.email || "",
});
return `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
};
@ -67,7 +68,7 @@ const CreateWorkspacePage = observer(() => {
href="/"
>
<div className="h-[30px] w-[133px]">
<Image src={logo} alt={t("plane_logo")} />
<Image src={logo} alt="Plane logo" />
</div>
</Link>
<div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5">
@ -77,30 +78,25 @@ const CreateWorkspacePage = observer(() => {
<div className="relative flex h-full justify-center px-8 pb-8 sm:w-10/12 sm:items-center sm:justify-start sm:p-0 sm:pr-[8.33%] md:w-9/12 lg:w-4/5">
{isWorkspaceCreationDisabled ? (
<div className="w-4/5 h-full flex flex-col items-center justify-center text-lg font-medium gap-1">
<Image
src={WorkspaceCreationDisabled}
width={200}
alt={t("workspace_creation_disabled")}
className="mb-4"
/>
<Image src={WorkspaceCreationDisabled} width={200} alt="Workspace creation disabled" className="mb-4" />
<div className="text-lg font-medium text-center">
{t("only_your_instance_admin_can_create_workspaces")}
{t("workspace_creation.errors.creation_disabled.title")}
</div>
<p className="text-sm text-custom-text-300 break-words text-center">
{t("only_your_instance_admin_can_create_workspaces_description")}
{t("workspace_creation.errors.creation_disabled.description")}
</p>
<div className="flex gap-4 mt-6">
<Button variant="primary" onClick={() => router.back()}>
{t("go_back")}
{t("common.go_back")}
</Button>
<a href={getMailtoHref()} className={getButtonStyling("outline-primary", "md")}>
{t("request_instance_admin")}
{t("workspace_creation.errors.creation_disabled.request_button")}
</a>
</div>
</div>
) : (
<div className="w-full space-y-7 sm:space-y-10">
<h4 className="text-2xl font-semibold">{t("create_your_workspace")}</h4>
<h4 className="text-2xl font-semibold">{t("workspace_creation.heading")}</h4>
<div className="sm:w-3/4 md:w-2/5">
<CreateWorkspaceForm
onSubmit={onSubmit}

View file

@ -8,6 +8,8 @@ import Link from "next/link";
import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr";
import { CheckCircle2 } from "lucide-react";
// plane imports
import { ROLE, MEMBER_ACCEPTED, EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import type { IWorkspaceMemberInvitation } from "@plane/types";
@ -15,10 +17,7 @@ import type { IWorkspaceMemberInvitation } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { EmptyState } from "@/components/common";
// constants
import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
import { ROLE } from "@/constants/workspace";
// helpers
import { truncateText } from "@/helpers/string.helper";
import { getUserRole } from "@/helpers/user.helper";
@ -27,8 +26,6 @@ import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/
import { useAppRouter } from "@/hooks/use-app-router";
// services
import { AuthenticationWrapper } from "@/lib/wrappers";
// plane web constants
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
// plane web services
import { WorkspaceService } from "@/plane-web/services";
// images

View file

@ -6,7 +6,8 @@ import "@/styles/command-pallette.css";
import "@/styles/emoji.css";
import "@/styles/react-day-picker.css";
// meta data info
import { SITE_NAME, SITE_DESCRIPTION } from "@/constants/meta";
import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// local
@ -17,12 +18,11 @@ export const metadata: Metadata = {
description: SITE_DESCRIPTION,
openGraph: {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
description: "Open-source project management tool to manage work items, cycles, and product roadmaps easily",
url: "https://app.plane.so/",
},
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 item tracking, agile, scrum, kanban, collaboration",
twitter: {
site: "@planepowers",
},

View file

@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// types
import { USER_ONBOARDING_COMPLETED } from "@plane/constants";
import { TOnboardingSteps, TUserProfile } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
@ -11,7 +12,6 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
import { LogoSpinner } from "@/components/common";
import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding";
// constants
import { USER_ONBOARDING_COMPLETED } from "@/constants/event-tracker";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";

View file

@ -7,10 +7,11 @@ import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components
import { NAVIGATE_TO_SIGNUP } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AuthRoot } from "@/components/account";
import { PageHead } from "@/components/core";
// constants
import { NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker";
// helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks
@ -27,6 +28,8 @@ import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue
const HomePage = observer(() => {
const { resolvedTheme } = useTheme();
// plane hooks
const { t } = useTranslation();
// hooks
const { captureEvent } = useEventTracker();
@ -37,7 +40,7 @@ const HomePage = observer(() => {
<AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
<>
<div className="relative w-screen h-screen overflow-hidden">
<PageHead title="Log in - Plane" />
<PageHead title={t("auth.common.login") + " - Plane"} />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
@ -53,13 +56,13 @@ const HomePage = observer(() => {
</Link>
</div>
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
New to Plane?{" "}
{t("auth.common.new_to_plane")}
<Link
href="/sign-up"
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
className="font-semibold text-custom-primary-100 hover:underline"
>
Create an account
{t("auth.common.create_account")}
</Link>
</div>
</div>

View file

@ -7,24 +7,27 @@ import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/ui";
// components
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { DetailedEmptyState } from "@/components/empty-state";
import {
ProfileActivityListPage,
ProfileSettingContentHeader,
ProfileSettingContentWrapper,
} from "@/components/profile";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const PER_PAGE = 100;
const ProfileActivityPage = observer(() => {
const { t } = useTranslation();
// states
const [pageCount, setPageCount] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [resultsCount, setResultsCount] = useState(0);
const [isEmpty, setIsEmpty] = useState(false);
// plane hooks
const { t } = useTranslation();
// derived values
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/profile/activity" });
const updateTotalPages = (count: number) => setTotalPages(count);
@ -50,7 +53,13 @@ const ProfileActivityPage = observer(() => {
const isLoadMoreVisible = pageCount < totalPages && resultsCount !== 0;
if (isEmpty) {
return <EmptyState type={EmptyStateType.PROFILE_ACTIVITY} layout="screen-detailed" />;
return (
<DetailedEmptyState
title={t("profile.empty_state.activity.title")}
description={t("profile.empty_state.activity.description")}
assetPath={resolvedPath}
/>
);
}
return (

View file

@ -3,6 +3,8 @@
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useTheme } from "next-themes";
// plane imports
import { I_THEME_OPTION, THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IUserTheme } from "@plane/types";
import { setPromiseToast } from "@plane/ui";
@ -11,7 +13,6 @@ import { LogoSpinner } from "@/components/common";
import { CustomThemeSelector, ThemeSwitch, PageHead } from "@/components/core";
import { ProfileSettingContentHeader, ProfileSettingContentWrapper } from "@/components/profile";
// constants
import { I_THEME_OPTION, THEME_OPTIONS } from "@/constants/themes";
// helpers
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
// hooks

View file

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

View file

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

View file

@ -79,16 +79,16 @@ const SecurityPage = observer(() => {
setShowPassword(defaultShowPassword);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("success"),
message: t("password_changed_successfully"),
title: t("auth.common.password.toast.change_password.success.title"),
message: t("auth.common.password.toast.change_password.success.message"),
});
} catch (err: any) {
const errorInfo = authErrorHandler(err.error_code?.toString());
setToast({
type: TOAST_TYPE.ERROR,
title: errorInfo?.title ?? "Error!",
title: errorInfo?.title ?? t("auth.common.password.toast.error.title"),
message:
typeof errorInfo?.message === "string" ? errorInfo.message : t("something_went_wrong_please_try_again"),
typeof errorInfo?.message === "string" ? errorInfo.message : t("auth.common.password.toast.error.message"),
});
}
};
@ -112,17 +112,17 @@ const SecurityPage = observer(() => {
<>
<PageHead title="Profile - Security" />
<ProfileSettingContentWrapper>
<ProfileSettingContentHeader title={t("change_password")} />
<ProfileSettingContentHeader title={t("auth.common.password.change_password.label.default")} />
<form onSubmit={handleSubmit(handleChangePassword)} className="flex flex-col gap-8 py-6">
<div className="flex flex-col gap-10 w-full max-w-96">
<div className="space-y-1">
<h4 className="text-sm">{t("current_password")}</h4>
<h4 className="text-sm">{t("auth.common.password.current_password.label")}</h4>
<div className="relative flex items-center rounded-md">
<Controller
control={control}
name="old_password"
rules={{
required: t("this_field_is_required"),
required: t("common.errors.required"),
}}
render={({ field: { value, onChange } }) => (
<Input
@ -151,20 +151,20 @@ const SecurityPage = observer(() => {
{errors.old_password && <span className="text-xs text-red-500">{errors.old_password.message}</span>}
</div>
<div className="space-y-1">
<h4 className="text-sm">{t("new_password")}</h4>
<h4 className="text-sm">{t("auth.common.password.new_password.label")}</h4>
<div className="relative flex items-center rounded-md">
<Controller
control={control}
name="new_password"
rules={{
required: t("this_field_is_required"),
required: t("common.errors.required"),
}}
render={({ field: { value, onChange } }) => (
<Input
id="new_password"
type={showPassword?.password ? "text" : "password"}
value={value}
placeholder={t("new_password")}
placeholder={t("auth.common.password.new_password.placeholder")}
onChange={onChange}
className="w-full"
hasError={Boolean(errors.new_password)}
@ -191,19 +191,19 @@ const SecurityPage = observer(() => {
)}
</div>
<div className="space-y-1">
<h4 className="text-sm">{t("confirm_password")}</h4>
<h4 className="text-sm">{t("auth.common.password.confirm_password.label")}</h4>
<div className="relative flex items-center rounded-md">
<Controller
control={control}
name="confirm_password"
rules={{
required: t("this_field_is_required"),
required: t("common.errors.required"),
}}
render={({ field: { value, onChange } }) => (
<Input
id="confirm_password"
type={showPassword?.confirmPassword ? "text" : "password"}
placeholder={t("confirm_password")}
placeholder={t("auth.common.password.confirm_password.placeholder")}
value={value}
onChange={onChange}
className="w-full"
@ -226,14 +226,16 @@ const SecurityPage = observer(() => {
)}
</div>
{!!confirmPassword && password !== confirmPassword && renderPasswordMatchError && (
<span className="text-sm text-red-500">{t("passwords_dont_match")}</span>
<span className="text-sm text-red-500">{t("auth.common.password.errors.match")}</span>
)}
</div>
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isSubmitting} disabled={isButtonDisabled}>
{isSubmitting ? `${t("changing_password")}...` : t("change_password")}
{isSubmitting
? `${t("auth.common.password.change_password.label.submitting")}`
: t("auth.common.password.change_password.label.default")}
</Button>
</div>
</form>

View file

@ -5,15 +5,26 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
// icons
import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react";
import {
ChevronLeft,
LogOut,
MoveLeft,
Plus,
UserPlus,
Activity,
Bell,
CircleUser,
KeyRound,
Settings2,
} from "lucide-react";
// plane imports
import { PROFILE_ACTION_LINKS } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// components
import { SidebarNavItem } from "@/components/sidebar";
// constants
import { PROFILE_ACTION_LINKS } from "@/constants/profile";
// helpers
import { cn } from "@/helpers/common.helper";
import { getFileURL } from "@/helpers/file.helper";
@ -36,6 +47,19 @@ const WORKSPACE_ACTION_LINKS = [
},
];
export const ProjectActionIcons = ({ type, size, className }: { type: string; size?: number; className?: string }) => {
const icons = {
profile: CircleUser,
security: KeyRound,
activity: Activity,
appearance: Settings2,
notifications: Bell,
};
if (type === undefined) return null;
const Icon = icons[type as keyof typeof icons];
return <Icon size={size} className={className} />;
};
export const ProfileLayoutSidebar = observer(() => {
// states
const [isSigningOut, setIsSigningOut] = useState(false);
@ -92,8 +116,8 @@ export const ProfileLayoutSidebar = observer(() => {
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
message: t("failed_to_sign_out_please_try_again"),
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
})
)
.finally(() => setIsSigningOut(false));
@ -145,8 +169,9 @@ export const ProfileLayoutSidebar = observer(() => {
isActive={link.highlight(pathname)}
>
<div className="flex items-center gap-1.5 py-[1px]">
<link.Icon className="size-4" />
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(link.key)}</p>}
<ProjectActionIcons type={link.key} size={16} />
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(link.i18n_label)}</p>}
</div>
</SidebarNavItem>
</Tooltip>

View file

@ -5,10 +5,9 @@ import dynamic from "next/dynamic";
import { useTheme, ThemeProvider } from "next-themes";
import { SWRConfig } from "swr";
// Plane Imports
import { WEB_SWR_CONFIG } from "@plane/constants";
import { TranslationProvider } from "@plane/i18n";
import { Toast } from "@plane/ui";
// constants
import { SWR_CONFIG } from "@/constants/swr-config";
//helpers
import { resolveGeneralTheme } from "@/helpers/theme.helper";
// nprogress
@ -47,7 +46,7 @@ export const AppProvider: FC<IAppProvider> = (props) => {
<InstanceWrapper>
<IntercomProvider>
<PostHogProvider>
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
</PostHogProvider>
</IntercomProvider>
</InstanceWrapper>

View file

@ -6,9 +6,10 @@ import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components
import { NAVIGATE_TO_SIGNIN } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AuthRoot } from "@/components/account";
// constants
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
// helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks
@ -23,6 +24,8 @@ import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue
export type AuthType = "sign-in" | "sign-up";
const SignInPage = observer(() => {
// plane hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
// hooks
@ -48,13 +51,13 @@ const SignInPage = observer(() => {
</Link>
</div>
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
Already have an account?{" "}
{t("auth.common.already_have_an_account")}
<Link
href="/"
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
className="font-semibold text-custom-primary-100 hover:underline"
>
Log in
{t("auth.common.login")}
</Link>
</div>
</div>

View file

@ -82,7 +82,7 @@ const WorkspaceInvitationPage = observer(() => {
) : (
<EmptySpace
title={`You have been invited to ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account."
>
<EmptySpaceItem Icon={Check} title="Accept" action={handleAccept} />
<EmptySpaceItem Icon={X} title="Ignore" action={handleReject} />
@ -92,14 +92,14 @@ const WorkspaceInvitationPage = observer(() => {
invitationDetail?.accepted ? (
<EmptySpace
title={`You are already a member of ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account."
>
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
</EmptySpace>
) : (
<EmptySpace
title="This invitation link is not active anymore."
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account."
link={{ text: "Or start from an empty project", href: "/" }}
>
{!currentUser ? (