Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: status type | edit/create mode #3185

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/web/app/services/server/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,8 @@ export function serverFetch<T>({
};
});
}

export async function svgFetch(url: string): Promise<Response> {
// Fetch the SVG
return await fetch(url);
}
Comment on lines +58 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider implementing caching for SVG responses.

For better performance and reduced network calls, consider implementing a caching strategy for SVG responses.

Here's a suggested implementation using a simple in-memory cache:

const svgCache = new Map<string, {
  content: Response,
  timestamp: number
}>();

const CACHE_DURATION = 1000 * 60 * 60; // 1 hour

export async function fetchSvgContent(url: string): Promise<Response> {
  const cached = svgCache.get(url);
  const now = Date.now();
  
  if (cached && (now - cached.timestamp) < CACHE_DURATION) {
    return cached.content.clone();
  }
  
  const response = await /* previous implementation */;
  
  svgCache.set(url, {
    content: response.clone(),
    timestamp: now
  });
  
  return response;
}

⚠️ Potential issue

Add input validation and error handling for SVG fetching.

The current implementation lacks several important safety checks and error handling mechanisms:

  1. URL validation
  2. Response type verification
  3. Error handling
  4. Timeout handling

Consider implementing these improvements:

-export async function svgFetch(url: string): Promise<Response> {
-	// Fetch the SVG
-	return await fetch(url);
+export async function fetchSvgContent(url: string): Promise<Response> {
+	try {
+		// Validate URL
+		const validUrl = new URL(url);
+		
+		const response = await fetch(validUrl.toString(), {
+			timeout: 5000,
+			headers: {
+				'Accept': 'image/svg+xml'
+			}
+		});
+
+		if (!response.ok) {
+			throw new Error(`Failed to fetch SVG: ${response.statusText}`);
+		}
+
+		const contentType = response.headers.get('content-type');
+		if (!contentType?.includes('svg')) {
+			throw new Error('Response is not an SVG image');
+		}
+
+		return response;
+	} catch (error) {
+		if (error instanceof Error) {
+			throw new Error(`SVG fetch failed: ${error.message}`);
+		}
+		throw new Error('Unknown error occurred while fetching SVG');
+	}
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function svgFetch(url: string): Promise<Response> {
// Fetch the SVG
return await fetch(url);
}
export async function fetchSvgContent(url: string): Promise<Response> {
try {
// Validate URL
const validUrl = new URL(url);
const response = await fetch(validUrl.toString(), {
timeout: 5000,
headers: {
'Accept': 'image/svg+xml'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch SVG: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (!contentType?.includes('svg')) {
throw new Error('Response is not an SVG image');
}
return response;
} catch (error) {
if (error instanceof Error) {
throw new Error(`SVG fetch failed: ${error.message}`);
}
throw new Error('Unknown error occurred while fetching SVG');
}
}

5 changes: 4 additions & 1 deletion 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';

export type TStatusItem = {
id?: string;
Expand Down Expand Up @@ -853,7 +854,8 @@ export function TaskStatus({
className
)}
style={{
backgroundColor: active ? backgroundColor : undefined
backgroundColor: active ? backgroundColor : undefined,
color: getTextColor(backgroundColor ?? 'white')
}}
>
<div
Expand Down Expand Up @@ -1027,6 +1029,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
53 changes: 40 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,10 @@ export const StatusesListCard = ({
const textColor = getTextColor(bgColor);
const t = useTranslations();

useEffect(() => {
loadSVG(statusIcon, 'icon-container' + statusTitle, textColor);
}, []);
Comment on lines +31 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing dependencies and cleanup to useEffect.

The current implementation has several issues:

  1. Missing dependencies: statusIcon and textColor should be in the dependency array
  2. No cleanup handling for unmounting during SVG loading

Here's the suggested fix:

 useEffect(() => {
-  loadSVG(statusIcon, 'icon-container' + statusTitle, textColor);
+  let mounted = true;
+  const loadIcon = async () => {
+    if (mounted) {
+      await loadSVG(statusIcon, 'icon-container' + statusTitle, textColor);
+    }
+  };
+  loadIcon();
+  return () => {
+    mounted = false;
+  };
-}, []);
+}, [statusIcon, statusTitle, textColor]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
loadSVG(statusIcon, 'icon-container' + statusTitle, textColor);
}, []);
useEffect(() => {
let mounted = true;
const loadIcon = async () => {
if (mounted) {
await loadSVG(statusIcon, 'icon-container' + statusTitle, textColor);
}
};
loadIcon();
return () => {
mounted = false;
};
}, [statusIcon, statusTitle, textColor]);


return (
<div className="border w-[21.4rem] flex items-center p-1 rounded-xl justify-between">
<div
Expand All @@ -37,18 +42,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>}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve ID generation security and uniqueness.

The current ID generation method has potential security and uniqueness issues:

  1. No sanitization of statusTitle when used in ID
  2. Non-unique IDs if multiple instances have the same title

Consider using a more secure approach:

-{statusIcon && <div id={'icon-container' + statusTitle}></div>}
+{statusIcon && (
+  <div
+    id={`icon-container-${encodeURIComponent(statusTitle)}-${Math.random().toString(36).substr(2, 9)}`}
+  />
+)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{statusIcon && <div id={'icon-container' + statusTitle}></div>}
{statusIcon && (
<div
id={`icon-container-${encodeURIComponent(statusTitle)}-${Math.random().toString(36).substr(2, 9)}`}
/>
)}

<Tooltip
label={statusTitle}
enabled={statusTitle.length >= CHARACTER_LIMIT_TO_SHOW}
Expand Down Expand Up @@ -84,3 +78,36 @@ 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) {
container.innerHTML = svgContent;
} else {
console.error(`Container with ID "${containerId}" not found.`);
}
} catch (error) {
console.error(`Error loading SVG: ${(error as Error).message}`);
}
};
Comment on lines +90 to +113
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

⚠️ Potential issue

Based on the context gathered, I can now generate the final response:

Security improvements needed for SVG loading implementation

The current implementation in list-card.tsx loads SVGs from status icons and directly injects them using innerHTML, which poses security risks. However, the usage context shows this is specifically for status icons within a controlled environment, where:

  1. SVGs are loaded via svgFetch service, suggesting a controlled fetching mechanism
  2. The usage is limited to status icons within the settings list card component
  3. The color modification is specifically for status indication

While the security concerns are valid, here's a more targeted improvement suggestion:

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}`);
    }

    const parser = new DOMParser();
    const svgDoc = parser.parseFromString(await response.text(), 'image/svg+xml');
    
    // Verify we have an SVG element
    const svgElement = svgDoc.querySelector('svg');
    if (!svgElement) {
      throw new Error('Invalid SVG content');
    }

    // Update stroke colors
    svgElement.querySelectorAll('*').forEach(el => {
      if (el instanceof Element && el.hasAttribute('stroke')) {
        el.setAttribute('stroke', color);
      }
    });

    const container = document.getElementById(containerId);
    if (container) {
      container.replaceChildren(svgElement);
    }
  } catch (error) {
    console.error(`Error loading SVG: ${(error as Error).message}`);
  }
};
  • Consider adding a whitelist of allowed SVG attributes to further restrict potential XSS vectors
  • Add MIME type validation in the svgFetch service
🔗 Analysis chain

Enhance SVG loading security and robustness.

The current implementation has several security and reliability concerns:

  1. Direct innerHTML usage is vulnerable to XSS
  2. Simple regex replacement might miss complex SVG color attributes
  3. No sanitization of SVG content before insertion

Consider implementing these security improvements:

 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();
+    
+    // Create a DOMParser to safely parse the SVG
+    const parser = new DOMParser();
+    const doc = parser.parseFromString(svgContent, 'image/svg+xml');
+    
+    // Validate that we received an SVG
+    if (doc.documentElement.tagName !== 'svg') {
+      throw new Error('Invalid SVG content');
+    }
+    
+    // Update colors using DOM methods instead of regex
+    const elements = doc.querySelectorAll('*');
+    elements.forEach(el => {
+      if (el.hasAttribute('stroke')) {
+        el.setAttribute('stroke', color);
+      }
+    });
 
     const container = document.getElementById(containerId);
     if (container) {
-      container.innerHTML = svgContent;
+      // Clear existing content
+      while (container.firstChild) {
+        container.removeChild(container.firstChild);
+      }
+      // Safely append the new SVG
+      container.appendChild(doc.documentElement);
     } else {
       console.error(`Container with ID "${containerId}" not found.`);
     }
   } catch (error) {
     console.error(`Error loading SVG: ${(error as Error).message}`);
   }
 };

Additionally, consider implementing a content security policy (CSP) to restrict SVG sources to trusted domains.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if we have any Content-Security-Policy headers defined
rg -l "Content-Security-Policy" --type-add 'config:*.{json,js,ts}' -t config

Length of output: 76


Script:

#!/bin/bash
# Let's check where this SVG loading function is used and what SVG sources are being loaded
rg -A 2 "loadSVG\(" 

# Also check for any existing SVG security measures or sanitization
rg -l "DOMParser|createElementNS|sanitize.*svg" --type=typescript

# Check for any SVG-related utility functions or components
fd -e ts -e tsx -X rg "svg" -l

Length of output: 10728


Script:

#!/bin/bash
# Let's check the svgFetch implementation and where SVGs are being loaded from
rg -A 5 "svgFetch" --type=typescript

# Check the specific usage context in list-card.tsx
cat "apps/web/lib/settings/list-card.tsx"

# Look for any existing security configurations or middleware
rg -l "security|sanitize|xss" --type=typescript

Length of output: 3616

70 changes: 60 additions & 10 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 @@ -30,6 +30,8 @@ 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',
Expand All @@ -53,11 +55,12 @@ export const TaskStatusesForm = ({
'low'
]);

const iconList: IIcon[] = [
const iconList: IIcon[] = useMemo(() => [
...taskStatusIconList,
...taskSizesIconList,
...taskPrioritiesIconList
];
// eslint-disable-next-line react-hooks/exhaustive-deps
],[]) ;
Comment on lines +58 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add dependencies to useMemo

The empty dependency array means the iconList will never be recomputed even if the source lists change. Add the source lists as dependencies.

  const iconList: IIcon[] = useMemo(() => [
    ...taskStatusIconList,
    ...taskSizesIconList,
    ...taskPrioritiesIconList
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  ],[]) ;
+  ],[taskStatusIconList, taskSizesIconList, taskPrioritiesIconList]) ;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const iconList: IIcon[] = useMemo(() => [
...taskStatusIconList,
...taskSizesIconList,
...taskPrioritiesIconList
];
// eslint-disable-next-line react-hooks/exhaustive-deps
],[]) ;
const iconList: IIcon[] = useMemo(() => [
...taskStatusIconList,
...taskSizesIconList,
...taskPrioritiesIconList
],[taskStatusIconList, taskSizesIconList, taskPrioritiesIconList]) ;


const {
loading,
Expand Down Expand Up @@ -153,6 +156,45 @@ export const TaskStatusesForm = ({
: [];
const { isOpen, closeModal, openModal } = useModal();

/**
* Get Icon by status name
*
* @param {string} iconName - Name of the icon
* @returns {IIcon} - Icon of the status
*/
const getIcon = useCallback((iconName: string) => {
const name = iconName == "ready-for-review" ? "ready" : iconName;
const icon = iconList.find(icon => icon.title === name)

if(icon){
setValue("icon", icon.path)
}

return icon

},[iconList, setValue])
Comment on lines +165 to +175
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance getIcon function robustness

The function needs better error handling and could use a more maintainable approach for icon name mapping.

+const ICON_NAME_MAP: Record<string, string> = {
+  'ready-for-review': 'ready'
+};

 const getIcon = useCallback((iconName: string) => {
-  const name = iconName == "ready-for-review" ? "ready" : iconName;
+  const name = ICON_NAME_MAP[iconName] || iconName;
   const icon = iconList.find(icon => icon.title === name)

+  if (!icon) {
+    console.warn(`Icon not found for status: ${iconName}`);
+  }

   if(icon){
     setValue("icon", icon.path)
   }

   return icon
 },[iconList, setValue])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getIcon = useCallback((iconName: string) => {
const name = iconName == "ready-for-review" ? "ready" : iconName;
const icon = iconList.find(icon => icon.title === name)
if(icon){
setValue("icon", icon.path)
}
return icon
},[iconList, setValue])
const ICON_NAME_MAP: Record<string, string> = {
'ready-for-review': 'ready'
};
const getIcon = useCallback((iconName: string) => {
const name = ICON_NAME_MAP[iconName] || iconName;
const icon = iconList.find(icon => icon.title === name)
if (!icon) {
console.warn(`Icon not found for status: ${iconName}`);
}
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;
}, []);
Comment on lines +183 to +190
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify random color generation

The current implementation can be simplified using array methods.

 const getRandomColor = useCallback(() => {
-  const letters = '0123456789ABCDEF';
-  let color = '#';
-  for (let i = 0; i < 6; i++) {
-    color += letters[Math.floor(Math.random() * 16)];
-  }
-  return color;
+  return '#' + Array.from({ length: 6 }, () => 
+    Math.floor(Math.random() * 16).toString(16)
+  ).join('');
 }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getRandomColor = useCallback(() => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}, []);
const getRandomColor = useCallback(() => {
return '#' + Array.from({ length: 6 }, () =>
Math.floor(Math.random() * 16).toString(16)
).join('');
}, []);


useEffect(() => {
if (!edit && selectedStatusType) {
setRandomColor(getRandomColor());
}
}, [selectedStatusType, edit, getRandomColor]);

return (
<>
<Modal isOpen={isOpen} closeModal={closeModal}>
Expand Down Expand Up @@ -191,7 +233,7 @@ export const TaskStatusesForm = ({
variant="outline"
className="rounded-[10px]"
>
Sort
{t('common.SORT')}
</Button>
</div>
{(createNew || edit) && (
Expand All @@ -214,22 +256,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}
Comment on lines +259 to +264
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle defaultValue prop consistently

The defaultValue prop is only set for edit mode, which might lead to unexpected behavior when creating a new status.

 <StandardTaskStatusDropDown
   onValueChange={(status) => {
     setValue('template', status)
     setSelectedStatusType(status)
   }}
   className="h-14 shrink-0"
-  defaultValue={edit?.value}
+  defaultValue={edit?.value || selectedStatusType}
 />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onValueChange={(status) => {
setValue('template', status)
setSelectedStatusType(status)
} }
className="h-14 shrink-0"
defaultValue={edit?.value}
onValueChange={(status) => {
setValue('template', status)
setSelectedStatusType(status)
} }
className="h-14 shrink-0"
defaultValue={edit?.value || selectedStatusType}

/>
<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 @@ -243,6 +289,9 @@ export const TaskStatusesForm = ({
createTaskStatusLoading || editTaskStatusLoading
}
loading={createTaskStatusLoading || editTaskStatusLoading}
onClick={() => {
setSelectedStatusType(null);
}}
>
{edit ? t('common.SAVE') : t('common.CREATE')}
</Button>
Expand All @@ -253,6 +302,7 @@ export const TaskStatusesForm = ({
onClick={() => {
setCreateNew(false);
setEdit(null);
setSelectedStatusType(null);
}}
>
{t('common.CANCEL')}
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "المسند إليه",
"CHANGE_RELATIONS": "تغيير العلاقات",
"SET_AS_NEXT": "تعيين كالتالي",
"MOVE_TO": "نقل إلى"
"MOVE_TO": "نقل إلى",
"SORT": "فرز",
"ICON": "أيقونة"
},
"hotkeys": {
"HELP": "مساعدة",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "Назначен",
"CHANGE_RELATIONS": "Променете отношенията",
"SET_AS_NEXT": "Задайте като следващо",
"MOVE_TO": "Преместете в"
"MOVE_TO": "Преместете в",
"SORT": "Подредете",
"ICON": "Икона"
},
"hotkeys": {
"HELP": "Помощ",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "Zessionar",
"CHANGE_RELATIONS": "Beziehungen ändern",
"SET_AS_NEXT": "Als nächstes festlegen",
"MOVE_TO": "Verschieben nach"
"MOVE_TO": "Verschieben nach",
"SORT": "Sortieren",
"ICON": "Symbol"
},
"hotkeys": {
"HELP": "Hilfe",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "Assignee",
"CHANGE_RELATIONS": "Change relations",
"SET_AS_NEXT": "Set as next",
"MOVE_TO": "Move to"
"MOVE_TO": "Move to",
"SORT": "Sort",
"ICON": "Icon"
},
"hotkeys": {
"HELP": "Help",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "Cesionario",
"CHANGE_RELATIONS": "Cambiar relaciones",
"SET_AS_NEXT": "Establecer como siguiente",
"MOVE_TO": "Mover a"
"MOVE_TO": "Mover a",
"SORT": "Ordenar",
"ICON": "Ícono"
},
"hotkeys": {
"HELP": "Ayuda",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "Cessionnaire",
"CHANGE_RELATIONS": "Changer les relations",
"SET_AS_NEXT": "Définir comme suivant",
"MOVE_TO": "Déplacer vers"
"MOVE_TO": "Déplacer vers",
"SORT": "Trier",
"ICON": "Icône"
},
"hotkeys": {
"HELP": "Aide",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "נמען",
"CHANGE_RELATIONS": "שנה יחסים",
"SET_AS_NEXT": "הגדר כהבא",
"MOVE_TO": "העבר אל"
"MOVE_TO": "העבר אל",
"SORT": "מיין",
"ICON": "סמל"
},
"hotkeys": {
"HELP": "עזרה",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "Assegnatario",
"CHANGE_RELATIONS": "Cambia relazioni",
"SET_AS_NEXT": "Imposta come successivo",
"MOVE_TO": "Sposta a"
"MOVE_TO": "Sposta a",
"SORT": "Ordina",
"ICON": "Icona"
},
"hotkeys": {
"HELP": "Aiuto",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@
"ASSIGNEE": "Cessionaris",
"CHANGE_RELATIONS": "Verander relaties",
"SET_AS_NEXT": "Instellen als volgende",
"MOVE_TO": "Verplaatsen naar"
"MOVE_TO": "Verplaatsen naar",
"SORT": "Sorteren",
"ICON": "Icoon"
},
"hotkeys": {
"HELP": "Help",
Expand Down
Loading
Loading