diff --git a/packages/ui/src/components/databases/boards/board-view-columns-collaborator.tsx b/packages/ui/src/components/databases/boards/board-view-columns-collaborator.tsx index 41350e9f..e6cd475e 100644 --- a/packages/ui/src/components/databases/boards/board-view-columns-collaborator.tsx +++ b/packages/ui/src/components/databases/boards/board-view-columns-collaborator.tsx @@ -4,7 +4,6 @@ import { toast } from 'sonner'; import { CollaboratorFieldAttributes, DatabaseViewFilterAttributes, - FieldValue, } from '@brainbox/core'; import { Avatar } from '@brainbox/ui/components/avatars/avatar'; import { BoardViewColumn } from '@brainbox/ui/components/databases/boards/board-view-column'; @@ -103,28 +102,13 @@ export const BoardViewColumnsCollaborator = ({ return; } - let newValue: FieldValue = value; - const currentValue = record.fields[field.id]; - if (currentValue && currentValue.type === 'string_array') { - const newOptions = [ - ...currentValue.value.filter( - (collaboratorId) => - collaboratorId !== collaborator.value - ), - ...newValue.value, - ]; - - newValue = { - type: 'string_array', - value: newOptions, - }; - } - + // For board view, REPLACE the value entirely (move to new column) + // Don't merge - dropping on a column means "assign to this collaborator" const result = await window.brainbox.executeMutation({ type: 'record.field.value.set', recordId: record.id, fieldId: field.id, - value: newValue, + value: value, accountId: workspace.accountId, workspaceId: workspace.id, }); @@ -162,31 +146,17 @@ export const BoardViewColumnsCollaborator = ({ return; } - if (!value) { - const result = await window.brainbox.executeMutation({ - type: 'record.field.value.delete', - recordId: record.id, - fieldId: field.id, - accountId: workspace.accountId, - workspaceId: workspace.id, - }); - - if (!result.success) { - toast.error(result.error.message); - } - } else { - const result = await window.brainbox.executeMutation({ - type: 'record.field.value.set', - recordId: record.id, - fieldId: field.id, - value, - accountId: workspace.accountId, - workspaceId: workspace.id, - }); - - if (!result.success) { - toast.error(result.error.message); - } + // "No Value" column always clears the field + const result = await window.brainbox.executeMutation({ + type: 'record.field.value.delete', + recordId: record.id, + fieldId: field.id, + accountId: workspace.accountId, + workspaceId: workspace.id, + }); + + if (!result.success) { + toast.error(result.error.message); } }, }} diff --git a/packages/ui/src/components/databases/boards/board-view-columns-multi-select.tsx b/packages/ui/src/components/databases/boards/board-view-columns-multi-select.tsx index b1c37857..29c7007b 100644 --- a/packages/ui/src/components/databases/boards/board-view-columns-multi-select.tsx +++ b/packages/ui/src/components/databases/boards/board-view-columns-multi-select.tsx @@ -3,7 +3,6 @@ import { toast } from 'sonner'; import { DatabaseViewFilterAttributes, - FieldValue, MultiSelectFieldAttributes, SelectOptionAttributes, } from '@brainbox/core'; @@ -113,27 +112,13 @@ export const BoardViewColumnsMultiSelect = ({ return; } - let newValue: FieldValue = value; - const currentValue = record.fields[field.id]; - if (currentValue && currentValue.type === 'string_array') { - const newOptions = [ - ...currentValue.value.filter( - (optionId) => optionId !== option.id - ), - ...newValue.value, - ]; - - newValue = { - type: 'string_array', - value: newOptions, - }; - } - + // For board view, REPLACE the value entirely (move to new column) + // Don't merge - dropping on a column means "set to this option" const result = await window.brainbox.executeMutation({ type: 'record.field.value.set', recordId: record.id, fieldId: field.id, - value: newValue, + value: value, accountId: workspace.accountId, workspaceId: workspace.id, }); @@ -172,31 +157,17 @@ export const BoardViewColumnsMultiSelect = ({ return; } - if (!value) { - const result = await window.brainbox.executeMutation({ - type: 'record.field.value.delete', - recordId: record.id, - fieldId: field.id, - accountId: workspace.accountId, - workspaceId: workspace.id, - }); - - if (!result.success) { - toast.error(result.error.message); - } - } else { - const result = await window.brainbox.executeMutation({ - type: 'record.field.value.set', - recordId: record.id, - fieldId: field.id, - value, - accountId: workspace.accountId, - workspaceId: workspace.id, - }); - - if (!result.success) { - toast.error(result.error.message); - } + // "No Value" column always clears the field + const result = await window.brainbox.executeMutation({ + type: 'record.field.value.delete', + recordId: record.id, + fieldId: field.id, + accountId: workspace.accountId, + workspaceId: workspace.id, + }); + + if (!result.success) { + toast.error(result.error.message); } }, }} diff --git a/packages/ui/src/components/databases/boards/board-view.tsx b/packages/ui/src/components/databases/boards/board-view.tsx index bf3ec001..6fb3b3ac 100644 --- a/packages/ui/src/components/databases/boards/board-view.tsx +++ b/packages/ui/src/components/databases/boards/board-view.tsx @@ -33,7 +33,7 @@ const BoardViewContent = () => {
-
+
diff --git a/packages/ui/src/components/databases/calendars/calendar-view.tsx b/packages/ui/src/components/databases/calendars/calendar-view.tsx index b7e78657..770e5829 100644 --- a/packages/ui/src/components/databases/calendars/calendar-view.tsx +++ b/packages/ui/src/components/databases/calendars/calendar-view.tsx @@ -24,7 +24,7 @@ export const CalendarView = () => {
-
+
{groupByField && } diff --git a/packages/ui/src/components/databases/database-form.tsx b/packages/ui/src/components/databases/database-form.tsx index d10bc7d9..0366702b 100644 --- a/packages/ui/src/components/databases/database-form.tsx +++ b/packages/ui/src/components/databases/database-form.tsx @@ -4,6 +4,8 @@ import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod/v4'; +import { Avatar } from '@brainbox/ui/components/avatars/avatar'; +import { AvatarPopover } from '@brainbox/ui/components/avatars/avatar-popover'; import { Button } from '@brainbox/ui/components/ui/button'; import { Form, @@ -31,7 +33,7 @@ interface DatabaseFormProps { } export const DatabaseForm = ({ - id: _id, + id, values, isPending, submitText, @@ -44,6 +46,8 @@ export const DatabaseForm = ({ defaultValues: values, }); + const avatarValue = form.watch('avatar'); + useEffect(() => { if (readOnly) return; @@ -54,6 +58,16 @@ export const DatabaseForm = ({ return () => clearTimeout(timeoutId); }, [readOnly]); + const iconButton = ( + + ); + return (
- + {readOnly ? ( + iconButton + ) : ( + form.setValue('avatar', avatar)}> + {iconButton} + + )} ( - + diff --git a/packages/ui/src/components/databases/tables/table-column-drag-layer.tsx b/packages/ui/src/components/databases/tables/table-column-drag-layer.tsx index 11e0b9d7..f6fca4db 100644 --- a/packages/ui/src/components/databases/tables/table-column-drag-layer.tsx +++ b/packages/ui/src/components/databases/tables/table-column-drag-layer.tsx @@ -3,7 +3,6 @@ import { useDragLayer } from 'react-dnd'; import { ViewField } from '@brainbox/client/types'; import { FieldIcon } from '@brainbox/ui/components/databases/fields/field-icon'; -import { dragPreviewStyles } from '@brainbox/ui/lib/drag-feedback'; import { cn } from '@brainbox/ui/lib/utils'; interface TableColumnDragLayerProps { @@ -73,18 +72,20 @@ export const TableColumnDragLayer: React.FC = ({
-

+

{item.field.name}

diff --git a/packages/ui/src/components/databases/tables/table-view-empty-placeholder.tsx b/packages/ui/src/components/databases/tables/table-view-empty-placeholder.tsx index 6b48db76..bce7a1d0 100644 --- a/packages/ui/src/components/databases/tables/table-view-empty-placeholder.tsx +++ b/packages/ui/src/components/databases/tables/table-view-empty-placeholder.tsx @@ -1,7 +1,36 @@ +import { Plus, Table } from 'lucide-react'; + +import { Button } from '@brainbox/ui/components/ui/button'; +import { useDatabase } from '@brainbox/ui/contexts/database'; +import { useDatabaseView } from '@brainbox/ui/contexts/database-view'; + export const TableViewEmptyPlaceholder = () => { + const database = useDatabase(); + const view = useDatabaseView(); + const hasFilters = view.filters.length > 0; + return ( -
-

No records

+
+
+ + +

No records yet

+

+ {hasFilters + ? 'No records match your current filters. Try adjusting or clearing filters to see more records.' + : 'Add your first record to start organizing your data in this database.'} +

+ {database.canCreateRecord && ( + + )} ); }; diff --git a/packages/ui/src/components/databases/tables/table-view-field-header.tsx b/packages/ui/src/components/databases/tables/table-view-field-header.tsx index c9cc2088..60afc2f5 100644 --- a/packages/ui/src/components/databases/tables/table-view-field-header.tsx +++ b/packages/ui/src/components/databases/tables/table-view-field-header.tsx @@ -308,8 +308,8 @@ export const TableViewFieldHeader = memo( width: `${viewField.width}px`, height: '2rem', }} - minWidth={100} - maxWidth={500} + minWidth={50} + maxWidth={1200} size={{ width: isResizing ? `${resizeWidth}px` : `${viewField.width}px`, height: '2rem', @@ -380,6 +380,16 @@ export const TableViewFieldHeader = memo( } }} > + {/* Double-click target for auto-fit */} + {database.canEdit && ( +
{ + e.stopPropagation(); + view.autoFitFieldWidth(viewField.field.id); + }} + /> + )} diff --git a/packages/ui/src/components/databases/tables/table-view-name-header.tsx b/packages/ui/src/components/databases/tables/table-view-name-header.tsx index 6ad2d075..173f86fe 100644 --- a/packages/ui/src/components/databases/tables/table-view-name-header.tsx +++ b/packages/ui/src/components/databases/tables/table-view-name-header.tsx @@ -65,8 +65,8 @@ export const TableViewNameHeader = () => { width: `${view.nameWidth}px`, height: '2rem', }} - minWidth={300} - maxWidth={500} + minWidth={120} + maxWidth={800} size={{ width: isResizing ? `${resizeWidth}px` : `${view.nameWidth}px`, height: '2rem', diff --git a/packages/ui/src/components/databases/tables/table-view-settings.tsx b/packages/ui/src/components/databases/tables/table-view-settings.tsx index fbe27d70..e9108188 100644 --- a/packages/ui/src/components/databases/tables/table-view-settings.tsx +++ b/packages/ui/src/components/databases/tables/table-view-settings.tsx @@ -1,4 +1,4 @@ -import { Eye, EyeOff, Trash2 } from 'lucide-react'; +import { Eye, EyeOff, Settings2, Trash2 } from 'lucide-react'; import { Fragment, useState } from 'react'; import { AvatarPopover } from '@brainbox/ui/components/avatars/avatar-popover'; @@ -13,8 +13,13 @@ import { PopoverContent, PopoverTrigger, } from '@brainbox/ui/components/ui/popover'; -import { Separator } from '@brainbox/ui/components/ui/separator'; import { SmartTextInput } from '@brainbox/ui/components/ui/smart-text-input'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@brainbox/ui/components/ui/tabs'; import { Tooltip, TooltipContent, @@ -38,133 +43,153 @@ export const TableViewSettings = () => { - -
- {database.canEdit ? ( - { - view.updateAvatar(avatar); - }} + + + + + + View + + - - - ) : ( - + + ) : ( + + )} + { + if (newName === view.name) return; + view.rename(newName); + }} /> - - )} - { - if (newName === view.name) return; +
- view.rename(newName); - }} - /> -
- -
-

Fields

- {database.fields.map((field) => { - const isDisplayed = - view.fields.find((f) => f.field.id === field.id)?.display ?? - false; + {database.canEdit && ( +
+

+ Danger zone +

+ +
+ )} + - return ( -
-
- -
{field.name}
-
-
- - - { - if (!database.canEdit) return; + +
+ {database.fields.map((field) => { + const isDisplayed = + view.fields.find((f) => f.field.id === field.id)?.display ?? + false; - view.setFieldDisplay(field.id, !isDisplayed); - }} - > - {isDisplayed ? ( - - ) : ( - - )} - - - - {isDisplayed - ? 'Hide field from this view' - : 'Show field in this view'} - - - {database.canEdit && ( - - - { - setDeleteFieldId(field.id); - setOpen(false); - }} - /> - - - Delete field from database - - - )} -
-
- ); - })} -
- {database.canEdit && ( - - -
-

Settings

-
{ - setOpenDelete(true); - setOpen(false); - }} - > - - Delete view -
+ return ( +
+
+ + {field.name} +
+
+ + + + + + {isDisplayed ? 'Hide from view' : 'Show in view'} + + + {database.canEdit && ( + + + + + Delete field + + )} +
+
+ ); + })}
-
- )} + + {deleteFieldId && ( diff --git a/packages/ui/src/components/databases/tables/table-view.tsx b/packages/ui/src/components/databases/tables/table-view.tsx index c86dd98f..a5d9b79f 100644 --- a/packages/ui/src/components/databases/tables/table-view.tsx +++ b/packages/ui/src/components/databases/tables/table-view.tsx @@ -16,7 +16,7 @@ export const TableView = () => {
-
+
diff --git a/packages/ui/src/components/databases/view-create-dropdown.tsx b/packages/ui/src/components/databases/view-create-dropdown.tsx index ef2f107d..bcb1491c 100644 --- a/packages/ui/src/components/databases/view-create-dropdown.tsx +++ b/packages/ui/src/components/databases/view-create-dropdown.tsx @@ -20,21 +20,9 @@ interface ViewTypeOption { } const viewTypes: ViewTypeOption[] = [ - { - name: 'Table', - icon: Table, - type: 'table', - }, - { - name: 'Board', - icon: Columns, - type: 'board', - }, - { - name: 'Calendar', - icon: Calendar, - type: 'calendar', - }, + { name: 'Table', icon: Table, type: 'table' }, + { name: 'Board', icon: Columns, type: 'board' }, + { name: 'Calendar', icon: Calendar, type: 'calendar' }, ]; export const ViewCreateDropdown = () => { @@ -49,12 +37,10 @@ export const ViewCreateDropdown = () => { return; } - // Generate unique view name to avoid conflicts const baseName = `${viewType.name} view`; let viewName = baseName; let counter = 1; - // Find a unique name by appending numbers if needed while (views.some((view) => view.attributes.name === viewName)) { counter++; viewName = `${baseName} ${counter}`; @@ -72,16 +58,13 @@ export const ViewCreateDropdown = () => { onSuccess(data) { setOpen(false); - // Properly coordinate view switching using mutation response if ( data && typeof data === 'object' && 'id' in data && typeof data.id === 'string' ) { - // Mark this view as pending to prevent race condition resets setPendingViewId(data.id); - // Switch to the newly created view setActiveViewId(data.id); } }, @@ -98,25 +81,19 @@ export const ViewCreateDropdown = () => { return ( - - + {viewTypes.map((viewType) => ( createView(viewType)} > -
-

{viewType.name}

-
+ {viewType.name}
))}
diff --git a/packages/ui/src/components/databases/view-tab.tsx b/packages/ui/src/components/databases/view-tab.tsx index a6b93db0..a8fdcb2d 100644 --- a/packages/ui/src/components/databases/view-tab.tsx +++ b/packages/ui/src/components/databases/view-tab.tsx @@ -10,14 +10,25 @@ interface ViewTabProps { export const ViewTab = ({ view, isActive, onClick }: ViewTabProps) => { return ( -
onClick()} - onKeyDown={() => onClick()} + onClick={onClick} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + }} > { layout={view.attributes.layout} className="size-4" /> - {view.attributes.name} -
+ {view.attributes.name} + ); }; diff --git a/packages/ui/src/components/databases/view.tsx b/packages/ui/src/components/databases/view.tsx index 9c681901..92102dbb 100644 --- a/packages/ui/src/components/databases/view.tsx +++ b/packages/ui/src/components/databases/view.tsx @@ -220,6 +220,36 @@ export const View = ({ view }: ViewProps) => { ] ); + const autoFitFieldWidth = useCallback( + async (id: string) => { + if (!database.canEdit) { + return; + } + + const field = database.fields.find((f) => f.id === id); + if (!field) { + return; + } + + const cells = document.querySelectorAll(`[data-cell-field-id="${id}"]`); + let maxWidth = 80; + + cells.forEach((cell) => { + const content = cell.querySelector('[data-cell-content]'); + if (content) { + const contentWidth = content.scrollWidth + 32; + maxWidth = Math.max(maxWidth, Math.min(contentWidth, 600)); + } + }); + + const headerWidth = field.name.length * 8 + 48; + maxWidth = Math.max(maxWidth, Math.min(headerWidth, 400)); + + await resizeField(id, maxWidth); + }, + [database.canEdit, database.fields, resizeField] + ); + const resizeName = useCallback( async (width: number) => { if (!database.canEdit) { @@ -732,6 +762,7 @@ export const View = ({ view }: ViewProps) => { updateAvatar, setFieldDisplay, resizeField, + autoFitFieldWidth, resizeName, setGroupBy, setSwimlaneBy, @@ -769,6 +800,7 @@ export const View = ({ view }: ViewProps) => { updateAvatar, setFieldDisplay, resizeField, + autoFitFieldWidth, resizeName, setGroupBy, setSwimlaneBy, diff --git a/packages/ui/src/contexts/database-view.ts b/packages/ui/src/contexts/database-view.ts index 61f2adbe..8f90e393 100644 --- a/packages/ui/src/contexts/database-view.ts +++ b/packages/ui/src/contexts/database-view.ts @@ -25,6 +25,7 @@ interface DatabaseViewContext { updateAvatar: (avatar: string) => void; setFieldDisplay: (id: string, display: boolean) => void; resizeField: (id: string, width: number) => void; + autoFitFieldWidth: (id: string) => void; resizeName: (width: number) => void; setGroupBy: (fieldId: string | null) => void; setSwimlaneBy: (fieldId: string | null) => void;