Skip to content

Commit

Permalink
improvement: status type | edit/create mode (#3185)
Browse files Browse the repository at this point in the history
* improvement: status type | edit/create mode

* fix svg icon colors

* fix svg icon default value

* adapt requested changes

* add requested changes

* fix build errors
  • Loading branch information
CREDO23 authored Nov 18, 2024
1 parent ae9cb25 commit 5b1a6bd
Show file tree
Hide file tree
Showing 20 changed files with 196 additions and 40 deletions.
4 changes: 1 addition & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
},
"vsicons.presets.angular": true,
"deepscan.enable": true,
"cSpell.words": [
"Timepicker"
],
"cSpell.words": ["Timepicker"],
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ export const TaskStatus = {
INPROGRESS: 'in-progress'
};

export const tasksStatusSvgCacheDuration = 1000 * 60 * 60;

export const languagesFlags = [
{
Flag: US,
Expand Down
39 changes: 38 additions & 1 deletion apps/web/app/services/server/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GAUZY_API_SERVER_URL } from '@app/constants';
import { GAUZY_API_SERVER_URL, tasksStatusSvgCacheDuration } from '@app/constants';

export function serverFetch<T>({
path,
Expand Down Expand Up @@ -54,3 +54,40 @@ export function serverFetch<T>({
};
});
}

/** 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<Response> {
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');
}
}
9 changes: 6 additions & 3 deletions apps/web/lib/features/task/task-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -854,7 +855,8 @@ export function TaskStatus({
className
)}
style={{
backgroundColor: active ? backgroundColor : undefined
backgroundColor: active ? backgroundColor : undefined,
color: getTextColor(backgroundColor ?? 'white')
}}
>
<div
Expand Down Expand Up @@ -975,10 +977,10 @@ export function StatusDropdown<T extends TStatusItem>({
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',
Expand Down Expand Up @@ -1028,6 +1030,7 @@ export function StatusDropdown<T extends TStatusItem>({
<Tooltip
enabled={hasBtnIcon && (value?.name || '').length > 10}
label={capitalize(value?.name) || ''}
className="h-full"
>
{button}
</Tooltip>
Expand Down
9 changes: 5 additions & 4 deletions apps/web/lib/settings/icon-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IIcon>;
Expand Down Expand Up @@ -53,6 +54,7 @@ export function IconItem({
url: string;
disabled?: boolean;
}) {
const t = useTranslations();
return (
<div
title={title}
Expand All @@ -63,7 +65,7 @@ export function IconItem({
)}
>
<div>
{url && (
{url ? (
<div
className={clsxm(
'w-[17px] h-[17px]',
Expand All @@ -82,11 +84,10 @@ export function IconItem({
loading="lazy"
/>
</div>
) : (
<span>{t('common.ICON')}</span>
)}
</div>
<span className={clsxm('text-normal', 'whitespace-nowrap text-ellipsis overflow-hidden capitalize')}>
{title}
</span>
</div>
);
}
Expand Down
56 changes: 43 additions & 13 deletions apps/web/lib/settings/list-card.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
<div className="border w-[21.4rem] flex items-center p-1 rounded-xl justify-between">
<div
Expand All @@ -37,18 +44,7 @@ export const StatusesListCard = ({
)}
style={{ backgroundColor: bgColor === '' ? undefined : bgColor }}
>
{statusIcon && (
<Image
src={statusIcon}
alt={statusTitle}
width={20}
height={20}
decoding="async"
data-nimg="1"
loading="lazy"
className="min-h-[20px]"
/>
)}
{statusIcon && <div id={'icon-container' + statusTitle}></div>}
<Tooltip
label={statusTitle}
enabled={statusTitle.length >= CHARACTER_LIMIT_TO_SHOW}
Expand Down Expand Up @@ -84,3 +80,37 @@ export const StatusesListCard = ({
</div>
);
};

/**
* 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<void> => {
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}`);
}
};
91 changes: 75 additions & 16 deletions apps/web/lib/settings/task-statuses-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,21 +31,24 @@ export const TaskStatusesForm = ({
const [createNew, setCreateNew] = useState(formOnly);
const [edit, setEdit] = useState<ITaskStatusItemList | null>(null);
const t = useTranslations();
const [selectedStatusType, setSelectedStatusType] = useState<string | null>(null);
const [randomColor, setRandomColor] = useState<string | undefined>(undefined);

const taskStatusIconList: IIcon[] = generateIconList('task-statuses', [
'open',
'in-progress',
'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',
Expand All @@ -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,
Expand Down Expand Up @@ -157,6 +161,53 @@ export const TaskStatusesForm = ({
const [statusToDelete, setStatusToDelete] = useState<ITaskStatusItemList | null>(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<string, string> = {
'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 (
<>
<Modal isOpen={isOpen} closeModal={closeModal}>
Expand Down Expand Up @@ -195,7 +246,7 @@ export const TaskStatusesForm = ({
variant="outline"
className="rounded-[10px]"
>
Sort
{t('common.SORT')}
</Button>
</div>
{(createNew || edit) && (
Expand All @@ -218,22 +269,26 @@ export const TaskStatusesForm = ({
{...register('name')}
/>
<StandardTaskStatusDropDown
onValueChange={(status) => setValue('template', status)}
className=" h-14 shrink-0"
onValueChange={(status) => {
setValue('template', status)
setSelectedStatusType(status)
} }
className="h-14 shrink-0"
defaultValue={edit?.value}
/>
<IconPopover
iconList={iconList}
setValue={setValue}
active={
edit
selectedStatusType ? getIcon(selectedStatusType)
: edit
? (iconList.find(
(icon) => icon.path === edit.icon
) as IIcon)
: null
) as IIcon) : null
}
/>
<ColorPicker
defaultColor={edit ? edit.color : undefined}
defaultColor={edit ? edit.color : randomColor}
onChange={(color) => setValue('color', color)}
className=" shrink-0"
/>
Expand All @@ -247,6 +302,9 @@ export const TaskStatusesForm = ({
createTaskStatusLoading || editTaskStatusLoading
}
loading={createTaskStatusLoading || editTaskStatusLoading}
onClick={() => {
setSelectedStatusType(null);
}}
>
{edit ? t('common.SAVE') : t('common.CREATE')}
</Button>
Expand All @@ -257,6 +315,7 @@ export const TaskStatusesForm = ({
onClick={() => {
setCreateNew(false);
setEdit(null);
setSelectedStatusType(null);
}}
>
{t('common.CANCEL')}
Expand Down
Loading

0 comments on commit 5b1a6bd

Please sign in to comment.