Compare commits
6 Commits
chore/add-
...
imp/settin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136dac17fb | ||
|
|
884cb9c462 | ||
|
|
d1bd36e0a4 | ||
|
|
7c42087854 | ||
|
|
14c89dec24 | ||
|
|
b1bdf0ac11 |
@@ -1,41 +0,0 @@
|
|||||||
-- Test script to verify the sort order constraint fix
|
|
||||||
|
|
||||||
-- Test the helper function
|
|
||||||
SELECT get_sort_column_name('status'); -- Should return 'status_sort_order'
|
|
||||||
SELECT get_sort_column_name('priority'); -- Should return 'priority_sort_order'
|
|
||||||
SELECT get_sort_column_name('phase'); -- Should return 'phase_sort_order'
|
|
||||||
SELECT get_sort_column_name('members'); -- Should return 'member_sort_order'
|
|
||||||
SELECT get_sort_column_name('unknown'); -- Should return 'status_sort_order' (default)
|
|
||||||
|
|
||||||
-- Test bulk update function (example - would need real project_id and task_ids)
|
|
||||||
/*
|
|
||||||
SELECT update_task_sort_orders_bulk(
|
|
||||||
'[
|
|
||||||
{"task_id": "example-uuid", "sort_order": 1, "status_id": "status-uuid"},
|
|
||||||
{"task_id": "example-uuid-2", "sort_order": 2, "status_id": "status-uuid"}
|
|
||||||
]'::json,
|
|
||||||
'status'
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
-- Verify that sort_order constraint still exists and works
|
|
||||||
SELECT
|
|
||||||
tc.constraint_name,
|
|
||||||
tc.table_name,
|
|
||||||
kcu.column_name
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name
|
|
||||||
WHERE tc.constraint_name = 'tasks_sort_order_unique';
|
|
||||||
|
|
||||||
-- Check that new sort order columns don't have unique constraints (which is correct)
|
|
||||||
SELECT
|
|
||||||
tc.constraint_name,
|
|
||||||
tc.table_name,
|
|
||||||
kcu.column_name
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name
|
|
||||||
WHERE kcu.table_name = 'tasks'
|
|
||||||
AND kcu.column_name IN ('status_sort_order', 'priority_sort_order', 'phase_sort_order', 'member_sort_order')
|
|
||||||
AND tc.constraint_type = 'UNIQUE';
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
-- Test script to validate the separate sort order implementation
|
|
||||||
|
|
||||||
-- Check if new columns exist
|
|
||||||
SELECT column_name, data_type, is_nullable, column_default
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_name = 'tasks'
|
|
||||||
AND column_name IN ('status_sort_order', 'priority_sort_order', 'phase_sort_order', 'member_sort_order')
|
|
||||||
ORDER BY column_name;
|
|
||||||
|
|
||||||
-- Check if helper function exists
|
|
||||||
SELECT routine_name, routine_type
|
|
||||||
FROM information_schema.routines
|
|
||||||
WHERE routine_name IN ('get_sort_column_name', 'update_task_sort_orders_bulk', 'handle_task_list_sort_order_change');
|
|
||||||
|
|
||||||
-- Sample test data to verify different sort orders work
|
|
||||||
-- (This would be run after the migrations)
|
|
||||||
/*
|
|
||||||
-- Test: Tasks should have different orders for different groupings
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
sort_order,
|
|
||||||
status_sort_order,
|
|
||||||
priority_sort_order,
|
|
||||||
phase_sort_order,
|
|
||||||
member_sort_order
|
|
||||||
FROM tasks
|
|
||||||
WHERE project_id = '<test-project-id>'
|
|
||||||
ORDER BY status_sort_order;
|
|
||||||
*/
|
|
||||||
@@ -80,6 +80,37 @@ export default class LabelsController extends WorklenzControllerBase {
|
|||||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HandleExceptions()
|
||||||
|
public static async updateLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
|
const updates = [];
|
||||||
|
const values = [req.params.id, req.user?.team_id];
|
||||||
|
let paramIndex = 3;
|
||||||
|
|
||||||
|
if (req.body.name) {
|
||||||
|
updates.push(`name = $${paramIndex++}`);
|
||||||
|
values.push(req.body.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body.color) {
|
||||||
|
if (!WorklenzColorCodes.includes(req.body.color))
|
||||||
|
return res.status(400).send(new ServerResponse(false, null));
|
||||||
|
updates.push(`color_code = $${paramIndex++}`);
|
||||||
|
values.push(req.body.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
return res.status(400).send(new ServerResponse(false, "No valid fields to update"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = `UPDATE team_labels
|
||||||
|
SET ${updates.join(', ')}
|
||||||
|
WHERE id = $1
|
||||||
|
AND team_id = $2;`;
|
||||||
|
|
||||||
|
const result = await db.query(q, values);
|
||||||
|
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||||
|
}
|
||||||
|
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const q = `DELETE
|
const q = `DELETE
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ labelsApiRouter.get("/", safeControllerFunction(LabelsController.get));
|
|||||||
labelsApiRouter.get("/tasks/:id", idParamValidator, safeControllerFunction(LabelsController.getByTask));
|
labelsApiRouter.get("/tasks/:id", idParamValidator, safeControllerFunction(LabelsController.getByTask));
|
||||||
labelsApiRouter.get("/project/:id", idParamValidator, safeControllerFunction(LabelsController.getByProject));
|
labelsApiRouter.get("/project/:id", idParamValidator, safeControllerFunction(LabelsController.getByProject));
|
||||||
labelsApiRouter.put("/tasks/:id", idParamValidator, teamOwnerOrAdminValidator, safeControllerFunction(LabelsController.updateColor));
|
labelsApiRouter.put("/tasks/:id", idParamValidator, teamOwnerOrAdminValidator, safeControllerFunction(LabelsController.updateColor));
|
||||||
|
labelsApiRouter.put("/team/:id", idParamValidator, teamOwnerOrAdminValidator, safeControllerFunction(LabelsController.updateLabel));
|
||||||
labelsApiRouter.delete("/team/:id", idParamValidator, teamOwnerOrAdminValidator, safeControllerFunction(LabelsController.deleteById));
|
labelsApiRouter.delete("/team/:id", idParamValidator, teamOwnerOrAdminValidator, safeControllerFunction(LabelsController.deleteById));
|
||||||
|
|
||||||
export default labelsApiRouter;
|
export default labelsApiRouter;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const DEFAULT_ERROR_MESSAGE = "Unknown error has occurred.";
|
|||||||
export const SessionsStatus = {
|
export const SessionsStatus = {
|
||||||
IDLE: "IDLE",
|
IDLE: "IDLE",
|
||||||
STARTED: "STARTED",
|
STARTED: "STARTED",
|
||||||
ENDED: "ENDED"
|
ENDED: "ENDED",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LOG_DESCRIPTIONS = {
|
export const LOG_DESCRIPTIONS = {
|
||||||
@@ -18,6 +18,33 @@ export const LOG_DESCRIPTIONS = {
|
|||||||
PROJECT_MEMBER_REMOVED: "was removed from the project by",
|
PROJECT_MEMBER_REMOVED: "was removed from the project by",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WorklenzColorShades = {
|
||||||
|
"#154c9b": ["#0D2A50", "#112E54", "#153258", "#19365C", "#1D3A60", "#213E64", "#254268", "#29466C", "#2D4A70", "#314E74"],
|
||||||
|
"#3b7ad4": ["#224884", "#26528A", "#2A5C90", "#2E6696", "#32709C", "#367AA2", "#3A84A8", "#3E8EAE", "#4298B4", "#46A2BA"],
|
||||||
|
"#70a6f3": ["#3D5D8A", "#46679E", "#5071B2", "#597BC6", "#6385DA", "#6C8FEE", "#7699F2", "#7FA3F6", "#89ADFA", "#92B7FE"],
|
||||||
|
"#7781ca": ["#42486F", "#4C5283", "#565C97", "#6066AB", "#6A70BF", "#747AD3", "#7E84E7", "#888EFB", "#9298FF", "#9CA2FF"],
|
||||||
|
"#9877ca": ["#542D70", "#6E3A8A", "#8847A4", "#A254BE", "#BC61D8", "#D66EF2", "#E07BFC", "#EA88FF", "#F495FF", "#FEA2FF"],
|
||||||
|
"#c178c9": ["#6A2E6F", "#843B89", "#9E48A3", "#B855BD", "#D262D7", "#EC6FF1", "#F67CFB", "#FF89FF", "#FF96FF", "#FFA3FF"],
|
||||||
|
"#ee87c5": ["#832C6A", "#9D3984", "#B7469E", "#D153B8", "#EB60D2", "#FF6DEC", "#FF7AF6", "#FF87FF", "#FF94FF", "#FFA1FF"],
|
||||||
|
"#ca7881": ["#6F2C3E", "#893958", "#A34672", "#BD538C", "#D760A6", "#F16DC0", "#FB7ADA", "#FF87F4", "#FF94FF", "#FFA1FF"],
|
||||||
|
"#75c9c0": ["#3F6B66", "#497E7A", "#53918E", "#5DA4A2", "#67B7B6", "#71CBCA", "#7BDEDE", "#85F2F2", "#8FFFFF", "#99FFFF"],
|
||||||
|
"#75c997": ["#3F6B54", "#497E6A", "#53917F", "#5DA495", "#67B7AA", "#71CBBF", "#7BDED4", "#85F2E9", "#8FFFFF", "#99FFFF"],
|
||||||
|
"#80ca79": ["#456F3E", "#5A804D", "#6F935C", "#84A66B", "#99B97A", "#AECC89", "#C3DF98", "#D8F2A7", "#EDFFB6", "#FFFFC5"],
|
||||||
|
"#aacb78": ["#5F6F3E", "#7A804D", "#94935C", "#AFA66B", "#CAB97A", "#E5CC89", "#FFDF98", "#FFF2A7", "#FFFFB6", "#FFFFC5"],
|
||||||
|
"#cbbc78": ["#6F5D3E", "#8A704D", "#A4835C", "#BF966B", "#DAA97A", "#F5BC89", "#FFCF98", "#FFE2A7", "#FFF5B6", "#FFFFC5"],
|
||||||
|
"#cb9878": ["#704D3E", "#8B604D", "#A6735C", "#C1866B", "#DC997A", "#F7AC89", "#FFBF98", "#FFD2A7", "#FFE5B6", "#FFF8C5"],
|
||||||
|
"#bb774c": ["#653D27", "#80502C", "#9B6331", "#B67636", "#D1893B", "#EC9C40", "#FFAF45", "#FFC24A", "#FFD54F", "#FFE854"],
|
||||||
|
"#905b39": ["#4D2F1A", "#623C23", "#774A2C", "#8C5735", "#A1643E", "#B67147", "#CB7E50", "#E08B59", "#F59862", "#FFA56B"],
|
||||||
|
"#903737": ["#4D1A1A", "#622323", "#772C2C", "#8C3535", "#A13E3E", "#B64747", "#CB5050", "#E05959", "#F56262", "#FF6B6B"],
|
||||||
|
"#bf4949": ["#661212", "#801B1B", "#992424", "#B32D2D", "#CC3636", "#E63F3F", "#FF4848", "#FF5151", "#FF5A5A", "#FF6363"],
|
||||||
|
"#f37070": ["#853A3A", "#A04D4D", "#BA6060", "#D47373", "#EF8686", "#FF9999", "#FFA3A3", "#FFACAC", "#FFB6B6", "#FFBFBF"],
|
||||||
|
"#ff9c3c": ["#8F5614", "#AA6F1F", "#C48829", "#DFA233", "#F9BB3D", "#FFC04E", "#FFC75F", "#FFCE70", "#FFD581", "#FFDB92"],
|
||||||
|
"#fbc84c": ["#8F6D14", "#AA862F", "#C4A029", "#DFB933", "#F9D23D", "#FFD74E", "#FFDC5F", "#FFE170", "#FFE681", "#FFEB92"],
|
||||||
|
"#cbc8a1": ["#6F6D58", "#8A886F", "#A4A286", "#BFBC9D", "#DAD6B4", "#F5F0CB", "#FFFEDE", "#FFFFF2", "#FFFFCD", "#FFFFCD"],
|
||||||
|
"#a9a9a9": ["#5D5D5D", "#757575", "#8D8D8D", "#A5A5A5", "#BDBDBD", "#D5D5D5", "#EDEDED", "#F5F5F5", "#FFFFFF", "#FFFFFF"],
|
||||||
|
"#767676": ["#404040", "#4D4D4D", "#5A5A5A", "#676767", "#747474", "#818181", "#8E8E8E", "#9B9B9B", "#A8A8A8", "#B5B5B5"]
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const WorklenzColorCodes = [
|
export const WorklenzColorCodes = [
|
||||||
"#154c9b",
|
"#154c9b",
|
||||||
"#3b7ad4",
|
"#3b7ad4",
|
||||||
@@ -46,33 +73,33 @@ export const WorklenzColorCodes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const AvatarNamesMap: { [x: string]: string } = {
|
export const AvatarNamesMap: { [x: string]: string } = {
|
||||||
"A": "#154c9b",
|
A: "#154c9b",
|
||||||
"B": "#3b7ad4",
|
B: "#3b7ad4",
|
||||||
"C": "#70a6f3",
|
C: "#70a6f3",
|
||||||
"D": "#7781ca",
|
D: "#7781ca",
|
||||||
"E": "#9877ca",
|
E: "#9877ca",
|
||||||
"F": "#c178c9",
|
F: "#c178c9",
|
||||||
"G": "#ee87c5",
|
G: "#ee87c5",
|
||||||
"H": "#ca7881",
|
H: "#ca7881",
|
||||||
"I": "#75c9c0",
|
I: "#75c9c0",
|
||||||
"J": "#75c997",
|
J: "#75c997",
|
||||||
"K": "#80ca79",
|
K: "#80ca79",
|
||||||
"L": "#aacb78",
|
L: "#aacb78",
|
||||||
"M": "#cbbc78",
|
M: "#cbbc78",
|
||||||
"N": "#cb9878",
|
N: "#cb9878",
|
||||||
"O": "#bb774c",
|
O: "#bb774c",
|
||||||
"P": "#905b39",
|
P: "#905b39",
|
||||||
"Q": "#903737",
|
Q: "#903737",
|
||||||
"R": "#bf4949",
|
R: "#bf4949",
|
||||||
"S": "#f37070",
|
S: "#f37070",
|
||||||
"T": "#ff9c3c",
|
T: "#ff9c3c",
|
||||||
"U": "#fbc84c",
|
U: "#fbc84c",
|
||||||
"V": "#cbc8a1",
|
V: "#cbc8a1",
|
||||||
"W": "#a9a9a9",
|
W: "#a9a9a9",
|
||||||
"X": "#767676",
|
X: "#767676",
|
||||||
"Y": "#cb9878",
|
Y: "#cb9878",
|
||||||
"Z": "#903737",
|
Z: "#903737",
|
||||||
"+": "#9e9e9e"
|
"+": "#9e9e9e",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NumbersColorMap: { [x: string]: string } = {
|
export const NumbersColorMap: { [x: string]: string } = {
|
||||||
@@ -85,19 +112,19 @@ export const NumbersColorMap: { [x: string]: string } = {
|
|||||||
"6": "#ee87c5",
|
"6": "#ee87c5",
|
||||||
"7": "#ca7881",
|
"7": "#ca7881",
|
||||||
"8": "#75c9c0",
|
"8": "#75c9c0",
|
||||||
"9": "#75c997"
|
"9": "#75c997",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PriorityColorCodes: { [x: number]: string; } = {
|
export const PriorityColorCodes: { [x: number]: string } = {
|
||||||
0: "#2E8B57",
|
0: "#2E8B57",
|
||||||
1: "#DAA520",
|
1: "#DAA520",
|
||||||
2: "#CD5C5C"
|
2: "#CD5C5C",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PriorityColorCodesDark: { [x: number]: string; } = {
|
export const PriorityColorCodesDark: { [x: number]: string } = {
|
||||||
0: "#3CB371",
|
0: "#3CB371",
|
||||||
1: "#B8860B",
|
1: "#B8860B",
|
||||||
2: "#F08080"
|
2: "#F08080",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TASK_STATUS_TODO_COLOR = "#a9a9a9";
|
export const TASK_STATUS_TODO_COLOR = "#a9a9a9";
|
||||||
@@ -113,7 +140,6 @@ export const TASK_DUE_UPCOMING_COLOR = "#70a6f3";
|
|||||||
export const TASK_DUE_OVERDUE_COLOR = "#f37070";
|
export const TASK_DUE_OVERDUE_COLOR = "#f37070";
|
||||||
export const TASK_DUE_NO_DUE_COLOR = "#a9a9a9";
|
export const TASK_DUE_NO_DUE_COLOR = "#a9a9a9";
|
||||||
|
|
||||||
|
|
||||||
export const DEFAULT_PAGE_SIZE = 20;
|
export const DEFAULT_PAGE_SIZE = 20;
|
||||||
|
|
||||||
// S3 Credentials
|
// S3 Credentials
|
||||||
@@ -125,7 +151,8 @@ export const S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY || "";
|
|||||||
|
|
||||||
// Azure Blob Storage Credentials
|
// Azure Blob Storage Credentials
|
||||||
export const STORAGE_PROVIDER = process.env.STORAGE_PROVIDER || "s3";
|
export const STORAGE_PROVIDER = process.env.STORAGE_PROVIDER || "s3";
|
||||||
export const AZURE_STORAGE_ACCOUNT_NAME = process.env.AZURE_STORAGE_ACCOUNT_NAME;
|
export const AZURE_STORAGE_ACCOUNT_NAME =
|
||||||
|
process.env.AZURE_STORAGE_ACCOUNT_NAME;
|
||||||
export const AZURE_STORAGE_CONTAINER = process.env.AZURE_STORAGE_CONTAINER;
|
export const AZURE_STORAGE_CONTAINER = process.env.AZURE_STORAGE_CONTAINER;
|
||||||
export const AZURE_STORAGE_ACCOUNT_KEY = process.env.AZURE_STORAGE_ACCOUNT_KEY;
|
export const AZURE_STORAGE_ACCOUNT_KEY = process.env.AZURE_STORAGE_ACCOUNT_KEY;
|
||||||
export const AZURE_STORAGE_URL = process.env.AZURE_STORAGE_URL;
|
export const AZURE_STORAGE_URL = process.env.AZURE_STORAGE_URL;
|
||||||
@@ -150,12 +177,16 @@ export const TEAM_MEMBER_TREE_MAP_COLOR_ALPHA = "40";
|
|||||||
|
|
||||||
// LICENSING SERVER URLS
|
// LICENSING SERVER URLS
|
||||||
export const LOCAL_URL = "http://localhost:3001";
|
export const LOCAL_URL = "http://localhost:3001";
|
||||||
export const UAT_SERVER_URL = process.env.UAT_SERVER_URL || "https://your-uat-server-url";
|
export const UAT_SERVER_URL =
|
||||||
export const DEV_SERVER_URL = process.env.DEV_SERVER_URL || "https://your-dev-server-url";
|
process.env.UAT_SERVER_URL || "https://your-uat-server-url";
|
||||||
export const PRODUCTION_SERVER_URL = process.env.PRODUCTION_SERVER_URL || "https://your-production-server-url";
|
export const DEV_SERVER_URL =
|
||||||
|
process.env.DEV_SERVER_URL || "https://your-dev-server-url";
|
||||||
|
export const PRODUCTION_SERVER_URL =
|
||||||
|
process.env.PRODUCTION_SERVER_URL || "https://your-production-server-url";
|
||||||
|
|
||||||
// *Sync with the client
|
// *Sync with the client
|
||||||
export const PASSWORD_POLICY = "Minimum of 8 characters, with upper and lowercase and a number and a symbol.";
|
export const PASSWORD_POLICY =
|
||||||
|
"Minimum of 8 characters, with upper and lowercase and a number and a symbol.";
|
||||||
|
|
||||||
// paddle status to exclude
|
// paddle status to exclude
|
||||||
export const statusExclude = ["past_due", "paused", "deleted"];
|
export const statusExclude = ["past_due", "paused", "deleted"];
|
||||||
@@ -172,5 +203,5 @@ export const DATE_RANGES = {
|
|||||||
LAST_WEEK: "LAST_WEEK",
|
LAST_WEEK: "LAST_WEEK",
|
||||||
LAST_MONTH: "LAST_MONTH",
|
LAST_MONTH: "LAST_MONTH",
|
||||||
LAST_QUARTER: "LAST_QUARTER",
|
LAST_QUARTER: "LAST_QUARTER",
|
||||||
ALL_TIME: "ALL_TIME"
|
ALL_TIME: "ALL_TIME",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class HubSpotManager {
|
|||||||
* Load HubSpot script with dark mode support
|
* Load HubSpot script with dark mode support
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
if (!this.isProduction) return;
|
// if (!this.isProduction) return;
|
||||||
|
|
||||||
const loadHubSpot = () => {
|
const loadHubSpot = () => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
@@ -52,6 +52,7 @@ class HubSpotManager {
|
|||||||
existingStyle.remove();
|
existingStyle.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply dark mode CSS if dark theme is active
|
||||||
if (isDark) {
|
if (isDark) {
|
||||||
this.injectDarkModeCSS();
|
this.injectDarkModeCSS();
|
||||||
}
|
}
|
||||||
@@ -122,3 +123,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Make available globally for potential cleanup
|
// Make available globally for potential cleanup
|
||||||
window.HubSpotManager = hubspot;
|
window.HubSpotManager = hubspot;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add this style to ensure the chat widget uses the light color scheme
|
||||||
|
(function() {
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.innerHTML = '#hubspot-messages-iframe-container { color-scheme: light !important; }';
|
||||||
|
document.head.appendChild(style);
|
||||||
|
})();
|
||||||
@@ -6,5 +6,12 @@
|
|||||||
"reconnecting": "Jeni shkëputur nga serveri.",
|
"reconnecting": "Jeni shkëputur nga serveri.",
|
||||||
"connection-lost": "Lidhja me serverin dështoi. Ju lutemi kontrolloni lidhjen tuaj me internet.",
|
"connection-lost": "Lidhja me serverin dështoi. Ju lutemi kontrolloni lidhjen tuaj me internet.",
|
||||||
"connection-restored": "U lidhët me serverin me sukses",
|
"connection-restored": "U lidhët me serverin me sukses",
|
||||||
"cancel": "Anulo"
|
"cancel": "Anulo",
|
||||||
|
"update-available": "Worklenz u përditesua!",
|
||||||
|
"update-description": "Një version i ri i Worklenz është i disponueshëm me karakteristikat dhe përmirësimet më të fundit.",
|
||||||
|
"update-instruction": "Për eksperiencën më të mirë, ju lutemi rifreskoni faqen për të aplikuar ndryshimet e reja.",
|
||||||
|
"update-whats-new": "💡 <1>Çfarë ka të re:</1> Përmirësim i performancës, rregullime të gabimeve dhe eksperiencön e përmirësuar e përdoruesit",
|
||||||
|
"update-now": "Përditeso tani",
|
||||||
|
"update-later": "Më vonë",
|
||||||
|
"updating": "Duke u përditesuar..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,9 @@
|
|||||||
"searchPlaceholder": "Kërko sipas emrit",
|
"searchPlaceholder": "Kërko sipas emrit",
|
||||||
"emptyText": "Etiketat mund të krijohen gjatë përditësimit ose krijimit të detyrave.",
|
"emptyText": "Etiketat mund të krijohen gjatë përditësimit ose krijimit të detyrave.",
|
||||||
"pinTooltip": "Klikoni për ta fiksuar në menynë kryesore",
|
"pinTooltip": "Klikoni për ta fiksuar në menynë kryesore",
|
||||||
"colorChangeTooltip": "Klikoni për të ndryshuar ngjyrën"
|
"colorChangeTooltip": "Klikoni për të ndryshuar ngjyrën",
|
||||||
|
"pageTitle": "Menaxho Etiketat",
|
||||||
|
"deleteConfirmTitle": "Jeni i sigurt që dëshironi ta fshini këtë?",
|
||||||
|
"deleteButton": "Fshi",
|
||||||
|
"cancelButton": "Anulo"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,12 @@
|
|||||||
"reconnecting": "Vom Server getrennt.",
|
"reconnecting": "Vom Server getrennt.",
|
||||||
"connection-lost": "Verbindung zum Server fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.",
|
"connection-lost": "Verbindung zum Server fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||||
"connection-restored": "Erfolgreich mit dem Server verbunden",
|
"connection-restored": "Erfolgreich mit dem Server verbunden",
|
||||||
"cancel": "Abbrechen"
|
"cancel": "Abbrechen",
|
||||||
|
"update-available": "Worklenz aktualisiert!",
|
||||||
|
"update-description": "Eine neue Version von Worklenz ist verfügbar mit den neuesten Funktionen und Verbesserungen.",
|
||||||
|
"update-instruction": "Für die beste Erfahrung laden Sie bitte die Seite neu, um die neuen Änderungen zu übernehmen.",
|
||||||
|
"update-whats-new": "💡 <1>Was ist neu:</1> Verbesserte Leistung, Fehlerbehebungen und verbesserte Benutzererfahrung",
|
||||||
|
"update-now": "Jetzt aktualisieren",
|
||||||
|
"update-later": "Später",
|
||||||
|
"updating": "Wird aktualisiert..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,9 @@
|
|||||||
"searchPlaceholder": "Nach Name suchen",
|
"searchPlaceholder": "Nach Name suchen",
|
||||||
"emptyText": "Labels können beim Aktualisieren oder Erstellen von Aufgaben erstellt werden.",
|
"emptyText": "Labels können beim Aktualisieren oder Erstellen von Aufgaben erstellt werden.",
|
||||||
"pinTooltip": "Zum Anheften an das Hauptmenü klicken",
|
"pinTooltip": "Zum Anheften an das Hauptmenü klicken",
|
||||||
"colorChangeTooltip": "Zum Ändern der Farbe klicken"
|
"colorChangeTooltip": "Zum Ändern der Farbe klicken",
|
||||||
|
"pageTitle": "Labels verwalten",
|
||||||
|
"deleteConfirmTitle": "Sind Sie sicher, dass Sie dies löschen möchten?",
|
||||||
|
"deleteButton": "Löschen",
|
||||||
|
"cancelButton": "Abbrechen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,12 @@
|
|||||||
"reconnecting": "Disconnected from server.",
|
"reconnecting": "Disconnected from server.",
|
||||||
"connection-lost": "Failed to connect to server. Please check your internet connection.",
|
"connection-lost": "Failed to connect to server. Please check your internet connection.",
|
||||||
"connection-restored": "Connected to server successfully",
|
"connection-restored": "Connected to server successfully",
|
||||||
"cancel": "Cancel"
|
"cancel": "Cancel",
|
||||||
|
"update-available": "Worklenz Updated!",
|
||||||
|
"update-description": "A new version of Worklenz is available with the latest features and improvements.",
|
||||||
|
"update-instruction": "To get the best experience, please reload the page to apply the new changes.",
|
||||||
|
"update-whats-new": "💡 <1>What's new:</1> Enhanced performance, bug fixes, and improved user experience",
|
||||||
|
"update-now": "Update Now",
|
||||||
|
"update-later": "Later",
|
||||||
|
"updating": "Updating..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,9 @@
|
|||||||
"searchPlaceholder": "Search by name",
|
"searchPlaceholder": "Search by name",
|
||||||
"emptyText": "Labels can be created while updating or creating tasks.",
|
"emptyText": "Labels can be created while updating or creating tasks.",
|
||||||
"pinTooltip": "Click to pin this into the main menu",
|
"pinTooltip": "Click to pin this into the main menu",
|
||||||
"colorChangeTooltip": "Click to change color"
|
"colorChangeTooltip": "Click to change color",
|
||||||
|
"pageTitle": "Manage Labels",
|
||||||
|
"deleteConfirmTitle": "Are you sure you want to delete this?",
|
||||||
|
"deleteButton": "Delete",
|
||||||
|
"cancelButton": "Cancel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,12 @@
|
|||||||
"reconnecting": "Reconectando al servidor...",
|
"reconnecting": "Reconectando al servidor...",
|
||||||
"connection-lost": "Conexión perdida. Intentando reconectarse...",
|
"connection-lost": "Conexión perdida. Intentando reconectarse...",
|
||||||
"connection-restored": "Conexión restaurada. Reconectando al servidor...",
|
"connection-restored": "Conexión restaurada. Reconectando al servidor...",
|
||||||
"cancel": "Cancelar"
|
"cancel": "Cancelar",
|
||||||
|
"update-available": "¡Worklenz actualizado!",
|
||||||
|
"update-description": "Una nueva versión de Worklenz está disponible con las últimas funciones y mejoras.",
|
||||||
|
"update-instruction": "Para obtener la mejor experiencia, por favor recarga la página para aplicar los nuevos cambios.",
|
||||||
|
"update-whats-new": "💡 <1>Qué hay de nuevo:</1> Rendimiento mejorado, correcciones de errores y experiencia de usuario mejorada",
|
||||||
|
"update-now": "Actualizar ahora",
|
||||||
|
"update-later": "Más tarde",
|
||||||
|
"updating": "Actualizando..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,9 @@
|
|||||||
"searchPlaceholder": "Buscar por nombre",
|
"searchPlaceholder": "Buscar por nombre",
|
||||||
"emptyText": "Las etiquetas se pueden crear al actualizar o crear tareas.",
|
"emptyText": "Las etiquetas se pueden crear al actualizar o crear tareas.",
|
||||||
"pinTooltip": "Haz clic para fijar esto en el menú principal",
|
"pinTooltip": "Haz clic para fijar esto en el menú principal",
|
||||||
"colorChangeTooltip": "Haz clic para cambiar el color"
|
"colorChangeTooltip": "Haz clic para cambiar el color",
|
||||||
|
"pageTitle": "Administrar Etiquetas",
|
||||||
|
"deleteConfirmTitle": "¿Estás seguro de que quieres eliminar esto?",
|
||||||
|
"deleteButton": "Eliminar",
|
||||||
|
"cancelButton": "Cancelar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,12 @@
|
|||||||
"reconnecting": "Reconectando ao servidor...",
|
"reconnecting": "Reconectando ao servidor...",
|
||||||
"connection-lost": "Conexão perdida. Tentando reconectar...",
|
"connection-lost": "Conexão perdida. Tentando reconectar...",
|
||||||
"connection-restored": "Conexão restaurada. Reconectando ao servidor...",
|
"connection-restored": "Conexão restaurada. Reconectando ao servidor...",
|
||||||
"cancel": "Cancelar"
|
"cancel": "Cancelar",
|
||||||
|
"update-available": "Worklenz atualizado!",
|
||||||
|
"update-description": "Uma nova versão do Worklenz está disponível com os recursos e melhorias mais recentes.",
|
||||||
|
"update-instruction": "Para obter a melhor experiência, por favor recarregue a página para aplicar as novas mudanças.",
|
||||||
|
"update-whats-new": "💡 <1>O que há de novo:</1> Performance aprimorada, correções de bugs e experiência do usuário melhorada",
|
||||||
|
"update-now": "Atualizar agora",
|
||||||
|
"update-later": "Mais tarde",
|
||||||
|
"updating": "Atualizando..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,9 @@
|
|||||||
"searchPlaceholder": "Pesquisar por nome",
|
"searchPlaceholder": "Pesquisar por nome",
|
||||||
"emptyText": "Os rótulos podem ser criados ao atualizar ou criar tarefas.",
|
"emptyText": "Os rótulos podem ser criados ao atualizar ou criar tarefas.",
|
||||||
"pinTooltip": "Clique para fixar isso no menu principal",
|
"pinTooltip": "Clique para fixar isso no menu principal",
|
||||||
"colorChangeTooltip": "Clique para mudar a cor"
|
"colorChangeTooltip": "Clique para mudar a cor",
|
||||||
|
"pageTitle": "Gerenciar Rótulos",
|
||||||
|
"deleteConfirmTitle": "Tem certeza de que deseja excluir isto?",
|
||||||
|
"deleteButton": "Excluir",
|
||||||
|
"cancelButton": "Cancelar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,12 @@
|
|||||||
"reconnecting": "与服务器断开连接。",
|
"reconnecting": "与服务器断开连接。",
|
||||||
"connection-lost": "无法连接到服务器。请检查您的互联网连接。",
|
"connection-lost": "无法连接到服务器。请检查您的互联网连接。",
|
||||||
"connection-restored": "成功连接到服务器",
|
"connection-restored": "成功连接到服务器",
|
||||||
"cancel": "取消"
|
"cancel": "取消",
|
||||||
|
"update-available": "Worklenz 已更新!",
|
||||||
|
"update-description": "Worklenz 的新版本已可用,具有最新的功能和改进。",
|
||||||
|
"update-instruction": "为了获得最佳体验,请刷新页面以应用新更改。",
|
||||||
|
"update-whats-new": "💡 <1>新增内容:</1>性能增强、错误修复和用户体验改善",
|
||||||
|
"update-now": "立即更新",
|
||||||
|
"update-later": "稍后",
|
||||||
|
"updating": "正在更新..."
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,9 @@
|
|||||||
"searchPlaceholder": "按名称搜索",
|
"searchPlaceholder": "按名称搜索",
|
||||||
"emptyText": "标签可以在更新或创建任务时创建。",
|
"emptyText": "标签可以在更新或创建任务时创建。",
|
||||||
"pinTooltip": "点击将其固定到主菜单",
|
"pinTooltip": "点击将其固定到主菜单",
|
||||||
"colorChangeTooltip": "点击更改颜色"
|
"colorChangeTooltip": "点击更改颜色",
|
||||||
|
"pageTitle": "管理标签",
|
||||||
|
"deleteConfirmTitle": "您确定要删除这个吗?",
|
||||||
|
"deleteButton": "删除",
|
||||||
|
"cancelButton": "取消"
|
||||||
}
|
}
|
||||||
@@ -325,6 +325,12 @@ self.addEventListener('message', event => {
|
|||||||
event.ports[0].postMessage({ version: CACHE_VERSION });
|
event.ports[0].postMessage({ version: CACHE_VERSION });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'CHECK_FOR_UPDATES':
|
||||||
|
checkForUpdates().then((hasUpdates) => {
|
||||||
|
event.ports[0].postMessage({ hasUpdates });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
case 'CLEAR_CACHE':
|
case 'CLEAR_CACHE':
|
||||||
clearAllCaches().then(() => {
|
clearAllCaches().then(() => {
|
||||||
event.ports[0].postMessage({ success: true });
|
event.ports[0].postMessage({ success: true });
|
||||||
@@ -349,6 +355,44 @@ async function clearAllCaches() {
|
|||||||
console.log('Service Worker: All caches cleared');
|
console.log('Service Worker: All caches cleared');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
// Check if there's a new service worker available
|
||||||
|
const registration = await self.registration.update();
|
||||||
|
const hasNewWorker = registration.installing || registration.waiting;
|
||||||
|
|
||||||
|
if (hasNewWorker) {
|
||||||
|
console.log('Service Worker: New version detected');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if the main app files have been updated by trying to fetch index.html
|
||||||
|
// and comparing it with the cached version
|
||||||
|
try {
|
||||||
|
const cache = await caches.open(CACHE_NAMES.STATIC);
|
||||||
|
const cachedResponse = await cache.match('/');
|
||||||
|
const networkResponse = await fetch('/', { cache: 'no-cache' });
|
||||||
|
|
||||||
|
if (cachedResponse && networkResponse.ok) {
|
||||||
|
const cachedContent = await cachedResponse.text();
|
||||||
|
const networkContent = await networkResponse.text();
|
||||||
|
|
||||||
|
if (cachedContent !== networkContent) {
|
||||||
|
console.log('Service Worker: App content has changed');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Service Worker: Could not check for content updates', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service Worker: Error checking for updates', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
try {
|
try {
|
||||||
// Clear all caches
|
// Clear all caches
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import i18next from 'i18next';
|
|||||||
// Components
|
// Components
|
||||||
import ThemeWrapper from './features/theme/ThemeWrapper';
|
import ThemeWrapper from './features/theme/ThemeWrapper';
|
||||||
import ModuleErrorBoundary from './components/ModuleErrorBoundary';
|
import ModuleErrorBoundary from './components/ModuleErrorBoundary';
|
||||||
|
import { UpdateNotificationProvider } from './components/update-notification';
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
import router from './app/routes';
|
import router from './app/routes';
|
||||||
@@ -202,14 +203,16 @@ const App: React.FC = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<ModuleErrorBoundary>
|
<UpdateNotificationProvider>
|
||||||
<RouterProvider
|
<ModuleErrorBoundary>
|
||||||
router={router}
|
<RouterProvider
|
||||||
future={{
|
router={router}
|
||||||
v7_startTransition: true,
|
future={{
|
||||||
}}
|
v7_startTransition: true,
|
||||||
/>
|
}}
|
||||||
</ModuleErrorBoundary>
|
/>
|
||||||
|
</ModuleErrorBoundary>
|
||||||
|
</UpdateNotificationProvider>
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,12 +27,17 @@ export const labelsApiService = {
|
|||||||
|
|
||||||
updateColor: async (labelId: string, color: string): Promise<IServerResponse<ITaskLabel>> => {
|
updateColor: async (labelId: string, color: string): Promise<IServerResponse<ITaskLabel>> => {
|
||||||
const response = await apiClient.put<IServerResponse<ITaskLabel>>(
|
const response = await apiClient.put<IServerResponse<ITaskLabel>>(
|
||||||
`${rootUrl}/tasks/${labelId}/color`,
|
`${rootUrl}/tasks/${labelId}`,
|
||||||
{ color }
|
{ color }
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateLabel: async (labelId: string, data: { name?: string; color?: string }): Promise<IServerResponse<ITaskLabel>> => {
|
||||||
|
const response = await apiClient.put<IServerResponse<ITaskLabel>>(`${rootUrl}/team/${labelId}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
deleteById: async (labelId: string): Promise<IServerResponse<void>> => {
|
deleteById: async (labelId: string): Promise<IServerResponse<void>> => {
|
||||||
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/team/${labelId}`);
|
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/team/${labelId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallba
|
|||||||
const HomePage = lazy(() => import('@/pages/home/home-page'));
|
const HomePage = lazy(() => import('@/pages/home/home-page'));
|
||||||
const ProjectList = lazy(() => import('@/pages/projects/project-list'));
|
const ProjectList = lazy(() => import('@/pages/projects/project-list'));
|
||||||
const Schedule = lazy(() => import('@/pages/schedule/schedule'));
|
const Schedule = lazy(() => import('@/pages/schedule/schedule'));
|
||||||
const ProjectTemplateEditView = lazy(
|
|
||||||
() => import('@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView')
|
|
||||||
);
|
|
||||||
const LicenseExpired = lazy(() => import('@/pages/license-expired/license-expired'));
|
const LicenseExpired = lazy(() => import('@/pages/license-expired/license-expired'));
|
||||||
const ProjectView = lazy(() => import('@/pages/projects/projectView/project-view'));
|
const ProjectView = lazy(() => import('@/pages/projects/projectView/project-view'));
|
||||||
const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized'));
|
const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized'));
|
||||||
@@ -91,14 +89,6 @@ const mainRoutes: RouteObject[] = [
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: `settings/project-templates/edit/:templateId/:templateName`,
|
|
||||||
element: (
|
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
|
||||||
<ProjectTemplateEditView />
|
|
||||||
</Suspense>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'unauthorized',
|
path: 'unauthorized',
|
||||||
element: (
|
element: (
|
||||||
|
|||||||
@@ -10,3 +10,6 @@ export { default as LabelsSelector } from './LabelsSelector';
|
|||||||
export { default as Progress } from './Progress';
|
export { default as Progress } from './Progress';
|
||||||
export { default as Tag } from './Tag';
|
export { default as Tag } from './Tag';
|
||||||
export { default as Tooltip } from './Tooltip';
|
export { default as Tooltip } from './Tooltip';
|
||||||
|
|
||||||
|
// Update Notification Components
|
||||||
|
export * from './update-notification';
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { Tag, Typography } from '@/shared/antd-imports';
|
|||||||
import { colors } from '@/styles/colors';
|
import { colors } from '@/styles/colors';
|
||||||
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||||
import { ALPHA_CHANNEL } from '@/shared/constants';
|
import { ALPHA_CHANNEL } from '@/shared/constants';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
|
||||||
const CustomColorLabel = ({ label }: { label: ITaskLabel | null }) => {
|
const CustomColorLabel = ({ label }: { label: ITaskLabel | null }) => {
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={label?.id}
|
key={label?.id}
|
||||||
@@ -17,7 +19,7 @@ const CustomColorLabel = ({ label }: { label: ITaskLabel | null }) => {
|
|||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography.Text style={{ fontSize: 11, color: colors.darkGray }}>
|
<Typography.Text style={{ fontSize: 11, color: themeMode === 'dark' ? 'rgba(255, 255, 255, 0.85)' : colors.darkGray }}>
|
||||||
{label?.name}
|
{label?.name}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Update Notification Component
|
||||||
|
// Shows a notification when new build is available and provides update options
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal, Button, Space, Typography } from '@/shared/antd-imports';
|
||||||
|
import { ReloadOutlined, CloseOutlined, DownloadOutlined } from '@/shared/antd-imports';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useServiceWorker } from '../../utils/serviceWorkerRegistration';
|
||||||
|
|
||||||
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
interface UpdateNotificationProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onUpdate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateNotification: React.FC<UpdateNotificationProps> = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onUpdate
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||||
|
const { hardReload } = useServiceWorker();
|
||||||
|
|
||||||
|
const handleUpdate = async () => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
if (hardReload) {
|
||||||
|
await hardReload();
|
||||||
|
} else {
|
||||||
|
// Fallback to regular reload
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
onUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during update:', error);
|
||||||
|
// Fallback to regular reload
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLater = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<DownloadOutlined style={{ color: '#1890ff' }} />
|
||||||
|
<Title level={4} style={{ margin: 0, color: '#1890ff' }}>
|
||||||
|
{t('update-available')}
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={visible}
|
||||||
|
onCancel={handleLater}
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
closable={false}
|
||||||
|
maskClosable={false}
|
||||||
|
width={460}
|
||||||
|
styles={{
|
||||||
|
body: { padding: '20px 24px' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<Text style={{ fontSize: '16px', lineHeight: '1.6' }}>
|
||||||
|
{t('update-description')}
|
||||||
|
</Text>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<Text style={{ fontSize: '14px', color: '#8c8c8c' }}>
|
||||||
|
{t('update-instruction')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
background: '#f6ffed',
|
||||||
|
border: '1px solid #b7eb8f',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '12px',
|
||||||
|
marginBottom: '20px'
|
||||||
|
}}>
|
||||||
|
<Text style={{ fontSize: '13px', color: '#389e0d' }}>
|
||||||
|
{t('update-whats-new', {
|
||||||
|
interpolation: { escapeValue: false }
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
}}
|
||||||
|
size="middle"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={handleLater}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
{t('update-later')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={isUpdating}
|
||||||
|
onClick={handleUpdate}
|
||||||
|
>
|
||||||
|
{isUpdating ? t('updating') : t('update-now')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateNotification;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Update Notification Provider
|
||||||
|
// Provides global update notification management
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useUpdateChecker } from '../../hooks/useUpdateChecker';
|
||||||
|
import UpdateNotification from './UpdateNotification';
|
||||||
|
|
||||||
|
interface UpdateNotificationProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
checkInterval?: number;
|
||||||
|
enableAutoCheck?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateNotificationProvider: React.FC<UpdateNotificationProviderProps> = ({
|
||||||
|
children,
|
||||||
|
checkInterval = 5 * 60 * 1000, // 5 minutes
|
||||||
|
enableAutoCheck = true
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
showUpdateNotification,
|
||||||
|
setShowUpdateNotification,
|
||||||
|
dismissUpdate
|
||||||
|
} = useUpdateChecker({
|
||||||
|
checkInterval,
|
||||||
|
enableAutoCheck,
|
||||||
|
showNotificationOnUpdate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dismissUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = () => {
|
||||||
|
// The hardReload function in UpdateNotification will handle the actual update
|
||||||
|
setShowUpdateNotification(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<UpdateNotification
|
||||||
|
visible={showUpdateNotification}
|
||||||
|
onClose={handleClose}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateNotificationProvider;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as UpdateNotification } from './UpdateNotification';
|
||||||
|
export { default as UpdateNotificationProvider } from './UpdateNotificationProvider';
|
||||||
141
worklenz-frontend/src/hooks/useUpdateChecker.ts
Normal file
141
worklenz-frontend/src/hooks/useUpdateChecker.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Update Checker Hook
|
||||||
|
// Periodically checks for app updates and manages update notifications
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useServiceWorker } from '../utils/serviceWorkerRegistration';
|
||||||
|
|
||||||
|
interface UseUpdateCheckerOptions {
|
||||||
|
checkInterval?: number; // Check interval in milliseconds (default: 5 minutes)
|
||||||
|
enableAutoCheck?: boolean; // Enable automatic checking (default: true)
|
||||||
|
showNotificationOnUpdate?: boolean; // Show notification when update is found (default: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseUpdateCheckerReturn {
|
||||||
|
hasUpdate: boolean;
|
||||||
|
isChecking: boolean;
|
||||||
|
lastChecked: Date | null;
|
||||||
|
checkForUpdates: () => Promise<void>;
|
||||||
|
dismissUpdate: () => void;
|
||||||
|
showUpdateNotification: boolean;
|
||||||
|
setShowUpdateNotification: (show: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateChecker(options: UseUpdateCheckerOptions = {}): UseUpdateCheckerReturn {
|
||||||
|
const {
|
||||||
|
checkInterval = 5 * 60 * 1000, // 5 minutes
|
||||||
|
enableAutoCheck = true,
|
||||||
|
showNotificationOnUpdate = true
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const { checkForUpdates: serviceWorkerCheckUpdates, swManager } = useServiceWorker();
|
||||||
|
|
||||||
|
const [hasUpdate, setHasUpdate] = React.useState(false);
|
||||||
|
const [isChecking, setIsChecking] = React.useState(false);
|
||||||
|
const [lastChecked, setLastChecked] = React.useState<Date | null>(null);
|
||||||
|
const [showUpdateNotification, setShowUpdateNotification] = React.useState(false);
|
||||||
|
const [updateDismissed, setUpdateDismissed] = React.useState(false);
|
||||||
|
|
||||||
|
// Check for updates function
|
||||||
|
const checkForUpdates = React.useCallback(async () => {
|
||||||
|
if (!serviceWorkerCheckUpdates || isChecking) return;
|
||||||
|
|
||||||
|
setIsChecking(true);
|
||||||
|
try {
|
||||||
|
const hasUpdates = await serviceWorkerCheckUpdates();
|
||||||
|
setHasUpdate(hasUpdates);
|
||||||
|
setLastChecked(new Date());
|
||||||
|
|
||||||
|
// Show notification if update found and user hasn't dismissed it
|
||||||
|
if (hasUpdates && showNotificationOnUpdate && !updateDismissed) {
|
||||||
|
setShowUpdateNotification(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Update check completed:', { hasUpdates });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking for updates:', error);
|
||||||
|
} finally {
|
||||||
|
setIsChecking(false);
|
||||||
|
}
|
||||||
|
}, [serviceWorkerCheckUpdates, isChecking, showNotificationOnUpdate, updateDismissed]);
|
||||||
|
|
||||||
|
// Dismiss update notification
|
||||||
|
const dismissUpdate = React.useCallback(() => {
|
||||||
|
setUpdateDismissed(true);
|
||||||
|
setShowUpdateNotification(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set up automatic checking interval
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enableAutoCheck || !swManager) return;
|
||||||
|
|
||||||
|
// Initial check after a short delay
|
||||||
|
const initialTimeout = setTimeout(() => {
|
||||||
|
checkForUpdates();
|
||||||
|
}, 10000); // 10 seconds after component mount
|
||||||
|
|
||||||
|
// Set up interval for periodic checks
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
checkForUpdates();
|
||||||
|
}, checkInterval);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(initialTimeout);
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [enableAutoCheck, swManager, checkInterval, checkForUpdates]);
|
||||||
|
|
||||||
|
// Listen for visibility change to check for updates when user returns to tab
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enableAutoCheck) return;
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (!document.hidden && swManager) {
|
||||||
|
// Check for updates when user returns to the tab
|
||||||
|
setTimeout(() => {
|
||||||
|
checkForUpdates();
|
||||||
|
}, 2000); // 2 second delay
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [enableAutoCheck, swManager, checkForUpdates]);
|
||||||
|
|
||||||
|
// Listen for focus events to check for updates
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enableAutoCheck) return;
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (swManager && !isChecking) {
|
||||||
|
// Check for updates when window regains focus
|
||||||
|
setTimeout(() => {
|
||||||
|
checkForUpdates();
|
||||||
|
}, 1000); // 1 second delay
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('focus', handleFocus);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', handleFocus);
|
||||||
|
};
|
||||||
|
}, [enableAutoCheck, swManager, isChecking, checkForUpdates]);
|
||||||
|
|
||||||
|
// Reset dismissed state when new update is found
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (hasUpdate && updateDismissed) {
|
||||||
|
setUpdateDismissed(false);
|
||||||
|
}
|
||||||
|
}, [hasUpdate, updateDismissed]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasUpdate,
|
||||||
|
isChecking,
|
||||||
|
lastChecked,
|
||||||
|
checkForUpdates,
|
||||||
|
dismissUpdate,
|
||||||
|
showUpdateNotification,
|
||||||
|
setShowUpdateNotification
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,40 +4,25 @@ import { Outlet } from 'react-router-dom';
|
|||||||
import { useMediaQuery } from 'react-responsive';
|
import { useMediaQuery } from 'react-responsive';
|
||||||
import AdminCenterSidebar from '@/pages/admin-center/sidebar/sidebar';
|
import AdminCenterSidebar from '@/pages/admin-center/sidebar/sidebar';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|
||||||
|
|
||||||
const AdminCenterLayout: React.FC = () => {
|
const AdminCenterLayout: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const isTablet = useMediaQuery({ query: '(min-width:768px)' });
|
const isTablet = useMediaQuery({ query: '(min-width:768px)' });
|
||||||
const isMarginAvailable = useMediaQuery({ query: '(min-width: 1000px)' });
|
|
||||||
const { t } = useTranslation('admin-center/sidebar');
|
const { t } = useTranslation('admin-center/sidebar');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="my-6">
|
||||||
style={{
|
|
||||||
marginBlock: 96,
|
|
||||||
minHeight: '90vh',
|
|
||||||
marginLeft: `${isMarginAvailable ? '5%' : ''}`,
|
|
||||||
marginRight: `${isMarginAvailable ? '5%' : ''}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography.Title level={4}>{t('adminCenter')}</Typography.Title>
|
<Typography.Title level={4}>{t('adminCenter')}</Typography.Title>
|
||||||
|
|
||||||
{isTablet ? (
|
{isTablet ? (
|
||||||
<Flex
|
<Flex
|
||||||
gap={24}
|
gap={24}
|
||||||
align="flex-start"
|
align="flex-start"
|
||||||
style={{
|
className="w-full mt-6"
|
||||||
width: '100%',
|
|
||||||
marginBlockStart: 24,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Flex style={{ width: '100%', maxWidth: 240 }}>
|
<Flex className="w-full max-w-60">
|
||||||
<AdminCenterSidebar />
|
<AdminCenterSidebar />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex style={{ width: '100%' }}>
|
<Flex className="w-full">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -45,9 +30,7 @@ const AdminCenterLayout: React.FC = () => {
|
|||||||
<Flex
|
<Flex
|
||||||
vertical
|
vertical
|
||||||
gap={24}
|
gap={24}
|
||||||
style={{
|
className="mt-6"
|
||||||
marginBlockStart: 24,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<AdminCenterSidebar />
|
<AdminCenterSidebar />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -1,35 +1,25 @@
|
|||||||
import { Flex, Typography } from '@/shared/antd-imports';
|
import { Flex, Typography } from '@/shared/antd-imports';
|
||||||
import SettingsSidebar from '../pages/settings/sidebar/settings-sidebar';
|
import SettingsSidebar from '../pages/settings/sidebar/settings-sidebar';
|
||||||
import { Outlet, useNavigate } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { useMediaQuery } from 'react-responsive';
|
import { useMediaQuery } from 'react-responsive';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useAuthService } from '@/hooks/useAuth';
|
|
||||||
|
|
||||||
const SettingsLayout = () => {
|
const SettingsLayout = () => {
|
||||||
const isTablet = useMediaQuery({ query: '(min-width: 768px)' });
|
const isTablet = useMediaQuery({ query: '(min-width: 768px)' });
|
||||||
const { getCurrentSession } = useAuthService();
|
|
||||||
const currentSession = getCurrentSession();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBlock: 96, minHeight: '90vh' }}>
|
<div className="my-6 min-h-[90vh]">
|
||||||
<Typography.Title level={4}>Settings</Typography.Title>
|
<Typography.Title level={4}>Settings</Typography.Title>
|
||||||
|
|
||||||
{isTablet ? (
|
{isTablet ? (
|
||||||
<Flex
|
<Flex
|
||||||
gap={24}
|
gap={24}
|
||||||
align="flex-start"
|
align="flex-start"
|
||||||
style={{
|
className="w-full mt-6"
|
||||||
width: '100%',
|
|
||||||
marginBlockStart: 24,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Flex style={{ width: '100%', maxWidth: 240 }}>
|
<Flex className="w-full max-w-60">
|
||||||
<SettingsSidebar />
|
<SettingsSidebar />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex style={{ width: '100%' }}>
|
<Flex className="w-full">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -37,9 +27,7 @@ const SettingsLayout = () => {
|
|||||||
<Flex
|
<Flex
|
||||||
vertical
|
vertical
|
||||||
gap={24}
|
gap={24}
|
||||||
style={{
|
className="mt-6"
|
||||||
marginBlockStart: 24,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SettingsSidebar />
|
<SettingsSidebar />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const ProfileSettings = lazy(() => import('../../pages/settings/profile/profile-
|
|||||||
const NotificationsSettings = lazy(() => import('../../pages/settings/notifications/notifications-settings'));
|
const NotificationsSettings = lazy(() => import('../../pages/settings/notifications/notifications-settings'));
|
||||||
const ClientsSettings = lazy(() => import('../../pages/settings/clients/clients-settings'));
|
const ClientsSettings = lazy(() => import('../../pages/settings/clients/clients-settings'));
|
||||||
const JobTitlesSettings = lazy(() => import('@/pages/settings/job-titles/job-titles-settings'));
|
const JobTitlesSettings = lazy(() => import('@/pages/settings/job-titles/job-titles-settings'));
|
||||||
const LabelsSettings = lazy(() => import('../../pages/settings/labels/labels-settings'));
|
const LabelsSettings = lazy(() => import('../../pages/settings/labels/LabelsSettings'));
|
||||||
const CategoriesSettings = lazy(() => import('../../pages/settings/categories/categories-settings'));
|
const CategoriesSettings = lazy(() => import('../../pages/settings/categories/categories-settings'));
|
||||||
const ProjectTemplatesSettings = lazy(() => import('@/pages/settings/project-templates/project-templates-settings'));
|
const ProjectTemplatesSettings = lazy(() => import('@/pages/settings/project-templates/project-templates-settings'));
|
||||||
const TaskTemplatesSettings = lazy(() => import('@/pages/settings/task-templates/task-templates-settings'));
|
const TaskTemplatesSettings = lazy(() => import('@/pages/settings/task-templates/task-templates-settings'));
|
||||||
|
|||||||
@@ -8,22 +8,28 @@ import {
|
|||||||
TableProps,
|
TableProps,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
|
DeleteOutlined,
|
||||||
|
ExclamationCircleFilled,
|
||||||
|
SearchOutlined,
|
||||||
|
EditOutlined,
|
||||||
} from '@/shared/antd-imports';
|
} from '@/shared/antd-imports';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import PinRouteToNavbarButton from '../../../components/PinRouteToNavbarButton';
|
import PinRouteToNavbarButton from '@/components/PinRouteToNavbarButton';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DeleteOutlined, ExclamationCircleFilled, SearchOutlined } from '@/shared/antd-imports';
|
|
||||||
import { ITaskLabel } from '@/types/label.type';
|
import { ITaskLabel } from '@/types/label.type';
|
||||||
import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service';
|
import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service';
|
||||||
import CustomColorLabel from '@components/task-list-common/labelsSelector/custom-color-label';
|
import CustomColorLabel from '@components/task-list-common/labelsSelector/custom-color-label';
|
||||||
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
|
import LabelsDrawer from './labels-drawer';
|
||||||
|
|
||||||
const LabelsSettings = () => {
|
const LabelsSettings = () => {
|
||||||
const { t } = useTranslation('settings/labels');
|
const { t } = useTranslation('settings/labels');
|
||||||
useDocumentTitle('Manage Labels');
|
useDocumentTitle(t('pageTitle', 'Manage Labels'));
|
||||||
|
|
||||||
|
const [selectedLabelId, setSelectedLabelId] = useState<string | null>(null);
|
||||||
|
const [showDrawer, setShowDrawer] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [labels, setLabels] = useState<ITaskLabel[]>([]);
|
const [labels, setLabels] = useState<ITaskLabel[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -64,32 +70,62 @@ const LabelsSettings = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (id: string) => {
|
||||||
|
setSelectedLabelId(id);
|
||||||
|
setShowDrawer(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
setSelectedLabelId(null);
|
||||||
|
setShowDrawer(false);
|
||||||
|
getLabels();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// table columns
|
// table columns
|
||||||
const columns: TableProps['columns'] = [
|
const columns: TableProps['columns'] = [
|
||||||
{
|
{
|
||||||
key: 'label',
|
key: 'label',
|
||||||
title: t('labelColumn'),
|
title: t('labelColumn', 'Label'),
|
||||||
|
onCell: record => ({
|
||||||
|
onClick: () => handleEditClick(record.id!),
|
||||||
|
}),
|
||||||
render: (record: ITaskLabel) => <CustomColorLabel label={record} />,
|
render: (record: ITaskLabel) => <CustomColorLabel label={record} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'associatedTask',
|
key: 'associatedTask',
|
||||||
title: t('associatedTaskColumn'),
|
title: t('associatedTaskColumn', 'Associated Task Count'),
|
||||||
render: (record: ITaskLabel) => <Typography.Text>{record.usage}</Typography.Text>,
|
render: (record: ITaskLabel) => <Typography.Text>{record.usage}</Typography.Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actionBtns',
|
key: 'actionBtns',
|
||||||
width: 60,
|
width: 100,
|
||||||
render: (record: ITaskLabel) => (
|
render: (record: ITaskLabel) => (
|
||||||
<div className="action-button opacity-0 transition-opacity duration-200">
|
<div className="action-button opacity-0 transition-opacity duration-200">
|
||||||
<Popconfirm
|
<Flex gap={4}>
|
||||||
title="Are you sure you want to delete this?"
|
<Tooltip title={t('editTooltip', 'Edit')}>
|
||||||
icon={<ExclamationCircleFilled style={{ color: '#ff9800' }} />}
|
<Button
|
||||||
okText="Delete"
|
shape="default"
|
||||||
cancelText="Cancel"
|
icon={<EditOutlined />}
|
||||||
onConfirm={() => deleteLabel(record.id!)}
|
size="small"
|
||||||
>
|
onClick={(e) => {
|
||||||
<Button shape="default" icon={<DeleteOutlined />} size="small" />
|
e.stopPropagation();
|
||||||
</Popconfirm>
|
handleEditClick(record.id!);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title={t('deleteConfirmTitle', 'Are you sure you want to delete this?')}
|
||||||
|
icon={<ExclamationCircleFilled style={{ color: '#ff9800' }} />}
|
||||||
|
okText={t('deleteButton', 'Delete')}
|
||||||
|
cancelText={t('cancelButton', 'Cancel')}
|
||||||
|
onConfirm={() => deleteLabel(record.id!)}
|
||||||
|
>
|
||||||
|
<Tooltip title={t('deleteTooltip', 'Delete')}>
|
||||||
|
<Button shape="default" icon={<DeleteOutlined />} size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -104,12 +140,12 @@ const LabelsSettings = () => {
|
|||||||
<Input
|
<Input
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={e => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
placeholder={t('searchPlaceholder')}
|
placeholder={t('searchPlaceholder', 'Search by name')}
|
||||||
style={{ maxWidth: 232 }}
|
style={{ maxWidth: 232 }}
|
||||||
suffix={<SearchOutlined />}
|
suffix={<SearchOutlined />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip title={t('pinTooltip')} trigger={'hover'}>
|
<Tooltip title={t('pinTooltip', 'Click to pin this into the main menu')} trigger={'hover'}>
|
||||||
{/* this button pin this route to navbar */}
|
{/* this button pin this route to navbar */}
|
||||||
<PinRouteToNavbarButton name="labels" path="/worklenz/settings/labels" />
|
<PinRouteToNavbarButton name="labels" path="/worklenz/settings/labels" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -119,13 +155,17 @@ const LabelsSettings = () => {
|
|||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
locale={{
|
locale={{
|
||||||
emptyText: <Typography.Text>{t('emptyText')}</Typography.Text>,
|
emptyText: <Typography.Text>{t('emptyText', 'Labels can be created while updating or creating tasks.')}</Typography.Text>,
|
||||||
}}
|
}}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className="custom-two-colors-row-table"
|
className="custom-two-colors-row-table"
|
||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey={record => record.id!}
|
rowKey={record => record.id!}
|
||||||
|
onRow={(record) => ({
|
||||||
|
style: { cursor: 'pointer' },
|
||||||
|
onClick: () => handleEditClick(record.id!),
|
||||||
|
})}
|
||||||
pagination={{
|
pagination={{
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
defaultPageSize: 20,
|
defaultPageSize: 20,
|
||||||
@@ -133,6 +173,12 @@ const LabelsSettings = () => {
|
|||||||
size: 'small',
|
size: 'small',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LabelsDrawer
|
||||||
|
drawerOpen={showDrawer}
|
||||||
|
labelId={selectedLabelId}
|
||||||
|
drawerClosed={handleDrawerClose}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
228
worklenz-frontend/src/pages/settings/labels/labels-drawer.tsx
Normal file
228
worklenz-frontend/src/pages/settings/labels/labels-drawer.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { Button, Drawer, Form, Input, message, Typography, Flex, Dropdown } from '@/shared/antd-imports';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { theme } from 'antd';
|
||||||
|
import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service';
|
||||||
|
|
||||||
|
const WorklenzColorShades = {
|
||||||
|
"#154c9b": ["#0D2A50", "#112E54", "#153258", "#19365C", "#1D3A60", "#213E64", "#254268", "#29466C", "#2D4A70", "#314E74"],
|
||||||
|
"#3b7ad4": ["#224884", "#26528A", "#2A5C90", "#2E6696", "#32709C", "#367AA2", "#3A84A8", "#3E8EAE", "#4298B4", "#46A2BA"],
|
||||||
|
"#70a6f3": ["#3D5D8A", "#46679E", "#5071B2", "#597BC6", "#6385DA", "#6C8FEE", "#7699F2", "#7FA3F6", "#89ADFA", "#92B7FE"],
|
||||||
|
"#7781ca": ["#42486F", "#4C5283", "#565C97", "#6066AB", "#6A70BF", "#747AD3", "#7E84E7", "#888EFB", "#9298FF", "#9CA2FF"],
|
||||||
|
"#9877ca": ["#542D70", "#6E3A8A", "#8847A4", "#A254BE", "#BC61D8", "#D66EF2", "#E07BFC", "#EA88FF", "#F495FF", "#FEA2FF"],
|
||||||
|
"#c178c9": ["#6A2E6F", "#843B89", "#9E48A3", "#B855BD", "#D262D7", "#EC6FF1", "#F67CFB", "#FF89FF", "#FF96FF", "#FFA3FF"],
|
||||||
|
"#ee87c5": ["#832C6A", "#9D3984", "#B7469E", "#D153B8", "#EB60D2", "#FF6DEC", "#FF7AF6", "#FF87FF", "#FF94FF", "#FFA1FF"],
|
||||||
|
"#ca7881": ["#6F2C3E", "#893958", "#A34672", "#BD538C", "#D760A6", "#F16DC0", "#FB7ADA", "#FF87F4", "#FF94FF", "#FFA1FF"],
|
||||||
|
"#75c9c0": ["#3F6B66", "#497E7A", "#53918E", "#5DA4A2", "#67B7B6", "#71CBCA", "#7BDEDE", "#85F2F2", "#8FFFFF", "#99FFFF"],
|
||||||
|
"#75c997": ["#3F6B54", "#497E6A", "#53917F", "#5DA495", "#67B7AA", "#71CBBF", "#7BDED4", "#85F2E9", "#8FFFFF", "#99FFFF"],
|
||||||
|
"#80ca79": ["#456F3E", "#5A804D", "#6F935C", "#84A66B", "#99B97A", "#AECC89", "#C3DF98", "#D8F2A7", "#EDFFB6", "#FFFFC5"],
|
||||||
|
"#aacb78": ["#5F6F3E", "#7A804D", "#94935C", "#AFA66B", "#CAB97A", "#E5CC89", "#FFDF98", "#FFF2A7", "#FFFFB6", "#FFFFC5"],
|
||||||
|
"#cbbc78": ["#6F5D3E", "#8A704D", "#A4835C", "#BF966B", "#DAA97A", "#F5BC89", "#FFCF98", "#FFE2A7", "#FFF5B6", "#FFFFC5"],
|
||||||
|
"#cb9878": ["#704D3E", "#8B604D", "#A6735C", "#C1866B", "#DC997A", "#F7AC89", "#FFBF98", "#FFD2A7", "#FFE5B6", "#FFF8C5"],
|
||||||
|
"#bb774c": ["#653D27", "#80502C", "#9B6331", "#B67636", "#D1893B", "#EC9C40", "#FFAF45", "#FFC24A", "#FFD54F", "#FFE854"],
|
||||||
|
"#905b39": ["#4D2F1A", "#623C23", "#774A2C", "#8C5735", "#A1643E", "#B67147", "#CB7E50", "#E08B59", "#F59862", "#FFA56B"],
|
||||||
|
"#903737": ["#4D1A1A", "#622323", "#772C2C", "#8C3535", "#A13E3E", "#B64747", "#CB5050", "#E05959", "#F56262", "#FF6B6B"],
|
||||||
|
"#bf4949": ["#661212", "#801B1B", "#992424", "#B32D2D", "#CC3636", "#E63F3F", "#FF4848", "#FF5151", "#FF5A5A", "#FF6363"],
|
||||||
|
"#f37070": ["#853A3A", "#A04D4D", "#BA6060", "#D47373", "#EF8686", "#FF9999", "#FFA3A3", "#FFACAC", "#FFB6B6", "#FFBFBF"],
|
||||||
|
"#ff9c3c": ["#8F5614", "#AA6F1F", "#C48829", "#DFA233", "#F9BB3D", "#FFC04E", "#FFC75F", "#FFCE70", "#FFD581", "#FFDB92"],
|
||||||
|
"#fbc84c": ["#8F6D14", "#AA862F", "#C4A029", "#DFB933", "#F9D23D", "#FFD74E", "#FFDC5F", "#FFE170", "#FFE681", "#FFEB92"],
|
||||||
|
"#cbc8a1": ["#6F6D58", "#8A886F", "#A4A286", "#BFBC9D", "#DAD6B4", "#F5F0CB", "#FFFEDE", "#FFFFF2", "#FFFFCD", "#FFFFCD"],
|
||||||
|
"#a9a9a9": ["#5D5D5D", "#757575", "#8D8D8D", "#A5A5A5", "#BDBDBD", "#D5D5D5", "#EDEDED", "#F5F5F5", "#FFFFFF", "#FFFFFF"],
|
||||||
|
"#767676": ["#404040", "#4D4D4D", "#5A5A5A", "#676767", "#747474", "#818181", "#8E8E8E", "#9B9B9B", "#A8A8A8", "#B5B5B5"]
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Flatten the color shades into a single array for the color picker
|
||||||
|
const WorklenzColorCodes = Object.values(WorklenzColorShades).flat();
|
||||||
|
|
||||||
|
type LabelsDrawerProps = {
|
||||||
|
drawerOpen: boolean;
|
||||||
|
labelId: string | null;
|
||||||
|
drawerClosed: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabelsDrawer = ({
|
||||||
|
drawerOpen = false,
|
||||||
|
labelId = null,
|
||||||
|
drawerClosed,
|
||||||
|
}: LabelsDrawerProps) => {
|
||||||
|
const { t } = useTranslation('settings/labels');
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (labelId) {
|
||||||
|
getLabelById(labelId);
|
||||||
|
} else {
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({ color_code: Object.keys(WorklenzColorShades)[0] }); // Set default color
|
||||||
|
}
|
||||||
|
}, [labelId, form]);
|
||||||
|
|
||||||
|
const getLabelById = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await labelsApiService.getLabels();
|
||||||
|
if (response.done) {
|
||||||
|
const label = response.body.find((l: any) => l.id === id);
|
||||||
|
if (label) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: label.name,
|
||||||
|
color_code: label.color_code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(t('fetchLabelErrorMessage', 'Failed to fetch label'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (values: { name: string; color_code: string }) => {
|
||||||
|
try {
|
||||||
|
if (labelId) {
|
||||||
|
const response = await labelsApiService.updateLabel(labelId, {
|
||||||
|
name: values.name,
|
||||||
|
color: values.color_code,
|
||||||
|
});
|
||||||
|
if (response.done) {
|
||||||
|
message.success(t('updateLabelSuccessMessage', 'Label updated successfully'));
|
||||||
|
drawerClosed();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For creating new labels, we'd need a create API endpoint
|
||||||
|
message.info(t('createNotSupported', 'Creating new labels is done through tasks'));
|
||||||
|
drawerClosed();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(labelId ? t('updateLabelErrorMessage', 'Failed to update label') : t('createLabelErrorMessage', 'Failed to create label'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
form.resetFields();
|
||||||
|
drawerClosed();
|
||||||
|
};
|
||||||
|
|
||||||
|
const ColorPicker = ({ value, onChange }: { value?: string; onChange?: (color: string) => void }) => (
|
||||||
|
<Dropdown
|
||||||
|
dropdownRender={() => (
|
||||||
|
<div style={{
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: token.colorBgElevated,
|
||||||
|
borderRadius: token.borderRadius,
|
||||||
|
boxShadow: token.boxShadowSecondary,
|
||||||
|
border: `1px solid ${token.colorBorder}`,
|
||||||
|
width: 400,
|
||||||
|
maxHeight: 500,
|
||||||
|
overflowY: 'auto'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(10, 1fr)',
|
||||||
|
gap: 6,
|
||||||
|
justifyItems: 'center'
|
||||||
|
}}>
|
||||||
|
{WorklenzColorCodes.map((color) => (
|
||||||
|
<div
|
||||||
|
key={color}
|
||||||
|
style={{
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
backgroundColor: color,
|
||||||
|
borderRadius: 2,
|
||||||
|
border: value === color ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorder}`,
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
onClick={() => onChange?.(color)}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (value !== color) {
|
||||||
|
e.currentTarget.style.transform = 'scale(1.2)';
|
||||||
|
e.currentTarget.style.boxShadow = token.boxShadow;
|
||||||
|
e.currentTarget.style.zIndex = '10';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'scale(1)';
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
e.currentTarget.style.zIndex = '1';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
trigger={['click']}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
backgroundColor: value || Object.keys(WorklenzColorShades)[0],
|
||||||
|
borderRadius: 4,
|
||||||
|
border: `1px solid ${token.colorBorder}`,
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.boxShadow = token.boxShadow;
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={
|
||||||
|
<Typography.Text style={{ fontWeight: 500, fontSize: 16 }}>
|
||||||
|
{labelId ? t('updateLabelDrawerTitle', 'Edit Label') : t('createLabelDrawerTitle', 'Create Label')}
|
||||||
|
</Typography.Text>
|
||||||
|
}
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
destroyOnClose
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleFormSubmit}>
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label={t('nameLabel', 'Name')}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('nameRequiredMessage', 'Please enter a label name'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder={t('namePlaceholder', 'Enter label name')} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="color_code"
|
||||||
|
label={t('colorLabel', 'Color')}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('colorRequiredMessage', 'Please select a color'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ColorPicker />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Flex justify="end" gap={8}>
|
||||||
|
<Button onClick={handleClose}>
|
||||||
|
{t('cancelButton', 'Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
{labelId ? t('updateButton', 'Update') : t('createButton', 'Create')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Form>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LabelsDrawer;
|
||||||
@@ -313,3 +313,293 @@ export const durations: IRPTDuration[] = [
|
|||||||
dates: '',
|
dates: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const WorklenzColorCodes = [
|
||||||
|
// Row 1: Slate/Gray spectrum
|
||||||
|
'#0f172a',
|
||||||
|
'#1e293b',
|
||||||
|
'#334155',
|
||||||
|
'#475569',
|
||||||
|
'#64748b',
|
||||||
|
'#94a3b8',
|
||||||
|
'#cbd5e1',
|
||||||
|
'#e2e8f0',
|
||||||
|
'#f1f5f9',
|
||||||
|
'#f8fafc',
|
||||||
|
'#ffffff',
|
||||||
|
'#000000',
|
||||||
|
'#1a1a1a',
|
||||||
|
'#2d2d30',
|
||||||
|
'#3e3e42',
|
||||||
|
'#525252',
|
||||||
|
|
||||||
|
// Row 2: Blue spectrum - dark to light
|
||||||
|
'#0c4a6e',
|
||||||
|
'#075985',
|
||||||
|
'#0369a1',
|
||||||
|
'#0284c7',
|
||||||
|
'#0ea5e9',
|
||||||
|
'#38bdf8',
|
||||||
|
'#7dd3fc',
|
||||||
|
'#bae6fd',
|
||||||
|
'#e0f2fe',
|
||||||
|
'#f0f9ff',
|
||||||
|
'#1e3a8a',
|
||||||
|
'#1d4ed8',
|
||||||
|
'#2563eb',
|
||||||
|
'#3b82f6',
|
||||||
|
'#60a5fa',
|
||||||
|
'#93c5fd',
|
||||||
|
|
||||||
|
// Row 3: Indigo/Violet spectrum
|
||||||
|
'#312e81',
|
||||||
|
'#3730a3',
|
||||||
|
'#4338ca',
|
||||||
|
'#4f46e5',
|
||||||
|
'#6366f1',
|
||||||
|
'#818cf8',
|
||||||
|
'#a5b4fc',
|
||||||
|
'#c7d2fe',
|
||||||
|
'#e0e7ff',
|
||||||
|
'#eef2ff',
|
||||||
|
'#581c87',
|
||||||
|
'#6b21a8',
|
||||||
|
'#7c3aed',
|
||||||
|
'#8b5cf6',
|
||||||
|
'#a78bfa',
|
||||||
|
'#c4b5fd',
|
||||||
|
|
||||||
|
// Row 4: Purple/Fuchsia spectrum
|
||||||
|
'#701a75',
|
||||||
|
'#86198f',
|
||||||
|
'#a21caf',
|
||||||
|
'#c026d3',
|
||||||
|
'#d946ef',
|
||||||
|
'#e879f9',
|
||||||
|
'#f0abfc',
|
||||||
|
'#f3e8ff',
|
||||||
|
'#faf5ff',
|
||||||
|
'#fdf4ff',
|
||||||
|
'#831843',
|
||||||
|
'#be185d',
|
||||||
|
'#e11d48',
|
||||||
|
'#f43f5e',
|
||||||
|
'#fb7185',
|
||||||
|
'#fda4af',
|
||||||
|
|
||||||
|
// Row 5: Pink/Rose spectrum
|
||||||
|
'#9f1239',
|
||||||
|
'#be123c',
|
||||||
|
'#e11d48',
|
||||||
|
'#f43f5e',
|
||||||
|
'#fb7185',
|
||||||
|
'#fda4af',
|
||||||
|
'#fecdd3',
|
||||||
|
'#fed7d7',
|
||||||
|
'#fef2f2',
|
||||||
|
'#fff1f2',
|
||||||
|
'#450a0a',
|
||||||
|
'#7f1d1d',
|
||||||
|
'#991b1b',
|
||||||
|
'#dc2626',
|
||||||
|
'#ef4444',
|
||||||
|
'#f87171',
|
||||||
|
|
||||||
|
// Row 6: Red spectrum
|
||||||
|
'#7f1d1d',
|
||||||
|
'#991b1b',
|
||||||
|
'#dc2626',
|
||||||
|
'#ef4444',
|
||||||
|
'#f87171',
|
||||||
|
'#fca5a5',
|
||||||
|
'#fecaca',
|
||||||
|
'#fef2f2',
|
||||||
|
'#fffbeb',
|
||||||
|
'#fefce8',
|
||||||
|
'#92400e',
|
||||||
|
'#a16207',
|
||||||
|
'#ca8a04',
|
||||||
|
'#eab308',
|
||||||
|
'#facc15',
|
||||||
|
'#fef08a',
|
||||||
|
|
||||||
|
// Row 7: Orange spectrum
|
||||||
|
'#9a3412',
|
||||||
|
'#c2410c',
|
||||||
|
'#ea580c',
|
||||||
|
'#f97316',
|
||||||
|
'#fb923c',
|
||||||
|
'#fdba74',
|
||||||
|
'#fed7aa',
|
||||||
|
'#ffedd5',
|
||||||
|
'#fff7ed',
|
||||||
|
'#fffbeb',
|
||||||
|
'#78350f',
|
||||||
|
'#92400e',
|
||||||
|
'#c2410c',
|
||||||
|
'#ea580c',
|
||||||
|
'#f97316',
|
||||||
|
'#fb923c',
|
||||||
|
|
||||||
|
// Row 8: Amber/Yellow spectrum
|
||||||
|
'#451a03',
|
||||||
|
'#78350f',
|
||||||
|
'#92400e',
|
||||||
|
'#a16207',
|
||||||
|
'#ca8a04',
|
||||||
|
'#eab308',
|
||||||
|
'#facc15',
|
||||||
|
'#fef08a',
|
||||||
|
'#fefce8',
|
||||||
|
'#fffbeb',
|
||||||
|
'#365314',
|
||||||
|
'#4d7c0f',
|
||||||
|
'#65a30d',
|
||||||
|
'#84cc16',
|
||||||
|
'#a3e635',
|
||||||
|
'#bef264',
|
||||||
|
|
||||||
|
// Row 9: Lime/Green spectrum
|
||||||
|
'#1a2e05',
|
||||||
|
'#365314',
|
||||||
|
'#4d7c0f',
|
||||||
|
'#65a30d',
|
||||||
|
'#84cc16',
|
||||||
|
'#a3e635',
|
||||||
|
'#bef264',
|
||||||
|
'#d9f99d',
|
||||||
|
'#ecfccb',
|
||||||
|
'#f7fee7',
|
||||||
|
'#14532d',
|
||||||
|
'#166534',
|
||||||
|
'#15803d',
|
||||||
|
'#16a34a',
|
||||||
|
'#22c55e',
|
||||||
|
'#4ade80',
|
||||||
|
|
||||||
|
// Row 10: Emerald spectrum
|
||||||
|
'#064e3b',
|
||||||
|
'#065f46',
|
||||||
|
'#047857',
|
||||||
|
'#059669',
|
||||||
|
'#10b981',
|
||||||
|
'#34d399',
|
||||||
|
'#6ee7b7',
|
||||||
|
'#a7f3d0',
|
||||||
|
'#d1fae5',
|
||||||
|
'#ecfdf5',
|
||||||
|
'#0f766e',
|
||||||
|
'#0d9488',
|
||||||
|
'#14b8a6',
|
||||||
|
'#2dd4bf',
|
||||||
|
'#5eead4',
|
||||||
|
'#99f6e4',
|
||||||
|
|
||||||
|
// Row 11: Teal/Cyan spectrum
|
||||||
|
'#134e4a',
|
||||||
|
'#155e75',
|
||||||
|
'#0891b2',
|
||||||
|
'#0e7490',
|
||||||
|
'#0284c7',
|
||||||
|
'#0ea5e9',
|
||||||
|
'#22d3ee',
|
||||||
|
'#67e8f9',
|
||||||
|
'#a5f3fc',
|
||||||
|
'#cffafe',
|
||||||
|
'#164e63',
|
||||||
|
'#0c4a6e',
|
||||||
|
'#075985',
|
||||||
|
'#0369a1',
|
||||||
|
'#0284c7',
|
||||||
|
'#0ea5e9',
|
||||||
|
|
||||||
|
// Row 12: Sky spectrum
|
||||||
|
'#0c4a6e',
|
||||||
|
'#075985',
|
||||||
|
'#0369a1',
|
||||||
|
'#0284c7',
|
||||||
|
'#0ea5e9',
|
||||||
|
'#38bdf8',
|
||||||
|
'#7dd3fc',
|
||||||
|
'#bae6fd',
|
||||||
|
'#e0f2fe',
|
||||||
|
'#f0f9ff',
|
||||||
|
'#1e40af',
|
||||||
|
'#1d4ed8',
|
||||||
|
'#2563eb',
|
||||||
|
'#3b82f6',
|
||||||
|
'#60a5fa',
|
||||||
|
'#93c5fd',
|
||||||
|
|
||||||
|
// Row 13: Warm grays and browns
|
||||||
|
'#292524',
|
||||||
|
'#44403c',
|
||||||
|
'#57534e',
|
||||||
|
'#78716c',
|
||||||
|
'#a8a29e',
|
||||||
|
'#d6d3d1',
|
||||||
|
'#e7e5e4',
|
||||||
|
'#f5f5f4',
|
||||||
|
'#fafaf9',
|
||||||
|
'#ffffff',
|
||||||
|
'#7c2d12',
|
||||||
|
'#9a3412',
|
||||||
|
'#c2410c',
|
||||||
|
'#ea580c',
|
||||||
|
'#f97316',
|
||||||
|
'#fb923c',
|
||||||
|
|
||||||
|
// Row 14: Cool grays
|
||||||
|
'#111827',
|
||||||
|
'#1f2937',
|
||||||
|
'#374151',
|
||||||
|
'#4b5563',
|
||||||
|
'#6b7280',
|
||||||
|
'#9ca3af',
|
||||||
|
'#d1d5db',
|
||||||
|
'#e5e7eb',
|
||||||
|
'#f3f4f6',
|
||||||
|
'#f9fafb',
|
||||||
|
'#030712',
|
||||||
|
'#0c0a09',
|
||||||
|
'#1c1917',
|
||||||
|
'#292524',
|
||||||
|
'#44403c',
|
||||||
|
'#57534e',
|
||||||
|
|
||||||
|
// Row 15: Neutral spectrum
|
||||||
|
'#171717',
|
||||||
|
'#262626',
|
||||||
|
'#404040',
|
||||||
|
'#525252',
|
||||||
|
'#737373',
|
||||||
|
'#a3a3a3',
|
||||||
|
'#d4d4d4',
|
||||||
|
'#e5e5e5',
|
||||||
|
'#f5f5f5',
|
||||||
|
'#fafafa',
|
||||||
|
'#09090b',
|
||||||
|
'#18181b',
|
||||||
|
'#27272a',
|
||||||
|
'#3f3f46',
|
||||||
|
'#52525b',
|
||||||
|
'#71717a',
|
||||||
|
|
||||||
|
// Row 16: Extended colors
|
||||||
|
'#a1a1aa',
|
||||||
|
'#d4d4d8',
|
||||||
|
'#e4e4e7',
|
||||||
|
'#f4f4f5',
|
||||||
|
'#fafafa',
|
||||||
|
'#27272a',
|
||||||
|
'#3f3f46',
|
||||||
|
'#52525b',
|
||||||
|
'#71717a',
|
||||||
|
'#a1a1aa',
|
||||||
|
'#d4d4d8',
|
||||||
|
'#e4e4e7',
|
||||||
|
'#f4f4f5',
|
||||||
|
'#fafafa',
|
||||||
|
'#ffffff',
|
||||||
|
'#000000',
|
||||||
|
];
|
||||||
|
|||||||
@@ -198,6 +198,17 @@ export class ServiceWorkerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
async checkForUpdates(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.sendMessage('CHECK_FOR_UPDATES');
|
||||||
|
return response.hasUpdates;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check for updates:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Force update service worker
|
// Force update service worker
|
||||||
async forceUpdate(): Promise<void> {
|
async forceUpdate(): Promise<void> {
|
||||||
if (!this.registration) return;
|
if (!this.registration) return;
|
||||||
@@ -212,6 +223,27 @@ export class ServiceWorkerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Perform hard reload (clear cache and reload)
|
||||||
|
async hardReload(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Clear all caches first
|
||||||
|
await this.clearCache();
|
||||||
|
|
||||||
|
// Force update the service worker
|
||||||
|
if (this.registration) {
|
||||||
|
await this.registration.update();
|
||||||
|
await this.sendMessage('SKIP_WAITING');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform hard reload by clearing browser cache
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to perform hard reload:', error);
|
||||||
|
// Fallback to regular reload
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if app is running offline
|
// Check if app is running offline
|
||||||
isOffline(): boolean {
|
isOffline(): boolean {
|
||||||
return !navigator.onLine;
|
return !navigator.onLine;
|
||||||
@@ -268,6 +300,8 @@ export function useServiceWorker() {
|
|||||||
swManager,
|
swManager,
|
||||||
clearCache: () => swManager?.clearCache(),
|
clearCache: () => swManager?.clearCache(),
|
||||||
forceUpdate: () => swManager?.forceUpdate(),
|
forceUpdate: () => swManager?.forceUpdate(),
|
||||||
|
hardReload: () => swManager?.hardReload(),
|
||||||
|
checkForUpdates: () => swManager?.checkForUpdates(),
|
||||||
getVersion: () => swManager?.getVersion(),
|
getVersion: () => swManager?.getVersion(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user