diff --git a/src/application/database-yjs/cell.parse.ts b/src/application/database-yjs/cell.parse.ts index a85bd7b9..9831f5e1 100644 --- a/src/application/database-yjs/cell.parse.ts +++ b/src/application/database-yjs/cell.parse.ts @@ -107,9 +107,7 @@ export function getCellDataText(cell: YDatabaseCell, field: YDatabaseField, curr return ( data .split(',') - .map((item) => { - return options?.find((option) => option.id === item)?.name; - }) + .map((item) => options?.find((option) => option?.id === item)?.name) .filter((item) => item) .join(',') || '' ); diff --git a/src/application/database-yjs/dispatch.ts b/src/application/database-yjs/dispatch.ts index 8e83caf0..9d2ada1e 100644 --- a/src/application/database-yjs/dispatch.ts +++ b/src/application/database-yjs/dispatch.ts @@ -187,13 +187,19 @@ function generateGroupByField(field: YDatabaseField) { case FieldType.MultiSelect: { group.set(YjsDatabaseKey.content, ''); const typeOption = parseSelectOptionTypeOptions(field); - const options = typeOption?.options || []; + const options = (typeOption?.options || []).filter((option) => Boolean(option && option.id)); columns.push([{ id: fieldId, visible: true }]); // Add a column for each option options.forEach((option) => { - columns.push([{ id: option.id, visible: true }]); + const optionId = option?.id; + + if (!optionId) { + return; + } + + columns.push([{ id: optionId, visible: true }]); }); break; } @@ -2678,13 +2684,20 @@ export function useSwitchPropertyType() { // 1. to RichText if ([FieldType.RichText, FieldType.URL].includes(fieldType)) { const cellType = Number(cell.get(YjsDatabaseKey.field_type)); - const typeOption = field.get(YjsDatabaseKey.type_option)?.get(String(cellType)); + const existingTypeOption = field + .get(YjsDatabaseKey.type_option) + ?.get(String(cellType)) as YMapFieldTypeOption | undefined; switch (cellType) { // From Number to RichText, keep the number format value case FieldType.Number: { + const formatRaw = existingTypeOption?.get(YjsDatabaseKey.format); + const parsedFormat = + formatRaw === undefined || formatRaw === null ? undefined : Number(formatRaw); const format = - (Number(typeOption.get(YjsDatabaseKey.format)) as NumberFormat) ?? NumberFormat.Num; + parsedFormat === undefined || Number.isNaN(parsedFormat) + ? NumberFormat.Num + : (parsedFormat as NumberFormat); if (data) { newData = EnhancedBigStats.parse(data.toString(), format) || ''; @@ -2696,8 +2709,13 @@ export function useSwitchPropertyType() { case FieldType.SingleSelect: case FieldType.MultiSelect: { const selectedIds = (data as string).split(','); - const typeOption = typeOptionMap.get(String(cellType)); - const content = typeOption.get(YjsDatabaseKey.content); + const optionSource = typeOptionMap?.get(String(cellType)) as YMapFieldTypeOption | undefined; + const content = optionSource?.get(YjsDatabaseKey.content); + + if (typeof content !== 'string') { + newData = ''; + break; + } try { const parsedContent = JSON.parse(content) as SelectTypeOption; @@ -2766,29 +2784,35 @@ export function useSwitchPropertyType() { // 3. to SingleSelect or MultiSelect if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) { - const typeOption = typeOptionMap.get(String(fieldType)); - const content = typeOption.get(YjsDatabaseKey.content); + const targetTypeOption = typeOptionMap?.get(String(fieldType)) as + | YMapFieldTypeOption + | undefined; + const content = targetTypeOption?.get(YjsDatabaseKey.content); - try { - const parsedContent = JSON.parse(content) as SelectTypeOption; - const options = parsedContent.options; + if (typeof content === 'string') { + try { + const parsedContent = JSON.parse(content) as SelectTypeOption; + const options = parsedContent.options; - const selectedOptionNames = (data as string).split(','); - const selectedOptionIds = selectedOptionNames - .map((name) => { - const option = options.find((opt) => opt.name === name || opt.id === name); + const selectedOptionNames = (data as string).split(','); + const selectedOptionIds = selectedOptionNames + .map((name) => { + const option = options.find((opt) => opt.name === name || opt.id === name); - if (!option) { - return ''; - } + if (!option) { + return ''; + } - return option.id; - }) - .filter((id) => id !== ''); + return option.id; + }) + .filter((id) => id !== ''); - newData = selectedOptionIds.join(','); - } catch (e) { - // do nothing + newData = selectedOptionIds.join(','); + } catch (e) { + // do nothing + } + } else { + newData = ''; } } @@ -2996,12 +3020,14 @@ export function useAddSelectOption(fieldId: string) { if (group) { const columns = group.get(YjsDatabaseKey.groups); - const column = columns.toArray().find((col) => col.id === option.id); + const optionId = option.id; + + const column = columns.toArray().find((col) => col.id === optionId); if (!column) { columns.push([ { - id: option.id, + id: optionId, visible: true, }, ]); @@ -3838,4 +3864,4 @@ export function useUpdateCalendarSetting() { }, [sharedRoot, view] ); -} \ No newline at end of file +} diff --git a/src/application/database-yjs/fields/checklist/parse.ts b/src/application/database-yjs/fields/checklist/parse.ts index c1857b69..eb728e77 100644 --- a/src/application/database-yjs/fields/checklist/parse.ts +++ b/src/application/database-yjs/fields/checklist/parse.ts @@ -7,6 +7,10 @@ export interface ChecklistCellData { percentage: number; } +function normalizeChecklistOptions(options: SelectOption[] = []) { + return options.filter((option): option is SelectOption => Boolean(option && option.id)); +} + export function parseChecklistData(data: string): ChecklistCellData | null { try { const { options, selected_option_ids } = JSON.parse(data); @@ -39,13 +43,14 @@ export function addTask(data: string, taskName: string): string { } const { options = [], selectedOptionIds } = parsedData; + const normalizedOptions = normalizeChecklistOptions(options); - if (options.find((option) => option.id === task.id)) { + if (normalizedOptions.find((option) => option.id === task.id)) { return data; } return JSON.stringify({ - options: [...options, task], + options: [...normalizedOptions, task], selected_option_ids: selectedOptionIds, }); } @@ -58,6 +63,7 @@ export function toggleSelectedTask(data: string, taskId: string): string { } const { options, selectedOptionIds = [] } = parsedData; + const normalizedOptions = normalizeChecklistOptions(options); const isSelected = selectedOptionIds.includes(taskId); const newSelectedOptionIds = isSelected @@ -65,7 +71,7 @@ export function toggleSelectedTask(data: string, taskId: string): string { : [...selectedOptionIds, taskId]; return JSON.stringify({ - options, + options: normalizedOptions, selected_option_ids: newSelectedOptionIds, }); } @@ -78,8 +84,9 @@ export function updateTask(data: string, taskId: string, taskName: string): stri } const { options = [], selectedOptionIds } = parsedData; + const normalizedOptions = normalizeChecklistOptions(options); - const newOptions = options.map((option) => { + const newOptions = normalizedOptions.map((option) => { if (option.id === taskId) { return { ...option, @@ -104,8 +111,9 @@ export function removeTask(data: string, taskId: string): string { } const { options = [], selectedOptionIds = [] } = parsedData; + const normalizedOptions = normalizeChecklistOptions(options); - const newOptions = options.filter((option) => option.id !== taskId); + const newOptions = normalizedOptions.filter((option) => option.id !== taskId); const newSelectedOptionIds = selectedOptionIds.filter((id) => id !== taskId); return JSON.stringify({ @@ -122,15 +130,16 @@ export function reorderTasks(data: string, { beforeId, taskId }: { beforeId?: st } const { selectedOptionIds, options = [] } = parsedData; + const normalizedOptions = normalizeChecklistOptions(options); - const index = options.findIndex((opt) => opt.id === taskId); - const option = options[index]; + const index = normalizedOptions.findIndex((opt) => opt.id === taskId); + const option = normalizedOptions[index]; if (index === -1) { return data; } - const newOptions = [...options]; + const newOptions = [...normalizedOptions]; const beforeIndex = newOptions.findIndex((opt) => opt.id === beforeId); if (beforeIndex === index) { diff --git a/src/application/database-yjs/fields/select-option/parse.ts b/src/application/database-yjs/fields/select-option/parse.ts index a9077844..b104b644 100644 --- a/src/application/database-yjs/fields/select-option/parse.ts +++ b/src/application/database-yjs/fields/select-option/parse.ts @@ -27,7 +27,7 @@ export function parseSelectOptionCellData(field: YDatabaseField, data: string) { return selectedIds .map((id) => { - const option = typeOption?.options?.find((option) => option.id === id); + const option = typeOption?.options?.find((option) => option?.id === id); return option?.name ?? ''; }) diff --git a/src/application/database-yjs/fields/type_option.ts b/src/application/database-yjs/fields/type_option.ts index 39a8a40e..66f89f96 100644 --- a/src/application/database-yjs/fields/type_option.ts +++ b/src/application/database-yjs/fields/type_option.ts @@ -1,7 +1,7 @@ -import { YDatabaseField, YjsDatabaseKey } from '@/application/types'; +import { YDatabaseField, YMapFieldTypeOption, YjsDatabaseKey } from '@/application/types'; import { FieldType } from '@/application/database-yjs'; -export function getTypeOptions (field: YDatabaseField) { +export function getTypeOptions(field?: YDatabaseField): YMapFieldTypeOption | undefined { const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); diff --git a/src/application/database-yjs/group.ts b/src/application/database-yjs/group.ts index e4590094..d6ca810d 100644 --- a/src/application/database-yjs/group.ts +++ b/src/application/database-yjs/group.ts @@ -41,9 +41,11 @@ export function getGroupColumns(field: YDatabaseField) { return [{ id: field.get(YjsDatabaseKey.id) }]; } - const options = typeOption.options.map((option) => ({ - id: option.id, - })); + const options = typeOption.options + .map((option) => ({ + id: option?.id, + })) + .filter((option): option is { id: string } => Boolean(option.id)); return [{ id: field.get(YjsDatabaseKey.id) }, ...options]; } @@ -114,7 +116,11 @@ export function groupBySelectOption( } typeOption.options.forEach((option) => { - const groupName = option.id; + const groupName = option?.id; + + if (!groupName) { + return; + } if (filter) { const condition = Number(filter?.get(YjsDatabaseKey.condition)) as SelectOptionFilterCondition; @@ -156,7 +162,7 @@ export function groupBySelectOption( } selectedIds.forEach((id) => { - const option = typeOption.options.find((option) => option.id === id); + const option = typeOption.options.find((option) => option?.id === id); const groupName = option?.id ?? fieldId; if (!result.has(groupName)) { diff --git a/src/application/database-yjs/selector.ts b/src/application/database-yjs/selector.ts index 59f86108..bba9e213 100644 --- a/src/application/database-yjs/selector.ts +++ b/src/application/database-yjs/selector.ts @@ -1294,7 +1294,11 @@ export const useSelectFieldOptions = (fieldId: string, searchValue?: string) => if (!typeOption) return [] as SelectOption[]; - return typeOption.options.filter((option) => { + const normalizedOptions = typeOption.options.filter((option): option is SelectOption => { + return Boolean(option && option.id && option.name); + }); + + return normalizedOptions.filter((option) => { if (!searchValue) return true; return option.name.toLowerCase().includes(searchValue.toLowerCase()); }); diff --git a/src/components/database/components/board/column/ColumnRename.tsx b/src/components/database/components/board/column/ColumnRename.tsx index 96e99fc0..2d163dcb 100644 --- a/src/components/database/components/board/column/ColumnRename.tsx +++ b/src/components/database/components/board/column/ColumnRename.tsx @@ -30,7 +30,7 @@ function ColumnRename({ const option = useMemo(() => { if (!field || ![FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) return; - return parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id); + return parseSelectOptionTypeOptions(field)?.options.find((option) => option?.id === id); // eslint-disable-next-line react-hooks/exhaustive-deps }, [field, clock, id]); diff --git a/src/components/database/components/board/column/useRenderColumn.tsx b/src/components/database/components/board/column/useRenderColumn.tsx index 942aa267..c6f6eac6 100644 --- a/src/components/database/components/board/column/useRenderColumn.tsx +++ b/src/components/database/components/board/column/useRenderColumn.tsx @@ -35,7 +35,7 @@ export function useRenderColumn(id: string, fieldId: string) { ); if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) { - const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id); + const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option?.id === id); const isFieldId = fieldId === id; const label = isFieldId ? `${t('button.no')} ${fieldName}` : option?.name || ''; diff --git a/src/components/database/components/cell/select-option/SelectOptionCell.tsx b/src/components/database/components/cell/select-option/SelectOptionCell.tsx index 61bb1d73..1145ed43 100644 --- a/src/components/database/components/cell/select-option/SelectOptionCell.tsx +++ b/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -37,7 +37,7 @@ export function SelectOptionCell({ const renderSelectedOptions = useCallback( (selected: string[]) => selected.map((id) => { - const option = typeOption?.options?.find((option) => option.id === id); + const option = typeOption?.options?.find((option) => option?.id === id); if (!option) return null; return ( diff --git a/src/components/database/components/cell/select-option/SelectOptionCellMenu.tsx b/src/components/database/components/cell/select-option/SelectOptionCellMenu.tsx index 4044361b..c37cedeb 100644 --- a/src/components/database/components/cell/select-option/SelectOptionCellMenu.tsx +++ b/src/components/database/components/cell/select-option/SelectOptionCellMenu.tsx @@ -79,7 +79,7 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio if (!typeOption) return []; return selectOptionIds.map((id) => { - const option = typeOption.options?.find((option) => option.id === id); + const option = typeOption.options?.find((option) => option?.id === id); if (!option) return null; return { @@ -151,6 +151,7 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio const lastOption = options[options.length - 1]; if (hoveredId === 'create') { + if (!lastOption) return; setHoveredId(lastOption.id); return; } @@ -167,7 +168,11 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio return; } - const nextHoveredId = options[hoveredIndex - 1].id; + const previousOption = options[hoveredIndex - 1]; + + if (!previousOption) return; + + const nextHoveredId = previousOption.id; setHoveredId(nextHoveredId); @@ -181,6 +186,7 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio const firstOption = options[0]; if (hoveredId === 'create') { + if (!firstOption) return; setHoveredId(firstOption.id); return; } @@ -197,7 +203,11 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio return; } - const nextHoveredId = options[hoveredIndex + 1].id; + const nextOption = options[hoveredIndex + 1]; + + if (!nextOption) return; + + const nextHoveredId = nextOption.id; setHoveredId(nextHoveredId); }, [createdShow, options]); @@ -290,4 +300,4 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio ); } -export default SelectOptionCellMenu; \ No newline at end of file +export default SelectOptionCellMenu; diff --git a/src/components/database/components/filters/filter-menu/SelectOptionList.tsx b/src/components/database/components/filters/filter-menu/SelectOptionList.tsx index 61db66cc..780233cd 100644 --- a/src/components/database/components/filters/filter-menu/SelectOptionList.tsx +++ b/src/components/database/components/filters/filter-menu/SelectOptionList.tsx @@ -48,5 +48,9 @@ export function SelectOptionList({ ); if (!field || !typeOption) return null; - return