mirror of
https://github.com/makeplane/plane.git
synced 2025-12-25 08:09:33 +01:00
* chore: ln support modules constants * fix: translation key * chore: empty state refactor (#6404) * chore: asset path helper hook added * chore: detailed and simple empty state component added * chore: section empty state component added * chore: language translation for all empty states * chore: new empty state implementation * improvement: add more translations * improvement: user permissions and workspace draft empty state * chore: update translation structure * chore: inbox empty states * chore: disabled project features empty state * chore: active cycle progress empty state * chore: notification empty state * chore: connections translation * chore: issue comment, relation, bulk delete, and command k empty state translation * chore: project pages empty state and translations * chore: project module and view related empty state * chore: remove project draft related empty state * chore: project cycle, views and archived issues empty state * chore: project cycles related empty state * chore: project settings empty state * chore: profile issue and acitivity empty state * chore: workspace settings realted constants * chore: stickies and home widgets empty state * chore: remove all reference to deprecated empty state component and constnats * chore: add support to ignore theme in resolved asset path hook * chore: minor updates * fix: build errors --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * fix: language support fo profile (#6461) * fix: ln support fo profile * fix: merge changes * fix: merge changes * [WEB-3165]feat: language support for issues (#6452) * * chore: moved issue constants to packages * chore: restructured issue constants * improvement: added translations to issue constants * chore: updated translation structure * * chore: updated chinese, spanish and french translation * chore: updated translation for issues mobile header * chore: updated spanish translation * chore: removed translation for issue priorities * fix: build errors * chore: minor updates --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: migrated filters.ts to packages (#6459) Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: workspace drafts constant moved to plane constant package * feat: home language support without stickies (#6443) * feat: home language support without stickies * fix: home sidebar * fix: added missing keys * fix: show all btn * fix: recents empty state * chore: translation update * feat: workspace constant language support and refactor (#6462) * chore: workspace constant language support and refactor * chore: workspace constant language support and refactor * chore: code refactor * chore: code refactor * merge conflict * chore: code refactor --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: tab indices constant moved to plane package (#6464) * chore: notification language support and refactor * chore: ln support for inbox constants (#6432) * chore: ln support for inbox constants * fix: snooze duration * fix: enum * fix: translation keys * fix: inbox status icon * fix: status icon * fix: naming --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * fix: ln support for views constants (#6431) * fix: ln support for views constants * fix: added translation * fix: translation keys * fix: access * chore: code refactor * chore: ln support workspace projects constants (#6429) * chore: ln support workspace projects constants * fix: translation key * fix: removed state translation * fix: removed state translation * fi: added translations * Chore: theme language support and refactor (#6465) * chore: themes language support and refactor * chore: theme language support and refactor * fix * [WEB-3173] chore: language support for cycles constant file (#6415) * chore: ln support for cycles constant file * fix: added chinese * fix: lint * fix: translation key * fix: build errors * minor updates * chore: minor translation update * chore: minor translation update * refactor: move labels contants to packages * refactor: move swr, file and error related constants to packages * chore: timezones constant moved to plane package * chore: metadata constant code refactor * chore: code refactor * fix: dashboard constants moved * chore: code refactor (#6478) * refactor: spreadsheet constants * chore: drafts language support (#6485) * chore: workspace drafts language support * chore: code refactor * feat: ln support for notifications (#6486) * feat: ln support for notifications * fix: translations * * refactor: moved page constants to packages (#6480) * fix: removed use-client * chore: removed unnecessary commnets * chore: workspace draft language support (#6490) * chore: workspace drafts language support * chore: code refactor * chore: draft language support * Feat constant event tracker (#6479) * fix: event tracjer constants * fix: constants event tracker * feat: language translation - projects list (#6493) * feat: added translation to projects list page * chore: restructured translation file * chore: module language support (#6499) * chore: module language support added * chore: code refactor * chore: workspace views language support (#6492) * chore: workspace views language support * chore: code refactor * feat: custom analytics language support (#6494) * feat: custom analytics language support * fix: key * fix: refactoring --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> * chore: minor improvements * feat: language support for intake (#6498) * feat: language support for intake * fix: key name * refactor: authentications related translations * feat: language support issues (#6501) * enhancement: added translations for issue list view * chore: added translations for issue detail widgets * chore: added missing translations * chore: modified issue to work items * chore: updated translations * Feat: workspace settings language support (#6508) * feat: language support for workspace settings * fix: lint * fix: export title * chore project settings language support (#6502) * chore: project settings language support * chore: code refactor * refactor: workspace creation related translations * chore: renamed issues to work items * fix: build errors * fix: lint * chore: modified translations * chore: remove duplicate * improvement: french translation * chore: chinese translation improvement * fix: japanese translations * chore: added spanish translation * minor improvements * fix: miscelleous language translations * fix: clear_all key * fix: moved user permission constants (#6516) * feat: language support for issues (#6513) * chore: added language support to issue detail widgets * improvement: added translation for issue detail * enhancement: added language trasnlation to issue layouts * chore: translation improvement (#6518) * feat: language support description (#6519) * enhancement: added language support for description * fix: updated keys * chore: renamed issue to work item (#6522) * chore: replace missing issue occurances to work items * fix: build errors * minor improvements * fix: profile links * Feat ln cycles (#6528) * feat: added language support for cycles * feat: added language support for cycles * chore: added core.json * fix: translation keys * fix: translation keys (#6530) * fix: changed sidebar keys * fix: removed extras * fix: updated keys * chore: optimize translation imports * fix: updated keys (#6534) * fix: updated keys * fix-sub work items toasts * chore: add missing translation and minor fixes * chore: code refactor * fix: language support keys (#6553) * minor improvements * minor fixes * fix: remove lucide import from constants package * chore: regenerate all translations * chore: addded chinese and japanese translation files * chore: remove all from translations * fix: added member * fix: language support keys (#6558) * fix: renamed keys * fix: space app * chore: renamed issues to work items * chore: update site manifest * chore: updated translations * fix: lang keys * chore: update translations --------- Co-authored-by: gakshita <akshitagoyal1516@gmail.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Co-authored-by: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com> Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so> Co-authored-by: Vamsi krishna <matalav55@gmail.com> Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com>
461 lines
14 KiB
TypeScript
461 lines
14 KiB
TypeScript
import * as Comlink from "comlink";
|
|
import set from "lodash/set";
|
|
// plane
|
|
import { EIssueGroupBYServerToProperty } from "@plane/constants";
|
|
import { TIssue } from "@plane/types";
|
|
// lib
|
|
import { rootStore } from "@/lib/store-context";
|
|
// services
|
|
import { IssueService } from "@/services/issue/issue.service";
|
|
//
|
|
import { ARRAY_FIELDS, BOOLEAN_FIELDS } from "./utils/constants";
|
|
import { getSubIssuesWithDistribution } from "./utils/data.utils";
|
|
import createIndexes from "./utils/indexes";
|
|
import { addIssuesBulk, syncDeletesToLocal } from "./utils/load-issues";
|
|
import { loadWorkSpaceData } from "./utils/load-workspace";
|
|
import { issueFilterCountQueryConstructor, issueFilterQueryConstructor } from "./utils/query-constructor";
|
|
import { runQuery } from "./utils/query-executor";
|
|
import { createTables } from "./utils/tables";
|
|
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
|
|
|
|
const DB_VERSION = 1;
|
|
const PAGE_SIZE = 500;
|
|
const BATCH_SIZE = 50;
|
|
|
|
type TProjectStatus = {
|
|
issues: { status: undefined | "loading" | "ready" | "error" | "syncing"; sync: Promise<void> | undefined };
|
|
};
|
|
|
|
type TDBStatus = "initializing" | "ready" | "error" | undefined;
|
|
export class Storage {
|
|
db: any;
|
|
status: TDBStatus = undefined;
|
|
dbName = "plane";
|
|
projectStatus: Record<string, TProjectStatus> = {};
|
|
workspaceSlug: string = "";
|
|
|
|
constructor() {
|
|
this.db = null;
|
|
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("beforeunload", this.closeDBConnection);
|
|
}
|
|
}
|
|
|
|
closeDBConnection = async () => {
|
|
if (this.db) {
|
|
await this.db.close();
|
|
}
|
|
};
|
|
|
|
reset = () => {
|
|
if (this.db) {
|
|
this.db.close();
|
|
}
|
|
this.db = null;
|
|
this.status = undefined;
|
|
this.projectStatus = {};
|
|
this.workspaceSlug = "";
|
|
};
|
|
|
|
clearStorage = async (force = false) => {
|
|
try {
|
|
await this.db?.close();
|
|
await clearOPFS(force);
|
|
this.reset();
|
|
} catch (e) {
|
|
console.error("Error clearing sqlite sync storage", e);
|
|
}
|
|
};
|
|
|
|
initialize = async (workspaceSlug: string): Promise<boolean> => {
|
|
if (!rootStore.user.localDBEnabled) return false; // return if the window gets hidden
|
|
|
|
if (workspaceSlug !== this.workspaceSlug) {
|
|
this.reset();
|
|
}
|
|
|
|
try {
|
|
await this._initialize(workspaceSlug);
|
|
return true;
|
|
} catch (err) {
|
|
logError(err);
|
|
this.status = "error";
|
|
return false;
|
|
}
|
|
};
|
|
|
|
_initialize = async (workspaceSlug: string): Promise<boolean> => {
|
|
if (this.status === "initializing") {
|
|
console.warn(`Initialization already in progress for workspace ${workspaceSlug}`);
|
|
return false;
|
|
}
|
|
if (this.status === "ready") {
|
|
console.warn(`Already initialized for workspace ${workspaceSlug}`);
|
|
return true;
|
|
}
|
|
if (this.status === "error") {
|
|
console.warn(`Initialization failed for workspace ${workspaceSlug}`);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const { DBClass } = await import("./worker/db");
|
|
const worker = new Worker(new URL("./worker/db.ts", import.meta.url));
|
|
const MyWorker = Comlink.wrap<typeof DBClass>(worker);
|
|
|
|
// Add cleanup on window unload
|
|
window.addEventListener("unload", () => worker.terminate());
|
|
|
|
this.workspaceSlug = workspaceSlug;
|
|
this.dbName = workspaceSlug;
|
|
const instance = await new MyWorker();
|
|
await instance.init(workspaceSlug);
|
|
|
|
this.db = {
|
|
exec: instance.exec,
|
|
close: instance.close,
|
|
};
|
|
|
|
this.status = "ready";
|
|
// Your SQLite code here.
|
|
await createTables();
|
|
|
|
await this.setOption("DB_VERSION", DB_VERSION.toString());
|
|
return true;
|
|
} catch (error) {
|
|
this.status = "error";
|
|
this.db = null;
|
|
throw new Error(`Failed to initialize database worker: ${error}`);
|
|
}
|
|
};
|
|
|
|
syncWorkspace = async () => {
|
|
if (!rootStore.user.localDBEnabled) return;
|
|
const syncInProgress = await this.getIsWriteInProgress("sync_workspace");
|
|
if (syncInProgress) {
|
|
log("Sync in progress, skipping");
|
|
return;
|
|
}
|
|
try {
|
|
this.setOption("sync_workspace", new Date().toUTCString());
|
|
await loadWorkSpaceData(this.workspaceSlug);
|
|
this.deleteOption("sync_workspace");
|
|
} catch (e) {
|
|
logError(e);
|
|
this.deleteOption("sync_workspace");
|
|
}
|
|
};
|
|
|
|
syncProject = async (projectId: string) => {
|
|
if (
|
|
// document.hidden ||
|
|
!rootStore.user.localDBEnabled
|
|
)
|
|
return false; // return if the window gets hidden
|
|
|
|
// Load labels, members, states, modules, cycles
|
|
await this.syncIssues(projectId);
|
|
|
|
// // Sync rest of the projects
|
|
// const projects = await getProjectIds();
|
|
|
|
// // Exclude the one we just synced
|
|
// const projectsToSync = projects.filter((p: string) => p !== projectId);
|
|
// for (const project of projectsToSync) {
|
|
// await delay(8000);
|
|
// await this.syncIssues(project);
|
|
// }
|
|
// this.setOption("workspace_synced_at", new Date().toISOString());
|
|
};
|
|
|
|
syncIssues = async (projectId: string) => {
|
|
if (!rootStore.user.localDBEnabled || !this.db) {
|
|
return false;
|
|
}
|
|
try {
|
|
const sync = this._syncIssues(projectId);
|
|
this.setSync(projectId, sync);
|
|
await sync;
|
|
} catch (e) {
|
|
logError(e);
|
|
this.setStatus(projectId, "error");
|
|
}
|
|
};
|
|
|
|
_syncIssues = async (projectId: string) => {
|
|
log("### Sync started");
|
|
let status = this.getStatus(projectId);
|
|
if (status === "loading" || status === "syncing") {
|
|
log(`Project ${projectId} is already loading or syncing`);
|
|
return;
|
|
}
|
|
const syncPromise = this.getSync(projectId);
|
|
|
|
if (syncPromise) {
|
|
// Redundant check?
|
|
return;
|
|
}
|
|
|
|
const queryParams: { cursor: string; updated_at__gt?: string; description: boolean } = {
|
|
cursor: `${PAGE_SIZE}:0:0`,
|
|
description: true,
|
|
};
|
|
|
|
const syncedAt = await this.getLastSyncTime(projectId);
|
|
const projectSync = await this.getOption(projectId);
|
|
|
|
if (syncedAt) {
|
|
queryParams["updated_at__gt"] = syncedAt;
|
|
}
|
|
|
|
this.setStatus(projectId, projectSync === "ready" ? "syncing" : "loading");
|
|
status = this.getStatus(projectId);
|
|
|
|
log(`### ${projectSync === "ready" ? "Syncing" : "Loading"} issues to local db for project ${projectId}`);
|
|
|
|
const start = performance.now();
|
|
const issueService = new IssueService();
|
|
|
|
const response = await issueService.getIssuesForSync(this.workspaceSlug, projectId, queryParams);
|
|
|
|
await addIssuesBulk(response.results, BATCH_SIZE);
|
|
if (response.total_pages > 1) {
|
|
const promiseArray = [];
|
|
for (let i = 1; i < response.total_pages; i++) {
|
|
queryParams.cursor = `${PAGE_SIZE}:${i}:0`;
|
|
promiseArray.push(issueService.getIssuesForSync(this.workspaceSlug, projectId, queryParams));
|
|
}
|
|
const pages = await Promise.all(promiseArray);
|
|
for (const page of pages) {
|
|
await addIssuesBulk(page.results, BATCH_SIZE);
|
|
}
|
|
}
|
|
|
|
if (syncedAt) {
|
|
await syncDeletesToLocal(this.workspaceSlug, projectId, { updated_at__gt: syncedAt });
|
|
}
|
|
log("### Time taken to add work items", performance.now() - start);
|
|
|
|
if (status === "loading") {
|
|
await createIndexes();
|
|
}
|
|
this.setOption(projectId, "ready");
|
|
this.setStatus(projectId, "ready");
|
|
this.setSync(projectId, undefined);
|
|
};
|
|
|
|
getIssueCount = async (projectId: string) => {
|
|
const count = await runQuery(`select count(*) as count from issues where project_id='${projectId}'`);
|
|
return count[0]["count"];
|
|
};
|
|
|
|
getLastUpdatedIssue = async (projectId: string) => {
|
|
const lastUpdatedIssue = await runQuery(
|
|
`select id, name, updated_at , sequence_id from issues WHERE project_id='${projectId}' AND is_local_update IS NULL order by datetime(updated_at) desc limit 1 `
|
|
);
|
|
|
|
if (lastUpdatedIssue.length) {
|
|
return lastUpdatedIssue[0];
|
|
}
|
|
return;
|
|
};
|
|
|
|
getLastSyncTime = async (projectId: string) => {
|
|
const issue = await this.getLastUpdatedIssue(projectId);
|
|
if (!issue) {
|
|
return false;
|
|
}
|
|
return issue.updated_at;
|
|
};
|
|
|
|
getIssues = async (workspaceSlug: string, projectId: string, queries: any, config: any) => {
|
|
log("#### Queries", queries);
|
|
|
|
const currentProjectStatus = this.getStatus(projectId);
|
|
if (
|
|
!currentProjectStatus ||
|
|
this.status !== "ready" ||
|
|
currentProjectStatus === "loading" ||
|
|
currentProjectStatus === "error" ||
|
|
!rootStore.user.localDBEnabled
|
|
) {
|
|
if (rootStore.user.localDBEnabled) {
|
|
log(`Project ${projectId} is loading, falling back to server`);
|
|
}
|
|
const issueService = new IssueService();
|
|
|
|
// Ignore projectStatus if projectId is not provided
|
|
if (projectId) {
|
|
return await issueService.getIssuesFromServer(workspaceSlug, projectId, queries, config);
|
|
}
|
|
if (this.status !== "ready" && !rootStore.user.localDBEnabled) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const { cursor, group_by, sub_group_by } = queries;
|
|
|
|
const query = issueFilterQueryConstructor(this.workspaceSlug, projectId, queries);
|
|
log("#### Query", query);
|
|
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, queries);
|
|
const start = performance.now();
|
|
let issuesRaw: any[] = [];
|
|
let count: any[];
|
|
try {
|
|
[issuesRaw, count] = await Promise.all([runQuery(query), runQuery(countQuery)]);
|
|
} catch (e) {
|
|
logError(e);
|
|
const issueService = new IssueService();
|
|
return await issueService.getIssuesFromServer(workspaceSlug, projectId, queries, config);
|
|
}
|
|
const end = performance.now();
|
|
|
|
const { total_count } = count[0];
|
|
|
|
const [pageSize, page, offset] = cursor.split(":");
|
|
|
|
const groupByProperty: string =
|
|
EIssueGroupBYServerToProperty[group_by as keyof typeof EIssueGroupBYServerToProperty];
|
|
const subGroupByProperty =
|
|
EIssueGroupBYServerToProperty[sub_group_by as keyof typeof EIssueGroupBYServerToProperty];
|
|
|
|
const parsingStart = performance.now();
|
|
let issueResults = issuesRaw.map((issue: any) => formatLocalIssue(issue));
|
|
|
|
log("#### Work item Results", issueResults.length);
|
|
|
|
const parsingEnd = performance.now();
|
|
|
|
const grouping = performance.now();
|
|
if (groupByProperty && page === "0") {
|
|
if (subGroupByProperty) {
|
|
issueResults = getSubGroupedIssueResults(issueResults);
|
|
} else {
|
|
issueResults = getGroupedIssueResults(issueResults);
|
|
}
|
|
}
|
|
const groupCount = group_by ? Object.keys(issueResults).length : undefined;
|
|
// const subGroupCount = sub_group_by ? Object.keys(issueResults[Object.keys(issueResults)[0]]).length : undefined;
|
|
const groupingEnd = performance.now();
|
|
|
|
const times = {
|
|
IssueQuery: end - start,
|
|
Parsing: parsingEnd - parsingStart,
|
|
Grouping: groupingEnd - grouping,
|
|
};
|
|
if ((window as any).DEBUG) {
|
|
console.table(times);
|
|
}
|
|
const total_pages = Math.ceil(total_count / Number(pageSize));
|
|
const next_page_results = total_pages > parseInt(page) + 1;
|
|
|
|
const out = {
|
|
results: issueResults,
|
|
next_cursor: `${pageSize}:${parseInt(page) + 1}:${Number(offset) + Number(pageSize)}`,
|
|
prev_cursor: `${pageSize}:${parseInt(page) - 1}:${Number(offset) - Number(pageSize)}`,
|
|
total_results: total_count,
|
|
total_count,
|
|
next_page_results,
|
|
total_pages,
|
|
};
|
|
return out;
|
|
};
|
|
|
|
getIssue = async (issueId: string) => {
|
|
try {
|
|
if (!rootStore.user.localDBEnabled || this.status !== "ready") return;
|
|
|
|
const issues = await runQuery(`select * from issues where id='${issueId}'`);
|
|
if (Array.isArray(issues) && issues.length) {
|
|
return formatLocalIssue(issues[0]);
|
|
}
|
|
} catch (err) {
|
|
logError(err);
|
|
console.warn("unable to fetch issue from local db");
|
|
}
|
|
|
|
return;
|
|
};
|
|
|
|
getSubIssues = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
|
const workspace_synced_at = await this.getOption("workspace_synced_at");
|
|
if (!workspace_synced_at) {
|
|
const issueService = new IssueService();
|
|
return await issueService.subIssues(workspaceSlug, projectId, issueId);
|
|
}
|
|
return await getSubIssuesWithDistribution(issueId);
|
|
};
|
|
|
|
getStatus = (projectId: string) => this.projectStatus[projectId]?.issues?.status || undefined;
|
|
setStatus = (projectId: string, status: "loading" | "ready" | "error" | "syncing" | undefined = undefined) => {
|
|
set(this.projectStatus, `${projectId}.issues.status`, status);
|
|
};
|
|
|
|
getSync = (projectId: string) => this.projectStatus[projectId]?.issues?.sync;
|
|
setSync = (projectId: string, sync: Promise<void> | undefined) => {
|
|
set(this.projectStatus, `${projectId}.issues.sync`, sync);
|
|
};
|
|
|
|
getOption = async (key: string, fallback?: string | boolean | number) => {
|
|
try {
|
|
const options = await runQuery(`select * from options where key='${key}'`);
|
|
if (options.length) {
|
|
return options[0].value;
|
|
}
|
|
|
|
return fallback;
|
|
} catch (e) {
|
|
return fallback;
|
|
}
|
|
};
|
|
setOption = async (key: string, value: string) => {
|
|
await runQuery(`insert or replace into options (key, value) values ('${key}', '${value}')`);
|
|
};
|
|
|
|
deleteOption = async (key: string) => {
|
|
await runQuery(` DELETE FROM options where key='${key}'`);
|
|
};
|
|
getOptions = async (keys: string[]) => {
|
|
const options = await runQuery(`select * from options where key in ('${keys.join("','")}')`);
|
|
return options.reduce((acc: any, option: any) => {
|
|
acc[option.key] = option.value;
|
|
return acc;
|
|
}, {});
|
|
};
|
|
|
|
getIsWriteInProgress = async (op: string) => {
|
|
const writeStartTime = await this.getOption(op, false);
|
|
if (writeStartTime) {
|
|
// Check if it has been more than 5seconds
|
|
const current = new Date();
|
|
const start = new Date(writeStartTime);
|
|
|
|
if (current.getTime() - start.getTime() < 5000) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
|
|
export const persistence = new Storage();
|
|
|
|
/**
|
|
* format the issue fetched from local db into an issue
|
|
* @param issue
|
|
* @returns
|
|
*/
|
|
export const formatLocalIssue = (issue: any) => {
|
|
const currIssue = issue;
|
|
ARRAY_FIELDS.forEach((field: string) => {
|
|
currIssue[field] = currIssue[field] ? JSON.parse(currIssue[field]) : [];
|
|
});
|
|
// Convert boolean fields to actual boolean values
|
|
BOOLEAN_FIELDS.forEach((field: string) => {
|
|
currIssue[field] = currIssue[field] === 1;
|
|
});
|
|
return currIssue as TIssue & { group_id?: string; total_issues: number; sub_group_id?: string };
|
|
};
|