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:
Dakshesh Jain 2023-10-27 12:32:24 +05:30 committed by GitHub
parent d95ea463b2
commit 4aad35e007
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 2734 additions and 951 deletions

View file

@ -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";

View file

@ -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;

View file

@ -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(() => {

View 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 },
};
});
};
}