diff --git a/package.json b/package.json index eebc54597..496818696 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74", + "@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@077179c", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877", "@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 3a4161ed8..827befa27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,20 +12,20 @@ importers: specifier: ^1.1.24 version: 1.1.24(svelte@5.25.3)(zod@3.24.3) '@appwrite.io/console': - specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74 - version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74 + specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39 + version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39 '@appwrite.io/pink-icons': specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877(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@077179c - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 @@ -260,8 +260,8 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74': - resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39': + resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39} version: 1.10.0 '@appwrite.io/pink-icons-svelte@2.0.0-RC.1': @@ -269,8 +269,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877} 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@077179c': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -3703,13 +3703,13 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74': {} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@636ed39': {} '@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)': dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877(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@077179c(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877(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/alerts/emailVerificationBanner.svelte b/src/lib/components/alerts/emailVerificationBanner.svelte index 96574d801..76f622d64 100644 --- a/src/lib/components/alerts/emailVerificationBanner.svelte +++ b/src/lib/components/alerts/emailVerificationBanner.svelte @@ -4,13 +4,13 @@ import { Typography } from '@appwrite.io/pink-svelte'; import { user } from '$lib/stores/user'; import SendVerificationEmailModal from '../account/sendVerificationEmailModal.svelte'; - import { page } from '$app/stores'; + import { page } from '$app/state'; import { wizard, isNewWizardStatusOpen } from '$lib/stores/wizard'; - import { isCloud, VARS } from '$lib/system'; + import { isCloud, isProductionCloud, VARS } from '$lib/system'; const hasUser = $derived(!!$user); const needsEmailVerification = $derived(hasUser && !$user.emailVerification); - const notOnOnboarding = $derived(!$page.route.id.includes('/onboarding')); + const notOnOnboarding = $derived(!page.route.id.includes('/onboarding')); const notOnWizard = $derived(!$wizard.show && !$isNewWizardStatusOpen); const isEnabledViaEnvConfig = $derived(VARS.EMAIL_VERIFICATION); const shouldShowEmailBanner = $derived( @@ -23,9 +23,10 @@ ); let showSendVerification = $state(false); + let isProduction = $derived(isProductionCloud(page.url)); -{#if shouldShowEmailBanner} +{#if shouldShowEmailBanner && isProduction} To avoid losing access to your projects, make sure { return { label: button.name, - onClick: button.method + onClick: button.method, + isHtml: button.isHtml }; })} on:dismiss={() => dismissNotification(notification.id)} /> diff --git a/src/lib/stores/notifications.ts b/src/lib/stores/notifications.ts index 97f6cfb36..c250820f7 100644 --- a/src/lib/stores/notifications.ts +++ b/src/lib/stores/notifications.ts @@ -1,13 +1,14 @@ import { writable } from 'svelte/store'; +import type { ComponentType } from 'svelte'; export type Notification = { id: number; - type: 'success' | 'error' | 'info' | 'warning'; + type?: 'success' | 'error' | 'info' | 'warning'; dismissible?: boolean; timeout?: number; message: string; title?: string; - icon?: string; + icon?: ComponentType; buttons?: Buttons[]; isHtml?: boolean; }; @@ -15,32 +16,52 @@ export type Notification = { export type Buttons = { method: () => void | Promise; name: string; + isHtml?: boolean; }; let counter = 0; +const activeTimeouts = new Map(); + export const notifications = writable([]); export const dismissNotification = (id: number) => { + const timeoutId = activeTimeouts.get(id); + + if (timeoutId) { + clearTimeout(timeoutId); + activeTimeouts.delete(id); + } + notifications.update((all) => all.filter((t) => t.id !== id)); }; export const dismissAllNotifications = () => { + activeTimeouts.forEach((timeoutId) => clearTimeout(timeoutId)); + activeTimeouts.clear(); + notifications.set([]); }; -export const addNotification = (notification: Omit) => { - const defaults = { +export const addNotification = (notification: Omit): number => { + const defaults: Notification = { id: counter++, type: 'info', dismissible: true, - timeout: 6000 + timeout: 6000, + ...notification }; - const n = { ...defaults, ...notification }; - notifications.update((all) => { - return [n, ...all.slice(0, 4)]; - }); + notifications.update((all) => [defaults, ...all.slice(0, 4)]); - if (n.timeout) setTimeout(() => dismissNotification(n.id), n.timeout); + if (defaults.timeout) { + const timeoutId = setTimeout(() => { + activeTimeouts.delete(defaults.id); + dismissNotification(defaults.id); + }, defaults.timeout); + + activeTimeouts.set(defaults.id, timeoutId); + } + + return defaults.id; }; 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 38ee34a9a..f944bb427 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 @@ -10,7 +10,7 @@ FloatingActionBar, Popover } from '@appwrite.io/pink-svelte'; - import { IconCalendar, IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte'; + import { IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport'; import type { Column } from '$lib/helpers/types'; import { expandTabs } from '../table-[table]/store'; @@ -25,9 +25,10 @@ tableColumnSuggestions, basicColumnOptions, mockSuggestions, - createTableRequest + createTableRequest, + showIndexesSuggestions } from './store'; - import { addNotification } from '$lib/stores/notifications'; + import { addNotification, dismissNotification } from '$lib/stores/notifications'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { sleep } from '$lib/helpers/promises'; import { invalidate, beforeNavigate, goto } from '$app/navigation'; @@ -41,7 +42,9 @@ import Options from './options.svelte'; import { InputSelect, InputText } from '$lib/elements/forms'; import { Confirm } from '$lib/components'; - import { VARS } from '$lib/system'; + import { isCloud, VARS } from '$lib/system'; + + import IconAINotification from './icon/aiNotification.svelte'; let resizeObserver: ResizeObserver; let spreadsheetContainer: HTMLElement; @@ -51,7 +54,10 @@ let rangeOverlayEl: HTMLDivElement | null = null; let fadeBottomOverlayEl: HTMLDivElement | null = null; - let customColumns = $state([]); + let customColumns = $state< + (SuggestedColumnSchema & { elements?: []; isPlaceholder?: boolean })[] + >(Array.from({ length: 7 }, (_, index) => createPlaceholderColumn(index))); + let showFloatingBar = $state(true); let hasTransitioned = $state(false); let scrollAnimationFrame: number | null = null; @@ -63,6 +69,8 @@ let creatingColumns = $state(false); const baseColProps = { draggable: false, resizable: false }; + const NOTIFICATION_AND_MOCK_DELAY = 1250; + const getColumnWidth = (columnKey: string) => Math.max(180, columnKey.length * 8 + 60); const safeNumericValue = (value: number | undefined) => value !== undefined && isWithinSafeRange(value) ? value : undefined; @@ -128,12 +136,6 @@ rangeOverlayEl.style.display = 'block'; } - if (customColumns.length === 0) { - spreadsheetContainer.style.setProperty('--group-left', '40px'); - spreadsheetContainer.style.setProperty('--group-width', '100%'); - return; - } - // Custom columns mode: calculate precise overlay bounds const containerRect = spreadsheetContainer.getBoundingClientRect(); const getById = (id: string) => @@ -141,6 +143,33 @@ `[role="cell"][data-header="true"][data-column-id="${id}"]` ); + const hasRealColumns = customColumns.some((col) => !col.isPlaceholder); + if (!hasRealColumns) { + // 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"]' + ); + + if (idCell && actionsCell) { + const idRect = idCell.getBoundingClientRect(); + const actionsRect = actionsCell.getBoundingClientRect(); + const left = Math.round(idRect.right - containerRect.left); + const actionsLeft = actionsRect.left - containerRect.left; + const width = Math.min( + customColumns.length * 180, // estimated minimum placeholder width + actionsLeft - left // don't exceed actions column + ); + + spreadsheetContainer.style.setProperty('--group-left', `${left - 2}px`); + spreadsheetContainer.style.setProperty('--group-width', `${width + 2}px`); + } else { + spreadsheetContainer.style.setProperty('--group-left', '40px'); + spreadsheetContainer.style.setProperty('--group-width', 'calc(100% - 80px)'); + } + return; + } + // Calculate visible viewport bounds const scrollerRect = hScroller ? hScroller.getBoundingClientRect() : containerRect; const visibleRight = scrollerRect.right; @@ -219,7 +248,7 @@ // only for mobile, we can remove if not needed! const scrollToFirstCustomColumn = () => { - if (!$isSmallViewport || customColumns.length === 0) return; + if (!$isSmallViewport) return; if (!headerElement || !headerElement.isConnected) { headerElement = spreadsheetContainer.querySelector('[role="rowheader"]'); @@ -231,12 +260,19 @@ `[role="cell"][data-header="true"][data-column-id="${customColumns[0]?.key}"]` ); - if (firstCustomColumnCell && hScroller) { - const cellRect = firstCustomColumnCell.getBoundingClientRect(); - const scrollerRect = hScroller.getBoundingClientRect(); - const scrollLeft = hScroller.scrollLeft + cellRect.left - scrollerRect.left - 40; + const directAccessScroller = + hScroller ?? + findHorizontalScroller(headerElement) ?? + // internal spreadsheet root main container! + spreadsheetContainer.querySelector('.spreadsheet-container'); - hScroller.scrollTo({ + if (firstCustomColumnCell && directAccessScroller) { + const cellRect = firstCustomColumnCell.getBoundingClientRect(); + const scrollerRect = directAccessScroller.getBoundingClientRect(); + const scrollLeft = + directAccessScroller.scrollLeft + cellRect.left - scrollerRect.left - 40; + + directAccessScroller.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' }); @@ -263,7 +299,8 @@ // Handle create table requests from subNavigation const unsubscribeCreateTable = createTableRequest.subscribe((requested) => { if (requested) { - if (customColumns.length > 0 && !creatingColumns) { + const hasRealColumns = customColumns.some((col) => !col.isPlaceholder); + if (hasRealColumns && !creatingColumns) { confirmNavigation = true; pendingNavigationUrl = 'create-table'; } else { @@ -311,26 +348,6 @@ ...baseColProps }, ...customSuggestedColumns, - ...(customColumns.length === 0 - ? [ - { - id: '$createdAt', - title: '$createdAt', - type: 'datetime' as Column['type'], - width: 180, - icon: IconCalendar, - ...baseColProps - }, - { - id: '$updatedAt', - title: '$updatedAt', - type: 'datetime' as Column['type'], - width: 180, - icon: IconCalendar, - ...baseColProps - } - ] - : []), { id: 'actions', title: '', @@ -343,7 +360,8 @@ // Handle browser back/forward navigation const handleBeforeUnload = (event: BeforeUnloadEvent) => { - if (customColumns.length > 0 && !creatingColumns) { + const hasRealColumns = customColumns.some((col) => !col.isPlaceholder); + if (hasRealColumns && !creatingColumns) { event.preventDefault(); event.returnValue = 'You have unsaved column suggestions. Are you sure you want to leave?'; @@ -365,7 +383,8 @@ }); beforeNavigate(({ cancel, to }) => { - if (customColumns.length > 0 && !creatingColumns) { + const hasRealColumns = customColumns.some((col) => !col.isPlaceholder); + if (hasRealColumns && !creatingColumns) { cancel(); confirmNavigation = true; $navigationCancelled = true; @@ -391,6 +410,12 @@ async function suggestColumns() { $tableColumnSuggestions.thinking = true; + + if ($isSmallViewport) { + await tick(); + scrollToFirstCustomColumn(); + } + let suggestedColumns: { total: number; columns: ColumnInput[]; @@ -402,7 +427,7 @@ try { if (VARS.MOCK_AI_SUGGESTIONS) { /* animation */ - await sleep(1250); + await sleep(NOTIFICATION_AND_MOCK_DELAY); suggestedColumns = mockSuggestions; } else { suggestedColumns = (await sdk @@ -410,7 +435,8 @@ .console.suggestColumns({ databaseId: page.params.database, tableId: page.params.table, - context: $tableColumnSuggestions.context ?? undefined + context: $tableColumnSuggestions.context ?? undefined, + min: 6 // TODO: to not break the sheet layout's width! })) as unknown as { total: number; columns: ColumnInput[]; @@ -423,21 +449,42 @@ total: suggestedColumns.total }); - customColumns = mapSuggestedColumns(suggestedColumns.columns); + const mappedColumns = mapSuggestedColumns(suggestedColumns.columns); - if (customColumns.length > 0) { - setTimeout(scrollToFirstCustomColumn, 100); - setTimeout(() => (hasTransitioned = true), 300); - } - } catch (error) { - addNotification({ - type: 'error', - message: error.message + mappedColumns.forEach((column, index) => { + if (index < customColumns.length) { + setTimeout(() => { + customColumns[index] = { ...column, isPlaceholder: false }; + + // recalculate overlay bounds + // after each column is populated! + requestAnimationFrame(() => updateOverlayBounds()); + }, index * 150); + } }); - trackError(error, Submit.ColumnSuggestions); - } finally { + if (mappedColumns.length > 0) { + setTimeout( + () => { + hasTransitioned = true; + // final recal after + // all animations complete + requestAnimationFrame(() => { + recalcAll(); + }); + }, + mappedColumns.length * 150 + 300 + ); + } + resetSuggestionsStore(false); + } catch (error) { + // remove completely! + resetSuggestionsStore(); + + // track & notify! + trackError(error, Submit.ColumnSuggestions); + addNotification({ type: 'error', message: error.message }); } } @@ -483,6 +530,31 @@ return !['$id', '$createdAt', '$updatedAt', 'actions'].includes(id); } + function showIndexSuggestionsNotification() { + // safeguard anyways! + if (!isCloud) return; + + setTimeout(() => { + const notifId = addNotification({ + isHtml: true, + title: 'Next step: Add indexes', + message: 'See suggested indexes based on your columns', + dismissible: true, + icon: IconAINotification, + timeout: 10000, // ten seconds + buttons: [ + { + name: 'Create indexes', + method: () => { + dismissNotification(notifId); + showIndexesSuggestions.update(() => true); + } + } + ] + }); + }, NOTIFICATION_AND_MOCK_DELAY); + } + async function createColumns() { creatingColumns = true; const client = sdk.forProject(page.params.region, page.params.project); @@ -589,9 +661,13 @@ addNotification({ type: 'success', - message: 'Columns created successfully' + message: 'Columns created successfully', + timeout: NOTIFICATION_AND_MOCK_DELAY }); + // show index notification! + showIndexSuggestionsNotification(); + trackEvent(Submit.ColumnCreate, { type: 'suggestions' }); } catch (error) { trackError(error, Submit.ColumnCreate); @@ -603,6 +679,23 @@ } } + function createPlaceholderColumn( + index: number + ): SuggestedColumnSchema & { elements?: []; isPlaceholder?: boolean } { + return { + key: `column${index + 1}`, + type: 'string', + required: false, + default: null, + format: null, + size: undefined, + min: undefined, + max: undefined, + elements: undefined, + isPlaceholder: true + }; + } + onDestroy(() => { resizeObserver?.disconnect(); hScroller?.removeEventListener('scroll', recalcAllThrottled); @@ -621,6 +714,7 @@
0} + class:thinking={$tableColumnSuggestions.thinking} class="databases-spreadsheet spreadsheet-container-outer" style:--overlay-icon-color="#fd366e99" style:--non-overlay-icon-color="--fgcolor-neutral-weak"> @@ -643,7 +737,6 @@ bottomActionClick={() => {}}> {#each spreadsheetColumns as column, index (index)} - {@const isColumnInteractable = isCustomColumn(column.id)} {#if column.isAction} @@ -658,6 +751,8 @@ {@const columnIconColor = !columnObj?.type ? '--non-overlay-icon-color' : '--overlay-icon-color'} + {@const isColumnInteractable = + isCustomColumn(column.id) && !columnObj.isPlaceholder} - {column.title} + + {column.title} + - { - if ( - isColumnInteractable && - !$isTabletViewport - ) { - toggle(event); - } - }}> - - +
+ { + if ( + isColumnInteractable && + !$isTabletViewport + ) { + toggle(event); + } + }}> + {#if !columnObj?.isPlaceholder} + + {/if} + +
- + Thinking of column suggestions - + @@ -847,7 +957,7 @@
- {:else if customColumns.length > 0 && showFloatingBar} + {:else if customColumns.some((col) => !col.isPlaceholder) && showFloatingBar} - Enable AI to suggest useful columns based on your table name + {subtitle} - {#if $tableColumnSuggestions.enabled} + {#if !featureActive} + + + + {/if} + + + {#if $tableColumnSuggestions.enabled && featureActive}
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 e0432b6f2..b8c022bc4 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 @@ -1,4 +1,5 @@ import { writable } from 'svelte/store'; +import { IndexType } from '@appwrite.io/console'; import { columnOptions } from '../table-[table]/columns/store'; export type TableColumnSuggestions = { @@ -24,6 +25,20 @@ export type SuggestedColumnSchema = { format?: string | null; }; +export enum IndexOrder { + ASC = 'ASC', + DESC = 'DESC', + NONE = null +} + +export type SuggestedIndexSchema = { + key: string; + type: IndexType; + orders: IndexOrder; + columns: string[]; + lengths?: number[] | undefined; +}; + export const tableColumnSuggestions = writable({ enabled: false, context: null, @@ -33,6 +48,8 @@ export const tableColumnSuggestions = writable({ export const createTableRequest = writable(false); +export const showIndexesSuggestions = writable(false); + export const mockSuggestions: { total: number; columns: ColumnInput[] } = { total: 7, columns: [ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte index 9fb56506a..6c6a8de18 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte @@ -23,6 +23,7 @@ import { trackEvent } from '$lib/actions/analytics'; import { Layout, Typography } from '@appwrite.io/pink-svelte'; import { page } from '$app/state'; + import IconQuestionMarkCircle from './components/questionIcon.svelte'; let policyCreateError: string; let totalPolicies: UserBackupPolicy[] = []; @@ -40,7 +41,7 @@ if (parsedCounter === showOnCount || !counter) { addNotification({ type: 'info', - icon: 'question-mark-circle', + icon: IconQuestionMarkCircle, message: 'How was your experience with our new Backups feature? Give us your feedback and help us improve!', timeout: 15000, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/components/questionIcon.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/components/questionIcon.svelte new file mode 100644 index 000000000..98e32e2bd --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/components/questionIcon.svelte @@ -0,0 +1,6 @@ + + + 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 fcc995b71..6e37e782f 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 @@ -64,6 +64,9 @@ import { chunks } from '$lib/helpers/array'; import { Submit, trackEvent } from '$lib/actions/analytics'; + import IndexesSuggestions from '../(suggestions)/indexes.svelte'; + import { showIndexesSuggestions, tableColumnSuggestions } from '../(suggestions)'; + let editRow: EditRow; let editRelatedRow: EditRelatedRow; let editRowPermissions: EditRowPermissions; @@ -89,7 +92,15 @@ response.events.includes('databases.*.tables.*.columns.*') || response.events.includes('databases.*.tables.*.indexes.*') ) { - if (!isWaterfallFromFaker) { + // don't invalidate when - + // 1. from faker + // 2. ai columns creation + // 3. ai indexes creation + if ( + !isWaterfallFromFaker && + !$showIndexesSuggestions && + !$tableColumnSuggestions.table + ) { invalidate(Dependencies.TABLE); } } @@ -470,3 +481,5 @@ + + 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 b6d825c4f..4889cfe4d 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 @@ -31,6 +31,7 @@ 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)'; export let data: PageData; @@ -75,6 +76,12 @@ $: hasColumns = !!$table.columns.length; $: hasValidColumns = $table?.columns?.some((col) => col.status === 'available'); + $: canShowSuggestionsSheet = + // enabled, has table details + // and it matches current table + $tableColumnSuggestions.enabled && + $tableColumnSuggestions.table && + $tableColumnSuggestions.table.id === page.params.table; async function onSelect(file: Models.File, localFile = false) { $isCsvImportInProgress = true; @@ -216,7 +223,7 @@ } }} /> {/if} - {:else if $tableColumnSuggestions.enabled && $tableColumnSuggestions.table && $tableColumnSuggestions.table.id === page.params.table} + {:else if isCloud && canShowSuggestionsSheet} {:else} { const baseAttrs = [ @@ -182,6 +183,10 @@ return `Type: ${formattedType}`; } + function isSystemColumnKey(column: Columns) { + return column.key.startsWith('$'); + } + onDestroy(() => ($showCreateColumnSheet.show = false)); $effect(() => { @@ -226,9 +231,9 @@ bind:selectedRows={selectedColumns} columns={[ // more size until we decide if we want a new column! - { id: 'key', width: { min: $isSmallViewport ? 250 : 200 } }, - { id: 'indexed', width: { min: 150 } }, - { id: 'default', width: { min: 200 } }, + { id: 'key', width: { min: 300 }, resizable: false }, + { id: 'indexed', width: { min: 150 }, resizable: false }, + { id: 'default', width: { min: 200 }, resizable: false }, { id: 'actions', width: 40, isAction: true } ]} bottomActionClick={() => ($showCreateColumnSheet.show = true)}> @@ -269,13 +274,17 @@ direction="row" alignItems="center" gap="xxs"> - - {#if column.key === '$id' || column.key === '$sequence' || column.key === '$createdAt' || column.key === '$updatedAt'} - {column['name']} + + {#if isSystemColumnKey(column)} + {column.key} {:else} - {column.key} {column.array ? '[]' : undefined} + {@const key = !column.required + ? column.key + : formatName(column.key, 6)} + {key} + {column.array ? '[]' : undefined} {/if} - + {#if isString(column) && column.encrypt} ({ value: column.key, label: column.key })) + .map((column) => ({ + value: column.key, + label: column.key, + leadingIcon: baseColumnOptions.find((option) => option.type === column.type)?.icon + })) ); let columnList = $state([{ value: '', order: '', length: null }]); @@ -154,7 +159,7 @@ ] : undefined }); - trackEvent(Submit.IndexCreate); + trackEvent(Submit.IndexCreate, { type: 'manual' }); showCreateIndex = false; } catch (err) { addNotification({ @@ -196,9 +201,17 @@ ...(selectedType === IndexType.Spatial ? [] : [ - { value: '$id', label: '$id' }, - { value: '$createdAt', label: '$createdAt' }, - { value: '$updatedAt', label: '$updatedAt' } + { value: '$id', label: '$id', leadingIcon: IconFingerPrint }, + { + value: '$createdAt', + label: '$createdAt', + leadingIcon: IconCalendar + }, + { + value: '$updatedAt', + label: '$updatedAt', + leadingIcon: IconCalendar + } ]), ...columnOptions ]} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/deleteIndex.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/deleteIndex.svelte index 956bb1b6a..5916d398a 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/deleteIndex.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/deleteIndex.svelte @@ -34,8 +34,6 @@ }) ) ); - await invalidate(Dependencies.TABLE); - showDelete = false; addNotification({ type: 'success', message: @@ -43,7 +41,13 @@ ? 'Index has been deleted' : `${selectedKeys.length} indexes have been deleted` }); + trackEvent(Submit.IndexDelete); + + await invalidate(Dependencies.TABLE); + + showDelete = false; + selectedIndex = Array.isArray(selectedIndex) ? [] : null; } catch (e) { error = e.message; trackError(e, Submit.IndexDelete); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte index 68b981d3a..01d1782fd 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte @@ -5,16 +5,19 @@ import { Button, Form } from '$lib/elements/forms'; import { isTabletViewport } from '$lib/stores/viewport'; import { Badge, Divider, Layout, Sheet, Tag, Typography } from '@appwrite.io/pink-svelte'; + import type { HTMLAttributes } from 'svelte/elements'; let { show = $bindable(false), title, closeOnBlur = false, submit, + cancel, children = null, footer = null, titleBadge = null, - topAction = null + topAction = null, + ...restProps }: { show: boolean; title: string; @@ -36,9 +39,16 @@ onClick?: () => boolean | void | Promise; } | undefined; + cancel?: + | { + text?: string; + disabled?: boolean; + onClick?: () => void; + } + | undefined; children?: Snippet; footer?: Snippet | null; - } = $props(); + } & HTMLAttributes = $props(); let form: Form; let submitting = $state(writable(false)); @@ -46,7 +56,7 @@ let copyText = $state(undefined); -
+
@@ -110,8 +120,19 @@ {#if footer} {@render footer?.()} {/if} - + + +