fix: project loaders for mobx store (#3356)

* add loaders to all the dropdowns outside project wrpper

* fix build errors

* minor refactor for project states color

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
This commit is contained in:
rahulramesha 2024-01-12 13:51:00 +05:30 committed by sriram veeraghanta
parent 151c355177
commit 9789068880
11 changed files with 112 additions and 53 deletions

View File

@ -464,7 +464,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
{
issueIds?.filter(
(issueId) =>
getProjectStates(issueMap[issueId]?.project_id).find(
getProjectStates(issueMap[issueId]?.project_id)?.find(
(issue) => issue.id === issueMap[issueId]?.state_id
)?.group === "completed"
)?.length

View File

@ -129,34 +129,37 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select issue</h2>
)}
<ul className="text-sm text-custom-text-100">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
as="div"
value={issue.id}
className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
} `
}
>
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor:
getProjectStates(issue?.project_id)?.find(
(state) => state?.id == issue?.state_id
)?.color || "",
}}
/>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
</span>
<span className="text-custom-text-200">{issue.name}</span>
</div>
</Combobox.Option>
))}
{filteredIssues.map((issue) => {
const stateColor =
getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)
?.color || "";
return (
<Combobox.Option
key={issue.id}
as="div"
value={issue.id}
className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
} `
}
>
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: stateColor,
}}
/>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
</span>
<span className="text-custom-text-200">{issue.name}</span>
</div>
</Combobox.Option>
);
})}
</ul>
</li>
) : (

View File

@ -59,6 +59,10 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
if (!issues?.[issueId]) return null;
const issue = issues?.[issueId];
const stateColor =
getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || "";
return (
<Draggable key={issue.id} draggableId={issue.id} index={index}>
{(provided, snapshot) => (
@ -90,9 +94,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
<span
className="h-full w-0.5 flex-shrink-0 rounded"
style={{
backgroundColor: getProjectStates(issue?.project_id).find(
(state) => state?.id == issue?.state_id
)?.color,
backgroundColor: stateColor,
}}
/>
<div className="flex-shrink-0 text-xs text-custom-text-300">

View File

@ -22,11 +22,13 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
data.id &&
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
const stateColor = getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id)?.color || "";
return (
<div
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={{
backgroundColor: getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id)?.color,
backgroundColor: stateColor,
}}
onClick={handleIssuePeekOverview}
>

View File

@ -30,7 +30,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const { workspaceSlug } = router.query;
// store hooks
const {
project: { projectLabels, fetchProjectLabels },
project: { getProjectLabels, fetchProjectLabels },
} = useLabel();
// states
const [query, setQuery] = useState("");
@ -43,6 +43,9 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-start",
});
const projectLabels = getProjectLabels(projectId);
// derived values
const filteredOptions =
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));

View File

@ -12,6 +12,8 @@ import { IssueService } from "services/issue";
import { CycleService } from "services/cycle.service";
export interface ICycleStore {
//Loaders
fetchedMap: Record<string, boolean>;
// observables
cycleMap: Record<string, ICycle>;
activeCycleIdMap: Record<string, boolean>;
@ -50,6 +52,8 @@ export class CycleStore implements ICycleStore {
// observables
cycleMap: Record<string, ICycle> = {};
activeCycleIdMap: Record<string, boolean> = {};
//loaders
fetchedMap: Record<string, boolean> = {};
// root store
rootStore;
// services
@ -62,6 +66,7 @@ export class CycleStore implements ICycleStore {
// observables
cycleMap: observable,
activeCycleIdMap: observable,
fetchedMap: observable,
// computed
currentProjectCycleIds: computed,
currentProjectCompletedCycleIds: computed,
@ -96,11 +101,11 @@ export class CycleStore implements ICycleStore {
*/
get currentProjectCycleIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
if (!projectId || !this.fetchedMap[projectId]) return null;
let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project === projectId);
allCycles = sortBy(allCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
const allCycleIds = allCycles.map((c) => c.id);
return allCycleIds || null;
return allCycleIds;
}
/**
@ -108,14 +113,14 @@ export class CycleStore implements ICycleStore {
*/
get currentProjectCompletedCycleIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
if (!projectId || !this.fetchedMap[projectId]) return null;
let completedCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
return c.project === projectId && hasEndDatePassed;
});
completedCycles = sortBy(completedCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
const completedCycleIds = completedCycles.map((c) => c.id);
return completedCycleIds || null;
return completedCycleIds;
}
/**
@ -123,14 +128,14 @@ export class CycleStore implements ICycleStore {
*/
get currentProjectUpcomingCycleIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
if (!projectId || !this.fetchedMap[projectId]) return null;
let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
return c.project === projectId && isStartDateUpcoming;
});
upcomingCycles = sortBy(upcomingCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
const upcomingCycleIds = upcomingCycles.map((c) => c.id);
return upcomingCycleIds || null;
return upcomingCycleIds;
}
/**
@ -138,14 +143,14 @@ export class CycleStore implements ICycleStore {
*/
get currentProjectIncompleteCycleIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
if (!projectId || !this.fetchedMap[projectId]) return null;
let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
return c.project === projectId && !hasEndDatePassed;
});
incompleteCycles = sortBy(incompleteCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
const incompleteCycleIds = incompleteCycles.map((c) => c.id);
return incompleteCycleIds || null;
return incompleteCycleIds;
}
/**
@ -153,13 +158,13 @@ export class CycleStore implements ICycleStore {
*/
get currentProjectDraftCycleIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
if (!projectId || !this.fetchedMap[projectId]) return null;
let draftCycles = Object.values(this.cycleMap ?? {}).filter(
(c) => c.project === projectId && !c.start_date && !c.end_date
);
draftCycles = sortBy(draftCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
const draftCycleIds = draftCycles.map((c) => c.id);
return draftCycleIds || null;
return draftCycleIds;
}
/**
@ -194,6 +199,8 @@ export class CycleStore implements ICycleStore {
* @param projectId
*/
getProjectCycleIds = (projectId: string): string[] | null => {
if (!this.fetchedMap[projectId]) return null;
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId);
cycles = sortBy(cycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
const cycleIds = cycles.map((c) => c.id);
@ -222,6 +229,7 @@ export class CycleStore implements ICycleStore {
response.forEach((cycle) => {
set(this.cycleMap, [cycle.id], cycle);
});
set(this.fetchedMap, projectId, true);
});
return response;
});

View File

@ -1,4 +1,4 @@
import { action, computed, makeObservable, runInAction } from "mobx";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import set from "lodash/set";
// services
import { IssueLabelService } from "services/issue";
@ -10,9 +10,13 @@ import { IIssueLabel, IIssueLabelTree } from "@plane/types";
import { ILabelRootStore } from "store/label";
export interface IProjectLabelStore {
//Loaders
fetchedMap: Record<string, boolean>;
// computed
projectLabels: IIssueLabel[] | undefined;
projectLabelsTree: IIssueLabelTree[] | undefined;
//computed actions
getProjectLabels: (projectId: string) => IIssueLabel[] | undefined;
// fetch actions
fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise<IIssueLabel[]>;
// crud actions
@ -40,15 +44,21 @@ export class ProjectLabelStore implements IProjectLabelStore {
rootStore;
// root store labelMap
labelMap: Record<string, IIssueLabel> = {};
//loaders
fetchedMap: Record<string, boolean> = {};
// services
issueLabelService;
constructor(_labelRoot: ILabelRootStore, _rootStore: RootStore) {
makeObservable(this, {
labelMap: observable,
fetchedMap: observable,
// computed
projectLabels: computed,
projectLabelsTree: computed,
// actions
getProjectLabels: action,
fetchProjectLabels: action,
createLabel: action,
updateLabel: action,
@ -68,7 +78,7 @@ export class ProjectLabelStore implements IProjectLabelStore {
*/
get projectLabels() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.labelMap) return;
if (!projectId || !this.fetchedMap[projectId] || !this.labelMap) return;
return Object.values(this.labelMap ?? {}).filter((label) => label.project === projectId);
}
@ -80,6 +90,11 @@ export class ProjectLabelStore implements IProjectLabelStore {
return buildTree(this.projectLabels);
}
getProjectLabels = (projectId: string) => {
if (!this.fetchedMap[projectId] || !this.labelMap) return;
return Object.values(this.labelMap ?? {}).filter((label) => label.project === projectId);
};
/**
* Fetches all the labelMap belongs to a specific project
* @param workspaceSlug
@ -92,6 +107,7 @@ export class ProjectLabelStore implements IProjectLabelStore {
response.forEach((label) => {
set(this.labelMap, [label.id], label);
});
set(this.fetchedMap, projectId, true);
});
return response;
});

View File

@ -120,7 +120,8 @@ export class ProjectMemberStore implements IProjectMemberStore {
* @param projectId
*/
getProjectMemberIds = (projectId: string): string[] | null => {
let members = Object.values(this.projectMemberMap?.[projectId] ?? {});
if (!this.projectMemberMap?.[projectId]) return null;
let members = Object.values(this.projectMemberMap?.[projectId]);
members = sortBy(members, [
(m) => m.member !== this.userStore.currentUser?.id,
(m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(),

View File

@ -9,6 +9,8 @@ import { IModule, ILinkDetails } from "@plane/types";
import { RootStore } from "store/root.store";
export interface IModuleStore {
//Loaders
fetchedMap: Record<string, boolean>;
// observables
moduleMap: Record<string, IModule>;
// computed
@ -51,6 +53,8 @@ export interface IModuleStore {
export class ModulesStore implements IModuleStore {
// observables
moduleMap: Record<string, IModule> = {};
//loaders
fetchedMap: Record<string, boolean> = {};
// root store
rootStore;
// services
@ -61,6 +65,7 @@ export class ModulesStore implements IModuleStore {
makeObservable(this, {
// observables
moduleMap: observable,
fetchedMap: observable,
// computed
projectModuleIds: computed,
// computed actions
@ -92,7 +97,7 @@ export class ModulesStore implements IModuleStore {
*/
get projectModuleIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
if (!projectId || !this.fetchedMap[projectId]) return null;
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
projectModules = sortBy(projectModules, [(m) => !m.is_favorite, (m) => m.name.toLowerCase()]);
const projectModuleIds = projectModules.map((m) => m.id);
@ -111,10 +116,12 @@ export class ModulesStore implements IModuleStore {
* @param projectId
*/
getProjectModuleIds = (projectId: string) => {
if (!this.fetchedMap[projectId]) return null;
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
projectModules = sortBy(projectModules, [(m) => !m.is_favorite, (m) => m.name.toLowerCase()]);
const projectModuleIds = projectModules.map((m) => m.id);
return projectModuleIds || null;
return projectModuleIds;
};
/**
@ -129,6 +136,7 @@ export class ModulesStore implements IModuleStore {
response.forEach((module) => {
set(this.moduleMap, [module.id], { ...this.moduleMap[module.id], ...module });
});
set(this.fetchedMap, projectId, true);
});
return response;
});

View File

@ -7,6 +7,8 @@ import { RootStore } from "store/root.store";
import { IProjectView } from "@plane/types";
export interface IProjectViewStore {
//Loaders
fetchedMap: Record<string, boolean>;
// observables
viewMap: Record<string, IProjectView>;
// computed
@ -33,6 +35,8 @@ export interface IProjectViewStore {
export class ProjectViewStore implements IProjectViewStore {
// observables
viewMap: Record<string, IProjectView> = {};
//loaders
fetchedMap: Record<string, boolean> = {};
// root store
rootStore;
// services
@ -42,6 +46,7 @@ export class ProjectViewStore implements IProjectViewStore {
makeObservable(this, {
// observables
viewMap: observable,
fetchedMap: observable,
// computed
projectViewIds: computed,
// computed actions
@ -68,7 +73,7 @@ export class ProjectViewStore implements IProjectViewStore {
*/
get projectViewIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
if (!projectId || !this.fetchedMap[projectId]) return null;
const viewIds = Object.keys(this.viewMap ?? {})?.filter((viewId) => this.viewMap?.[viewId]?.project === projectId);
return viewIds;
}
@ -90,6 +95,7 @@ export class ProjectViewStore implements IProjectViewStore {
response.forEach((view) => {
set(this.viewMap, [view.id], view);
});
set(this.fetchedMap, projectId, true);
});
return response;
});

View File

@ -9,6 +9,8 @@ import { IState } from "@plane/types";
import { ProjectStateService } from "services/project";
export interface IStateStore {
//Loaders
fetchedMap: Record<string, boolean>;
// observables
stateMap: Record<string, IState>;
// computed
@ -16,7 +18,7 @@ export interface IStateStore {
groupedProjectStates: Record<string, IState[]> | undefined;
// computed actions
getStateById: (stateId: string) => IState | undefined;
getProjectStates: (projectId: string) => IState[];
getProjectStates: (projectId: string) => IState[] | undefined;
// fetch actions
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<IState[]>;
// crud actions
@ -40,6 +42,8 @@ export interface IStateStore {
export class StateStore implements IStateStore {
stateMap: Record<string, IState> = {};
//loaders
fetchedMap: Record<string, boolean> = {};
router;
stateService;
@ -47,6 +51,7 @@ export class StateStore implements IStateStore {
makeObservable(this, {
// observables
stateMap: observable,
fetchedMap: observable,
// computed
projectStates: computed,
groupedProjectStates: computed,
@ -71,7 +76,8 @@ export class StateStore implements IStateStore {
* Returns the stateMap belongs to a specific project
*/
get projectStates() {
if (!this.router.query?.projectId) return;
const projectId = this.router.query?.projectId?.toString();
if (!projectId || !this.fetchedMap[projectId]) return;
return Object.values(this.stateMap).filter((state) => state.project === this.router.query.projectId);
}
@ -97,7 +103,10 @@ export class StateStore implements IStateStore {
* @param projectId
* @returns IState[]
*/
getProjectStates = (projectId: string) => Object.values(this.stateMap).filter((state) => state.project === projectId);
getProjectStates = (projectId: string) => {
if (!projectId || !this.fetchedMap[projectId]) return;
return Object.values(this.stateMap).filter((state) => state.project === projectId);
};
/**
* fetches the stateMap of a project
@ -111,6 +120,7 @@ export class StateStore implements IStateStore {
statesResponse.forEach((state) => {
set(this.stateMap, [state.id], state);
});
set(this.fetchedMap, projectId, true);
});
return statesResponse;
};