diff --git a/web/components/project/delete-project-modal.tsx b/web/components/project/delete-project-modal.tsx index 010341688..a40e37403 100644 --- a/web/components/project/delete-project-modal.tsx +++ b/web/components/project/delete-project-modal.tsx @@ -30,7 +30,7 @@ export const DeleteProjectModal: React.FC = (props) => { const { project: projectStore } = useMobxStore(); // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; // toast const { setToastAlert } = useToast(); // form info @@ -59,6 +59,8 @@ export const DeleteProjectModal: React.FC = (props) => { await projectStore .deleteProject(workspaceSlug.toString(), project.id) .then(() => { + if (projectId && projectId.toString() === project.id) router.push(`/${workspaceSlug}/projects`); + handleClose(); }) .catch(() => { diff --git a/web/components/project/index.ts b/web/components/project/index.ts index ff0213d52..040a0f3df 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -1,18 +1,18 @@ +export * from "./publish-project"; +export * from "./settings"; +export * from "./card-list"; +export * from "./card"; export * from "./create-project-modal"; export * from "./delete-project-modal"; -export * from "./sidebar-list"; -export * from "./settings-sidebar"; -export * from "./single-integration-card"; -export * from "./sidebar-list-item"; +export * from "./delete-project-section"; +export * from "./form-loader"; +export * from "./form"; +export * from "./join-project-modal"; +export * from "./label-select"; export * from "./leave-project-modal"; export * from "./member-select"; export * from "./members-select"; -export * from "./label-select"; export * from "./priority-select"; -export * from "./card-list"; -export * from "./card"; -export * from "./join-project-modal"; -export * from "./form"; -export * from "./form-loader"; -export * from "./delete-project-section"; -export * from "./publish-project"; +export * from "./sidebar-list-item"; +export * from "./sidebar-list"; +export * from "./single-integration-card"; diff --git a/web/components/project/settings-sidebar.tsx b/web/components/project/settings-sidebar.tsx deleted file mode 100644 index 123dc6282..000000000 --- a/web/components/project/settings-sidebar.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; - -export const SettingsSidebar = () => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const projectLinks: Array<{ - label: string; - href: string; - }> = [ - { - label: "General", - href: `/${workspaceSlug}/projects/${projectId}/settings`, - }, - { - label: "Members", - href: `/${workspaceSlug}/projects/${projectId}/settings/members`, - }, - { - label: "Features", - href: `/${workspaceSlug}/projects/${projectId}/settings/features`, - }, - { - label: "States", - href: `/${workspaceSlug}/projects/${projectId}/settings/states`, - }, - { - label: "Labels", - href: `/${workspaceSlug}/projects/${projectId}/settings/labels`, - }, - { - label: "Integrations", - href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`, - }, - { - label: "Estimates", - href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`, - }, - { - label: "Automations", - href: `/${workspaceSlug}/projects/${projectId}/settings/automations`, - }, - ]; - - const workspaceLinks: Array<{ - label: string; - href: string; - }> = [ - { - label: "General", - href: `/${workspaceSlug}/settings`, - }, - { - label: "Members", - href: `/${workspaceSlug}/settings/members`, - }, - { - label: "Billing & Plans", - href: `/${workspaceSlug}/settings/billing`, - }, - { - label: "Integrations", - href: `/${workspaceSlug}/settings/integrations`, - }, - { - label: "Imports", - href: `/${workspaceSlug}/settings/imports`, - }, - { - label: "Exports", - href: `/${workspaceSlug}/settings/exports`, - }, - ]; - - const profileLinks: Array<{ - label: string; - href: string; - }> = [ - { - label: "Profile", - href: `/${workspaceSlug}/me/profile`, - }, - { - label: "Activity", - href: `/${workspaceSlug}/me/profile/activity`, - }, - { - label: "Preferences", - href: `/${workspaceSlug}/me/profile/preferences`, - }, - ]; - - return ( -
-
- SETTINGS -
- {(projectId ? projectLinks : workspaceLinks).map((link) => ( - - -
- {link.label} -
-
- - ))} -
-
- {!projectId && ( -
- My Account -
- {profileLinks.map((link) => ( - - -
- {link.label} -
-
- - ))} -
-
- )} -
- ); -}; diff --git a/web/components/project/settings/features-list.tsx b/web/components/project/settings/features-list.tsx new file mode 100644 index 000000000..7efae23d4 --- /dev/null +++ b/web/components/project/settings/features-list.tsx @@ -0,0 +1,135 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { ContrastIcon, FileText, Inbox, Layers } from "lucide-react"; +import { DiceIcon, ToggleSwitch } from "@plane/ui"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { MiscellaneousEventType, TrackEventService } from "services/track_event.service"; +// hooks +import useToast from "hooks/use-toast"; +// types +import { IProject } from "types"; + +type Props = {}; + +const PROJECT_FEATURES_LIST = [ + { + title: "Cycles", + description: "Cycles are enabled for all the projects in this workspace. Access them from the sidebar.", + icon: , + + property: "cycle_view", + }, + { + title: "Modules", + description: "Modules are enabled for all the projects in this workspace. Access it from the sidebar.", + icon: , + property: "module_view", + }, + { + title: "Views", + description: "Views are enabled for all the projects in this workspace. Access it from the sidebar.", + icon: , + property: "issue_views_view", + }, + { + title: "Pages", + description: "Pages are enabled for all the projects in this workspace. Access it from the sidebar.", + icon: , + property: "page_view", + }, + { + title: "Inbox", + description: "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.", + icon: , + property: "inbox_view", + }, +]; + +const getEventType = (feature: string, toggle: boolean): MiscellaneousEventType => { + switch (feature) { + case "Cycles": + return toggle ? "TOGGLE_CYCLE_ON" : "TOGGLE_CYCLE_OFF"; + case "Modules": + return toggle ? "TOGGLE_MODULE_ON" : "TOGGLE_MODULE_OFF"; + case "Views": + return toggle ? "TOGGLE_VIEW_ON" : "TOGGLE_VIEW_OFF"; + case "Pages": + return toggle ? "TOGGLE_PAGES_ON" : "TOGGLE_PAGES_OFF"; + case "Inbox": + return toggle ? "TOGGLE_INBOX_ON" : "TOGGLE_INBOX_OFF"; + default: + throw new Error("Invalid feature"); + } +}; + +// services +const trackEventService = new TrackEventService(); + +export const ProjectFeaturesList: React.FC = observer((props) => { + const {} = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { project: projectStore, user: userStore } = useMobxStore(); + + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined; + const user = userStore.currentUser ?? undefined; + const isAdmin = userStore.projectMemberInfo?.role === 20; + + const { setToastAlert } = useToast(); + + const handleSubmit = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !projectDetails) return; + + setToastAlert({ + type: "success", + title: "Success!", + message: "Project feature updated successfully.", + }); + + projectStore.updateProject(workspaceSlug.toString(), projectId.toString(), formData); + }; + + return ( +
+ {PROJECT_FEATURES_LIST.map((feature) => ( +
+
+
{feature.icon}
+
+

{feature.title}

+

{feature.description}

+
+
+ { + trackEventService.trackMiscellaneousEvent( + { + workspaceId: (projectDetails?.workspace as any)?.id, + workspaceSlug, + projectId, + projectIdentifier: projectDetails?.identifier, + projectName: projectDetails?.name, + }, + getEventType(feature.title, !projectDetails?.[feature.property as keyof IProject]), + user + ); + handleSubmit({ + [feature.property]: !projectDetails?.[feature.property as keyof IProject], + }); + }} + disabled={!isAdmin} + size="sm" + /> +
+ ))} +
+ ); +}); diff --git a/web/components/project/settings/index.ts b/web/components/project/settings/index.ts new file mode 100644 index 000000000..65333a0e2 --- /dev/null +++ b/web/components/project/settings/index.ts @@ -0,0 +1 @@ +export * from "./features-list"; diff --git a/web/components/project/settings/single-label.tsx b/web/components/project/settings/single-label.tsx deleted file mode 100644 index 5dc0d36dc..000000000 --- a/web/components/project/settings/single-label.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { TwitterPicker } from "react-color"; -import { Popover, Transition } from "@headlessui/react"; -// ui -import { Button, CustomMenu, Input } from "@plane/ui"; -// icons -import { Component, Pencil } from "lucide-react"; -// types -import { IIssueLabels } from "types"; - -type Props = { - label: IIssueLabels; - issueLabels: IIssueLabels[]; - editLabel: (label: IIssueLabels) => void; - handleLabelDelete: (labelId: string) => void; -}; - -const defaultValues: Partial = { - name: "", - color: "#ff0000", -}; - -const SingleLabel: React.FC = ({ label, issueLabels, editLabel, handleLabelDelete }) => { - const [newLabelForm, setNewLabelForm] = useState(false); - - const { - formState: { errors, isSubmitting }, - watch, - control, - } = useForm({ defaultValues }); - - const children = issueLabels?.filter((l) => l.parent === label.id); - - return ( - <> - {children && children.length === 0 ? ( -
-
-
- -
{label.name}
-
- - {/* Convert to group */} - editLabel(label)}>Edit - handleLabelDelete(label.id)}>Delete - -
-
-
- - {({ open }) => ( - <> - - {watch("color") && watch("color") !== "" && ( - - )} - - - - - ( - onChange(value.hex)} /> - )} - /> - - - - )} - -
-
- ( - - )} - /> -
- - -
-
- ) : ( -
-

- - This is the label group title -

-
-
-
-
- This is the label title -
- -
-
-
- )} - - ); -}; - -export default SingleLabel; diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 986737a4a..58a006fd0 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -207,14 +207,6 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { ellipsis placement="bottom-start" > - {!shortContextMenu && isAdmin && ( - - - - Delete project - - - )} {!project.is_favorite && ( @@ -286,6 +278,15 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { )} + + {!shortContextMenu && isAdmin && ( + + + + Delete project + + + )} )} diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 2c33ca1b4..32c741fcf 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -1,19 +1,17 @@ import React, { useState, FC, useRef, useEffect } from "react"; import { useRouter } from "next/router"; -import useSWR from "swr"; import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd"; import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; // components import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; // icons import { ChevronDown, ChevronRight, Plus } from "lucide-react"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { orderArrayBy } from "helpers/array.helper"; // types import { IProject } from "types"; @@ -25,19 +23,14 @@ export const ProjectSidebarList: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // swr - useSWR( - workspaceSlug ? "PROJECTS_LIST" : null, - workspaceSlug ? () => projectStore.fetchProjects(workspaceSlug?.toString()) : null - ); + // states const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); // scroll animation state // refs const containerRef = useRef(null); - // user - const { user } = useUserAuth(); + // toast const { setToastAlert } = useToast(); @@ -53,8 +46,7 @@ export const ProjectSidebarList: FC = observer(() => { : undefined; const handleCopyText = (projectId: string) => { - const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues`).then(() => { + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index 41878525c..b477ebc9d 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -1,139 +1,34 @@ import React from "react"; import { useRouter } from "next/router"; - -import useSWR, { mutate } from "swr"; -// services -import { ProjectService } from "services/project"; -import { TrackEventService, MiscellaneousEventType } from "services/track_event.service"; +import useSWR from "swr"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/setting-layout"; // hooks -import useToast from "hooks/use-toast"; import useUserAuth from "hooks/use-user-auth"; // components import { ProjectSettingHeader } from "components/headers"; -// ui -import { ContrastIcon, DiceIcon, ToggleSwitch } from "@plane/ui"; -// icons -import { FileText, Inbox, Layers } from "lucide-react"; +import { ProjectFeaturesList } from "components/project"; // types -import { IProject, IUser } from "types"; import type { NextPage } from "next"; -// fetch-keys -import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys"; - -const featuresList = [ - { - title: "Cycles", - description: "Cycles are enabled for all the projects in this workspace. Access them from the sidebar.", - icon: , - - property: "cycle_view", - }, - { - title: "Modules", - description: "Modules are enabled for all the projects in this workspace. Access it from the sidebar.", - icon: , - property: "module_view", - }, - { - title: "Views", - description: "Views are enabled for all the projects in this workspace. Access it from the sidebar.", - icon: , - property: "issue_views_view", - }, - { - title: "Pages", - description: "Pages are enabled for all the projects in this workspace. Access it from the sidebar.", - icon: , - property: "page_view", - }, - { - title: "Inbox", - description: "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.", - icon: , - property: "inbox_view", - }, -]; - -const getEventType = (feature: string, toggle: boolean): MiscellaneousEventType => { - switch (feature) { - case "Cycles": - return toggle ? "TOGGLE_CYCLE_ON" : "TOGGLE_CYCLE_OFF"; - case "Modules": - return toggle ? "TOGGLE_MODULE_ON" : "TOGGLE_MODULE_OFF"; - case "Views": - return toggle ? "TOGGLE_VIEW_ON" : "TOGGLE_VIEW_OFF"; - case "Pages": - return toggle ? "TOGGLE_PAGES_ON" : "TOGGLE_PAGES_OFF"; - case "Inbox": - return toggle ? "TOGGLE_INBOX_ON" : "TOGGLE_INBOX_OFF"; - default: - throw new Error("Invalid feature"); - } -}; - -// services -const projectService = new ProjectService(); -const trackEventService = new TrackEventService(); const FeaturesSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { user } = useUserAuth(); + const {} = useUserAuth(); - const { setToastAlert } = useToast(); - - const { data: projectDetails } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null - ); + const { user: userStore } = useMobxStore(); const { data: memberDetails } = useSWR( - workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, + workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId - ? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) + ? () => userStore.fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null ); - const handleSubmit = async (formData: Partial) => { - if (!workspaceSlug || !projectId || !projectDetails) return; - - mutate( - PROJECTS_LIST(workspaceSlug.toString(), { - is_favorite: "all", - }), - (prevData) => prevData?.map((p) => (p.id === projectId ? { ...p, ...formData } : p)), - false - ); - - mutate( - PROJECT_DETAILS(projectId as string), - (prevData) => { - if (!prevData) return prevData; - - return { ...prevData, ...formData }; - }, - false - ); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Project feature updated successfully.", - }); - - await projectService.updateProject(workspaceSlug as string, projectId as string, formData, user).catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Project feature could not be updated. Please try again.", - }) - ); - }; - const isAdmin = memberDetails?.role === 20; return ( @@ -143,45 +38,7 @@ const FeaturesSettings: NextPage = () => {

Features

-
- {featuresList.map((feature) => ( -
-
-
- {feature.icon} -
-
-

{feature.title}

-

{feature.description}

-
-
- { - trackEventService.trackMiscellaneousEvent( - { - workspaceId: (projectDetails?.workspace as any)?.id, - workspaceSlug, - projectId, - projectIdentifier: projectDetails?.identifier, - projectName: projectDetails?.name, - }, - getEventType(feature.title, !projectDetails?.[feature.property as keyof IProject]), - user as IUser - ); - handleSubmit({ - [feature.property]: !projectDetails?.[feature.property as keyof IProject], - }); - }} - disabled={!isAdmin} - size="sm" - /> -
- ))} -
+ diff --git a/web/services/track_event.service.ts b/web/services/track_event.service.ts index 32ef9a542..d3a2bd743 100644 --- a/web/services/track_event.service.ts +++ b/web/services/track_event.service.ts @@ -632,8 +632,8 @@ export class TrackEventService extends APIService { }); } - async trackMiscellaneousEvent(data: any, eventName: MiscellaneousEventType, user: IUser): Promise { - if (!trackEvent) return; + async trackMiscellaneousEvent(data: any, eventName: MiscellaneousEventType, user: IUser | undefined): Promise { + if (!trackEvent || !user) return; return this.request({ url: "/api/track-event", diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 3c3678e06..b4361ec2f 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -575,6 +575,10 @@ export class ProjectStore implements IProjectStore { ...this.projects, [workspaceSlug]: this.projects[workspaceSlug].map((p) => (p.id === projectId ? { ...p, ...data } : p)), }; + this.project_details = { + ...this.project_details, + [projectId]: { ...this.project_details[projectId], ...data }, + }; }); const response = await this.projectService.updateProject( @@ -588,6 +592,7 @@ export class ProjectStore implements IProjectStore { console.log("Failed to create project from project store"); this.fetchProjects(workspaceSlug); + this.fetchProjectDetails(workspaceSlug, projectId); throw error; } }; diff --git a/web/store/user.store.ts b/web/store/user.store.ts index 1ebc107e8..42024a775 100644 --- a/web/store/user.store.ts +++ b/web/store/user.store.ts @@ -20,7 +20,7 @@ export interface IUserStore { workspaceMemberInfo: any; hasPermissionToWorkspace: boolean | null; - projectMemberInfo: any; + projectMemberInfo: IProjectMember | null; projectNotFound: boolean; hasPermissionToProject: boolean | null; @@ -48,7 +48,7 @@ class UserStore implements IUserStore { workspaceMemberInfo: any = null; hasPermissionToWorkspace: boolean | null = null; - projectMemberInfo: any = null; + projectMemberInfo: IProjectMember | null = null; projectNotFound: boolean = false; hasPermissionToProject: boolean | null = null; // root store