mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #2407 from appwrite/index-suggestions
Feat: AI Index Suggestions
This commit is contained in:
+3
-3
@@ -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",
|
||||
|
||||
Generated
+15
-15
@@ -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
|
||||
|
||||
@@ -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));
|
||||
</script>
|
||||
|
||||
{#if shouldShowEmailBanner}
|
||||
{#if shouldShowEmailBanner && isProduction}
|
||||
<HeaderAlert type="warning" title="Your email address needs to be verified">
|
||||
<svelte:fragment>
|
||||
To avoid losing access to your projects, make sure <Typography.Text
|
||||
|
||||
@@ -198,15 +198,21 @@ function generateSingleValue(
|
||||
|
||||
case 'integer': {
|
||||
const intAttr = column as Models.ColumnInteger;
|
||||
const min = !isWithinSafeRange(intAttr.min) ? 0 : intAttr.min;
|
||||
const max = !isWithinSafeRange(intAttr.max) ? 100 : intAttr.max;
|
||||
const min = isWithinSafeRange(intAttr.min) ? intAttr.min : 0;
|
||||
const fallbackMax = Math.max(min + 100, 100);
|
||||
const max = isWithinSafeRange(intAttr.max)
|
||||
? intAttr.max
|
||||
: Math.min(fallbackMax, Number.MAX_SAFE_INTEGER);
|
||||
return faker.number.int({ min, max });
|
||||
}
|
||||
|
||||
case 'double': {
|
||||
const floatAttr = column as Models.ColumnFloat;
|
||||
const min = !isWithinSafeRange(floatAttr.min) ? 0 : floatAttr.min;
|
||||
const max = !isWithinSafeRange(floatAttr.max) ? 100 : floatAttr.max;
|
||||
const min = isWithinSafeRange(floatAttr.min) ? floatAttr.min : 0;
|
||||
const fallbackMax = Math.max(min + 100, 100);
|
||||
const max = isWithinSafeRange(floatAttr.max)
|
||||
? floatAttr.max
|
||||
: Math.min(fallbackMax, Number.MAX_SAFE_INTEGER);
|
||||
const precision = 4;
|
||||
|
||||
return faker.number.float({ min, max, fractionDigits: precision });
|
||||
|
||||
@@ -181,6 +181,11 @@
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& :global(i) {
|
||||
// because the `IconCloud` shows above floating action bars.
|
||||
position: unset;
|
||||
}
|
||||
}
|
||||
|
||||
:global(main:has(.sub-navigation)) footer {
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
{#each $notifications as notification (notification.id)}
|
||||
<span animate:flip={{ duration: 500 }} transition:fly|global={{ x: 50 }}>
|
||||
<Toast
|
||||
isHtml={notification.isHtml}
|
||||
title={notification.title}
|
||||
status={notification.type}
|
||||
icon={notification.icon}
|
||||
description={notification.message}
|
||||
actions={notification.buttons?.map((button) => {
|
||||
return {
|
||||
label: button.name,
|
||||
onClick: button.method
|
||||
onClick: button.method,
|
||||
isHtml: button.isHtml
|
||||
};
|
||||
})}
|
||||
on:dismiss={() => dismissNotification(notification.id)} />
|
||||
|
||||
@@ -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<void>;
|
||||
name: string;
|
||||
isHtml?: boolean;
|
||||
};
|
||||
|
||||
let counter = 0;
|
||||
|
||||
const activeTimeouts = new Map<number, NodeJS.Timeout>();
|
||||
|
||||
export const notifications = writable<Notification[]>([]);
|
||||
|
||||
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<Notification, 'id'>) => {
|
||||
const defaults = {
|
||||
export const addNotification = (notification: Omit<Notification, 'id'>): 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;
|
||||
};
|
||||
|
||||
+222
-78
@@ -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<HTMLElement>(
|
||||
'[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: '<b>Next step: Add indexes</b>',
|
||||
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 @@
|
||||
<div
|
||||
bind:this={spreadsheetContainer}
|
||||
class:custom-columns={customColumns.length > 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={() => {}}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
{#each spreadsheetColumns as column, index (index)}
|
||||
{@const isColumnInteractable = isCustomColumn(column.id)}
|
||||
{#if column.isAction}
|
||||
<Spreadsheet.Header.Cell column="actions" {root}>
|
||||
<Button.Button icon variant="extra-compact">
|
||||
@@ -658,6 +751,8 @@
|
||||
{@const columnIconColor = !columnObj?.type
|
||||
? '--non-overlay-icon-color'
|
||||
: '--overlay-icon-color'}
|
||||
{@const isColumnInteractable =
|
||||
isCustomColumn(column.id) && !columnObj.isPlaceholder}
|
||||
|
||||
<Options
|
||||
enabled={isColumnInteractable}
|
||||
@@ -680,30 +775,42 @@
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
{column.title}
|
||||
<span
|
||||
class="column-title"
|
||||
class:animate-in={!columnObj?.isPlaceholder}
|
||||
style:--animation-delay={`${isColumnInteractable ? (index - 1) * 100 : 0}ms`}>
|
||||
{column.title}
|
||||
</span>
|
||||
|
||||
<Popover
|
||||
let:toggle
|
||||
portal
|
||||
padding="none"
|
||||
placement="bottom-start">
|
||||
<Button.Button
|
||||
size="xs"
|
||||
variant="extra-compact"
|
||||
disabled={!isColumnInteractable}
|
||||
on:click={(event) => {
|
||||
if (
|
||||
isColumnInteractable &&
|
||||
!$isTabletViewport
|
||||
) {
|
||||
toggle(event);
|
||||
}
|
||||
}}>
|
||||
<Icon
|
||||
size="s"
|
||||
color={columnIconColor}
|
||||
icon={column.icon ?? undefined} />
|
||||
</Button.Button>
|
||||
<div
|
||||
class="column-icon-wrapper"
|
||||
class:animate-in={!columnObj?.isPlaceholder}
|
||||
style:--animation-delay={`${isColumnInteractable ? (index - 1) * 100 : 0}ms`}>
|
||||
<Button.Button
|
||||
size="xs"
|
||||
variant="extra-compact"
|
||||
disabled={!isColumnInteractable}
|
||||
on:click={(event) => {
|
||||
if (
|
||||
isColumnInteractable &&
|
||||
!$isTabletViewport
|
||||
) {
|
||||
toggle(event);
|
||||
}
|
||||
}}>
|
||||
{#if !columnObj?.isPlaceholder}
|
||||
<Icon
|
||||
size="s"
|
||||
color={columnIconColor}
|
||||
icon={column.icon ?? undefined} />
|
||||
{/if}
|
||||
</Button.Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
let:toggle
|
||||
@@ -832,9 +939,12 @@
|
||||
<svelte:fragment slot="start">
|
||||
<Layout.Stack direction="row" gap="xxs" alignItems="center">
|
||||
<Spinner size="s" />
|
||||
<Typography.Text style="white-space: nowrap">
|
||||
<Typography.Caption
|
||||
variant="500"
|
||||
color="--fgcolor-neutral-secondary"
|
||||
style="white-space: nowrap">
|
||||
Thinking of column suggestions
|
||||
</Typography.Text>
|
||||
</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
@@ -847,7 +957,7 @@
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
</div>
|
||||
{:else if customColumns.length > 0 && showFloatingBar}
|
||||
{:else if customColumns.some((col) => !col.isPlaceholder) && showFloatingBar}
|
||||
<div
|
||||
class="floating-action-wrapper"
|
||||
class:expanded={!creatingColumns}
|
||||
@@ -959,7 +1069,7 @@
|
||||
transition: box-shadow 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
& :global(.column-resizer-disabled) {
|
||||
& :global([role='cell']:not([data-column-id='actions']) .column-resizer-disabled) {
|
||||
border-left: var(--border-width-s, 1px) solid rgba(253, 54, 110, 0.24) !important;
|
||||
transition: border-color 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
@@ -1021,7 +1131,17 @@
|
||||
& .floating-action-wrapper {
|
||||
& :global(:first-child) {
|
||||
z-index: 21;
|
||||
left: calc(65% - 525px / 2);
|
||||
transition: all 600ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
left: calc(50% - 525px / 2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
left: calc(50% - 400px / 2);
|
||||
max-width: 400px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded :global(:first-child) {
|
||||
@@ -1059,6 +1179,12 @@
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
&.thinking {
|
||||
& :global(.spreadsheet-container) {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
& :global([data-select='true']) {
|
||||
opacity: 0.85;
|
||||
pointer-events: none;
|
||||
@@ -1146,7 +1272,8 @@
|
||||
border: var(--border-width-L, 2px) solid rgba(253, 54, 110, 0.24) !important;
|
||||
|
||||
& :global(i) {
|
||||
margin-inline-end: 8px !important;
|
||||
margin-inline-end: 6px !important;
|
||||
margin-block-start: -4px !important;
|
||||
}
|
||||
|
||||
& :global(::selection) {
|
||||
@@ -1157,4 +1284,21 @@
|
||||
:global(.filter-modal-actions-menu.variant) {
|
||||
max-height: 184px;
|
||||
}
|
||||
|
||||
/* Sequential animation for column titles and icons */
|
||||
.column-title,
|
||||
.column-icon-wrapper {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transition:
|
||||
opacity 0.4s ease-out,
|
||||
transform 0.4s ease-out;
|
||||
}
|
||||
|
||||
.column-title.animate-in,
|
||||
.column-icon-wrapper.animate-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition-delay: var(--animation-delay, 0ms);
|
||||
}
|
||||
</style>
|
||||
|
||||
+16
-1
@@ -1,8 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let {
|
||||
notification = false
|
||||
}: {
|
||||
notification?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Layout.Stack inline alignItems="center" justifyContent="center" class="ai-icon-holder">
|
||||
<Layout.Stack
|
||||
inline
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
class="ai-icon-holder {notification ? 'notification' : ''}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none">
|
||||
<path
|
||||
d="M11.0156 6.20801C11.3539 5.29392 12.6461 5.29392 12.9844 6.20801L14.5801 10.5186C14.7776 11.052 15.198 11.4724 15.7314 11.6699L20.042 13.2656C20.9561 13.6039 20.9561 14.8961 20.042 15.2344L15.7314 16.8301C15.198 17.0276 14.7776 17.448 14.5801 17.9814L12.9844 22.292C12.6461 23.2061 11.3539 23.2061 11.0156 22.292L9.41992 17.9814C9.2224 17.448 8.80197 17.0276 8.26855 16.8301L3.95801 15.2344C3.04392 14.8961 3.04392 13.6039 3.95801 13.2656L8.26855 11.6699C8.80196 11.4724 9.2224 11.052 9.41992 10.5186L11.0156 6.20801Z"
|
||||
@@ -64,4 +74,9 @@
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
:global(.ai-icon-holder.notification) {
|
||||
width: 36px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import IconAI from './ai.svelte';
|
||||
</script>
|
||||
|
||||
<IconAI notification />
|
||||
+569
@@ -0,0 +1,569 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Accordion, Icon, Layout, Skeleton, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Button, InputSelect /*InputNumber*/ } from '$lib/elements/forms';
|
||||
import {
|
||||
showIndexesSuggestions,
|
||||
IndexOrder,
|
||||
mockSuggestions,
|
||||
type SuggestedIndexSchema
|
||||
} from './store';
|
||||
import { Modal, Confirm } 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';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { type Columns, table } from '../table-[table]/store';
|
||||
import { isRelationship } from '../table-[table]/rows/store';
|
||||
import { VARS } from '$lib/system';
|
||||
import { sleep } from '$lib/helpers/promises';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { page } from '$app/state';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { type ComponentType, onDestroy, onMount } from 'svelte';
|
||||
import { columnOptions as baseColumnOptions } from '../table-[table]/columns/store';
|
||||
|
||||
const MAX_INDEXES = 5;
|
||||
|
||||
let modalError = $state(null);
|
||||
let creatingIndexes = $state(false);
|
||||
let loadingSuggestions = $state(false);
|
||||
let indexes = $state<SuggestedIndexSchema[]>([]);
|
||||
let confirmDismiss = $state(false);
|
||||
let columnOptions: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
leadingIcon?: ComponentType;
|
||||
}> = $state();
|
||||
|
||||
const tableId = page.params.table;
|
||||
const databaseId = page.params.database;
|
||||
|
||||
function makeColumnOptions() {
|
||||
if (VARS.MOCK_AI_SUGGESTIONS) {
|
||||
columnOptions = mockSuggestions.columns.map((column) => ({
|
||||
value: column.name,
|
||||
label: column.name,
|
||||
leadingIcon: baseColumnOptions.find((option) => option.type === column.type)?.icon
|
||||
}));
|
||||
} else {
|
||||
columnOptions = $table.columns
|
||||
.filter((column) => !isRelationship(column))
|
||||
.map((column) => ({
|
||||
value: column.key,
|
||||
label: column.key,
|
||||
leadingIcon: baseColumnOptions.find((option) => option.type === column.type)
|
||||
?.icon
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadIndexSuggestions(): Promise<SuggestedIndexSchema[]> {
|
||||
modalError = null;
|
||||
loadingSuggestions = true;
|
||||
|
||||
if (VARS.MOCK_AI_SUGGESTIONS) {
|
||||
await sleep(1250);
|
||||
|
||||
indexes = mockSuggestions.columns.slice(0, 3).map((column, index) => ({
|
||||
key: column.name,
|
||||
type: IndexType.Key,
|
||||
columns: [column.name],
|
||||
orders: index === 2 ? IndexOrder.DESC : IndexOrder.ASC,
|
||||
lengths: []
|
||||
}));
|
||||
} else {
|
||||
try {
|
||||
const suggestions = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.console.suggestIndexes({
|
||||
databaseId,
|
||||
tableId: $table.$id
|
||||
});
|
||||
|
||||
indexes = suggestions.indexes.map((index) => {
|
||||
return {
|
||||
key: index.columns[0],
|
||||
type: index.type as IndexType,
|
||||
orders: (index.orders?.[0] as IndexOrder) || IndexOrder.ASC,
|
||||
columns: index.columns,
|
||||
lengths: index.lengths ?? []
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
|
||||
await closeAndInvalidate();
|
||||
return []; // formality, UI is closed by now!
|
||||
}
|
||||
}
|
||||
|
||||
makeColumnOptions();
|
||||
|
||||
loadingSuggestions = false;
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
function addIndex() {
|
||||
if (indexes.length < MAX_INDEXES) {
|
||||
indexes.push({
|
||||
key: '',
|
||||
type: IndexType.Key,
|
||||
orders: IndexOrder.ASC,
|
||||
columns: [],
|
||||
lengths: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeIndex(index: number) {
|
||||
indexes.splice(index, 1);
|
||||
}
|
||||
|
||||
function syncIndexState(event: CustomEvent, index: SuggestedIndexSchema) {
|
||||
const selected = event.detail;
|
||||
index.key = selected;
|
||||
index.columns = selected ? [selected] : [];
|
||||
if (index.lengths) {
|
||||
index.lengths = index.lengths.slice(0, index.columns.length);
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderOptions(selectedType: IndexType) {
|
||||
const base = [IndexOrder.ASC, IndexOrder.DESC];
|
||||
const values = selectedType === IndexType.Spatial ? [...base, IndexOrder.NONE] : base;
|
||||
|
||||
return values.map((order) => ({
|
||||
label: capitalize(String(order)),
|
||||
value: order
|
||||
}));
|
||||
}
|
||||
|
||||
function generateUniqueIndexKey(index: SuggestedIndexSchema, usedKeys: Set<string>): string {
|
||||
const existingKeys = $table.indexes.map((idx) => idx.key);
|
||||
let suggestedKey = `${index.key || index.columns[0]}_${index.type.toLowerCase()}`;
|
||||
let uniqueKey = suggestedKey;
|
||||
let counter = 1;
|
||||
|
||||
while (existingKeys.includes(uniqueKey) || usedKeys.has(uniqueKey)) {
|
||||
uniqueKey = `${suggestedKey}_${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
usedKeys.add(uniqueKey);
|
||||
return uniqueKey;
|
||||
}
|
||||
|
||||
function prepareIndexForCreation(index: SuggestedIndexSchema, columnMap: Map<string, Columns>) {
|
||||
// prepare orders array
|
||||
const orders = index.orders !== null ? index.columns.map(() => String(index.orders)) : [];
|
||||
|
||||
// prepare lengths array
|
||||
let lengths: (number | null)[];
|
||||
if (index.type === IndexType.Key) {
|
||||
// only validate if it's a key index
|
||||
lengths = index.columns.map((columnKey, i) => {
|
||||
const column = columnMap.get(columnKey);
|
||||
if (column?.type === 'string') {
|
||||
const stringColumn = column as Models.ColumnString;
|
||||
const requestedLength = index.lengths?.[i];
|
||||
if (
|
||||
requestedLength &&
|
||||
stringColumn.size &&
|
||||
requestedLength > stringColumn.size
|
||||
) {
|
||||
return stringColumn.size;
|
||||
}
|
||||
return requestedLength || null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
// non-key indexes, lengths are null!
|
||||
lengths = Array(index.columns.length).fill(null);
|
||||
}
|
||||
|
||||
return { orders, lengths };
|
||||
}
|
||||
|
||||
function dismissIndexes() {
|
||||
indexes = [];
|
||||
confirmDismiss = false;
|
||||
$showIndexesSuggestions = false;
|
||||
}
|
||||
|
||||
async function closeAndInvalidate() {
|
||||
// close modal/sheet.
|
||||
$showIndexesSuggestions = false;
|
||||
|
||||
/**
|
||||
* Invalidate dependencies after modal/sheet close,
|
||||
* because otherwise the `await` block will re-run in the modal.
|
||||
*/
|
||||
await invalidate(Dependencies.TABLE);
|
||||
}
|
||||
|
||||
async function applySuggestedIndexes(): Promise<boolean> {
|
||||
modalError = null;
|
||||
creatingIndexes = true;
|
||||
|
||||
for (const [i, index] of indexes.entries()) {
|
||||
if (!index.key || !index.type || !index.columns || index.columns.length === 0) {
|
||||
modalError = `Index ${i + 1}: Selected column or type invalid`;
|
||||
creatingIndexes = false;
|
||||
return true; // keep sheet open!
|
||||
}
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
const usedKeys = new Set<string>();
|
||||
const columnMap = new Map($table.columns.map((col) => [col.key, col]));
|
||||
const sdkClient = sdk.forProject(page.params.region, page.params.project);
|
||||
|
||||
for (const [_, index] of indexes.entries()) {
|
||||
try {
|
||||
// prepare and validate index data
|
||||
const { orders, lengths } = prepareIndexForCreation(index, columnMap);
|
||||
|
||||
// generate unique key name for the index
|
||||
const uniqueIndexKey = generateUniqueIndexKey(index, usedKeys);
|
||||
|
||||
await sdkClient.tablesDB.createIndex({
|
||||
databaseId,
|
||||
tableId,
|
||||
key: uniqueIndexKey,
|
||||
type: index.type,
|
||||
columns: index.columns,
|
||||
lengths,
|
||||
...(orders.length ? { orders } : {})
|
||||
});
|
||||
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
if (!modalError) {
|
||||
modalError = error.message;
|
||||
}
|
||||
trackError(error, Submit.IndexCreate);
|
||||
creatingIndexes = false;
|
||||
return true; // keep sheet open!
|
||||
}
|
||||
}
|
||||
|
||||
creatingIndexes = false;
|
||||
|
||||
if (successCount > 0) {
|
||||
trackEvent(Submit.IndexCreate, { type: 'suggestions', count: successCount });
|
||||
}
|
||||
|
||||
if (successCount === indexes.length) {
|
||||
// all succeeded
|
||||
addNotification({
|
||||
message: `${successCount} index${successCount > 1 ? 'es are' : ' is'} being created`,
|
||||
type: 'success'
|
||||
});
|
||||
|
||||
await closeAndInvalidate();
|
||||
} else if (successCount > 0) {
|
||||
// some succeeded, some failed
|
||||
addNotification({
|
||||
message: 'Some indexes could not be created',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
await closeAndInvalidate();
|
||||
} else {
|
||||
// all failed
|
||||
addNotification({
|
||||
message: 'Failed to create indexes',
|
||||
type: 'error'
|
||||
});
|
||||
|
||||
return true; // keep sheet open!
|
||||
}
|
||||
|
||||
return false; // close the sheet!
|
||||
}
|
||||
|
||||
const typeOptions = Object.values(IndexType).map((type) => ({
|
||||
label: capitalize(type),
|
||||
value: type
|
||||
}));
|
||||
|
||||
onMount(() => showIndexesSuggestions.set(false));
|
||||
|
||||
onDestroy(() => showIndexesSuggestions.set(false));
|
||||
</script>
|
||||
|
||||
{#if !$isSmallViewport}
|
||||
<Modal
|
||||
dismissible={false}
|
||||
title="Suggested indexes"
|
||||
bind:error={modalError}
|
||||
bind:show={$showIndexesSuggestions}
|
||||
onSubmit={async () => {
|
||||
await applySuggestedIndexes();
|
||||
}}>
|
||||
<Layout.Stack gap="m">
|
||||
{#await loadIndexSuggestions()}
|
||||
<Layout.Stack gap="xs">
|
||||
<Layout.Stack gap="m">
|
||||
{#each Array(3) as _, index}
|
||||
{@const firstItem = index === 0}
|
||||
<Layout.Stack gap="m" direction="row" alignItems="center">
|
||||
{@render fieldSkeleton({ label: 'Column', showLabel: firstItem })}
|
||||
{@render fieldSkeleton({ label: 'Type', showLabel: firstItem })}
|
||||
{@render fieldSkeleton({ label: 'Order', showLabel: firstItem })}
|
||||
<!--{@render fieldSkeleton({ label: 'Length', showLabel: firstItem })}-->
|
||||
|
||||
<div style:margin-top={firstItem ? '27.6px' : '0'}>
|
||||
<Skeleton
|
||||
variant="square"
|
||||
width="33px"
|
||||
height="33px"
|
||||
style="opacity: 0.25" />
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
{@render addIndexButton()}
|
||||
</Layout.Stack>
|
||||
{:then suggestedIndexes}
|
||||
<Layout.Stack gap="xs">
|
||||
<Layout.Stack gap="m">
|
||||
{#each suggestedIndexes as index, count}
|
||||
{@render indexEditForm({ index, count, isDesktop: true })}
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
|
||||
{@render addIndexButton()}
|
||||
</Layout.Stack>
|
||||
{/await}
|
||||
</Layout.Stack>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center" inline>
|
||||
<Layout.Stack direction="row" gap="m">
|
||||
<Button
|
||||
text
|
||||
size="s"
|
||||
disabled={loadingSuggestions || creatingIndexes}
|
||||
on:click={() => {
|
||||
if (indexes.length > 0 && !creatingIndexes) {
|
||||
confirmDismiss = true;
|
||||
} else {
|
||||
$showIndexesSuggestions = false;
|
||||
}
|
||||
}}>Cancel</Button>
|
||||
|
||||
<Button
|
||||
size="s"
|
||||
submit
|
||||
submissionLoader
|
||||
disabled={indexes.length === 0 || loadingSuggestions}
|
||||
forceShowLoader={creatingIndexes}>
|
||||
Create
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
{#if $isSmallViewport}
|
||||
<SideSheet
|
||||
closeOnBlur={false}
|
||||
title="Suggested indexes"
|
||||
data-index-edit-form-sheet
|
||||
bind:show={$showIndexesSuggestions}
|
||||
submit={{
|
||||
text: 'Create',
|
||||
disabled: indexes.length === 0 || creatingIndexes || loadingSuggestions,
|
||||
onClick: async () => await applySuggestedIndexes()
|
||||
}}
|
||||
cancel={{
|
||||
disabled: loadingSuggestions || creatingIndexes,
|
||||
onClick: () => {
|
||||
if (indexes.length > 0 && !creatingIndexes) {
|
||||
confirmDismiss = true;
|
||||
} else {
|
||||
$showIndexesSuggestions = false;
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{#if modalError}
|
||||
<Alert.Inline status="error">
|
||||
{modalError}
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
{#await loadIndexSuggestions()}
|
||||
<Layout.Stack gap="m" alignItems="center">
|
||||
{#each Array(3) as _}
|
||||
{@render fieldSkeleton({
|
||||
label: undefined,
|
||||
showLabel: false,
|
||||
isDesktop: false
|
||||
})}
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
{:then suggestedIndexes}
|
||||
<Layout.Stack gap="s">
|
||||
{#each suggestedIndexes as index, count}
|
||||
<Accordion title="{index.key}_index">
|
||||
{@render indexEditForm({ index, count, isDesktop: false })}
|
||||
</Accordion>
|
||||
{/each}
|
||||
|
||||
{@render addIndexButton()}
|
||||
</Layout.Stack>
|
||||
{/await}
|
||||
</SideSheet>
|
||||
{/if}
|
||||
|
||||
{#snippet fieldSkeleton({ label, showLabel, isDesktop = true })}
|
||||
<Layout.Stack gap="xxxs">
|
||||
{#if showLabel}
|
||||
<Typography.Text variant="m-500" style="margin-block-end: var(--gap-s);"
|
||||
>{label}</Typography.Text>
|
||||
{/if}
|
||||
<Skeleton
|
||||
variant="line"
|
||||
width="100%"
|
||||
height={isDesktop ? 33.602 : 39.398}
|
||||
style="opacity: 0.25" />
|
||||
</Layout.Stack>
|
||||
{/snippet}
|
||||
|
||||
{#snippet indexEditForm({ index, count, isDesktop = true })}
|
||||
{@const firstItem = count === 0}
|
||||
{#if isDesktop}
|
||||
<Layout.Stack gap="m" direction="row" alignItems="center" class="desktop-selects-form">
|
||||
<InputSelect
|
||||
id="key-{count}"
|
||||
label={firstItem ? 'Column' : undefined}
|
||||
bind:value={index.key}
|
||||
on:change={(event) => syncIndexState(event, index)}
|
||||
options={columnOptions}
|
||||
placeholder="Select column"
|
||||
required
|
||||
disabled={creatingIndexes} />
|
||||
|
||||
<InputSelect
|
||||
id="type-{count}"
|
||||
label={firstItem ? 'Type' : ''}
|
||||
bind:value={index.type}
|
||||
options={typeOptions}
|
||||
required
|
||||
disabled={creatingIndexes} />
|
||||
|
||||
<InputSelect
|
||||
id="order-{count}"
|
||||
label={firstItem ? 'Order' : ''}
|
||||
bind:value={index.orders}
|
||||
options={getOrderOptions(index.type)}
|
||||
required
|
||||
disabled={creatingIndexes} />
|
||||
|
||||
{@render removeIndexButton({ count, isDesktop: true })}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<!-- Mobile: Vertical layout -->
|
||||
<Layout.Stack gap="m">
|
||||
<Layout.Stack gap="s">
|
||||
<InputSelect
|
||||
id="key-{count}-mobile"
|
||||
bind:value={index.key}
|
||||
on:change={(event) => syncIndexState(event, index)}
|
||||
options={columnOptions}
|
||||
placeholder="Select column"
|
||||
required />
|
||||
|
||||
<InputSelect
|
||||
id="type-{count}-mobile"
|
||||
bind:value={index.type}
|
||||
options={typeOptions}
|
||||
required />
|
||||
|
||||
<InputSelect
|
||||
id="order-{count}-mobile"
|
||||
bind:value={index.orders}
|
||||
options={getOrderOptions(index.type)}
|
||||
required />
|
||||
</Layout.Stack>
|
||||
|
||||
{@render removeIndexButton({ count, isDesktop: false })}
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet addIndexButton()}
|
||||
{#if indexes.length < MAX_INDEXES}
|
||||
<Layout.Stack direction="row" justifyContent="flex-start">
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
compact
|
||||
on:click={addIndex}
|
||||
disabled={loadingSuggestions || creatingIndexes}>
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
Add Index
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet removeIndexButton({ count, isDesktop = true })}
|
||||
{#if isDesktop}
|
||||
<div style:margin-top={count === 0 ? '27.6px' : '0'}>
|
||||
<Button
|
||||
icon
|
||||
size="xs"
|
||||
secondary
|
||||
on:click={() => removeIndex(count)}
|
||||
disabled={indexes.length <= 1 || creatingIndexes}>
|
||||
<Icon icon={IconX} color="--fgcolor-danger-primary" />
|
||||
</Button>
|
||||
</div>
|
||||
{:else if indexes.length > 1}
|
||||
<Layout.Stack direction="row" justifyContent="flex-start">
|
||||
<Button
|
||||
size="s"
|
||||
secondary
|
||||
on:click={() => removeIndex(count)}
|
||||
disabled={indexes.length <= 1 || creatingIndexes}>Delete</Button>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<Confirm
|
||||
confirmDeletion
|
||||
action="Dismiss"
|
||||
title="Dismiss indexes"
|
||||
bind:open={confirmDismiss}
|
||||
onSubmit={dismissIndexes}>
|
||||
Are you sure you want to dismiss these suggested indexes? This action cannot be undone.
|
||||
</Confirm>
|
||||
|
||||
<style lang="scss">
|
||||
// Custom logic to hide the Sheet's
|
||||
// `X` close button (not configurable via props)
|
||||
:global([data-index-edit-form-sheet] header) {
|
||||
& :global(.divider),
|
||||
& :global(button) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
// stack > div > input-select button!
|
||||
:global(.desktop-selects-form :first-child button:first-of-type) {
|
||||
width: 164px;
|
||||
max-width: 164px;
|
||||
}
|
||||
</style>
|
||||
+39
-8
@@ -2,19 +2,34 @@
|
||||
import IconAI from './icon/ai.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { tableColumnSuggestions } from './store';
|
||||
import { InputTextarea } from '$lib/elements/forms';
|
||||
import { Card, Layout, Selector, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { Button, InputTextarea } from '$lib/elements/forms';
|
||||
import { Card, Icon, Layout, Selector, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { IconExternalLink } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
onMount(() => {
|
||||
// enable by default!
|
||||
$tableColumnSuggestions.enabled = true;
|
||||
if (featureActive) {
|
||||
$tableColumnSuggestions.enabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
const featureActive = $derived(isCloud);
|
||||
|
||||
const subtitle = $derived.by(() => {
|
||||
return featureActive
|
||||
? 'Enable AI to suggest useful columns based on your table name'
|
||||
: 'AI suggestions are available on all Appwrite Cloud projects';
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card.Base variant="secondary" radius="s" padding="xs">
|
||||
<Layout.Stack gap="m">
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center">
|
||||
<Layout.Stack
|
||||
gap="s"
|
||||
direction="row"
|
||||
alignItems={$isSmallViewport ? 'flex-start' : 'center'}>
|
||||
<IconAI />
|
||||
|
||||
<Layout.Stack direction="column" gap="none">
|
||||
@@ -26,17 +41,33 @@
|
||||
<Selector.Switch
|
||||
id="suggestions"
|
||||
label={undefined}
|
||||
disabled={!featureActive}
|
||||
bind:checked={$tableColumnSuggestions.enabled} />
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
|
||||
<Typography.Text color="--fgcolor-neutral-secondary">
|
||||
Enable AI to suggest useful columns based on your table name
|
||||
{subtitle}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
|
||||
{#if $tableColumnSuggestions.enabled}
|
||||
{#if !featureActive}
|
||||
<Layout.Stack inline alignItems="flex-end">
|
||||
<Button
|
||||
external
|
||||
secondary
|
||||
fullWidthMobile
|
||||
href="https://cloud.appwrite.io/register">
|
||||
Sign up
|
||||
|
||||
<Icon icon={IconExternalLink} size="s" />
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<!-- just being safe with extra guard! -->
|
||||
{#if $tableColumnSuggestions.enabled && featureActive}
|
||||
<div transition:slide={{ duration: 200 }}>
|
||||
<InputTextarea
|
||||
id="context"
|
||||
@@ -50,7 +81,7 @@
|
||||
</Card.Base>
|
||||
|
||||
<style lang="scss">
|
||||
.suggestions-switch :global(button) {
|
||||
.suggestions-switch :global(button):not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
+17
@@ -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<TableColumnSuggestions>({
|
||||
enabled: false,
|
||||
context: null,
|
||||
@@ -33,6 +48,8 @@ export const tableColumnSuggestions = writable<TableColumnSuggestions>({
|
||||
|
||||
export const createTableRequest = writable<boolean>(false);
|
||||
|
||||
export const showIndexesSuggestions = writable<boolean>(false);
|
||||
|
||||
export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
|
||||
total: 7,
|
||||
columns: [
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconQuestionMarkCircle } from '@appwrite.io/pink-icons-svelte';
|
||||
</script>
|
||||
|
||||
<Icon icon={IconQuestionMarkCircle} />
|
||||
+14
-1
@@ -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 @@
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Dialog>
|
||||
|
||||
<IndexesSuggestions />
|
||||
|
||||
+8
-1
@@ -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}
|
||||
<SuggestionsEmptySheet />
|
||||
{:else}
|
||||
<EmptySheet
|
||||
|
||||
+17
-8
@@ -51,6 +51,7 @@
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { page } from '$app/state';
|
||||
import { formatName } from '$lib/helpers/string';
|
||||
|
||||
const updatedColumnsForSheet = $derived.by(() => {
|
||||
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">
|
||||
<span class="text u-trim-1" data-private>
|
||||
{#if column.key === '$id' || column.key === '$sequence' || column.key === '$createdAt' || column.key === '$updatedAt'}
|
||||
{column['name']}
|
||||
<Typography.Text truncate>
|
||||
{#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}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
{#if isString(column) && column.encrypt}
|
||||
<Tooltip>
|
||||
<Icon
|
||||
|
||||
+5
-5
@@ -52,12 +52,12 @@
|
||||
let showDelete = $state(false);
|
||||
let showOverview = $state(false);
|
||||
|
||||
let columns = $state([
|
||||
{ id: 'key' },
|
||||
{ id: 'type' },
|
||||
{ id: 'columns' },
|
||||
let columns = $derived([
|
||||
{ id: 'key', width: { min: $isSmallViewport ? 250 : 200 }, resizable: false },
|
||||
{ id: 'type', width: 120, resizable: false },
|
||||
{ id: 'columns', width: { min: 200 }, resizable: false },
|
||||
// { id: 'orders' }, // design doesn't have orders atm
|
||||
{ id: 'lengths' },
|
||||
{ id: 'lengths', width: { min: 180 }, resizable: false },
|
||||
{ id: 'actions', width: 40, isAction: true }
|
||||
]);
|
||||
|
||||
|
||||
+19
-6
@@ -12,8 +12,9 @@
|
||||
import { isRelationship, isSpatialType } from '../rows/store';
|
||||
import { table, indexes } from '../store';
|
||||
import { Icon, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { IconCalendar, IconFingerPrint, IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { columnOptions as baseColumnOptions } from '../columns/store';
|
||||
|
||||
let {
|
||||
showCreateIndex = $bindable(false),
|
||||
@@ -37,7 +38,11 @@
|
||||
}
|
||||
return !isRelationship(column) && !isSpatialType(column); // keep non-relationship and non-spatial
|
||||
})
|
||||
.map((column) => ({ 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
|
||||
]}
|
||||
|
||||
+6
-2
@@ -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);
|
||||
|
||||
+26
-5
@@ -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<boolean | void>;
|
||||
}
|
||||
| undefined;
|
||||
cancel?:
|
||||
| {
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
| undefined;
|
||||
children?: Snippet;
|
||||
footer?: Snippet | null;
|
||||
} = $props();
|
||||
} & HTMLAttributes<HTMLDivElement> = $props();
|
||||
|
||||
let form: Form;
|
||||
let submitting = $state(writable(false));
|
||||
@@ -46,7 +56,7 @@
|
||||
let copyText = $state(undefined);
|
||||
</script>
|
||||
|
||||
<div class="sheet-container" data-side-sheet-visible={show}>
|
||||
<div class="sheet-container" data-side-sheet-visible={show} {...restProps}>
|
||||
<Sheet bind:open={show} {closeOnBlur}>
|
||||
<div slot="header" style:width="100%">
|
||||
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
@@ -110,8 +120,19 @@
|
||||
{#if footer}
|
||||
{@render footer?.()}
|
||||
{/if}
|
||||
<Button size="s" secondary on:click={() => (show = false)}
|
||||
>Cancel</Button>
|
||||
|
||||
<Button
|
||||
size="s"
|
||||
secondary
|
||||
disabled={cancel?.disabled}
|
||||
on:click={() => {
|
||||
if (cancel?.onClick) {
|
||||
cancel.onClick();
|
||||
} else {
|
||||
show = false;
|
||||
}
|
||||
}}>{cancel?.text ?? 'Cancel'}</Button>
|
||||
|
||||
<Button
|
||||
size="s"
|
||||
submit
|
||||
|
||||
Reference in New Issue
Block a user