[WEB-419] feat: manual issue archival (#3801)

* fix: issue archive without automation

* fix: unarchive issue endpoint change

* chore: archiving logic implemented in the quick-actions dropdowns

* chore: peek overview archive button

* chore: issue archive completed at state

* chore: updated archiving icon and added archive option everywhere

* chore: all issues quick actions dropdown

* chore: archive and unarchive response

* fix: archival mutation

* fix: restore issue from peek overview

* chore: update notification content for archive/restore

* refactor: activity user name

* fix: all issues mutation

* fix: restore issue auth

* chore: close peek overview on archival

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2024-02-28 16:53:26 +05:30 committed by GitHub
parent b1520783cf
commit 30cc923fdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 1402 additions and 691 deletions

View file

@ -1,5 +1,6 @@
import { action, observable, makeObservable, computed, runInAction } from "mobx";
import set from "lodash/set";
import pull from "lodash/pull";
// base class
import { IssueHelperStore } from "../helpers/issue-helper.store";
// services
@ -18,7 +19,7 @@ export interface IArchivedIssues {
// actions
fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
removeIssueFromArchived: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: undefined;
}
@ -48,7 +49,7 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
// action
fetchIssues: action,
removeIssue: action,
removeIssueFromArchived: action,
restoreIssue: action,
});
// root store
this.rootIssueStore = _rootStore;
@ -70,7 +71,7 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
const archivedIssueIds = this.issues[projectId];
if (!archivedIssueIds) return undefined;
const _issues = this.rootIssueStore.issues.getIssuesByIds(archivedIssueIds);
const _issues = this.rootIssueStore.issues.getIssuesByIds(archivedIssueIds, "archived");
if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined;
@ -113,25 +114,24 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
try {
await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId);
if (issueIndex >= 0)
runInAction(() => {
this.issues[projectId].splice(issueIndex, 1);
});
runInAction(() => {
pull(this.issues[projectId], issueId);
});
} catch (error) {
throw error;
}
};
removeIssueFromArchived = async (workspaceSlug: string, projectId: string, issueId: string) => {
restoreIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const response = await this.archivedIssueService.unarchiveIssue(workspaceSlug, projectId, issueId);
const response = await this.archivedIssueService.restoreIssue(workspaceSlug, projectId, issueId);
const issueIndex = this.issues[projectId]?.findIndex((_issueId) => _issueId === issueId);
if (issueIndex && issueIndex >= 0)
runInAction(() => {
this.issues[projectId].splice(issueIndex, 1);
runInAction(() => {
this.rootStore.issues.updateIssue(issueId, {
archived_at: null,
});
pull(this.issues[projectId], issueId);
});
return response;
} catch (error) {

View file

@ -48,6 +48,12 @@ export interface ICycleIssues {
issueId: string,
cycleId?: string | undefined
) => Promise<void>;
archiveIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
cycleId?: string | undefined
) => Promise<void>;
quickAddIssue: (
workspaceSlug: string,
projectId: string,
@ -100,6 +106,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
createIssue: action,
updateIssue: action,
removeIssue: action,
archiveIssue: action,
quickAddIssue: action,
addIssueToCycle: action,
removeIssueFromCycle: action,
@ -127,7 +134,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
const cycleIssueIds = this.issues[cycleId];
if (!cycleIssueIds) return;
const _issues = this.rootIssueStore.issues.getIssuesByIds(cycleIssueIds);
const _issues = this.rootIssueStore.issues.getIssuesByIds(cycleIssueIds, "un-archived");
if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = [];
@ -237,6 +244,26 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
}
};
archiveIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
cycleId: string | undefined = undefined
) => {
try {
if (!cycleId) throw new Error("Cycle Id is required");
await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
runInAction(() => {
pull(this.issues[cycleId], issueId);
});
} catch (error) {
throw error;
}
};
quickAddIssue = async (
workspaceSlug: string,
projectId: string,

View file

@ -81,7 +81,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
const draftIssueIds = this.issues[projectId];
if (!draftIssueIds) return undefined;
const _issues = this.rootIssueStore.issues.getIssuesByIds(draftIssueIds);
const _issues = this.rootIssueStore.issues.getIssuesByIds(draftIssueIds, "un-archived");
if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined;

View file

@ -16,6 +16,7 @@ export interface IIssueStoreActions {
) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>;
@ -156,6 +157,9 @@ export class IssueStore implements IIssueStore {
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.rootIssueDetailStore.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(
workspaceSlug,

View file

@ -47,6 +47,7 @@ export interface IIssueDetail
isIssueLinkModalOpen: boolean;
isParentIssueModalOpen: boolean;
isDeleteIssueModalOpen: boolean;
isArchiveIssueModalOpen: boolean;
isRelationModalOpen: TIssueRelationTypes | null;
// computed
isAnyModalOpen: boolean;
@ -55,6 +56,7 @@ export interface IIssueDetail
toggleIssueLinkModal: (value: boolean) => void;
toggleParentIssueModal: (value: boolean) => void;
toggleDeleteIssueModal: (value: boolean) => void;
toggleArchiveIssueModal: (value: boolean) => void;
toggleRelationModal: (value: TIssueRelationTypes | null) => void;
// store
rootIssueStore: IIssueRootStore;
@ -76,6 +78,7 @@ export class IssueDetail implements IIssueDetail {
isIssueLinkModalOpen: boolean = false;
isParentIssueModalOpen: boolean = false;
isDeleteIssueModalOpen: boolean = false;
isArchiveIssueModalOpen: boolean = false;
isRelationModalOpen: TIssueRelationTypes | null = null;
// store
rootIssueStore: IIssueRootStore;
@ -97,6 +100,7 @@ export class IssueDetail implements IIssueDetail {
isIssueLinkModalOpen: observable.ref,
isParentIssueModalOpen: observable.ref,
isDeleteIssueModalOpen: observable.ref,
isArchiveIssueModalOpen: observable.ref,
isRelationModalOpen: observable.ref,
// computed
isAnyModalOpen: computed,
@ -105,6 +109,7 @@ export class IssueDetail implements IIssueDetail {
toggleIssueLinkModal: action,
toggleParentIssueModal: action,
toggleDeleteIssueModal: action,
toggleArchiveIssueModal: action,
toggleRelationModal: action,
});
@ -128,6 +133,7 @@ export class IssueDetail implements IIssueDetail {
this.isIssueLinkModalOpen ||
this.isParentIssueModalOpen ||
this.isDeleteIssueModalOpen ||
this.isArchiveIssueModalOpen ||
Boolean(this.isRelationModalOpen)
);
}
@ -137,6 +143,7 @@ export class IssueDetail implements IIssueDetail {
toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value);
toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value);
toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value);
toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value);
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
// issue
@ -150,6 +157,8 @@ export class IssueDetail implements IIssueDetail {
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.issue.removeIssue(workspaceSlug, projectId, issueId);
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.issue.archiveIssue(workspaceSlug, projectId, issueId);
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) =>
this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>

View file

@ -18,7 +18,7 @@ export type IIssueStore = {
removeIssue(issueId: string): void;
// helper methods
getIssueById(issueId: string): undefined | TIssue;
getIssuesByIds(issueIds: string[]): undefined | Record<string, TIssue>; // Record defines issue_id as key and TIssue as value
getIssuesByIds(issueIds: string[], type: "archived" | "un-archived"): undefined | Record<string, TIssue>; // Record defines issue_id as key and TIssue as value
};
export class IssueStore implements IIssueStore {
@ -108,14 +108,17 @@ export class IssueStore implements IIssueStore {
/**
* @description This method will return the issues from the issuesMap
* @param {string[]} issueIds
* @param {boolean} archivedIssues
* @returns {Record<string, TIssue> | undefined}
*/
getIssuesByIds = computedFn((issueIds: string[]) => {
getIssuesByIds = computedFn((issueIds: string[], type: "archived" | "un-archived") => {
if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return undefined;
const filteredIssues: { [key: string]: TIssue } = {};
Object.values(this.issuesMap).forEach((issue) => {
if (issueIds.includes(issue.id)) {
filteredIssues[issue.id] = issue;
// if type is archived then check archived_at is not null
// if type is un-archived then check archived_at is null
if ((type === "archived" && issue.archived_at) || (type === "un-archived" && !issue.archived_at)) {
if (issueIds.includes(issue.id)) filteredIssues[issue.id] = issue;
}
});
return isEmpty(filteredIssues) ? undefined : filteredIssues;

View file

@ -46,6 +46,12 @@ export interface IModuleIssues {
issueId: string,
moduleId?: string | undefined
) => Promise<void>;
archiveIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleId?: string | undefined
) => Promise<void>;
quickAddIssue: (
workspaceSlug: string,
projectId: string,
@ -103,6 +109,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
createIssue: action,
updateIssue: action,
removeIssue: action,
archiveIssue: action,
quickAddIssue: action,
addIssuesToModule: action,
removeIssuesFromModule: action,
@ -131,7 +138,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
const moduleIssueIds = this.issues[moduleId];
if (!moduleIssueIds) return;
const _issues = this.rootIssueStore.issues.getIssuesByIds(moduleIssueIds);
const _issues = this.rootIssueStore.issues.getIssuesByIds(moduleIssueIds, "un-archived");
if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = [];
@ -242,6 +249,26 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
}
};
archiveIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleId: string | undefined = undefined
) => {
try {
if (!moduleId) throw new Error("Module Id is required");
await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
runInAction(() => {
pull(this.issues[moduleId], issueId);
});
} catch (error) {
throw error;
}
};
quickAddIssue = async (
workspaceSlug: string,
projectId: string,

View file

@ -1,5 +1,6 @@
import { action, observable, makeObservable, computed, runInAction } from "mobx";
import set from "lodash/set";
import pull from "lodash/pull";
// base class
import { IssueHelperStore } from "../helpers/issue-helper.store";
// services
@ -48,6 +49,12 @@ export interface IProfileIssues {
issueId: string,
userId?: string | undefined
) => Promise<void>;
archiveIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
userId?: string | undefined
) => Promise<void>;
quickAddIssue: undefined;
}
@ -77,6 +84,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
createIssue: action,
updateIssue: action,
removeIssue: action,
archiveIssue: action,
});
// root store
this.rootIssueStore = _rootStore;
@ -104,7 +112,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
if (!userIssueIds) return;
const _issues = this.rootStore.issues.getIssuesByIds(userIssueIds);
const _issues = this.rootStore.issues.getIssuesByIds(userIssueIds, "un-archived");
if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined;
@ -249,4 +257,24 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
throw error;
}
};
archiveIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
userId: string | undefined = undefined
) => {
if (!userId) return;
try {
await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
const uniqueViewId = `${workspaceSlug}_${this.currentView}`;
runInAction(() => {
pull(this.issues[userId][uniqueViewId], issueId);
});
} catch (error) {
throw error;
}
};
}

View file

@ -1,5 +1,6 @@
import { action, observable, makeObservable, computed, runInAction } from "mobx";
import set from "lodash/set";
import pull from "lodash/pull";
// base class
import { IssueHelperStore } from "../helpers/issue-helper.store";
// services
@ -41,6 +42,12 @@ export interface IProjectViewIssues {
issueId: string,
viewId?: string | undefined
) => Promise<void>;
archiveIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
viewId?: string | undefined
) => Promise<void>;
quickAddIssue: (
workspaceSlug: string,
projectId: string,
@ -75,6 +82,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
createIssue: action,
updateIssue: action,
removeIssue: action,
archiveIssue: action,
quickAddIssue: action,
});
// root store
@ -98,7 +106,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
const viewIssueIds = this.issues[viewId];
if (!viewIssueIds) return;
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds);
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds, "un-archived");
if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = [];
@ -210,6 +218,26 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
}
};
archiveIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
viewId: string | undefined = undefined
) => {
try {
if (!viewId) throw new Error("View Id is required");
await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
runInAction(() => {
pull(this.issues[viewId], issueId);
});
} catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation");
throw error;
}
};
quickAddIssue = async (
workspaceSlug: string,
projectId: string,

View file

@ -6,7 +6,7 @@ import concat from "lodash/concat";
// base class
import { IssueHelperStore } from "../helpers/issue-helper.store";
// services
import { IssueService } from "services/issue/issue.service";
import { IssueService, IssueArchiveService } from "services/issue";
// types
import { IIssueRootStore } from "../root.store";
import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types";
@ -23,6 +23,7 @@ export interface IProjectIssues {
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise<TIssue>;
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
}
@ -40,6 +41,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
rootIssueStore: IIssueRootStore;
// services
issueService;
issueArchiveService;
constructor(_rootStore: IIssueRootStore) {
super(_rootStore);
@ -54,6 +56,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
createIssue: action,
updateIssue: action,
removeIssue: action,
archiveIssue: action,
removeBulkIssues: action,
quickAddIssue: action,
});
@ -61,6 +64,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
this.rootIssueStore = _rootStore;
// services
this.issueService = new IssueService();
this.issueArchiveService = new IssueArchiveService();
}
get groupedIssueIds() {
@ -78,7 +82,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
const projectIssueIds = this.issues[projectId];
if (!projectIssueIds) return;
const _issues = this.rootStore.issues.getIssuesByIds(projectIssueIds);
const _issues = this.rootStore.issues.getIssuesByIds(projectIssueIds, "un-archived");
if (!_issues) return [];
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = [];
@ -165,6 +169,21 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
}
};
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId);
runInAction(() => {
this.rootStore.issues.updateIssue(issueId, {
archived_at: response.archived_at,
});
pull(this.issues[projectId], issueId);
});
} catch (error) {
throw error;
}
};
quickAddIssue = async (workspaceSlug: string, projectId: string, data: TIssue) => {
try {
runInAction(() => {

View file

@ -1,10 +1,11 @@
import { action, observable, makeObservable, computed, runInAction } from "mobx";
import set from "lodash/set";
import pull from "lodash/pull";
// base class
import { IssueHelperStore } from "../helpers/issue-helper.store";
// services
import { WorkspaceService } from "services/workspace.service";
import { IssueService } from "services/issue";
import { IssueService, IssueArchiveService } from "services/issue";
// types
import { IIssueRootStore } from "../root.store";
import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types";
@ -37,6 +38,12 @@ export interface IWorkspaceIssues {
issueId: string,
viewId?: string | undefined
) => Promise<void>;
archiveIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
viewId?: string | undefined
) => Promise<void>;
}
export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssues {
@ -52,6 +59,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
// service
workspaceService;
issueService;
issueArchiveService;
constructor(_rootStore: IIssueRootStore) {
super(_rootStore);
@ -67,12 +75,14 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
createIssue: action,
updateIssue: action,
removeIssue: action,
archiveIssue: action,
});
// root store
this.rootIssueStore = _rootStore;
// services
this.workspaceService = new WorkspaceService();
this.issueService = new IssueService();
this.issueArchiveService = new IssueArchiveService();
}
get groupedIssueIds() {
@ -91,7 +101,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
if (!viewIssueIds) return { dataViewId: viewId, issueIds: undefined };
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds);
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds, "un-archived");
if (!_issues) return { dataViewId: viewId, issueIds: [] };
let issueIds: TIssue | TUnGroupedIssues | undefined = undefined;
@ -196,4 +206,28 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
throw error;
}
};
archiveIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
viewId: string | undefined = undefined
) => {
try {
if (!viewId) throw new Error("View id is required");
const uniqueViewId = `${workspaceSlug}_${viewId}`;
const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId);
runInAction(() => {
this.rootStore.issues.updateIssue(issueId, {
archived_at: response.archived_at,
});
pull(this.issues[uniqueViewId], issueId);
});
} catch (error) {
throw error;
}
};
}