From 64e5f5b195e73440cb302d64140e189bfe853518 Mon Sep 17 00:00:00 2001 From: debuggingdan Date: Wed, 5 Nov 2025 06:20:04 +0100 Subject: [PATCH 1/2] feat: added date column type --- package.json | 2 + pnpm-lock.yaml | 31 +++++ src/app/layout-client.tsx | 1 + src/components/atoms/editable-date-cell.tsx | 118 ++++++++++++++++++ .../molecules/column-form-modal.tsx | 77 ++++++++++-- src/components/molecules/data-table-row.tsx | 21 ++++ src/components/organisms/data-view-table.tsx | 13 +- .../api/use-create-data-source-column.ts | 19 ++- .../endpoints/create-data-source-column.ts | 32 ++++- .../endpoints/update-data-source-column.ts | 44 ++++++- src/types/schemas/entities/container.ts | 24 +++- 11 files changed, 357 insertions(+), 25 deletions(-) create mode 100644 src/components/atoms/editable-date-cell.tsx diff --git a/package.json b/package.json index 8317519..509f83b 100755 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@blocknote/mantine": "^0.41.1", "@blocknote/react": "^0.41.1", "@mantine/core": "^8.3.4", + "@mantine/dates": "^8.3.6", "@mantine/form": "^8.3.5", "@mantine/hooks": "^8.3.4", "@mantine/modals": "^8.3.6", @@ -28,6 +29,7 @@ "@tabler/icons-react": "^3.35.0", "axios": "^1.12.2", "better-auth": "^1.3.28", + "dayjs": "^1.11.19", "dotenv": "^17.2.3", "envalid": "^8.1.0", "mysql2": "^3.15.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad59b47..2994223 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ importers: '@mantine/core': specifier: ^8.3.4 version: 8.3.5(@mantine/hooks@8.3.5(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mantine/dates': + specifier: ^8.3.6 + version: 8.3.6(@mantine/core@8.3.5(@mantine/hooks@8.3.5(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.3.5(react@19.1.0))(dayjs@1.11.19)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mantine/form': specifier: ^8.3.5 version: 8.3.5(react@19.1.0) @@ -43,6 +46,9 @@ importers: better-auth: specifier: ^1.3.28 version: 1.3.29(next@15.5.6(@babel/core@7.28.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + dayjs: + specifier: ^1.11.19 + version: 1.11.19 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -705,6 +711,16 @@ packages: react: ^18.x || ^19.x react-dom: ^18.x || ^19.x + '@mantine/dates@8.3.6': + resolution: + { integrity: sha512-lSi1zvyL86SKeePH0J3vOjAR7ZIVNOrZm6ja7jAH6IBdcpQOKH8TXbrcAi5okEStvmvkne7pVaGu0VkdE8KnAw== } + peerDependencies: + '@mantine/core': 8.3.6 + '@mantine/hooks': 8.3.6 + dayjs: '>=1.0.0' + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + '@mantine/form@8.3.5': resolution: { integrity: sha512-i9UFiHtO1dlrJXZkquyt+71YcNNxPPSkIcJCRp7k0Tif7bPqWK2xijPDEXzqvA53YvMvEMoqaQCEQLVmH7Esdg== } @@ -1855,6 +1871,10 @@ packages: { integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== } engines: { node: '>= 0.4' } + dayjs@1.11.19: + resolution: + { integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== } + debug@3.2.7: resolution: { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== } @@ -5418,6 +5438,15 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@mantine/dates@8.3.6(@mantine/core@8.3.5(@mantine/hooks@8.3.5(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.3.5(react@19.1.0))(dayjs@1.11.19)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@mantine/core': 8.3.5(@mantine/hooks@8.3.5(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mantine/hooks': 8.3.5(react@19.1.0) + clsx: 2.1.1 + dayjs: 1.11.19 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + '@mantine/form@8.3.5(react@19.1.0)': dependencies: fast-deep-equal: 3.1.3 @@ -6416,6 +6445,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + dayjs@1.11.19: {} + debug@3.2.7: dependencies: ms: 2.1.3 diff --git a/src/app/layout-client.tsx b/src/app/layout-client.tsx index 7edb3ec..0115347 100755 --- a/src/app/layout-client.tsx +++ b/src/app/layout-client.tsx @@ -1,5 +1,6 @@ 'use client'; import '@mantine/core/styles.css'; +import '@mantine/dates/styles.css'; import '@mantine/notifications/styles.css'; import { MantineProvider } from '@mantine/core'; import { ModalsProvider } from '@mantine/modals'; diff --git a/src/components/atoms/editable-date-cell.tsx b/src/components/atoms/editable-date-cell.tsx new file mode 100644 index 0000000..6dec733 --- /dev/null +++ b/src/components/atoms/editable-date-cell.tsx @@ -0,0 +1,118 @@ +/* eslint-disable unicorn/no-nested-ternary */ +'use client'; + +import { DatePicker, DateTimePicker } from '@mantine/dates'; +import { Popover } from '@mantine/core'; +import dayjs from 'dayjs'; +import { useState } from 'react'; + +type EditableDateCellProperties = { + value: string | null | undefined; + onBlur: (value: string) => void; + disabled?: boolean; + options?: { + dateFormat?: string; + includeTime?: boolean; + }; +}; + +export function EditableDateCell({ value, onBlur, disabled = false, options }: EditableDateCellProperties) { + const [opened, setOpened] = useState(false); + const includeTime = options?.includeTime ?? false; + const dateFormat = options?.dateFormat; + + // Parse ISO string to Date object for value prop, or null if empty + const dateValue = value ? dayjs(value).toDate() : null; + + // Convert to string format for DatePicker/DateTimePicker (they expect Date or string) + const dateValueString = value || null; + + // Format display value + const displayValue = + dateValue && dateFormat + ? dayjs(dateValue).format(dateFormat) + : dateValue + ? includeTime + ? dayjs(dateValue).format('YYYY-MM-DD HH:mm') + : dayjs(dateValue).format('YYYY-MM-DD') + : ''; + + const handleChange = (value: string | null) => { + if (!value) { + // If date is cleared, send empty string + onBlur(''); + setOpened(false); + return; + } + + // Parse the string value (could be formatted date string from picker) + const parsedDate = dayjs(value); + if (!parsedDate.isValid()) { + // If invalid, try to use the value as-is + onBlur(value); + setOpened(false); + return; + } + + // Convert to ISO string format + // If includeTime is false, only include date part (YYYY-MM-DD) + if (includeTime) { + const dateTime = parsedDate.toISOString(); + onBlur(dateTime); + } else { + const dateOnly = parsedDate.format('YYYY-MM-DD'); + onBlur(dateOnly); + } + setOpened(false); + }; + + const handleClick = () => { + if (!disabled) { + setOpened(true); + } + }; + + if (includeTime) { + return ( + + +
+ {displayValue} +
+
+ + + +
+ ); + } + + return ( + + +
+ {displayValue} +
+
+ + + +
+ ); +} diff --git a/src/components/molecules/column-form-modal.tsx b/src/components/molecules/column-form-modal.tsx index 743ed2a..3b000a7 100644 --- a/src/components/molecules/column-form-modal.tsx +++ b/src/components/molecules/column-form-modal.tsx @@ -1,12 +1,15 @@ -import { Button, Group, Modal, Select, Stack, TextInput } from '@mantine/core'; +import { Button, Checkbox, Group, Modal, Select, Stack, Text, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useEffect } from 'react'; import type { Column } from '@/types/schemas/entities/container'; -type ColumnFormValues = { - name: string; - type: 'string' | 'number' | 'boolean'; -}; +const ALLOWED_TYPES = ['string', 'number', 'boolean', 'date'] as const; + +type ColumnFormValues = + | { name: string; type: 'string' } + | { name: string; type: 'number' } + | { name: string; type: 'boolean' } + | { name: string; type: 'date'; options?: { dateFormat?: string; includeTime?: boolean } }; type ColumnFormModalProperties = { opened: boolean; @@ -31,17 +34,27 @@ export function ColumnFormModal({ initialValues: { name: initialValues?.name ?? '', type: initialValues?.type ?? 'string', + ...(initialValues?.type === 'date' && initialValues.options + ? { + options: { + ...(initialValues.options.dateFormat ? { dateFormat: initialValues.options.dateFormat } : {}), + ...(initialValues.options.includeTime === undefined + ? {} + : { includeTime: initialValues.options.includeTime }), + }, + } + : {}), }, validate: { name: (value) => (value.trim() ? null : 'Column name is required'), - type: (value) => { + type: (value: 'string' | 'number' | 'boolean' | 'date') => { if (!value) { return 'Column type is required'; } // TODO move this to a separate enum in the schema definition - const allowedTypes: ('string' | 'number' | 'boolean')[] = ['string', 'number', 'boolean']; - if (!allowedTypes.includes(value)) { - return 'Column type must be one of: string, number, boolean'; + + if (!ALLOWED_TYPES.includes(value)) { + return 'Column type must be one of: string, number, boolean, date'; } return null; }, @@ -53,6 +66,16 @@ export function ColumnFormModal({ form.setValues({ name: initialValues?.name ?? '', type: initialValues?.type ?? 'string', + ...(initialValues?.type === 'date' && initialValues.options + ? { + options: { + ...(initialValues.options.dateFormat ? { dateFormat: initialValues.options.dateFormat } : {}), + ...(initialValues.options.includeTime === undefined + ? {} + : { includeTime: initialValues.options.includeTime }), + }, + } + : {}), }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -65,7 +88,21 @@ export function ColumnFormModal({ const handleSubmit = async (values: ColumnFormValues) => { try { - await onSubmit(values); + // Only include options for date type, and only if there are values + const submitValues: ColumnFormValues = { + ...values, + ...(values.type === 'date' && + values.options && + (values.options.dateFormat || values.options.includeTime !== undefined) + ? { + options: { + ...(values.options.dateFormat ? { dateFormat: values.options.dateFormat } : {}), + ...(values.options.includeTime === undefined ? {} : { includeTime: values.options.includeTime }), + }, + } + : {}), + }; + await onSubmit(submitValues); handleClose(); } catch (error) { if (onError) { @@ -85,10 +122,30 @@ export function ColumnFormModal({ { value: 'string', label: 'Text' }, { value: 'number', label: 'Number' }, { value: 'boolean', label: 'Checkbox' }, + { value: 'date', label: 'Date' }, ]} {...form.getInputProps('type')} required /> + {form.values.type === 'date' && ( + <> + + Format string using{' '} + + dayjs tokens + {' '} + (optional) + + } + {...form.getInputProps('options.dateFormat')} + /> + + + )}