feat: init
This commit is contained in:
42
apps/space/core/store/cycle.store.ts
Normal file
42
apps/space/core/store/cycle.store.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { SitesCycleService } from "@plane/services";
|
||||
import type { TPublicCycle } from "@/types/cycle";
|
||||
// store
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
|
||||
export interface ICycleStore {
|
||||
// observables
|
||||
cycles: TPublicCycle[] | undefined;
|
||||
// computed actions
|
||||
getCycleById: (cycleId: string | undefined) => TPublicCycle | undefined;
|
||||
// fetch actions
|
||||
fetchCycles: (anchor: string) => Promise<TPublicCycle[]>;
|
||||
}
|
||||
|
||||
export class CycleStore implements ICycleStore {
|
||||
cycles: TPublicCycle[] | undefined = undefined;
|
||||
cycleService: SitesCycleService;
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
cycles: observable,
|
||||
// fetch action
|
||||
fetchCycles: action,
|
||||
});
|
||||
this.cycleService = new SitesCycleService();
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
getCycleById = (cycleId: string | undefined) => this.cycles?.find((cycle) => cycle.id === cycleId);
|
||||
|
||||
fetchCycles = async (anchor: string) => {
|
||||
const cyclesResponse = await this.cycleService.list(anchor);
|
||||
runInAction(() => {
|
||||
this.cycles = cyclesResponse;
|
||||
});
|
||||
return cyclesResponse;
|
||||
};
|
||||
}
|
||||
515
apps/space/core/store/helpers/base-issues.store.ts
Normal file
515
apps/space/core/store/helpers/base-issues.store.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
import { concat, get, set, uniq, update } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { SitesIssueService } from "@plane/services";
|
||||
import type {
|
||||
TIssueGroupByOptions,
|
||||
TGroupedIssues,
|
||||
TSubGroupedIssues,
|
||||
TLoader,
|
||||
IssuePaginationOptions,
|
||||
TIssues,
|
||||
TIssuePaginationData,
|
||||
TGroupedIssueCount,
|
||||
TPaginationData,
|
||||
} from "@plane/types";
|
||||
// types
|
||||
import type { IIssue, TIssuesResponse } from "@/types/issue";
|
||||
import type { CoreRootStore } from "../root.store";
|
||||
// constants
|
||||
// helpers
|
||||
|
||||
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null | "team_project"> | "target_date";
|
||||
|
||||
export enum EIssueGroupedAction {
|
||||
ADD = "ADD",
|
||||
DELETE = "DELETE",
|
||||
REORDER = "REORDER",
|
||||
}
|
||||
|
||||
export interface IBaseIssuesStore {
|
||||
// observable
|
||||
loader: Record<string, TLoader>;
|
||||
// actions
|
||||
addIssue(issues: IIssue[], shouldReplace?: boolean): void;
|
||||
// helper methods
|
||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined; // object to store Issue Ids based on group or subgroup
|
||||
groupedIssueCount: TGroupedIssueCount; // map of groupId/subgroup and issue count of that particular group/subgroup
|
||||
issuePaginationData: TIssuePaginationData; // map of groupId/subgroup and pagination Data of that particular group/subgroup
|
||||
|
||||
// helper methods
|
||||
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
|
||||
getPaginationData(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined;
|
||||
getIssueLoader(groupId?: string, subGroupId?: string): TLoader;
|
||||
getGroupIssueCount: (
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined,
|
||||
isSubGroupCumulative: boolean
|
||||
) => number | undefined;
|
||||
}
|
||||
|
||||
export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof IIssue> = {
|
||||
project: "project_id",
|
||||
cycle: "cycle_id",
|
||||
module: "module_ids",
|
||||
state: "state_id",
|
||||
"state_detail.group": "state_group" as keyof IIssue, // state_detail.group is only being used for state_group display,
|
||||
priority: "priority",
|
||||
labels: "label_ids",
|
||||
created_by: "created_by",
|
||||
assignees: "assignee_ids",
|
||||
target_date: "target_date",
|
||||
};
|
||||
|
||||
export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
loader: Record<string, TLoader> = {};
|
||||
groupedIssueIds: TIssues | undefined = undefined;
|
||||
issuePaginationData: TIssuePaginationData = {};
|
||||
groupedIssueCount: TGroupedIssueCount = {};
|
||||
//
|
||||
paginationOptions: IssuePaginationOptions | undefined = undefined;
|
||||
|
||||
issueService;
|
||||
// root store
|
||||
rootIssueStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
loader: observable,
|
||||
groupedIssueIds: observable,
|
||||
issuePaginationData: observable,
|
||||
groupedIssueCount: observable,
|
||||
|
||||
paginationOptions: observable,
|
||||
// action
|
||||
storePreviousPaginationValues: action.bound,
|
||||
|
||||
onfetchIssues: action.bound,
|
||||
onfetchNexIssues: action.bound,
|
||||
clear: action.bound,
|
||||
setLoader: action.bound,
|
||||
});
|
||||
this.rootIssueStore = _rootStore;
|
||||
this.issueService = new SitesIssueService();
|
||||
}
|
||||
|
||||
getIssueIds = (groupId?: string, subGroupId?: string) => {
|
||||
const groupedIssueIds = this.groupedIssueIds;
|
||||
|
||||
if (!groupedIssueIds) return undefined;
|
||||
|
||||
const allIssues = groupedIssueIds[ALL_ISSUES] ?? [];
|
||||
if (allIssues && Array.isArray(allIssues)) {
|
||||
return allIssues as string[];
|
||||
}
|
||||
|
||||
if (groupId && groupedIssueIds?.[groupId] && Array.isArray(groupedIssueIds[groupId])) {
|
||||
return (groupedIssueIds[groupId] ?? []) as string[];
|
||||
}
|
||||
|
||||
if (groupId && subGroupId) {
|
||||
return ((groupedIssueIds as TSubGroupedIssues)[groupId]?.[subGroupId] ?? []) as string[];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method will add issues to the issuesMap
|
||||
* @param {IIssue[]} issues
|
||||
* @returns {void}
|
||||
*/
|
||||
addIssue = (issues: IIssue[], shouldReplace = false) => {
|
||||
if (issues && issues.length <= 0) return;
|
||||
runInAction(() => {
|
||||
issues.forEach((issue) => {
|
||||
if (!this.rootIssueStore.issueDetail.getIssueById(issue.id) || shouldReplace)
|
||||
set(this.rootIssueStore.issueDetail.details, issue.id, issue);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Store the pagination data required for next subsequent issue pagination calls
|
||||
* @param prevCursor cursor value of previous page
|
||||
* @param nextCursor cursor value of next page
|
||||
* @param nextPageResults boolean to indicate if the next page results exist i.e, have we reached end of pages
|
||||
* @param groupId groupId and subGroupId to add the pagination data for the particular group/subgroup
|
||||
* @param subGroupId
|
||||
*/
|
||||
setPaginationData(
|
||||
prevCursor: string,
|
||||
nextCursor: string,
|
||||
nextPageResults: boolean,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) {
|
||||
const cursorObject = {
|
||||
prevCursor,
|
||||
nextCursor,
|
||||
nextPageResults,
|
||||
};
|
||||
|
||||
set(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)], cursorObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the loader value of the particular groupId/subGroupId, or to ALL_ISSUES if both are undefined
|
||||
* @param loaderValue
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
*/
|
||||
setLoader(loaderValue: TLoader, groupId?: string, subGroupId?: string) {
|
||||
runInAction(() => {
|
||||
set(this.loader, this.getGroupKey(groupId, subGroupId), loaderValue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the Loader value of particular group/subgroup/ALL_ISSUES
|
||||
*/
|
||||
getIssueLoader = (groupId?: string, subGroupId?: string) => get(this.loader, this.getGroupKey(groupId, subGroupId));
|
||||
|
||||
/**
|
||||
* gets the pagination data of particular group/subgroup/ALL_ISSUES
|
||||
*/
|
||||
getPaginationData = computedFn(
|
||||
(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined =>
|
||||
get(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)])
|
||||
);
|
||||
|
||||
/**
|
||||
* gets the issue count of particular group/subgroup/ALL_ISSUES
|
||||
*
|
||||
* if isSubGroupCumulative is true, sum up all the issueCount of the subGroupId, across all the groupIds
|
||||
*/
|
||||
getGroupIssueCount = computedFn(
|
||||
(
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined,
|
||||
isSubGroupCumulative: boolean
|
||||
): number | undefined => {
|
||||
if (isSubGroupCumulative && subGroupId) {
|
||||
const groupIssuesKeys = Object.keys(this.groupedIssueCount);
|
||||
let subGroupCumulativeCount = 0;
|
||||
|
||||
for (const groupKey of groupIssuesKeys) {
|
||||
if (groupKey.includes(`_${subGroupId}`)) subGroupCumulativeCount += this.groupedIssueCount[groupKey];
|
||||
}
|
||||
|
||||
return subGroupCumulativeCount;
|
||||
}
|
||||
|
||||
return get(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)]);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* This Method is called after fetching the first paginated issues
|
||||
*
|
||||
* This method updates the appropriate issue list based on if groupByKey or subGroupByKey are defined
|
||||
* If both groupByKey and subGroupByKey are not defined, then the issue list are added to another group called ALL_ISSUES
|
||||
* @param issuesResponse Paginated Response received from the API
|
||||
* @param options Pagination options
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param id Id can be anything from cycleId, moduleId, viewId or userId based on the store
|
||||
*/
|
||||
onfetchIssues(issuesResponse: TIssuesResponse, options: IssuePaginationOptions) {
|
||||
// Process the Issue Response to get the following data from it
|
||||
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
|
||||
|
||||
// The Issue list is added to the main Issue Map
|
||||
this.addIssue(issueList);
|
||||
|
||||
// Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts
|
||||
runInAction(() => {
|
||||
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount);
|
||||
this.loader[this.getGroupKey()] = undefined;
|
||||
});
|
||||
|
||||
// store Pagination options for next subsequent calls and data like next cursor etc
|
||||
this.storePreviousPaginationValues(issuesResponse, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* This Method is called on the subsequent pagination calls after the first initial call
|
||||
*
|
||||
* This method updates the appropriate issue list based on if groupId or subgroupIds are Passed
|
||||
* @param issuesResponse Paginated Response received from the API
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
*/
|
||||
onfetchNexIssues(issuesResponse: TIssuesResponse, groupId?: string, subGroupId?: string) {
|
||||
// Process the Issue Response to get the following data from it
|
||||
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
|
||||
|
||||
// The Issue list is added to the main Issue Map
|
||||
this.addIssue(issueList);
|
||||
|
||||
// Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts
|
||||
runInAction(() => {
|
||||
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount, groupId, subGroupId);
|
||||
this.loader[this.getGroupKey(groupId, subGroupId)] = undefined;
|
||||
});
|
||||
|
||||
// store Pagination data like next cursor etc
|
||||
this.storePreviousPaginationValues(issuesResponse, undefined, groupId, subGroupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called to clear out the current store
|
||||
*/
|
||||
clear(shouldClearPaginationOptions = true) {
|
||||
runInAction(() => {
|
||||
this.groupedIssueIds = undefined;
|
||||
this.issuePaginationData = {};
|
||||
this.groupedIssueCount = {};
|
||||
if (shouldClearPaginationOptions) {
|
||||
this.paginationOptions = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method processes the issueResponse to provide data that can be used to update the store
|
||||
* @param issueResponse
|
||||
* @returns issueList, list of issue Data
|
||||
* @returns groupedIssues, grouped issue Ids
|
||||
* @returns groupedIssueCount, object containing issue counts of individual groups
|
||||
*/
|
||||
processIssueResponse(issueResponse: TIssuesResponse): {
|
||||
issueList: IIssue[];
|
||||
groupedIssues: TIssues;
|
||||
groupedIssueCount: TGroupedIssueCount;
|
||||
} {
|
||||
const issueResult = issueResponse?.results;
|
||||
|
||||
// if undefined return empty objects
|
||||
if (!issueResult)
|
||||
return {
|
||||
issueList: [],
|
||||
groupedIssues: {},
|
||||
groupedIssueCount: {},
|
||||
};
|
||||
|
||||
//if is an array then it's an ungrouped response. return values with groupId as ALL_ISSUES
|
||||
if (Array.isArray(issueResult)) {
|
||||
return {
|
||||
issueList: issueResult,
|
||||
groupedIssues: {
|
||||
[ALL_ISSUES]: issueResult.map((issue) => issue.id),
|
||||
},
|
||||
groupedIssueCount: {
|
||||
[ALL_ISSUES]: issueResponse.total_count,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const issueList: IIssue[] = [];
|
||||
const groupedIssues: TGroupedIssues | TSubGroupedIssues = {};
|
||||
const groupedIssueCount: TGroupedIssueCount = {};
|
||||
|
||||
// update total issue count to ALL_ISSUES
|
||||
set(groupedIssueCount, [ALL_ISSUES], issueResponse.total_count);
|
||||
|
||||
// loop through all the groupIds from issue Result
|
||||
for (const groupId in issueResult) {
|
||||
const groupIssuesObject = issueResult[groupId];
|
||||
const groupIssueResult = groupIssuesObject?.results;
|
||||
|
||||
// if groupIssueResult is undefined then continue the loop
|
||||
if (!groupIssueResult) continue;
|
||||
|
||||
// set grouped Issue count of the current groupId
|
||||
set(groupedIssueCount, [groupId], groupIssuesObject.total_results);
|
||||
|
||||
// if groupIssueResult, the it is not subGrouped
|
||||
if (Array.isArray(groupIssueResult)) {
|
||||
// add the result to issueList
|
||||
issueList.push(...groupIssueResult);
|
||||
// set the issue Ids to the groupId path
|
||||
set(
|
||||
groupedIssues,
|
||||
[groupId],
|
||||
groupIssueResult.map((issue) => issue.id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// loop through all the subGroupIds from issue Result
|
||||
for (const subGroupId in groupIssueResult) {
|
||||
const subGroupIssuesObject = groupIssueResult[subGroupId];
|
||||
const subGroupIssueResult = subGroupIssuesObject?.results;
|
||||
|
||||
// if subGroupIssueResult is undefined then continue the loop
|
||||
if (!subGroupIssueResult) continue;
|
||||
|
||||
// set sub grouped Issue count of the current groupId
|
||||
set(groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], subGroupIssuesObject.total_results);
|
||||
|
||||
if (Array.isArray(subGroupIssueResult)) {
|
||||
// add the result to issueList
|
||||
issueList.push(...subGroupIssueResult);
|
||||
// set the issue Ids to the [groupId, subGroupId] path
|
||||
set(
|
||||
groupedIssues,
|
||||
[groupId, subGroupId],
|
||||
subGroupIssueResult.map((issue) => issue.id)
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { issueList, groupedIssues, groupedIssueCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to update the grouped issue Ids to it's respected lists and also to update group Issue Counts
|
||||
* @param groupedIssues Object that contains list of issueIds with respect to their groups/subgroups
|
||||
* @param groupedIssueCount Object the contains the issue count of each groups
|
||||
* @param groupId groupId string
|
||||
* @param subGroupId subGroupId string
|
||||
* @returns updates the store with the values
|
||||
*/
|
||||
updateGroupedIssueIds(
|
||||
groupedIssues: TIssues,
|
||||
groupedIssueCount: TGroupedIssueCount,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) {
|
||||
// if groupId exists and groupedIssues has ALL_ISSUES as a group,
|
||||
// then it's an individual group/subgroup pagination
|
||||
if (groupId && groupedIssues[ALL_ISSUES] && Array.isArray(groupedIssues[ALL_ISSUES])) {
|
||||
const issueGroup = groupedIssues[ALL_ISSUES];
|
||||
const issueGroupCount = groupedIssueCount[ALL_ISSUES];
|
||||
const issuesPath = [groupId];
|
||||
// issuesPath is the path for the issue List in the Grouped Issue List
|
||||
// issuePath is either [groupId] for grouped pagination or [groupId, subGroupId] for subGrouped pagination
|
||||
if (subGroupId) issuesPath.push(subGroupId);
|
||||
|
||||
// update the issue Count of the particular group/subGroup
|
||||
set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueGroupCount);
|
||||
|
||||
// update the issue list in the issuePath
|
||||
this.updateIssueGroup(issueGroup, issuesPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// if not in the above condition the it's a complete grouped pagination not individual group/subgroup pagination
|
||||
// update total issue count as ALL_ISSUES count in `groupedIssueCount` object
|
||||
set(this.groupedIssueCount, [ALL_ISSUES], groupedIssueCount[ALL_ISSUES]);
|
||||
|
||||
// loop through the groups of groupedIssues.
|
||||
for (const groupId in groupedIssues) {
|
||||
const issueGroup = groupedIssues[groupId];
|
||||
const issueGroupCount = groupedIssueCount[groupId];
|
||||
|
||||
// update the groupId's issue count
|
||||
set(this.groupedIssueCount, [groupId], issueGroupCount);
|
||||
|
||||
// This updates the group issue list in the store, if the issueGroup is a string
|
||||
const storeUpdated = this.updateIssueGroup(issueGroup, [groupId]);
|
||||
// if issueGroup is indeed a string, continue
|
||||
if (storeUpdated) continue;
|
||||
|
||||
// if issueGroup is not a string, loop through the sub group Issues
|
||||
for (const subGroupId in issueGroup) {
|
||||
const issueSubGroup = (issueGroup as TGroupedIssues)[subGroupId];
|
||||
const issueSubGroupCount = groupedIssueCount[this.getGroupKey(groupId, subGroupId)];
|
||||
|
||||
// update the subGroupId's issue count
|
||||
set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueSubGroupCount);
|
||||
// This updates the subgroup issue list in the store
|
||||
this.updateIssueGroup(issueSubGroup, [groupId, subGroupId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This Method is used to update the issue Id list at the particular issuePath
|
||||
* @param groupedIssueIds could be an issue Id List for grouped issues or an object that contains a issue Id list in case of subGrouped
|
||||
* @param issuePath array of string, to identify the path of the issueList to be updated with the above issue Id list
|
||||
* @returns a boolean that indicates if the groupedIssueIds is indeed a array Id list, in which case the issue Id list is added to the store at issuePath
|
||||
*/
|
||||
updateIssueGroup(groupedIssueIds: TGroupedIssues | string[], issuePath: string[]): boolean {
|
||||
if (!groupedIssueIds) return true;
|
||||
|
||||
// if groupedIssueIds is an array, update the `groupedIssueIds` store at the issuePath
|
||||
if (groupedIssueIds && Array.isArray(groupedIssueIds)) {
|
||||
update(this, ["groupedIssueIds", ...issuePath], (issueIds: string[] = []) =>
|
||||
uniq(concat(issueIds, groupedIssueIds as string[]))
|
||||
);
|
||||
// return true to indicate the store has been updated
|
||||
return true;
|
||||
}
|
||||
|
||||
// return false to indicate the store has been updated and the groupedIssueIds is likely Object for subGrouped Issues
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to update the count of the issues at the path with the increment
|
||||
* @param path issuePath, corresponding key is to be incremented
|
||||
* @param increment
|
||||
*/
|
||||
updateIssueCount(accumulatedUpdatesForCount: { [key: string]: EIssueGroupedAction }) {
|
||||
const updateKeys = Object.keys(accumulatedUpdatesForCount);
|
||||
for (const updateKey of updateKeys) {
|
||||
const update = accumulatedUpdatesForCount[updateKey];
|
||||
if (!update) continue;
|
||||
|
||||
const increment = update === EIssueGroupedAction.ADD ? 1 : -1;
|
||||
// get current count at the key
|
||||
const issueCount = get(this.groupedIssueCount, updateKey) ?? 0;
|
||||
// update the count at the key
|
||||
set(this.groupedIssueCount, updateKey, issueCount + increment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This Method is called to store the pagination options and paginated data from response
|
||||
* @param issuesResponse issue list response
|
||||
* @param options pagination options to be stored for next page call
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
*/
|
||||
storePreviousPaginationValues = (
|
||||
issuesResponse: TIssuesResponse,
|
||||
options?: IssuePaginationOptions,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
if (options) this.paginationOptions = options;
|
||||
|
||||
this.setPaginationData(
|
||||
issuesResponse.prev_cursor,
|
||||
issuesResponse.next_cursor,
|
||||
issuesResponse.next_page_results,
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns,
|
||||
* A compound key, if both groupId & subGroupId are defined
|
||||
* groupId, only if groupId is defined
|
||||
* ALL_ISSUES, if both groupId & subGroupId are not defined
|
||||
* @param groupId
|
||||
* @param subGroupId
|
||||
* @returns
|
||||
*/
|
||||
getGroupKey = (groupId?: string, subGroupId?: string) => {
|
||||
if (groupId && subGroupId && subGroupId !== "null") return `${groupId}_${subGroupId}`;
|
||||
|
||||
if (groupId) return groupId;
|
||||
|
||||
return ALL_ISSUES;
|
||||
};
|
||||
}
|
||||
73
apps/space/core/store/helpers/filter.helpers.ts
Normal file
73
apps/space/core/store/helpers/filter.helpers.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { EIssueGroupByToServerOptions, EServerGroupByToFilterOptions } from "@plane/constants";
|
||||
import type { IssuePaginationOptions, TIssueParams } from "@plane/types";
|
||||
|
||||
/**
|
||||
* This Method is used to construct the url params along with paginated values
|
||||
* @param filterParams params generated from filters
|
||||
* @param options pagination options
|
||||
* @param cursor cursor if exists
|
||||
* @param groupId groupId if to fetch By group
|
||||
* @param subGroupId groupId if to fetch By sub group
|
||||
* @returns
|
||||
*/
|
||||
export const getPaginationParams = (
|
||||
filterParams: Partial<Record<TIssueParams, string | boolean>> | undefined,
|
||||
options: IssuePaginationOptions,
|
||||
cursor: string | undefined,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
// if cursor exists, use the cursor. If it doesn't exist construct the cursor based on per page count
|
||||
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:1:0` : `${options.perPageCount}:0:0`;
|
||||
|
||||
// pagination params
|
||||
const paginationParams: Partial<Record<TIssueParams, string | boolean>> = {
|
||||
...filterParams,
|
||||
cursor: pageCursor,
|
||||
per_page: options.perPageCount.toString(),
|
||||
};
|
||||
|
||||
// If group by is specifically sent through options, like that for calendar layout, use that to group
|
||||
if (options.groupedBy) {
|
||||
paginationParams.group_by = EIssueGroupByToServerOptions[options.groupedBy];
|
||||
}
|
||||
|
||||
// If group by is specifically sent through options, like that for calendar layout, use that to group
|
||||
if (options.subGroupedBy) {
|
||||
paginationParams.sub_group_by = EIssueGroupByToServerOptions[options.subGroupedBy];
|
||||
}
|
||||
|
||||
// If group by is specifically sent through options, like that for calendar layout, use that to group
|
||||
if (options.orderBy) {
|
||||
paginationParams.order_by = options.orderBy;
|
||||
}
|
||||
|
||||
// If before and after dates are sent from option to filter by then, add them to filter the options
|
||||
if (options.after && options.before) {
|
||||
paginationParams["target_date"] = `${options.after};after,${options.before};before`;
|
||||
}
|
||||
|
||||
// If groupId is passed down, add a filter param for that group Id
|
||||
if (groupId) {
|
||||
const groupBy = paginationParams["group_by"] as EIssueGroupByToServerOptions | undefined;
|
||||
delete paginationParams["group_by"];
|
||||
|
||||
if (groupBy) {
|
||||
const groupByFilterOption = EServerGroupByToFilterOptions[groupBy];
|
||||
paginationParams[groupByFilterOption] = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
// If subGroupId is passed down, add a filter param for that subGroup Id
|
||||
if (subGroupId) {
|
||||
const subGroupBy = paginationParams["sub_group_by"] as EIssueGroupByToServerOptions | undefined;
|
||||
delete paginationParams["sub_group_by"];
|
||||
|
||||
if (subGroupBy) {
|
||||
const subGroupByFilterOption = EServerGroupByToFilterOptions[subGroupBy];
|
||||
paginationParams[subGroupByFilterOption] = subGroupId;
|
||||
}
|
||||
}
|
||||
|
||||
return paginationParams;
|
||||
};
|
||||
77
apps/space/core/store/instance.store.ts
Normal file
77
apps/space/core/store/instance.store.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { set } from "lodash-es";
|
||||
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { InstanceService } from "@plane/services";
|
||||
import type { IInstance, IInstanceConfig } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
type TError = {
|
||||
status: string;
|
||||
message: string;
|
||||
data?: {
|
||||
is_activated: boolean;
|
||||
is_setup_done: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export interface IInstanceStore {
|
||||
// observables
|
||||
isLoading: boolean;
|
||||
instance: IInstance | undefined;
|
||||
config: IInstanceConfig | undefined;
|
||||
error: TError | undefined;
|
||||
// action
|
||||
fetchInstanceInfo: () => Promise<void>;
|
||||
hydrate: (data: IInstance) => void;
|
||||
}
|
||||
|
||||
export class InstanceStore implements IInstanceStore {
|
||||
isLoading: boolean = true;
|
||||
instance: IInstance | undefined = undefined;
|
||||
config: IInstanceConfig | undefined = undefined;
|
||||
error: TError | undefined = undefined;
|
||||
// services
|
||||
instanceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
isLoading: observable.ref,
|
||||
instance: observable,
|
||||
config: observable,
|
||||
error: observable,
|
||||
// actions
|
||||
fetchInstanceInfo: action,
|
||||
hydrate: action,
|
||||
});
|
||||
// services
|
||||
this.instanceService = new InstanceService();
|
||||
}
|
||||
|
||||
hydrate = (data: IInstance) => set(this, "instance", data);
|
||||
|
||||
/**
|
||||
* @description fetching instance information
|
||||
*/
|
||||
fetchInstanceInfo = async () => {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
const instanceInfo = await this.instanceService.info();
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.instance = instanceInfo.instance;
|
||||
this.config = instanceInfo.config;
|
||||
});
|
||||
} catch (_error) {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.error = {
|
||||
status: "error",
|
||||
message: "Failed to fetch instance info",
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
441
apps/space/core/store/issue-detail.store.ts
Normal file
441
apps/space/core/store/issue-detail.store.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
import { isEmpty, set } from "lodash-es";
|
||||
import { makeObservable, observable, action, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// plane imports
|
||||
import { SitesFileService, SitesIssueService } from "@plane/services";
|
||||
import type { TFileSignedURLResponse, TIssuePublicComment } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
// types
|
||||
import type { IIssue, IPeekMode, IVote } from "@/types/issue";
|
||||
|
||||
export interface IIssueDetailStore {
|
||||
loader: boolean;
|
||||
error: any;
|
||||
// observables
|
||||
peekId: string | null;
|
||||
peekMode: IPeekMode;
|
||||
details: {
|
||||
[key: string]: IIssue;
|
||||
};
|
||||
// computed actions
|
||||
getIsIssuePeeked: (issueID: string) => boolean;
|
||||
// actions
|
||||
getIssueById: (issueId: string) => IIssue | undefined;
|
||||
setPeekId: (issueID: string | null) => void;
|
||||
setPeekMode: (mode: IPeekMode) => void;
|
||||
// issue actions
|
||||
fetchIssueDetails: (anchor: string, issueID: string) => void;
|
||||
// comment actions
|
||||
addIssueComment: (anchor: string, issueID: string, data: any) => Promise<TIssuePublicComment>;
|
||||
updateIssueComment: (anchor: string, issueID: string, commentID: string, data: any) => Promise<any>;
|
||||
deleteIssueComment: (anchor: string, issueID: string, commentID: string) => void;
|
||||
uploadCommentAsset: (file: File, anchor: string, commentID?: string) => Promise<TFileSignedURLResponse>;
|
||||
uploadIssueAsset: (file: File, anchor: string, commentID?: string) => Promise<TFileSignedURLResponse>;
|
||||
addCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void;
|
||||
removeCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void;
|
||||
// reaction actions
|
||||
addIssueReaction: (anchor: string, issueID: string, reactionHex: string) => void;
|
||||
removeIssueReaction: (anchor: string, issueID: string, reactionHex: string) => void;
|
||||
// vote actions
|
||||
addIssueVote: (anchor: string, issueID: string, data: { vote: 1 | -1 }) => Promise<void>;
|
||||
removeIssueVote: (anchor: string, issueID: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export class IssueDetailStore implements IIssueDetailStore {
|
||||
loader: boolean = false;
|
||||
error: any = null;
|
||||
// observables
|
||||
peekId: string | null = null;
|
||||
peekMode: IPeekMode = "side";
|
||||
details: {
|
||||
[key: string]: IIssue;
|
||||
} = {};
|
||||
// root store
|
||||
rootStore: CoreRootStore;
|
||||
// services
|
||||
issueService: SitesIssueService;
|
||||
fileService: SitesFileService;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
loader: observable.ref,
|
||||
error: observable.ref,
|
||||
// observables
|
||||
peekId: observable.ref,
|
||||
peekMode: observable.ref,
|
||||
details: observable,
|
||||
// actions
|
||||
setPeekId: action,
|
||||
setPeekMode: action,
|
||||
// issue actions
|
||||
fetchIssueDetails: action,
|
||||
// comment actions
|
||||
addIssueComment: action,
|
||||
updateIssueComment: action,
|
||||
deleteIssueComment: action,
|
||||
uploadCommentAsset: action,
|
||||
uploadIssueAsset: action,
|
||||
addCommentReaction: action,
|
||||
removeCommentReaction: action,
|
||||
// reaction actions
|
||||
addIssueReaction: action,
|
||||
removeIssueReaction: action,
|
||||
// vote actions
|
||||
addIssueVote: action,
|
||||
removeIssueVote: action,
|
||||
});
|
||||
this.rootStore = _rootStore;
|
||||
this.issueService = new SitesIssueService();
|
||||
this.fileService = new SitesFileService();
|
||||
}
|
||||
|
||||
setPeekId = (issueID: string | null) => {
|
||||
this.peekId = issueID;
|
||||
};
|
||||
|
||||
setPeekMode = (mode: IPeekMode) => {
|
||||
this.peekMode = mode;
|
||||
};
|
||||
|
||||
getIsIssuePeeked = (issueID: string) => this.peekId === issueID;
|
||||
|
||||
/**
|
||||
* @description This method will return the issue from the issuesMap
|
||||
* @param {string} issueId
|
||||
* @returns {IIssue | undefined}
|
||||
*/
|
||||
getIssueById = computedFn((issueId: string) => {
|
||||
if (!issueId || isEmpty(this.details) || !this.details[issueId]) return undefined;
|
||||
return this.details[issueId];
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves issue from API
|
||||
* @param anchorId ]
|
||||
* @param issueId
|
||||
* @returns
|
||||
*/
|
||||
fetchIssueById = async (anchorId: string, issueId: string) => {
|
||||
try {
|
||||
const issueDetails = await this.issueService.retrieve(anchorId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.details, [issueId], issueDetails);
|
||||
});
|
||||
|
||||
return issueDetails;
|
||||
} catch (e) {
|
||||
console.error(`Error fetching issue details for issueId ${issueId}: `, e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description fetc
|
||||
* @param {string} anchor
|
||||
* @param {string} issueID
|
||||
*/
|
||||
fetchIssueDetails = async (anchor: string, issueID: string) => {
|
||||
try {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
|
||||
const issueDetails = await this.fetchIssueById(anchor, issueID);
|
||||
const commentsResponse = await this.issueService.listComments(anchor, issueID);
|
||||
|
||||
if (issueDetails) {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...(this.details[issueID] ?? issueDetails),
|
||||
comments: commentsResponse,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
}
|
||||
};
|
||||
|
||||
addIssueComment = async (anchor: string, issueID: string, data: any) => {
|
||||
try {
|
||||
const issueDetails = this.getIssueById(issueID);
|
||||
const issueCommentResponse = await this.issueService.addComment(anchor, issueID, data);
|
||||
if (issueDetails) {
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "comments"], [...(issueDetails?.comments ?? []), issueCommentResponse]);
|
||||
});
|
||||
}
|
||||
return issueCommentResponse;
|
||||
} catch (error) {
|
||||
console.log("Failed to add issue comment");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateIssueComment = async (anchor: string, issueID: string, commentID: string, data: any) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
comments: this.details[issueID].comments.map((c) => ({
|
||||
...c,
|
||||
...(c.id === commentID ? data : {}),
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueService.updateComment(anchor, issueID, commentID, data);
|
||||
} catch (_error) {
|
||||
const issueComments = await this.issueService.listComments(anchor, issueID);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
comments: issueComments,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
deleteIssueComment = async (anchor: string, issueID: string, commentID: string) => {
|
||||
try {
|
||||
await this.issueService.removeComment(anchor, issueID, commentID);
|
||||
const remainingComments = this.details[issueID].comments.filter((c) => c.id != commentID);
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
comments: remainingComments,
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (_error) {
|
||||
console.log("Failed to add issue vote");
|
||||
}
|
||||
};
|
||||
|
||||
uploadCommentAsset = async (file: File, anchor: string, commentID?: string) => {
|
||||
try {
|
||||
const res = await this.fileService.uploadAsset(
|
||||
anchor,
|
||||
{
|
||||
entity_identifier: commentID ?? "",
|
||||
entity_type: EFileAssetType.COMMENT_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading comment asset:", error);
|
||||
throw new Error("Asset upload failed. Please try again later.");
|
||||
}
|
||||
};
|
||||
|
||||
uploadIssueAsset = async (file: File, anchor: string, commentID?: string) => {
|
||||
try {
|
||||
const res = await this.fileService.uploadAsset(
|
||||
anchor,
|
||||
{
|
||||
entity_identifier: commentID ?? "",
|
||||
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading comment asset:", error);
|
||||
throw new Error("Asset upload failed. Please try again later.");
|
||||
}
|
||||
};
|
||||
|
||||
addCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => {
|
||||
const newReaction = {
|
||||
id: uuidv4(),
|
||||
comment: commentID,
|
||||
reaction: reactionHex,
|
||||
actor_detail: this.rootStore.user.currentActor,
|
||||
};
|
||||
const newComments = this.details[issueID].comments.map((comment) => ({
|
||||
...comment,
|
||||
comment_reactions:
|
||||
comment.id === commentID ? [...comment.comment_reactions, newReaction] : comment.comment_reactions,
|
||||
}));
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
comments: [...newComments],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueService.addCommentReaction(anchor, commentID, {
|
||||
reaction: reactionHex,
|
||||
});
|
||||
} catch (_error) {
|
||||
const issueComments = await this.issueService.listComments(anchor, issueID);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
comments: issueComments,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => {
|
||||
try {
|
||||
const comment = this.details[issueID].comments.find((c) => c.id === commentID);
|
||||
const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? [];
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
comments: this.details[issueID].comments.map((c) => ({
|
||||
...c,
|
||||
comment_reactions: c.id === commentID ? newCommentReactions : c.comment_reactions,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueService.removeCommentReaction(anchor, commentID, reactionHex);
|
||||
} catch (_error) {
|
||||
const issueComments = await this.issueService.listComments(anchor, issueID);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueID]: {
|
||||
...this.details[issueID],
|
||||
comments: issueComments,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(
|
||||
this.details,
|
||||
[issueID, "reaction_items"],
|
||||
[
|
||||
...this.details[issueID].reaction_items,
|
||||
{
|
||||
reaction: reactionHex,
|
||||
actor_details: this.rootStore.user.currentActor,
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
await this.issueService.addReaction(anchor, issueID, {
|
||||
reaction: reactionHex,
|
||||
});
|
||||
} catch (_error) {
|
||||
console.log("Failed to add issue vote");
|
||||
const issueReactions = await this.issueService.listReactions(anchor, issueID);
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "reaction_items"], issueReactions);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => {
|
||||
try {
|
||||
const newReactions = this.details[issueID].reaction_items.filter(
|
||||
(_r) => !(_r.reaction === reactionHex && _r.actor_details.id === this.rootStore.user.data?.id)
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "reaction_items"], newReactions);
|
||||
});
|
||||
|
||||
await this.issueService.removeReaction(anchor, issueID, reactionHex);
|
||||
} catch (_error) {
|
||||
console.log("Failed to remove issue reaction");
|
||||
const reactions = await this.issueService.listReactions(anchor, issueID);
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "reaction_items"], reactions);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addIssueVote = async (anchor: string, issueID: string, data: { vote: 1 | -1 }) => {
|
||||
const publishSettings = this.rootStore.publishList?.publishMap?.[anchor];
|
||||
const projectID = publishSettings?.project;
|
||||
const workspaceSlug = publishSettings?.workspace_detail?.slug;
|
||||
if (!projectID || !workspaceSlug) throw new Error("Publish settings not found");
|
||||
|
||||
const newVote: IVote = {
|
||||
actor_details: this.rootStore.user.currentActor,
|
||||
vote: data.vote,
|
||||
};
|
||||
|
||||
const filteredVotes = this.details[issueID].vote_items.filter(
|
||||
(v) => v.actor_details?.id !== this.rootStore.user.data?.id
|
||||
);
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "vote_items"], [...filteredVotes, newVote]);
|
||||
});
|
||||
});
|
||||
|
||||
await this.issueService.addVote(anchor, issueID, data);
|
||||
} catch (_error) {
|
||||
console.log("Failed to add issue vote");
|
||||
const issueVotes = await this.issueService.listVotes(anchor, issueID);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "vote_items"], issueVotes);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeIssueVote = async (anchor: string, issueID: string) => {
|
||||
const newVotes = this.details[issueID].vote_items.filter(
|
||||
(v) => v.actor_details?.id !== this.rootStore.user.data?.id
|
||||
);
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "vote_items"], newVotes);
|
||||
});
|
||||
|
||||
await this.issueService.removeVote(anchor, issueID);
|
||||
} catch (_error) {
|
||||
console.log("Failed to remove issue vote");
|
||||
const issueVotes = await this.issueService.listVotes(anchor, issueID);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.details, [issueID, "vote_items"], issueVotes);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
158
apps/space/core/store/issue-filters.store.ts
Normal file
158
apps/space/core/store/issue-filters.store.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { cloneDeep, isEqual, set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane internal
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@plane/constants";
|
||||
import type { IssuePaginationOptions, TIssueParams } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
// types
|
||||
import type {
|
||||
TIssueLayoutOptions,
|
||||
TIssueFilters,
|
||||
TIssueQueryFilters,
|
||||
TIssueQueryFiltersParams,
|
||||
TIssueFilterKeys,
|
||||
} from "@/types/issue";
|
||||
import { getPaginationParams } from "./helpers/filter.helpers";
|
||||
|
||||
export interface IIssueFilterStore {
|
||||
// observables
|
||||
layoutOptions: TIssueLayoutOptions;
|
||||
filters: { [anchor: string]: TIssueFilters } | undefined;
|
||||
// computed
|
||||
isIssueFiltersUpdated: (anchor: string, filters: TIssueFilters) => boolean;
|
||||
// helpers
|
||||
getIssueFilters: (anchor: string) => TIssueFilters | undefined;
|
||||
getAppliedFilters: (anchor: string) => TIssueQueryFiltersParams | undefined;
|
||||
// actions
|
||||
updateLayoutOptions: (layout: TIssueLayoutOptions) => void;
|
||||
initIssueFilters: (anchor: string, filters: TIssueFilters, shouldFetchIssues?: boolean) => void;
|
||||
updateIssueFilters: <K extends keyof TIssueFilters>(
|
||||
anchor: string,
|
||||
filterKind: K,
|
||||
filterKey: keyof TIssueFilters[K],
|
||||
filters: TIssueFilters[K][typeof filterKey]
|
||||
) => Promise<void>;
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
anchor: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
}
|
||||
|
||||
export class IssueFilterStore implements IIssueFilterStore {
|
||||
// observables
|
||||
layoutOptions: TIssueLayoutOptions = {
|
||||
list: true,
|
||||
kanban: false,
|
||||
calendar: false,
|
||||
gantt: false,
|
||||
spreadsheet: false,
|
||||
};
|
||||
filters: { [anchor: string]: TIssueFilters } | undefined = undefined;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
layoutOptions: observable,
|
||||
filters: observable,
|
||||
// actions
|
||||
updateLayoutOptions: action,
|
||||
initIssueFilters: action,
|
||||
updateIssueFilters: action,
|
||||
});
|
||||
}
|
||||
|
||||
// helper methods
|
||||
computedFilter = (filters: TIssueQueryFilters, filteredParams: TIssueFilterKeys[]) => {
|
||||
const computedFilters: TIssueQueryFiltersParams = {};
|
||||
|
||||
Object.keys(filters).map((key) => {
|
||||
const currentFilterKey = key as TIssueFilterKeys;
|
||||
const filterValue = filters[currentFilterKey] as any;
|
||||
|
||||
if (filterValue !== undefined && filteredParams.includes(currentFilterKey)) {
|
||||
if (Array.isArray(filterValue)) computedFilters[currentFilterKey] = filterValue.join(",");
|
||||
else if (typeof filterValue === "string" || typeof filterValue === "boolean")
|
||||
computedFilters[currentFilterKey] = filterValue.toString();
|
||||
}
|
||||
});
|
||||
|
||||
return computedFilters;
|
||||
};
|
||||
|
||||
// computed
|
||||
getIssueFilters = computedFn((anchor: string) => {
|
||||
const currentFilters = this.filters?.[anchor];
|
||||
return currentFilters;
|
||||
});
|
||||
|
||||
getAppliedFilters = computedFn((anchor: string) => {
|
||||
const issueFilters = this.getIssueFilters(anchor);
|
||||
if (!issueFilters) return undefined;
|
||||
|
||||
const currentLayout = issueFilters?.display_filters?.layout;
|
||||
if (!currentLayout) return undefined;
|
||||
|
||||
const currentFilters: TIssueQueryFilters = {
|
||||
priority: issueFilters?.filters?.priority || undefined,
|
||||
state: issueFilters?.filters?.state || undefined,
|
||||
labels: issueFilters?.filters?.labels || undefined,
|
||||
};
|
||||
const filteredParams = ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[currentLayout]?.filters || [];
|
||||
const currentFilterQueryParams: TIssueQueryFiltersParams = this.computedFilter(currentFilters, filteredParams);
|
||||
|
||||
return currentFilterQueryParams;
|
||||
});
|
||||
|
||||
isIssueFiltersUpdated = computedFn((anchor: string, userFilters: TIssueFilters) => {
|
||||
const issueFilters = this.getIssueFilters(anchor);
|
||||
if (!issueFilters) return false;
|
||||
const currentUserFilters = cloneDeep(userFilters?.filters || {});
|
||||
const currentIssueFilters = cloneDeep(issueFilters?.filters || {});
|
||||
return isEqual(currentUserFilters, currentIssueFilters);
|
||||
});
|
||||
|
||||
// actions
|
||||
updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options);
|
||||
|
||||
initIssueFilters = async (anchor: string, initFilters: TIssueFilters, shouldFetchIssues: boolean = false) => {
|
||||
if (this.filters === undefined) runInAction(() => (this.filters = {}));
|
||||
if (this.filters && initFilters) set(this.filters, [anchor], initFilters);
|
||||
|
||||
if (shouldFetchIssues) await this.store.issue.fetchPublicIssuesWithExistingPagination(anchor, "mutation");
|
||||
};
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
anchor: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
const filterParams = this.getAppliedFilters(anchor);
|
||||
const paginationParams = getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
updateIssueFilters = async <K extends keyof TIssueFilters>(
|
||||
anchor: string,
|
||||
filterKind: K,
|
||||
filterKey: keyof TIssueFilters[K],
|
||||
filterValue: TIssueFilters[K][typeof filterKey]
|
||||
) => {
|
||||
if (!filterKind || !filterKey || !filterValue) return;
|
||||
if (this.filters === undefined) runInAction(() => (this.filters = {}));
|
||||
|
||||
runInAction(() => {
|
||||
if (this.filters) set(this.filters, [anchor, filterKind, filterKey], filterValue);
|
||||
});
|
||||
|
||||
if (filterKey !== "layout") await this.store.issue.fetchPublicIssuesWithExistingPagination(anchor, "mutation");
|
||||
};
|
||||
}
|
||||
112
apps/space/core/store/issue.store.ts
Normal file
112
apps/space/core/store/issue.store.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { SitesIssueService } from "@plane/services";
|
||||
import type { IssuePaginationOptions, TLoader } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
// types
|
||||
import { BaseIssuesStore } from "./helpers/base-issues.store";
|
||||
import type { IBaseIssuesStore } from "./helpers/base-issues.store";
|
||||
|
||||
export interface IIssueStore extends IBaseIssuesStore {
|
||||
// actions
|
||||
fetchPublicIssues: (
|
||||
anchor: string,
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
isExistingPaginationOptions?: boolean
|
||||
) => Promise<void>;
|
||||
fetchNextPublicIssues: (anchor: string, groupId?: string, subGroupId?: string) => Promise<void>;
|
||||
fetchPublicIssuesWithExistingPagination: (anchor: string, loadType?: TLoader) => Promise<void>;
|
||||
}
|
||||
|
||||
export class IssueStore extends BaseIssuesStore implements IIssueStore {
|
||||
// root store
|
||||
rootStore: CoreRootStore;
|
||||
// services
|
||||
issueService: SitesIssueService;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
super(_rootStore);
|
||||
makeObservable(this, {
|
||||
// actions
|
||||
fetchPublicIssues: action,
|
||||
fetchNextPublicIssues: action,
|
||||
fetchPublicIssuesWithExistingPagination: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
this.issueService = new SitesIssueService();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description fetch issues, states and labels
|
||||
* @param {string} anchor
|
||||
* @param params
|
||||
*/
|
||||
fetchPublicIssues = async (
|
||||
anchor: string,
|
||||
loadType: TLoader = "init-loader",
|
||||
options: IssuePaginationOptions,
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
});
|
||||
this.clear(!isExistingPaginationOptions);
|
||||
|
||||
const params = this.rootStore.issueFilter.getFilterParams(options, anchor, undefined, undefined, undefined);
|
||||
|
||||
const response = await this.issueService.list(anchor, params);
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options);
|
||||
} catch (error) {
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
fetchNextPublicIssues = async (anchor: string, groupId?: string, subGroupId?: string) => {
|
||||
const cursorObject = this.getPaginationData(groupId, subGroupId);
|
||||
// if there are no pagination options and the next page results do not exist the return
|
||||
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
|
||||
try {
|
||||
// set Loader
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
|
||||
// get params from stored pagination options
|
||||
const params = this.rootStore.issueFilter.getFilterParams(
|
||||
this.paginationOptions,
|
||||
anchor,
|
||||
cursorObject?.nextCursor,
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
// call the fetch issues API with the params for next page in issues
|
||||
const response = await this.issueService.list(anchor, params);
|
||||
|
||||
// after the next page of issues are fetched, call the base method to process the response
|
||||
this.onfetchNexIssues(response, groupId, subGroupId);
|
||||
} catch (error) {
|
||||
// set Loader as undefined if errored out
|
||||
this.setLoader(undefined, groupId, subGroupId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method exists to fetch the first page of the issues with the existing stored pagination
|
||||
* This is useful for refetching when filters, groupBy, orderBy etc changes
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @returns
|
||||
*/
|
||||
fetchPublicIssuesWithExistingPagination = async (anchor: string, loadType: TLoader = "mutation") => {
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchPublicIssues(anchor, loadType, this.paginationOptions, true);
|
||||
};
|
||||
}
|
||||
65
apps/space/core/store/label.store.ts
Normal file
65
apps/space/core/store/label.store.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { SitesLabelService } from "@plane/services";
|
||||
import type { IIssueLabel } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
|
||||
export interface IIssueLabelStore {
|
||||
// observables
|
||||
labels: IIssueLabel[] | undefined;
|
||||
// computed actions
|
||||
getLabelById: (labelId: string | undefined) => IIssueLabel | undefined;
|
||||
getLabelsByIds: (labelIds: string[]) => IIssueLabel[];
|
||||
// fetch actions
|
||||
fetchLabels: (anchor: string) => Promise<IIssueLabel[]>;
|
||||
}
|
||||
|
||||
export class LabelStore implements IIssueLabelStore {
|
||||
labelMap: Record<string, IIssueLabel> = {};
|
||||
labelService: SitesLabelService;
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
labelMap: observable,
|
||||
// computed
|
||||
labels: computed,
|
||||
// fetch action
|
||||
fetchLabels: action,
|
||||
});
|
||||
this.labelService = new SitesLabelService();
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
get labels() {
|
||||
return Object.values(this.labelMap);
|
||||
}
|
||||
|
||||
getLabelById = (labelId: string | undefined) => (labelId ? this.labelMap[labelId] : undefined);
|
||||
|
||||
getLabelsByIds = (labelIds: string[]) => {
|
||||
const currLabels = [];
|
||||
for (const labelId of labelIds) {
|
||||
const label = this.getLabelById(labelId);
|
||||
if (label) {
|
||||
currLabels.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
return currLabels;
|
||||
};
|
||||
|
||||
fetchLabels = async (anchor: string) => {
|
||||
const labelsResponse = await this.labelService.list(anchor);
|
||||
runInAction(() => {
|
||||
this.labelMap = {};
|
||||
for (const label of labelsResponse) {
|
||||
set(this.labelMap, [label.id], label);
|
||||
}
|
||||
});
|
||||
return labelsResponse;
|
||||
};
|
||||
}
|
||||
69
apps/space/core/store/members.store.ts
Normal file
69
apps/space/core/store/members.store.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { SitesMemberService } from "@plane/services";
|
||||
import type { TPublicMember } from "@/types/member";
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
|
||||
export interface IIssueMemberStore {
|
||||
// observables
|
||||
members: TPublicMember[] | undefined;
|
||||
// computed actions
|
||||
getMemberById: (memberId: string | undefined) => TPublicMember | undefined;
|
||||
getMembersByIds: (memberIds: string[]) => TPublicMember[];
|
||||
// fetch actions
|
||||
fetchMembers: (anchor: string) => Promise<TPublicMember[]>;
|
||||
}
|
||||
|
||||
export class MemberStore implements IIssueMemberStore {
|
||||
memberMap: Record<string, TPublicMember> = {};
|
||||
memberService: SitesMemberService;
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
memberMap: observable,
|
||||
// computed
|
||||
members: computed,
|
||||
// fetch action
|
||||
fetchMembers: action,
|
||||
});
|
||||
this.memberService = new SitesMemberService();
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
get members() {
|
||||
return Object.values(this.memberMap);
|
||||
}
|
||||
|
||||
getMemberById = (memberId: string | undefined) => (memberId ? this.memberMap[memberId] : undefined);
|
||||
|
||||
getMembersByIds = (memberIds: string[]) => {
|
||||
const currMembers = [];
|
||||
for (const memberId of memberIds) {
|
||||
const member = this.getMemberById(memberId);
|
||||
if (member) {
|
||||
currMembers.push(member);
|
||||
}
|
||||
}
|
||||
|
||||
return currMembers;
|
||||
};
|
||||
|
||||
fetchMembers = async (anchor: string) => {
|
||||
try {
|
||||
const membersResponse = await this.memberService.list(anchor);
|
||||
runInAction(() => {
|
||||
this.memberMap = {};
|
||||
for (const member of membersResponse) {
|
||||
set(this.memberMap, [member.member], member);
|
||||
}
|
||||
});
|
||||
return membersResponse;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch members:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
}
|
||||
71
apps/space/core/store/module.store.ts
Normal file
71
apps/space/core/store/module.store.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { SitesModuleService } from "@plane/services";
|
||||
// types
|
||||
import type { TPublicModule } from "@/types/modules";
|
||||
// root store
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
|
||||
export interface IIssueModuleStore {
|
||||
// observables
|
||||
modules: TPublicModule[] | undefined;
|
||||
// computed actions
|
||||
getModuleById: (moduleId: string | undefined) => TPublicModule | undefined;
|
||||
getModulesByIds: (moduleIds: string[]) => TPublicModule[];
|
||||
// fetch actions
|
||||
fetchModules: (anchor: string) => Promise<TPublicModule[]>;
|
||||
}
|
||||
|
||||
export class ModuleStore implements IIssueModuleStore {
|
||||
moduleMap: Record<string, TPublicModule> = {};
|
||||
moduleService: SitesModuleService;
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
moduleMap: observable,
|
||||
// computed
|
||||
modules: computed,
|
||||
// fetch action
|
||||
fetchModules: action,
|
||||
});
|
||||
this.moduleService = new SitesModuleService();
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
get modules() {
|
||||
return Object.values(this.moduleMap);
|
||||
}
|
||||
|
||||
getModuleById = (moduleId: string | undefined) => (moduleId ? this.moduleMap[moduleId] : undefined);
|
||||
|
||||
getModulesByIds = (moduleIds: string[]) => {
|
||||
const currModules = [];
|
||||
for (const moduleId of moduleIds) {
|
||||
const issueModule = this.getModuleById(moduleId);
|
||||
if (issueModule) {
|
||||
currModules.push(issueModule);
|
||||
}
|
||||
}
|
||||
|
||||
return currModules;
|
||||
};
|
||||
|
||||
fetchModules = async (anchor: string) => {
|
||||
try {
|
||||
const modulesResponse = await this.moduleService.list(anchor);
|
||||
runInAction(() => {
|
||||
this.moduleMap = {};
|
||||
for (const issueModule of modulesResponse) {
|
||||
set(this.moduleMap, [issueModule.id], issueModule);
|
||||
}
|
||||
});
|
||||
return modulesResponse;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch members:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
}
|
||||
138
apps/space/core/store/profile.store.ts
Normal file
138
apps/space/core/store/profile.store.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { UserService } from "@plane/services";
|
||||
import type { TUserProfile } from "@plane/types";
|
||||
import { EStartOfTheWeek } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
type TError = {
|
||||
status: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface IProfileStore {
|
||||
// observables
|
||||
isLoading: boolean;
|
||||
error: TError | undefined;
|
||||
data: TUserProfile;
|
||||
// actions
|
||||
fetchUserProfile: () => Promise<TUserProfile | undefined>;
|
||||
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
|
||||
}
|
||||
|
||||
export class ProfileStore implements IProfileStore {
|
||||
isLoading: boolean = false;
|
||||
error: TError | undefined = undefined;
|
||||
data: TUserProfile = {
|
||||
id: undefined,
|
||||
user: undefined,
|
||||
role: undefined,
|
||||
last_workspace_id: undefined,
|
||||
theme: {
|
||||
theme: undefined,
|
||||
text: undefined,
|
||||
palette: undefined,
|
||||
primary: undefined,
|
||||
background: undefined,
|
||||
darkPalette: undefined,
|
||||
sidebarText: undefined,
|
||||
sidebarBackground: undefined,
|
||||
},
|
||||
onboarding_step: {
|
||||
workspace_join: false,
|
||||
profile_complete: false,
|
||||
workspace_create: false,
|
||||
workspace_invite: false,
|
||||
},
|
||||
is_onboarded: false,
|
||||
is_tour_completed: false,
|
||||
use_case: undefined,
|
||||
billing_address_country: undefined,
|
||||
billing_address: undefined,
|
||||
has_billing_address: false,
|
||||
has_marketing_email_consent: false,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
language: "",
|
||||
start_of_the_week: EStartOfTheWeek.SUNDAY,
|
||||
};
|
||||
|
||||
// services
|
||||
userService: UserService;
|
||||
|
||||
constructor(public store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
error: observable,
|
||||
data: observable,
|
||||
// actions
|
||||
fetchUserProfile: action,
|
||||
updateUserProfile: action,
|
||||
});
|
||||
// services
|
||||
this.userService = new UserService();
|
||||
}
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description fetches user profile information
|
||||
* @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
fetchUserProfile = async () => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
});
|
||||
const userProfile = await this.userService.profile();
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.data = userProfile;
|
||||
});
|
||||
return userProfile;
|
||||
} catch (_error) {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.error = {
|
||||
status: "user-profile-fetch-error",
|
||||
message: "Failed to fetch user profile",
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updated the user profile information
|
||||
* @param {Partial<TUserProfile>} data
|
||||
* @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
updateUserProfile = async (data: Partial<TUserProfile>) => {
|
||||
const currentUserProfileData = this.data;
|
||||
try {
|
||||
if (currentUserProfileData) {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof TUserProfile = key as keyof TUserProfile;
|
||||
if (this.data) set(this.data, userKey, data[userKey]);
|
||||
});
|
||||
}
|
||||
const userProfile = await this.userService.updateProfile(data);
|
||||
return userProfile;
|
||||
} catch (_error) {
|
||||
if (currentUserProfileData) {
|
||||
Object.keys(currentUserProfileData).forEach((key: string) => {
|
||||
const userKey: keyof TUserProfile = key as keyof TUserProfile;
|
||||
if (this.data) set(this.data, userKey, currentUserProfileData[userKey]);
|
||||
});
|
||||
}
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
status: "user-profile-update-error",
|
||||
message: "Failed to update user profile",
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
117
apps/space/core/store/publish/publish.store.ts
Normal file
117
apps/space/core/store/publish/publish.store.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { observable, makeObservable, computed } from "mobx";
|
||||
// types
|
||||
import type {
|
||||
IWorkspaceLite,
|
||||
TProjectDetails,
|
||||
TPublishEntityType,
|
||||
TProjectPublishSettings,
|
||||
TProjectPublishViewProps,
|
||||
} from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "../root.store";
|
||||
|
||||
export interface IPublishStore extends TProjectPublishSettings {
|
||||
// computed
|
||||
workspaceSlug: string | undefined;
|
||||
canComment: boolean;
|
||||
canReact: boolean;
|
||||
canVote: boolean;
|
||||
}
|
||||
|
||||
export class PublishStore implements IPublishStore {
|
||||
// observables
|
||||
anchor: string | undefined;
|
||||
is_comments_enabled: boolean;
|
||||
created_at: string | undefined;
|
||||
created_by: string | undefined;
|
||||
entity_identifier: string | undefined;
|
||||
entity_name: TPublishEntityType | undefined;
|
||||
id: string | undefined;
|
||||
inbox: unknown;
|
||||
project: string | undefined;
|
||||
project_details: TProjectDetails | undefined;
|
||||
is_reactions_enabled: boolean;
|
||||
updated_at: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
view_props: TProjectPublishViewProps | undefined;
|
||||
is_votes_enabled: boolean;
|
||||
workspace: string | undefined;
|
||||
workspace_detail: IWorkspaceLite | undefined;
|
||||
|
||||
constructor(
|
||||
private store: CoreRootStore,
|
||||
publishSettings: TProjectPublishSettings
|
||||
) {
|
||||
this.anchor = publishSettings.anchor;
|
||||
this.is_comments_enabled = publishSettings.is_comments_enabled;
|
||||
this.created_at = publishSettings.created_at;
|
||||
this.created_by = publishSettings.created_by;
|
||||
this.entity_identifier = publishSettings.entity_identifier;
|
||||
this.entity_name = publishSettings.entity_name;
|
||||
this.id = publishSettings.id;
|
||||
this.inbox = publishSettings.inbox;
|
||||
this.project = publishSettings.project;
|
||||
this.project_details = publishSettings.project_details;
|
||||
this.is_reactions_enabled = publishSettings.is_reactions_enabled;
|
||||
this.updated_at = publishSettings.updated_at;
|
||||
this.updated_by = publishSettings.updated_by;
|
||||
this.view_props = publishSettings.view_props;
|
||||
this.is_votes_enabled = publishSettings.is_votes_enabled;
|
||||
this.workspace = publishSettings.workspace;
|
||||
this.workspace_detail = publishSettings.workspace_detail;
|
||||
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
anchor: observable.ref,
|
||||
is_comments_enabled: observable.ref,
|
||||
created_at: observable.ref,
|
||||
created_by: observable.ref,
|
||||
entity_identifier: observable.ref,
|
||||
entity_name: observable.ref,
|
||||
id: observable.ref,
|
||||
inbox: observable,
|
||||
project: observable.ref,
|
||||
project_details: observable,
|
||||
is_reactions_enabled: observable.ref,
|
||||
updated_at: observable.ref,
|
||||
updated_by: observable.ref,
|
||||
view_props: observable,
|
||||
is_votes_enabled: observable.ref,
|
||||
workspace: observable.ref,
|
||||
workspace_detail: observable,
|
||||
// computed
|
||||
workspaceSlug: computed,
|
||||
canComment: computed,
|
||||
canReact: computed,
|
||||
canVote: computed,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns the workspace slug from the workspace details
|
||||
*/
|
||||
get workspaceSlug() {
|
||||
return this?.workspace_detail?.slug ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns whether commenting is enabled or not
|
||||
*/
|
||||
get canComment() {
|
||||
return !!this.is_comments_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns whether reacting is enabled or not
|
||||
*/
|
||||
get canReact() {
|
||||
return !!this.is_reactions_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns whether voting is enabled or not
|
||||
*/
|
||||
get canVote() {
|
||||
return !!this.is_votes_enabled;
|
||||
}
|
||||
}
|
||||
47
apps/space/core/store/publish/publish_list.store.ts
Normal file
47
apps/space/core/store/publish/publish_list.store.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { set } from "lodash-es";
|
||||
import { makeObservable, observable, runInAction, action } from "mobx";
|
||||
// plane imports
|
||||
import { SitesProjectPublishService } from "@plane/services";
|
||||
import type { TProjectPublishSettings } from "@plane/types";
|
||||
// store
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
export interface IPublishListStore {
|
||||
// observables
|
||||
publishMap: Record<string, PublishStore>; // anchor => PublishStore
|
||||
// actions
|
||||
fetchPublishSettings: (pageId: string) => Promise<TProjectPublishSettings>;
|
||||
}
|
||||
|
||||
export class PublishListStore implements IPublishListStore {
|
||||
// observables
|
||||
publishMap: Record<string, PublishStore> = {}; // anchor => PublishStore
|
||||
// service
|
||||
publishService;
|
||||
|
||||
constructor(private rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
publishMap: observable,
|
||||
// actions
|
||||
fetchPublishSettings: action,
|
||||
});
|
||||
// services
|
||||
this.publishService = new SitesProjectPublishService();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description fetch publish settings
|
||||
* @param {string} anchor
|
||||
*/
|
||||
fetchPublishSettings = async (anchor: string) => {
|
||||
const response = await this.publishService.retrieveSettingsByAnchor(anchor);
|
||||
runInAction(() => {
|
||||
if (response.anchor) {
|
||||
set(this.publishMap, [response.anchor], new PublishStore(this.rootStore, response));
|
||||
}
|
||||
});
|
||||
return response;
|
||||
};
|
||||
}
|
||||
76
apps/space/core/store/root.store.ts
Normal file
76
apps/space/core/store/root.store.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { enableStaticRendering } from "mobx-react";
|
||||
// store imports
|
||||
import type { IInstanceStore } from "@/store/instance.store";
|
||||
import { InstanceStore } from "@/store/instance.store";
|
||||
import type { IIssueDetailStore } from "@/store/issue-detail.store";
|
||||
import { IssueDetailStore } from "@/store/issue-detail.store";
|
||||
import type { IIssueStore } from "@/store/issue.store";
|
||||
import { IssueStore } from "@/store/issue.store";
|
||||
import type { IUserStore } from "@/store/user.store";
|
||||
import { UserStore } from "@/store/user.store";
|
||||
import type { ICycleStore } from "./cycle.store";
|
||||
import { CycleStore } from "./cycle.store";
|
||||
import type { IIssueFilterStore } from "./issue-filters.store";
|
||||
import { IssueFilterStore } from "./issue-filters.store";
|
||||
import type { IIssueLabelStore } from "./label.store";
|
||||
import { LabelStore } from "./label.store";
|
||||
import type { IIssueMemberStore } from "./members.store";
|
||||
import { MemberStore } from "./members.store";
|
||||
import type { IIssueModuleStore } from "./module.store";
|
||||
import { ModuleStore } from "./module.store";
|
||||
import type { IPublishListStore } from "./publish/publish_list.store";
|
||||
import { PublishListStore } from "./publish/publish_list.store";
|
||||
import type { IStateStore } from "./state.store";
|
||||
import { StateStore } from "./state.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
export class CoreRootStore {
|
||||
instance: IInstanceStore;
|
||||
user: IUserStore;
|
||||
issue: IIssueStore;
|
||||
issueDetail: IIssueDetailStore;
|
||||
state: IStateStore;
|
||||
label: IIssueLabelStore;
|
||||
module: IIssueModuleStore;
|
||||
member: IIssueMemberStore;
|
||||
cycle: ICycleStore;
|
||||
issueFilter: IIssueFilterStore;
|
||||
publishList: IPublishListStore;
|
||||
|
||||
constructor() {
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.issue = new IssueStore(this);
|
||||
this.issueDetail = new IssueDetailStore(this);
|
||||
this.state = new StateStore(this);
|
||||
this.label = new LabelStore(this);
|
||||
this.module = new ModuleStore(this);
|
||||
this.member = new MemberStore(this);
|
||||
this.cycle = new CycleStore(this);
|
||||
this.issueFilter = new IssueFilterStore(this);
|
||||
this.publishList = new PublishListStore(this);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
hydrate = (data: any) => {
|
||||
if (!data) return;
|
||||
this.instance.hydrate(data?.instance || undefined);
|
||||
this.user.hydrate(data?.user || undefined);
|
||||
};
|
||||
|
||||
reset() {
|
||||
localStorage.setItem("theme", "system");
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.issue = new IssueStore(this);
|
||||
this.issueDetail = new IssueDetailStore(this);
|
||||
this.state = new StateStore(this);
|
||||
this.label = new LabelStore(this);
|
||||
this.module = new ModuleStore(this);
|
||||
this.member = new MemberStore(this);
|
||||
this.cycle = new CycleStore(this);
|
||||
this.issueFilter = new IssueFilterStore(this);
|
||||
this.publishList = new PublishListStore(this);
|
||||
}
|
||||
}
|
||||
54
apps/space/core/store/state.store.ts
Normal file
54
apps/space/core/store/state.store.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { clone } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { SitesStateService } from "@plane/services";
|
||||
import type { IState } from "@plane/types";
|
||||
// helpers
|
||||
import { sortStates } from "@/helpers/state.helper";
|
||||
// store
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
|
||||
export interface IStateStore {
|
||||
// observables
|
||||
states: IState[] | undefined;
|
||||
//computed
|
||||
sortedStates: IState[] | undefined;
|
||||
// computed actions
|
||||
getStateById: (stateId: string | undefined) => IState | undefined;
|
||||
// fetch actions
|
||||
fetchStates: (anchor: string) => Promise<IState[]>;
|
||||
}
|
||||
|
||||
export class StateStore implements IStateStore {
|
||||
states: IState[] | undefined = undefined;
|
||||
stateService: SitesStateService;
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
states: observable,
|
||||
// computed
|
||||
sortedStates: computed,
|
||||
// fetch action
|
||||
fetchStates: action,
|
||||
});
|
||||
this.stateService = new SitesStateService();
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
get sortedStates() {
|
||||
if (!this.states) return;
|
||||
return sortStates(clone(this.states));
|
||||
}
|
||||
|
||||
getStateById = (stateId: string | undefined) => this.states?.find((state) => state.id === stateId);
|
||||
|
||||
fetchStates = async (anchor: string) => {
|
||||
const statesResponse = await this.stateService.list(anchor);
|
||||
runInAction(() => {
|
||||
this.states = statesResponse;
|
||||
});
|
||||
return statesResponse;
|
||||
};
|
||||
}
|
||||
184
apps/space/core/store/user.store.ts
Normal file
184
apps/space/core/store/user.store.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { set } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { UserService } from "@plane/services";
|
||||
import type { ActorDetail, IUser } from "@plane/types";
|
||||
// store types
|
||||
import type { IProfileStore } from "@/store/profile.store";
|
||||
import { ProfileStore } from "@/store/profile.store";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
type TUserErrorStatus = {
|
||||
status: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface IUserStore {
|
||||
// observables
|
||||
isAuthenticated: boolean;
|
||||
isInitializing: boolean;
|
||||
error: TUserErrorStatus | undefined;
|
||||
data: IUser | undefined;
|
||||
// store observables
|
||||
profile: IProfileStore;
|
||||
// computed
|
||||
currentActor: ActorDetail;
|
||||
// actions
|
||||
fetchCurrentUser: () => Promise<IUser | undefined>;
|
||||
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser | undefined>;
|
||||
hydrate: (data: IUser | undefined) => void;
|
||||
reset: () => void;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class UserStore implements IUserStore {
|
||||
// observables
|
||||
isAuthenticated: boolean = false;
|
||||
isInitializing: boolean = true;
|
||||
error: TUserErrorStatus | undefined = undefined;
|
||||
data: IUser | undefined = undefined;
|
||||
// store observables
|
||||
profile: IProfileStore;
|
||||
// service
|
||||
userService: UserService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
// stores
|
||||
this.profile = new ProfileStore(store);
|
||||
// service
|
||||
this.userService = new UserService();
|
||||
// observables
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isAuthenticated: observable.ref,
|
||||
isInitializing: observable.ref,
|
||||
error: observable,
|
||||
// model observables
|
||||
data: observable,
|
||||
profile: observable,
|
||||
// computed
|
||||
currentActor: computed,
|
||||
// actions
|
||||
fetchCurrentUser: action,
|
||||
updateCurrentUser: action,
|
||||
reset: action,
|
||||
signOut: action,
|
||||
});
|
||||
}
|
||||
|
||||
// computed
|
||||
get currentActor(): ActorDetail {
|
||||
return {
|
||||
id: this.data?.id,
|
||||
first_name: this.data?.first_name,
|
||||
last_name: this.data?.last_name,
|
||||
display_name: this.data?.display_name,
|
||||
avatar_url: this.data?.avatar_url || undefined,
|
||||
is_bot: false,
|
||||
};
|
||||
}
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description fetches the current user
|
||||
* @returns {Promise<IUser>}
|
||||
*/
|
||||
fetchCurrentUser = async (): Promise<IUser> => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
if (this.data === undefined && !this.error) this.isInitializing = true;
|
||||
this.error = undefined;
|
||||
});
|
||||
const user = await this.userService.me();
|
||||
if (user && user?.id) {
|
||||
await this.profile.fetchUserProfile();
|
||||
runInAction(() => {
|
||||
this.data = user;
|
||||
this.isInitializing = false;
|
||||
this.isAuthenticated = true;
|
||||
});
|
||||
} else
|
||||
runInAction(() => {
|
||||
this.data = user;
|
||||
this.isInitializing = false;
|
||||
this.isAuthenticated = false;
|
||||
});
|
||||
return user;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.isInitializing = false;
|
||||
this.isAuthenticated = false;
|
||||
this.error = {
|
||||
status: "user-fetch-error",
|
||||
message: "Failed to fetch current user",
|
||||
};
|
||||
if (error instanceof AxiosError && error.status === 401) {
|
||||
this.data = undefined;
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updates the current user
|
||||
* @param data
|
||||
* @returns {Promise<IUser>}
|
||||
*/
|
||||
updateCurrentUser = async (data: Partial<IUser>): Promise<IUser> => {
|
||||
const currentUserData = this.data;
|
||||
try {
|
||||
if (currentUserData) {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof IUser = key as keyof IUser;
|
||||
if (this.data) set(this.data, userKey, data[userKey]);
|
||||
});
|
||||
}
|
||||
const user = await this.userService.update(data);
|
||||
return user;
|
||||
} catch (error) {
|
||||
if (currentUserData) {
|
||||
Object.keys(currentUserData).forEach((key: string) => {
|
||||
const userKey: keyof IUser = key as keyof IUser;
|
||||
if (this.data) set(this.data, userKey, currentUserData[userKey]);
|
||||
});
|
||||
}
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
status: "user-update-error",
|
||||
message: "Failed to update current user",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
hydrate = (data: IUser | undefined): void => {
|
||||
if (!data) return;
|
||||
this.data = { ...this.data, ...data };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description resets the user store
|
||||
* @returns {void}
|
||||
*/
|
||||
reset = (): void => {
|
||||
runInAction(() => {
|
||||
this.isAuthenticated = false;
|
||||
this.isInitializing = false;
|
||||
this.error = undefined;
|
||||
this.data = undefined;
|
||||
this.profile = new ProfileStore(this.store);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description signs out the current user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
signOut = async (): Promise<void> => {
|
||||
this.store.reset();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user