Merge branch 'main' into chore/fix-publish-relation

This commit is contained in:
Nathan
2025-10-31 07:53:35 +08:00
15 changed files with 119 additions and 64 deletions

View File

@@ -107,9 +107,7 @@ export function getCellDataText(cell: YDatabaseCell, field: YDatabaseField, curr
return ( return (
data data
.split(',') .split(',')
.map((item) => { .map((item) => options?.find((option) => option?.id === item)?.name)
return options?.find((option) => option.id === item)?.name;
})
.filter((item) => item) .filter((item) => item)
.join(',') || '' .join(',') || ''
); );

View File

@@ -187,13 +187,19 @@ function generateGroupByField(field: YDatabaseField) {
case FieldType.MultiSelect: { case FieldType.MultiSelect: {
group.set(YjsDatabaseKey.content, ''); group.set(YjsDatabaseKey.content, '');
const typeOption = parseSelectOptionTypeOptions(field); const typeOption = parseSelectOptionTypeOptions(field);
const options = typeOption?.options || []; const options = (typeOption?.options || []).filter((option) => Boolean(option && option.id));
columns.push([{ id: fieldId, visible: true }]); columns.push([{ id: fieldId, visible: true }]);
// Add a column for each option // Add a column for each option
options.forEach((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; break;
} }
@@ -2678,13 +2684,20 @@ export function useSwitchPropertyType() {
// 1. to RichText // 1. to RichText
if ([FieldType.RichText, FieldType.URL].includes(fieldType)) { if ([FieldType.RichText, FieldType.URL].includes(fieldType)) {
const cellType = Number(cell.get(YjsDatabaseKey.field_type)); 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) { switch (cellType) {
// From Number to RichText, keep the number format value // From Number to RichText, keep the number format value
case FieldType.Number: { case FieldType.Number: {
const formatRaw = existingTypeOption?.get(YjsDatabaseKey.format);
const parsedFormat =
formatRaw === undefined || formatRaw === null ? undefined : Number(formatRaw);
const format = const format =
(Number(typeOption.get(YjsDatabaseKey.format)) as NumberFormat) ?? NumberFormat.Num; parsedFormat === undefined || Number.isNaN(parsedFormat)
? NumberFormat.Num
: (parsedFormat as NumberFormat);
if (data) { if (data) {
newData = EnhancedBigStats.parse(data.toString(), format) || ''; newData = EnhancedBigStats.parse(data.toString(), format) || '';
@@ -2696,8 +2709,13 @@ export function useSwitchPropertyType() {
case FieldType.SingleSelect: case FieldType.SingleSelect:
case FieldType.MultiSelect: { case FieldType.MultiSelect: {
const selectedIds = (data as string).split(','); const selectedIds = (data as string).split(',');
const typeOption = typeOptionMap.get(String(cellType)); const optionSource = typeOptionMap?.get(String(cellType)) as YMapFieldTypeOption | undefined;
const content = typeOption.get(YjsDatabaseKey.content); const content = optionSource?.get(YjsDatabaseKey.content);
if (typeof content !== 'string') {
newData = '';
break;
}
try { try {
const parsedContent = JSON.parse(content) as SelectTypeOption; const parsedContent = JSON.parse(content) as SelectTypeOption;
@@ -2766,29 +2784,35 @@ export function useSwitchPropertyType() {
// 3. to SingleSelect or MultiSelect // 3. to SingleSelect or MultiSelect
if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) { if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) {
const typeOption = typeOptionMap.get(String(fieldType)); const targetTypeOption = typeOptionMap?.get(String(fieldType)) as
const content = typeOption.get(YjsDatabaseKey.content); | YMapFieldTypeOption
| undefined;
const content = targetTypeOption?.get(YjsDatabaseKey.content);
try { if (typeof content === 'string') {
const parsedContent = JSON.parse(content) as SelectTypeOption; try {
const options = parsedContent.options; const parsedContent = JSON.parse(content) as SelectTypeOption;
const options = parsedContent.options;
const selectedOptionNames = (data as string).split(','); const selectedOptionNames = (data as string).split(',');
const selectedOptionIds = selectedOptionNames const selectedOptionIds = selectedOptionNames
.map((name) => { .map((name) => {
const option = options.find((opt) => opt.name === name || opt.id === name); const option = options.find((opt) => opt.name === name || opt.id === name);
if (!option) { if (!option) {
return ''; return '';
} }
return option.id; return option.id;
}) })
.filter((id) => id !== ''); .filter((id) => id !== '');
newData = selectedOptionIds.join(','); newData = selectedOptionIds.join(',');
} catch (e) { } catch (e) {
// do nothing // do nothing
}
} else {
newData = '';
} }
} }
@@ -2996,12 +3020,14 @@ export function useAddSelectOption(fieldId: string) {
if (group) { if (group) {
const columns = group.get(YjsDatabaseKey.groups); 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) { if (!column) {
columns.push([ columns.push([
{ {
id: option.id, id: optionId,
visible: true, visible: true,
}, },
]); ]);
@@ -3838,4 +3864,4 @@ export function useUpdateCalendarSetting() {
}, },
[sharedRoot, view] [sharedRoot, view]
); );
} }

View File

@@ -7,6 +7,10 @@ export interface ChecklistCellData {
percentage: number; percentage: number;
} }
function normalizeChecklistOptions(options: SelectOption[] = []) {
return options.filter((option): option is SelectOption => Boolean(option && option.id));
}
export function parseChecklistData(data: string): ChecklistCellData | null { export function parseChecklistData(data: string): ChecklistCellData | null {
try { try {
const { options, selected_option_ids } = JSON.parse(data); 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 { 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 data;
} }
return JSON.stringify({ return JSON.stringify({
options: [...options, task], options: [...normalizedOptions, task],
selected_option_ids: selectedOptionIds, selected_option_ids: selectedOptionIds,
}); });
} }
@@ -58,6 +63,7 @@ export function toggleSelectedTask(data: string, taskId: string): string {
} }
const { options, selectedOptionIds = [] } = parsedData; const { options, selectedOptionIds = [] } = parsedData;
const normalizedOptions = normalizeChecklistOptions(options);
const isSelected = selectedOptionIds.includes(taskId); const isSelected = selectedOptionIds.includes(taskId);
const newSelectedOptionIds = isSelected const newSelectedOptionIds = isSelected
@@ -65,7 +71,7 @@ export function toggleSelectedTask(data: string, taskId: string): string {
: [...selectedOptionIds, taskId]; : [...selectedOptionIds, taskId];
return JSON.stringify({ return JSON.stringify({
options, options: normalizedOptions,
selected_option_ids: newSelectedOptionIds, selected_option_ids: newSelectedOptionIds,
}); });
} }
@@ -78,8 +84,9 @@ export function updateTask(data: string, taskId: string, taskName: string): stri
} }
const { options = [], selectedOptionIds } = parsedData; const { options = [], selectedOptionIds } = parsedData;
const normalizedOptions = normalizeChecklistOptions(options);
const newOptions = options.map((option) => { const newOptions = normalizedOptions.map((option) => {
if (option.id === taskId) { if (option.id === taskId) {
return { return {
...option, ...option,
@@ -104,8 +111,9 @@ export function removeTask(data: string, taskId: string): string {
} }
const { options = [], selectedOptionIds = [] } = parsedData; 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); const newSelectedOptionIds = selectedOptionIds.filter((id) => id !== taskId);
return JSON.stringify({ return JSON.stringify({
@@ -122,15 +130,16 @@ export function reorderTasks(data: string, { beforeId, taskId }: { beforeId?: st
} }
const { selectedOptionIds, options = [] } = parsedData; const { selectedOptionIds, options = [] } = parsedData;
const normalizedOptions = normalizeChecklistOptions(options);
const index = options.findIndex((opt) => opt.id === taskId); const index = normalizedOptions.findIndex((opt) => opt.id === taskId);
const option = options[index]; const option = normalizedOptions[index];
if (index === -1) { if (index === -1) {
return data; return data;
} }
const newOptions = [...options]; const newOptions = [...normalizedOptions];
const beforeIndex = newOptions.findIndex((opt) => opt.id === beforeId); const beforeIndex = newOptions.findIndex((opt) => opt.id === beforeId);
if (beforeIndex === index) { if (beforeIndex === index) {

View File

@@ -27,7 +27,7 @@ export function parseSelectOptionCellData(field: YDatabaseField, data: string) {
return selectedIds return selectedIds
.map((id) => { .map((id) => {
const option = typeOption?.options?.find((option) => option.id === id); const option = typeOption?.options?.find((option) => option?.id === id);
return option?.name ?? ''; return option?.name ?? '';
}) })

View File

@@ -1,7 +1,7 @@
import { YDatabaseField, YjsDatabaseKey } from '@/application/types'; import { YDatabaseField, YMapFieldTypeOption, YjsDatabaseKey } from '@/application/types';
import { FieldType } from '@/application/database-yjs'; 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; const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType));

View File

@@ -41,9 +41,11 @@ export function getGroupColumns(field: YDatabaseField) {
return [{ id: field.get(YjsDatabaseKey.id) }]; return [{ id: field.get(YjsDatabaseKey.id) }];
} }
const options = typeOption.options.map((option) => ({ const options = typeOption.options
id: option.id, .map((option) => ({
})); id: option?.id,
}))
.filter((option): option is { id: string } => Boolean(option.id));
return [{ id: field.get(YjsDatabaseKey.id) }, ...options]; return [{ id: field.get(YjsDatabaseKey.id) }, ...options];
} }
@@ -114,7 +116,11 @@ export function groupBySelectOption(
} }
typeOption.options.forEach((option) => { typeOption.options.forEach((option) => {
const groupName = option.id; const groupName = option?.id;
if (!groupName) {
return;
}
if (filter) { if (filter) {
const condition = Number(filter?.get(YjsDatabaseKey.condition)) as SelectOptionFilterCondition; const condition = Number(filter?.get(YjsDatabaseKey.condition)) as SelectOptionFilterCondition;
@@ -156,7 +162,7 @@ export function groupBySelectOption(
} }
selectedIds.forEach((id) => { 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; const groupName = option?.id ?? fieldId;
if (!result.has(groupName)) { if (!result.has(groupName)) {

View File

@@ -1294,7 +1294,11 @@ export const useSelectFieldOptions = (fieldId: string, searchValue?: string) =>
if (!typeOption) return [] as SelectOption[]; 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; if (!searchValue) return true;
return option.name.toLowerCase().includes(searchValue.toLowerCase()); return option.name.toLowerCase().includes(searchValue.toLowerCase());
}); });

View File

@@ -30,7 +30,7 @@ function ColumnRename({
const option = useMemo(() => { const option = useMemo(() => {
if (!field || ![FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) return; 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [field, clock, id]); }, [field, clock, id]);

View File

@@ -35,7 +35,7 @@ export function useRenderColumn(id: string, fieldId: string) {
</div> </div>
); );
if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) { 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 isFieldId = fieldId === id;
const label = isFieldId ? `${t('button.no')} ${fieldName}` : option?.name || ''; const label = isFieldId ? `${t('button.no')} ${fieldName}` : option?.name || '';

View File

@@ -37,7 +37,7 @@ export function SelectOptionCell({
const renderSelectedOptions = useCallback( const renderSelectedOptions = useCallback(
(selected: string[]) => (selected: string[]) =>
selected.map((id) => { 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; if (!option) return null;
return ( return (

View File

@@ -79,7 +79,7 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio
if (!typeOption) return []; if (!typeOption) return [];
return selectOptionIds.map((id) => { 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; if (!option) return null;
return { return {
@@ -151,6 +151,7 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio
const lastOption = options[options.length - 1]; const lastOption = options[options.length - 1];
if (hoveredId === 'create') { if (hoveredId === 'create') {
if (!lastOption) return;
setHoveredId(lastOption.id); setHoveredId(lastOption.id);
return; return;
} }
@@ -167,7 +168,11 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio
return; return;
} }
const nextHoveredId = options[hoveredIndex - 1].id; const previousOption = options[hoveredIndex - 1];
if (!previousOption) return;
const nextHoveredId = previousOption.id;
setHoveredId(nextHoveredId); setHoveredId(nextHoveredId);
@@ -181,6 +186,7 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio
const firstOption = options[0]; const firstOption = options[0];
if (hoveredId === 'create') { if (hoveredId === 'create') {
if (!firstOption) return;
setHoveredId(firstOption.id); setHoveredId(firstOption.id);
return; return;
} }
@@ -197,7 +203,11 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio
return; return;
} }
const nextHoveredId = options[hoveredIndex + 1].id; const nextOption = options[hoveredIndex + 1];
if (!nextOption) return;
const nextHoveredId = nextOption.id;
setHoveredId(nextHoveredId); setHoveredId(nextHoveredId);
}, [createdShow, options]); }, [createdShow, options]);
@@ -290,4 +300,4 @@ function SelectOptionCellMenu ({ open, onOpenChange, fieldId, rowId, selectOptio
); );
} }
export default SelectOptionCellMenu; export default SelectOptionCellMenu;

View File

@@ -48,5 +48,9 @@ export function SelectOptionList({
); );
if (!field || !typeOption) return null; if (!field || !typeOption) return null;
return <div className={'flex flex-col'}>{typeOption.options.map(renderOption)}</div>; const normalizedOptions = typeOption.options.filter((option): option is SelectOption => {
return Boolean(option && option.id && option.name);
});
return <div className={'flex flex-col'}>{normalizedOptions.map(renderOption)}</div>;
} }

View File

@@ -16,7 +16,7 @@ function SelectFilterContentOverview({ filter, field }: { filter: SelectOptionFi
const options = filter.optionIds const options = filter.optionIds
.map((optionId) => { .map((optionId) => {
const option = typeOption?.options?.find((option) => option.id === optionId); const option = typeOption?.options?.find((option) => option?.id === optionId);
return option?.name; return option?.name;
}) })

View File

@@ -22,7 +22,8 @@ function DataTimePropertyMenuContent({
const { t } = useTranslation(); const { t } = useTranslation();
const typeOption = useFieldTypeOption(fieldId); const typeOption = useFieldTypeOption(fieldId);
const includeTime = Boolean(typeOption.get(YjsDatabaseKey.include_time)); const includeTimeRaw = typeOption?.get(YjsDatabaseKey.include_time);
const includeTime = typeof includeTimeRaw === 'boolean' ? includeTimeRaw : Boolean(includeTimeRaw);
const updateFormat = useUpdateDateTimeFieldFormat(fieldId); const updateFormat = useUpdateDateTimeFieldFormat(fieldId);

View File

@@ -1,5 +1,6 @@
import { getFieldDateTimeFormats } from '@/application/database-yjs';
import { useUpdateDateTimeFieldFormat } from '@/application/database-yjs/dispatch'; import { useUpdateDateTimeFieldFormat } from '@/application/database-yjs/dispatch';
import { DateFormat, TimeFormat, YjsDatabaseKey } from '@/application/types'; import { DateFormat, TimeFormat } from '@/application/types';
import { useFieldTypeOption } from '@/components/database/components/cell/Cell.hooks'; import { useFieldTypeOption } from '@/components/database/components/cell/Cell.hooks';
import { import {
DropdownMenuGroup, DropdownMenuItem, DropdownMenuItemTick, DropdownMenuGroup, DropdownMenuItem, DropdownMenuItemTick,
@@ -17,11 +18,7 @@ function DateTimeFormatGroup ({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const typeOption = useFieldTypeOption(fieldId); const typeOption = useFieldTypeOption(fieldId);
const typeOptionDateFormat = typeOption.get(YjsDatabaseKey.date_format); const { dateFormat: selectedDateFormat, timeFormat: selectedTimeFormat } = getFieldDateTimeFormats(typeOption, undefined);
const typeOptionTimeFormat = typeOption.get(YjsDatabaseKey.time_format);
const selectedDateFormat = typeOptionDateFormat !== undefined ? Number(typeOptionDateFormat) : undefined;
const selectedTimeFormat = typeOptionTimeFormat !== undefined ? Number(typeOptionTimeFormat) : undefined;
const updateFormat = useUpdateDateTimeFieldFormat(fieldId); const updateFormat = useUpdateDateTimeFieldFormat(fieldId);
const dateFormats = useMemo(() => [{ const dateFormats = useMemo(() => [{
@@ -128,4 +125,4 @@ function DateTimeFormatGroup ({
); );
} }
export default DateTimeFormatGroup; export default DateTimeFormatGroup;