diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index f0ad4b4ab..d6f0894a5 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -2,7 +2,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer from plane.db.models import ( @@ -16,7 +16,7 @@ from plane.db.models import ( ) -class WorkSpaceSerializer(BaseSerializer): +class WorkSpaceSerializer(DynamicBaseSerializer): owner = UserLiteSerializer(read_only=True) total_members = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True) diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 840266dc4..a690124db 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -42,6 +42,7 @@ from plane.app.permissions import ( WorkspaceUserPermission, ProjectBasePermission, ProjectMemberPermission, + ProjectLitePermission, ) from plane.db.models import ( @@ -600,6 +601,18 @@ class ProjectMemberViewSet(BaseViewSet): ProjectMemberPermission, ] + def get_permissions(self): + if self.action == "leave": + self.permission_classes = [ + ProjectLitePermission, + ] + else: + self.permission_classes = [ + ProjectMemberPermission, + ] + + return super(ProjectMemberViewSet, self).get_permissions() + search_fields = [ "member__display_name", "member__first_name", diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index f522f1781..90258259d 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -214,7 +214,7 @@ class UserWorkSpacesEndpoint(BaseAPIView): fields=fields if fields else None, many=True, ).data - workspace_dict = {str(workspaces["id"]): workspaces for workspace in workspaces} + workspace_dict = {str(workspace["id"]): workspace for workspace in workspaces} return Response(workspace_dict, status=status.HTTP_200_OK) diff --git a/space/components/accounts/sign-in-forms/self-hosted-sign-in.tsx b/space/components/accounts/sign-in-forms/self-hosted-sign-in.tsx index e7a7d8115..486573ffa 100644 --- a/space/components/accounts/sign-in-forms/self-hosted-sign-in.tsx +++ b/space/components/accounts/sign-in-forms/self-hosted-sign-in.tsx @@ -130,7 +130,7 @@ export const SelfHostedSignInForm: React.FC = (props) => { />

When you click the button above, you agree with our{" "} diff --git a/web/components/account/sign-in-forms/self-hosted-sign-in.tsx b/web/components/account/sign-in-forms/self-hosted-sign-in.tsx index 30e45bba6..27b5b4789 100644 --- a/web/components/account/sign-in-forms/self-hosted-sign-in.tsx +++ b/web/components/account/sign-in-forms/self-hosted-sign-in.tsx @@ -130,7 +130,7 @@ export const SelfHostedSignInForm: React.FC = (props) => { />

When you click the button above, you agree with our{" "} diff --git a/web/components/gantt-chart/sidebar/sidebar.tsx b/web/components/gantt-chart/sidebar/sidebar.tsx index d163f8a89..23f8f8d76 100644 --- a/web/components/gantt-chart/sidebar/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/sidebar.tsx @@ -27,11 +27,21 @@ type Props = { viewId?: string ) => Promise; viewId?: string; + disableIssueCreation?: boolean; }; export const IssueGanttSidebar: React.FC = (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, enableReorder, enableQuickIssueCreate, quickAddCallback, viewId } = props; + const { + title, + blockUpdateHandler, + blocks, + enableReorder, + enableQuickIssueCreate, + quickAddCallback, + viewId, + disableIssueCreation, + } = props; const router = useRouter(); const { cycleId } = router.query; @@ -160,7 +170,7 @@ export const IssueGanttSidebar: React.FC = (props) => { )} {droppableProvided.placeholder} - {enableQuickIssueCreate && ( + {enableQuickIssueCreate && !disableIssueCreation && ( )} diff --git a/web/components/instance/setup-done-view.tsx b/web/components/instance/setup-done-view.tsx index 693e5855e..6c1c53f7c 100644 --- a/web/components/instance/setup-done-view.tsx +++ b/web/components/instance/setup-done-view.tsx @@ -5,7 +5,7 @@ import { useTheme } from "next-themes"; import { Button } from "@plane/ui"; import { UserCog2 } from "lucide-react"; // images -import instanceSetupDone from "public/instance-setup-done.svg"; +import instanceSetupDone from "public/instance-setup-done.webp"; import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; import { useMobxStore } from "lib/mobx/store-provider"; diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index ed3797383..9d8247d55 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -16,6 +16,8 @@ import { IProjectIssuesFilterStore, IViewIssuesFilterStore, } from "store_legacy/issues"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; type Props = { issuesFilterStore: @@ -41,7 +43,14 @@ export const CalendarChart: React.FC = observer((props) => { const { issuesFilterStore, issues, groupedIssueIds, layout, showWeekends, quickActions, quickAddCallback, viewId } = props; - const { calendar: calendarStore } = useMobxStore(); + const { + calendar: calendarStore, + projectIssues: issueStore, + user: { currentProjectRole }, + } = useMobxStore(); + + const { enableIssueCreation } = issueStore?.viewFlags || {}; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const calendarPayload = calendarStore.calendarPayload; @@ -71,6 +80,7 @@ export const CalendarChart: React.FC = observer((props) => { issues={issues} groupedIssueIds={groupedIssueIds} enableQuickIssueCreate + disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} viewId={viewId} @@ -85,6 +95,7 @@ export const CalendarChart: React.FC = observer((props) => { issues={issues} groupedIssueIds={groupedIssueIds} enableQuickIssueCreate + disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} viewId={viewId} diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 7f6a94944..ef06e34fa 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -26,6 +26,7 @@ type Props = { groupedIssueIds: IGroupedIssues; quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; + disableIssueCreation?: boolean; quickAddCallback?: ( workspaceSlug: string, projectId: string, @@ -43,6 +44,7 @@ export const CalendarDayTile: React.FC = observer((props) => { groupedIssueIds, quickActions, enableQuickIssueCreate, + disableIssueCreation, quickAddCallback, viewId, } = props; @@ -86,7 +88,7 @@ export const CalendarDayTile: React.FC = observer((props) => { ref={provided.innerRef} > - {enableQuickIssueCreate && ( + {enableQuickIssueCreate && !disableIssueCreation && (

React.ReactNode; enableQuickIssueCreate?: boolean; + disableIssueCreation?: boolean; quickAddCallback?: ( workspaceSlug: string, projectId: string, @@ -42,6 +43,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { week, quickActions, enableQuickIssueCreate, + disableIssueCreation, quickAddCallback, viewId, } = props; @@ -69,6 +71,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { groupedIssueIds={groupedIssueIds} quickActions={quickActions} enableQuickIssueCreate={enableQuickIssueCreate} + disableIssueCreation={disableIssueCreation} quickAddCallback={quickAddCallback} viewId={viewId} /> diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 7ccfd006b..d6a71a923 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -51,6 +51,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan const issuesResponse = issueStore.getIssues; const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues; + const { enableIssueCreation } = issueStore?.viewFlags || {}; const issues = issueIds.map((id) => issuesResponse?.[id]); @@ -87,6 +88,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan quickAddCallback={issueStore.quickAddIssue} viewId={viewId} enableQuickIssueCreate + disableIssueCreation={!enableIssueCreation || !isAllowed} /> )} enableBlockLeftResize={isAllowed} diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index eaa3e9341..e3f032bb1 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -330,7 +330,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas projects={workspaceProjects} showEmptyGroup={userDisplayFilters?.show_empty_groups || true} isDragStarted={isDragStarted} - disableIssueCreation={true} + disableIssueCreation={!enableIssueCreation || !isEditingAllowed} enableQuickIssueCreate={enableQuickAdd} currentStore={currentStore} quickAddCallback={issueStore?.quickAddIssue} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index ee9f2c331..30b57b84f 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -149,7 +149,7 @@ const GroupByKanBan: React.FC = observer((props) => {
- {enableQuickIssueCreate && ( + {enableQuickIssueCreate && !disableIssueCreation && ( = (props) => { /> )} - {enableIssueQuickAdd && ( + {enableIssueQuickAdd && !disableIssueCreation && (
{ user: userStore, } = useMobxStore(); - const { enableInlineEditing, enableQuickAdd } = issueStore?.viewFlags || {}; + const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {}; const { currentProjectRole } = userStore; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; @@ -120,6 +120,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { quickAddCallback={issueStore.quickAddIssue} viewId={viewId} enableQuickCreateIssue={enableQuickAdd} + disableIssueCreation={!enableIssueCreation || !isEditingAllowed} /> ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index 69ac625e8..370bbfa0f 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -33,6 +33,7 @@ type Props = { viewId?: string; canEditProperties: (projectId: string | undefined) => boolean; enableQuickCreateIssue?: boolean; + disableIssueCreation?: boolean; }; export const SpreadsheetView: React.FC = observer((props) => { @@ -50,6 +51,7 @@ export const SpreadsheetView: React.FC = observer((props) => { viewId, canEditProperties, enableQuickCreateIssue, + disableIssueCreation, } = props; // states const [expandedIssues, setExpandedIssues] = useState([]); @@ -147,7 +149,7 @@ export const SpreadsheetView: React.FC = observer((props) => {
- {enableQuickCreateIssue && ( + {enableQuickCreateIssue && !disableIssueCreation && ( )}
diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 2d987fd53..aa70b4703 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -168,7 +168,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { ); })} - {isAdmin && ( + {(isAdmin || memberDetails.id === currentProjectMemberInfo?.member.id) && ( (appRootStore); + +const initializeStore = () => { + const _appRootStore: IAppRootStore = appRootStore ?? new AppRootStore(); + if (typeof window === "undefined") return _appRootStore; + if (!appRootStore) appRootStore = _appRootStore; + return _appRootStore; +}; + +export const AppRootStoreProvider = ({ children }: any) => { + const store: IAppRootStore = initializeStore(); + return {children}; +}; diff --git a/web/contexts/app-root/index.ts b/web/contexts/app-root/index.ts new file mode 100644 index 000000000..416a8ec63 --- /dev/null +++ b/web/contexts/app-root/index.ts @@ -0,0 +1,2 @@ +export * from "./app-root-provider"; +export * from "./use-app-root"; diff --git a/web/contexts/app-root/use-app-root.tsx b/web/contexts/app-root/use-app-root.tsx new file mode 100644 index 000000000..363192716 --- /dev/null +++ b/web/contexts/app-root/use-app-root.tsx @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { AppRootStoreContext } from "./app-root-provider"; + +export const useAppRoot = () => { + const context = useContext(AppRootStoreContext); + if (context === undefined) throw new Error("useAppRoot must be used within AppRootStoreContext"); + return context; +}; diff --git a/web/contexts/page.context/index.ts b/web/contexts/page.context/index.ts new file mode 100644 index 000000000..78f8c7a77 --- /dev/null +++ b/web/contexts/page.context/index.ts @@ -0,0 +1,2 @@ +export * from "./page-provider"; +export * from "./use-page"; diff --git a/web/contexts/page.context/page-provider.tsx b/web/contexts/page.context/page-provider.tsx new file mode 100644 index 000000000..5026edabc --- /dev/null +++ b/web/contexts/page.context/page-provider.tsx @@ -0,0 +1,20 @@ +import { createContext } from "react"; +// mobx store +import { PageStore } from "store/page.store"; +import { AppRootStore } from "store/application"; + +let pageStore: PageStore = new PageStore(new AppRootStore()); + +export const PageContext = createContext(pageStore); + +const initializeStore = () => { + const _pageStore: PageStore = pageStore ?? new PageStore(pageStore); + if (typeof window === "undefined") return _pageStore; + if (!pageStore) pageStore = _pageStore; + return _pageStore; +}; + +export const AppRootStoreProvider = ({ children }: any) => { + const store: PageStore = initializeStore(); + return {children}; +}; diff --git a/web/contexts/page.context/use-page.tsx b/web/contexts/page.context/use-page.tsx new file mode 100644 index 000000000..85e3d1500 --- /dev/null +++ b/web/contexts/page.context/use-page.tsx @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { PageContext } from "./page-provider"; + +export const usePage = () => { + const context = useContext(PageContext); + if (context === undefined) throw new Error("useAppRoot must be used within AppRootStoreContext"); + return context; +}; diff --git a/web/hooks/use-page.tsx b/web/hooks/use-page.tsx new file mode 100644 index 000000000..3852cc79b --- /dev/null +++ b/web/hooks/use-page.tsx @@ -0,0 +1,8 @@ +import { useMobxStore } from "lib/mobx/store-provider"; + +const usePage = () => { + const { page } = useMobxStore(); + return { ...page }; +}; + +export default usePage; diff --git a/web/lib/mobx/store-provider.tsx b/web/lib/mobx/store-provider.tsx index 7e9a57c51..1fd33b256 100644 --- a/web/lib/mobx/store-provider.tsx +++ b/web/lib/mobx/store-provider.tsx @@ -1,5 +1,3 @@ -"use client"; - import { createContext, useContext } from "react"; // mobx store import { RootStore } from "store_legacy/root"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index 69d6db1d3..4b12e6d5f 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -82,7 +82,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { description_html: newDescription, }) .then(() => { - mutatePageDetails((prevData) => ({ ...prevData, description_html: newDescription }) as IPage, false); + mutatePageDetails((prevData) => ({ ...prevData, description_html: newDescription } as IPage), false); }); }; @@ -162,15 +162,12 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { }, [pageDetails?.description_html]); // TODO: Verify the exhaustive-deps warning function createObjectFromArray(keys: string[], options: any): any { - return keys.reduce( - (obj, key) => { - if (options[key] !== undefined) { - obj[key] = options[key]; - } - return obj; - }, - {} as { [key: string]: any } - ); + return keys.reduce((obj, key) => { + if (options[key] !== undefined) { + obj[key] = options[key]; + } + return obj; + }, {} as { [key: string]: any }); } const mutatePageDetailsHelper = ( diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index f6df0c4fc..d8469f129 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -158,8 +158,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : currentUser?.email + ? value + : currentUser?.email } src={currentUser?.avatar} size={35} @@ -174,8 +174,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { {currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : null} + ? value + : null}

)} diff --git a/web/public/instance-setup-done.svg b/web/public/instance-setup-done.svg deleted file mode 100644 index 3474f7f38..000000000 --- a/web/public/instance-setup-done.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/instance-setup-done.webp b/web/public/instance-setup-done.webp new file mode 100644 index 000000000..4773ed70b Binary files /dev/null and b/web/public/instance-setup-done.webp differ diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index e83a8e435..9c49f9876 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -16,8 +16,6 @@ import { IUserProjectsRole, } from "types"; import { IWorkspaceView } from "types/workspace-views"; -// store -import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "store_legacy/issue"; import { IIssueResponse } from "store_legacy/issues/types"; export class WorkspaceService extends APIService { @@ -25,7 +23,7 @@ export class WorkspaceService extends APIService { super(API_BASE_URL); } - async userWorkspaces(): Promise { + async userWorkspaces(): Promise> { return this.get("/api/users/me/workspaces/") .then((response) => response?.data) .catch((error) => { diff --git a/web/store/application/app-config.store.ts b/web/store/application/app-config.store.ts index 8e6ff558c..99d8d10ec 100644 --- a/web/store/application/app-config.store.ts +++ b/web/store/application/app-config.store.ts @@ -1,6 +1,5 @@ import { observable, action, makeObservable, runInAction } from "mobx"; // types -import { RootStore } from "../root.store"; import { IAppConfig } from "types/app"; // services import { AppConfigService } from "services/app_config.service"; @@ -14,13 +13,10 @@ export interface IAppConfigStore { export class AppConfigStore implements IAppConfigStore { // observables envConfig: IAppConfig | null = null; - - // root store - rootStore; // service appConfigService; - constructor(_rootStore: RootStore) { + constructor() { makeObservable(this, { // observables envConfig: observable.ref, @@ -28,9 +24,8 @@ export class AppConfigStore implements IAppConfigStore { fetchAppConfig: action, }); this.appConfigService = new AppConfigService(); - - this.rootStore = _rootStore; } + fetchAppConfig = async () => { try { const config = await this.appConfigService.envConfig(); diff --git a/web/store/application/command-palette.store.ts b/web/store/application/command-palette.store.ts index 4403c786a..b98a14049 100644 --- a/web/store/application/command-palette.store.ts +++ b/web/store/application/command-palette.store.ts @@ -1,6 +1,4 @@ import { observable, action, makeObservable, computed } from "mobx"; -// types -import { RootStore } from "../root.store"; // services import { ProjectService } from "services/project"; import { PageService } from "services/page.service"; @@ -58,15 +56,13 @@ export class CommandPaletteStore implements ICommandPaletteStore { isCreateIssueModalOpen: boolean = false; isDeleteIssueModalOpen: boolean = false; isBulkDeleteIssueModalOpen: boolean = false; - // root store - rootStore; // service projectService; pageService; createIssueStoreType: EProjectStore = EProjectStore.PROJECT; - constructor(_rootStore: RootStore) { + constructor() { makeObservable(this, { // observable isCommandPaletteOpen: observable.ref, @@ -95,7 +91,6 @@ export class CommandPaletteStore implements ICommandPaletteStore { toggleBulkDeleteIssueModal: action, }); - this.rootStore = _rootStore; this.projectService = new ProjectService(); this.pageService = new PageService(); } diff --git a/web/store/application/index.ts b/web/store/application/index.ts index 6e2ea3577..efc83f613 100644 --- a/web/store/application/index.ts +++ b/web/store/application/index.ts @@ -1,7 +1,6 @@ -import { RootStore } from "../root.store"; import { AppConfigStore, IAppConfigStore } from "./app-config.store"; import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store"; -import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; +// import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; import { InstanceStore, IInstanceStore } from "./instance.store"; import { RouterStore, IRouterStore } from "./router.store"; import { ThemeStore, IThemeStore } from "./theme.store"; @@ -9,7 +8,7 @@ import { ThemeStore, IThemeStore } from "./theme.store"; export interface IAppRootStore { config: IAppConfigStore; commandPalette: ICommandPaletteStore; - eventTracker: IEventTrackerStore; + // eventTracker: IEventTrackerStore; instance: IInstanceStore; theme: IThemeStore; router: IRouterStore; @@ -18,17 +17,17 @@ export interface IAppRootStore { export class AppRootStore implements IAppRootStore { config: IAppConfigStore; commandPalette: ICommandPaletteStore; - eventTracker: IEventTrackerStore; + // eventTracker: IEventTrackerStore; instance: IInstanceStore; theme: IThemeStore; router: IRouterStore; - constructor(rootStore: RootStore) { - this.config = new AppConfigStore(rootStore); - this.commandPalette = new CommandPaletteStore(rootStore); - this.eventTracker = new EventTrackerStore(rootStore); - this.instance = new InstanceStore(rootStore); - this.theme = new ThemeStore(rootStore); + constructor() { this.router = new RouterStore(); + this.config = new AppConfigStore(); + this.commandPalette = new CommandPaletteStore(); + // this.eventTracker = new EventTrackerStore(this.router); + this.instance = new InstanceStore(); + this.theme = new ThemeStore(); } } diff --git a/web/store/application/instance.store.ts b/web/store/application/instance.store.ts index 3332a11d9..009d8aa6d 100644 --- a/web/store/application/instance.store.ts +++ b/web/store/application/instance.store.ts @@ -1,6 +1,4 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; -// store -import { RootStore } from "../root.store"; // types import { IInstance, IInstanceConfiguration, IFormattedInstanceConfiguration, IInstanceAdmin } from "types/instance"; // services @@ -31,9 +29,8 @@ export class InstanceStore implements IInstanceStore { configurations: IInstanceConfiguration[] | null = null; // service instanceService; - rootStore; - constructor(_rootStore: RootStore) { + constructor() { makeObservable(this, { // observable loader: observable.ref, @@ -51,7 +48,6 @@ export class InstanceStore implements IInstanceStore { updateInstanceConfigurations: action, }); - this.rootStore = _rootStore; this.instanceService = new InstanceService(); } diff --git a/web/store/page.store.ts b/web/store/page.store.ts index b787f742b..8d61e2033 100644 --- a/web/store/page.store.ts +++ b/web/store/page.store.ts @@ -1,6 +1,7 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"; import keyBy from "lodash/keyBy"; import set from "lodash/set"; +import omit from "lodash/omit"; import isToday from "date-fns/isToday"; import isThisWeek from "date-fns/isThisWeek"; import isYesterday from "date-fns/isYesterday"; @@ -9,18 +10,35 @@ import { PageService } from "services/page.service"; // types import { IPage, IRecentPages } from "types"; // store -import { RootStore } from "./root.store"; +import { AppRootStore } from "store/application"; export interface IPageStore { pages: Record; archivedPages: Record; - + // project computed projectPages: IPage[] | undefined; favoriteProjectPages: IPage[] | undefined; privateProjectPages: IPage[] | undefined; - sharedProjectPages: IPage[] | undefined; - + publicProjectPages: IPage[] | undefined; + recentProjectPages: IRecentPages | undefined; + // archived pages computed + archivedProjectPages: IPage[] | undefined; + // fetch actions fetchProjectPages: (workspaceSlug: string, projectId: string) => Promise; + fetchArchivedProjectPages: (workspaceSlug: string, projectId: string) => Promise; + // favorites actions + addToFavorites: (workspaceSlug: string, projectId: string, pageId: string) => Promise; + removeFromFavorites: (workspaceSlug: string, projectId: string, pageId: string) => Promise; + // crud + createPage: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updatePage: (workspaceSlug: string, projectId: string, pageId: string, data: Partial) => Promise; + deletePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise; + // access control actions + makePublic: (workspaceSlug: string, projectId: string, pageId: string) => Promise; + makePrivate: (workspaceSlug: string, projectId: string, pageId: string) => Promise; + // archive actions + archivePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise; + restorePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise; } export class PageStore { @@ -31,20 +49,36 @@ export class PageStore { // stores router; - constructor(_rootStore: RootStore) { + constructor(appRootStore: AppRootStore) { makeObservable(this, { pages: observable, archivedPages: observable, - // computed + // project computed projectPages: computed, favoriteProjectPages: computed, - sharedProjectPages: computed, + publicProjectPages: computed, privateProjectPages: computed, - // actions + // archived pages in current project computed + archivedProjectPages: computed, + // fetch actions fetchProjectPages: action, + fetchArchivedProjectPages: action, + // favorites actions + addToFavorites: action, + removeFromFavorites: action, + // crud + createPage: action, + updatePage: action, + deletePage: action, + // access control actions + makePublic: action, + makePrivate: action, + // archive actions + archivePage: action, + restorePage: action, }); // stores - this.router = _rootStore.app.router; + this.router = appRootStore.router; // services this.pageService = new PageService(); } @@ -76,7 +110,7 @@ export class PageStore { /** * retrieves all shared pages which are public to everyone in the project for a projectId that is available in the url. */ - get sharedProjectPages() { + get publicProjectPages() { if (!this.projectPages) return; return this.projectPages.filter((page) => page.access === 0); } @@ -231,4 +265,96 @@ export class PageStore { throw error; } }; + + /** + * delete a page using the api and updates the local state in store + * @param workspaceSlug + * @param projectId + * @param pageId + * @returns + */ + deletePage = async (workspaceSlug: string, projectId: string, pageId: string) => { + try { + const response = await this.pageService.deletePage(workspaceSlug, projectId, pageId); + runInAction(() => { + this.archivedPages = set(this.archivedPages, [pageId], this.pages[pageId]); + delete this.pages[pageId]; + }); + return response; + } catch (error) { + throw error; + } + }; + + /** + * make a page public + * @param workspaceSlug + * @param projectId + * @param pageId + * @returns + */ + makePublic = async (workspaceSlug: string, projectId: string, pageId: string) => { + try { + runInAction(() => { + this.pages[pageId] = { ...this.pages[pageId], access: 0 }; + }); + const response = await this.pageService.patchPage(workspaceSlug, projectId, pageId, { access: 0 }); + return response; + } catch (error) { + runInAction(() => { + this.pages[pageId] = { ...this.pages[pageId], access: 1 }; + }); + throw error; + } + }; + + /** + * Make a page private + * @param workspaceSlug + * @param projectId + * @param pageId + * @returns + */ + makePrivate = async (workspaceSlug: string, projectId: string, pageId: string) => { + try { + runInAction(() => { + this.pages[pageId] = { ...this.pages[pageId], access: 1 }; + }); + const response = await this.pageService.patchPage(workspaceSlug, projectId, pageId, { access: 1 }); + return response; + } catch (error) { + runInAction(() => { + this.pages[pageId] = { ...this.pages[pageId], access: 0 }; + }); + throw error; + } + }; + + /** + * Mark a page archived + * @param workspaceSlug + * @param projectId + * @param pageId + */ + archivePage = async (workspaceSlug: string, projectId: string, pageId: string) => { + await this.pageService.archivePage(workspaceSlug, projectId, pageId); + runInAction(() => { + this.archivedPages[pageId] = this.pages[pageId]; + this.pages = omit(this.pages, [pageId]); + }); + }; + + /** + * Restore a page from archived pages to pages + * @param workspaceSlug + * @param projectId + * @param pageId + */ + restorePage = async (workspaceSlug: string, projectId: string, pageId: string) => { + await this.pageService.restorePage(workspaceSlug, projectId, pageId); + runInAction(() => { + this.pages[pageId] = this.archivedPages[pageId]; + this.archivedPages = omit(this.archivedPages, [pageId]); + }); + }; } diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index 0738a60ba..d8ee12546 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -1,5 +1,6 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { RootStore } from "../root.store"; +import { set } from "lodash"; // types import { IWorkspace } from "types"; // services @@ -12,24 +13,19 @@ export interface IWorkspaceRootStore { // states loader: boolean; error: any | null; - // observables - workspaces: IWorkspace[] | undefined; - + workspaces: Record; // computed currentWorkspace: IWorkspace | null; workspacesCreatedByCurrentUser: IWorkspace[] | null; - // computed actions getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null; getWorkspaceById: (workspaceId: string) => IWorkspace | null; - // actions - fetchWorkspaces: () => Promise; + fetchWorkspaces: () => Promise>; createWorkspace: (data: Partial) => Promise; updateWorkspace: (workspaceSlug: string, data: Partial) => Promise; deleteWorkspace: (workspaceSlug: string) => Promise; - // sub-stores webhook: IWebhookStore; apiToken: IApiTokenStore; @@ -39,10 +35,8 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { // states loader: boolean = false; error: any | null = null; - // observables - workspaces: IWorkspace[] | undefined = []; - + workspaces: Record = {}; // services workspaceService; // root store @@ -56,18 +50,14 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { // states loader: observable.ref, error: observable.ref, - // observables workspaces: observable, - // computed currentWorkspace: computed, workspacesCreatedByCurrentUser: computed, - // computed actions getWorkspaceBySlug: action, getWorkspaceById: action, - // actions fetchWorkspaces: action, createWorkspace: action, @@ -92,7 +82,9 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { if (!workspaceSlug) return null; - return this.workspaces?.find((workspace) => workspace.slug === workspaceSlug) || null; + const workspaceDetails = Object.values(this.workspaces ?? {})?.find((w) => w.slug === workspaceSlug); + + return workspaceDetails || null; } /** @@ -105,20 +97,23 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { if (!user) return null; - return this.workspaces.filter((w) => w.created_by === user?.id); + const userWorkspaces = Object.values(this.workspaces ?? {})?.filter((w) => w.created_by === user?.id); + + return userWorkspaces || null; } /** * get workspace info from the array of workspaces in the store using workspace slug * @param workspaceSlug */ - getWorkspaceBySlug = (workspaceSlug: string) => this.workspaces?.find((w) => w.slug == workspaceSlug) || null; + getWorkspaceBySlug = (workspaceSlug: string) => + Object.values(this.workspaces ?? {})?.find((w) => w.slug == workspaceSlug) || null; /** * get workspace info from the array of workspaces in the store using workspace id * @param workspaceId */ - getWorkspaceById = (workspaceId: string) => this.workspaces?.find((w) => w.id == workspaceId) || null; + getWorkspaceById = (workspaceId: string) => this.workspaces?.[workspaceId] || null; /** * fetch user workspaces from API @@ -143,7 +138,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { runInAction(() => { this.loader = false; this.error = error; - this.workspaces = []; + this.workspaces = {}; }); throw error; @@ -163,10 +158,12 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { const response = await this.workspaceService.createWorkspace(data); + const updatedWorkspacesList = set(this.workspaces, response.id, response); + runInAction(() => { this.loader = false; this.error = null; - this.workspaces = [...(this.workspaces ?? []), response]; + this.workspaces = updatedWorkspacesList; }); return response; @@ -186,8 +183,6 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { * @param data */ updateWorkspace = async (workspaceSlug: string, data: Partial) => { - const newWorkspaces = this.workspaces?.map((w) => (w.slug === workspaceSlug ? { ...w, ...data } : w)); - try { runInAction(() => { this.loader = true; @@ -196,10 +191,12 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { const response = await this.workspaceService.updateWorkspace(workspaceSlug, data); + const updatedWorkspacesList = set(this.workspaces, response.id, data); + runInAction(() => { this.loader = false; this.error = null; - this.workspaces = newWorkspaces; + this.workspaces = updatedWorkspacesList; }); return response; @@ -218,8 +215,6 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { * @param workspaceSlug */ deleteWorkspace = async (workspaceSlug: string) => { - const newWorkspaces = this.workspaces?.filter((w) => w.slug !== workspaceSlug); - try { runInAction(() => { this.loader = true; @@ -228,10 +223,15 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { await this.workspaceService.deleteWorkspace(workspaceSlug); + const updatedWorkspacesList = this.workspaces; + const workspaceId = this.getWorkspaceBySlug(workspaceSlug)?.id; + + delete updatedWorkspacesList[`${workspaceId}`]; + runInAction(() => { this.loader = false; this.error = null; - this.workspaces = newWorkspaces; + this.workspaces = updatedWorkspacesList; }); } catch (error) { runInAction(() => {