chore: implement mobx in project features settings (#2533)

This commit is contained in:
Aaryan Khandelwal 2023-10-26 14:58:00 +05:30 committed by GitHub
parent 993b388f00
commit d95ea463b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 182 additions and 489 deletions

View File

@ -30,7 +30,7 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
const { project: projectStore } = useMobxStore(); const { project: projectStore } = useMobxStore();
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug, projectId } = router.query;
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// form info // form info
@ -59,6 +59,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
await projectStore await projectStore
.deleteProject(workspaceSlug.toString(), project.id) .deleteProject(workspaceSlug.toString(), project.id)
.then(() => { .then(() => {
if (projectId && projectId.toString() === project.id) router.push(`/${workspaceSlug}/projects`);
handleClose(); handleClose();
}) })
.catch(() => { .catch(() => {

View File

@ -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 "./create-project-modal";
export * from "./delete-project-modal"; export * from "./delete-project-modal";
export * from "./sidebar-list"; export * from "./delete-project-section";
export * from "./settings-sidebar"; export * from "./form-loader";
export * from "./single-integration-card"; export * from "./form";
export * from "./sidebar-list-item"; export * from "./join-project-modal";
export * from "./label-select";
export * from "./leave-project-modal"; export * from "./leave-project-modal";
export * from "./member-select"; export * from "./member-select";
export * from "./members-select"; export * from "./members-select";
export * from "./label-select";
export * from "./priority-select"; export * from "./priority-select";
export * from "./card-list"; export * from "./sidebar-list-item";
export * from "./card"; export * from "./sidebar-list";
export * from "./join-project-modal"; export * from "./single-integration-card";
export * from "./form";
export * from "./form-loader";
export * from "./delete-project-section";
export * from "./publish-project";

View File

@ -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 (
<div className="flex flex-col gap-6 w-80 px-5">
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
<div className="flex flex-col gap-1 w-full">
{(projectId ? projectLinks : workspaceLinks).map((link) => (
<Link key={link.href} href={link.href}>
<a>
<div
className={`px-4 py-2 text-sm font-medium rounded-md ${
(
link.label === "Import"
? router.asPath.includes(link.href)
: router.asPath === link.href
)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
}`}
>
{link.label}
</div>
</a>
</Link>
))}
</div>
</div>
{!projectId && (
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">My Account</span>
<div className="flex flex-col gap-1 w-full">
{profileLinks.map((link) => (
<Link key={link.href} href={link.href}>
<a>
<div
className={`px-4 py-2 text-sm font-medium rounded-md ${
(
link.label === "Import"
? router.asPath.includes(link.href)
: router.asPath === link.href
)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
}`}
>
{link.label}
</div>
</a>
</Link>
))}
</div>
</div>
)}
</div>
);
};

View File

@ -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: <ContrastIcon className="h-4 w-4 text-purple-500 flex-shrink-0 rotate-180" />,
property: "cycle_view",
},
{
title: "Modules",
description: "Modules are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <DiceIcon width={16} height={16} className="flex-shrink-0" />,
property: "module_view",
},
{
title: "Views",
description: "Views are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <Layers className="h-4 w-4 text-cyan-500 flex-shrink-0" />,
property: "issue_views_view",
},
{
title: "Pages",
description: "Pages are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <FileText className="h-4 w-4 text-red-400 flex-shrink-0" />,
property: "page_view",
},
{
title: "Inbox",
description: "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
icon: <Inbox className="h-4 w-4 text-fuchsia-500 flex-shrink-0" />,
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<Props> = 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<IProject>) => {
if (!workspaceSlug || !projectId || !projectDetails) return;
setToastAlert({
type: "success",
title: "Success!",
message: "Project feature updated successfully.",
});
projectStore.updateProject(workspaceSlug.toString(), projectId.toString(), formData);
};
return (
<div>
{PROJECT_FEATURES_LIST.map((feature) => (
<div
key={feature.property}
className="flex items-center justify-between gap-x-8 gap-y-2 border-b border-custom-border-200 bg-custom-background-100 p-4"
>
<div className="flex items-start gap-3">
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">{feature.icon}</div>
<div className="">
<h4 className="text-sm font-medium">{feature.title}</h4>
<p className="text-sm text-custom-text-200 tracking-tight">{feature.description}</p>
</div>
</div>
<ToggleSwitch
value={projectDetails?.[feature.property as keyof IProject]}
onChange={() => {
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"
/>
</div>
))}
</div>
);
});

View File

@ -0,0 +1 @@
export * from "./features-list";

View File

@ -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<IIssueLabels> = {
name: "",
color: "#ff0000",
};
const SingleLabel: React.FC<Props> = ({ label, issueLabels, editLabel, handleLabelDelete }) => {
const [newLabelForm, setNewLabelForm] = useState(false);
const {
formState: { errors, isSubmitting },
watch,
control,
} = useForm<IIssueLabels>({ defaultValues });
const children = issueLabels?.filter((l) => l.parent === label.id);
return (
<>
{children && children.length === 0 ? (
<div className="gap-2 space-y-3 divide-y rounded-md border border-custom-border-200 p-3 md:w-2/3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className="h-3 w-3 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color,
}}
/>
<h6 className="text-sm">{label.name}</h6>
</div>
<CustomMenu ellipsis>
{/* <CustomMenu.MenuItem>Convert to group</CustomMenu.MenuItem> */}
<CustomMenu.MenuItem onClick={() => editLabel(label)}>Edit</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label.id)}>Delete</CustomMenu.MenuItem>
</CustomMenu>
</div>
<div className={`flex items-center gap-2 ${newLabelForm ? "" : "hidden"}`}>
<div className="h-8 w-8 flex-shrink-0">
<Popover className="relative flex h-full w-full items-center justify-center rounded-xl bg-custom-background-80">
{({ open }) => (
<>
<Popover.Button
className={`group inline-flex items-center text-base font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
{watch("color") && watch("color") !== "" && (
<span
className="h-4 w-4 rounded"
style={{
backgroundColor: watch("color") ?? "black",
}}
/>
)}
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
<Controller
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
)}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
<div className="flex w-full flex-col justify-center">
<Controller
control={control}
name="name"
rules={{
required: "Label title is required",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="labelName"
name="name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.name)}
placeholder="Label title"
className="w-full"
/>
)}
/>
</div>
<Button variant="neutral-primary" onClick={() => setNewLabelForm(false)}>
Cancel
</Button>
<Button variant="primary" loading={isSubmitting}>
{isSubmitting ? "Adding" : "Add"}
</Button>
</div>
</div>
) : (
<div className="rounded-md bg-custom-background-80 p-4 text-custom-text-100">
<h3 className="flex items-center gap-2 font-medium leading-5">
<Component className="h-5 w-5" />
This is the label group title
</h3>
<div className="mt-4 pl-5">
<div className="group flex items-center justify-between rounded p-2 text-sm hover:bg-custom-background-90">
<h5 className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-red-600" />
This is the label title
</h5>
<button type="button" className="hidden group-hover:block">
<Pencil className="h-3 w-3" />
</button>
</div>
</div>
</div>
)}
</>
);
};
export default SingleLabel;

View File

@ -207,14 +207,6 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
ellipsis ellipsis
placement="bottom-start" placement="bottom-start"
> >
{!shortContextMenu && isAdmin && (
<CustomMenu.MenuItem onClick={handleDeleteProjectClick}>
<span className="flex items-center justify-start gap-2 ">
<Trash2 className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Delete project</span>
</span>
</CustomMenu.MenuItem>
)}
{!project.is_favorite && ( {!project.is_favorite && (
<CustomMenu.MenuItem onClick={handleAddToFavorites}> <CustomMenu.MenuItem onClick={handleAddToFavorites}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
@ -286,6 +278,15 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} )}
{!shortContextMenu && isAdmin && (
<CustomMenu.MenuItem onClick={handleDeleteProjectClick}>
<span className="flex items-center justify-start gap-2 ">
<Trash2 className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Delete project</span>
</span>
</CustomMenu.MenuItem>
)}
</CustomMenu> </CustomMenu>
)} )}
</div> </div>

View File

@ -1,19 +1,17 @@
import React, { useState, FC, useRef, useEffect } from "react"; import React, { useState, FC, useRef, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd"; import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// components // components
import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; import { CreateProjectModal, ProjectSidebarListItem } from "components/project";
// icons // icons
import { ChevronDown, ChevronRight, Plus } from "lucide-react"; import { ChevronDown, ChevronRight, Plus } from "lucide-react";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IProject } from "types"; import { IProject } from "types";
@ -25,19 +23,14 @@ export const ProjectSidebarList: FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// swr
useSWR(
workspaceSlug ? "PROJECTS_LIST" : null,
workspaceSlug ? () => projectStore.fetchProjects(workspaceSlug?.toString()) : null
);
// states // states
const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false); const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false);
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
// refs // refs
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
// user
const { user } = useUserAuth();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -53,8 +46,7 @@ export const ProjectSidebarList: FC = observer(() => {
: undefined; : undefined;
const handleCopyText = (projectId: string) => { const handleCopyText = (projectId: string) => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => {
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues`).then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Link Copied!", title: "Link Copied!",

View File

@ -1,139 +1,34 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
import useSWR, { mutate } from "swr"; // mobx store
// services import { useMobxStore } from "lib/mobx/store-provider";
import { ProjectService } from "services/project";
import { TrackEventService, MiscellaneousEventType } from "services/track_event.service";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/setting-layout"; import { ProjectSettingLayout } from "layouts/setting-layout";
// hooks // hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// components // components
import { ProjectSettingHeader } from "components/headers"; import { ProjectSettingHeader } from "components/headers";
// ui import { ProjectFeaturesList } from "components/project";
import { ContrastIcon, DiceIcon, ToggleSwitch } from "@plane/ui";
// icons
import { FileText, Inbox, Layers } from "lucide-react";
// types // types
import { IProject, IUser } from "types";
import type { NextPage } from "next"; 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: <ContrastIcon className="h-4 w-4 text-purple-500 flex-shrink-0 rotate-180" />,
property: "cycle_view",
},
{
title: "Modules",
description: "Modules are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <DiceIcon width={16} height={16} className="flex-shrink-0" />,
property: "module_view",
},
{
title: "Views",
description: "Views are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <Layers className="h-4 w-4 text-cyan-500 flex-shrink-0" />,
property: "issue_views_view",
},
{
title: "Pages",
description: "Pages are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <FileText className="h-4 w-4 text-red-400 flex-shrink-0" />,
property: "page_view",
},
{
title: "Inbox",
description: "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
icon: <Inbox className="h-4 w-4 text-fuchsia-500 flex-shrink-0" />,
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 FeaturesSettings: NextPage = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth(); const {} = useUserAuth();
const { setToastAlert } = useToast(); const { user: userStore } = useMobxStore();
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const { data: memberDetails } = useSWR( const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) ? () => userStore.fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString())
: null : null
); );
const handleSubmit = async (formData: Partial<IProject>) => {
if (!workspaceSlug || !projectId || !projectDetails) return;
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug.toString(), {
is_favorite: "all",
}),
(prevData) => prevData?.map((p) => (p.id === projectId ? { ...p, ...formData } : p)),
false
);
mutate<IProject>(
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; const isAdmin = memberDetails?.role === 20;
return ( return (
@ -143,45 +38,7 @@ const FeaturesSettings: NextPage = () => {
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Features</h3> <h3 className="text-xl font-medium">Features</h3>
</div> </div>
<div> <ProjectFeaturesList />
{featuresList.map((feature) => (
<div
key={feature.property}
className="flex items-center justify-between gap-x-8 gap-y-2 border-b border-custom-border-200 bg-custom-background-100 p-4"
>
<div className="flex items-start gap-3">
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
{feature.icon}
</div>
<div className="">
<h4 className="text-sm font-medium">{feature.title}</h4>
<p className="text-sm text-custom-text-200 tracking-tight">{feature.description}</p>
</div>
</div>
<ToggleSwitch
value={projectDetails?.[feature.property as keyof IProject]}
onChange={() => {
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"
/>
</div>
))}
</div>
</section> </section>
</ProjectSettingLayout> </ProjectSettingLayout>
</AppLayout> </AppLayout>

View File

@ -632,8 +632,8 @@ export class TrackEventService extends APIService {
}); });
} }
async trackMiscellaneousEvent(data: any, eventName: MiscellaneousEventType, user: IUser): Promise<any> { async trackMiscellaneousEvent(data: any, eventName: MiscellaneousEventType, user: IUser | undefined): Promise<any> {
if (!trackEvent) return; if (!trackEvent || !user) return;
return this.request({ return this.request({
url: "/api/track-event", url: "/api/track-event",

View File

@ -575,6 +575,10 @@ export class ProjectStore implements IProjectStore {
...this.projects, ...this.projects,
[workspaceSlug]: this.projects[workspaceSlug].map((p) => (p.id === projectId ? { ...p, ...data } : p)), [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( const response = await this.projectService.updateProject(
@ -588,6 +592,7 @@ export class ProjectStore implements IProjectStore {
console.log("Failed to create project from project store"); console.log("Failed to create project from project store");
this.fetchProjects(workspaceSlug); this.fetchProjects(workspaceSlug);
this.fetchProjectDetails(workspaceSlug, projectId);
throw error; throw error;
} }
}; };

View File

@ -20,7 +20,7 @@ export interface IUserStore {
workspaceMemberInfo: any; workspaceMemberInfo: any;
hasPermissionToWorkspace: boolean | null; hasPermissionToWorkspace: boolean | null;
projectMemberInfo: any; projectMemberInfo: IProjectMember | null;
projectNotFound: boolean; projectNotFound: boolean;
hasPermissionToProject: boolean | null; hasPermissionToProject: boolean | null;
@ -48,7 +48,7 @@ class UserStore implements IUserStore {
workspaceMemberInfo: any = null; workspaceMemberInfo: any = null;
hasPermissionToWorkspace: boolean | null = null; hasPermissionToWorkspace: boolean | null = null;
projectMemberInfo: any = null; projectMemberInfo: IProjectMember | null = null;
projectNotFound: boolean = false; projectNotFound: boolean = false;
hasPermissionToProject: boolean | null = null; hasPermissionToProject: boolean | null = null;
// root store // root store