refactor: quick add (#2541)
* refactor: store and helper setup for quick-add * refactor: kanban quick add with optimistic issue create * refactor: added function definition * refactor: list quick add with optimistic issue create * refactor: spreadsheet quick add with optimistic issue create * refactor: calender quick add with optimistic issue create * refactor: gantt quick add with optimistic issue create * refactor: input component and pre-loading data logic * style: calender quick-add height and content shift * feat: sub-group quick-add issue * feat: displaying loading state when issue is being created * fix: setting string null to null
This commit is contained in:
parent
d95ea463b2
commit
4aad35e007
35 changed files with 2734 additions and 951 deletions
|
|
@ -4,3 +4,4 @@ export * from "./issue_filters.store";
|
|||
export * from "./issue_kanban_view.store";
|
||||
export * from "./issue_calendar_view.store";
|
||||
export * from "./issue.store";
|
||||
export * from "./issue_quick_add.store";
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export interface IIssueStore {
|
|||
// action
|
||||
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
||||
removeIssueFromStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
||||
deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
||||
updateGanttIssueStructure: (workspaceSlug: string, issue: IIssue, payload: IBlockUpdateData) => void;
|
||||
}
|
||||
|
|
@ -70,6 +71,7 @@ export class IssueStore implements IIssueStore {
|
|||
// actions
|
||||
fetchIssues: action,
|
||||
updateIssueStructure: action,
|
||||
removeIssueFromStructure: action,
|
||||
deleteIssue: action,
|
||||
updateGanttIssueStructure: action,
|
||||
});
|
||||
|
|
@ -129,24 +131,33 @@ export class IssueStore implements IIssueStore {
|
|||
|
||||
if (issueType === "grouped" && group_id) {
|
||||
issues = issues as IIssueGroupedStructure;
|
||||
const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.id === issue.id);
|
||||
issues = {
|
||||
...issues,
|
||||
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
|
||||
[group_id]: _currentIssueId
|
||||
? issues[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i))
|
||||
: [...(issues?.[group_id] ?? []), issue],
|
||||
};
|
||||
}
|
||||
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
||||
issues = issues as IIssueGroupWithSubGroupsStructure;
|
||||
const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.id === issue.id);
|
||||
issues = {
|
||||
...issues,
|
||||
[sub_group_id]: {
|
||||
...issues[sub_group_id],
|
||||
[group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
|
||||
[group_id]: _currentIssueId
|
||||
? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i))
|
||||
: [...(issues?.[sub_group_id]?.[group_id] ?? []), issue],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (issueType === "ungrouped") {
|
||||
issues = issues as IIssueUnGroupedStructure;
|
||||
issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i));
|
||||
const _currentIssueId = issues?.find((_i) => _i?.id === issue.id);
|
||||
issues = _currentIssueId
|
||||
? issues?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i))
|
||||
: [...(issues ?? []), issue];
|
||||
}
|
||||
|
||||
const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || "";
|
||||
|
|
@ -168,6 +179,43 @@ export class IssueStore implements IIssueStore {
|
|||
});
|
||||
};
|
||||
|
||||
removeIssueFromStructure = (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
||||
const projectId: string | null = issue?.project;
|
||||
const issueType = this.getIssueType;
|
||||
|
||||
if (!projectId || !issueType) return null;
|
||||
|
||||
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
|
||||
this.getIssues;
|
||||
if (!issues) return null;
|
||||
|
||||
if (issueType === "grouped" && group_id) {
|
||||
issues = issues as IIssueGroupedStructure;
|
||||
issues = {
|
||||
...issues,
|
||||
[group_id]: (issues[group_id] ?? []).filter((i) => i?.id !== issue?.id),
|
||||
};
|
||||
}
|
||||
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
||||
issues = issues as IIssueGroupWithSubGroupsStructure;
|
||||
issues = {
|
||||
...issues,
|
||||
[sub_group_id]: {
|
||||
...issues[sub_group_id],
|
||||
[group_id]: (issues[sub_group_id]?.[group_id] ?? []).filter((i) => i?.id !== issue?.id),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (issueType === "ungrouped") {
|
||||
issues = issues as IIssueUnGroupedStructure;
|
||||
issues = issues.filter((i) => i?.id !== issue?.id);
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } };
|
||||
});
|
||||
};
|
||||
|
||||
updateGanttIssueStructure = async (workspaceSlug: string, issue: IIssue, payload: IBlockUpdateData) => {
|
||||
if (!issue || !workspaceSlug) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { RootStore } from "../root";
|
|||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { groupReactionEmojis } from "constants/issue";
|
||||
// uuid
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export interface IIssueDetailStore {
|
||||
loader: boolean;
|
||||
|
|
@ -39,6 +41,7 @@ export interface IIssueDetailStore {
|
|||
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
||||
// creating issue
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
|
||||
optimisticallyCreateIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
|
||||
// updating issue
|
||||
updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>) => Promise<IIssue>;
|
||||
// deleting issue
|
||||
|
|
@ -129,6 +132,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
|
||||
fetchIssueDetails: action,
|
||||
createIssue: action,
|
||||
optimisticallyCreateIssue: action,
|
||||
updateIssue: action,
|
||||
deleteIssue: action,
|
||||
|
||||
|
|
@ -208,6 +212,44 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||
}
|
||||
};
|
||||
|
||||
optimisticallyCreateIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => {
|
||||
const tempId = data?.id || uuidv4();
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
this.issues = {
|
||||
...this.issues,
|
||||
[tempId]: data as IIssue,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.issueService.createIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
data,
|
||||
this.rootStore.user.currentUser!
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
this.issues = {
|
||||
...this.issues,
|
||||
[response.id]: response,
|
||||
};
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
createIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
|
|
|
|||
227
web/store/issue/issue_quick_add.store.ts
Normal file
227
web/store/issue/issue_quick_add.store.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// types
|
||||
import { RootStore } from "../root";
|
||||
import { IIssue } from "types";
|
||||
// uuid
|
||||
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
|
||||
import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "./issue.store";
|
||||
|
||||
export interface IIssueQuickAddStore {
|
||||
loader: boolean;
|
||||
error: any | null;
|
||||
|
||||
createIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
grouping: {
|
||||
group_id: string | null;
|
||||
sub_group_id: string | null;
|
||||
},
|
||||
data: Partial<IIssue>
|
||||
) => Promise<IIssue>;
|
||||
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
||||
updateQuickAddIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
||||
}
|
||||
|
||||
export class IssueQuickAddStore implements IIssueQuickAddStore {
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
// service
|
||||
issueService;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
loader: observable.ref,
|
||||
error: observable.ref,
|
||||
|
||||
createIssue: action,
|
||||
updateIssueStructure: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
this.issueService = new IssueService();
|
||||
}
|
||||
|
||||
createIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
grouping: {
|
||||
group_id: string | null;
|
||||
sub_group_id: string | null;
|
||||
},
|
||||
data: Partial<IIssue>
|
||||
) => {
|
||||
runInAction(() => {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
const { group_id, sub_group_id } = grouping;
|
||||
|
||||
try {
|
||||
this.updateIssueStructure(group_id, sub_group_id, data as IIssue);
|
||||
|
||||
const response = await this.issueService.createIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
data,
|
||||
this.rootStore.user.currentUser!
|
||||
);
|
||||
|
||||
this.updateQuickAddIssueStructure(group_id, sub_group_id, {
|
||||
...data,
|
||||
...response,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
||||
const projectId: string | null = issue?.project;
|
||||
const issueType = this.rootStore.issue.getIssueType;
|
||||
if (!projectId || !issueType) return null;
|
||||
|
||||
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
|
||||
this.rootStore.issue.getIssues;
|
||||
if (!issues) return null;
|
||||
|
||||
if (group_id === "null") group_id = null;
|
||||
if (sub_group_id === "null") sub_group_id = null;
|
||||
|
||||
if (issueType === "grouped" && group_id) {
|
||||
issues = issues as IIssueGroupedStructure;
|
||||
const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.id === issue.id);
|
||||
issues = {
|
||||
...issues,
|
||||
[group_id]: _currentIssueId
|
||||
? issues[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i))
|
||||
: [...(issues?.[group_id] ?? []), issue],
|
||||
};
|
||||
}
|
||||
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
||||
issues = issues as IIssueGroupWithSubGroupsStructure;
|
||||
const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.id === issue.id);
|
||||
issues = {
|
||||
...issues,
|
||||
[sub_group_id]: {
|
||||
...issues[sub_group_id],
|
||||
[group_id]: _currentIssueId
|
||||
? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i))
|
||||
: [...(issues?.[sub_group_id]?.[group_id] ?? []), issue],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (issueType === "ungrouped") {
|
||||
issues = issues as IIssueUnGroupedStructure;
|
||||
const _currentIssueId = issues?.find((_i) => _i?.id === issue.id);
|
||||
issues = _currentIssueId
|
||||
? issues?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i))
|
||||
: [...(issues ?? []), issue];
|
||||
}
|
||||
|
||||
const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || "";
|
||||
if (orderBy === "-created_at") {
|
||||
issues = sortArrayByDate(issues as any, "created_at");
|
||||
}
|
||||
if (orderBy === "-updated_at") {
|
||||
issues = sortArrayByDate(issues as any, "updated_at");
|
||||
}
|
||||
if (orderBy === "start_date") {
|
||||
issues = sortArrayByDate(issues as any, "updated_at");
|
||||
}
|
||||
if (orderBy === "priority") {
|
||||
issues = sortArrayByPriority(issues as any, "priority");
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.rootStore.issue.issues = {
|
||||
...this.rootStore.issue.issues,
|
||||
[projectId]: { ...this.rootStore.issue.issues[projectId], [issueType]: issues },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// same as above function but will use temp id instead of real id
|
||||
updateQuickAddIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
||||
const projectId: string | null = issue?.project;
|
||||
const issueType = this.rootStore.issue.getIssueType;
|
||||
if (!projectId || !issueType) return null;
|
||||
|
||||
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
|
||||
this.rootStore.issue.getIssues;
|
||||
if (!issues) return null;
|
||||
|
||||
if (issueType === "grouped" && group_id) {
|
||||
issues = issues as IIssueGroupedStructure;
|
||||
const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.tempId === issue.tempId);
|
||||
issues = {
|
||||
...issues,
|
||||
[group_id]: _currentIssueId
|
||||
? issues[group_id]?.map((i: IIssue) =>
|
||||
i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i
|
||||
)
|
||||
: [...(issues?.[group_id] ?? []), issue],
|
||||
};
|
||||
}
|
||||
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
||||
issues = issues as IIssueGroupWithSubGroupsStructure;
|
||||
const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.tempId === issue.tempId);
|
||||
issues = {
|
||||
...issues,
|
||||
[sub_group_id]: {
|
||||
...issues[sub_group_id],
|
||||
[group_id]: _currentIssueId
|
||||
? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) =>
|
||||
i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i
|
||||
)
|
||||
: [...(issues?.[sub_group_id]?.[group_id] ?? []), issue],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (issueType === "ungrouped") {
|
||||
issues = issues as IIssueUnGroupedStructure;
|
||||
const _currentIssueId = issues?.find((_i) => _i?.tempId === issue.tempId);
|
||||
issues = _currentIssueId
|
||||
? issues?.map((i: IIssue) => (i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i))
|
||||
: [...(issues ?? []), issue];
|
||||
}
|
||||
|
||||
const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || "";
|
||||
if (orderBy === "-created_at") {
|
||||
issues = sortArrayByDate(issues as any, "created_at");
|
||||
}
|
||||
if (orderBy === "-updated_at") {
|
||||
issues = sortArrayByDate(issues as any, "updated_at");
|
||||
}
|
||||
if (orderBy === "start_date") {
|
||||
issues = sortArrayByDate(issues as any, "updated_at");
|
||||
}
|
||||
if (orderBy === "priority") {
|
||||
issues = sortArrayByPriority(issues as any, "priority");
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.rootStore.issue.issues = {
|
||||
...this.rootStore.issue.issues,
|
||||
[projectId]: { ...this.rootStore.issue.issues[projectId], [issueType]: issues },
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue