fix: workspace members store added and implemented across the app (#2732)

* fix: minor changes

* fix: workspace members store added and implemnted across the app
This commit is contained in:
sriram veeraghanta 2023-11-09 00:35:12 +05:30 committed by GitHub
parent 556b2d2617
commit a6567bbce4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 529 additions and 352 deletions

View File

@ -41,6 +41,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
globalViewFilters: globalViewFiltersStore, globalViewFilters: globalViewFiltersStore,
workspaceFilter: workspaceFilterStore, workspaceFilter: workspaceFilterStore,
workspace: workspaceStore, workspace: workspaceStore,
workspaceMember: { workspaceMembers },
project: projectStore, project: projectStore,
} = useMobxStore(); } = useMobxStore();
@ -145,7 +146,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet} layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
labels={workspaceStore.workspaceLabels ?? undefined} labels={workspaceStore.workspaceLabels ?? undefined}
members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined} members={workspaceMembers?.map((m) => m.member) ?? undefined}
projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined}
/> />
</FiltersDropdown> </FiltersDropdown>

View File

@ -22,6 +22,7 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
globalViewFilters: globalViewFiltersStore, globalViewFilters: globalViewFiltersStore,
project: projectStore, project: projectStore,
workspace: workspaceStore, workspace: workspaceStore,
workspaceMember: { workspaceMembers },
} = useMobxStore(); } = useMobxStore();
const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined; const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined;
@ -101,7 +102,7 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
handleClearAllFilters={handleClearAllFilters} handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={workspaceStore.workspaceLabels ?? undefined} labels={workspaceStore.workspaceLabels ?? undefined}
members={workspaceStore.workspaceMembers?.map((m) => m.member)} members={workspaceMembers?.map((m) => m.member)}
projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined}
/> />
{storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data.filters ?? {}) && ( {storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data.filters ?? {}) && (

View File

@ -39,18 +39,19 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
multiple = false, multiple = false,
noLabelBorder = false, noLabelBorder = false,
} = props; } = props;
// store
const { workspace: workspaceStore, project: projectStore } = useMobxStore(); const {
workspace: workspaceStore,
project: projectStore,
workspaceMember: { workspaceMembers, fetchWorkspaceMembers },
} = useMobxStore();
const workspaceSlug = workspaceStore?.workspaceSlug; const workspaceSlug = workspaceStore?.workspaceSlug;
// states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null); const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [isLoading, setIsLoading] = useState<Boolean>(false); const [isLoading, setIsLoading] = useState<Boolean>(false);
const workspaceMembers = workspaceSlug ? workspaceStore?.workspaceMembers : undefined;
const fetchProjectMembers = () => { const fetchProjectMembers = () => {
setIsLoading(true); setIsLoading(true);
if (workspaceSlug && projectId) if (workspaceSlug && projectId)
@ -59,10 +60,9 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
projectStore.fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false)); projectStore.fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false));
}; };
const fetchWorkspaceMembers = () => { const getWorkspaceMembers = () => {
setIsLoading(true); setIsLoading(true);
if (workspaceSlug) if (workspaceSlug) workspaceSlug && fetchWorkspaceMembers(workspaceSlug).then(() => setIsLoading(false));
workspaceSlug && workspaceStore.fetchWorkspaceMembers(workspaceSlug).then(() => setIsLoading(false));
}; };
const options = (workspaceMembers ?? [])?.map((member) => ({ const options = (workspaceMembers ?? [])?.map((member) => ({
@ -151,7 +151,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
className={`flex items-center justify-between gap-1 w-full text-xs ${ className={`flex items-center justify-between gap-1 w-full text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80" disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => !workspaceMembers && fetchWorkspaceMembers()} onClick={() => !workspaceMembers && getWorkspaceMembers()}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}

View File

@ -27,6 +27,7 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
globalViewFilters: globalViewFiltersStore, globalViewFilters: globalViewFiltersStore,
workspaceFilter: workspaceFilterStore, workspaceFilter: workspaceFilterStore,
workspace: workspaceStore, workspace: workspaceStore,
workspaceMember: { workspaceMembers },
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
project: projectStore, project: projectStore,
} = useMobxStore(); } = useMobxStore();
@ -106,7 +107,7 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
displayFilters={workspaceFilterStore.workspaceDisplayFilters} displayFilters={workspaceFilterStore.workspaceDisplayFilters}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate} handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues} issues={issues}
members={workspaceStore.workspaceMembers ? workspaceStore.workspaceMembers.map((m) => m.member) : undefined} members={workspaceMembers?.map((m) => m.member)}
labels={workspaceStore.workspaceLabels ? workspaceStore.workspaceLabels : undefined} labels={workspaceStore.workspaceLabels ? workspaceStore.workspaceLabels : undefined}
handleIssueAction={() => {}} handleIssueAction={() => {}}
handleUpdateIssue={handleUpdateIssue} handleUpdateIssue={handleUpdateIssue}

View File

@ -22,15 +22,12 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore(); const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore();
const { isLoading } = useSWR( useSWR(workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) { if (workspaceSlug && projectId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString()); await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
} }
} });
);
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilterStore.userDisplayFilters.layout;

View File

@ -2,7 +2,7 @@ import { FC } from "react";
import Link from "next/link"; import Link from "next/link";
import { History } from "lucide-react"; import { History } from "lucide-react";
// packages // packages
import { Loader, Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// components // components
import { ActivityIcon, ActivityMessage } from "components/core"; import { ActivityIcon, ActivityMessage } from "components/core";
import { IssueCommentCard } from "./comment-card"; import { IssueCommentCard } from "./comment-card";

View File

@ -63,8 +63,10 @@ export interface ICreateProjectForm {
export const CreateProjectModal: FC<Props> = observer((props) => { export const CreateProjectModal: FC<Props> = observer((props) => {
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
// store // store
const { project: projectStore, workspace: workspaceStore } = useMobxStore(); const {
const workspaceMembers = workspaceStore.members[workspaceSlug] || []; project: projectStore,
workspaceMember: { workspaceMembers },
} = useMobxStore();
// states // states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
// toast // toast
@ -370,7 +372,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
<WorkspaceMemberSelect <WorkspaceMemberSelect
value={value} value={value}
onChange={onChange} onChange={onChange}
options={workspaceMembers} options={workspaceMembers || []}
placeholder="Select Lead" placeholder="Select Lead"
/> />
)} )}

View File

@ -72,9 +72,10 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store // store
const { project: projectStore, user: userStore } = useMobxStore(); const {
const { currentUser, currentProjectRole } = userStore; project: { currentProjectDetails, updateProject },
const { currentProjectDetails } = projectStore; user: { currentUser, currentProjectRole },
} = useMobxStore();
const isAdmin = currentProjectRole === 20; const isAdmin = currentProjectRole === 20;
// hooks // hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -86,7 +87,7 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
title: "Success!", title: "Success!",
message: "Project feature updated successfully.", message: "Project feature updated successfully.",
}); });
projectStore.updateProject(workspaceSlug.toString(), projectId.toString(), formData); updateProject(workspaceSlug.toString(), projectId.toString(), formData);
}; };
if (!currentUser) return <></>; if (!currentUser) return <></>;

View File

@ -19,8 +19,9 @@ export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((props) =>
const [isRemoving, setIsRemoving] = useState(false); const [isRemoving, setIsRemoving] = useState(false);
const { user: userStore } = useMobxStore(); const {
const user = userStore.currentUser; user: { currentUser },
} = useMobxStore();
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
@ -69,10 +70,10 @@ export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((props) =>
</div> </div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100"> <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
{user?.id === data?.memberId ? "Leave workspace?" : `Remove ${data?.display_name}?`} {currentUser?.id === data?.memberId ? "Leave workspace?" : `Remove ${data?.display_name}?`}
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
{user?.id === data?.memberId ? ( {currentUser?.id === data?.memberId ? (
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200">
Are you sure you want to leave the workspace? You will no longer have access to this Are you sure you want to leave the workspace? You will no longer have access to this
workspace. This action cannot be undone. workspace. This action cannot be undone.

View File

@ -1,28 +1,19 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { mutate } from "swr";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services
import { WorkspaceService } from "services/workspace.service";
// hooks
import useToast from "hooks/use-toast";
// ui // ui
import { Button, CustomSelect, Input } from "@plane/ui"; import { Button, CustomSelect, Input } from "@plane/ui";
// icons // icons
import { Plus, X } from "lucide-react"; import { Plus, X } from "lucide-react";
// types // types
import { IUser, TUserWorkspaceRole } from "types"; import { IWorkspaceBulkInviteFormData, TUserWorkspaceRole } from "types";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
// fetch-keys
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
workspaceSlug: string; onSubmit: (data: IWorkspaceBulkInviteFormData) => Promise<void> | undefined;
user: IUser | undefined;
onSuccess?: () => Promise<void>;
}; };
type EmailRole = { type EmailRole = {
@ -43,11 +34,10 @@ const defaultValues: FormValues = {
], ],
}; };
const workspaceService = new WorkspaceService();
export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => { export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
const { isOpen, onClose, workspaceSlug, user, onSuccess } = props; const { isOpen, onClose, onSubmit } = props;
// form info
const { const {
control, control,
reset, reset,
@ -60,8 +50,6 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
name: "emails", name: "emails",
}); });
const { setToastAlert } = useToast();
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
@ -71,31 +59,29 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
}, 350); }, 350);
}; };
const onSubmit = async (formData: FormValues) => { // const onSubmit = async (formData: FormValues) => {
if (!workspaceSlug) return; // if (!workspaceSlug) return;
await workspaceService // return workspaceService
.inviteWorkspace(workspaceSlug, formData, user) // .inviteWorkspace(workspaceSlug, formData, user)
.then(async () => {
if (onSuccess) await onSuccess();
handleClose(); // .then(async () => {
// if (onSuccess) await onSuccess();
setToastAlert({ // handleClose();
type: "success", // setToastAlert({
title: "Success!", // type: "success",
message: "Invitations sent successfully.", // title: "Success!",
}); // message: "Invitations sent successfully.",
}) // });
.catch((err) => // })
setToastAlert({ // .catch((err) =>
type: "error", // setToastAlert({
title: "Error!", // type: "error",
message: `${err.error ?? "Something went wrong. Please try again."}`, // title: "Error!",
}) // message: `${err.error ?? "Something went wrong. Please try again."}`,
) // })
.finally(() => mutate(WORKSPACE_INVITATIONS)); // );
}; // };
const appendField = () => { const appendField = () => {
append({ email: "", role: 15 }); append({ email: "", role: 15 });

View File

@ -3,8 +3,6 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// services
import { WorkspaceService } from "services/workspace.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
@ -33,17 +31,16 @@ type Props = {
}; };
}; };
// services
const workspaceService = new WorkspaceService();
export const WorkspaceMembersListItem: FC<Props> = (props) => { export const WorkspaceMembersListItem: FC<Props> = (props) => {
const { member } = props; const { member } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { workspace: workspaceStore, user: userStore } = useMobxStore(); const {
const { currentWorkspaceMemberInfo, currentWorkspaceRole } = userStore; workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
user: { currentWorkspaceMemberInfo, currentWorkspaceRole },
} = useMobxStore();
const isAdmin = currentWorkspaceRole === 20; const isAdmin = currentWorkspaceRole === 20;
// states // states
const [removeMemberModal, setRemoveMemberModal] = useState(false); const [removeMemberModal, setRemoveMemberModal] = useState(false);
@ -54,7 +51,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
if (member.member) if (member.member)
await workspaceStore.removeMember(workspaceSlug.toString(), member.id).catch((err) => { await removeMember(workspaceSlug.toString(), member.id).catch((err) => {
const error = err?.error; const error = err?.error;
setToastAlert({ setToastAlert({
type: "error", type: "error",
@ -63,8 +60,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
}); });
}); });
else else
await workspaceService await deleteWorkspaceInvitation(workspaceSlug.toString(), member.id)
.deleteWorkspaceInvitations(workspaceSlug.toString(), member.id)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -157,11 +153,9 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
onChange={(value: TUserWorkspaceRole | undefined) => { onChange={(value: TUserWorkspaceRole | undefined) => {
if (!workspaceSlug || !value) return; if (!workspaceSlug || !value) return;
workspaceStore updateMember(workspaceSlug.toString(), member.id, {
.updateMember(workspaceSlug.toString(), member.id, {
role: value, role: value,
}) }).catch(() => {
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",

View File

@ -1,64 +1,45 @@
import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// services
import { WorkspaceService } from "services/workspace.service";
// components // components
import { WorkspaceMembersListItem } from "components/workspace"; import { WorkspaceMembersListItem } from "components/workspace";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
const workspaceService = new WorkspaceService(); export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ searchQuery }) => {
export const WorkspaceMembersList: React.FC<{ searchQuery: string }> = observer(({ searchQuery }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { workspace: workspaceStore, user: userStore } = useMobxStore(); const {
const workspaceMembers = workspaceStore.workspaceMembers; workspaceMember: {
const user = userStore.currentWorkspaceMemberInfo; workspaceMembers,
workspaceMembersWithInvitations,
workspaceMemberInvitations,
fetchWorkspaceMemberInvitations,
},
user: { currentWorkspaceMemberInfo },
} = useMobxStore();
// fetching workspace invitations // fetching workspace invitations
const { data: workspaceInvitations } = useSWR( useSWR(
workspaceSlug ? `WORKSPACE_INVITATIONS_${workspaceSlug.toString()}` : null, workspaceSlug ? `WORKSPACE_INVITATIONS_${workspaceSlug.toString()}` : null,
workspaceSlug ? () => workspaceService.workspaceInvitations(workspaceSlug.toString()) : null workspaceSlug ? () => fetchWorkspaceMemberInvitations(workspaceSlug.toString()) : null
); );
const members = [ const searchedMembers = workspaceMembersWithInvitations?.filter((member: any) => {
...(workspaceInvitations?.map((item) => ({
id: item.id,
memberId: item.id,
avatar: "",
first_name: item.email,
last_name: "",
email: item.email,
display_name: item.email,
role: item.role,
status: item.accepted,
member: false,
accountCreated: item.accepted,
})) || []),
...(workspaceMembers?.map((item) => ({
id: item.id,
memberId: item.member?.id,
avatar: item.member?.avatar,
first_name: item.member?.first_name,
last_name: item.member?.last_name,
email: item.member?.email,
display_name: item.member?.display_name,
role: item.role,
status: true,
member: true,
accountCreated: true,
})) || []),
];
const searchedMembers = members?.filter((member) => {
const fullName = `${member.first_name} ${member.last_name}`.toLowerCase(); const fullName = `${member.first_name} ${member.last_name}`.toLowerCase();
const displayName = member.display_name.toLowerCase(); const displayName = member.display_name.toLowerCase();
return displayName.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase()); return displayName.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
}); });
if (!workspaceMembers || !workspaceInvitations || !user) if (
!workspaceMembers ||
!workspaceMemberInvitations ||
!workspaceMembersWithInvitations ||
!currentWorkspaceMemberInfo
)
return ( return (
<Loader className="space-y-5"> <Loader className="space-y-5">
<Loader.Item height="40px" /> <Loader.Item height="40px" />
@ -70,10 +51,10 @@ export const WorkspaceMembersList: React.FC<{ searchQuery: string }> = observer(
return ( return (
<div className="divide-y-[0.5px] divide-custom-border-200"> <div className="divide-y-[0.5px] divide-custom-border-200">
{members.length > 0 {workspaceMembersWithInvitations.length > 0
? searchedMembers.map((member) => <WorkspaceMembersListItem key={member.id} member={member} />) ? searchedMembers?.map((member) => <WorkspaceMembersListItem key={member.id} member={member} />)
: null} : null}
{searchedMembers.length === 0 && ( {searchedMembers?.length === 0 && (
<h4 className="text-md text-custom-text-400 text-center mt-20">No matching member</h4> <h4 className="text-md text-custom-text-400 text-center mt-20">No matching member</h4>
)} )}
</div> </div>

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState, FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
@ -29,7 +29,7 @@ const defaultValues: Partial<IWorkspace> = {
// services // services
const fileService = new FileService(); const fileService = new FileService();
export const WorkspaceDetails: React.FC = observer(() => { export const WorkspaceDetails: FC = observer(() => {
// states // states
const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false); const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -37,9 +37,10 @@ export const WorkspaceDetails: React.FC = observer(() => {
const [isImageRemoving, setIsImageRemoving] = useState(false); const [isImageRemoving, setIsImageRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
// store // store
const { workspace: workspaceStore, user: userStore } = useMobxStore(); const {
const activeWorkspace = workspaceStore.currentWorkspace; workspace: { currentWorkspace, updateWorkspace },
const { currentWorkspaceRole } = userStore; user: { currentWorkspaceRole },
} = useMobxStore();
const isAdmin = currentWorkspaceRole === 20; const isAdmin = currentWorkspaceRole === 20;
// hooks // hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -52,11 +53,11 @@ export const WorkspaceDetails: React.FC = observer(() => {
setValue, setValue,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<IWorkspace>({ } = useForm<IWorkspace>({
defaultValues: { ...defaultValues, ...activeWorkspace }, defaultValues: { ...defaultValues, ...currentWorkspace },
}); });
const onSubmit = async (formData: IWorkspace) => { const onSubmit = async (formData: IWorkspace) => {
if (!activeWorkspace) return; if (!currentWorkspace) return;
const payload: Partial<IWorkspace> = { const payload: Partial<IWorkspace> = {
logo: formData.logo, logo: formData.logo,
@ -64,8 +65,7 @@ export const WorkspaceDetails: React.FC = observer(() => {
organization_size: formData.organization_size, organization_size: formData.organization_size,
}; };
await workspaceStore await updateWorkspace(currentWorkspace.slug, payload)
.updateWorkspace(activeWorkspace.slug, payload)
.then(() => .then(() =>
setToastAlert({ setToastAlert({
title: "Success", title: "Success",
@ -77,13 +77,12 @@ export const WorkspaceDetails: React.FC = observer(() => {
}; };
const handleDelete = (url: string | null | undefined) => { const handleDelete = (url: string | null | undefined) => {
if (!activeWorkspace || !url) return; if (!currentWorkspace || !url) return;
setIsImageRemoving(true); setIsImageRemoving(true);
fileService.deleteFile(activeWorkspace.id, url).then(() => { fileService.deleteFile(currentWorkspace.id, url).then(() => {
workspaceStore updateWorkspace(currentWorkspace.slug, { logo: "" })
.updateWorkspace(activeWorkspace.slug, { logo: "" })
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -104,10 +103,10 @@ export const WorkspaceDetails: React.FC = observer(() => {
}; };
useEffect(() => { useEffect(() => {
if (activeWorkspace) reset({ ...activeWorkspace }); if (currentWorkspace) reset({ ...currentWorkspace });
}, [activeWorkspace, reset]); }, [currentWorkspace, reset]);
if (!activeWorkspace) if (!currentWorkspace)
return ( return (
<div className="grid place-items-center h-full w-full px-4 sm:px-0"> <div className="grid place-items-center h-full w-full px-4 sm:px-0">
<Spinner /> <Spinner />
@ -119,13 +118,13 @@ export const WorkspaceDetails: React.FC = observer(() => {
<DeleteWorkspaceModal <DeleteWorkspaceModal
isOpen={deleteWorkspaceModal} isOpen={deleteWorkspaceModal}
onClose={() => setDeleteWorkspaceModal(false)} onClose={() => setDeleteWorkspaceModal(false)}
data={activeWorkspace} data={currentWorkspace}
/> />
<ImageUploadModal <ImageUploadModal
isOpen={isImageUploadModalOpen} isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)} onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isImageRemoving} isRemoving={isImageRemoving}
handleDelete={() => handleDelete(activeWorkspace?.logo)} handleDelete={() => handleDelete(currentWorkspace?.logo)}
onSuccess={(imageUrl) => { onSuccess={(imageUrl) => {
setIsImageUploading(true); setIsImageUploading(true);
setValue("logo", imageUrl); setValue("logo", imageUrl);
@ -148,7 +147,7 @@ export const WorkspaceDetails: React.FC = observer(() => {
</div> </div>
) : ( ) : (
<div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white"> <div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
{activeWorkspace?.name?.charAt(0) ?? "N"} {currentWorkspace?.name?.charAt(0) ?? "N"}
</div> </div>
)} )}
</button> </button>
@ -157,7 +156,7 @@ export const WorkspaceDetails: React.FC = observer(() => {
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3> <h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
<span className="text-sm tracking-tight">{`${ <span className="text-sm tracking-tight">{`${
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "") typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}</span> }/${currentWorkspace.slug}`}</span>
<div className="flex item-center gap-2.5"> <div className="flex item-center gap-2.5">
<button <button
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium" className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
@ -246,7 +245,7 @@ export const WorkspaceDetails: React.FC = observer(() => {
value={`${ value={`${
typeof window !== "undefined" && typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "") window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`} }/${currentWorkspace.slug}`}
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.url)} hasError={Boolean(errors.url)}

View File

@ -49,16 +49,17 @@ const authService = new AuthService();
export const WorkspaceSidebarDropdown = observer(() => { export const WorkspaceSidebarDropdown = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store
const { theme: themeStore, workspace: workspaceStore, user: userStore } = useMobxStore(); const {
const { workspaces, currentWorkspace: activeWorkspace } = workspaceStore; theme: { sidebarCollapsed },
const user = userStore.currentUser; workspace: { workspaces, currentWorkspace: activeWorkspace },
user: { currentUser, updateCurrentUser },
} = useMobxStore();
// hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const handleWorkspaceNavigation = (workspace: IWorkspace) => { const handleWorkspaceNavigation = (workspace: IWorkspace) => {
userStore updateCurrentUser({
.updateCurrentUser({
last_workspace_id: workspace?.id, last_workspace_id: workspace?.id,
}) })
.then(() => { .then(() => {
@ -94,7 +95,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
<Menu.Button className="text-custom-sidebar-text-200 rounded-sm text-sm font-medium focus:outline-none w-full h-full truncate"> <Menu.Button className="text-custom-sidebar-text-200 rounded-sm text-sm font-medium focus:outline-none w-full h-full truncate">
<div <div
className={`flex items-center gap-x-2 rounded-sm bg-custom-sidebar-background-80 p-1 truncate ${ className={`flex items-center gap-x-2 rounded-sm bg-custom-sidebar-background-80 p-1 truncate ${
themeStore.sidebarCollapsed ? "justify-center" : "" sidebarCollapsed ? "justify-center" : ""
}`} }`}
> >
<div className="relative grid h-6 w-6 place-items-center rounded bg-gray-700 uppercase text-white flex-shrink-0"> <div className="relative grid h-6 w-6 place-items-center rounded bg-gray-700 uppercase text-white flex-shrink-0">
@ -109,7 +110,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
)} )}
</div> </div>
{!themeStore.sidebarCollapsed && ( {!sidebarCollapsed && (
<h4 className="text-custom-text-100 truncate"> <h4 className="text-custom-text-100 truncate">
{activeWorkspace?.name ? activeWorkspace.name : "Loading..."} {activeWorkspace?.name ? activeWorkspace.name : "Loading..."}
</h4> </h4>
@ -198,7 +199,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
)} )}
</div> </div>
<div className="flex w-full flex-col items-start justify-start gap-2 border-t border-custom-sidebar-border-200 px-3 py-2 text-sm"> <div className="flex w-full flex-col items-start justify-start gap-2 border-t border-custom-sidebar-border-200 px-3 py-2 text-sm">
{userLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map((link, index) => ( {userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Menu.Item <Menu.Item
key={index} key={index}
as="div" as="div"
@ -224,10 +225,16 @@ export const WorkspaceSidebarDropdown = observer(() => {
</Transition> </Transition>
</Menu> </Menu>
{!themeStore.sidebarCollapsed && ( {!sidebarCollapsed && (
<Menu as="div" className="relative flex-shrink-0"> <Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none"> <Menu.Button className="grid place-items-center outline-none">
<Avatar name={user?.display_name} src={user?.avatar} size={30} shape="square" className="!text-base" /> <Avatar
name={currentUser?.display_name}
src={currentUser?.avatar}
size={30}
shape="square"
className="!text-base"
/>
</Menu.Button> </Menu.Button>
<Transition <Transition
@ -244,8 +251,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none" border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none"
> >
<div className="flex flex-col gap-2.5 pb-2"> <div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{user?.email}</span> <span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
{profileLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map((link, index) => ( {profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Menu.Item key={index} as="button" type="button"> <Menu.Item key={index} as="button" type="button">
<Link href={link.link}> <Link href={link.link}>
<a className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"> <a className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">

View File

@ -31,7 +31,11 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { workspace: workspaceStore, project: projectStore } = useMobxStore(); const {
workspace: workspaceStore,
project: projectStore,
workspaceMember: { workspaceMembers },
} = useMobxStore();
const { const {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -143,7 +147,7 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
}} }}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet} layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
labels={workspaceStore.workspaceLabels ?? undefined} labels={workspaceStore.workspaceLabels ?? undefined}
members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined} members={workspaceMembers?.map((m) => m.member) ?? undefined}
projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined}
/> />
</FiltersDropdown> </FiltersDropdown>
@ -157,7 +161,7 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
handleClearAllFilters={clearAllFilters} handleClearAllFilters={clearAllFilters}
handleRemoveFilter={() => {}} handleRemoveFilter={() => {}}
labels={workspaceStore.workspaceLabels ?? undefined} labels={workspaceStore.workspaceLabels ?? undefined}
members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined} members={workspaceMembers?.map((m) => m.member) ?? undefined}
states={undefined} states={undefined}
/> />
</div> </div>

View File

@ -11,8 +11,9 @@ declare global {
} }
const Crisp = observer(() => { const Crisp = observer(() => {
const { user: userStore } = useMobxStore(); const {
const { currentUser } = userStore; user: { currentUser },
} = useMobxStore();
const validateCurrentUser = useCallback(() => { const validateCurrentUser = useCallback(() => {
if (currentUser) return currentUser.email; if (currentUser) return currentUser.email;

View File

@ -13,19 +13,22 @@ export interface IUserAuthWrapper {
export const UserAuthWrapper: FC<IUserAuthWrapper> = (props) => { export const UserAuthWrapper: FC<IUserAuthWrapper> = (props) => {
const { children } = props; const { children } = props;
// store // store
const { user: userStore, workspace: workspaceStore } = useMobxStore(); const {
user: { fetchCurrentUser, fetchCurrentUserSettings },
workspace: { fetchWorkspaces },
} = useMobxStore();
// router // router
const router = useRouter(); const router = useRouter();
// fetching user information // fetching user information
const { data: currentUser, error } = useSWR("CURRENT_USER_DETAILS", () => userStore.fetchCurrentUser(), { const { data: currentUser, error } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
shouldRetryOnError: false, shouldRetryOnError: false,
}); });
// fetching user settings // fetching user settings
useSWR("CURRENT_USER_SETTINGS", () => userStore.fetchCurrentUserSettings(), { useSWR("CURRENT_USER_SETTINGS", () => fetchCurrentUserSettings(), {
shouldRetryOnError: false, shouldRetryOnError: false,
}); });
// fetching all workspaces // fetching all workspaces
useSWR(`USER_WORKSPACES_LIST`, () => workspaceStore.fetchWorkspaces(), { useSWR(`USER_WORKSPACES_LIST`, () => fetchWorkspaces(), {
shouldRetryOnError: false, shouldRetryOnError: false,
}); });

View File

@ -15,30 +15,35 @@ export interface IWorkspaceAuthWrapper {
export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props) => { export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props) => {
const { children } = props; const { children } = props;
// store // store
const { user: userStore, project: projectStore, workspace: workspaceStore } = useMobxStore(); const {
const { currentWorkspaceMemberInfo, hasPermissionToCurrentWorkspace } = userStore; user: { currentWorkspaceMemberInfo, hasPermissionToCurrentWorkspace, fetchUserWorkspaceInfo },
project: { fetchProjects },
workspace: { fetchWorkspaceLabels },
workspaceMember: { fetchWorkspaceMembers },
} = useMobxStore();
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// fetching user workspace information // fetching user workspace information
useSWR( useSWR(
workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null, workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null,
workspaceSlug ? () => userStore.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null workspaceSlug ? () => fetchUserWorkspaceInfo(workspaceSlug.toString()) : null
); );
// fetching workspace projects // fetching workspace projects
useSWR( useSWR(
workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null, workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null,
workspaceSlug ? () => projectStore.fetchProjects(workspaceSlug.toString()) : null workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null
); );
// fetch workspace members // fetch workspace members
useSWR( useSWR(
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
workspaceSlug ? () => workspaceStore.fetchWorkspaceMembers(workspaceSlug.toString()) : null workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null
); );
// fetch workspace labels // fetch workspace labels
useSWR( useSWR(
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null, workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
workspaceSlug ? () => workspaceStore.fetchWorkspaceLabels(workspaceSlug.toString()) : null workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null
); );
// while data is being loaded // while data is being loaded

View File

@ -28,38 +28,36 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { project: projectStore, user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); const {
project: { workspaceProjects },
const user = userStore.currentUser; user: { currentUser },
const projects = workspaceSlug ? projectStore.projects[workspaceSlug?.toString()] : null; commandPalette: { toggleCreateProjectModal },
} = useMobxStore();
const trackAnalyticsEvent = (tab: string) => { const trackAnalyticsEvent = (tab: string) => {
if (!user) return; if (!currentUser) return;
const eventPayload = { const eventPayload = {
workspaceSlug: workspaceSlug?.toString(), workspaceSlug: workspaceSlug?.toString(),
}; };
const eventType = const eventType =
tab === "scope_and_demand" ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" : "WORKSPACE_CUSTOM_ANALYTICS"; tab === "scope_and_demand" ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" : "WORKSPACE_CUSTOM_ANALYTICS";
trackEventService.trackAnalyticsEvent(eventPayload, eventType, currentUser);
trackEventService.trackAnalyticsEvent(eventPayload, eventType, user);
}; };
useEffect(() => { useEffect(() => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
if (user && workspaceSlug) if (currentUser && workspaceSlug)
trackEventService.trackAnalyticsEvent( trackEventService.trackAnalyticsEvent(
{ workspaceSlug: workspaceSlug?.toString() }, { workspaceSlug: workspaceSlug?.toString() },
"WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS", "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS",
user currentUser
); );
}, [user, workspaceSlug]); }, [currentUser, workspaceSlug]);
return ( return (
<> <>
{projects && projects.length > 0 ? ( {workspaceProjects && workspaceProjects.length > 0 ? (
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100"> <div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment}> <Tab.Group as={Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 px-5 py-3"> <Tab.List as="div" className="space-x-2 border-b border-custom-border-200 px-5 py-3">
@ -96,7 +94,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
primaryButton={{ primaryButton={{
icon: <Plus className="h-4 w-4" />, icon: <Plus className="h-4 w-4" />,
text: "New Project", text: "New Project",
onClick: () => commandPaletteStore.toggleCreateProjectModal(true), onClick: () => toggleCreateProjectModal(true),
}} }}
/> />
</> </>

View File

@ -18,11 +18,13 @@ import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes";
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
const ProfilePreferencesPage: NextPageWithLayout = observer(() => { const ProfilePreferencesPage: NextPageWithLayout = observer(() => {
const { user: userStore } = useMobxStore(); const {
user: { currentUser, updateCurrentUserTheme },
} = useMobxStore();
// states // states
const [currentTheme, setCurrentTheme] = useState<I_THEME_OPTION | null>(null); const [currentTheme, setCurrentTheme] = useState<I_THEME_OPTION | null>(null);
// computed // computed
const userTheme = userStore.currentUser?.theme; const userTheme = currentUser?.theme;
// hooks // hooks
const { setTheme } = useTheme(); const { setTheme } = useTheme();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -38,7 +40,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => {
const handleThemeChange = (themeOption: I_THEME_OPTION) => { const handleThemeChange = (themeOption: I_THEME_OPTION) => {
setTheme(themeOption.value); setTheme(themeOption.value);
userStore.updateCurrentUserTheme(themeOption.value).catch(() => { updateCurrentUserTheme(themeOption.value).catch(() => {
setToastAlert({ setToastAlert({
title: "Failed to Update the theme", title: "Failed to Update the theme",
type: "error", type: "error",
@ -48,7 +50,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
{userStore.currentUser ? ( {currentUser ? (
<div className="pr-9 py-8 w-full overflow-y-auto"> <div className="pr-9 py-8 w-full overflow-y-auto">
<div className="flex items-center py-3.5 border-b border-custom-border-100"> <div className="flex items-center py-3.5 border-b border-custom-border-100">
<h3 className="text-xl font-medium">Preferences</h3> <h3 className="text-xl font-medium">Preferences</h3>

View File

@ -1,7 +1,9 @@
import { useState, ReactElement } from "react"; import { useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks // hooks
import useUser from "hooks/use-user"; import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout";
@ -14,15 +16,41 @@ import { Button } from "@plane/ui";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
// types // types
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
import { IWorkspaceBulkInviteFormData } from "types";
const WorkspaceMembersSettingsPage: NextPageWithLayout = () => { const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store
const {
workspaceMember: { inviteMembersToWorkspace },
} = useMobxStore();
// states // states
const [inviteModal, setInviteModal] = useState(false); const [inviteModal, setInviteModal] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
// hooks // hooks
const { user } = useUser(); const { setToastAlert } = useToast();
const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => {
if (!workspaceSlug) return;
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
.then(async () => {
setInviteModal(false);
setToastAlert({
type: "success",
title: "Success!",
message: "Invitations sent successfully.",
});
})
.catch((err) =>
setToastAlert({
type: "error",
title: "Error!",
message: `${err.error ?? "Something went wrong. Please try again."}`,
})
);
};
return ( return (
<> <>
@ -30,8 +58,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = () => {
<SendWorkspaceInvitationModal <SendWorkspaceInvitationModal
isOpen={inviteModal} isOpen={inviteModal}
onClose={() => setInviteModal(false)} onClose={() => setInviteModal(false)}
workspaceSlug={workspaceSlug.toString()} onSubmit={handleWorkspaceInvite}
user={user}
/> />
)} )}
<section className="pr-9 py-8 w-full overflow-y-auto"> <section className="pr-9 py-8 w-full overflow-y-auto">
@ -55,7 +82,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = () => {
</section> </section>
</> </>
); );
}; });
WorkspaceMembersSettingsPage.getLayout = function getLayout(page: ReactElement) { WorkspaceMembersSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -18,23 +18,23 @@ import { IWorkspace } from "types";
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
const CreateWorkspacePage: NextPageWithLayout = observer(() => { const CreateWorkspacePage: NextPageWithLayout = observer(() => {
// router
const router = useRouter();
// store
const {
user: { currentUser, updateCurrentUser },
} = useMobxStore();
// states
const [defaultValues, setDefaultValues] = useState({ const [defaultValues, setDefaultValues] = useState({
name: "", name: "",
slug: "", slug: "",
organization_size: "", organization_size: "",
}); });
// hooks
const router = useRouter();
const { user: userStore } = useMobxStore();
const user = userStore.currentUser;
const { theme } = useTheme(); const { theme } = useTheme();
const onSubmit = async (workspace: IWorkspace) => { const onSubmit = async (workspace: IWorkspace) => {
await userStore await updateCurrentUser({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`));
.updateCurrentUser({ last_workspace_id: workspace.id })
.then(() => router.push(`/${workspace.slug}`));
}; };
return ( return (
@ -54,7 +54,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
</div> </div>
</button> </button>
<div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5"> <div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email} {currentUser?.email}
</div> </div>
</div> </div>
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5"> <div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">

View File

@ -30,9 +30,12 @@ const workspaceService = new WorkspaceService();
const OnboardingPage: NextPageWithLayout = observer(() => { const OnboardingPage: NextPageWithLayout = observer(() => {
const [step, setStep] = useState<number | null>(null); const [step, setStep] = useState<number | null>(null);
const { user: userStore, workspace: workspaceStore } = useMobxStore(); const {
user: { currentUser, updateCurrentUser, updateUserOnBoard },
workspace: workspaceStore,
} = useMobxStore();
const user = userStore.currentUser ?? undefined; const user = currentUser ?? undefined;
const workspaces = workspaceStore.workspaces; const workspaces = workspaceStore.workspaces;
const userWorkspaces = workspaceStore.workspacesCreateByCurrentUser; const userWorkspaces = workspaceStore.workspacesCreateByCurrentUser;
@ -48,7 +51,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
const updateLastWorkspace = async () => { const updateLastWorkspace = async () => {
if (!workspaces) return; if (!workspaces) return;
await userStore.updateCurrentUser({ await updateCurrentUser({
last_workspace_id: workspaces[0]?.id, last_workspace_id: workspaces[0]?.id,
}); });
}; };
@ -64,14 +67,14 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
}, },
}; };
await userStore.updateCurrentUser(payload); await updateCurrentUser(payload);
}; };
// complete onboarding // complete onboarding
const finishOnboarding = async () => { const finishOnboarding = async () => {
if (!user) return; if (!user) return;
await userStore.updateUserOnBoard(); await updateUserOnBoard();
}; };
useEffect(() => { useEffect(() => {

View File

@ -28,7 +28,7 @@ export interface IProjectStore {
// computed // computed
searchedProjects: IProject[]; searchedProjects: IProject[];
workspaceProjects: IProject[]; workspaceProjects: IProject[] | null;
projectLabels: IIssueLabels[] | null; projectLabels: IIssueLabels[] | null;
projectMembers: IProjectMember[] | null; projectMembers: IProjectMember[] | null;
projectEstimates: IEstimate[] | null; projectEstimates: IEstimate[] | null;
@ -183,8 +183,10 @@ export class ProjectStore implements IProjectStore {
} }
get workspaceProjects() { get workspaceProjects() {
if (!this.rootStore.workspace.workspaceSlug) return []; if (!this.rootStore.workspace.workspaceSlug) return null;
return this.projects?.[this.rootStore.workspace.workspaceSlug]; const projects = this.projects[this.rootStore.workspace.workspaceSlug];
if (!projects) return null;
return projects;
} }
get currentProjectDetails() { get currentProjectDetails() {

View File

@ -19,7 +19,14 @@ import {
IIssueQuickAddStore, IIssueQuickAddStore,
IssueQuickAddStore, IssueQuickAddStore,
} from "store/issue"; } from "store/issue";
import { IWorkspaceFilterStore, IWorkspaceStore, WorkspaceFilterStore, WorkspaceStore } from "store/workspace"; import {
IWorkspaceFilterStore,
IWorkspaceStore,
WorkspaceFilterStore,
WorkspaceStore,
WorkspaceMemberStore,
IWorkspaceMemberStore,
} from "store/workspace";
import { import {
IProjectPublishStore, IProjectPublishStore,
IProjectStore, IProjectStore,
@ -113,6 +120,7 @@ export class RootStore {
commandPalette: ICommandPaletteStore; commandPalette: ICommandPaletteStore;
workspace: IWorkspaceStore; workspace: IWorkspaceStore;
workspaceFilter: IWorkspaceFilterStore; workspaceFilter: IWorkspaceFilterStore;
workspaceMember: IWorkspaceMemberStore;
projectPublish: IProjectPublishStore; projectPublish: IProjectPublishStore;
project: IProjectStore; project: IProjectStore;
@ -176,6 +184,7 @@ export class RootStore {
this.workspace = new WorkspaceStore(this); this.workspace = new WorkspaceStore(this);
this.workspaceFilter = new WorkspaceFilterStore(this); this.workspaceFilter = new WorkspaceFilterStore(this);
this.workspaceMember = new WorkspaceMemberStore(this);
this.project = new ProjectStore(this); this.project = new ProjectStore(this);
this.projectState = new ProjectStateStore(this); this.projectState = new ProjectStateStore(this);

View File

@ -1,2 +1,3 @@
export * from "./workspace_filters.store"; export * from "./workspace_filters.store";
export * from "./workspace.store"; export * from "./workspace.store";
export * from "./workspace-member.store";

View File

@ -0,0 +1,274 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { RootStore } from "../root";
// types
import { IUser, IWorkspaceMember, IWorkspaceMemberInvitation, IWorkspaceBulkInviteFormData } from "types";
// services
import { WorkspaceService } from "services/workspace.service";
export interface IWorkspaceMemberStore {
// states
loader: boolean;
error: any | null;
// observables
members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[]
memberInvitations: { [workspaceSlug: string]: IWorkspaceMemberInvitation[] };
// actions
fetchWorkspaceMembers: (workspaceSlug: string) => Promise<void>;
fetchWorkspaceMemberInvitations: (workspaceSlug: string) => Promise<IWorkspaceMemberInvitation[]>;
updateMember: (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => Promise<void>;
removeMember: (workspaceSlug: string, memberId: string) => Promise<void>;
inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise<any>;
deleteWorkspaceInvitation: (workspaceSlug: string, memberId: string) => Promise<void>;
// computed
workspaceMembers: IWorkspaceMember[] | null;
workspaceMemberInvitations: IWorkspaceMemberInvitation[] | null;
workspaceMembersWithInvitations: any[] | null;
}
export class WorkspaceMemberStore implements IWorkspaceMemberStore {
// states
loader: boolean = false;
error: any | null = null;
// observables
members: { [workspaceSlug: string]: IWorkspaceMember[] } = {};
memberInvitations: { [workspaceSlug: string]: IWorkspaceMemberInvitation[] } = {};
// services
workspaceService;
// root store
rootStore;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// states
loader: observable.ref,
error: observable.ref,
// observables
members: observable.ref,
memberInvitations: observable.ref,
// actions
fetchWorkspaceMembers: action,
fetchWorkspaceMemberInvitations: action,
updateMember: action,
removeMember: action,
inviteMembersToWorkspace: action,
deleteWorkspaceInvitation: action,
// computed
workspaceMembers: computed,
workspaceMemberInvitations: computed,
workspaceMembersWithInvitations: computed,
});
this.rootStore = _rootStore;
this.workspaceService = new WorkspaceService();
}
/**
* computed value of workspace members using the workspace slug from the store
*/
get workspaceMembers() {
if (!this.rootStore.workspace.workspaceSlug) return null;
const members = this.members?.[this.rootStore.workspace.workspaceSlug];
if (!members) return null;
return members;
}
/**
* Computed value of workspace member invitations using workspace slug from store
*/
get workspaceMemberInvitations() {
if (!this.rootStore.workspace.workspaceSlug) return null;
const invitations = this.memberInvitations?.[this.rootStore.workspace.workspaceSlug];
if (!invitations) return null;
return invitations;
}
/**
* computed value provides the members information including the invitations.
*/
get workspaceMembersWithInvitations() {
if (!this.workspaceMembers || !this.workspaceMemberInvitations) return null;
return [
...(this.workspaceMemberInvitations?.map((item) => ({
id: item.id,
memberId: item.id,
avatar: "",
first_name: item.email,
last_name: "",
email: item.email,
display_name: item.email,
role: item.role,
status: item.accepted,
member: false,
accountCreated: item.accepted,
})) || []),
...(this.workspaceMembers?.map((item) => ({
id: item.id,
memberId: item.member?.id,
avatar: item.member?.avatar,
first_name: item.member?.first_name,
last_name: item.member?.last_name,
email: item.member?.email,
display_name: item.member?.display_name,
role: item.role,
status: true,
member: true,
accountCreated: true,
})) || []),
];
}
/**
* fetch workspace members using workspace slug
* @param workspaceSlug
*/
fetchWorkspaceMembers = async (workspaceSlug: string) => {
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
const membersResponse = await this.workspaceService.fetchWorkspaceMembers(workspaceSlug);
runInAction(() => {
this.members = {
...this.members,
[workspaceSlug]: membersResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
}
};
/**
* fetching workspace member invitations
* @param workspaceSlug
* @returns
*/
fetchWorkspaceMemberInvitations = async (workspaceSlug: string) => {
try {
const membersInvitations = await this.workspaceService.workspaceInvitations(workspaceSlug);
runInAction(() => {
this.memberInvitations = {
...this.memberInvitations,
[workspaceSlug]: membersInvitations,
};
});
return membersInvitations;
} catch (error) {
throw error;
}
};
/**
* invite members to the workspace using emails
* @param workspaceSlug
* @param data
*/
inviteMembersToWorkspace = async (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => {
try {
await this.workspaceService.inviteWorkspace(workspaceSlug, data, this.rootStore.user.currentUser as IUser);
await this.fetchWorkspaceMemberInvitations(workspaceSlug);
} catch (error) {
throw error;
}
};
/**
* delete the workspace invitation
* @param workspaceSlug
* @param memberId
*/
deleteWorkspaceInvitation = async (workspaceSlug: string, memberId: string) => {
try {
runInAction(() => {
this.memberInvitations = {
...this.memberInvitations,
[workspaceSlug]: [...this.memberInvitations[workspaceSlug].filter((inv) => inv.id !== memberId)],
};
});
await this.workspaceService.deleteWorkspaceInvitations(workspaceSlug.toString(), memberId);
} catch (error) {
throw error;
}
};
/**
* update workspace member using workspace slug and member id and data
* @param workspaceSlug
* @param memberId
* @param data
*/
updateMember = async (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => {
const members = this.members?.[workspaceSlug];
members?.map((m) => (m.id === memberId ? { ...m, ...data } : m));
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data);
runInAction(() => {
this.loader = false;
this.error = null;
this.members = {
...this.members,
[workspaceSlug]: members,
};
});
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
/**
* remove workspace member using workspace slug and member id
* @param workspaceSlug
* @param memberId
*/
removeMember = async (workspaceSlug: string, memberId: string) => {
const members = this.members?.[workspaceSlug];
members?.filter((m) => m.id !== memberId);
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId);
runInAction(() => {
this.loader = false;
this.error = null;
this.members = {
...this.members,
[workspaceSlug]: members,
};
});
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
}

View File

@ -16,7 +16,6 @@ export interface IWorkspaceStore {
workspaceSlug: string | null; workspaceSlug: string | null;
workspaces: IWorkspace[] | undefined; workspaces: IWorkspace[] | undefined;
labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[] labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[]
members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[]
// actions // actions
setWorkspaceSlug: (workspaceSlug: string) => void; setWorkspaceSlug: (workspaceSlug: string) => void;
@ -24,22 +23,16 @@ export interface IWorkspaceStore {
getWorkspaceLabelById: (workspaceSlug: string, labelId: string) => IIssueLabels | null; getWorkspaceLabelById: (workspaceSlug: string, labelId: string) => IIssueLabels | null;
fetchWorkspaces: () => Promise<IWorkspace[]>; fetchWorkspaces: () => Promise<IWorkspace[]>;
fetchWorkspaceLabels: (workspaceSlug: string) => Promise<void>; fetchWorkspaceLabels: (workspaceSlug: string) => Promise<void>;
fetchWorkspaceMembers: (workspaceSlug: string) => Promise<void>;
// workspace write operations // workspace write operations
createWorkspace: (data: Partial<IWorkspace>) => Promise<IWorkspace>; createWorkspace: (data: Partial<IWorkspace>) => Promise<IWorkspace>;
updateWorkspace: (workspaceSlug: string, data: Partial<IWorkspace>) => Promise<IWorkspace>; updateWorkspace: (workspaceSlug: string, data: Partial<IWorkspace>) => Promise<IWorkspace>;
deleteWorkspace: (workspaceSlug: string) => Promise<void>; deleteWorkspace: (workspaceSlug: string) => Promise<void>;
// members write operations
updateMember: (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => Promise<void>;
removeMember: (workspaceSlug: string, memberId: string) => Promise<void>;
// computed // computed
currentWorkspace: IWorkspace | null; currentWorkspace: IWorkspace | null;
workspacesCreateByCurrentUser: IWorkspace[] | null; workspacesCreateByCurrentUser: IWorkspace[] | null;
workspaceLabels: IIssueLabels[] | null; workspaceLabels: IIssueLabels[] | null;
workspaceMembers: IWorkspaceMember[] | null;
} }
export class WorkspaceStore implements IWorkspaceStore { export class WorkspaceStore implements IWorkspaceStore {
@ -72,7 +65,6 @@ export class WorkspaceStore implements IWorkspaceStore {
workspaceSlug: observable.ref, workspaceSlug: observable.ref,
workspaces: observable.ref, workspaces: observable.ref,
labels: observable.ref, labels: observable.ref,
members: observable.ref,
// actions // actions
setWorkspaceSlug: action, setWorkspaceSlug: action,
@ -80,21 +72,15 @@ export class WorkspaceStore implements IWorkspaceStore {
getWorkspaceLabelById: action, getWorkspaceLabelById: action,
fetchWorkspaces: action, fetchWorkspaces: action,
fetchWorkspaceLabels: action, fetchWorkspaceLabels: action,
fetchWorkspaceMembers: action,
// workspace write operations // workspace write operations
createWorkspace: action, createWorkspace: action,
updateWorkspace: action, updateWorkspace: action,
deleteWorkspace: action, deleteWorkspace: action,
// members write operations
updateMember: action,
removeMember: action,
// computed // computed
currentWorkspace: computed, currentWorkspace: computed,
workspaceLabels: computed, workspaceLabels: computed,
workspaceMembers: computed,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
@ -135,15 +121,6 @@ export class WorkspaceStore implements IWorkspaceStore {
return _labels && Object.keys(_labels).length > 0 ? _labels : []; return _labels && Object.keys(_labels).length > 0 ? _labels : [];
} }
/**
* computed value of workspace members using the workspace slug from the store
*/
get workspaceMembers() {
if (!this.workspaceSlug) return [];
const _members = this.members?.[this.workspaceSlug];
return _members && Object.keys(_members).length > 0 ? _members : [];
}
/** /**
* set workspace slug in the store * set workspace slug in the store
* @param workspaceSlug * @param workspaceSlug
@ -224,35 +201,6 @@ export class WorkspaceStore implements IWorkspaceStore {
} }
}; };
/**
* fetch workspace members using workspace slug
* @param workspaceSlug
*/
fetchWorkspaceMembers = async (workspaceSlug: string) => {
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
const membersResponse = await this.workspaceService.fetchWorkspaceMembers(workspaceSlug);
runInAction(() => {
this.members = {
...this.members,
[workspaceSlug]: membersResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
}
};
/** /**
* create workspace using the workspace data * create workspace using the workspace data
* @param data * @param data
@ -351,75 +299,4 @@ export class WorkspaceStore implements IWorkspaceStore {
throw error; throw error;
} }
}; };
/**
* update workspace member using workspace slug and member id and data
* @param workspaceSlug
* @param memberId
* @param data
*/
updateMember = async (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => {
const members = this.members?.[workspaceSlug];
members?.map((m) => (m.id === memberId ? { ...m, ...data } : m));
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data);
runInAction(() => {
this.loader = false;
this.error = null;
this.members = {
...this.members,
[workspaceSlug]: members,
};
});
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
/**
* remove workspace member using workspace slug and member id
* @param workspaceSlug
* @param memberId
*/
removeMember = async (workspaceSlug: string, memberId: string) => {
const members = this.members?.[workspaceSlug];
members?.filter((m) => m.id !== memberId);
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId);
runInAction(() => {
this.loader = false;
this.error = null;
this.members = {
...this.members,
[workspaceSlug]: members,
};
});
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
} }