mirror of
https://github.com/makeplane/plane.git
synced 2026-01-27 16:49:09 -06:00
* use common getIssues from issue service instead of multiple different services for modules and cycles * Use SQLite to store issues locally and load issues from it. * Fix incorrect total count and filtering on assignees. * enable parallel API calls * use common getIssues from issue service instead of multiple different services for modules and cycles * Use SQLite to store issues locally and load issues from it. * Fix incorrect total count and filtering on assignees. * enable parallel API calls * chore: deleted issue list * - Handle local mutations - Implement getting the updates - Use SWR to update/sync data * Wait for sync to complete in get issues * Fix build errors * Fix build issue * - Sync updates to local-db - Fallback to server when the local data is loading - Wait when the updates are being fetched * Add issues in batches * Disable skeleton loaders for first 10 issues * Load issues in bulk * working version of sql lite with grouped issues * Use window queries for group by * - Fix sort by date fields - Fix the total count * - Fix grouping by created by - Fix order by and limit * fix pagination * Fix sorting on issue priority * - Add secondary sort order - Fix group by priority * chore: added timestamp filter for deleted issues * - Extract local DB into its own class - Implement sorting by label names * Implement subgroup by * sub group by changes * Refactor query constructor * Insert or update issues instead of directly adding them. * Segregated queries. Not working though!! * - Get filtered issues and then group them. - Cleanup code. - Implement order by labels. * Fix build issues * Remove debuggers * remove loaders while changing sorting or applying filters * fix loader while clearing all filters * Fix issue with project being synced twice * Improve project sync * Optimize the queries * Make create dummy data more realistic * dev: added total pages in the global paginator * chore: updated total_paged count * chore: added state_group in the issues pagination * chore: removed deleted_at from the issue pagination payload * chore: replaced state_group with state__group * Integrate new getIssues API, and fix sync issues bug. * Fix issue with SWR running twice in workspace wrapper * Fix DB initialization called when opening project for the first time. * Add all the tables required for sorting * Exclude description from getIssues * Add getIssue function. * Add only selected fields to get query. * Fix the count query * Minor query optimization when no joins are required. * fetch issue description from local db * clear local db on signout * Correct dummy data creation * Fix sort by assignee * sync to local changes * chore: added archived issues in the deleted endpoint * Sync deletes to local db. * - Add missing indexes for tables used in sorting in spreadsheet layout. - Add options table * Make fallback optional in getOption * Kanban column virtualization * persist project sync readiness to sqlite and use that as the source of truth for the project issues to be ready * fix build errors * Fix calendar view * fetch slimed down version of modules in project wrapper * fetch toned down modules and then fetch complete modules * Fix multi value order by in spread sheet layout * Fix sort by * Fix the query when ordering by multi field names * Remove unused import * Fix sort by multi value fields * Format queries and fix order by * fix order by for multi issue * fix loaders for spreadsheet * Fallback to manual order whn moving away from spreadsheet layout * fix minor bug * Move fix for order_by when switching from spreadsheet layout to translateQueryParams * fix default rendering of kanban groups * Fix none priority being saved as null * Remove debugger statement * Fix issue load * chore: updated isue paginated query from to * Fix sub issues and start and target date filters * Fix active and backlog filter * Add default order by * Update the Query param to match with backend. * local sqlite db versioning * When window is hidden, do not perform any db versioning * fix error handling and fall back to server when database errors out * Add ability to disable local db cache * remove db version check from getIssues function * change db version to number and remove workspaceInitPromise in storage.sqlite * - Sync the entire workspace in the background - Add get sub issue method with distribution * Make changes to get issues for sync to match backend. * chore: handled workspace and project in v2 paginted issues * disable issue description and title until fetched from server * sync issues post bulk operations * fix server error * fix front end build * Remove full workspace sync * - Remove the toast message on sync. - Update the disable local message. * Add Hardcoded constant to disable the local db caching * fix lint errors * Fix order by in grouping * update yarn lock * fix build * fix plane-web imports * address review comments --------- Co-authored-by: rahulramesha <rahulramesham@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com>
336 lines
9.8 KiB
TypeScript
336 lines
9.8 KiB
TypeScript
import { ARRAY_FIELDS, GROUP_BY_MAP, PRIORITY_MAP } from "./constants";
|
|
import { SPECIAL_ORDER_BY } from "./query-constructor";
|
|
import { issueSchema } from "./schemas";
|
|
import { wrapDateTime } from "./utils";
|
|
|
|
export const translateQueryParams = (queries: any) => {
|
|
const { group_by, sub_group_by, labels, assignees, state, cycle, module, priority, type, ...otherProps } = queries;
|
|
|
|
const order_by = queries.order_by;
|
|
if (state) otherProps.state_id = state;
|
|
if (cycle) otherProps.cycle_id = cycle;
|
|
if (module) otherProps.module_ids = module;
|
|
if (labels) otherProps.label_ids = labels;
|
|
if (assignees) otherProps.assignee_ids = assignees;
|
|
if (group_by) otherProps.group_by = GROUP_BY_MAP[group_by as keyof typeof GROUP_BY_MAP];
|
|
if (sub_group_by) otherProps.sub_group_by = GROUP_BY_MAP[sub_group_by as keyof typeof GROUP_BY_MAP];
|
|
if (priority) {
|
|
otherProps.priority_proxy = priority
|
|
.split(",")
|
|
.map((priority: string) => PRIORITY_MAP[priority as keyof typeof PRIORITY_MAP])
|
|
.join(",");
|
|
}
|
|
if (type) {
|
|
otherProps.state_group = type === "backlog" ? "backlog" : "unstarted,started";
|
|
}
|
|
|
|
if (order_by?.includes("priority")) {
|
|
otherProps.order_by = order_by.replace("priority", "priority_proxy");
|
|
}
|
|
|
|
// Fix invalid orderby when switching from spreadsheet layout
|
|
if ((group_by || sub_group_by) && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
|
otherProps.order_by = "sort_order";
|
|
}
|
|
// For each property value, replace None with empty string
|
|
Object.keys(otherProps).forEach((key) => {
|
|
if (otherProps[key] === "None") {
|
|
otherProps[key] = "";
|
|
}
|
|
});
|
|
|
|
return otherProps;
|
|
};
|
|
|
|
export const getOrderByFragment = (order_by: string, table = "") => {
|
|
let orderByString = "";
|
|
if (!order_by) return orderByString;
|
|
|
|
if (order_by.startsWith("-")) {
|
|
orderByString += ` ORDER BY ${wrapDateTime(order_by.slice(1))} DESC NULLS LAST, datetime(${table}created_at) DESC`;
|
|
} else {
|
|
orderByString += ` ORDER BY ${wrapDateTime(order_by)} ASC NULLS LAST, datetime(${table}created_at) DESC`;
|
|
}
|
|
return orderByString;
|
|
};
|
|
|
|
export const isMetaJoinRequired = (groupBy: string, subGroupBy: string) =>
|
|
ARRAY_FIELDS.includes(groupBy) || ARRAY_FIELDS.includes(subGroupBy);
|
|
|
|
export const getMetaKeysFragment = (queries: any) => {
|
|
const { group_by, sub_group_by, ...otherProps } = translateQueryParams(queries);
|
|
|
|
const fields: Set<string> = new Set();
|
|
if (ARRAY_FIELDS.includes(group_by)) {
|
|
fields.add(group_by);
|
|
}
|
|
|
|
if (ARRAY_FIELDS.includes(sub_group_by)) {
|
|
fields.add(sub_group_by);
|
|
}
|
|
|
|
const keys = Object.keys(otherProps);
|
|
|
|
keys.forEach((field: string) => {
|
|
if (ARRAY_FIELDS.includes(field)) {
|
|
fields.add(field);
|
|
}
|
|
});
|
|
|
|
let sql;
|
|
|
|
sql = ` ('${Array.from(fields).join("','")}')`;
|
|
|
|
return sql;
|
|
};
|
|
|
|
export const getMetaKeys = (queries: any): string[] => {
|
|
const { group_by, sub_group_by, ...otherProps } = translateQueryParams(queries);
|
|
|
|
const fields: Set<string> = new Set();
|
|
if (ARRAY_FIELDS.includes(group_by)) {
|
|
fields.add(group_by);
|
|
}
|
|
|
|
if (ARRAY_FIELDS.includes(sub_group_by)) {
|
|
fields.add(sub_group_by);
|
|
}
|
|
|
|
const keys = Object.keys(otherProps);
|
|
|
|
keys.forEach((field: string) => {
|
|
if (ARRAY_FIELDS.includes(field)) {
|
|
fields.add(field);
|
|
}
|
|
});
|
|
|
|
return Array.from(fields);
|
|
};
|
|
|
|
const areJoinsRequired = (queries: any) => {
|
|
const { group_by, sub_group_by, ...otherProps } = translateQueryParams(queries);
|
|
|
|
if (ARRAY_FIELDS.includes(group_by) || ARRAY_FIELDS.includes(sub_group_by)) {
|
|
return true;
|
|
}
|
|
if (Object.keys(otherProps).some((field) => ARRAY_FIELDS.includes(field))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Apply filters to the query
|
|
export const getFilteredRowsForGrouping = (projectId: string, queries: any) => {
|
|
const { group_by, sub_group_by, ...otherProps } = translateQueryParams(queries);
|
|
|
|
const filterJoinFields = getMetaKeys(otherProps);
|
|
|
|
const temp = getSingleFilterFields(queries);
|
|
const issueTableFilterFields = temp.length ? "," + temp.join(",") : "";
|
|
|
|
const joinsRequired = areJoinsRequired(queries);
|
|
|
|
let sql = "";
|
|
if (!joinsRequired) {
|
|
sql = `WITH fi as (SELECT i.id,i.created_at ${issueTableFilterFields}`;
|
|
if (group_by) {
|
|
if (group_by === "target_date") {
|
|
sql += `, date(i.${group_by}) as group_id`;
|
|
} else {
|
|
sql += `, i.${group_by} as group_id`;
|
|
}
|
|
}
|
|
if (sub_group_by) {
|
|
sql += `, i.${sub_group_by} as sub_group_id`;
|
|
}
|
|
sql += ` FROM issues i `;
|
|
if (otherProps.state_group) {
|
|
sql += `LEFT JOIN states ON i.state_id = states.id `;
|
|
}
|
|
sql += `WHERE i.project_id = '${projectId}'
|
|
`;
|
|
sql += `${singleFilterConstructor(otherProps)})
|
|
`;
|
|
return sql;
|
|
}
|
|
|
|
sql = `WITH fi AS (`;
|
|
sql += `SELECT i.id,i.created_at ${issueTableFilterFields} `;
|
|
if (group_by) {
|
|
if (ARRAY_FIELDS.includes(group_by)) {
|
|
sql += `, ${group_by}.value as group_id
|
|
`;
|
|
} else if (group_by === "target_date") {
|
|
sql += `, date(i.${group_by}) as group_id
|
|
`;
|
|
} else {
|
|
sql += `, i.${group_by} as group_id
|
|
`;
|
|
}
|
|
}
|
|
if (sub_group_by) {
|
|
if (ARRAY_FIELDS.includes(sub_group_by)) {
|
|
sql += `, ${sub_group_by}.value as sub_group_id
|
|
`;
|
|
} else {
|
|
sql += `, i.${sub_group_by} as sub_group_id
|
|
`;
|
|
}
|
|
}
|
|
|
|
sql += ` from issues i
|
|
`;
|
|
if (otherProps.state_group) {
|
|
sql += `LEFT JOIN states ON i.state_id = states.id `;
|
|
}
|
|
filterJoinFields.forEach((field: string) => {
|
|
sql += ` INNER JOIN issue_meta ${field} ON i.id = ${field}.issue_id AND ${field}.key = '${field}' AND ${field}.value IN ('${otherProps[field].split(",").join("','")}')
|
|
`;
|
|
});
|
|
|
|
// If group by field is not already joined, join it
|
|
if (ARRAY_FIELDS.includes(group_by) && !filterJoinFields.includes(group_by)) {
|
|
sql += ` LEFT JOIN issue_meta ${group_by} ON i.id = ${group_by}.issue_id AND ${group_by}.key = '${group_by}'
|
|
`;
|
|
}
|
|
if (ARRAY_FIELDS.includes(sub_group_by) && !filterJoinFields.includes(sub_group_by)) {
|
|
sql += ` LEFT JOIN issue_meta ${sub_group_by} ON i.id = ${sub_group_by}.issue_id AND ${sub_group_by}.key = '${sub_group_by}'
|
|
`;
|
|
}
|
|
|
|
sql += ` WHERE i.project_id = '${projectId}'
|
|
`;
|
|
sql += singleFilterConstructor(otherProps);
|
|
|
|
sql += `)
|
|
`;
|
|
return sql;
|
|
};
|
|
|
|
export const singleFilterConstructor = (queries: any) => {
|
|
const {
|
|
order_by,
|
|
cursor,
|
|
per_page,
|
|
group_by,
|
|
sub_group_by,
|
|
state_group,
|
|
sub_issue,
|
|
target_date,
|
|
start_date,
|
|
...filters
|
|
} = translateQueryParams(queries);
|
|
|
|
let sql = "";
|
|
if (!sub_issue) {
|
|
sql += ` AND parent_id IS NULL
|
|
`;
|
|
}
|
|
if (target_date) {
|
|
sql += createDateFilter("target_date", target_date);
|
|
}
|
|
if (start_date) {
|
|
sql += createDateFilter("start_date", start_date);
|
|
}
|
|
if (state_group) {
|
|
sql += ` AND state_group in ('${state_group.split(",").join("','")}')
|
|
`;
|
|
}
|
|
const keys = Object.keys(filters);
|
|
|
|
keys.forEach((key) => {
|
|
const value = filters[key] ? filters[key].split(",") : "";
|
|
if (!value) return;
|
|
if (!ARRAY_FIELDS.includes(key)) {
|
|
sql += ` AND ${key} in ('${value.join("','")}')
|
|
`;
|
|
}
|
|
});
|
|
//
|
|
|
|
return sql;
|
|
};
|
|
|
|
// let q = '2_months;after;fromnow,1_months;after;fromnow,2024-09-01;after,2024-10-06;after,2_weeks;after;fromnow'
|
|
|
|
// ["2_months;after;fromnow", "1_months;after;fromnow", "2024-09-01;after", "2024-10-06;before", "2_weeks;after;fromnow"];
|
|
|
|
const createDateFilter = (key: string, q: string) => {
|
|
let sql = " ";
|
|
// get todays date in YYYY-MM-DD format
|
|
const queries = q.split(",");
|
|
const customRange: string[] = [];
|
|
let isAnd = true;
|
|
queries.forEach((query: string) => {
|
|
const [date, type, from] = query.split(";");
|
|
if (from) {
|
|
// Assuming type is always after
|
|
let after = "";
|
|
const [_length, unit] = date.split("_");
|
|
const length = parseInt(_length);
|
|
|
|
if (unit === "weeks") {
|
|
// get date in yyyy-mm-dd format one week from now
|
|
after = new Date(new Date().setDate(new Date().getDate() + length * 7)).toISOString().split("T")[0];
|
|
}
|
|
if (unit === "months") {
|
|
after = new Date(new Date().setDate(new Date().getDate() + length * 30)).toISOString().split("T")[0];
|
|
}
|
|
sql += ` ${isAnd ? "AND" : "OR"} ${key} >= date('${after}')`;
|
|
isAnd = false;
|
|
// sql += ` AND ${key} ${type === "after" ? ">=" : "<="} date('${date}', '${today}')`;
|
|
} else {
|
|
customRange.push(query);
|
|
}
|
|
});
|
|
|
|
if (customRange.length === 2) {
|
|
const end = customRange.find((date) => date.includes("before"))?.split(";")[0];
|
|
const start = customRange.find((date) => date.includes("after"))?.split(";")[0];
|
|
if (end && start) {
|
|
sql += ` ${isAnd ? "AND" : "OR"} ${key} BETWEEN date('${start}') AND date('${end}')`;
|
|
}
|
|
}
|
|
if (customRange.length === 1) {
|
|
sql += ` AND ${key}=date('${customRange[0].split(";")[0]}')`;
|
|
}
|
|
|
|
return sql;
|
|
};
|
|
const getSingleFilterFields = (queries: any) => {
|
|
const { order_by, cursor, per_page, group_by, sub_group_by, sub_issue, state_group, ...otherProps } =
|
|
translateQueryParams(queries);
|
|
|
|
const fields = new Set();
|
|
|
|
if (order_by && !order_by.includes("created_at") && !Object.keys(SPECIAL_ORDER_BY).includes(order_by))
|
|
fields.add(order_by.replace("-", ""));
|
|
|
|
const keys = Object.keys(otherProps);
|
|
|
|
keys.forEach((field: string) => {
|
|
if (!ARRAY_FIELDS.includes(field)) {
|
|
fields.add(field);
|
|
}
|
|
});
|
|
|
|
if (order_by?.includes("state__name")) {
|
|
fields.add("state_id");
|
|
}
|
|
if (order_by?.includes("cycle__name")) {
|
|
fields.add("cycle_id");
|
|
}
|
|
if (state_group) {
|
|
fields.add("states.'group' as state_group");
|
|
}
|
|
return Array.from(fields);
|
|
};
|
|
|
|
export const getIssueFieldsFragment = () => {
|
|
const { description_html, ...filtered } = issueSchema;
|
|
const keys = Object.keys(filtered);
|
|
const sql = ` ${keys.map((key, index) => `i.${key}`).join(`,
|
|
`)}`;
|
|
return sql;
|
|
};
|