diff --git a/.vscode/settings.json b/.vscode/settings.json index 06dae99cd..be7e65f57 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,9 +23,7 @@ }, "vsicons.presets.angular": true, "deepscan.enable": true, - "cSpell.words": [ - "Timepicker" - ], + "cSpell.words": ["Timepicker"], "files.exclude": { "**/.git": true, "**/.DS_Store": true, diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index e2a2e0564..b7d0ad636 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -209,6 +209,8 @@ export const TaskStatus = { INPROGRESS: 'in-progress' }; +export const tasksStatusSvgCacheDuration = 1000 * 60 * 60; + export const languagesFlags = [ { Flag: US, diff --git a/apps/web/app/services/server/fetch.ts b/apps/web/app/services/server/fetch.ts index 32d56cb05..f026969ed 100644 --- a/apps/web/app/services/server/fetch.ts +++ b/apps/web/app/services/server/fetch.ts @@ -1,4 +1,4 @@ -import { GAUZY_API_SERVER_URL } from '@app/constants'; +import { GAUZY_API_SERVER_URL, tasksStatusSvgCacheDuration } from '@app/constants'; export function serverFetch({ path, @@ -54,3 +54,40 @@ export function serverFetch({ }; }); } + +/** Tasks status SVG icons fetch */ + +// In memory cache for performance + +const tasksStatusSvgCache = new Map< + string, + { + content: Response; + timestamp: number; + } +>(); + +export async function svgFetch(url: string): Promise { + try { + //Url validation + new URL(url); + + const cached = tasksStatusSvgCache.get(url); + const now = Date.now(); + + if (cached && now - cached.timestamp < tasksStatusSvgCacheDuration) { + return cached.content.clone(); + } + + // Fetch the SVG + const response = await fetch(url); + + tasksStatusSvgCache.set(url, { + content: response.clone(), + timestamp: now + }); + return response; + } catch { + throw new Error('Invalid URL provided'); + } +} diff --git a/apps/web/lib/features/task/task-status.tsx b/apps/web/lib/features/task/task-status.tsx index 73096654c..eb9c4e2d7 100644 --- a/apps/web/lib/features/task/task-status.tsx +++ b/apps/web/lib/features/task/task-status.tsx @@ -33,6 +33,7 @@ import { XMarkIcon } from '@heroicons/react/24/outline'; import { readableColor } from 'polished'; import { useTheme } from 'next-themes'; import { Square4OutlineIcon, CircleIcon } from 'assets/svg'; +import { getTextColor } from '@app/helpers'; import { cn } from '@/lib/utils'; export type TStatusItem = { @@ -854,7 +855,8 @@ export function TaskStatus({ className )} style={{ - backgroundColor: active ? backgroundColor : undefined + backgroundColor: active ? backgroundColor : undefined, + color: getTextColor(backgroundColor ?? 'white') }} >
({ className={clsxm( `justify-between capitalize`, sidebarUI && ['text-xs'], - !value && ['text-dark dark:text-white dark:bg-dark--theme-light'], + !value && ['!text-dark/40 dark:text-white'], isVersion || (forDetails && !value) ? 'bg-transparent border border-solid border-color-[#F2F2F2]' - : 'bg-[#F2F2F2] ', + : 'bg-white border', 'dark:bg-[#1B1D22] dark:border dark:border-[#FFFFFF33]', taskStatusClassName, isVersion && 'dark:text-white', @@ -1028,6 +1030,7 @@ export function StatusDropdown({ 10} label={capitalize(value?.name) || ''} + className="h-full" > {button} diff --git a/apps/web/lib/settings/icon-items.tsx b/apps/web/lib/settings/icon-items.tsx index 5ba87b2db..8c72290e2 100644 --- a/apps/web/lib/settings/icon-items.tsx +++ b/apps/web/lib/settings/icon-items.tsx @@ -2,6 +2,7 @@ import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; import { IIcon } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { DropdownItem } from 'lib/components'; +import { useTranslations } from 'next-intl'; import Image from 'next/image'; export type IconItem = DropdownItem; @@ -53,6 +54,7 @@ export function IconItem({ url: string; disabled?: boolean; }) { + const t = useTranslations(); return (
- {url && ( + {url ? (
+ ) : ( + {t('common.ICON')} )}
- - {title} -
); } diff --git a/apps/web/lib/settings/list-card.tsx b/apps/web/lib/settings/list-card.tsx index f07ef63b0..e9ad47a24 100644 --- a/apps/web/lib/settings/list-card.tsx +++ b/apps/web/lib/settings/list-card.tsx @@ -1,11 +1,12 @@ import { EditPenUnderlineIcon, TrashIcon } from 'assets/svg'; import { Button, Text, Tooltip } from 'lib/components'; -import Image from 'next/image'; import { CHARACTER_LIMIT_TO_SHOW } from '@app/constants'; import { IClassName } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { getTextColor } from '@app/helpers'; import { useTranslations } from 'next-intl'; +import { useEffect } from 'react'; +import { svgFetch } from '@app/services/server/fetch'; export const StatusesListCard = ({ statusIcon, @@ -27,6 +28,12 @@ export const StatusesListCard = ({ const textColor = getTextColor(bgColor); const t = useTranslations(); + useEffect(() => { + if (statusIcon) { + loadSVG(statusIcon, 'icon-container' + statusTitle, textColor); + } + }, [statusIcon, statusTitle, textColor]); + return (
- {statusIcon && ( - {statusTitle} - )} + {statusIcon &&
} = CHARACTER_LIMIT_TO_SHOW} @@ -84,3 +80,37 @@ export const StatusesListCard = ({
); }; + +/** + * A function to load an SVG and gives the ability to + * update its attributes. e.g: fill color + * + * @param {string} url the URL of the SVG file to load + * @param {string} containerId the ID of the container where the SVG will be inserted + * @param {string} color the fill color for the SVG + */ +const loadSVG = async (url: string, containerId: string, color: string): Promise => { + try { + const response = await svgFetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch SVG: ${response.statusText}`); + } + + let svgContent = await response.text(); + + // Update the fill color in the SVG content + svgContent = svgContent.replace(/stroke="[^"]*"/g, `stroke="${color}"`); + + const container = document.getElementById(containerId); + + if (container) { + console.log(container); + container.innerHTML = svgContent; + } else { + console.error(`Container with ID "${containerId}" not found.`); + } + } catch (error) { + console.error(`Error loading SVG: ${(error as Error).message}`); + } +}; diff --git a/apps/web/lib/settings/task-statuses-form.tsx b/apps/web/lib/settings/task-statuses-form.tsx index 678e9b569..c07e48b7b 100644 --- a/apps/web/lib/settings/task-statuses-form.tsx +++ b/apps/web/lib/settings/task-statuses-form.tsx @@ -6,7 +6,7 @@ import { clsxm } from '@app/utils'; import { Spinner } from '@components/ui/loaders/spinner'; import { PlusIcon } from '@heroicons/react/20/solid'; import { Button, ColorPicker, InputField, Modal, Text } from 'lib/components'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslations } from 'next-intl'; import { useAtom } from 'jotai'; @@ -31,6 +31,8 @@ export const TaskStatusesForm = ({ const [createNew, setCreateNew] = useState(formOnly); const [edit, setEdit] = useState(null); const t = useTranslations(); + const [selectedStatusType, setSelectedStatusType] = useState(null); + const [randomColor, setRandomColor] = useState(undefined); const taskStatusIconList: IIcon[] = generateIconList('task-statuses', [ 'open', @@ -38,14 +40,15 @@ export const TaskStatusesForm = ({ 'ready', 'in-review', 'blocked', - 'completed' + 'completed', + 'backlog', ]); const taskSizesIconList: IIcon[] = generateIconList('task-sizes', [ - 'x-large' - // 'large', - // 'medium', - // 'small', - // 'tiny', + 'x-large', + 'large', + 'medium', + 'small', + 'tiny', ]); const taskPrioritiesIconList: IIcon[] = generateIconList('task-priorities', [ 'urgent', @@ -54,11 +57,12 @@ export const TaskStatusesForm = ({ 'low' ]); - const iconList: IIcon[] = [ + const iconList: IIcon[] = useMemo(() => [ ...taskStatusIconList, ...taskSizesIconList, ...taskPrioritiesIconList - ]; + // eslint-disable-next-line react-hooks/exhaustive-deps + ],[]) ; const { loading, @@ -157,6 +161,53 @@ export const TaskStatusesForm = ({ const [statusToDelete, setStatusToDelete] = useState(null) const {tasks} = useTeamTasks() + /** + * Get Icon by status name + * + * @param {string} iconName - Name of the icon + * @returns {IIcon} - Icon of the status + */ + const getIcon = useCallback( + (iconName: string | null) => { + if (!iconName) return null; + + const STATUS_MAPPINGS: Record = { + 'ready-for-review': 'ready' + }; + + const name = STATUS_MAPPINGS[iconName] || iconName; + + const icon = iconList.find((icon) => icon.title === name); + + if (icon) { + setValue('icon', icon.path); + } + return icon; + }, + [iconList, setValue] + ); + + + /** + * Get random color for new status + * + * @returns {string} - Random color + */ + const getRandomColor = useCallback(() => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; + }, []); + + useEffect(() => { + if (!edit && selectedStatusType) { + setRandomColor(getRandomColor()); + } + }, [selectedStatusType, edit, getRandomColor]); + return ( <> @@ -195,7 +246,7 @@ export const TaskStatusesForm = ({ variant="outline" className="rounded-[10px]" > - Sort + {t('common.SORT')}
{(createNew || edit) && ( @@ -218,22 +269,26 @@ export const TaskStatusesForm = ({ {...register('name')} /> setValue('template', status)} - className=" h-14 shrink-0" + onValueChange={(status) => { + setValue('template', status) + setSelectedStatusType(status) + } } + className="h-14 shrink-0" + defaultValue={edit?.value} /> icon.path === edit.icon - ) as IIcon) - : null + ) as IIcon) : null } /> setValue('color', color)} className=" shrink-0" /> @@ -247,6 +302,9 @@ export const TaskStatusesForm = ({ createTaskStatusLoading || editTaskStatusLoading } loading={createTaskStatusLoading || editTaskStatusLoading} + onClick={() => { + setSelectedStatusType(null); + }} > {edit ? t('common.SAVE') : t('common.CREATE')} @@ -257,6 +315,7 @@ export const TaskStatusesForm = ({ onClick={() => { setCreateNew(false); setEdit(null); + setSelectedStatusType(null); }} > {t('common.CANCEL')} diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index 77d575a46..2b45b804d 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -235,6 +235,8 @@ "CHANGE_RELATIONS": "تغيير العلاقات", "SET_AS_NEXT": "تعيين كالتالي", "MOVE_TO": "نقل إلى", + "SORT": "فرز", + "ICON": "أيقونة", "SELECT_DATE": "اختر التاريخ", "SELECT_AND_CLOSE": "اختر وأغلق", "SELECT_ROLE": "يرجى اختيار أي دور", diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 146962a2e..6a35a5b89 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -255,6 +255,8 @@ "MOVE_TO": "Преместете в", "SELECT_DATE": "Изберете дата", "SELECT_AND_CLOSE": "Изберете и затворете", + "SORT": "Подредете", + "ICON": "Икона", "SELECT_ROLE": "Моля, изберете роля", "ADD_TIME": "Добавете време", "VIEW_TIMESHEET": "Преглед на работния час", diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index c42de6e56..a8cd98b43 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -255,6 +255,8 @@ "MOVE_TO": "Verschieben nach", "SELECT_DATE": "Datum auswählen", "SELECT_AND_CLOSE": "Auswählen und schließen", + "SORT": "Sortieren", + "ICON": "Symbol", "SELECT_ROLE": "Bitte wählen Sie eine Rolle", "ADD_TIME": "Zeit hinzufügen", "VIEW_TIMESHEET": "Zeiterfassung anzeigen", diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index a35d7ec92..e230c7a31 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -255,6 +255,8 @@ "MOVE_TO": "Move to", "SELECT_DATE": "Select date", "SELECT_AND_CLOSE": "Select & Close", + "SORT": "Sort", + "ICON": "Icon", "SELECT_ROLE": "Please Select any Role", "ADD_TIME": "Add Time", "VIEW_TIMESHEET": "View timesheet", diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 8a30a3032..7e8997c94 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -255,6 +255,8 @@ "MOVE_TO": "Mover a", "SELECT_DATE": "Seleccionar fecha", "SELECT_AND_CLOSE": "Seleccionar y cerrar", + "SORT": "Ordenar", + "ICON": "Ícono", "SELECT_ROLE": "Por favor, selecciona un rol", "ADD_TIME": "Agregar tiempo", "VIEW_TIMESHEET": "Ver hoja de tiempo", diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index bbd5f0430..2c70c0917 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -255,6 +255,8 @@ "MOVE_TO": "Déplacer vers", "SELECT_DATE": "Sélectionner la date", "SELECT_AND_CLOSE": "Sélectionner et fermer", + "SORT": "Trier", + "ICON": "Icône", "SELECT_ROLE": "Veuillez sélectionner un rôle", "ADD_TIME": "Ajouter du temps", "VIEW_TIMESHEET": "Voir la feuille de temps", diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 0cf6d5c9e..e6c81b8fc 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -255,6 +255,8 @@ "MOVE_TO": "העבר אל", "SELECT_DATE": "בחר תאריך", "SELECT_AND_CLOSE": "בחר וסגור", + "SORT": "מיין", + "ICON": "סמל", "SELECT_ROLE": "אנא בחר תפקיד", "ADD_TIME": "הוסף זמן", "VIEW_TIMESHEET": "הצג גיליון זמן", diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 576c9af34..945980bd8 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -255,6 +255,8 @@ "MOVE_TO": "Sposta a", "SELECT_DATE": "Seleziona data", "SELECT_AND_CLOSE": "Seleziona e chiudi", + "SORT": "Ordina", + "ICON": "Icona", "SELECT_ROLE": "Seleziona un ruolo", "ADD_TIME": "Aggiungi tempo", "VIEW_TIMESHEET": "Vedi foglio ore", diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index 1c7612d5c..2445311c8 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -255,6 +255,8 @@ "MOVE_TO": "Verplaatsen naar", "SELECT_DATE": "Selecteer datum", "SELECT_AND_CLOSE": "Selecteren en sluiten", + "SORT": "Sorteren", + "ICON": "Icoon", "SELECT_ROLE": "Selecteer een rol", "ADD_TIME": "Tijd toevoegen", "VIEW_TIMESHEET": "Bekijk tijdregistratie", diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 7e45f58ba..198f39004 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -255,6 +255,8 @@ "MOVE_TO": "Przenieś do", "SELECT_DATE": "Wybierz datę", "SELECT_AND_CLOSE": "Wybierz i zamknij", + "SORT": "Sortuj", + "ICON": "Ikona", "SELECT_ROLE": "Proszę wybrać rolę", "ADD_TIME": "Dodaj czas", "VIEW_TIMESHEET": "Zobacz Ewidencję czasu", diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index 4a832f9ab..033cc1a79 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -255,6 +255,8 @@ "MOVE_TO": "Mover para", "SELECT_DATE": "Selecionar data", "SELECT_AND_CLOSE": "Selecionar e fechar", + "SORT": "Classificar", + "ICON": "Ícone", "SELECT_ROLE": "Por favor, selecione um cargo", "ADD_TIME": "Adicionar tempo", "VIEW_TIMESHEET": "Ver folha de ponto", diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 20c2605ab..3ab812538 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -255,6 +255,8 @@ "MOVE_TO": "Переместить в", "SELECT_DATE": "Выберите дату", "SELECT_AND_CLOSE": "Выбрать и закрыть", + "SORT": "Сортировать", + "ICON": "Иконка", "SELECT_ROLE": "Пожалуйста, выберите роль", "ADD_TIME": "Добавить время", "VIEW_TIMESHEET": "Просмотреть табель", diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index 3735c7f97..50d39d2d2 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -255,6 +255,8 @@ "MOVE_TO": "移动到", "SELECT_DATE": "选择日期", "SELECT_AND_CLOSE": "选择并关闭", + "SORT": "排序", + "ICON": "图标", "SELECT_ROLE": "请选择角色", "ADD_TIME": "添加时间", "VIEW_TIMESHEET": "查看工时表",