Files
plane/web/core/local-db/storage.sqlite.ts
Prateek Shourya d36c3acbf7 feat: language support (#6472)
* 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>
2025-02-06 20:41:31 +05:30

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