forked from github/plane
chore: adding page titles using project title. (#3692)
* chore: adding page titles * chore: added title to remaining pages * fix: added observer at required places --------- Co-authored-by: LAKHAN BAHETI <lakhanbaheti9@gmail.com>
This commit is contained in:
parent
07a4cb1f7d
commit
cf3b888465
@ -4,3 +4,4 @@ export * from "./sidebar";
|
||||
export * from "./theme";
|
||||
export * from "./activity";
|
||||
export * from "./image-picker-popover";
|
||||
export * from "./page-title";
|
||||
|
18
web/components/core/page-title.tsx
Normal file
18
web/components/core/page-title.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import Head from "next/head";
|
||||
|
||||
type PageHeadTitleProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export const PageHead: React.FC<PageHeadTitleProps> = (props) => {
|
||||
const { title } = props;
|
||||
|
||||
if (!title) return null;
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
);
|
||||
};
|
@ -6,6 +6,7 @@ import { useApplication, useUser } from "hooks/store";
|
||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||
// components
|
||||
import { SignInRoot } from "components/account";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// images
|
||||
@ -34,19 +35,22 @@ export const SignInView = observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
<>
|
||||
<PageHead title="Sign In" />
|
||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
|
||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||
<SignInRoot />
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
|
||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||
<SignInRoot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -2,7 +2,8 @@ import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
// layouts
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
// ui
|
||||
@ -14,6 +15,7 @@ import type { NextPage } from "next";
|
||||
|
||||
const PageNotFound: NextPage = () => (
|
||||
<DefaultLayout>
|
||||
<PageHead title="404 - Page Not Found" />
|
||||
<div className="grid h-full place-items-center p-4">
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
||||
|
@ -1,13 +1,28 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { WorkspaceActiveCycleHeader } from "components/headers";
|
||||
import { WorkspaceActiveCyclesUpgrade } from "components/workspace";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// hooks
|
||||
import { useWorkspace } from "hooks/store";
|
||||
|
||||
const WorkspaceActiveCyclesPage: NextPageWithLayout = () => <WorkspaceActiveCyclesUpgrade />;
|
||||
const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Active Cycles` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceActiveCyclesUpgrade />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<WorkspaceActiveCycleHeader />}>{page}</AppLayout>;
|
||||
|
@ -1,22 +1,23 @@
|
||||
import React, { Fragment, ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||
import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
import { WorkspaceAnalyticsHeader } from "components/headers";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { ANALYTICS_TABS } from "constants/analytics";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
// type
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { useRouter } from "next/router";
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
@ -33,13 +34,16 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { workspaceProjectIds } = useProject();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode);
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
||||
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
||||
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
||||
|
@ -1,13 +1,28 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { WorkspaceDashboardView } from "components/page-views";
|
||||
import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// hooks
|
||||
import { useWorkspace } from "hooks/store";
|
||||
|
||||
const WorkspacePage: NextPageWithLayout = () => <WorkspaceDashboardView />;
|
||||
const WorkspacePage: NextPageWithLayout = observer(() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Dashboard` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceDashboardView />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
WorkspacePage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<WorkspaceDashboardHeader />}>{page}</AppLayout>;
|
||||
|
@ -4,11 +4,17 @@ import { AppLayout } from "layouts/app-layout";
|
||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||
// components
|
||||
import { UserProfileHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
||||
|
||||
const ProfileAssignedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="assigned" />;
|
||||
const ProfileAssignedIssuesPage: NextPageWithLayout = () => (
|
||||
<>
|
||||
<PageHead title="Profile - Assigned" />
|
||||
<ProfileIssuesPage type="assigned" />
|
||||
</>
|
||||
);
|
||||
|
||||
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -6,11 +6,17 @@ import { AppLayout } from "layouts/app-layout";
|
||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||
// components
|
||||
import { UserProfileHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
||||
|
||||
const ProfileCreatedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="created" />;
|
||||
const ProfileCreatedIssuesPage: NextPageWithLayout = () => (
|
||||
<>
|
||||
<PageHead title="Profile - Created" />
|
||||
<ProfileIssuesPage type="created" />
|
||||
</>
|
||||
);
|
||||
|
||||
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -8,6 +8,7 @@ import { AppLayout } from "layouts/app-layout";
|
||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||
// components
|
||||
import { UserProfileHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
import {
|
||||
ProfileActivity,
|
||||
ProfilePriorityDistribution,
|
||||
@ -42,21 +43,24 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9">
|
||||
<ProfileStats userProfile={userProfile} />
|
||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||
<div className="grid grid-cols-1 items-stretch gap-5 xl:grid-cols-2">
|
||||
<ProfilePriorityDistribution userProfile={userProfile} />
|
||||
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
|
||||
<>
|
||||
<PageHead title="Profile - Summary" />
|
||||
<div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9">
|
||||
<ProfileStats userProfile={userProfile} />
|
||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||
<div className="grid grid-cols-1 items-stretch gap-5 xl:grid-cols-2">
|
||||
<ProfilePriorityDistribution userProfile={userProfile} />
|
||||
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
|
||||
</div>
|
||||
<ProfileActivity />
|
||||
</div>
|
||||
<ProfileActivity />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AppLayout header={<UserProfileHeader type='Summary' />}>
|
||||
<AppLayout header={<UserProfileHeader type="Summary" />}>
|
||||
<ProfileAuthWrapper>{page}</ProfileAuthWrapper>
|
||||
</AppLayout>
|
||||
);
|
||||
|
@ -6,11 +6,17 @@ import { AppLayout } from "layouts/app-layout";
|
||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||
// components
|
||||
import { UserProfileHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
||||
|
||||
const ProfileSubscribedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="subscribed" />;
|
||||
const ProfileSubscribedIssuesPage: NextPageWithLayout = () => (
|
||||
<>
|
||||
<PageHead title="Profile - Subscribed" />
|
||||
<ProfileIssuesPage type="subscribed" />
|
||||
</>
|
||||
);
|
||||
|
||||
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState, ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
@ -9,6 +10,7 @@ import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { IssueDetailRoot } from "components/issues";
|
||||
import { ProjectArchivedIssueDetailsHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { ArchiveIcon, Loader } from "@plane/ui";
|
||||
// icons
|
||||
@ -18,7 +20,7 @@ import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
||||
const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, archivedIssueId } = router.query;
|
||||
@ -45,6 +47,9 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
||||
);
|
||||
|
||||
const issue = getIssueById(archivedIssueId?.toString() || "") || undefined;
|
||||
const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined;
|
||||
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
const handleUnArchive = async () => {
|
||||
@ -79,6 +84,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{issueLoader ? (
|
||||
<Loader className="flex h-full gap-5 p-5">
|
||||
<div className="basis-2/3 space-y-2">
|
||||
@ -126,7 +132,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ArchivedIssueDetailsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,38 +1,51 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// contexts
|
||||
import { ArchivedIssueLayoutRoot } from "components/issues";
|
||||
// ui
|
||||
import { ArchiveIcon } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectArchivedIssuesHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
|
||||
const ProjectArchivedIssuesPage: NextPageWithLayout = () => {
|
||||
const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name && `${project?.name} - Archived Issues`;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
<span>Archived Issues</span>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
<span>Archived Issues</span>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<ArchivedIssueLayoutRoot />
|
||||
</div>
|
||||
<ArchivedIssueLayoutRoot />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useCycle } from "hooks/store";
|
||||
import { useCycle, useProject } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { CycleIssuesHeader } from "components/headers";
|
||||
import { CycleDetailsSidebar } from "components/cycles";
|
||||
import { CycleLayoutRoot } from "components/issues/issue-layouts";
|
||||
@ -17,27 +19,36 @@ import emptyCycle from "public/empty-state/cycle.svg";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const CycleDetailPage: NextPageWithLayout = () => {
|
||||
const CycleDetailPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
// store hooks
|
||||
const { fetchCycleDetails } = useCycle();
|
||||
|
||||
const { fetchCycleDetails, getCycleById } = useCycle();
|
||||
const { getProjectById } = useProject();
|
||||
// hooks
|
||||
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||
|
||||
// fetching cycle details
|
||||
const { error } = useSWR(
|
||||
workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null,
|
||||
workspaceSlug && projectId && cycleId
|
||||
? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||
: null
|
||||
);
|
||||
// derived values
|
||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||
const cycle = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name && cycle?.name ? `${project?.name} - ${cycle?.name}` : undefined;
|
||||
|
||||
/**
|
||||
* Toggles the sidebar
|
||||
*/
|
||||
const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{error ? (
|
||||
<EmptyState
|
||||
image={emptyCycle}
|
||||
@ -70,7 +81,7 @@ const CycleDetailPage: NextPageWithLayout = () => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
CycleDetailPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -4,11 +4,12 @@ import { observer } from "mobx-react-lite";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle, useUser } from "hooks/store";
|
||||
import { useEventTracker, useCycle, useUser, useProject } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { CyclesHeader } from "components/headers";
|
||||
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
@ -34,12 +35,20 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { currentProjectCycleIds, loader } = useCycle();
|
||||
const { getProjectById } = useProject();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||
// local storage
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
|
||||
const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
|
||||
// derived values
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
|
||||
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
|
||||
|
||||
const handleCurrentLayout = useCallback(
|
||||
(_layout: TCycleLayout) => {
|
||||
@ -56,13 +65,6 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
[handleCurrentLayout, setCycleTab]
|
||||
);
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
|
||||
|
||||
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
|
||||
if (loader)
|
||||
@ -75,143 +77,146 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<CycleCreateUpdateModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isOpen={createModal}
|
||||
handleClose={() => setCreateModal(false)}
|
||||
/>
|
||||
{totalCycles === 0 ? (
|
||||
<div className="h-full place-items-center">
|
||||
<EmptyState
|
||||
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
|
||||
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
|
||||
image={EmptyStateImagePath}
|
||||
comicBox={{
|
||||
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
|
||||
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Cycle empty state");
|
||||
setCreateModal(true);
|
||||
},
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Tab.Group
|
||||
as="div"
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
||||
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
||||
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
|
||||
>
|
||||
<div className="flex flex-col items-start justify-between gap-4 border-b border-custom-border-200 px-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
|
||||
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
||||
{CYCLE_TAB_LIST.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`border-b-2 p-4 text-sm font-medium outline-none ${
|
||||
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<div className="hidden sm:block">
|
||||
{cycleTab !== "active" && (
|
||||
<div className="flex items-center self-end sm:self-center md:self-center lg:self-center gap-1 rounded bg-custom-background-80 p-1">
|
||||
{CYCLE_VIEW_LAYOUTS.map((layout) => {
|
||||
if (layout.key === "gantt" && cycleTab === "draft") return null;
|
||||
|
||||
return (
|
||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||
<button
|
||||
type="button"
|
||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||
}`}
|
||||
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
|
||||
>
|
||||
<layout.icon
|
||||
strokeWidth={2}
|
||||
className={`h-3.5 w-3.5 ${
|
||||
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-full h-full">
|
||||
<CycleCreateUpdateModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isOpen={createModal}
|
||||
handleClose={() => setCreateModal(false)}
|
||||
/>
|
||||
{totalCycles === 0 ? (
|
||||
<div className="h-full place-items-center">
|
||||
<EmptyState
|
||||
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
|
||||
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
|
||||
image={EmptyStateImagePath}
|
||||
comicBox={{
|
||||
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
|
||||
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Cycle empty state");
|
||||
setCreateModal(true);
|
||||
},
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Tab.Group
|
||||
as="div"
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
||||
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)}
|
||||
onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")}
|
||||
>
|
||||
<div className="flex flex-col items-start justify-between gap-4 border-b border-custom-border-200 px-4 sm:flex-row sm:items-center sm:px-5 sm:pb-0">
|
||||
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
||||
{CYCLE_TAB_LIST.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`border-b-2 p-4 text-sm font-medium outline-none ${
|
||||
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<div className="hidden sm:block">
|
||||
{cycleTab !== "active" && (
|
||||
<div className="flex items-center self-end sm:self-center md:self-center lg:self-center gap-1 rounded bg-custom-background-80 p-1">
|
||||
{CYCLE_VIEW_LAYOUTS.map((layout) => {
|
||||
if (layout.key === "gantt" && cycleTab === "draft") return null;
|
||||
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
{cycleTab && cycleLayout && (
|
||||
<CyclesView
|
||||
filter="all"
|
||||
layout={cycleLayout}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
peekCycle={peekCycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
return (
|
||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||
<button
|
||||
type="button"
|
||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||
}`}
|
||||
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
|
||||
>
|
||||
<layout.icon
|
||||
strokeWidth={2}
|
||||
className={`h-3.5 w-3.5 ${
|
||||
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
|
||||
<ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
{cycleTab && cycleLayout && (
|
||||
<CyclesView
|
||||
filter="all"
|
||||
layout={cycleLayout}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
peekCycle={peekCycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
{cycleTab && cycleLayout && (
|
||||
<CyclesView
|
||||
filter="upcoming"
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
peekCycle={peekCycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
|
||||
<ActiveCycleDetails workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
</Tab.Panel>
|
||||
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
||||
<CyclesView
|
||||
filter="completed"
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
peekCycle={peekCycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
{cycleTab && cycleLayout && (
|
||||
<CyclesView
|
||||
filter="upcoming"
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
peekCycle={peekCycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
||||
<CyclesView
|
||||
filter="draft"
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
peekCycle={peekCycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
)}
|
||||
</div>
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
||||
<CyclesView
|
||||
filter="completed"
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
peekCycle={peekCycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||
{cycleTab && cycleLayout && workspaceSlug && projectId && (
|
||||
<CyclesView
|
||||
filter="draft"
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
peekCycle={peekCycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -5,31 +5,43 @@ import { X, PenSquare } from "lucide-react";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root";
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectDraftIssueHeader } from "components/headers";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
const ProjectDraftIssuesPage: NextPageWithLayout = () => {
|
||||
const ProjectDraftIssuesPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Draft Issues` : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||
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>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="ga-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||
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>
|
||||
</button>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<DraftIssueLayoutRoot />
|
||||
</div>
|
||||
<DraftIssueLayoutRoot />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ProjectDraftIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -7,9 +7,9 @@ import { useProject, useInboxIssues } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectInboxHeader } from "components/headers";
|
||||
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
|
||||
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
@ -22,7 +22,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
filters: { fetchInboxFilters },
|
||||
issues: { fetchInboxIssues },
|
||||
} = useInboxIssues();
|
||||
|
||||
// fetching the Inbox filters and issues
|
||||
useSWR(
|
||||
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
|
||||
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
|
||||
@ -34,26 +34,32 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
}
|
||||
}
|
||||
);
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full overflow-hidden">
|
||||
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
|
||||
<InboxSidebarRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
/>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="relative flex h-full overflow-hidden">
|
||||
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
|
||||
<InboxSidebarRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<InboxContentRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<InboxContentRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -5,14 +5,15 @@ import { observer } from "mobx-react-lite";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectIssueDetailsHeader } from "components/headers";
|
||||
import { IssueDetailRoot } from "components/issues";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// fetch-keys
|
||||
import { useApplication, useIssueDetail } from "hooks/store";
|
||||
// store hooks
|
||||
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
||||
|
||||
const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
@ -23,17 +24,20 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
fetchIssue,
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getProjectById } = useProject();
|
||||
const { theme: themeStore } = useApplication();
|
||||
|
||||
// fetching issue details
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
// derived values
|
||||
const issue = getIssueById(issueId?.toString() || "") || undefined;
|
||||
const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined;
|
||||
const issueLoader = !issue || isLoading ? true : false;
|
||||
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const handleToggleIssueDetailSidebar = () => {
|
||||
@ -52,6 +56,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{issueLoader ? (
|
||||
<Loader className="flex h-full gap-5 p-5">
|
||||
<div className="basis-2/3 space-y-2">
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { ReactElement } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { ProjectLayoutRoot } from "components/issues";
|
||||
import { ProjectIssuesHeader } from "components/headers";
|
||||
@ -6,12 +9,36 @@ import { ProjectIssuesHeader } from "components/headers";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const ProjectIssuesPage: NextPageWithLayout = () => (
|
||||
<div className="h-full w-full">
|
||||
<ProjectLayoutRoot />
|
||||
</div>
|
||||
);
|
||||
const ProjectIssuesPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { projectId } = router.query;
|
||||
// store
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
if (!projectId) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// derived values
|
||||
const project = getProjectById(projectId.toString());
|
||||
const pageTitle = project?.name ? `${project?.name} - Issues` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<Head>
|
||||
<title>{project?.name} - Issues</title>
|
||||
</Head>
|
||||
<div className="h-full w-full">
|
||||
<ProjectLayoutRoot />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useModule } from "hooks/store";
|
||||
import { useModule, useProject } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
@ -10,37 +11,44 @@ import { AppLayout } from "layouts/app-layout";
|
||||
import { ModuleDetailsSidebar } from "components/modules";
|
||||
import { ModuleLayoutRoot } from "components/issues";
|
||||
import { ModuleIssuesHeader } from "components/headers";
|
||||
// ui
|
||||
import { PageHead } from "components/core";
|
||||
import { EmptyState } from "components/common";
|
||||
// assets
|
||||
import emptyModule from "public/empty-state/module.svg";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const ModuleIssuesPage: NextPageWithLayout = () => {
|
||||
const ModuleIssuesPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
// store hooks
|
||||
const { fetchModuleDetails } = useModule();
|
||||
const { fetchModuleDetails, getModuleById } = useModule();
|
||||
const { getProjectById } = useProject();
|
||||
// local storage
|
||||
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
|
||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||
|
||||
// fetching module details
|
||||
const { error } = useSWR(
|
||||
workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : null,
|
||||
workspaceSlug && projectId && moduleId
|
||||
? () => fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
||||
: null
|
||||
);
|
||||
// derived values
|
||||
const projectModule = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name && projectModule?.name ? `${project?.name} - ${projectModule?.name}` : undefined;
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setValue(`${!isSidebarCollapsed}`);
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{error ? (
|
||||
<EmptyState
|
||||
image={emptyModule}
|
||||
@ -71,7 +79,7 @@ const ModuleIssuesPage: NextPageWithLayout = () => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,13 +1,33 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ModulesListView } from "components/modules";
|
||||
import { ModulesListHeader } from "components/headers";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
const ProjectModulesPage: NextPageWithLayout = () => <ModulesListView />;
|
||||
const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { projectId } = router.query;
|
||||
// store
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Modules` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<ModulesListView />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectModulesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -14,7 +14,7 @@ import { FileService } from "services/file.service";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { GptAssistantPopover } from "components/core";
|
||||
import { GptAssistantPopover, PageHead } from "components/core";
|
||||
import { PageDetailsHeader } from "components/headers/page-details";
|
||||
// ui
|
||||
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
|
||||
@ -256,113 +256,116 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return pageIdMobx ? (
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
{isPageReadOnly ? (
|
||||
<DocumentReadOnlyEditorWithRef
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
ref={editorRef}
|
||||
value={pageDescription}
|
||||
customClassName={"tracking-tight w-full px-0"}
|
||||
borderOnFocus={false}
|
||||
noBorder
|
||||
documentDetails={{
|
||||
title: pageTitle,
|
||||
created_by: created_by,
|
||||
created_on: created_at,
|
||||
last_updated_at: updated_at,
|
||||
last_updated_by: updated_by,
|
||||
}}
|
||||
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
|
||||
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
|
||||
pageArchiveConfig={
|
||||
userCanArchive
|
||||
? {
|
||||
action: archived_at ? unArchivePage : archivePage,
|
||||
is_archived: archived_at ? true : false,
|
||||
archived_at: archived_at ? new Date(archived_at) : undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<DocumentEditorWithRef
|
||||
isSubmitting={isSubmitting}
|
||||
documentDetails={{
|
||||
title: pageTitle,
|
||||
created_by: created_by,
|
||||
created_on: created_at,
|
||||
last_updated_at: updated_at,
|
||||
last_updated_by: updated_by,
|
||||
}}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
value={pageDescription}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
updatePageTitle={updatePageTitle}
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
customClassName="tracking-tight self-center h-full w-full right-[0.675rem]"
|
||||
onChange={(_description_json: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
onChange(description_html);
|
||||
handleSubmit(updatePage)();
|
||||
}}
|
||||
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
|
||||
pageArchiveConfig={
|
||||
userCanArchive
|
||||
? {
|
||||
is_archived: archived_at ? true : false,
|
||||
action: archived_at ? unArchivePage : archivePage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
{isPageReadOnly ? (
|
||||
<DocumentReadOnlyEditorWithRef
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
ref={editorRef}
|
||||
value={pageDescription}
|
||||
customClassName={"tracking-tight w-full px-0"}
|
||||
borderOnFocus={false}
|
||||
noBorder
|
||||
documentDetails={{
|
||||
title: pageTitle,
|
||||
created_by: created_by,
|
||||
created_on: created_at,
|
||||
last_updated_at: updated_at,
|
||||
last_updated_by: updated_by,
|
||||
}}
|
||||
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
|
||||
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
|
||||
pageArchiveConfig={
|
||||
userCanArchive
|
||||
? {
|
||||
action: archived_at ? unArchivePage : archivePage,
|
||||
is_archived: archived_at ? true : false,
|
||||
archived_at: archived_at ? new Date(archived_at) : undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{projectId && envConfig?.has_openai_configured && (
|
||||
<div className="absolute right-[68px] top-2.5">
|
||||
<GptAssistantPopover
|
||||
isOpen={gptModalOpen}
|
||||
projectId={projectId.toString()}
|
||||
handleClose={() => {
|
||||
setGptModal((prevData) => !prevData);
|
||||
// this is done so that the title do not reset after gpt popover closed
|
||||
reset(getValues());
|
||||
}}
|
||||
onResponse={(response) => {
|
||||
handleAiAssistance(response);
|
||||
}}
|
||||
placement="top-end"
|
||||
button={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||
onClick={() => setGptModal((prevData) => !prevData)}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" />
|
||||
AI
|
||||
</button>
|
||||
}
|
||||
className="!min-w-[38rem]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<IssuePeekOverview />
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<DocumentEditorWithRef
|
||||
isSubmitting={isSubmitting}
|
||||
documentDetails={{
|
||||
title: pageTitle,
|
||||
created_by: created_by,
|
||||
created_on: created_at,
|
||||
last_updated_at: updated_at,
|
||||
last_updated_by: updated_by,
|
||||
}}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
value={pageDescription}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
updatePageTitle={updatePageTitle}
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
customClassName="tracking-tight self-center h-full w-full right-[0.675rem]"
|
||||
onChange={(_description_json: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
onChange(description_html);
|
||||
handleSubmit(updatePage)();
|
||||
}}
|
||||
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
|
||||
pageArchiveConfig={
|
||||
userCanArchive
|
||||
? {
|
||||
is_archived: archived_at ? true : false,
|
||||
action: archived_at ? unArchivePage : archivePage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{projectId && envConfig?.has_openai_configured && (
|
||||
<div className="absolute right-[68px] top-2.5">
|
||||
<GptAssistantPopover
|
||||
isOpen={gptModalOpen}
|
||||
projectId={projectId.toString()}
|
||||
handleClose={() => {
|
||||
setGptModal((prevData) => !prevData);
|
||||
// this is done so that the title do not reset after gpt popover closed
|
||||
reset(getValues());
|
||||
}}
|
||||
onResponse={(response) => {
|
||||
handleAiAssistance(response);
|
||||
}}
|
||||
placement="top-end"
|
||||
button={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||
onClick={() => setGptModal((prevData) => !prevData)}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" />
|
||||
AI
|
||||
</button>
|
||||
}
|
||||
className="!min-w-[38rem]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<Spinner />
|
||||
|
@ -6,7 +6,7 @@ import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useUser } from "hooks/store";
|
||||
import { useApplication, useEventTracker, useUser, useProject } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useSize from "hooks/use-window-size";
|
||||
@ -24,6 +24,7 @@ import { PAGE_TABS_LIST } from "constants/page";
|
||||
import { useProjectPages } from "hooks/store/use-project-page";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
|
||||
ssr: false,
|
||||
@ -63,7 +64,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
commandPalette: { toggleCreatePageModal },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
||||
const { getProjectById } = useProject();
|
||||
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
|
||||
useProjectPages();
|
||||
// hooks
|
||||
@ -101,10 +102,12 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
// derived values
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
|
||||
|
||||
const MobileTabList = () => (
|
||||
<Tab.List as="div" className="flex items-center justify-between border-b border-custom-border-200 px-3 pt-3 mb-4">
|
||||
@ -129,6 +132,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
|
@ -10,6 +10,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
@ -41,16 +42,21 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
// derived values
|
||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
|
||||
|
||||
return (
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Automations</h3>
|
||||
</div>
|
||||
<AutoArchiveAutomation handleChange={handleChange} />
|
||||
<AutoCloseAutomation handleChange={handleChange} />
|
||||
</section>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Automations</h3>
|
||||
</div>
|
||||
<AutoArchiveAutomation handleChange={handleChange} />
|
||||
<AutoCloseAutomation handleChange={handleChange} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useUser, useProject } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
import { EstimatesList } from "components/estimates";
|
||||
// types
|
||||
@ -17,13 +18,18 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const { currentProjectDetails } = useProject();
|
||||
// derived values
|
||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined;
|
||||
|
||||
return (
|
||||
<div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
|
||||
<EstimatesList />
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
|
||||
<EstimatesList />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,41 +1,48 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useProject, useUser } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
import { ProjectFeaturesList } from "components/project";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const FeaturesSettingsPage: NextPageWithLayout = () => {
|
||||
const FeaturesSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store
|
||||
const {
|
||||
membership: { fetchUserProjectInfo },
|
||||
} = useUser();
|
||||
|
||||
const { currentProjectDetails } = useProject();
|
||||
// fetch the project details
|
||||
const { data: memberDetails } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
|
||||
// derived values
|
||||
const isAdmin = memberDetails?.role === 20;
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined;
|
||||
|
||||
return (
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Features</h3>
|
||||
</div>
|
||||
<ProjectFeaturesList />
|
||||
</section>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Features</h3>
|
||||
</div>
|
||||
<ProjectFeaturesList />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
FeaturesSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -8,6 +8,7 @@ import { useProject } from "hooks/store";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
import {
|
||||
DeleteProjectModal,
|
||||
@ -32,14 +33,15 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => {
|
||||
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
|
||||
// derived values
|
||||
const isAdmin = currentProjectDetails?.member_role === 20;
|
||||
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"));
|
||||
|
||||
const isAdmin = currentProjectDetails?.member_role === 20;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{currentProjectDetails && (
|
||||
<DeleteProjectModal
|
||||
project={currentProjectDetails}
|
||||
|
@ -2,6 +2,7 @@ import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { useTheme } from "next-themes";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
// layouts
|
||||
@ -11,6 +12,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
import { IntegrationService } from "services/integrations";
|
||||
import { ProjectService } from "services/project";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { IntegrationCard } from "components/project";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
@ -27,63 +29,66 @@ import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
const integrationService = new IntegrationService();
|
||||
const projectService = new ProjectService();
|
||||
|
||||
const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
||||
const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
|
||||
// fetch project details
|
||||
const { data: projectDetails } = useSWR<IProject>(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
|
||||
// fetch Integrations list
|
||||
const { data: workspaceIntegrations } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
|
||||
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
|
||||
);
|
||||
|
||||
// derived values
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode);
|
||||
|
||||
const isAdmin = projectDetails?.member_role === 20;
|
||||
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined;
|
||||
|
||||
return (
|
||||
<div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Integrations</h3>
|
||||
</div>
|
||||
{workspaceIntegrations ? (
|
||||
workspaceIntegrations.length > 0 ? (
|
||||
<div>
|
||||
{workspaceIntegrations.map((integration) => (
|
||||
<IntegrationCard key={integration.integration_detail.id} integration={integration} />
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Integrations</h3>
|
||||
</div>
|
||||
{workspaceIntegrations ? (
|
||||
workspaceIntegrations.length > 0 ? (
|
||||
<div>
|
||||
{workspaceIntegrations.map((integration) => (
|
||||
<IntegrationCard key={integration.integration_detail.id} integration={integration} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
primaryButton={{
|
||||
text: "Configure now",
|
||||
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
primaryButton={{
|
||||
text: "Configure now",
|
||||
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<IntegrationsSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
<IntegrationsSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,18 +1,30 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectSettingsLabelList } from "components/labels";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
|
||||
const LabelsSettingsPage: NextPageWithLayout = () => (
|
||||
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
|
||||
<ProjectSettingsLabelList />
|
||||
</div>
|
||||
);
|
||||
const LabelsSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const { currentProjectDetails } = useProject();
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
|
||||
<ProjectSettingsLabelList />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
LabelsSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,19 +1,33 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
|
||||
const MembersSettingsPage: NextPageWithLayout = () => (
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9`}>
|
||||
<ProjectSettingsMemberDefaults />
|
||||
<ProjectMemberList />
|
||||
</section>
|
||||
);
|
||||
const MembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9`}>
|
||||
<ProjectSettingsMemberDefaults />
|
||||
<ProjectMemberList />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
MembersSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useProjectView } from "hooks/store";
|
||||
import { useProject, useProjectView } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { ProjectViewLayoutRoot } from "components/issues";
|
||||
import { ProjectViewIssuesHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { EmptyState } from "components/common";
|
||||
// assets
|
||||
@ -15,12 +17,17 @@ import emptyView from "public/empty-state/view.svg";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const ProjectViewIssuesPage: NextPageWithLayout = () => {
|
||||
const ProjectViewIssuesPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
// store hooks
|
||||
const { fetchViewDetails } = useProjectView();
|
||||
const { fetchViewDetails, getViewById } = useProjectView();
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const projectView = viewId ? getViewById(viewId.toString()) : undefined;
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name && projectView?.name ? `${project?.name} - ${projectView?.name}` : undefined;
|
||||
|
||||
const { error } = useSWR(
|
||||
workspaceSlug && projectId && viewId ? `VIEW_DETAILS_${viewId.toString()}` : null,
|
||||
@ -42,11 +49,14 @@ const ProjectViewIssuesPage: NextPageWithLayout = () => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ProjectViewLayoutRoot />
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<ProjectViewLayoutRoot />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ProjectViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,13 +1,34 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { ProjectViewsHeader } from "components/headers";
|
||||
import { ProjectViewsList } from "components/views";
|
||||
import { PageHead } from "components/core";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const ProjectViewsPage: NextPageWithLayout = () => <ProjectViewsList />;
|
||||
const ProjectViewsPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { projectId } = router.query;
|
||||
// store
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Views` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<ProjectViewsList />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectViewsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,13 +1,28 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { ProjectCardList } from "components/project";
|
||||
import { ProjectsHeader } from "components/headers";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// type
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { useWorkspace } from "hooks/store";
|
||||
|
||||
const ProjectsPage: NextPageWithLayout = () => <ProjectCardList />;
|
||||
const ProjectsPage: NextPageWithLayout = observer(() => {
|
||||
// store
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<ProjectCardList />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<ProjectsHeader />}>{page}</AppLayout>;
|
||||
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import { useTheme } from "next-themes";
|
||||
// store hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useUser, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
@ -23,6 +23,7 @@ import { NextPageWithLayout } from "lib/types";
|
||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
@ -39,6 +40,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
|
||||
@ -49,12 +51,16 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!tokens) {
|
||||
@ -63,6 +69,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||
<section className="h-full w-full overflow-y-auto py-8 pr-9">
|
||||
{tokens.length > 0 ? (
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useUser, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// component
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
@ -18,33 +19,41 @@ const BillingSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||
<div>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-6">
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
||||
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
|
||||
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
|
||||
<Button variant="neutral-primary">View Plans</Button>
|
||||
</a>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="px-4 py-6">
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
||||
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
|
||||
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
|
||||
<Button variant="neutral-primary">View Plans</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useUser, useWorkspace } from "hooks/store";
|
||||
// layout
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import ExportGuide from "components/exporter/guide";
|
||||
import { PageHead } from "components/core";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
@ -17,24 +18,33 @@ const ExportsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
// derived values
|
||||
const hasPageAccess =
|
||||
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined;
|
||||
|
||||
if (!hasPageAccess)
|
||||
return (
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-y-auto py-8 pr-9">
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Exports</h3>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-full overflow-y-auto py-8 pr-9">
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Exports</h3>
|
||||
</div>
|
||||
<ExportGuide />
|
||||
</div>
|
||||
<ExportGuide />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useUser, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import IntegrationGuide from "components/integration/guide";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
@ -17,23 +18,32 @@ const ImportsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
// derived values
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Imports</h3>
|
||||
</div>
|
||||
<IntegrationGuide />
|
||||
</section>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Imports</h3>
|
||||
</div>
|
||||
<IntegrationGuide />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,14 +1,30 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// hooks
|
||||
import { useWorkspace } from "hooks/store";
|
||||
// components
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { WorkspaceDetails } from "components/workspace";
|
||||
import { PageHead } from "components/core";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const WorkspaceSettingsPage: NextPageWithLayout = () => <WorkspaceDetails />;
|
||||
const WorkspaceSettingsPage: NextPageWithLayout = observer(() => {
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - General Settings` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceDetails />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
WorkspaceSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useUser, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// layouts
|
||||
@ -12,6 +12,7 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { SingleIntegrationCard } from "components/integration";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui";
|
||||
// types
|
||||
@ -31,14 +32,20 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
// derived values
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
||||
@ -46,16 +53,21 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||
<div>
|
||||
{appIntegrations ? (
|
||||
appIntegrations.map((integration) => <SingleIntegrationCard key={integration.id} integration={integration} />)
|
||||
) : (
|
||||
<IntegrationsSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||
<div>
|
||||
{appIntegrations ? (
|
||||
appIntegrations.map((integration) => (
|
||||
<SingleIntegrationCard key={integration.id} integration={integration} />
|
||||
))
|
||||
) : (
|
||||
<IntegrationsSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Search } from "lucide-react";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useUser } from "hooks/store";
|
||||
import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
@ -11,6 +11,7 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
@ -37,6 +38,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
workspace: { inviteMembersToWorkspace },
|
||||
} = useMember();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -83,11 +85,14 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
// derived values
|
||||
const hasAddMemberPermission =
|
||||
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<SendWorkspaceInvitationModal
|
||||
isOpen={inviteModal}
|
||||
onClose={() => setInviteModal(false)}
|
||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { useUser, useWebhook } from "hooks/store";
|
||||
import { useUser, useWebhook, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
@ -12,6 +12,7 @@ import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
@ -29,6 +30,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -38,6 +40,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
||||
// }, [clearSecretKey, isCreated]);
|
||||
|
||||
const isAdmin = currentWorkspaceRole === 20;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined;
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && webhookId && isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null,
|
||||
@ -76,9 +79,12 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!currentWebhook)
|
||||
@ -90,6 +96,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
|
||||
<div className="w-full space-y-8 overflow-y-auto py-8 pr-9">
|
||||
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />
|
||||
|
@ -19,6 +19,7 @@ import { WebhookSettingsLoader } from "components/ui";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
// states
|
||||
@ -47,6 +48,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
|
||||
|
||||
// clear secret key when modal is closed.
|
||||
useEffect(() => {
|
||||
@ -55,53 +57,59 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!webhooks) return <WebhookSettingsLoader />;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden py-8 pr-9">
|
||||
<CreateWebhookModal
|
||||
createWebhook={createWebhook}
|
||||
clearSecretKey={clearSecretKey}
|
||||
currentWorkspace={currentWorkspace}
|
||||
isOpen={showCreateWebhookModal}
|
||||
onClose={() => {
|
||||
setShowCreateWebhookModal(false);
|
||||
}}
|
||||
/>
|
||||
{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>
|
||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||
Add webhook
|
||||
</Button>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="h-full w-full overflow-hidden py-8 pr-9">
|
||||
<CreateWebhookModal
|
||||
createWebhook={createWebhook}
|
||||
clearSecretKey={clearSecretKey}
|
||||
currentWorkspace={currentWorkspace}
|
||||
isOpen={showCreateWebhookModal}
|
||||
onClose={() => {
|
||||
setShowCreateWebhookModal(false);
|
||||
}}
|
||||
/>
|
||||
{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>
|
||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||
Add webhook
|
||||
</Button>
|
||||
</div>
|
||||
<WebhooksList />
|
||||
</div>
|
||||
<WebhooksList />
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||
Add webhook
|
||||
</Button>
|
||||
) : (
|
||||
<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>
|
||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||
Add webhook
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -7,15 +7,26 @@ import { AllIssueLayoutRoot } from "components/issues";
|
||||
import { GlobalIssuesHeader } from "components/headers";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { observer } from "mobx-react";
|
||||
import { useWorkspace } from "hooks/store";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const GlobalViewIssuesPage: NextPageWithLayout = () => (
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewsHeader />
|
||||
<AllIssueLayoutRoot />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const GlobalViewIssuesPage: NextPageWithLayout = observer(() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Views` : undefined;
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewsHeader />
|
||||
<AllIssueLayoutRoot />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { useState, ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace";
|
||||
import { GlobalIssuesHeader } from "components/headers";
|
||||
// ui
|
||||
@ -12,31 +14,40 @@ import { Search } from "lucide-react";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
|
||||
// hooks
|
||||
import { useWorkspace } from "hooks/store";
|
||||
|
||||
const WorkspaceViewsPage: NextPageWithLayout = () => {
|
||||
const WorkspaceViewsPage: NextPageWithLayout = observer(() => {
|
||||
const [query, setQuery] = useState("");
|
||||
// store
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="flex w-full items-center gap-2.5 border-b border-custom-border-200 px-5 py-3">
|
||||
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||
<Input
|
||||
className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
mode="true-transparent"
|
||||
/>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="flex w-full items-center gap-2.5 border-b border-custom-border-200 px-5 py-3">
|
||||
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||
<Input
|
||||
className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
mode="true-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
|
||||
<GlobalDefaultViewListItem key={option.key} view={option} />
|
||||
))}
|
||||
<GlobalViewsList searchQuery={query} />
|
||||
</div>
|
||||
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
|
||||
<GlobalDefaultViewListItem key={option.key} view={option} />
|
||||
))}
|
||||
<GlobalViewsList searchQuery={query} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
WorkspaceViewsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<GlobalIssuesHeader activeLayout="list" />}>{page}</AppLayout>;
|
||||
|
@ -12,6 +12,7 @@ import { useEventTracker } from "hooks/store";
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
// components
|
||||
import { LatestFeatureBlock } from "components/common";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// images
|
||||
@ -85,59 +86,62 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 ">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 ">
|
||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||
<div className="mx-auto flex flex-col divide-y divide-custom-border-200 sm:w-96">
|
||||
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
|
||||
Get on your flight deck
|
||||
</h1>
|
||||
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">Get a link to reset your password</p>
|
||||
<form onSubmit={handleSubmit(handleForgotPassword)} className="mx-auto mt-5 space-y-4 sm:w-96">
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required",
|
||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting || resendTimerCode > 0}
|
||||
>
|
||||
{resendTimerCode > 0 ? `Request new link in ${resendTimerCode}s` : "Get link"}
|
||||
</Button>
|
||||
</form>
|
||||
<>
|
||||
<PageHead title="Forgot Password" />
|
||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 ">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 ">
|
||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||
<div className="mx-auto flex flex-col divide-y divide-custom-border-200 sm:w-96">
|
||||
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
|
||||
Get on your flight deck
|
||||
</h1>
|
||||
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">Get a link to reset your password</p>
|
||||
<form onSubmit={handleSubmit(handleForgotPassword)} className="mx-auto mt-5 space-y-4 sm:w-96">
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required",
|
||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting || resendTimerCode > 0}
|
||||
>
|
||||
{resendTimerCode > 0 ? `Request new link in ${resendTimerCode}s` : "Get link"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<LatestFeatureBlock />
|
||||
</div>
|
||||
<LatestFeatureBlock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -12,6 +12,7 @@ import { useEventTracker } from "hooks/store";
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
// components
|
||||
import { LatestFeatureBlock } from "components/common";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// images
|
||||
@ -90,90 +91,93 @@ const ResetPasswordPage: NextPageWithLayout = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 ">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
<>
|
||||
<PageHead title="Reset Password" />
|
||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28 ">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 ">
|
||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||
<div className="mx-auto flex flex-col divide-y divide-custom-border-200 sm:w-96">
|
||||
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
|
||||
Let{"'"}s get a new password
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit(handleResetPassword)} className="mx-auto mt-11 space-y-4 sm:w-96">
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required",
|
||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
rules={{
|
||||
required: "Password is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 ">
|
||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||
<div className="mx-auto flex flex-col divide-y divide-custom-border-200 sm:w-96">
|
||||
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
|
||||
Let{"'"}s get a new password
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit(handleResetPassword)} className="mx-auto mt-11 space-y-4 sm:w-96">
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required",
|
||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
minLength={8}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400"
|
||||
disabled
|
||||
/>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
rules={{
|
||||
required: "Password is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
minLength={8}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Set password
|
||||
</Button>
|
||||
</form>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Set password
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<LatestFeatureBlock />
|
||||
</div>
|
||||
<LatestFeatureBlock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { useApplication, useUser } from "hooks/store";
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
// components
|
||||
import { SignUpRoot } from "components/account";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// assets
|
||||
@ -29,20 +30,23 @@ const SignUpPage: NextPageWithLayout = observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
<>
|
||||
<PageHead title="Sign Up" />
|
||||
<div className="h-full w-full bg-onboarding-gradient-100">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
|
||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||
<SignUpRoot />
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
|
||||
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
|
||||
<SignUpRoot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -11,6 +11,7 @@ import DefaultLayout from "layouts/default-layout";
|
||||
import { UserAuthWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
import { CreateWorkspaceForm } from "components/workspace";
|
||||
import { PageHead } from "components/core";
|
||||
// images
|
||||
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
|
||||
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
|
||||
@ -37,38 +38,41 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
|
||||
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
|
||||
<div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" />
|
||||
<Link
|
||||
className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3"
|
||||
href="/"
|
||||
>
|
||||
<div className="h-[30px] w-[133px]">
|
||||
{theme === "light" ? (
|
||||
<Image src={BlackHorizontalLogo} alt="Plane black logo" />
|
||||
) : (
|
||||
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
|
||||
)}
|
||||
<>
|
||||
<PageHead title="Create Workspace" />
|
||||
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
|
||||
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
|
||||
<div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" />
|
||||
<Link
|
||||
className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3"
|
||||
href="/"
|
||||
>
|
||||
<div className="h-[30px] w-[133px]">
|
||||
{theme === "light" ? (
|
||||
<Image src={BlackHorizontalLogo} alt="Plane black logo" />
|
||||
) : (
|
||||
<Image src={WhiteHorizontalLogo} alt="Plane white 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">
|
||||
{currentUser?.email}
|
||||
</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">
|
||||
{currentUser?.email}
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<div className="w-full space-y-7 sm:space-y-10">
|
||||
<h4 className="text-2xl font-semibold">Create your workspace</h4>
|
||||
<div className="sm:w-3/4 md:w-2/5">
|
||||
<CreateWorkspaceForm
|
||||
onSubmit={onSubmit}
|
||||
defaultValues={defaultValues}
|
||||
setDefaultValues={setDefaultValues}
|
||||
/>
|
||||
<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">
|
||||
<div className="w-full space-y-7 sm:space-y-10">
|
||||
<h4 className="text-2xl font-semibold">Create your workspace</h4>
|
||||
<div className="sm:w-3/4 md:w-2/5">
|
||||
<CreateWorkspaceForm
|
||||
onSubmit={onSubmit}
|
||||
defaultValues={defaultValues}
|
||||
setDefaultValues={setDefaultValues}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -13,6 +13,7 @@ import { Loader } from "@plane/ui";
|
||||
import { Lightbulb } from "lucide-react";
|
||||
// components
|
||||
import { InstanceAIForm } from "components/instance";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const InstanceAdminAIPage: NextPageWithLayout = observer(() => {
|
||||
// store
|
||||
@ -23,37 +24,40 @@ const InstanceAdminAIPage: NextPageWithLayout = observer(() => {
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
|
||||
<>
|
||||
<PageHead title="God Mode - AI" />
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{formattedConfig ? (
|
||||
<>
|
||||
<div>
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div>
|
||||
</div>
|
||||
<InstanceAIForm config={formattedConfig} />
|
||||
<div className="my-2 flex">
|
||||
<div className="flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
|
||||
<Lightbulb height="14" width="14" />
|
||||
<div>If you have a preferred AI models vendor, please get in touch with us.</div>
|
||||
{formattedConfig ? (
|
||||
<>
|
||||
<div>
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div>
|
||||
</div>
|
||||
<InstanceAIForm config={formattedConfig} />
|
||||
<div className="my-2 flex">
|
||||
<div className="flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
|
||||
<Lightbulb height="14" width="14" />
|
||||
<div>If you have a preferred AI models vendor, please get in touch with us.</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -14,6 +14,7 @@ import useToast from "hooks/use-toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
||||
// store
|
||||
@ -64,69 +65,71 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">Single sign-on and OAuth</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the
|
||||
settings.
|
||||
<>
|
||||
<PageHead title="God Mode - SSO and OAuth" />
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">Single sign-on and OAuth</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the
|
||||
settings.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{formattedConfig ? (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-12 border-b border-custom-border-100 pb-8 lg:w-2/5">
|
||||
<div className="pointer-events-none mr-4 flex items-center gap-14 opacity-50">
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100">
|
||||
Turn Magic Links {Boolean(parseInt(enableMagicLogin)) ? "off" : "on"}
|
||||
{formattedConfig ? (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-12 border-b border-custom-border-100 pb-8 lg:w-2/5">
|
||||
<div className="pointer-events-none mr-4 flex items-center gap-14 opacity-50">
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100">
|
||||
Turn Magic Links {Boolean(parseInt(enableMagicLogin)) ? "off" : "on"}
|
||||
</div>
|
||||
<div className="text-xs font-normal text-custom-text-300">
|
||||
<p>Slack-like emails for authentication.</p>
|
||||
You need to have set up email{" "}
|
||||
<Link href="email">
|
||||
<span className="text-custom-primary-100 hover:underline">here</span>
|
||||
</Link>{" "}
|
||||
to enable this.
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-normal text-custom-text-300">
|
||||
<p>Slack-like emails for authentication.</p>
|
||||
You need to have set up email{" "}
|
||||
<Link href="email">
|
||||
<span className="text-custom-primary-100 hover:underline">here</span>
|
||||
</Link>{" "}
|
||||
to enable this.
|
||||
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
// onChange={() => {
|
||||
// Boolean(parseInt(enableMagicLogin)) === true
|
||||
// ? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
|
||||
// : updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
|
||||
// }}
|
||||
onChange={() => {}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
// onChange={() => {
|
||||
// Boolean(parseInt(enableMagicLogin)) === true
|
||||
// ? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
|
||||
// : updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
|
||||
// }}
|
||||
onChange={() => {}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-4 flex items-center gap-14">
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100">
|
||||
Let your users log in via the methods below
|
||||
<div className="mr-4 flex items-center gap-14">
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100">
|
||||
Let your users log in via the methods below
|
||||
</div>
|
||||
<div className="text-xs font-normal text-custom-text-300">
|
||||
Toggling this off will disable all previous configs. Users will only be able to login with an e-mail
|
||||
and password combo.
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-normal text-custom-text-300">
|
||||
Toggling this off will disable all previous configs. Users will only be able to login with an e-mail
|
||||
and password combo.
|
||||
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableSignup))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(enableSignup)) === true
|
||||
? updateConfig("ENABLE_SIGNUP", "0")
|
||||
: updateConfig("ENABLE_SIGNUP", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shrink-0 ${isSubmitting && "opacity-70"}`}>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableSignup))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(enableSignup)) === true
|
||||
? updateConfig("ENABLE_SIGNUP", "0")
|
||||
: updateConfig("ENABLE_SIGNUP", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-14 mr-4">
|
||||
{/* <div className="flex items-center gap-14 mr-4">
|
||||
<div className="grow">
|
||||
<div className="text-custom-text-100 font-medium text-sm">
|
||||
Turn Email Password {Boolean(parseInt(enableEmailPassword)) ? "off" : "on"}
|
||||
@ -146,36 +149,37 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-6 py-2">
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between border-b border-custom-border-100 py-2">
|
||||
<span className="text-lg font-medium tracking-tight">Google</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-6 py-2">
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between border-b border-custom-border-100 py-2">
|
||||
<span className="text-lg font-medium tracking-tight">Google</span>
|
||||
</div>
|
||||
<div className="px-2 py-6">
|
||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 py-6">
|
||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between border-b border-custom-border-100 py-2">
|
||||
<span className="text-lg font-medium tracking-tight">Github</span>
|
||||
</div>
|
||||
<div className="px-2 py-6">
|
||||
<InstanceGithubConfigForm config={formattedConfig} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between border-b border-custom-border-100 py-2">
|
||||
<span className="text-lg font-medium tracking-tight">Github</span>
|
||||
</div>
|
||||
<div className="px-2 py-6">
|
||||
<InstanceGithubConfigForm config={formattedConfig} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { useApplication } from "hooks/store";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { InstanceEmailForm } from "components/instance";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
|
||||
// store
|
||||
@ -21,29 +22,32 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => {
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
||||
</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Set it up below and please test your settings before you save them.{" "}
|
||||
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
|
||||
</div>
|
||||
</div>
|
||||
{formattedConfig ? (
|
||||
<InstanceEmailForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<>
|
||||
<PageHead title="God Mode - Email" />
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
||||
</div>
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Set it up below and please test your settings before you save them.{" "}
|
||||
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
|
||||
</div>
|
||||
</div>
|
||||
{formattedConfig ? (
|
||||
<InstanceEmailForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { useApplication } from "hooks/store";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { InstanceImageConfigForm } from "components/instance";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const InstanceAdminImagePage: NextPageWithLayout = observer(() => {
|
||||
// store
|
||||
@ -21,25 +22,28 @@ const InstanceAdminImagePage: NextPageWithLayout = observer(() => {
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">Third-party image libraries</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Let your users search and choose images from third-party libraries
|
||||
</div>
|
||||
</div>
|
||||
{formattedConfig ? (
|
||||
<InstanceImageConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<>
|
||||
<PageHead title="God Mode - Images" />
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">Third-party image libraries</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Let your users search and choose images from third-party libraries
|
||||
</div>
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{formattedConfig ? (
|
||||
<InstanceImageConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { useApplication } from "hooks/store";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { InstanceGeneralForm } from "components/instance";
|
||||
import { PageHead } from "components/core";
|
||||
|
||||
const InstanceAdminPage: NextPageWithLayout = observer(() => {
|
||||
// store hooks
|
||||
@ -22,26 +23,29 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => {
|
||||
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins());
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">ID your instance easily</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Change the name of your instance and instance admin e-mail addresses. If you have a paid subscription, you
|
||||
will find your license key here.
|
||||
</div>
|
||||
</div>
|
||||
{instance && instanceAdmins ? (
|
||||
<InstanceGeneralForm instance={instance} instanceAdmins={instanceAdmins} />
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<>
|
||||
<PageHead title="God Mode - General Settings" />
|
||||
<div className="flex h-full w-full flex-col gap-8">
|
||||
<div className="mb-2 border-b border-custom-border-100 pb-3">
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">ID your instance easily</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Change the name of your instance and instance admin e-mail addresses. If you have a paid subscription, you
|
||||
will find your license key here.
|
||||
</div>
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{instance && instanceAdmins ? (
|
||||
<InstanceGeneralForm instance={instance} instanceAdmins={instanceAdmins} />
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -32,7 +32,7 @@ import { ROLE } from "constants/workspace";
|
||||
import { MEMBER_ACCEPTED } from "constants/event-tracker";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
|
||||
import { PageHead } from "components/core";
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
const userService = new UserService();
|
||||
@ -126,108 +126,111 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
|
||||
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
|
||||
<div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" />
|
||||
<div className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3">
|
||||
<div className="h-[30px] w-[133px]">
|
||||
{theme === "light" ? (
|
||||
<Image src={BlackHorizontalLogo} alt="Plane black logo" />
|
||||
) : (
|
||||
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
{currentUser?.email}
|
||||
</div>
|
||||
</div>
|
||||
{invitations ? (
|
||||
invitations.length > 0 ? (
|
||||
<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">
|
||||
<div className="w-full space-y-10">
|
||||
<h5 className="text-lg">We see that someone has invited you to</h5>
|
||||
<h4 className="text-2xl font-semibold">Join a workspace</h4>
|
||||
<div className="max-h-[37vh] space-y-4 overflow-y-auto md:w-3/5">
|
||||
{invitations.map((invitation) => {
|
||||
const isSelected = invitationsRespond.includes(invitation.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={invitation.id}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded border px-3.5 py-5 ${
|
||||
isSelected
|
||||
? "border-custom-primary-100"
|
||||
: "border-custom-border-200 hover:bg-custom-background-80"
|
||||
}`}
|
||||
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="grid h-9 w-9 place-items-center rounded">
|
||||
{invitation.workspace.logo && invitation.workspace.logo.trim() !== "" ? (
|
||||
<img
|
||||
src={invitation.workspace.logo}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded"
|
||||
alt={invitation.workspace.name}
|
||||
/>
|
||||
) : (
|
||||
<span className="grid h-9 w-9 place-items-center rounded bg-gray-700 px-3 py-1.5 uppercase text-white">
|
||||
{invitation.workspace.name[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
|
||||
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`flex-shrink-0 ${isSelected ? "text-custom-primary-100" : "text-custom-text-200"}`}
|
||||
>
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="md"
|
||||
onClick={submitInvitations}
|
||||
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
|
||||
loading={isJoiningWorkspaces}
|
||||
>
|
||||
Accept & Join
|
||||
</Button>
|
||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||
<span>
|
||||
<Button variant="neutral-primary" size="md">
|
||||
Go Home
|
||||
</Button>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<>
|
||||
<PageHead title="Invitations" />
|
||||
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
|
||||
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
|
||||
<div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" />
|
||||
<div className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3">
|
||||
<div className="h-[30px] w-[133px]">
|
||||
{theme === "light" ? (
|
||||
<Image src={BlackHorizontalLogo} alt="Plane black logo" />
|
||||
) : (
|
||||
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="fixed left-0 top-0 grid h-full w-full place-items-center">
|
||||
<EmptyState
|
||||
title="No pending invites"
|
||||
description="You can see here if someone invites you to a workspace."
|
||||
image={emptyInvitation}
|
||||
primaryButton={{
|
||||
text: "Back to dashboard",
|
||||
onClick: () => router.push("/"),
|
||||
}}
|
||||
/>
|
||||
<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">
|
||||
{currentUser?.email}
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{invitations ? (
|
||||
invitations.length > 0 ? (
|
||||
<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">
|
||||
<div className="w-full space-y-10">
|
||||
<h5 className="text-lg">We see that someone has invited you to</h5>
|
||||
<h4 className="text-2xl font-semibold">Join a workspace</h4>
|
||||
<div className="max-h-[37vh] space-y-4 overflow-y-auto md:w-3/5">
|
||||
{invitations.map((invitation) => {
|
||||
const isSelected = invitationsRespond.includes(invitation.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={invitation.id}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded border px-3.5 py-5 ${
|
||||
isSelected
|
||||
? "border-custom-primary-100"
|
||||
: "border-custom-border-200 hover:bg-custom-background-80"
|
||||
}`}
|
||||
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="grid h-9 w-9 place-items-center rounded">
|
||||
{invitation.workspace.logo && invitation.workspace.logo.trim() !== "" ? (
|
||||
<img
|
||||
src={invitation.workspace.logo}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded"
|
||||
alt={invitation.workspace.name}
|
||||
/>
|
||||
) : (
|
||||
<span className="grid h-9 w-9 place-items-center rounded bg-gray-700 px-3 py-1.5 uppercase text-white">
|
||||
{invitation.workspace.name[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
|
||||
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`flex-shrink-0 ${isSelected ? "text-custom-primary-100" : "text-custom-text-200"}`}
|
||||
>
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="md"
|
||||
onClick={submitInvitations}
|
||||
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
|
||||
loading={isJoiningWorkspaces}
|
||||
>
|
||||
Accept & Join
|
||||
</Button>
|
||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||
<span>
|
||||
<Button variant="neutral-primary" size="md">
|
||||
Go Home
|
||||
</Button>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="fixed left-0 top-0 grid h-full w-full place-items-center">
|
||||
<EmptyState
|
||||
title="No pending invites"
|
||||
description="You can see here if someone invites you to a workspace."
|
||||
image={emptyInvitation}
|
||||
primaryButton={{
|
||||
text: "Back to dashboard",
|
||||
onClick: () => router.push("/"),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -17,6 +17,7 @@ import DefaultLayout from "layouts/default-layout";
|
||||
import { UserAuthWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Avatar, Spinner } from "@plane/ui";
|
||||
// images
|
||||
@ -142,6 +143,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title="Onboarding" />
|
||||
<SwitchOrDeleteAccountModal isOpen={showDeleteAccountModal} onClose={() => setShowDeleteAccountModal(false)} />
|
||||
{user && step !== null ? (
|
||||
<div className={`fixed flex h-full w-full flex-col bg-onboarding-gradient-100`}>
|
||||
|
@ -9,7 +9,7 @@ import { UserService } from "services/user.service";
|
||||
// layouts
|
||||
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { ActivityIcon, ActivityMessage, IssueLink } from "components/core";
|
||||
import { ActivityIcon, ActivityMessage, IssueLink, PageHead } from "components/core";
|
||||
import { RichReadOnlyEditor } from "@plane/rich-text-editor";
|
||||
// icons
|
||||
import { History, MessageSquare } from "lucide-react";
|
||||
@ -32,159 +32,162 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
||||
const { theme: themeStore } = useApplication();
|
||||
|
||||
return (
|
||||
<section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
|
||||
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
|
||||
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
||||
<h3 className="text-xl font-medium">Activity</h3>
|
||||
</div>
|
||||
{userActivity ? (
|
||||
<div className="flex h-full w-full flex-col gap-2 overflow-y-auto">
|
||||
<ul role="list" className="-mb-4">
|
||||
{userActivity.results.map((activityItem: any) => {
|
||||
if (activityItem.field === "comment") {
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" && (
|
||||
<History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
<PageHead title="Profile - Activity" />
|
||||
<section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
|
||||
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
|
||||
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
||||
<h3 className="text-xl font-medium">Activity</h3>
|
||||
</div>
|
||||
{userActivity ? (
|
||||
<div className="flex h-full w-full flex-col gap-2 overflow-y-auto">
|
||||
<ul role="list" className="-mb-4">
|
||||
{userActivity.results.map((activityItem: any) => {
|
||||
if (activityItem.field === "comment") {
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" && (
|
||||
<History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
<MessageSquare className="h-6 w-6 !text-2xl text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name + " Bot"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {calculateTimeAgo(activityItem.created_at)}
|
||||
</p>
|
||||
<span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
<MessageSquare className="h-6 w-6 !text-2xl text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RichReadOnlyEditor
|
||||
value={activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name + " Bot"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {calculateTimeAgo(activityItem.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RichReadOnlyEditor
|
||||
value={activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const message =
|
||||
activityItem.verb === "created" &&
|
||||
const message =
|
||||
activityItem.verb === "created" &&
|
||||
activityItem.field !== "cycles" &&
|
||||
activityItem.field !== "modules" &&
|
||||
activityItem.field !== "attachment" &&
|
||||
activityItem.field !== "link" &&
|
||||
activityItem.field !== "estimate" &&
|
||||
!activityItem.field ? (
|
||||
<span>
|
||||
created <IssueLink activity={activityItem} />
|
||||
</span>
|
||||
) : (
|
||||
<ActivityMessage activity={activityItem} showIssue />
|
||||
);
|
||||
<span>
|
||||
created <IssueLink activity={activityItem} />
|
||||
</span>
|
||||
) : (
|
||||
<ActivityMessage activity={activityItem} showIssue />
|
||||
);
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
<div className="relative flex items-center space-x-2">
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="flex h-6 w-6 items-center justify-center">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<History className="h-5 w-5 text-custom-text-200" />
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
<div className="relative flex items-center space-x-2">
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="flex h-6 w-6 items-center justify-center">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<History className="h-5 w-5 text-custom-text-200" />
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-6 w-6 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`grid h-6 w-6 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 border-b border-custom-border-100 py-4">
|
||||
<div className="flex gap-1 break-words text-sm text-custom-text-200">
|
||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.first_name} Bot
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
>
|
||||
<div className="min-w-0 flex-1 border-b border-custom-border-100 py-4">
|
||||
<div className="flex gap-1 break-words text-sm text-custom-text-200">
|
||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">
|
||||
{currentUser?.id === activityItem.actor_detail.id
|
||||
? "You"
|
||||
: activityItem.actor_detail.display_name}
|
||||
{activityItem.actor_detail.first_name} Bot
|
||||
</span>
|
||||
</Link>
|
||||
)}{" "}
|
||||
<div className="flex gap-1 truncate">
|
||||
{message}{" "}
|
||||
<span className="flex-shrink-0 whitespace-nowrap">
|
||||
{calculateTimeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
>
|
||||
<span className="text-gray font-medium">
|
||||
{currentUser?.id === activityItem.actor_detail.id
|
||||
? "You"
|
||||
: activityItem.actor_detail.display_name}
|
||||
</span>
|
||||
</Link>
|
||||
)}{" "}
|
||||
<div className="flex gap-1 truncate">
|
||||
{message}{" "}
|
||||
<span className="flex-shrink-0 whitespace-nowrap">
|
||||
{calculateTimeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<ActivitySettingsLoader />
|
||||
)}
|
||||
</section>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<ActivitySettingsLoader />
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -6,6 +6,8 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
// services
|
||||
import { UserService } from "services/user.service";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// layout
|
||||
@ -88,93 +90,98 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
|
||||
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
||||
<>
|
||||
<PageHead title="Profile - Change Password" />
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
|
||||
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit(handleChangePassword)}
|
||||
className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5"
|
||||
>
|
||||
<h3 className="text-xl font-medium">Change password</h3>
|
||||
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-10 xl:grid-cols-2 2xl:grid-cols-3">
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Current password</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="old_password"
|
||||
rules={{
|
||||
required: "This field is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="old_password"
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Old password"
|
||||
className="w-full rounded-md font-medium"
|
||||
hasError={Boolean(errors.old_password)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.old_password && <span className="text-xs text-red-500">{errors.old_password.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">New password</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="new_password"
|
||||
rules={{
|
||||
required: "This field is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="new_password"
|
||||
type="password"
|
||||
value={value}
|
||||
placeholder="New password"
|
||||
onChange={onChange}
|
||||
className="w-full"
|
||||
hasError={Boolean(errors.new_password)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.new_password && <span className="text-xs text-red-500">{errors.new_password.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Confirm password</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirm_password"
|
||||
rules={{
|
||||
required: "This field is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="w-full"
|
||||
hasError={Boolean(errors.confirm_password)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.confirm_password && (
|
||||
<span className="text-xs text-red-500">{errors.confirm_password.message}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Button variant="primary" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Changing password..." : "Change password"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit(handleChangePassword)}
|
||||
className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5"
|
||||
>
|
||||
<h3 className="text-xl font-medium">Change password</h3>
|
||||
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-10 xl:grid-cols-2 2xl:grid-cols-3">
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Current password</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="old_password"
|
||||
rules={{
|
||||
required: "This field is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="old_password"
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Old password"
|
||||
className="w-full rounded-md font-medium"
|
||||
hasError={Boolean(errors.old_password)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.old_password && <span className="text-xs text-red-500">{errors.old_password.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">New password</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="new_password"
|
||||
rules={{
|
||||
required: "This field is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="new_password"
|
||||
type="password"
|
||||
value={value}
|
||||
placeholder="New password"
|
||||
onChange={onChange}
|
||||
className="w-full"
|
||||
hasError={Boolean(errors.new_password)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.new_password && <span className="text-xs text-red-500">{errors.new_password.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Confirm password</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirm_password"
|
||||
rules={{
|
||||
required: "This field is required",
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="w-full"
|
||||
hasError={Boolean(errors.confirm_password)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.confirm_password && <span className="text-xs text-red-500">{errors.confirm_password.message}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Button variant="primary" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Changing password..." : "Change password"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -11,7 +11,7 @@ import useToast from "hooks/use-toast";
|
||||
// layouts
|
||||
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { ImagePickerPopover, UserImageUploadModal } from "components/core";
|
||||
import { ImagePickerPopover, UserImageUploadModal, PageHead } from "components/core";
|
||||
import { DeactivateAccountModal } from "components/account";
|
||||
// ui
|
||||
import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane/ui";
|
||||
@ -57,7 +57,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
// store hooks
|
||||
const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser();
|
||||
// custom hooks
|
||||
const { } = useUserAuth({ user: myProfile, isLoading: currentUserLoader });
|
||||
const {} = useUserAuth({ user: myProfile, isLoading: currentUserLoader });
|
||||
const { theme: themeStore } = useApplication();
|
||||
|
||||
useEffect(() => {
|
||||
@ -138,6 +138,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title="Profile - General Settings" />
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
|
||||
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
|
||||
@ -296,8 +297,9 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="Enter your email"
|
||||
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${errors.email ? "border-red-500" : ""
|
||||
}`}
|
||||
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${
|
||||
errors.email ? "border-red-500" : ""
|
||||
}`}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
@ -385,7 +387,9 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSearchSelect
|
||||
value={value}
|
||||
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"}
|
||||
label={
|
||||
value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"
|
||||
}
|
||||
options={timeZoneOptions}
|
||||
onChange={onChange}
|
||||
optionsClassName="w-full"
|
||||
@ -409,7 +413,11 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
<Disclosure as="div" className="border-t border-custom-border-100 px-8">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between py-4"
|
||||
>
|
||||
<span className="text-lg tracking-tight">Deactivate account</span>
|
||||
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
|
||||
</Disclosure.Button>
|
||||
@ -426,8 +434,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the profile page is a critical area that requires careful consideration and
|
||||
attention. When deactivating an account, all of the data and resources within that account will be
|
||||
permanently removed and cannot be recovered.
|
||||
attention. When deactivating an account, all of the data and resources within that account
|
||||
will be permanently removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>
|
||||
|
@ -6,6 +6,7 @@ import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile
|
||||
import { EmailSettingsLoader } from "components/ui";
|
||||
// components
|
||||
import { EmailNotificationForm } from "components/profile/preferences";
|
||||
import { PageHead } from "components/core";
|
||||
// services
|
||||
import { UserService } from "services/user.service";
|
||||
// type
|
||||
@ -25,9 +26,12 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-8 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
|
||||
<EmailNotificationForm data={data} />
|
||||
</div>
|
||||
<>
|
||||
<PageHead title="Profile - Email Preference" />
|
||||
<div className="mx-auto mt-8 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
|
||||
<EmailNotificationForm data={data} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,7 @@ import useToast from "hooks/use-toast";
|
||||
// layouts
|
||||
import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences";
|
||||
// components
|
||||
import { CustomThemeSelector, ThemeSwitch } from "components/core";
|
||||
import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
@ -47,6 +47,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title="Profile - Theme Prefrence" />
|
||||
{currentUser ? (
|
||||
<div className="mx-auto mt-10 md:mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
|
Loading…
Reference in New Issue
Block a user