Skip to content

Commit 4499a5f

Browse files
Sync issues and workspace data when the issue properties like labels/modules/cycles etc are deleted from the project (#6165)
1 parent 727dd40 commit 4499a5f

File tree

8 files changed

+197
-8
lines changed

8 files changed

+197
-8
lines changed

web/core/local-db/storage.sqlite.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,14 @@ export class Storage {
294294
log(`Project ${projectId} is loading, falling back to server`);
295295
}
296296
const issueService = new IssueService();
297-
return await issueService.getIssuesFromServer(workspaceSlug, projectId, queries, config);
297+
298+
// Ignore projectStatus if projectId is not provided
299+
if (projectId) {
300+
return await issueService.getIssuesFromServer(workspaceSlug, projectId, queries, config);
301+
}
302+
if (this.status !== "ready" && !rootStore.user.localDBEnabled) {
303+
return;
304+
}
298305
}
299306

300307
const { cursor, group_by, sub_group_by } = queries;

web/core/local-db/utils/load-workspace.ts

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { IEstimate, IEstimatePoint, IWorkspaceMember } from "@plane/types";
1+
import { difference } from "lodash";
2+
import { IEstimate, IEstimatePoint, IWorkspaceMember, TIssue } from "@plane/types";
23
import { API_BASE_URL } from "@/helpers/common.helper";
34
import { EstimateService } from "@/plane-web/services/project/estimate.service";
45
import { CycleService } from "@/services/cycle.service";
@@ -7,6 +8,7 @@ import { ModuleService } from "@/services/module.service";
78
import { ProjectStateService } from "@/services/project";
89
import { WorkspaceService } from "@/services/workspace.service";
910
import { persistence } from "../storage.sqlite";
11+
import { updateIssue } from "./load-issues";
1012
import {
1113
cycleSchema,
1214
estimatePointSchema,
@@ -103,6 +105,151 @@ export const getMembers = async (workspaceSlug: string) => {
103105
return objects;
104106
};
105107

108+
const syncLabels = async (currentLabels: any) => {
109+
const currentIdList = currentLabels.map((label: any) => label.id);
110+
const existingLabels = await persistence.db.exec("SELECT id FROM labels;");
111+
112+
const existingIdList = existingLabels.map((label: any) => label.id);
113+
114+
const deletedIds = difference(existingIdList, currentIdList);
115+
116+
await syncIssuesWithDeletedLabels(deletedIds as string[]);
117+
};
118+
119+
export const syncIssuesWithDeletedLabels = async (deletedLabelIds: string[]) => {
120+
if (!deletedLabelIds.length) {
121+
return;
122+
}
123+
124+
// Ideally we should use recursion to fetch all the issues, but 10000 issues is more than enough for now.
125+
const issues = await persistence.getIssues("", "", { labels: deletedLabelIds.join(","), cursor: "10000:0:0" }, {});
126+
if (issues?.results && Array.isArray(issues.results)) {
127+
const promises = issues.results.map(async (issue: TIssue) => {
128+
const updatedIssue = {
129+
...issue,
130+
label_ids: issue.label_ids.filter((id: string) => !deletedLabelIds.includes(id)),
131+
is_local_update: 1,
132+
};
133+
// We should await each update because it uses a transaction. But transaction are handled in the query executor.
134+
updateIssue(updatedIssue);
135+
});
136+
await Promise.all(promises);
137+
}
138+
};
139+
140+
const syncModules = async (currentModules: any) => {
141+
const currentIdList = currentModules.map((module: any) => module.id);
142+
const existingModules = await persistence.db.exec("SELECT id FROM modules;");
143+
const existingIdList = existingModules.map((module: any) => module.id);
144+
const deletedIds = difference(existingIdList, currentIdList);
145+
await syncIssuesWithDeletedModules(deletedIds as string[]);
146+
};
147+
148+
export const syncIssuesWithDeletedModules = async (deletedModuleIds: string[]) => {
149+
if (!deletedModuleIds.length) {
150+
return;
151+
}
152+
153+
const issues = await persistence.getIssues("", "", { modules: deletedModuleIds.join(","), cursor: "10000:0:0" }, {});
154+
if (issues?.results && Array.isArray(issues.results)) {
155+
const promises = issues.results.map(async (issue: TIssue) => {
156+
const updatedIssue = {
157+
...issue,
158+
module_ids: issue.module_ids?.filter((id: string) => !deletedModuleIds.includes(id)) || [],
159+
is_local_update: 1,
160+
};
161+
updateIssue(updatedIssue);
162+
});
163+
await Promise.all(promises);
164+
}
165+
};
166+
167+
const syncCycles = async (currentCycles: any) => {
168+
const currentIdList = currentCycles.map((cycle: any) => cycle.id);
169+
const existingCycles = await persistence.db.exec("SELECT id FROM cycles;");
170+
const existingIdList = existingCycles.map((cycle: any) => cycle.id);
171+
const deletedIds = difference(existingIdList, currentIdList);
172+
await syncIssuesWithDeletedCycles(deletedIds as string[]);
173+
};
174+
175+
export const syncIssuesWithDeletedCycles = async (deletedCycleIds: string[]) => {
176+
if (!deletedCycleIds.length) {
177+
return;
178+
}
179+
180+
const issues = await persistence.getIssues("", "", { cycles: deletedCycleIds.join(","), cursor: "10000:0:0" }, {});
181+
if (issues?.results && Array.isArray(issues.results)) {
182+
const promises = issues.results.map(async (issue: TIssue) => {
183+
const updatedIssue = {
184+
...issue,
185+
cycle_id: null,
186+
is_local_update: 1,
187+
};
188+
updateIssue(updatedIssue);
189+
});
190+
await Promise.all(promises);
191+
}
192+
};
193+
194+
const syncStates = async (currentStates: any) => {
195+
const currentIdList = currentStates.map((state: any) => state.id);
196+
const existingStates = await persistence.db.exec("SELECT id FROM states;");
197+
const existingIdList = existingStates.map((state: any) => state.id);
198+
const deletedIds = difference(existingIdList, currentIdList);
199+
await syncIssuesWithDeletedStates(deletedIds as string[]);
200+
};
201+
202+
export const syncIssuesWithDeletedStates = async (deletedStateIds: string[]) => {
203+
if (!deletedStateIds.length) {
204+
return;
205+
}
206+
207+
const issues = await persistence.getIssues("", "", { states: deletedStateIds.join(","), cursor: "10000:0:0" }, {});
208+
if (issues?.results && Array.isArray(issues.results)) {
209+
const promises = issues.results.map(async (issue: TIssue) => {
210+
const updatedIssue = {
211+
...issue,
212+
state_id: null,
213+
is_local_update: 1,
214+
};
215+
updateIssue(updatedIssue);
216+
});
217+
await Promise.all(promises);
218+
}
219+
};
220+
221+
const syncMembers = async (currentMembers: any) => {
222+
const currentIdList = currentMembers.map((member: any) => member.id);
223+
const existingMembers = await persistence.db.exec("SELECT id FROM members;");
224+
const existingIdList = existingMembers.map((member: any) => member.id);
225+
const deletedIds = difference(existingIdList, currentIdList);
226+
await syncIssuesWithDeletedMembers(deletedIds as string[]);
227+
};
228+
229+
export const syncIssuesWithDeletedMembers = async (deletedMemberIds: string[]) => {
230+
if (!deletedMemberIds.length) {
231+
return;
232+
}
233+
234+
const issues = await persistence.getIssues(
235+
"",
236+
"",
237+
{ assignees: deletedMemberIds.join(","), cursor: "10000:0:0" },
238+
{}
239+
);
240+
if (issues?.results && Array.isArray(issues.results)) {
241+
const promises = issues.results.map(async (issue: TIssue) => {
242+
const updatedIssue = {
243+
...issue,
244+
assignee_ids: issue.assignee_ids.filter((id: string) => !deletedMemberIds.includes(id)),
245+
is_local_update: 1,
246+
};
247+
updateIssue(updatedIssue);
248+
});
249+
await Promise.all(promises);
250+
}
251+
};
252+
106253
export const loadWorkSpaceData = async (workspaceSlug: string) => {
107254
if (!persistence.db || !persistence.db.exec) {
108255
return;
@@ -117,28 +264,45 @@ export const loadWorkSpaceData = async (workspaceSlug: string) => {
117264
promises.push(getMembers(workspaceSlug));
118265
const [labels, modules, cycles, states, estimates, members] = await Promise.all(promises);
119266

267+
// @todo: we don't need this manual sync here, when backend adds these changes to issue activity and updates the updated_at of the issue.
268+
await syncLabels(labels);
269+
await syncModules(modules);
270+
await syncCycles(cycles);
271+
await syncStates(states);
272+
// TODO: Not handling sync estimates yet, as we don't know the new estimate point assigned.
273+
// Backend should update the updated_at of the issue when estimate point is updated, or we should have realtime sync on the issues table.
274+
// await syncEstimates(estimates);
275+
await syncMembers(members);
276+
120277
const start = performance.now();
278+
121279
await persistence.db.exec("BEGIN;");
280+
await persistence.db.exec("DELETE FROM labels WHERE 1=1;");
122281
await batchInserts(labels, "labels", labelSchema);
123282
await persistence.db.exec("COMMIT;");
124283

125284
await persistence.db.exec("BEGIN;");
285+
await persistence.db.exec("DELETE FROM modules WHERE 1=1;");
126286
await batchInserts(modules, "modules", moduleSchema);
127287
await persistence.db.exec("COMMIT;");
128288

129289
await persistence.db.exec("BEGIN;");
290+
await persistence.db.exec("DELETE FROM cycles WHERE 1=1;");
130291
await batchInserts(cycles, "cycles", cycleSchema);
131292
await persistence.db.exec("COMMIT;");
132293

133294
await persistence.db.exec("BEGIN;");
295+
await persistence.db.exec("DELETE FROM states WHERE 1=1;");
134296
await batchInserts(states, "states", stateSchema);
135297
await persistence.db.exec("COMMIT;");
136298

137299
await persistence.db.exec("BEGIN;");
300+
await persistence.db.exec("DELETE FROM estimate_points WHERE 1=1;");
138301
await batchInserts(estimates, "estimate_points", estimatePointSchema);
139302
await persistence.db.exec("COMMIT;");
140303

141304
await persistence.db.exec("BEGIN;");
305+
await persistence.db.exec("DELETE FROM members WHERE 1=1;");
142306
await batchInserts(members, "members", memberSchema);
143307
await persistence.db.exec("COMMIT;");
144308

web/core/local-db/utils/query-constructor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,11 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
142142
`;
143143
});
144144

145-
sql += ` WHERE i.project_id = '${projectId}' ${singleFilterConstructor(otherProps)} group by i.id `;
145+
sql += ` WHERE 1=1 `;
146+
if (projectId) {
147+
sql += ` AND i.project_id = '${projectId}' `;
148+
}
149+
sql += ` ${singleFilterConstructor(otherProps)} group by i.id `;
146150
sql += orderByString;
147151

148152
// Add offset and paging to query

web/core/local-db/utils/query.utils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,11 @@ export const getFilteredRowsForGrouping = (projectId: string, queries: any) => {
161161
if (otherProps.state_group) {
162162
sql += `LEFT JOIN states ON i.state_id = states.id `;
163163
}
164-
sql += `WHERE i.project_id = '${projectId}'
165-
`;
164+
sql += `WHERE 1=1 `;
165+
if (projectId) {
166+
sql += ` AND i.project_id = '${projectId}'
167+
`;
168+
}
166169
sql += `${singleFilterConstructor(otherProps)})
167170
`;
168171
return sql;
@@ -212,8 +215,11 @@ export const getFilteredRowsForGrouping = (projectId: string, queries: any) => {
212215
`;
213216
}
214217

215-
sql += ` WHERE i.project_id = '${projectId}'
216-
`;
218+
sql += ` WHERE 1=1 `;
219+
if (projectId) {
220+
sql += ` AND i.project_id = '${projectId}'
221+
`;
222+
}
217223
sql += singleFilterConstructor(otherProps);
218224

219225
sql += `)

web/core/store/cycle.store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { orderCycles, shouldFilterCycle, formatActiveCycle } from "@/helpers/cyc
2020
import { getDate } from "@/helpers/date-time.helper";
2121
import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper";
2222
// services
23+
import { syncIssuesWithDeletedCycles } from "@/local-db/utils/load-workspace";
2324
import { CycleService } from "@/services/cycle.service";
2425
import { CycleArchiveService } from "@/services/cycle_archive.service";
2526
import { IssueService } from "@/services/issue";
@@ -675,6 +676,7 @@ export class CycleStore implements ICycleStore {
675676
delete this.cycleMap[cycleId];
676677
delete this.activeCycleIdMap[cycleId];
677678
if (this.rootStore.favorite.entityMap[cycleId]) this.rootStore.favorite.removeFavoriteFromStore(cycleId);
679+
syncIssuesWithDeletedCycles([cycleId]);
678680
});
679681
});
680682

web/core/store/label.store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IIssueLabel, IIssueLabelTree } from "@plane/types";
77
// helpers
88
import { buildTree } from "@/helpers/array.helper";
99
// services
10+
import { syncIssuesWithDeletedLabels } from "@/local-db/utils/load-workspace";
1011
import { IssueLabelService } from "@/services/issue";
1112
// store
1213
import { CoreRootStore } from "./root.store";
@@ -275,6 +276,7 @@ export class LabelStore implements ILabelStore {
275276
runInAction(() => {
276277
delete this.labelMap[labelId];
277278
});
279+
syncIssuesWithDeletedLabels([labelId]);
278280
});
279281
};
280282
}

web/core/store/module.store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { IModule, ILinkDetails, TModulePlotType } from "@plane/types";
1010
import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper";
1111
import { orderModules, shouldFilterModule } from "@/helpers/module.helper";
1212
// services
13+
import { syncIssuesWithDeletedModules } from "@/local-db/utils/load-workspace";
1314
import { ModuleService } from "@/services/module.service";
1415
import { ModuleArchiveService } from "@/services/module_archive.service";
1516
import { ProjectService } from "@/services/project";
@@ -438,6 +439,7 @@ export class ModulesStore implements IModuleStore {
438439
runInAction(() => {
439440
delete this.moduleMap[moduleId];
440441
if (this.rootStore.favorite.entityMap[moduleId]) this.rootStore.favorite.removeFavoriteFromStore(moduleId);
442+
syncIssuesWithDeletedModules([moduleId]);
441443
});
442444
});
443445
};

web/core/store/state.store.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import groupBy from "lodash/groupBy";
22
import set from "lodash/set";
3-
import { makeObservable, observable, computed, action, runInAction } from "mobx";
3+
import { action, computed, makeObservable, observable, runInAction } from "mobx";
44
import { computedFn } from "mobx-utils";
55
// types
66
import { IState } from "@plane/types";
77
// helpers
88
import { convertStringArrayToBooleanObject } from "@/helpers/array.helper";
99
import { sortStates } from "@/helpers/state.helper";
1010
// plane web
11+
import { syncIssuesWithDeletedStates } from "@/local-db/utils/load-workspace";
1112
import { ProjectStateService } from "@/plane-web/services/project/project-state.service";
1213
import { RootStore } from "@/plane-web/store/root.store";
1314

@@ -228,6 +229,7 @@ export class StateStore implements IStateStore {
228229
await this.stateService.deleteState(workspaceSlug, projectId, stateId).then(() => {
229230
runInAction(() => {
230231
delete this.stateMap[stateId];
232+
syncIssuesWithDeletedStates([stateId]);
231233
});
232234
});
233235
};

0 commit comments

Comments
 (0)