chore: setup inbox store and implement router store

This commit is contained in:
Aaryan Khandelwal 2023-12-18 14:29:53 +05:30
parent cc56140912
commit 34e4b6e2ff
12 changed files with 628 additions and 131 deletions

View File

@ -0,0 +1,11 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "contexts/store-context";
// types
import { IInboxRootStore } from "store/inbox";
export const useInbox = (): IInboxRootStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useInbox must be used within StoreProvider");
return context.inboxRoot;
};

View File

@ -52,17 +52,12 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
workspaceSlug ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) : null
);
console.log("workspaceMemberInfo", membership.workspaceMemberInfo);
console.log("currentWorkspaceMemberInfo", membership.currentWorkspaceMemberInfo);
console.log("hasPermissionToCurrentWorkspace", membership.hasPermissionToCurrentWorkspace);
// while data is being loaded
if (!membership.currentWorkspaceMemberInfo && membership.hasPermissionToCurrentWorkspace === undefined) {
return (
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
<div className="flex flex-col items-center gap-3 text-center">
{/* <Spinner /> */}
Not allowed
<Spinner />
</div>
</div>
);

View File

@ -0,0 +1,26 @@
import { createContext, useContext } from "react";
import { RootStore } from "store/root.store";
// mobx store
let rootStore: RootStore = new RootStore();
export const MobxStoreContext = createContext<RootStore>(rootStore);
const initializeStore = () => {
const _rootStore: RootStore = rootStore ?? new RootStore();
if (typeof window === "undefined") return _rootStore;
if (!rootStore) rootStore = _rootStore;
return _rootStore;
};
export const MobxStoreProvider = ({ children }: any) => {
const store: RootStore = initializeStore();
return <MobxStoreContext.Provider value={store}>{children}</MobxStoreContext.Provider>;
};
// hook
export const useMobxStore = () => {
const context = useContext(MobxStoreContext);
if (context === undefined) throw new Error("useMobxStore must be used within MobxStoreProvider");
return context;
};

View File

@ -1,4 +1,5 @@
import { ReactNode, useEffect, useState, FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useTheme } from "next-themes";
@ -15,10 +16,13 @@ const StoreWrapper: FC<IStoreWrapper> = observer((props) => {
const { children } = props;
// states
const [dom, setDom] = useState<any>();
// router
const router = useRouter();
// store hooks
const {
config: { fetchAppConfig },
theme: { sidebarCollapsed, toggleSidebar },
router: { setQuery },
} = useApplication();
const { currentUser } = useUser();
// fetching application Config
@ -32,9 +36,8 @@ const StoreWrapper: FC<IStoreWrapper> = observer((props) => {
useEffect(() => {
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
if (localValue && sidebarCollapsed === undefined) {
toggleSidebar(localBoolValue);
}
if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue);
}, [sidebarCollapsed, currentUser, setTheme, toggleSidebar]);
/**
@ -42,16 +45,18 @@ const StoreWrapper: FC<IStoreWrapper> = observer((props) => {
*/
useEffect(() => {
if (!currentUser) return;
if (window) {
setDom(window.document?.querySelector<HTMLElement>("[data-theme='custom']"));
}
if (window) setDom(window.document?.querySelector<HTMLElement>("[data-theme='custom']"));
setTheme(currentUser?.theme?.theme || "system");
if (currentUser?.theme?.theme === "custom" && dom) {
applyTheme(currentUser?.theme?.palette, false);
} else unsetCustomCssVariables();
if (currentUser?.theme?.theme === "custom" && dom) applyTheme(currentUser?.theme?.palette, false);
else unsetCustomCssVariables();
}, [currentUser, setTheme, dom]);
// TODO: set router values
useEffect(() => {
if (!router.query) return;
setQuery(router.query);
}, [router.query, setQuery]);
return <>{children}</>;
});

View File

@ -1,101 +0,0 @@
import { ReactNode, useEffect, useState, FC } from "react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// helpers
import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
interface IStoreWrapper {
children: ReactNode;
}
const StoreWrapper: FC<IStoreWrapper> = observer((props) => {
const { children } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, globalViewId, viewId, inboxId, webhookId } = router.query;
// store
const {
theme: { sidebarCollapsed, toggleSidebar },
user: { currentUser },
workspace: { setWorkspaceSlug },
project: { setProjectId },
cycle: { setCycleId },
module: { setModuleId },
globalViews: { setGlobalViewId },
projectViews: { setViewId },
inbox: { setInboxId },
webhook: { setCurrentWebhookId },
appConfig: { fetchAppConfig },
} = useMobxStore();
// fetching application Config
useSWR("APP_CONFIG", () => fetchAppConfig(), { revalidateIfStale: false, revalidateOnFocus: false });
// state
const [dom, setDom] = useState<any>();
// theme
const { setTheme } = useTheme();
/**
* Sidebar collapsed fetching from local storage
*/
useEffect(() => {
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
if (localValue && sidebarCollapsed === undefined) {
toggleSidebar(localBoolValue);
}
}, [sidebarCollapsed, currentUser, setTheme, toggleSidebar]);
/**
* Setting up the theme of the user by fetching it from local storage
*/
useEffect(() => {
if (!currentUser) return;
if (window) {
setDom(window.document?.querySelector<HTMLElement>("[data-theme='custom']"));
}
setTheme(currentUser?.theme?.theme || "system");
if (currentUser?.theme?.theme === "custom" && dom) {
applyTheme(currentUser?.theme?.palette, false);
} else unsetCustomCssVariables();
}, [currentUser, setTheme, dom]);
/**
* Setting router info to the respective stores.
*/
useEffect(() => {
if (workspaceSlug) setWorkspaceSlug(workspaceSlug.toString());
setProjectId(projectId?.toString() ?? null);
setCycleId(cycleId?.toString() ?? null);
setModuleId(moduleId?.toString() ?? null);
setGlobalViewId(globalViewId?.toString() ?? null);
setViewId(viewId?.toString() ?? null);
setInboxId(inboxId?.toString() ?? null);
setCurrentWebhookId(webhookId?.toString() ?? undefined);
}, [
workspaceSlug,
projectId,
cycleId,
moduleId,
globalViewId,
viewId,
inboxId,
webhookId,
setWorkspaceSlug,
setProjectId,
setCycleId,
setModuleId,
setGlobalViewId,
setViewId,
setInboxId,
setCurrentWebhookId,
]);
return <>{children}</>;
});
export default StoreWrapper;

View File

@ -1,10 +1,12 @@
import { action, makeObservable, observable, computed } from "mobx";
import { action, makeObservable, observable, computed, runInAction } from "mobx";
import { ParsedUrlQuery } from "node:querystring";
export interface IRouterStore {
// observables
query: ParsedUrlQuery;
// actions
setQuery: (query: ParsedUrlQuery) => void;
// computed
workspaceSlug: string | undefined;
projectId: string | undefined;
cycleId: string | undefined;
@ -18,13 +20,15 @@ export interface IRouterStore {
}
export class RouterStore implements IRouterStore {
// observables
query: ParsedUrlQuery = {};
constructor() {
makeObservable(this, {
// observables
query: observable,
// actions
setQuery: action,
//computed
workspaceSlug: computed,
projectId: computed,
@ -39,9 +43,11 @@ export class RouterStore implements IRouterStore {
});
}
setQuery(query: ParsedUrlQuery) {
this.query = query;
}
setQuery = (query: ParsedUrlQuery) => {
runInAction(() => {
this.query = query;
});
};
get workspaceSlug() {
return this.query?.workspaceSlug?.toString();

View File

@ -0,0 +1,134 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { set } from "lodash";
// services
import { InboxService } from "services/inbox.service";
// types
import { RootStore } from "store/root.store";
import { IInbox } from "types";
export interface IInboxStore {
// states
loader: boolean;
error: any | null;
// observables
inboxesList: {
[projectId: string]: IInbox[];
};
inboxDetails: {
[inboxId: string]: IInbox;
};
// computed
isInboxEnabled: boolean;
// computed actions
getInboxId: (projectId: string) => string | null;
// actions
fetchInboxesList: (workspaceSlug: string, projectId: string) => Promise<IInbox[]>;
fetchInboxDetails: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInbox>;
}
export class InboxStore implements IInboxStore {
// states
loader: boolean = false;
error: any | null = null;
// observables
inboxesList: {
[projectId: string]: IInbox[];
} = {};
inboxDetails: {
[inboxId: string]: IInbox;
} = {};
// root store
rootStore;
// services
inboxService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// states
loader: observable.ref,
error: observable.ref,
// observables
inboxesList: observable,
inboxDetails: observable,
// computed
isInboxEnabled: computed,
// computed actions
getInboxId: action,
// actions
fetchInboxesList: action,
});
// root store
this.rootStore = _rootStore;
// services
this.inboxService = new InboxService();
}
get isInboxEnabled() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return false;
const projectDetails = this.rootStore.projectRoot.project.currentProjectDetails;
if (!projectDetails) return false;
return projectDetails.inbox_view;
}
getInboxId = (projectId: string) => {
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId);
if (!projectDetails || !projectDetails.inbox_view) return null;
return this.inboxesList[projectId]?.[0]?.id ?? null;
};
fetchInboxesList = async (workspaceSlug: string, projectId: string) => {
try {
runInAction(() => {
this.loader = true;
});
const inboxesResponse = await this.inboxService.getInboxes(workspaceSlug, projectId);
runInAction(() => {
this.loader = false;
set(this.inboxesList, projectId, inboxesResponse);
});
return inboxesResponse;
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
fetchInboxDetails = async (workspaceSlug: string, projectId: string, inboxId: string) => {
try {
runInAction(() => {
this.loader = true;
});
const inboxDetailsResponse = await this.inboxService.getInboxById(workspaceSlug, projectId, inboxId);
runInAction(() => {
this.loader = false;
set(this.inboxDetails, inboxId, inboxDetailsResponse);
});
return inboxDetailsResponse;
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
}

View File

@ -0,0 +1,135 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { set } from "lodash";
// services
import { InboxService } from "services/inbox.service";
// types
import { RootStore } from "store/root.store";
import { IInbox, IInboxFilterOptions, IInboxQueryParams } from "types";
import { EUserWorkspaceRoles } from "constants/workspace";
import { EUserProjectRoles } from "constants/project";
export interface IInboxFiltersStore {
// states
loader: boolean;
error: any | null;
// observables
inboxFilters: {
[inboxId: string]: { filters: IInboxFilterOptions };
};
// computed
appliedFilters: IInboxQueryParams | null;
// actions
fetchInboxFilters: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInbox>;
updateInboxFilters: (
workspaceSlug: string,
projectId: string,
inboxId: string,
filters: Partial<IInboxFilterOptions>
) => Promise<void>;
}
export class InboxFiltersStore implements IInboxFiltersStore {
// states
loader: boolean = false;
error: any | null = null;
// observables
inboxFilters: {
[inboxId: string]: { filters: IInboxFilterOptions };
} = {};
// root store
rootStore;
// services
inboxService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// states
loader: observable.ref,
error: observable.ref,
// observables
inboxFilters: observable,
// computed
appliedFilters: computed,
// actions
fetchInboxFilters: action,
updateInboxFilters: action,
});
this.rootStore = _rootStore;
this.inboxService = new InboxService();
}
get appliedFilters(): IInboxQueryParams | null {
const inboxId = this.rootStore.app.router.inboxId;
if (!inboxId) return null;
const filtersList = this.inboxFilters[inboxId]?.filters;
if (!filtersList) return null;
const filteredRouteParams: IInboxQueryParams = {
priority: filtersList.priority ? filtersList.priority.join(",") : null,
inbox_status: filtersList.inbox_status ? filtersList.inbox_status.join(",") : null,
};
return filteredRouteParams;
}
fetchInboxFilters = async (workspaceSlug: string, projectId: string, inboxId: string) => {
try {
runInAction(() => {
this.loader = true;
});
const issuesResponse = await this.inboxService.getInboxById(workspaceSlug, projectId, inboxId);
runInAction(() => {
this.loader = false;
set(this.inboxFilters, [inboxId], issuesResponse.view_props);
});
return issuesResponse;
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
updateInboxFilters = async (
workspaceSlug: string,
projectId: string,
inboxId: string,
filters: Partial<IInboxFilterOptions>
) => {
const newViewProps = {
...this.inboxFilters[inboxId],
filters: {
...this.inboxFilters[inboxId]?.filters,
...filters,
},
};
try {
runInAction(() => {
set(this.inboxFilters, [inboxId], newViewProps);
});
const userRole = this.rootStore.user.membership?.currentProjectRole || EUserProjectRoles.GUEST;
if (userRole > EUserWorkspaceRoles.VIEWER)
await this.inboxService.patchInbox(workspaceSlug, projectId, inboxId, { view_props: newViewProps });
} catch (error) {
runInAction(() => {
this.error = error;
});
this.fetchInboxFilters(workspaceSlug, projectId, inboxId);
throw error;
}
};
}

View File

@ -0,0 +1,256 @@
import { observable, action, makeObservable, runInAction, autorun } from "mobx";
import { set } from "lodash";
// services
import { InboxService } from "services/inbox.service";
// types
import { RootStore } from "store/root.store";
import { IInboxIssue, IIssue, TInboxStatus } from "types";
// constants
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
export interface IInboxIssuesStore {
// states
loader: boolean;
error: any | null;
// observables
issueMap: Record<string, IInboxIssue>;
// computed actions
getIssueById: (issueId: string) => IInboxIssue | null;
// actions
fetchIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInboxIssue[]>;
fetchIssueDetails: (
workspaceSlug: string,
projectId: string,
inboxId: string,
issueId: string
) => Promise<IInboxIssue>;
createIssue: (
workspaceSlug: string,
projectId: string,
inboxId: string,
data: Partial<IIssue>
) => Promise<IInboxIssue>;
updateIssue: (
workspaceSlug: string,
projectId: string,
inboxId: string,
issueId: string,
data: Partial<IInboxIssue>
) => Promise<void>;
updateIssueStatus: (
workspaceSlug: string,
projectId: string,
inboxId: string,
issueId: string,
data: TInboxStatus
) => Promise<void>;
deleteIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
}
export class InboxIssuesStore implements IInboxIssuesStore {
// states
loader: boolean = false;
error: any | null = null;
// observables
issueMap: Record<string, IInboxIssue> = {};
// root store
rootStore;
// services
inboxService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// states
loader: observable.ref,
error: observable.ref,
// observables
issueMap: observable,
// computed actions
getIssueById: action,
// actions
fetchIssues: action,
fetchIssueDetails: action,
createIssue: action,
updateIssue: action,
updateIssueStatus: action,
deleteIssue: action,
});
// root store
this.rootStore = _rootStore;
// services
this.inboxService = new InboxService();
autorun(() => {
const routerStore = this.rootStore.app.router;
const workspaceSlug = routerStore?.workspaceSlug;
const projectId = routerStore?.projectId;
const inboxId = routerStore?.inboxId;
if (workspaceSlug && projectId && inboxId && this.rootStore.inboxRoot.inboxFilters.inboxFilters[inboxId])
this.fetchIssues(workspaceSlug, projectId, inboxId);
});
}
getIssueById = (issueId: string): IInboxIssue | null => this.issueMap[issueId] ?? null;
fetchIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => {
try {
runInAction(() => {
this.loader = true;
});
const queryParams = this.rootStore.inboxRoot.inboxFilters.appliedFilters ?? undefined;
const issuesResponse = await this.inboxService.getInboxIssues(workspaceSlug, projectId, inboxId, queryParams);
runInAction(() => {
this.loader = false;
issuesResponse.forEach((issue) => {
set(this.issueMap, issue.issue_inbox?.[0].id, issue);
});
});
return issuesResponse;
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
fetchIssueDetails = async (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => {
try {
runInAction(() => {
this.loader = true;
});
const issueResponse = await this.inboxService.getInboxIssueById(workspaceSlug, projectId, inboxId, issueId);
runInAction(() => {
this.loader = false;
set(this.issueMap, issueId, issueResponse);
});
return issueResponse;
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
createIssue = async (workspaceSlug: string, projectId: string, inboxId: string, data: Partial<IIssue>) => {
const payload = {
issue: {
name: data.name,
description: data.description,
description_html: data.description_html,
priority: data.priority,
},
source: INBOX_ISSUE_SOURCE,
};
try {
const response = await this.inboxService.createInboxIssue(workspaceSlug, projectId, inboxId, payload);
runInAction(() => {
set(this.issueMap, response.issue_inbox?.[0].id, response);
});
return response;
} catch (error) {
runInAction(() => {
this.error = error;
});
throw error;
}
};
updateIssue = async (
workspaceSlug: string,
projectId: string,
inboxId: string,
issueId: string,
data: Partial<IInboxIssue>
) => {
const issueDetails = this.rootStore.inboxRoot.inboxIssues.getIssueById(issueId);
try {
runInAction(() => {
set(this.issueMap, issueId, {
...issueDetails,
...data,
});
});
await this.inboxService.patchInboxIssue(workspaceSlug, projectId, inboxId, issueId, { issue: data });
} catch (error) {
runInAction(() => {
this.error = error;
});
this.rootStore.inboxRoot.inboxIssues.fetchIssues(workspaceSlug, projectId, inboxId);
this.fetchIssueDetails(workspaceSlug, projectId, inboxId, issueId);
throw error;
}
};
updateIssueStatus = async (
workspaceSlug: string,
projectId: string,
inboxId: string,
issueId: string,
data: TInboxStatus
) => {
const issueDetails = this.rootStore.inboxRoot.inboxIssues.getIssueById(issueId);
try {
runInAction(() => {
set(this.issueMap, [issueId, "issue_inbox", 0], {
...issueDetails?.issue_inbox?.[0],
...data,
});
});
await this.inboxService.markInboxStatus(workspaceSlug, projectId, inboxId, issueId, data);
} catch (error) {
runInAction(() => {
this.error = error;
});
this.rootStore.inboxRoot.inboxIssues.fetchIssues(workspaceSlug, projectId, inboxId);
this.fetchIssueDetails(workspaceSlug, projectId, inboxId, issueId);
throw error;
}
};
deleteIssue = async (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => {
try {
runInAction(() => {
delete this.issueMap[issueId];
});
await this.inboxService.deleteInboxIssue(workspaceSlug, projectId, inboxId, issueId);
} catch (error) {
runInAction(() => {
this.error = error;
});
this.rootStore.inboxRoot.inboxIssues.fetchIssues(workspaceSlug, projectId, inboxId);
this.fetchIssueDetails(workspaceSlug, projectId, inboxId, issueId);
throw error;
}
};
}

33
web/store/inbox/index.ts Normal file
View File

@ -0,0 +1,33 @@
import { makeAutoObservable } from "mobx";
// types
import { RootStore } from "store/root.store";
import { IInboxIssuesStore, InboxIssuesStore } from "./inbox_issue.store";
import { IInboxFiltersStore, InboxFiltersStore } from "./inbox_filter.store";
import { IInboxStore, InboxStore } from "./inbox.store";
export interface IInboxRootStore {
// sub-stores
inbox: IInboxStore;
inboxFilters: IInboxFiltersStore;
inboxIssues: IInboxIssuesStore;
}
export class InboxRootStore implements IInboxRootStore {
// root store
rootStore: RootStore;
// sub-stores
inbox: IInboxStore;
inboxFilters: IInboxFiltersStore;
inboxIssues: IInboxIssuesStore;
constructor(_rootStore: RootStore) {
makeAutoObservable(this, {});
// root store
this.rootStore = _rootStore;
// sub-stores
this.inbox = new InboxStore(_rootStore);
this.inboxFilters = new InboxFiltersStore(_rootStore);
this.inboxIssues = new InboxIssuesStore(_rootStore);
}
}

View File

@ -12,6 +12,7 @@ import { IStateStore, StateStore } from "./state.store";
import { IPageStore, PageStore } from "./page.store";
import { ILabelRootStore, LabelRootStore } from "./label";
import { IMemberRootStore, MemberRootStore } from "./member";
import { IInboxRootStore, InboxRootStore } from "./inbox";
enableStaticRendering(typeof window === "undefined");
@ -22,6 +23,7 @@ export class RootStore {
projectRoot: IProjectRootStore;
labelRoot: ILabelRootStore;
memberRoot: IMemberRootStore;
inboxRoot: IInboxRootStore;
cycle: ICycleStore;
module: IModuleStore;
projectView: IProjectViewStore;
@ -36,6 +38,7 @@ export class RootStore {
this.projectRoot = new ProjectRootStore(this);
this.labelRoot = new LabelRootStore(this);
this.memberRoot = new MemberRootStore(this);
this.inboxRoot = new InboxRootStore(this);
// independent stores
this.state = new StateStore(this);
this.issue = new IssueRootStore(this);

View File

@ -7,6 +7,7 @@ import { WorkspaceService } from "services/workspace.service";
// interfaces
import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "types";
import { RootStore } from "../root.store";
// constants
import { EUserProjectRoles } from "constants/project";
import { EUserWorkspaceRoles } from "constants/workspace";
@ -128,15 +129,8 @@ export class UserMembershipStore implements IUserMembershipStore {
try {
const response = await this.workspaceService.workspaceMemberMe(workspaceSlug);
console.log("response", response);
let memberInfo = this.workspaceMemberInfo;
if (!memberInfo) memberInfo = {};
memberInfo[workspaceSlug] = { ...response };
runInAction(() => {
this.workspaceMemberInfo = memberInfo;
// set(this.workspaceMemberInfo, [workspaceSlug], response);
set(this.workspaceMemberInfo, [workspaceSlug], response);
set(this.hasPermissionToWorkspace, [workspaceSlug], true);
});