diff --git a/worklenz-backend/src/controllers/labels-controller.ts b/worklenz-backend/src/controllers/labels-controller.ts index 414c31d3..5e3af115 100644 --- a/worklenz-backend/src/controllers/labels-controller.ts +++ b/worklenz-backend/src/controllers/labels-controller.ts @@ -80,6 +80,37 @@ export default class LabelsController extends WorklenzControllerBase { return res.status(200).send(new ServerResponse(true, result.rows)); } + @HandleExceptions() + public static async updateLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + 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() public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `DELETE diff --git a/worklenz-backend/src/routes/apis/labels-api-router.ts b/worklenz-backend/src/routes/apis/labels-api-router.ts index 8f6930c2..57b395f7 100644 --- a/worklenz-backend/src/routes/apis/labels-api-router.ts +++ b/worklenz-backend/src/routes/apis/labels-api-router.ts @@ -11,6 +11,7 @@ labelsApiRouter.get("/", safeControllerFunction(LabelsController.get)); labelsApiRouter.get("/tasks/:id", idParamValidator, safeControllerFunction(LabelsController.getByTask)); labelsApiRouter.get("/project/:id", idParamValidator, safeControllerFunction(LabelsController.getByProject)); 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)); export default labelsApiRouter; diff --git a/worklenz-backend/src/shared/constants.ts b/worklenz-backend/src/shared/constants.ts index badc5343..61166a2e 100644 --- a/worklenz-backend/src/shared/constants.ts +++ b/worklenz-backend/src/shared/constants.ts @@ -6,7 +6,7 @@ export const DEFAULT_ERROR_MESSAGE = "Unknown error has occurred."; export const SessionsStatus = { IDLE: "IDLE", STARTED: "STARTED", - ENDED: "ENDED" + ENDED: "ENDED", }; export const LOG_DESCRIPTIONS = { @@ -18,6 +18,33 @@ export const LOG_DESCRIPTIONS = { 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 = [ "#154c9b", "#3b7ad4", @@ -46,33 +73,33 @@ export const WorklenzColorCodes = [ ]; export const AvatarNamesMap: { [x: string]: string } = { - "A": "#154c9b", - "B": "#3b7ad4", - "C": "#70a6f3", - "D": "#7781ca", - "E": "#9877ca", - "F": "#c178c9", - "G": "#ee87c5", - "H": "#ca7881", - "I": "#75c9c0", - "J": "#75c997", - "K": "#80ca79", - "L": "#aacb78", - "M": "#cbbc78", - "N": "#cb9878", - "O": "#bb774c", - "P": "#905b39", - "Q": "#903737", - "R": "#bf4949", - "S": "#f37070", - "T": "#ff9c3c", - "U": "#fbc84c", - "V": "#cbc8a1", - "W": "#a9a9a9", - "X": "#767676", - "Y": "#cb9878", - "Z": "#903737", - "+": "#9e9e9e" + A: "#154c9b", + B: "#3b7ad4", + C: "#70a6f3", + D: "#7781ca", + E: "#9877ca", + F: "#c178c9", + G: "#ee87c5", + H: "#ca7881", + I: "#75c9c0", + J: "#75c997", + K: "#80ca79", + L: "#aacb78", + M: "#cbbc78", + N: "#cb9878", + O: "#bb774c", + P: "#905b39", + Q: "#903737", + R: "#bf4949", + S: "#f37070", + T: "#ff9c3c", + U: "#fbc84c", + V: "#cbc8a1", + W: "#a9a9a9", + X: "#767676", + Y: "#cb9878", + Z: "#903737", + "+": "#9e9e9e", }; export const NumbersColorMap: { [x: string]: string } = { @@ -85,19 +112,19 @@ export const NumbersColorMap: { [x: string]: string } = { "6": "#ee87c5", "7": "#ca7881", "8": "#75c9c0", - "9": "#75c997" + "9": "#75c997", }; -export const PriorityColorCodes: { [x: number]: string; } = { +export const PriorityColorCodes: { [x: number]: string } = { 0: "#2E8B57", 1: "#DAA520", - 2: "#CD5C5C" + 2: "#CD5C5C", }; -export const PriorityColorCodesDark: { [x: number]: string; } = { +export const PriorityColorCodesDark: { [x: number]: string } = { 0: "#3CB371", 1: "#B8860B", - 2: "#F08080" + 2: "#F08080", }; 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_NO_DUE_COLOR = "#a9a9a9"; - export const DEFAULT_PAGE_SIZE = 20; // S3 Credentials @@ -125,7 +151,8 @@ export const S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY || ""; // Azure Blob Storage Credentials 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_ACCOUNT_KEY = process.env.AZURE_STORAGE_ACCOUNT_KEY; export const AZURE_STORAGE_URL = process.env.AZURE_STORAGE_URL; @@ -136,7 +163,7 @@ export function getStorageUrl() { console.warn("AZURE_STORAGE_URL is not defined, falling back to S3_URL"); return S3_URL; } - + // Return just the base Azure Blob Storage URL // AZURE_STORAGE_URL should be in the format: https://storageaccountname.blob.core.windows.net return `${AZURE_STORAGE_URL}/${AZURE_STORAGE_CONTAINER}`; @@ -150,12 +177,16 @@ export const TEAM_MEMBER_TREE_MAP_COLOR_ALPHA = "40"; // LICENSING SERVER URLS export const LOCAL_URL = "http://localhost:3001"; -export const UAT_SERVER_URL = process.env.UAT_SERVER_URL || "https://your-uat-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"; +export const UAT_SERVER_URL = + process.env.UAT_SERVER_URL || "https://your-uat-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 -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 export const statusExclude = ["past_due", "paused", "deleted"]; @@ -172,5 +203,5 @@ export const DATE_RANGES = { LAST_WEEK: "LAST_WEEK", LAST_MONTH: "LAST_MONTH", LAST_QUARTER: "LAST_QUARTER", - ALL_TIME: "ALL_TIME" + ALL_TIME: "ALL_TIME", }; diff --git a/worklenz-frontend/public/locales/alb/settings/labels.json b/worklenz-frontend/public/locales/alb/settings/labels.json index 40e6361b..fe8cb40a 100644 --- a/worklenz-frontend/public/locales/alb/settings/labels.json +++ b/worklenz-frontend/public/locales/alb/settings/labels.json @@ -7,5 +7,9 @@ "searchPlaceholder": "Kërko sipas emrit", "emptyText": "Etiketat mund të krijohen gjatë përditësimit ose krijimit të detyrave.", "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" } diff --git a/worklenz-frontend/public/locales/de/settings/labels.json b/worklenz-frontend/public/locales/de/settings/labels.json index 18b6a021..8514b5cd 100644 --- a/worklenz-frontend/public/locales/de/settings/labels.json +++ b/worklenz-frontend/public/locales/de/settings/labels.json @@ -7,5 +7,9 @@ "searchPlaceholder": "Nach Name suchen", "emptyText": "Labels können beim Aktualisieren oder Erstellen von Aufgaben erstellt werden.", "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" } diff --git a/worklenz-frontend/public/locales/en/settings/labels.json b/worklenz-frontend/public/locales/en/settings/labels.json index 5c3d2479..4e1e173c 100644 --- a/worklenz-frontend/public/locales/en/settings/labels.json +++ b/worklenz-frontend/public/locales/en/settings/labels.json @@ -7,5 +7,9 @@ "searchPlaceholder": "Search by name", "emptyText": "Labels can be created while updating or creating tasks.", "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" } diff --git a/worklenz-frontend/public/locales/es/settings/labels.json b/worklenz-frontend/public/locales/es/settings/labels.json index 22cd9532..fa0f3364 100644 --- a/worklenz-frontend/public/locales/es/settings/labels.json +++ b/worklenz-frontend/public/locales/es/settings/labels.json @@ -7,5 +7,9 @@ "searchPlaceholder": "Buscar por nombre", "emptyText": "Las etiquetas se pueden crear al actualizar o crear tareas.", "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" } diff --git a/worklenz-frontend/public/locales/pt/settings/labels.json b/worklenz-frontend/public/locales/pt/settings/labels.json index 737dccef..20c5dc6b 100644 --- a/worklenz-frontend/public/locales/pt/settings/labels.json +++ b/worklenz-frontend/public/locales/pt/settings/labels.json @@ -7,5 +7,9 @@ "searchPlaceholder": "Pesquisar por nome", "emptyText": "Os rótulos podem ser criados ao atualizar ou criar tarefas.", "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" } diff --git a/worklenz-frontend/public/locales/zh/settings/labels.json b/worklenz-frontend/public/locales/zh/settings/labels.json index ab0d01cd..af3310b7 100644 --- a/worklenz-frontend/public/locales/zh/settings/labels.json +++ b/worklenz-frontend/public/locales/zh/settings/labels.json @@ -7,5 +7,9 @@ "searchPlaceholder": "按名称搜索", "emptyText": "标签可以在更新或创建任务时创建。", "pinTooltip": "点击将其固定到主菜单", - "colorChangeTooltip": "点击更改颜色" + "colorChangeTooltip": "点击更改颜色", + "pageTitle": "管理标签", + "deleteConfirmTitle": "您确定要删除这个吗?", + "deleteButton": "删除", + "cancelButton": "取消" } \ No newline at end of file diff --git a/worklenz-frontend/src/api/taskAttributes/labels/labels.api.service.ts b/worklenz-frontend/src/api/taskAttributes/labels/labels.api.service.ts index c9e36ff4..fd9a9259 100644 --- a/worklenz-frontend/src/api/taskAttributes/labels/labels.api.service.ts +++ b/worklenz-frontend/src/api/taskAttributes/labels/labels.api.service.ts @@ -27,12 +27,17 @@ export const labelsApiService = { updateColor: async (labelId: string, color: string): Promise> => { const response = await apiClient.put>( - `${rootUrl}/tasks/${labelId}/color`, + `${rootUrl}/tasks/${labelId}`, { color } ); return response.data; }, + updateLabel: async (labelId: string, data: { name?: string; color?: string }): Promise> => { + const response = await apiClient.put>(`${rootUrl}/team/${labelId}`, data); + return response.data; + }, + deleteById: async (labelId: string): Promise> => { const response = await apiClient.delete>(`${rootUrl}/team/${labelId}`); return response.data; diff --git a/worklenz-frontend/src/app/routes/main-routes.tsx b/worklenz-frontend/src/app/routes/main-routes.tsx index 4c96c8f9..b385a66d 100644 --- a/worklenz-frontend/src/app/routes/main-routes.tsx +++ b/worklenz-frontend/src/app/routes/main-routes.tsx @@ -11,9 +11,7 @@ import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallba const HomePage = lazy(() => import('@/pages/home/home-page')); const ProjectList = lazy(() => import('@/pages/projects/project-list')); 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 ProjectView = lazy(() => import('@/pages/projects/projectView/project-view')); const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized')); @@ -91,14 +89,6 @@ const mainRoutes: RouteObject[] = [ ), }, - { - path: `settings/project-templates/edit/:templateId/:templateName`, - element: ( - }> - - - ), - }, { path: 'unauthorized', element: ( diff --git a/worklenz-frontend/src/components/task-list-common/labelsSelector/custom-color-label.tsx b/worklenz-frontend/src/components/task-list-common/labelsSelector/custom-color-label.tsx index aff1f4c4..c7773de2 100644 --- a/worklenz-frontend/src/components/task-list-common/labelsSelector/custom-color-label.tsx +++ b/worklenz-frontend/src/components/task-list-common/labelsSelector/custom-color-label.tsx @@ -2,8 +2,10 @@ import { Tag, Typography } from '@/shared/antd-imports'; import { colors } from '@/styles/colors'; import { ITaskLabel } from '@/types/tasks/taskLabel.types'; import { ALPHA_CHANNEL } from '@/shared/constants'; +import { useAppSelector } from '@/hooks/useAppSelector'; const CustomColorLabel = ({ label }: { label: ITaskLabel | null }) => { + const themeMode = useAppSelector(state => state.themeReducer.mode); return ( { fontSize: 11, }} > - + {label?.name} diff --git a/worklenz-frontend/src/lib/settings/settings-constants.ts b/worklenz-frontend/src/lib/settings/settings-constants.ts index 04305a8e..95149bd2 100644 --- a/worklenz-frontend/src/lib/settings/settings-constants.ts +++ b/worklenz-frontend/src/lib/settings/settings-constants.ts @@ -18,7 +18,7 @@ const ProfileSettings = lazy(() => import('../../pages/settings/profile/profile- const NotificationsSettings = lazy(() => import('../../pages/settings/notifications/notifications-settings')); const ClientsSettings = lazy(() => import('../../pages/settings/clients/clients-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 ProjectTemplatesSettings = lazy(() => import('@/pages/settings/project-templates/project-templates-settings')); const TaskTemplatesSettings = lazy(() => import('@/pages/settings/task-templates/task-templates-settings')); diff --git a/worklenz-frontend/src/pages/settings/labels/labels-settings.tsx b/worklenz-frontend/src/pages/settings/labels/LabelsSettings.tsx similarity index 57% rename from worklenz-frontend/src/pages/settings/labels/labels-settings.tsx rename to worklenz-frontend/src/pages/settings/labels/LabelsSettings.tsx index dffc7eaa..f35113c2 100644 --- a/worklenz-frontend/src/pages/settings/labels/labels-settings.tsx +++ b/worklenz-frontend/src/pages/settings/labels/LabelsSettings.tsx @@ -8,22 +8,28 @@ import { TableProps, Tooltip, Typography, + DeleteOutlined, + ExclamationCircleFilled, + SearchOutlined, + EditOutlined, } from '@/shared/antd-imports'; import { useEffect, useMemo, useState } from 'react'; -import PinRouteToNavbarButton from '../../../components/PinRouteToNavbarButton'; +import PinRouteToNavbarButton from '@/components/PinRouteToNavbarButton'; import { useTranslation } from 'react-i18next'; -import { DeleteOutlined, ExclamationCircleFilled, SearchOutlined } from '@/shared/antd-imports'; import { ITaskLabel } from '@/types/label.type'; import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service'; import CustomColorLabel from '@components/task-list-common/labelsSelector/custom-color-label'; import { useDocumentTitle } from '@/hooks/useDoumentTItle'; import logger from '@/utils/errorLogger'; +import LabelsDrawer from './labels-drawer'; const LabelsSettings = () => { const { t } = useTranslation('settings/labels'); - useDocumentTitle('Manage Labels'); + useDocumentTitle(t('pageTitle', 'Manage Labels')); + const [selectedLabelId, setSelectedLabelId] = useState(null); + const [showDrawer, setShowDrawer] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [labels, setLabels] = useState([]); 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 const columns: TableProps['columns'] = [ { key: 'label', - title: t('labelColumn'), + title: t('labelColumn', 'Label'), + onCell: record => ({ + onClick: () => handleEditClick(record.id!), + }), render: (record: ITaskLabel) => , }, { key: 'associatedTask', - title: t('associatedTaskColumn'), + title: t('associatedTaskColumn', 'Associated Task Count'), render: (record: ITaskLabel) => {record.usage}, }, { key: 'actionBtns', - width: 60, + width: 100, render: (record: ITaskLabel) => (
- } - okText="Delete" - cancelText="Cancel" - onConfirm={() => deleteLabel(record.id!)} - > -
), }, @@ -104,12 +140,12 @@ const LabelsSettings = () => { setSearchQuery(e.target.value)} - placeholder={t('searchPlaceholder')} + placeholder={t('searchPlaceholder', 'Search by name')} style={{ maxWidth: 232 }} suffix={} /> - + {/* this button pin this route to navbar */} @@ -119,13 +155,17 @@ const LabelsSettings = () => { > {t('emptyText')}, + emptyText: {t('emptyText', 'Labels can be created while updating or creating tasks.')}, }} loading={loading} className="custom-two-colors-row-table" dataSource={filteredData} columns={columns} rowKey={record => record.id!} + onRow={(record) => ({ + style: { cursor: 'pointer' }, + onClick: () => handleEditClick(record.id!), + })} pagination={{ showSizeChanger: true, defaultPageSize: 20, @@ -133,6 +173,12 @@ const LabelsSettings = () => { size: 'small', }} /> + + ); }; diff --git a/worklenz-frontend/src/pages/settings/labels/labels-drawer.tsx b/worklenz-frontend/src/pages/settings/labels/labels-drawer.tsx new file mode 100644 index 00000000..2320b1d5 --- /dev/null +++ b/worklenz-frontend/src/pages/settings/labels/labels-drawer.tsx @@ -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 }) => ( + ( +
+
+ {WorklenzColorCodes.map((color) => ( +
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'; + }} + /> + ))} +
+
+ )} + trigger={['click']} + > +
{ + e.currentTarget.style.boxShadow = token.boxShadow; + }} + onMouseLeave={(e) => { + e.currentTarget.style.boxShadow = 'none'; + }} + /> + + ); + + return ( + + {labelId ? t('updateLabelDrawerTitle', 'Edit Label') : t('createLabelDrawerTitle', 'Create Label')} + + } + open={drawerOpen} + onClose={handleClose} + destroyOnClose + width={400} + > +
+ + + + + + + + + + + + + +
+ ); +}; + +export default LabelsDrawer; \ No newline at end of file diff --git a/worklenz-frontend/src/shared/constants.ts b/worklenz-frontend/src/shared/constants.ts index a4a5bd6e..8e57a2ca 100644 --- a/worklenz-frontend/src/shared/constants.ts +++ b/worklenz-frontend/src/shared/constants.ts @@ -313,3 +313,293 @@ export const durations: IRPTDuration[] = [ 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', +];