diff --git a/package.json b/package.json index 9fa20b9dc0..c955636f4c 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "@ai-sdk/svelte": "^1.1.24", "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@10305c4", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@634a501", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@634a501", "@faker-js/faker": "^9.9.0", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26d4c94dde..c79c99f737 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,14 +18,14 @@ importers: specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@10305c4 - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@10305c4(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@634a501 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@634a501(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4 - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@634a501 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@634a501(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 @@ -269,8 +269,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@10305c4': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@10305c4} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@634a501': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@634a501} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -284,8 +284,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@634a501': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@634a501} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -3709,7 +3709,7 @@ snapshots: dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@10305c4(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@634a501(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3722,7 +3722,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@634a501(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 diff --git a/src/lib/components/card.svelte b/src/lib/components/card.svelte index fd94436ab9..c21c90ef55 100644 --- a/src/lib/components/card.svelte +++ b/src/lib/components/card.svelte @@ -20,21 +20,23 @@ }; type ButtonProps = { - isButton: true; + isButton: boolean; href?: never; }; type AnchorProps = { href: string; - isButton?: never; + isButton?: boolean; + external?: boolean; }; + let classes = ''; type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps) & BaseCardProps; - export let isDashed = false; - export let isButton = false; + export let isDashed: boolean = false; + export let isButton: boolean = false; export let href: string = null; - let classes = ''; + export let external: boolean = false; export { classes as class }; export let style = ''; export let padding: $$Props['padding'] = 'm'; @@ -45,7 +47,15 @@ {#if href} - + diff --git a/src/lib/elements/forms/inputLine.svelte b/src/lib/elements/forms/inputLine.svelte index 3987b3635b..6bfeb0fe2e 100644 --- a/src/lib/elements/forms/inputLine.svelte +++ b/src/lib/elements/forms/inputLine.svelte @@ -14,6 +14,7 @@ onDeletePoint: (index: number) => void; onChangePoint: (pointIndex: number, coordIndex: number, newValue: number) => void; addLineButton?: Snippet; + disabled?: boolean; }; let { @@ -24,7 +25,8 @@ onAddPoint, onDeletePoint, onChangePoint, - addLineButton + addLineButton, + disabled }: Props = $props(); function isDeleteDisabled(index: number) { @@ -40,6 +42,7 @@ {#each values as value, index} - {@render addLineButton?.()} diff --git a/src/lib/elements/forms/inputPoint.svelte b/src/lib/elements/forms/inputPoint.svelte index 4c0b4ce40f..9a3f70183e 100644 --- a/src/lib/elements/forms/inputPoint.svelte +++ b/src/lib/elements/forms/inputPoint.svelte @@ -10,6 +10,7 @@ deletePoints?: boolean; onDeletePoint?: () => void; disableDelete?: boolean; + disabled?: boolean; onChangePoint: (index: number, newValue: number) => void; } @@ -21,7 +22,8 @@ deletePoints = false, disableDelete = false, onDeletePoint, - onChangePoint + onChangePoint, + disabled }: Props = $props(); @@ -38,6 +40,7 @@ placeholder="Enter value" step={0.0001} value={values[index]} + {disabled} on:change={(e) => onChangePoint(index, Number.parseFloat(`${e.detail}`))} /> {/each} {/if} @@ -45,7 +48,7 @@ diff --git a/src/lib/elements/forms/inputPolygon.svelte b/src/lib/elements/forms/inputPolygon.svelte index c2bcbb4db1..766dfa7d1b 100644 --- a/src/lib/elements/forms/inputPolygon.svelte +++ b/src/lib/elements/forms/inputPolygon.svelte @@ -17,6 +17,7 @@ coordIndex: number, newValue: number ) => void; + disabled?: boolean; }; let { @@ -26,7 +27,8 @@ onAddPoint, onAddLine, onDeletePoint, - onChangePoint + onChangePoint, + disabled }: Props = $props(); @@ -34,6 +36,7 @@ {#each values as value, index} onAddPoint(index)} {nullable} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/columns.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/columns.svelte new file mode 100644 index 0000000000..5673d8745e --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/columns.svelte @@ -0,0 +1,62 @@ + + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte index 09289f76f7..da45e31168 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte @@ -8,7 +8,8 @@ Spreadsheet, Typography, FloatingActionBar, - Popover + Popover, + Badge } from '@appwrite.io/pink-svelte'; import { IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport'; @@ -38,9 +39,16 @@ import Options from './options.svelte'; import { InputSelect, InputText } from '$lib/elements/forms'; import { isCloud, VARS } from '$lib/system'; + import { fade } from 'svelte/transition'; import IconAINotification from './icon/aiNotification.svelte'; + let { + userColumns = [] + }: { + userColumns?: Column[]; + } = $props(); + let resizeObserver: ResizeObserver; let spreadsheetContainer: HTMLElement; @@ -48,6 +56,7 @@ let headerElement: HTMLElement | null = null; let rangeOverlayEl: HTMLDivElement | null = null; let fadeBottomOverlayEl: HTMLDivElement | null = null; + let snowFadeBottomOverlayEl: HTMLDivElement | null = null; let customColumns = $state< (SuggestedColumnSchema & { elements?: []; isPlaceholder?: boolean })[] @@ -58,9 +67,25 @@ let scrollAnimationFrame: number | null = null; let creatingColumns = $state(false); + let selectedColumnId = $state(null); + let previousColumnId = $state(null); + let selectedColumnName = $state(null); + + let showHeadTooltip = $state(true); + let isInlineEditing = $state(false); + let tooltipTopPosition = $state(50); + let triggerColumnId = $state(null); + let hoveredColumnId = $state(null); + + // for deleting a column + undo + let undoTimer: ReturnType | null = $state(null); + let columnBeingDeleted: (SuggestedColumnSchema & { deletedIndex?: number }) | null = + $state(null); + const baseColProps = { draggable: false, resizable: false }; const NOTIFICATION_AND_MOCK_DELAY = 1250; + const COLUMN_DELETION_UNDO_TIMER_LIMIT = 10000; // 10 seconds const getColumnWidth = (columnKey: string) => Math.max(180, columnKey.length * 8 + 60); const safeNumericValue = (value: number | undefined) => @@ -85,7 +110,7 @@ const updateOverlayHeight = () => { if (!spreadsheetContainer) return; if (!headerElement || !headerElement.isConnected) { - headerElement = spreadsheetContainer.querySelector('[role="rowheader"]'); + headerElement = spreadsheetContainer?.querySelector('[role="rowheader"]'); } if (!headerElement) return; @@ -106,7 +131,7 @@ const updateOverlayBounds = () => { if (!spreadsheetContainer) return; if (!headerElement || !headerElement.isConnected) { - headerElement = spreadsheetContainer.querySelector('[role="rowheader"]'); + headerElement = spreadsheetContainer?.querySelector('[role="rowheader"]'); } if (!headerElement) return; @@ -136,7 +161,8 @@ const hasRealColumns = customColumns.some((col) => !col.isPlaceholder); if (!hasRealColumns) { - // For placeholders or no columns, position overlay to cover custom columns area + // for placeholders or no columns, + // position overlay to cover custom columns area const idCell = getById('$id'); const actionsCell = headerElement!.querySelector( '[role="cell"][data-column-id="actions"]' @@ -258,14 +284,51 @@ directAccessScroller.scrollTo({ left: Math.max(0, scrollLeft), - behavior: 'smooth' + behavior: 'instant' }); } }; + function updateColumnHighlight() { + const activeColumnId = selectedColumnId || hoveredColumnId; + if (!spreadsheetContainer || !activeColumnId) return; + + const headerCell = spreadsheetContainer.querySelector( + `[role="rowheader"] [role="cell"][data-column-id="${activeColumnId}"]` + ); + + if (!headerCell) return; + + // calculate position similar to columns-range-overlay logic + if (!headerElement || !headerElement.isConnected) { + headerElement = spreadsheetContainer.querySelector('[role="rowheader"]'); + } + + if (!headerElement) return; + + const containerRect = spreadsheetContainer.getBoundingClientRect(); + const cellRect = headerCell.getBoundingClientRect(); + + const left = Math.round(cellRect.left - containerRect.left); + const width = cellRect.width; + + const isHovered = !selectedColumnId && hoveredColumnId; + const isFirstColumn = activeColumnId === customColumns[0]?.key; + + let leftAdjustment = -2; + let widthAdjustment = 2; + if (isHovered && isFirstColumn) { + leftAdjustment = 0; + } + + spreadsheetContainer.style.setProperty('--highlight-left', `${left + leftAdjustment}px`); + spreadsheetContainer.style.setProperty('--highlight-width', `${width + widthAdjustment}px`); + } + const recalcAll = () => { updateOverlayHeight(); updateOverlayBounds(); + updateColumnHighlight(); }; /** @@ -276,6 +339,12 @@ scrollAnimationFrame = requestAnimationFrame(() => { recalcAll(); + + // check if selected column is still visible after scroll + if (selectedColumnId && !isColumnVisible(selectedColumnId)) { + resetSelectedColumn(); + } + scrollAnimationFrame = null; }); }; @@ -341,6 +410,7 @@ ...baseColProps }, ...finalCustomColumns, + /*...userColumns,*/ { id: 'actions', title: '', @@ -356,6 +426,8 @@ const emptyCells = $derived(($isSmallViewport ? 14 : 17) + (!$expandTabs ? 2 : 0)); onMount(async () => { + userColumns; /* silences lint check, variable not read */ + if (spreadsheetContainer) { resizeObserver = new ResizeObserver(recalcAll); resizeObserver.observe(spreadsheetContainer); @@ -374,11 +446,15 @@ // these are referenced in // `table-[table]/+page.svelte` $tableColumnSuggestions.table = null; + $tableColumnSuggestions.force = false; $tableColumnSuggestions.enabled = false; } $tableColumnSuggestions.context = null; $tableColumnSuggestions.thinking = false; + + // reset selection! + resetSelectedColumn(); } async function suggestColumns() { @@ -473,17 +549,22 @@ } } - function onPopoverShowStateChanged(value: boolean) { - showFloatingBar = !value; + async function updateOverlaysForMobile(value: boolean) { if ($isSmallViewport) { setTimeout(() => { - [rangeOverlayEl, fadeBottomOverlayEl].forEach((el) => { + [rangeOverlayEl, fadeBottomOverlayEl, snowFadeBottomOverlayEl].forEach((el) => { if (el) { el.style.opacity = value ? '0' : '1'; } }); }, 0); } + } + + function onPopoverShowStateChanged(value: boolean) { + showFloatingBar = !value; + showHeadTooltip = !value; + updateOverlaysForMobile(value); const currentScrollLeft = hScroller?.scrollLeft || 0; @@ -492,6 +573,9 @@ hScroller.scrollLeft = currentScrollLeft; } }); + + // reset selection! + resetSelectedColumn(); } function updateColumn(columnId: string, updates: Partial) { @@ -515,6 +599,174 @@ return !['$id', '$createdAt', '$updatedAt', 'actions'].includes(id); } + function resetSelectedColumn() { + selectedColumnId = null; + previousColumnId = null; + /*selectedColumnName = null;*/ + } + + // small decor, hides previous cell's right border visibility! + function handlePreviousColumnsBorder(columnId: string, hide: boolean = true) { + const allHeaders = Array.from( + spreadsheetContainer.querySelectorAll( + '[role="rowheader"] [role="cell"][data-column-id]' + ) + ); + + const selectedIndex = allHeaders.findIndex( + (cell) => cell.getAttribute('data-column-id') === columnId + ); + + if (selectedIndex > 0) { + const prevColumnId = allHeaders[selectedIndex - 1].getAttribute('data-column-id'); + if (prevColumnId) { + const previousCells = spreadsheetContainer.querySelectorAll( + `[role="rowheader"] [role="cell"][data-column-id="${prevColumnId}"]` + ); + + previousCells.forEach((cell) => { + if (hide) { + cell.classList.add('hide-border'); + } else { + cell.classList.remove('hide-border'); + } + }); + } + } + } + + function isColumnVisible(columnId: string) { + if (!spreadsheetContainer || !hScroller) return true; + + const columnCell = spreadsheetContainer.querySelector( + `[role="rowheader"] [role="cell"][data-column-id="${columnId}"]` + ); + + if (!columnCell) return false; + + const cellRect = columnCell.getBoundingClientRect(); + const scrollerRect = hScroller.getBoundingClientRect(); + + // stickies have 40px width + const STICKY_COLUMN_WIDTH = 40; + + // calculate available viewport bounds (excluding both 40px sticky columns) + const leftBound = scrollerRect.left + STICKY_COLUMN_WIDTH; // Selection column (40px) + const rightBound = scrollerRect.right - STICKY_COLUMN_WIDTH; // Actions column (40px) + + const safetyMargin = 2; + return ( + cellRect.left >= leftBound - safetyMargin && cellRect.right <= rightBound + safetyMargin + ); + } + + function scrollColumnIntoView(columnId: string) { + if (!spreadsheetContainer || !hScroller) return false; + + const columnCell = spreadsheetContainer.querySelector( + `[role="rowheader"] [role="cell"][data-column-id="${columnId}"]` + ); + + if (!columnCell) return false; + + const cellRect = columnCell.getBoundingClientRect(); + const scrollerRect = hScroller.getBoundingClientRect(); + + // calculate scroll needed to center the column in view + const scrollLeft = + hScroller.scrollLeft + + cellRect.left - + scrollerRect.left - + (scrollerRect.width - cellRect.width) / 2; + + hScroller.scrollTo({ + left: Math.max(0, scrollLeft), + behavior: 'smooth' + }); + + return true; + } + + function deleteColumn(columnId: string) { + if (!columnId) return; + + let columnIndex = -1; + let columnSchema: SuggestedColumnSchema = null; + + for (let index = 0; index < customColumns.length; index++) { + if (customColumns[index].key === columnId) { + columnIndex = index; + columnSchema = customColumns[index]; + break; + } + } + + if (columnIndex === -1 || !columnSchema) { + return; + } + + // remove the column + customColumns.splice(columnIndex, 1); + + // store column with its index for undo + columnBeingDeleted = { ...columnSchema, deletedIndex: columnIndex }; + + // clear any existing timer + if (undoTimer) { + clearTimeout(undoTimer); + } + + // start 10-second undo timer + undoTimer = setTimeout(() => { + undoTimer = null; + selectedColumnId = null; + columnBeingDeleted = null; + selectedColumnName = null; + }, COLUMN_DELETION_UNDO_TIMER_LIMIT); + + // reset selection! + resetSelectedColumn(); + + // see overlay is visible after deletion on mobile! + setTimeout(() => updateOverlaysForMobile(false), 150); + + // recalculate view after deletion + requestAnimationFrame(() => recalcAll()); + } + + function undoDelete() { + if (!columnBeingDeleted) return; + + const { deletedIndex, ...columnData } = columnBeingDeleted; + + // restore column at its original index + if (deletedIndex !== undefined && deletedIndex >= 0) { + customColumns.splice(deletedIndex, 0, columnData); + } else { + // fallback: add at the end if index is missing + customColumns.push(columnData); + } + + // clear undo state + columnBeingDeleted = null; + + // clear timer + if (undoTimer) { + clearTimeout(undoTimer); + undoTimer = null; + } + + // recalculate view after restore + requestAnimationFrame(() => { + recalcAll(); + + tick().then(() => { + selectedColumnId = columnData.key; + selectedColumnName = columnData.key; + }); + }); + } + function showIndexSuggestionsNotification() { // safeguard anyways! if (!isCloud) return; @@ -544,6 +796,16 @@ creatingColumns = true; const client = sdk.forProject(page.params.region, page.params.project); + const isAnyEmpty = customColumns.some((col) => !col.key); + if (isAnyEmpty) { + creatingColumns = false; + addNotification({ + type: 'warning', + message: 'Some columns have invalid keys' + }); + return; + } + try { const results = []; @@ -552,7 +814,8 @@ databaseId: page.params.database, tableId: page.params.table, key: column.key, - required: column.required || false + required: column.required || false, + encrypt: 'encrypt' in column ? column.encrypt : undefined }; let columnResult: Columns; @@ -650,6 +913,8 @@ timeout: NOTIFICATION_AND_MOCK_DELAY }); + resetSuggestionsStore(true); + // show index notification! showIndexSuggestionsNotification(); @@ -681,6 +946,121 @@ }; } + // scroll to view if needed and select! + function selectColumnWithId(column: Column) { + const columnId = column.id; + selectedColumnName = column.title; + if (!isColumnVisible(columnId)) { + scrollColumnIntoView(columnId); + setTimeout(() => (selectedColumnId = columnId), 300); + } else { + selectedColumnId = columnId; + } + + columnBeingDeleted = null; + } + + function fadeSlide(_: Node, { y = 8, duration = 200 } = {}) { + return { + duration, + css: (time: number) => ` + opacity: ${time}; + transform: translateY(${(1 - time) * y}px); + ` + }; + } + + function columnHoverMouseTracker(event: MouseEvent) { + if (hoveredColumnId && event.target instanceof Element) { + const hoveredButton = event.target.closest('[data-column-hover]'); + const currentColumnId = hoveredButton?.getAttribute('data-column-hover'); + + if (currentColumnId !== hoveredColumnId) { + hoveredColumnId = null; + } + } + } + + $effect(() => { + if (!spreadsheetContainer) return; + + // remove existing hide-border classes + const hiddenCells = spreadsheetContainer.querySelectorAll('[role="cell"].hide-border'); + hiddenCells.forEach((cell) => cell.classList.remove('hide-border')); + + if (!selectedColumnId) return; + + setTimeout(() => { + // hide borders for selected column and previous column + const selectedCells = spreadsheetContainer.querySelectorAll( + `[role="cell"][data-column-id="${selectedColumnId}"]` + ); + + selectedCells.forEach((cell) => cell.classList.add('hide-border')); + + // find and hide previous column's borders (which create the left edge of selected column) + const allHeaders = Array.from( + spreadsheetContainer.querySelectorAll( + '[role="rowheader"] [role="cell"][data-column-id]' + ) + ); + const selectedIndex = allHeaders.findIndex( + (cell) => cell.getAttribute('data-column-id') === selectedColumnId + ); + + if (selectedIndex > 0) { + const prevColumnId = allHeaders[selectedIndex - 1].getAttribute('data-column-id'); + if (prevColumnId) { + const previousCells = spreadsheetContainer.querySelectorAll( + `[role="cell"][data-column-id="${prevColumnId}"]` + ); + previousCells.forEach((cell) => cell.classList.add('hide-border')); + } + } + }, 300); + + // update position + updateColumnHighlight(); + + // track for next selection - + // but only if we had a `real` previous selection + if (previousColumnId !== null) { + previousColumnId = selectedColumnId; + } else { + // fresh after a deselect + // set it for future switches + setTimeout(() => (previousColumnId = selectedColumnId), 25); + } + }); + + $effect(() => { + if (!spreadsheetContainer) return; + + const allCells = spreadsheetContainer.querySelectorAll('[role="cell"]'); + allCells.forEach((cell) => { + const resizer = cell.querySelector('.column-resizer-disabled') as HTMLDivElement; + if (resizer) resizer.style.display = ''; + }); + + if (!hoveredColumnId) return; + + // auto-scroll if hovered column is out of bounds + /*if (!isColumnVisible(hoveredColumnId)) { + scrollColumnIntoView(hoveredColumnId); + }*/ + + const hoveredCells = spreadsheetContainer.querySelectorAll( + `[role="cell"][data-column-id="${hoveredColumnId}"]` + ); + + hoveredCells.forEach((cell) => { + const resizer = cell.querySelector('.column-resizer-disabled') as HTMLDivElement; + if (resizer) resizer.style.display = 'none'; + }); + + updateColumnHighlight(); + }); + onDestroy(() => { resizeObserver?.disconnect(); hScroller?.removeEventListener('scroll', recalcAllThrottled); @@ -696,12 +1076,14 @@
0} class:thinking={$tableColumnSuggestions.thinking} class="databases-spreadsheet spreadsheet-container-outer" style:--overlay-icon-color="#fd366e99" - style:--non-overlay-icon-color="--fgcolor-neutral-weak"> + style:--non-overlay-icon-color="--fgcolor-neutral-weak" + onmousemove={columnHoverMouseTracker}>
+ + + {#if selectedColumnId || hoveredColumnId} + {@const isHovered = !selectedColumnId && hoveredColumnId} +
+
+ + {@render customTooltip({ text: 'Click to select column', show: isHovered })} + {/if}
{}}> + bottomActionClick={() => {}} + let:root> {#each spreadsheetColumns as column, index (index)} {#if column.isAction} @@ -740,7 +1139,18 @@ + headerTooltipText={isColumnInteractable && showHeadTooltip + ? 'Right click for advanced editing' + : undefined} + onShowStateChanged={onPopoverShowStateChanged} + triggerOpen={() => { + if (triggerColumnId === column.id) { + triggerColumnId = null; + return true; + } + + return false; + }}> {#snippet children(toggle)} - -
- { - if ( - isColumnInteractable && - !$isTabletViewport - ) { - toggle(event); - } - }}> - {#if !columnObj?.isPlaceholder} - - {/if} - -
- -
- - - {#each basicColumnOptions as option} - { - toggle(); - updateColumn(column.id, { - type: option.type, - format: - option.format || null - }); - }}> - - - {option.name} - - - {/each} - - -
-
+ {@render changeColumnTypePopover({ + id: column.id, + columnObj, + iconColor: columnIconColor, + icon: column.icon, + isColumnInteractable, + index + })} {#if !$isTabletViewport} -
+
{ + isInlineEditing = true; + showHeadTooltip = false; + resetSelectedColumn(); + handlePreviousColumnsBorder(column.id); + }} + onfocusout={() => { + showHeadTooltip = true; + isInlineEditing = false; + handlePreviousColumnsBorder(column.id, false); + }}> {#if columnIcon} - + {@render changeColumnTypePopover({ + id: column.id, + columnObj, + iconColor: columnIconColor, + icon: column.icon, + isColumnInteractable, + index + })} {/if} @@ -899,15 +1271,68 @@ {#if ColumnComponent} - + {/if} {/if} {/snippet} + + {#snippet mobileFooterChildren(toggle)} + { + toggle(event); + deleteColumn(column.id); + }} + style="position: absolute; left: 1rem;" + >Delete + + {/snippet} {/if} {/each} + + {#each Array.from({ length: emptyCells }) as _} + + {#each spreadsheetColumns as column} + {@const columnObj = getColumn(column.id)} + {@const isColumnInteractable = + isCustomColumn(column.id) && !columnObj.isPlaceholder} + + + + {/each} + + {/each} @@ -917,6 +1342,12 @@ data-collapsed-tabs={!$expandTabs}>
+
+
+ {#if $tableColumnSuggestions.thinking}
@@ -942,10 +1373,82 @@
{:else if customColumns.some((col) => !col.isPlaceholder) && showFloatingBar} + + {@const isUndoDeleteMode = columnBeingDeleted && columnBeingDeleted?.key !== null} + {@const columnName = isUndoDeleteMode ? columnBeingDeleted?.key : selectedColumnName} + {@const hasSelection = selectedColumnId !== null || isUndoDeleteMode} + + {#if !creatingColumns} +
+ + + + + + + {#if isUndoDeleteMode} + was deleted. You can undo this action. + {:else} + is selected + {/if} + + + + + + + {#if !isUndoDeleteMode} + (selectedColumnId = null)}> + Cancel + + + + + {/if} + !col.isPlaceholder).length <= 1} + on:click={() => { + if (isUndoDeleteMode) { + undoDelete(); + } else { + deleteColumn(selectedColumnId); + } + }}> + {#if isUndoDeleteMode} + Undo + {:else} + Delete + {/if} + + + + +
+ {/if} + +
+ class:creating-columns={creatingColumns} + class:has-selection={hasSelection}> @@ -960,8 +1463,8 @@ {creatingColumns ? 'Creating columns' : $isSmallViewport - ? 'Review and edit suggested columns' - : 'Review and edit suggested columns before applying'} + ? 'Click headers or cells to edit columns' + : 'Click headers or cells to edit columns before applying'} @@ -994,13 +1497,108 @@ {/if}
+{#snippet customTooltip({ text, show })} +
+ + {text} + +
+{/snippet} + +{#snippet changeColumnTypePopover({ id, columnObj, iconColor, icon, isColumnInteractable, index })} + +
+ { + if (isColumnInteractable && !$isTabletViewport) { + toggle(event); + resetSelectedColumn(); + } + }}> + {#if !columnObj?.isPlaceholder} + + {/if} + +
+ +
+ + + {#each basicColumnOptions as option} + { + toggle(); + updateColumn(id, { + type: option.type, + format: option.format || null + }); + }}> + + + {option.name} + + + {/each} + + +
+
+{/snippet} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/aiForButton.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/aiForButton.svelte new file mode 100644 index 0000000000..df6de05161 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/aiForButton.svelte @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/indexes.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/indexes.svelte index 2f33a1af66..85158b884d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/indexes.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/indexes.svelte @@ -8,7 +8,7 @@ mockSuggestions, type SuggestedIndexSchema } from './store'; - import { Modal, Confirm } from '$lib/components'; + import { Modal } from '$lib/components'; import SideSheet from '../table-[table]/layout/sidesheet.svelte'; import { isSmallViewport } from '$lib/stores/viewport'; import { IndexType, type Models } from '@appwrite.io/console'; @@ -32,7 +32,6 @@ let creatingIndexes = $state(false); let loadingSuggestions = $state(false); let indexes = $state([]); - let confirmDismiss = $state(false); let columnOptions: Array<{ value: string; label: string; @@ -195,7 +194,6 @@ function dismissIndexes() { indexes = []; - confirmDismiss = false; $showIndexesSuggestions = false; } @@ -354,13 +352,7 @@ text size="s" disabled={loadingSuggestions || creatingIndexes} - on:click={() => { - if (indexes.length > 0 && !creatingIndexes) { - confirmDismiss = true; - } else { - $showIndexesSuggestions = false; - } - }}>Cancel + on:click={() => dismissIndexes()}>Cancel {:else} - +
+ + + + + {headerTooltipText} + + +
{/if}
@@ -56,6 +80,10 @@ showSheet = false; } }}> + {#snippet footer()} + {@render mobileFooterChildren?.(() => (showSheet = false))} + {/snippet} + {@render tooltipChildren(() => (showSheet = false))} {/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts index f1792e3911..39d76bb928 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts @@ -3,6 +3,7 @@ import { IndexType } from '@appwrite.io/console'; import { columnOptions } from '../table-[table]/columns/store'; export type TableColumnSuggestions = { + force: boolean; enabled: boolean; thinking: boolean; context?: string | undefined; @@ -23,6 +24,7 @@ export type SuggestedColumnSchema = { min?: number; max?: number; format?: string | null; + encrypt?: boolean | null; }; export enum IndexOrder { @@ -43,11 +45,14 @@ export const tableColumnSuggestions = writable({ enabled: false, context: null, thinking: false, - table: null + table: null, + force: false }); export const showIndexesSuggestions = writable(false); +export const showColumnsSuggestionsModal = writable(false); + export const mockSuggestions: { total: number; columns: ColumnInput[] } = { total: 7, columns: [ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte index 19aa513a5f..6800c12f28 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte @@ -35,7 +35,8 @@ spreadsheetRenderKey, expandTabs, databaseRelatedRowSheetOptions, - rowPermissionSheet + rowPermissionSheet, + type Columns } from './store'; import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter'; import CreateColumn from './createColumn.svelte'; @@ -65,7 +66,12 @@ import { Submit, trackEvent } from '$lib/actions/analytics'; import IndexesSuggestions from '../(suggestions)/indexes.svelte'; - import { showIndexesSuggestions, tableColumnSuggestions } from '../(suggestions)'; + import ColumnsSuggestions from '../(suggestions)/columns.svelte'; + import { + showColumnsSuggestionsModal, + showIndexesSuggestions, + tableColumnSuggestions + } from '../(suggestions)'; let editRow: EditRow; let editRelatedRow: EditRelatedRow; @@ -259,7 +265,7 @@ $spreadsheetLoading = true; $randomDataModalState.show = false; - let columns = $table.columns; + let columns = page.data.table.columns as Columns[]; const hasAnyRelationships = columns.some((column) => isRelationship(column)); const filteredColumns = columns.filter((col) => col.type !== 'relationship'); @@ -482,4 +488,6 @@ + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte index df77376faa..1c1c256f85 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte @@ -6,7 +6,7 @@ import { Container } from '$lib/layout'; import { preferences } from '$lib/stores/preferences'; import { canWriteTables, canWriteRows } from '$lib/stores/roles'; - import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte'; + import { Icon, Layout, Divider, Tooltip, Typography, Link } from '@appwrite.io/pink-svelte'; import type { PageData } from './$types'; import { table, @@ -26,13 +26,25 @@ import { addNotification } from '$lib/stores/notifications'; import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { isSmallViewport } from '$lib/stores/viewport'; - import { IconChevronDown, IconChevronUp, IconPlus } from '@appwrite.io/pink-icons-svelte'; + import { + IconBookOpen, + IconChevronDown, + IconChevronUp, + IconPlus, + IconViewBoards + } from '@appwrite.io/pink-icons-svelte'; import type { Models } from '@appwrite.io/console'; import EmptySheet from './layout/emptySheet.svelte'; import CreateRow from './rows/create.svelte'; import { onDestroy } from 'svelte'; import { isCloud } from '$lib/system'; - import { Empty as SuggestionsEmptySheet, tableColumnSuggestions } from '../(suggestions)'; + import { + Empty as SuggestionsEmptySheet, + tableColumnSuggestions, + showColumnsSuggestionsModal + } from '../(suggestions)'; + import EmptySheetCards from './layout/emptySheetCards.svelte'; + import IconAI from '../(suggestions)/icon/aiForButton.svelte'; export let data: PageData; @@ -147,36 +159,48 @@ Filters - - - {#if !$isSmallViewport} + + + {#if !$isSmallViewport} + - - {/if} + + {/if} + {#if $isSmallViewport} @@ -193,7 +217,7 @@
- {#if hasColumns && hasValidColumns} + {#if hasColumns && hasValidColumns && $tableColumnSuggestions.force !== true} {#if data.rows.total} @@ -201,58 +225,102 @@ { + customColumns={createTableColumns($table.columns, selected)}> + {#snippet actions()} + + {/snippet} + {:else} { + customColumns={createTableColumns($table.columns, selected)}> + {#snippet actions()} + { $showRowCreateSheet.show = true; - } - }, - random: { - onClick: () => { + }} /> + + { $randomDataModalState.show = true; - } - } - }} /> + }} /> + {/snippet} + {/if} {:else if isCloud && canShowSuggestionsSheet} - + {:else} - { + + {#snippet subtitle()} + {#if !isCloud} + + + Need a hand? Learn more in the + + docs. + + + {/if} + {/snippet} + + {#snippet actions()} + {#if isCloud} + + { + $showColumnsSuggestionsModal = true; + }} /> + {/if} + + { $showCreateColumnSheet.show = true; - } - }, - random: { - onClick: () => { + }} /> + + { $randomDataModalState.show = true; - } - } - }} /> + }} /> + + {#if isCloud} + + + {/if} + {/snippet} + {/if}
{/key} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte index 5ee690f499..7586ab311f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte @@ -368,8 +368,9 @@ {column.key}{column.array ? '[]' : undefined} {/if} + {#if isString(column) && column.encrypt} - + Encrypted
{/if} - - + {#if column.status !== 'available'} = { required: false, array: false, @@ -67,6 +68,7 @@ array: false, ...data }); + $: listen(data); $: handleDefaultState($required || $array); @@ -76,24 +78,26 @@ id="default" label="Default value" placeholder="Select a value" - disabled={data.required || data.array} + disabled={data.required || data.array || disabled} options={[ { label: 'NULL', value: null }, { label: 'True', value: true }, { label: 'False', value: false } ]} bind:value={data.default} /> + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/datetime.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/datetime.svelte index 827d77798b..5ef5afca14 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/datetime.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/datetime.svelte @@ -42,13 +42,13 @@
+ class:cursor-not-allowed={editing || disabled} + class:disabled-checkbox={!supportsStringEncryption || editing || disabled}> + disabled={!supportsStringEncryption || editing || disabled} />