Merge branch '1.8.x' into 'spreadsheet' ‼️.

This commit is contained in:
Darshan
2025-07-30 17:49:40 +05:30
140 changed files with 1939 additions and 1929 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2289",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7813f3d",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
"@appwrite.io/pink-legacy": "^1.0.3",
+9 -9
View File
@@ -12,8 +12,8 @@ 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@2289
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2289
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7813f3d
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7813f3d
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.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@2289':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2289}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7813f3d':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7813f3d}
version: 1.9.0
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
@@ -2551,8 +2551,8 @@ packages:
loupe@3.1.3:
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
loupe@3.2.0:
resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==}
loupe@3.1.4:
resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -3653,7 +3653,7 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2289': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7813f3d': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
@@ -5046,7 +5046,7 @@ snapshots:
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
loupe: 3.2.0
loupe: 3.1.4
tinyrainbow: 2.0.0
'@vue/compiler-core@3.5.13':
@@ -6186,7 +6186,7 @@ snapshots:
loupe@3.1.3: {}
loupe@3.2.0: {}
loupe@3.1.4: {}
lru-cache@10.4.3: {}
+16 -16
View File
@@ -148,9 +148,9 @@ export enum Click {
ConnectRepositoryClick = 'click_connect_repository',
CreditsRedeemClick = 'click_credits_redeem',
CloudSignupClick = 'click_cloud_signup',
DatabaseAttributeDelete = 'click_attribute_delete',
DatabaseColumnDelete = 'click_column_delete',
DatabaseIndexDelete = 'click_index_delete',
DatabaseCollectionDelete = 'click_collection_delete',
DatabaseTableDelete = 'click_table_delete',
DatabaseDatabaseDelete = 'click_database_delete',
DatabaseImportCsv = 'click_database_import_csv',
DomainCreateClick = 'click_domain_create',
@@ -273,22 +273,22 @@ export enum Submit {
DatabaseDelete = 'submit_database_delete',
DatabaseUpdateName = 'submit_database_update_name',
DatabaseImportCsv = 'submit_database_import_csv',
AttributeCreate = 'submit_attribute_create',
AttributeUpdate = 'submit_attribute_update',
AttributeDelete = 'submit_attribute_delete',
DocumentCreate = 'submit_document_create',
DocumentDelete = 'submit_document_delete',
DocumentUpdate = 'submit_document_update',
DocumentUpdatePermissions = 'submit_document_update_permissions',
ColumnCreate = 'submit_column_create',
ColumnUpdate = 'submit_column_update',
ColumnDelete = 'submit_column_delete',
RowCreate = 'submit_row_create',
RowDelete = 'submit_row_delete',
RowUpdate = 'submit_row_update',
RowUpdatePermissions = 'submit_row_update_permissions',
IndexCreate = 'submit_index_create',
IndexDelete = 'submit_index_delete',
CollectionCreate = 'submit_collection_create',
CollectionDelete = 'submit_collection_delete',
CollectionUpdateName = 'submit_collection_update_name',
CollectionUpdatePermissions = 'submit_collection_update_permissions',
CollectionUpdateSecurity = 'submit_collection_update_security',
CollectionUpdateEnabled = 'submit_collection_update_enabled',
CollectionUpdateDisplayNames = 'submit_collection_update_display_names',
TableCreate = 'submit_row_create',
TableDelete = 'submit_row_delete',
TableUpdateName = 'submit_row_update_name',
TableUpdatePermissions = 'submit_row_update_permissions',
TableUpdateSecurity = 'submit_row_update_security',
TableUpdateEnabled = 'submit_row_update_enabled',
TableUpdateDisplayNames = 'submit_row_update_display_names',
FunctionCreate = 'submit_function_create',
FunctionDelete = 'submit_function_delete',
FunctionUpdateName = 'submit_function_update_name',
+3 -3
View File
@@ -28,10 +28,10 @@ const groups = [
'integrations',
'migrations',
'users',
'collections',
'attributes',
'tables',
'columns',
'indexes',
'documents',
'rows',
'teams',
'security',
'buckets',
+1 -1
View File
@@ -27,7 +27,7 @@
const examples = [
'How to add platform in the console?',
'How can I manage users, permissions, and access control in Appwrite?',
'How can I set up database collections and documents in Appwrite?',
'How can I set up database tables and rows in Appwrite?',
'How do I configure and manage server-side functions in Appwrite?',
'How to add custom domain in the console?'
];
@@ -1,16 +1,16 @@
<script lang="ts">
import { initCreateAttribute } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
import { attributeOptions } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/attributes/store';
import { initCreateColumn } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte';
import { columnOptions } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/store';
import Template from './template.svelte';
let search = '';
let options = attributeOptions.map((option) => {
let options = columnOptions.map((option) => {
return {
label: option.name,
icon: option.icon,
callback() {
initCreateAttribute(option.name);
initCreateColumn(option.name);
}
};
});
+8 -8
View File
@@ -37,16 +37,16 @@ export const DatabasesPanel: SubPanel = {
component: Databases
};
import Collections from './collections.svelte';
export const CollectionsPanel: SubPanel = {
name: 'Collections',
component: Collections
import Tables from './tables.svelte';
export const TablesPanel: SubPanel = {
name: 'Tables',
component: Tables
};
import CreateAttribute from './createAttribute.svelte';
export const CreateAttributePanel: SubPanel = {
name: 'Create Attribute',
component: CreateAttribute
import CreateColumn from './createColumn.svelte';
export const CreateColumnPanel: SubPanel = {
name: 'Create Column',
component: CreateColumn
};
import Users from './users.svelte';
@@ -1,9 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { collectionsSearcher } from '../searchers';
import { tablesSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(collectionsSearcher);
const { search, results } = initSearcher(tablesSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -1,29 +0,0 @@
import { goto } from '$app/navigation';
import { database } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
import { page } from '$app/state';
export const collectionsSearcher = (async (query: string) => {
const databaseId = get(database).$id;
const { collections } = await sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections(databaseId);
return collections
.filter((col) => col.name.toLowerCase().includes(query.toLowerCase()))
.map(
(col) =>
({
group: 'collections',
label: col.name,
callback: () => {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${col.$id}`
);
}
}) as const
);
}) satisfies Searcher;
+1 -1
View File
@@ -8,7 +8,7 @@ import { page } from '$app/state';
export const dbSearcher = (async (query: string) => {
const { databases } = await sdk
.forProject(page.params.region, page.params.project)
.databases.list();
.grids.listDatabases();
return databases
.filter((db) => db.name.toLowerCase().includes(query.toLowerCase()))
+1 -1
View File
@@ -1,7 +1,7 @@
export * from './databases';
export * from './users';
export * from './organizations';
export * from './collections';
export * from './tables';
export * from './projects';
export * from './teams';
export * from './functions';
+27
View File
@@ -0,0 +1,27 @@
import { goto } from '$app/navigation';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
import { page } from '$app/state';
export const tablesSearcher = (async (query: string) => {
const databaseId = page.params.database;
const { tables } = await sdk
.forProject(page.params.region, page.params.project)
.grids.listTables(databaseId);
return tables
.filter((table) => table.name.toLowerCase().includes(query.toLowerCase()))
.map(
(table) =>
({
group: 'tables',
label: table.name,
callback: () => {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${table.$id}`
);
}
}) as const
);
}) satisfies Searcher;
+3 -2
View File
@@ -57,6 +57,7 @@
}
function updateOrAddItem(payload: Payload) {
// todo: @itznotabug - might need a change to $table?
const { $id, status, $collection, policyId } = payload;
if ($collection === 'archives' && policyId !== null) {
return;
@@ -117,9 +118,9 @@
}
function backupName(item: BackupArchive | BackupRestoration, key: string) {
const attribute = key === 'archives' ? '$createdAt' : 'startedAt';
const column = key === 'archives' ? '$createdAt' : 'startedAt';
return toLocaleDate(item[attribute]);
return toLocaleDate(item[column]);
}
onMount(() => {
+8 -8
View File
@@ -16,14 +16,14 @@
let {
columns,
isCustomCollection = false,
isCustomTable = false,
ui = 'legacy',
allowNoColumns = false,
showAnyway = false,
children
}: {
columns: Writable<Column[]>;
isCustomCollection?: boolean;
isCustomTable?: boolean;
allowNoColumns?: boolean;
ui?: 'legacy' | 'new';
showAnyway?: boolean;
@@ -44,7 +44,6 @@
let maxHeight = $state('none');
let containerRef = $state<HTMLElement>(null);
const collectionId = $derived(page.params.collection);
const isNewStyle = ui === 'new';
const calcMaxHeight = () => {
@@ -60,16 +59,16 @@
const saveColumnPreferences = () => {
const shownColumns = $columns.filter((n) => n.hide === true).map((n) => n.id);
if (isCustomCollection) {
preferences.setCustomCollectionColumns(collectionId, shownColumns);
if (isCustomTable) {
preferences.setCustomTableColumns(page.params.table, shownColumns);
} else {
preferences.setColumns(shownColumns);
}
};
onMount(() => {
if (isCustomCollection) {
const shownColumns = preferences.getCustomCollectionColumns(collectionId);
if (isCustomTable) {
const shownColumns = preferences.getCustomTableColumns(page.params.table);
columns.update((n) =>
n.map((column) => {
@@ -244,8 +243,9 @@
</Popover>
{/if}
<!-- svelte-ignore css_unused_selector -->
<style lang="scss">
/* global because its on a tooltip slot, nested */
/* global because its on a tooltip slot, nested */
:global(.actions-menu-wrapper) {
overflow: scroll;
margin-bottom: -0.35rem;
+18 -22
View File
@@ -13,7 +13,7 @@
type ImportItem = {
status: string;
collection?: string;
table?: string;
};
type ImportItemsMap = Map<string, ImportItem>;
@@ -22,15 +22,11 @@
* Keeps a track of the active and ongoing csv migrations.
*
* The structure is as follows -
* `{ migrationId: { status: status, collection: collection } }`
* `{ migrationId: { status: status, table: table } }`
*/
const importItems: Writable<ImportItemsMap> = writable(new Map());
async function showCompletionNotification(
database: string,
collection: string,
payload: Payload
) {
async function showCompletionNotification(database: string, table: string, payload: Payload) {
const isSuccess = payload.status === 'completed';
const isError = !isSuccess && !!payload.errors;
@@ -48,20 +44,20 @@
const type = isSuccess ? 'success' : 'error';
const message = isError ? errorMessage : 'CSV import finished successfully.';
const url = `${base}/project-${page.params.region}-${page.params.project}/databases/database-${database}/collection-${collection}`;
const url = `${base}/project-${page.params.region}-${page.params.project}/databases/database-${database}/table-${table}`;
addNotification({
type,
message,
isHtml: true,
buttons:
isSuccess && collection !== page.params.collection
? [{ name: 'View documents', method: () => goto(url) }]
isSuccess && table !== page.params.table
? [{ name: 'View rows', method: () => goto(url) }]
: undefined
});
if (isSuccess) {
await invalidate(Dependencies.DOCUMENTS);
await invalidate(Dependencies.ROWS);
}
}
@@ -70,19 +66,19 @@
const status = importData.status;
const resourceId = importData.resourceId ?? '';
const [databaseId, collectionId] = resourceId.split(':') ?? [];
const [databaseId, tableId] = resourceId.split(':') ?? [];
const current = $importItems.get(importData.$id);
let collectionName = current?.collection ?? null;
let tableName = current?.table ?? null;
if (!collectionName && collectionId) {
if (!tableName && tableId) {
try {
const collection = await sdk
const table = await sdk
.forProject(page.params.region, page.params.project)
.databases.getCollection(databaseId, collectionId);
collectionName = collection.name;
.grids.getTable(databaseId, tableId);
tableName = table.name;
} catch {
collectionName = null;
tableName = null;
}
}
@@ -99,12 +95,12 @@
if (shouldSkip) return items;
const next = new Map(items);
next.set(importData.$id, { status, collection: collectionName ?? undefined });
next.set(importData.$id, { status, table: tableName ?? undefined });
return next;
});
if (status === 'completed' || status === 'failed') {
await showCompletionNotification(databaseId, collectionId, importData);
await showCompletionNotification(databaseId, tableId, importData);
}
}
@@ -172,7 +168,7 @@
<header class="upload-box-header">
<h4 class="upload-box-title">
<Typography.Text variant="m-500">
Importing documents ({$importItems.size})
Importing rows ({$importItems.size})
</Typography.Text>
</h4>
<button
@@ -198,7 +194,7 @@
<div
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<Typography.Text>
{@html text(value.status, value.collection)}
{@html text(value.status, value.table)}
</Typography.Text>
</div>
<div
+24 -27
View File
@@ -26,10 +26,10 @@
...services.flatMap((s) => s.actions.map((a) => a.name)),
...services.flatMap((s) => s.resources.flatMap((r) => r.actions.map((a) => a.name)))
];
const attributeNames = [
...services.flatMap((s) => s.actions.flatMap((a) => a.attributes ?? [])),
const columnNames = [
...services.flatMap((s) => s.actions.flatMap((a) => a.columns ?? [])),
...services.flatMap((s) =>
s.resources.flatMap((r) => r.actions.flatMap((a) => a.attributes ?? []))
s.resources.flatMap((r) => r.actions.flatMap((a) => a.columns ?? []))
)
];
@@ -37,7 +37,7 @@
const isService = (s: string) => serviceNames.includes(s);
const isResource = (s: string) => resourceNames.includes(s);
const isAction = (s: string) => actionNames.includes(s);
const isAttribute = (s: string) => attributeNames.includes(s);
const isColumn = (s: string) => columnNames.includes(s);
const dispatch = createEventDispatcher();
@@ -69,7 +69,7 @@
case 'service': {
selected.resource = null;
selected.action = null;
selected.attribute = null;
selected.column = null;
break;
}
case 'resource': {
@@ -79,25 +79,25 @@
// We need to check if the selected action name is still
// on the available list, and if so, change the action to the
// appropriate one, as it may contain different attributes
// appropriate one, as it may contain different attributes/columns.
const availableAction = availableActions.find(
(a) => a.name === selected.action?.name
);
if (!availableAction) {
selected.action = null;
selected.attribute = null;
selected.column = null;
} else {
selected.action = availableAction;
if (!selected?.action?.attributes?.includes(selected.attribute)) {
selected.attribute = null;
if (!selected?.action?.columns?.includes(selected.column)) {
selected.column = null;
}
}
break;
}
case 'action': {
selected.attribute = null;
selected.column = null;
break;
}
}
@@ -113,16 +113,13 @@
if (isService(field)) {
selected.service = services.find((s) => s.name === field);
} else if (isResource(field)) {
const resource = selected.service?.resources.find((r) => r.name === field);
selected.resource = resource;
selected.resource = selected.service?.resources.find((r) => r.name === field);
} else if (isAction(field)) {
const action =
selected.action =
selected.resource?.actions.find((a) => a.name === field) ||
selected.service?.actions.find((a) => a.name === field);
selected.action = action;
} else if (isAttribute(field)) {
const attribute = selected.action?.attributes?.find((a) => a === field);
selected.attribute = attribute;
} else if (isColumn(field)) {
selected.column = selected.action?.columns?.find((a) => a === field);
}
}
}
@@ -143,7 +140,7 @@
const secondToLastField = at(fields, index - 2);
if (index === 0 || isService(currField)) return 'service';
if (isAttribute(currField) || isAction(prevField)) return 'attribute';
if (isColumn(currField) || isAction(prevField)) return 'attribute';
if (isAction(currField)) return 'action';
if (isResource(currField)) return 'resource';
if (isService(prevField) || isResource(prevField) || index === 1) {
@@ -182,7 +179,7 @@
service: null as EventService | null,
resource: null as EventResource | null,
action: null as EventAction | null,
attribute: null as string | null
column: null as string | null
};
let helper: string = null;
let customInput: string = null;
@@ -198,7 +195,7 @@
services: services,
resources: selected.service?.resources || [],
actions: (selected.resource ? selected.resource?.actions : selected.service?.actions) || [],
attributes: selected.action?.attributes || []
columns: selected.action?.columns || []
};
$: eventString = (function createEventString(): Array<{ value: string; description: string }> {
@@ -212,8 +209,8 @@
if (selected.service) {
fields.push(selected.service.name, '*');
}
if (selected.resource?.name === 'documents') {
fields.push('collections', '*');
if (selected.resource?.name === 'rows') {
fields.push('tables', '*');
}
if (selected.resource) {
fields.push(selected.resource.name, '*');
@@ -221,8 +218,8 @@
if (selected.action) {
fields.push(selected.action.name);
}
if (selected.attribute) {
fields.push(selected.attribute);
if (selected.column) {
fields.push(selected.column);
}
}
@@ -290,14 +287,14 @@
</Layout.Stack>
{/if}
{#if !empty(available.attributes)}
{#if !empty(available.columns)}
<Layout.Stack gap="s">
<Typography.Text variant="m-500">Choose an attribute (optional)</Typography.Text>
<Layout.Stack gap="s" wrap="wrap" direction="row">
{#each available.attributes as attribute}
{#each available.columns as attribute}
<Pill
disabled={showInput}
selected={selected.attribute === attribute}
selected={selected.column === attribute}
button
on:click={() => select('attribute', attribute)}>
{attribute}
+2 -2
View File
@@ -499,7 +499,7 @@
size="s"
event="empty_documentation"
href="https://appwrite.io/docs/products/storage/upload-download"
ariaLabel="create document"
ariaLabel="upload download"
>Documentation</Button>
<Button
secondary
@@ -526,7 +526,7 @@
size="s"
event="empty_documentation"
href="https://appwrite.io/docs/products/storage/buckets"
ariaLabel="create document">Documentation</Button>
ariaLabel="create buckets">Documentation</Button>
<Button
secondary
+10 -10
View File
@@ -12,8 +12,8 @@ export type TagValue = {
};
export type Operator = {
toTag: (attribute: string, input?: string | number | string[], type?: string) => TagValue;
toQuery: (attribute: string, input?: string | number | string[]) => string;
toTag: (column: string, input?: string | number | string[], type?: string) => TagValue;
toQuery: (column: string, input?: string | number | string[]) => string;
types: ColumnType[];
hideInput?: boolean;
};
@@ -134,7 +134,7 @@ export enum ValidTypes {
const operatorsDefault = new Map<
ValidOperators,
{
query: (attr: string, input: string | number | string[]) => string;
query: (col: string, input: string | number | string[]) => string;
types: ColumnType[];
hideInput?: boolean;
}
@@ -249,8 +249,8 @@ export function generateDefaultOperators() {
operatorsDefault.forEach((operator, operatorName) => {
operators[operatorName] = {
toQuery: operator.query,
toTag: (attribute, input = null, type = null) => {
return generateTag(attribute, operatorName, input, type);
toTag: (column, input = null, type = null) => {
return generateTag(column, operatorName, input, type);
},
types: operator.types,
hideInput: operator.hideInput
@@ -259,24 +259,24 @@ export function generateDefaultOperators() {
return operators;
}
export function generateTag(attribute: string, operatorName: string, input = null, type = null) {
export function generateTag(column: string, operatorName: string, input = null, type = null) {
if (input === null) {
return {
value: '',
tag: `**${attribute}** ${operatorName}`
tag: `**${column}** ${operatorName}`
};
} else if (Array.isArray(input) && input.length > 2) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
tag: `**${column}** ${operatorName} **${formatArray(input)}** `
};
} else if (type === ValidTypes.Datetime) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
tag: `**${column}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
};
} else {
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
return { value: input, tag: `**${column}** ${operatorName} **${input}**` };
}
}
+1 -1
View File
@@ -84,4 +84,4 @@ export { default as UsageCard } from './usageCard.svelte';
export { default as ViewToggle } from './viewToggle.svelte';
export { default as RegionEndpoint } from './regionEndpoint.svelte';
export { default as ExpirationInput } from './expirationInput.svelte';
export { default as SortButton } from './sortButton.svelte';
export { default as SortButton, type SortDirection } from './sortButton.svelte';
+2 -2
View File
@@ -11,7 +11,7 @@
export let ui: 'legacy' | 'new' = 'legacy';
export let columns: Writable<Column[]>;
export let view: View;
export let isCustomCollection = false;
export let isCustomTable = false;
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
@@ -19,7 +19,7 @@
</script>
{#if !hideColumns && view === View.Table}
<ColumnSelector {columns} {isCustomCollection} {allowNoColumns} {ui} {showAnyway}>
<ColumnSelector {columns} {isCustomTable} {allowNoColumns} {ui} {showAnyway}>
{#snippet children(toggle, selectedColumnsNumber)}
<Button.Button
size="s"
+51 -9
View File
@@ -38,9 +38,9 @@ export enum Dependencies {
MEMBERSHIPS = 'dependency:memberships',
DATABASE = 'dependency:database',
DATABASES = 'dependency:databases',
COLLECTION = 'dependency:collection',
DOCUMENT = 'dependency:document',
DOCUMENTS = 'dependency:documents',
TABLE = 'dependency:table',
ROW = 'dependency:row',
ROWS = 'dependency:rows',
BUCKET = 'dependency:bucket',
FILE = 'dependency:file',
FILE_TOKENS = 'dependency:file_tokens',
@@ -64,7 +64,7 @@ export enum Dependencies {
WEBHOOK = 'dependency:webhook',
WEBHOOKS = 'dependency:webhooks',
MIGRATIONS = 'dependency:migrations',
COLLECTIONS = 'dependency:collections',
TABLES = 'dependency:tables',
BACKUPS = 'dependency:backups',
RUNTIMES = 'dependency:runtimes',
CONSOLE_VARIABLES = 'dependency:console_variables',
@@ -92,6 +92,8 @@ export const defaultScopes: string[] = [
'teams.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'projects.read',
@@ -111,6 +113,8 @@ export const defaultScopes: string[] = [
'databases.write',
'collections.read',
'collections.write',
'tables.read',
'tables.write',
'buckets.read',
'buckets.write',
'functions.read',
@@ -141,6 +145,8 @@ export const defaultScopes: string[] = [
export const defaultRoles: string[] = ['owner'];
// these are kept for backwards compatibility with keys and events that already exists.
// for the new ones, we use the new terminology.
export const scopes: {
scope: string;
description: string;
@@ -201,6 +207,18 @@ export const scopes: {
category: 'Database',
icon: 'database'
},
{
scope: 'tables.read',
description: "Access to read your project's database tables",
category: 'Database',
icon: 'database'
},
{
scope: 'tables.write',
description: "Access to create, update, and delete your project's database tables",
category: 'Database',
icon: 'database'
},
{
scope: 'attributes.read',
description: "Access to read your project's database collection's attributes",
@@ -214,6 +232,18 @@ export const scopes: {
category: 'Database',
icon: 'database'
},
{
scope: 'columns.read',
description: "Access to read your project's database table's columns",
category: 'Database',
icon: 'database'
},
{
scope: 'columns.write',
description: "Access to create, update, and delete your project's database table's columns",
category: 'Database',
icon: 'database'
},
{
scope: 'indexes.read',
description: "Access to read your project's database collection's indexes",
@@ -239,6 +269,18 @@ export const scopes: {
category: 'Database',
icon: 'database'
},
{
scope: 'rows.read',
description: "Access to read your project's database rows",
category: 'Database',
icon: 'database'
},
{
scope: 'rows.write',
description: "Access to create, update, and delete your project's database rows",
category: 'Database',
icon: 'database'
},
{
scope: 'files.read',
description: "Access to read your project's storage files and preview images",
@@ -430,7 +472,7 @@ export type EventResource = {
export type EventAction = {
name: string;
attributes?: string[];
columns?: string[];
};
export const eventServices: Array<EventService> = [
@@ -448,11 +490,11 @@ export const eventServices: Array<EventService> = [
name: 'databases',
resources: [
{
name: 'collections',
name: 'tables',
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
},
{
name: 'documents',
name: 'rows',
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
}
],
@@ -479,7 +521,7 @@ export const eventServices: Array<EventService> = [
name: 'memberships',
actions: [
{ name: 'create' },
{ name: 'update', attributes: ['status'] },
{ name: 'update', columns: ['status'] },
{ name: 'delete' }
]
}
@@ -495,7 +537,7 @@ export const eventServices: Array<EventService> = [
],
actions: [
{ name: 'create' },
{ name: 'update', attributes: ['email', 'name', 'password', 'status', 'prefs'] },
{ name: 'update', columns: ['email', 'name', 'password', 'status', 'prefs'] },
{ name: 'delete' }
]
},
+16 -22
View File
@@ -1,32 +1,26 @@
import { faker } from '@faker-js/faker';
import type { Attributes } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store';
import type { Columns } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store';
import type { Models } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
export async function generateAttributes(
export async function generateColumns(
project: Models.Project,
databaseId: string,
collectionId: string
): Promise<Attributes[]> {
): Promise<Columns[]> {
const client = sdk.forProject(project.region, project.$id);
return await Promise.all([
client.databases.createStringAttribute(databaseId, collectionId, 'name', 255, false),
client.databases.createEmailAttribute(databaseId, collectionId, 'email', false),
client.databases.createIntegerAttribute(databaseId, collectionId, 'age', false, 0, 150),
client.databases.createStringAttribute(databaseId, collectionId, 'city', 100, false),
client.databases.createStringAttribute(
databaseId,
collectionId,
'description',
1000,
false
),
client.databases.createBooleanAttribute(databaseId, collectionId, 'active', false)
client.grids.createStringColumn(databaseId, collectionId, 'name', 255, false),
client.grids.createEmailColumn(databaseId, collectionId, 'email', false),
client.grids.createIntegerColumn(databaseId, collectionId, 'age', false, 0, 150),
client.grids.createStringColumn(databaseId, collectionId, 'city', 100, false),
client.grids.createStringColumn(databaseId, collectionId, 'description', 1000, false),
client.grids.createBooleanColumn(databaseId, collectionId, 'active', false)
]);
}
export function generateFakeDocuments(columns: Attributes[], count: number): object[] {
export function generateFakeRecords(columns: Columns[], count: number): object[] {
if (count <= 0) return [];
const filteredColumns = columns.filter((col) => col.type !== 'relationship');
@@ -46,7 +40,7 @@ export function generateFakeDocuments(columns: Attributes[], count: number): obj
const document: object = {};
for (const column of filteredColumns) {
document[column.key] = generateValueForAttribute(column);
document[column.key] = generateValueForColumn(column);
}
return document;
@@ -75,23 +69,23 @@ function generateStringValue(key: string, maxLength: number): string {
return text.length <= maxLength ? text : text.substring(0, maxLength);
}
function generateValueForAttribute(attribute: Attributes): string | number | boolean | null {
function generateValueForColumn(attribute: Columns): string | number | boolean | null {
switch (attribute.type) {
case 'string': {
const stringAttr = attribute as Models.AttributeString;
const stringAttr = attribute as Models.ColumnString;
const maxLength = Math.min(stringAttr.size ?? 255, 1000);
return generateStringValue(attribute.key, maxLength);
}
case 'integer': {
const intAttr = attribute as Models.AttributeInteger;
const intAttr = attribute as Models.ColumnInteger;
const min = intAttr.min ?? 0;
const max = intAttr.max ?? 1000000;
return faker.number.int({ min, max });
}
case 'float': {
const floatAttr = attribute as Models.AttributeFloat;
const floatAttr = attribute as Models.ColumnFloat;
const min = floatAttr.min ?? 0;
const max = floatAttr.max ?? 1000000;
const precision = 2;
@@ -121,7 +115,7 @@ function generateValueForAttribute(attribute: Attributes): string | number | boo
}
case 'enum': {
const enumAttr = attribute as Models.AttributeEnum;
const enumAttr = attribute as Models.ColumnEnum;
if (enumAttr.elements && enumAttr.elements.length > 0) {
return faker.helpers.arrayElement(enumAttr.elements);
}
+2
View File
@@ -43,6 +43,8 @@
users: 1,
executions: 1
};
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
const limitedServices = [
{ name: 'bandwidth', value: bandwidth },
{ name: 'documents', value: documents },
+3 -3
View File
@@ -13,14 +13,14 @@
view = $bindable(),
columns,
hideColumns = false,
isCustomCollection = false
isCustomTable = false
}: {
show?: boolean;
hideView?: boolean;
view?: View;
columns?: Writable<Column[]>;
hideColumns?: boolean;
isCustomCollection?: boolean;
isCustomTable?: boolean;
} = $props();
</script>
@@ -35,7 +35,7 @@
{#if !hideColumns && $columns?.length}
<Layout.Stack gap="xs">
<Typography.Text>Columns</Typography.Text>
<ColumnSelector {columns} {isCustomCollection}>
<ColumnSelector {columns} {isCustomTable}>
{#snippet children(toggle, selectedColumnsNumber)}
<Button.Button
size="s"
+2 -8
View File
@@ -8,7 +8,7 @@
export let title: string;
export let columns: Writable<Column[]>;
export let view: View;
export let isCustomCollection = false;
export let isCustomTable = false;
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
@@ -17,13 +17,7 @@
<header class="grid-header">
<Typography.Title size="m">{title}</Typography.Title>
<div class="u-flex u-gap-16 u-contents-mobile">
<ViewSelector
{view}
{columns}
{isCustomCollection}
{hideView}
{hideColumns}
{allowNoColumns} />
<ViewSelector {view} {columns} {isCustomTable} {hideView} {hideColumns} {allowNoColumns} />
<div class="grid-header-col-2">
<slot />
</div>
+3
View File
@@ -366,6 +366,8 @@ export async function checkForUsageLimit(org: Organization) {
return;
}
}
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {};
const resources = [
{ value: bandwidth, name: 'bandwidth' },
@@ -378,6 +380,7 @@ export async function checkForUsageLimit(org: Organization) {
const members = org.total;
const plan = get(currentPlan);
const membersOverflow =
// `plan` can be null on `onboarding/create-organization` route.
// nested null checks needed: GitHub Education plan have empty addons.
members > plan?.addons?.seats?.limit
? members - (plan?.addons?.seats?.limit || members)
+13 -12
View File
@@ -9,7 +9,7 @@ const initialFormData = {
},
databases: {
root: false,
documents: false
rows: false
},
functions: {
root: false,
@@ -45,18 +45,19 @@ export const ResourcesFriendly = {
'environment-variable': { singular: 'Environment Variable', plural: 'Environment Variables' },
deployment: { singular: 'Deployment', plural: 'Deployments' },
database: { singular: 'Database', plural: 'Databases' },
collection: { singular: 'Collection', plural: 'Collections' },
table: { singular: 'Table', plural: 'Tables' },
index: { singular: 'Index', plural: 'Indexes' },
attribute: { singular: 'Attribute', plural: 'Attributes' },
document: { singular: 'Document', plural: 'Documents' }
column: { singular: 'Column', plural: 'Columns' },
row: { singular: 'Row', plural: 'Rows' }
};
const resources = Object.keys(ResourcesFriendly);
type Resource = (typeof resources)[number];
// @todo: @itznotabug - check if other resources are correct and work fine!
export const providerResources: Record<Provider, Resource[]> = {
appwrite: [...resources],
appwrite: [...resources], // new terminology, others are ok?
supabase: [
'user',
'database',
@@ -91,12 +92,12 @@ export const migrationFormToResources = (
}
if (formData.databases.root) {
addResource('database');
addResource('collection');
addResource('attribute');
addResource('index');
addResource('table');
addResource('column');
addResource('columnIndex');
}
if (formData.databases.documents) {
addResource('document');
if (formData.databases.rows) {
addResource('row');
}
if (formData.functions.root) {
addResource('function');
@@ -150,8 +151,8 @@ export const resourcesToMigrationForm = (
if (resources.includes('database')) {
formData.databases.root = true;
}
if (includesAll(resources, ['collection', 'attribute', 'document'])) {
formData.databases.documents = true;
if (includesAll(resources, ['table', 'column', 'row'])) {
formData.databases.rows = true;
}
if (resources.includes('function') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.root = true;
+1
View File
@@ -42,6 +42,7 @@ export type OrganizationList = {
total: number;
};
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
export type BillingLimits = {
bandwidth: number;
documents: number;
+14 -10
View File
@@ -22,9 +22,13 @@ type TeamPreferences = {
type PreferencesStore = {
[key: string]: Preferences;
// kept for backwards compatibility
collections?: {
[key: string]: Preferences['columns'];
};
tables?: {
[key: string]: Preferences['columns'];
};
displayNames?: {
[key: string]: TeamPreferences['names'];
};
@@ -126,8 +130,8 @@ function createPreferences() {
}
);
},
getCustomCollectionColumns: (collectionId: string): Preferences['columns'] => {
return preferences?.collections?.[collectionId] ?? [];
getCustomTableColumns: (tableId: string): Preferences['columns'] => {
return preferences?.tables?.[tableId] ?? preferences?.collections?.[tableId] ?? [];
},
setLimit: (limit: Preferences['limit']) =>
updateAndSync((n) => {
@@ -168,14 +172,14 @@ function createPreferences() {
return n;
}),
setCustomCollectionColumns: (collectionId: string, columns: Preferences['columns']) =>
setCustomTableColumns: (tableId: string, columns: Preferences['columns']) =>
updateAndSync((n) => {
if (!n?.collections?.[collectionId]) {
n ??= {};
n.collections ??= {};
}
n ??= {};
n.tables ??= {};
n.collections[collectionId] = Array.from(new Set(columns));
n.tables[tableId] = Array.from(new Set(columns));
// let's not double save
// n.collections[tableId] = Array.from(new Set(columns));
return n;
}),
loadTeamPrefs: loadTeamPreferences,
@@ -184,7 +188,7 @@ function createPreferences() {
},
setDisplayNames: async (
orgId: string,
collectionId: string,
tableId: string,
names: TeamPreferences['names']
) => {
let teamPrefs: Models.Preferences;
@@ -195,7 +199,7 @@ function createPreferences() {
}
teamPrefs = n;
n.displayNames[collectionId] = names;
n.displayNames[tableId] = names;
return n;
});
+6 -4
View File
@@ -19,13 +19,15 @@ export const canWritePlatforms = derived(scopes, ($scopes) => $scopes.includes('
export const canWriteTargets = derived(scopes, ($scopes) => $scopes.includes('targets.write'));
export const canWriteUsers = derived(scopes, ($scopes) => $scopes.includes('users.write'));
export const canWriteTeams = derived(scopes, ($scopes) => $scopes.includes('teams.write'));
export const canWriteCollections = derived(scopes, ($scopes) =>
$scopes.includes('collections.write')
);
export const canWriteTables = derived(scopes, ($scopes) => {
return $scopes.includes('tables.write') || $scopes.includes('collections.write');
});
export const canWriteSites = derived(scopes, ($scopes) => $scopes.includes('sites.write'));
export const canWriteDomains = derived(scopes, ($scopes) => $scopes.includes('domains.write')); //TODO: check if correct
export const canWriteDocuments = derived(scopes, ($scopes) => $scopes.includes('documents.write'));
export const canWriteRows = derived(scopes, ($scopes) => {
return $scopes.includes('rows.write') || $scopes.includes('documents.write');
});
export const canWriteExecutions = derived(scopes, ($scopes) =>
$scopes.includes('executions.write')
);
+3 -3
View File
@@ -5,7 +5,6 @@ import {
Avatars,
Client,
Console,
Databases,
Functions,
Health,
Locale,
@@ -21,6 +20,7 @@ import {
Vcs,
Sites,
Tokens,
Grids,
Domains
} from '@appwrite.io/console';
import { Billing } from '../sdk/billing';
@@ -108,7 +108,6 @@ const sdkForProject = {
account: new Account(clientProject),
avatars: new Avatars(clientProject),
backups: new Backups(clientProject),
databases: new Databases(clientProject),
functions: new Functions(clientProject),
health: new Health(clientProject),
locale: new Locale(clientProject),
@@ -122,7 +121,8 @@ const sdkForProject = {
vcs: new Vcs(clientProject),
proxy: new Proxy(clientProject),
migrations: new Migrations(clientProject),
sites: new Sites(clientProject)
sites: new Sites(clientProject),
grids: new Grids(clientProject)
};
export const realtime = {
@@ -7,6 +7,7 @@ import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, params, route, depends }) => {
depends(Dependencies.ORGANIZATION);
depends(Dependencies.MEMBERS);
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, PAGE_LIMIT);
@@ -6,6 +6,7 @@
import { capitalize } from '$lib/helpers/string';
import { currentPlan } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
function getResource(id: string) {
id = id.replace('/(console)/project-[region]-[project]/', '');
let parts = id.split('/');
@@ -39,6 +40,7 @@
>{'status' in page.error
? page.error.status || 'Invalid Argument'
: 'Invalid Argument'}</Typography.Title>
<p class="body-text-2 u-bold u-margin-block-start-4">{page.error.message}</p>
<Typography.Title>{page.error.message}</Typography.Title>
{/if}
</Container>
@@ -50,7 +50,7 @@ async function fetchDatabasesAndBackups(
const databases = await sdk
.forProject(params.region, params.project)
.databases.list(
.grids.listDatabases(
[Query.limit(limit), Query.offset(offset), Query.orderDesc('$createdAt')],
search || undefined
);
@@ -81,7 +81,8 @@
const databaseId = id ? id : ID.unique();
const database = await sdk
.forProject(page.params.region, page.params.project)
.databases.create(databaseId, name);
.grids.createDatabase(databaseId, name);
await createPolicies(databaseId);
showCreate = false;
@@ -8,13 +8,13 @@
registerSearchers,
updateCommandGroupRanks
} from '$lib/commandCenter';
import { collectionsSearcher } from '$lib/commandCenter/searchers';
import { tablesSearcher } from '$lib/commandCenter/searchers';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import CreateCollection from './createCollection.svelte';
import CreateTable from './createTable.svelte';
import { showCreate } from './store';
import { CollectionsPanel } from '$lib/commandCenter/panels';
import { canWriteCollections, canWriteDatabases } from '$lib/stores/roles';
import { TablesPanel } from '$lib/commandCenter/panels';
import { canWriteTables, canWriteDatabases } from '$lib/stores/roles';
import { showCreateBackup, showCreatePolicy } from './backups/store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { currentPlan } from '$lib/stores/organization';
@@ -23,17 +23,17 @@
const project = page.params.project;
const databaseId = page.params.database;
async function handleCreate(event: CustomEvent<Models.Collection>) {
async function handleCreate(event: CustomEvent<Models.Table>) {
$showCreate = false;
await invalidate(Dependencies.DATABASE);
await goto(
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/collection-${event.detail.$id}`
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/table-${event.detail.$id}`
);
}
$: $registerCommands([
{
label: 'Create collection',
label: 'Create table',
callback() {
$showCreate = true;
if (!page.url.pathname.endsWith(databaseId)) {
@@ -43,7 +43,7 @@
}
},
keys: page.url.pathname.endsWith(databaseId) ? ['c'] : ['c', 'c'],
disabled: page.url.pathname.includes('collection-') || !$canWriteCollections,
disabled: page.url.pathname.includes('table-') || !$canWriteTables,
group: 'databases',
icon: IconPlus
},
@@ -80,14 +80,14 @@
disabled: !isCloud || !$currentPlan.backupsEnabled
},
{
label: 'Go to collections',
label: 'Go to tables',
callback() {
goto(
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}`
);
},
disabled:
page.url.pathname.endsWith(databaseId) || page.url.pathname.includes('collection-'),
page.url.pathname.endsWith(databaseId) || page.url.pathname.includes('table-'),
keys: ['g', 'c'],
group: 'databases'
},
@@ -98,8 +98,7 @@
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/usage`
);
},
disabled:
page.url.pathname.includes('/usage') || page.url.pathname.includes('collection-'),
disabled: page.url.pathname.includes('/usage') || page.url.pathname.includes('table-'),
keys: ['g', 'u'],
group: 'databases'
},
@@ -111,7 +110,7 @@
);
},
disabled:
page.url.pathname.includes('/backups') || page.url.pathname.includes('collection-'),
page.url.pathname.includes('/backups') || page.url.pathname.includes('table-'),
keys: ['g', 'b'],
group: 'databases'
},
@@ -124,24 +123,24 @@
},
disabled:
page.url.pathname.includes('/settings') ||
page.url.pathname.includes('collection-') ||
page.url.pathname.includes('table-') ||
!$canWriteDatabases,
keys: ['g', 's'],
group: 'databases'
},
{
label: 'Find collections',
label: 'Find tables',
callback: () => {
addSubPanel(CollectionsPanel);
addSubPanel(TablesPanel);
},
group: 'databases',
rank: -1
}
]);
$registerSearchers(collectionsSearcher);
$registerSearchers(tablesSearcher);
$: $updateCommandGroupRanks({ collections: 10 });
$: $updateCommandGroupRanks({ tables: 10 });
</script>
<svelte:head>
@@ -150,4 +149,4 @@
<slot />
<CreateCollection bind:showCreate={$showCreate} on:created={handleCreate} />
<CreateTable bind:showCreate={$showCreate} on:created={handleCreate} />
@@ -12,6 +12,8 @@ export const load: LayoutLoad = async ({ params, depends }) => {
header: Header,
breadcrumbs: Breadcrumbs,
subNavigation: SubNavigation,
database: await sdk.forProject(params.region, params.project).databases.get(params.database)
database: await sdk
.forProject(params.region, params.project)
.grids.getDatabase(params.database)
};
};
@@ -9,13 +9,13 @@
import { Card, Empty } from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
export let data: PageData;
</script>
<Container expanded slotSpacing paddingInlineEnd={false}>
{#if data.collections.total}
{#if data.tables.total}
{#if data.view === 'grid'}
<Grid {data} bind:showCreate={$showCreate} />
{:else}
@@ -23,10 +23,10 @@
{/if}
<PaginationWithLimit
name="Collections"
name="Tables"
limit={data.limit}
offset={data.offset}
total={data.collections.total} />
total={data.tables.total} />
{:else}
<Card.Base padding="none">
<Empty
@@ -38,19 +38,16 @@
Create, organize, and query structured data with Tables.
</span>
<span slot="actions">
<Button
external
text
event="empty_documentation"
ariaLabel={`create collection`}>Documentation</Button>
<Button external text event="empty_documentation" ariaLabel={`create table`}
>Documentation</Button>
{#if $canWriteCollections}
{#if $canWriteTables}
<Button
secondary
on:click={() => {
$showCreate = true;
}}>
Create collection
Create table
</Button>
{/if}
</span>
@@ -5,15 +5,16 @@ import type { PageLoad } from './$types';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
export const load: PageLoad = async ({ params, url, route, depends }) => {
depends(Dependencies.COLLECTIONS);
depends(Dependencies.TABLES);
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, CARD_LIMIT);
const view = getView(url, route, View.Grid);
const offset = pageToOffset(page, limit);
const collections = await sdk
const tables = await sdk
.forProject(params.region, params.project)
.databases.listCollections(
.grids.listTables(
params.database,
[Query.limit(limit), Query.offset(offset), Query.orderDesc('')],
search || undefined
@@ -24,6 +25,6 @@ export const load: PageLoad = async ({ params, url, route, depends }) => {
limit,
search,
view,
collections
tables
};
};
@@ -1,149 +0,0 @@
<script lang="ts">
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { type Writable, writable } from 'svelte/store';
import AttributeForm from './document-[document]/attributeForm.svelte';
import { Permissions } from '$lib/components/permissions';
import type { Attributes } from './store';
import { ID, type Models } from '@appwrite.io/console';
import { Alert, Layout, Typography } from '@appwrite.io/pink-svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import SideSheet from './layout/sidesheet.svelte';
let {
collection,
showSheet = $bindable(false),
existingData = $bindable(null)
}: {
showSheet: boolean;
collection: Models.Collection;
existingData: Models.Document | null;
} = $props();
let isSubmitting = $state(false);
type CreateDocument = {
id?: string;
document: object;
permissions: string[];
attributes: Attributes[];
};
function createDocumentWritable() {
const availableAttributes = collection.attributes.filter((a) => a.status === 'available');
const initial = {
id: null,
document: availableAttributes.reduce((acc, attr) => {
if (existingData && existingData[attr.key]) {
acc[attr.key] = attr.array
? Array.isArray(existingData[attr.key])
? [...existingData[attr.key]]
: []
: existingData[attr.key];
} else {
acc[attr.key] = attr.array ? [] : null;
}
return acc;
}, {}),
permissions: [],
attributes: availableAttributes
};
return writable<CreateDocument>({ ...initial });
}
let createDocument: Writable<CreateDocument> = $state<Writable<CreateDocument> | null>(null);
$effect(() => {
if (showSheet) {
createDocument = createDocumentWritable();
}
});
async function create() {
isSubmitting = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createDocument(
page.params.database,
page.params.collection,
$createDocument.id ?? ID.unique(),
$createDocument.document,
$createDocument.permissions
);
showSheet = false;
addNotification({
message: 'Document has been created',
type: 'success'
});
invalidate(Dependencies.DOCUMENTS);
trackEvent(Submit.DocumentCreate, {
customId: !!$createDocument.id
});
// post op clear.
existingData = null;
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DocumentCreate);
} finally {
isSubmitting = false;
}
}
</script>
{#if $createDocument}
<div class="sheet-container">
<!-- TODO: add a ID badge-->
<SideSheet
spaced
title={`${existingData ? 'Duplicate' : 'Create'} record`}
bind:show={showSheet}
closeOnBlur={false}
submit={{
text: 'Create',
disabled: isSubmitting,
onClick: () => create()
}}>
<Layout.Stack gap="xxl">
<AttributeForm
bind:formValues={$createDocument.document}
attributes={$createDocument.attributes}
bind:customId={$createDocument.id} />
<Layout.Stack gap="xl">
<Typography.Text>
Choose which permission scopes to grant your application. It is best
practice to allow only the permissions you need to meet your project goals.
</Typography.Text>
{#if collection.documentSecurity}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is enabled</svelte:fragment>
Users will be able to access this document if they have been granted
<b>either document or collection permissions</b>.
</Alert.Inline>
<Permissions bind:permissions={$createDocument.permissions} />
{:else}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is disabled</svelte:fragment>
If you want to assign document permissions, navigate to Collection settings
and enable document security. Otherwise, only collection permissions will
be used.
</Alert.Inline>
{/if}
</Layout.Stack>
</Layout.Stack>
</SideSheet>
</div>
{/if}
@@ -1,63 +0,0 @@
<script lang="ts">
import type { Models } from '@appwrite.io/console';
import Boolean from './attributes/boolean.svelte';
import Datetime from './attributes/datetime.svelte';
import Enum from './attributes/enum.svelte';
import Integer from './attributes/integer.svelte';
import Relationship from './attributes/relationship.svelte';
import String from './attributes/string.svelte';
import Url from './attributes/url.svelte';
export let id: string;
export let label: string;
export let optionalText: string | undefined = undefined;
export let value: string | number | boolean | null | string[];
export let editing = false;
export let attribute:
| Models.AttributeBoolean
| Models.AttributeEmail
| Models.AttributeEnum
| Models.AttributeFloat
| Models.AttributeInteger
| Models.AttributeIp
| Models.AttributeString
| Models.AttributeDatetime
| Models.AttributeUrl;
const attributesTypeMap = {
string: String,
integer: Integer,
double: Integer,
boolean: Boolean,
datetime: Datetime,
relationship: Relationship
};
const attributesFormatMap = {
ip: String,
url: Url,
email: String,
enum: Enum
};
</script>
{#if attribute.type in attributesTypeMap}
{#if 'format' in attribute && attribute.format}
<svelte:component
this={attributesFormatMap[attribute.format]}
{id}
{label}
{attribute}
{optionalText}
bind:value />
{:else}
<svelte:component
this={attributesTypeMap[attribute.type]}
{id}
{editing}
{label}
{attribute}
{optionalText}
bind:value />
{/if}
{/if}
@@ -1,37 +0,0 @@
import { type Models, Query } from '@appwrite.io/console';
import type { Attributes } from '../../store';
export function isRelationshipToMany(attribute: Models.AttributeRelationship) {
if (!attribute) return false;
if (!attribute?.relationType) return false;
if (attribute?.side === 'child') {
return !['oneToOne', 'oneToMany'].includes(attribute?.relationType);
} else {
return !['oneToOne', 'manyToOne'].includes(attribute?.relationType);
}
}
export function isRelationship(attribute: Attributes): attribute is Models.AttributeRelationship {
if (!attribute) return false;
return attribute?.type === 'relationship';
}
export function isString(attribute: Attributes): attribute is Models.AttributeString {
if (!attribute) return false;
return attribute?.type === 'string';
}
/**
* Returns select queries for all main and related fields in a collection.
*/
export function buildWildcardAttributesQuery(
collection: Models.Collection | null = null
): string[] {
return [
...(collection?.attributes
?.filter((attr) => isRelationship(attr))
?.map((attr) => Query.select([`${attr.key}.*`])) ?? []),
Query.select(['*'])
];
}
@@ -1,132 +0,0 @@
<script lang="ts">
import { symmetricDifference } from '$lib/helpers/array';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { type Writable, writable } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { invalidate } from '$app/navigation';
import { collection, type Attributes } from './store';
import AttributeItem from './document-[document]/attributeItem.svelte';
import { isRelationship, isRelationshipToMany } from './document-[document]/attributes/store';
import { deepClone } from '$lib/helpers/object';
const databaseId = page.params.database;
const collectionId = page.params.collection;
let {
document = $bindable()
}: {
document: Models.Document;
} = $props();
let work = $state<Writable<Models.Document> | null>(null);
function initWork() {
const prohibitedKeys = [
'$id',
'$collection',
'$collectionId',
'$databaseId',
'$createdAt',
'$updatedAt'
];
const filteredKeys = Object.keys(document).filter((key) => {
return !prohibitedKeys.includes(key);
});
const result = filteredKeys.reduce((obj, key) => {
obj[key] = document[key];
return obj;
}, {});
return writable(deepClone(result as Models.Document));
}
$effect(() => {
if (document) work = initWork();
});
function compareAttributes(
attribute: Attributes,
$work: Models.Document,
$doc: Models.Document
) {
if (!attribute) {
return false;
}
const workAttribute = $work?.[attribute.key];
const docAttribute = $doc?.[attribute.key];
if (attribute.array) {
return !symmetricDifference(Array.from(workAttribute), Array.from(docAttribute)).length;
}
if (isRelationship(attribute)) {
if (isRelationshipToMany(attribute as Models.AttributeRelationship)) {
const workIds = workAttribute.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
const relatedIds = docAttribute.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
return !symmetricDifference(workIds, relatedIds).length;
} else {
const workId =
typeof workAttribute === 'string' ? workAttribute : workAttribute?.$id;
const relatedId =
typeof docAttribute === 'string' ? docAttribute : docAttribute?.$id;
return workId === relatedId;
}
}
return workAttribute === docAttribute;
}
export async function update() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateDocument(
databaseId,
collectionId,
document.$id,
$work,
$work.$permissions
);
invalidate(Dependencies.DOCUMENT);
trackEvent(Submit.DocumentUpdate);
addNotification({
message: 'Document has been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DocumentUpdate);
}
}
export function isDisabled(): boolean {
if (!work || !$collection?.attributes?.length) return true;
return $collection.attributes.every((attribute) =>
compareAttributes(attribute, $work, document)
);
}
</script>
{#if $collection?.attributes?.length && work}
{#each $collection.attributes as attribute}
{@const label = attribute.key}
<AttributeItem {attribute} bind:formValues={$work} {label} editing />
{/each}
{/if}
@@ -1,72 +0,0 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, InputSwitch } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { collection } from '../store';
const databaseId = page.params.database;
let collectionDocumentSecurity: boolean = null;
onMount(() => {
collectionDocumentSecurity ??= $collection.documentSecurity;
});
async function updateSecurity() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateCollection(
databaseId,
$collection.$id,
$collection.name,
$collection.$permissions,
collectionDocumentSecurity,
$collection.enabled
);
await invalidate(Dependencies.COLLECTION);
addNotification({
message: 'Security has been updated',
type: 'success'
});
trackEvent(Submit.CollectionUpdateSecurity);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.CollectionUpdateSecurity);
}
}
</script>
<CardGrid>
<svelte:fragment slot="title">Document security</svelte:fragment>
<svelte:fragment slot="aside">
<InputSwitch
bind:value={collectionDocumentSecurity}
id="security"
label="Document security" />
<p class="text">
When document security is enabled, users will be able to access documents for which they
have been granted <b>either document or collection permissions</b>.
</p>
<p class="text">
If document security is disabled, users can access documents <b
>only if they have collection permissions</b
>. Document permissions will be ignored.
</p>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={collectionDocumentSecurity === $collection.documentSecurity}
on:click={updateSecurity}>Update</Button>
</svelte:fragment>
</CardGrid>
@@ -26,24 +26,24 @@
const create = async () => {
error = null;
try {
const collection = await sdk
const table = await sdk
.forProject(page.params.region, page.params.project)
.databases.createCollection(databaseId, id ? id : ID.unique(), name);
.grids.createTable(databaseId, id ? id : ID.unique(), name);
showCreate = false;
dispatch('created', collection);
dispatch('created', table);
addNotification({
type: 'success',
message: `${name} has been created`
});
name = id = null;
trackEvent(Submit.CollectionCreate, {
trackEvent(Submit.TableCreate, {
customId: !!id
});
} catch (e) {
error = e.message;
trackError(e, Submit.CollectionCreate);
trackError(e, Submit.TableCreate);
}
};
@@ -72,16 +72,18 @@
});
</script>
<Modal title="Create collection" size="m" bind:show={showCreate} onSubmit={create} bind:error>
<Modal title="Create table" size="m" bind:show={showCreate} onSubmit={create} bind:error>
<InputText
id="name"
label="Name"
placeholder="Enter collection name"
placeholder="Enter table name"
bind:value={name}
autofocus
required
on:input={() => {
if (!touchedId) id = toIdFormat(name);
if (!touchedId) {
id = toIdFormat(name);
}
}} />
<CustomId
@@ -18,17 +18,17 @@
let confirmedDeletion = false;
let error = null;
let isLoadingDocumentsCount = false;
let isLoadingRowsCount = false;
$: collectionItems = [];
let collections: Models.CollectionList = null;
$: tableItems = [];
let tables: Models.TableList = null;
function buildQueries(): string[] {
const queries = [Query.orderDesc('$updatedAt')];
if (collectionItems.length > 0) {
if (tableItems.length > 0) {
queries.push(Query.limit(25));
queries.push(Query.offset(collectionItems.length));
queries.push(Query.offset(tableItems.length));
} else {
queries.push(Query.limit(3));
}
@@ -36,32 +36,32 @@
return queries;
}
async function listCollections() {
async function listTables() {
// let's just wait...
if (isLoadingDocumentsCount) return;
if (isLoadingRowsCount) return;
isLoadingDocumentsCount = true;
isLoadingRowsCount = true;
try {
const queries = buildQueries();
collections = await sdk
tables = await sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections(databaseId, queries);
.grids.listTables(databaseId, queries);
const collectionPromises = collections.collections.map(async (collection) => {
const tablePromises = tables.tables.map(async (table) => {
return {
id: collection.$id,
name: collection.name,
updatedAt: collection.$updatedAt
id: table.$id,
name: table.name,
updatedAt: table.$updatedAt
};
});
collectionItems = [...collectionItems, ...(await Promise.all(collectionPromises))];
tableItems = [...tableItems, ...(await Promise.all(tablePromises))];
} catch (err) {
error = true;
} finally {
isLoadingDocumentsCount = false;
isLoadingRowsCount = false;
}
}
@@ -69,7 +69,7 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.delete(databaseId);
.grids.deleteDatabase(databaseId);
showDelete = false;
addNotification({
type: 'success',
@@ -88,24 +88,24 @@
/* reset data on modal close */
$: if (!showDelete) {
collections = null;
collectionItems = [];
tables = null;
tableItems = [];
} else {
listCollections();
listTables();
}
</script>
<Modal title="Delete database" bind:show={showDelete} onSubmit={handleDelete}>
<p class="text" slot="description">
{#if collectionItems.length > 0}
The following collections and all data associated with <b>{$database.name}</b>, will be
{#if tableItems.length > 0}
The following tables and all data associated with <b>{$database.name}</b>, will be
permanently deleted.
{:else}
Are you sure you want to delete <b>{$database.name}</b>?
{/if}
</p>
{#if isLoadingDocumentsCount}
{#if isLoadingRowsCount}
<div class="u-flex u-main-center">
<Spinner />
</div>
@@ -113,36 +113,36 @@
<p class="text">
Are you sure you want to delete <b>{$database.name}</b>?
</p>
{:else if collectionItems.length > 0}
{:else if tableItems.length > 0}
<div class="u-flex-vertical u-gap-16">
<Table.Root columns={2} let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Collection</Table.Header.Cell>
<Table.Header.Cell {root}>Table</Table.Header.Cell>
<Table.Header.Cell {root}>Last Updated</Table.Header.Cell>
</svelte:fragment>
{#each collectionItems as collection}
{#each tableItems as table}
<Table.Row.Base {root}>
<Table.Cell {root}>{collection.name}</Table.Cell>
<Table.Cell {root}>{toLocaleDate(collection.updatedAt)}</Table.Cell>
<Table.Cell {root}>{table.name}</Table.Cell>
<Table.Cell {root}>{toLocaleDate(table.updatedAt)}</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{#if collectionItems.length < collections.total}
{#if tableItems.length < tables.total}
<div class="u-flex u-gap-16 u-cross-center">
<button class="u-underline" on:click={listCollections} type="button">
<button class="u-underline" on:click={listTables} type="button">
Show more
</button>
{#if isLoadingDocumentsCount}
{#if isLoadingRowsCount}
<div class="loader is-small"></div>
{/if}
</div>
{:else if collectionItems.length > 25}
{:else if tableItems.length > 25}
<button
class="u-underline"
on:click={() => {
collectionItems = collectionItems.slice(0, 3);
tableItems = tableItems.slice(0, 3);
}}
type="button">
Show less
@@ -151,7 +151,7 @@
</div>
{/if}
{#if !isLoadingDocumentsCount}
{#if !isLoadingRowsCount}
<p class="text" data-private>
<b>
Once deleted, this database and its backups cannot be restored. This action is
@@ -162,7 +162,7 @@
<div class="input-check-box-friction">
<InputCheckbox
required
id="delete_policy"
id="delete_database"
bind:checked={confirmedDeletion}
label="I understand and confirm" />
</div>
@@ -2,9 +2,9 @@
import { base } from '$app/paths';
import { page } from '$app/state';
import { CardContainer, GridItem1, Id } from '$lib/components';
import { Pill } from '$lib/elements';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
import type { PageData } from './$types';
import { Badge } from '@appwrite.io/pink-svelte';
export let data: PageData;
export let showCreate = false;
const projectId = page.params.project;
@@ -12,20 +12,20 @@
</script>
<CardContainer
disableEmpty={!$canWriteCollections}
total={data.collections.total}
disableEmpty={!$canWriteTables}
total={data.tables.total}
on:click={() => (showCreate = true)}
event="collection">
{#each data.collections.collections as collection}
event="table">
{#each data.tables.tables as table}
<GridItem1
href={`${base}/project-${page.params.region}-${projectId}/databases/database-${databaseId}/collection-${collection.$id}`}>
<svelte:fragment slot="title">{collection.name}</svelte:fragment>
href={`${base}/project-${page.params.region}-${projectId}/databases/database-${databaseId}/table-${table.$id}`}>
<svelte:fragment slot="title">{table.name}</svelte:fragment>
<svelte:fragment slot="status">
{#if !collection.enabled}
<Pill>disabled</Pill>
{#if !table.enabled}
<Badge variant="secondary" content="disabled" />
{/if}</svelte:fragment>
<Id value={collection.$id}>{collection.$id}</Id>
<Id value={table.$id}>{table.$id}</Id>
</GridItem1>
{/each}
</CardContainer>
@@ -13,8 +13,8 @@
const tabs = [
{
href: path,
title: 'Collections',
event: 'collections',
title: 'Tables',
event: 'tables',
hasChildren: true
},
{
@@ -13,6 +13,7 @@
import Delete from '../delete.svelte';
import { database } from '../store';
import { Query } from '@appwrite.io/console';
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
let showDelete = false;
let showError: false | 'name' | 'email' | 'password' = false;
@@ -24,10 +25,10 @@
databaseName ??= $database.name;
});
async function loadCollectionCount() {
async function loadTableCount() {
const { total } = await sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections($database.$id, [Query.limit(1)]);
.grids.listTables($database.$id, [Query.limit(1)]);
return total;
}
@@ -41,7 +42,7 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.update(page.params.database, databaseName);
.grids.updateDatabase(page.params.database, databaseName);
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Name has been updated',
@@ -95,21 +96,21 @@
<CardGrid>
<svelte:fragment slot="title">Delete database</svelte:fragment>
The database will be permanently deleted, including all collections within it. This action
is irreversible.
The database will be permanently deleted, including all tables within it. This action is
irreversible.
<svelte:fragment slot="aside">
<BoxAvatar>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$database.name}</h6>
<span class="u-flex u-gap-8">
{#await loadCollectionCount()}
<div class="loader is-small"></div>
{:then count}
{count}
{/await}
Collections
</span>
<Layout.Stack direction="column" gap="xxs">
<h6 class="u-bold u-trim-1">{$database.name}</h6>
<Layout.Stack direction="row" gap="s">
{#await loadTableCount()}
<Skeleton variant="line" width="100%" height={19.5} />
{:then count}
{count} {count === 1 ? 'Table' : 'Tables'}
{/await}
</Layout.Stack>
</Layout.Stack>
</svelte:fragment>
</BoxAvatar>
</svelte:fragment>
@@ -6,8 +6,8 @@ import { derived, writable } from 'svelte/store';
export const database = derived(page, ($page) => $page.data.database as Models.Database);
export const showCreate = writable(false);
export const columns = writable<Column[]>([
{ id: '$id', title: 'Collection ID', type: 'string', width: 200 },
export const tableViewColumns = writable<Column[]>([
{ id: '$id', title: 'Table ID', type: 'string', width: 200 },
{ id: 'name', title: 'Name', type: 'string', width: { min: 120 } },
{ id: '$createdAt', title: 'Created', type: 'datetime', width: { min: 120 } },
{ id: '$updatedAt', title: 'Updated', type: 'datetime', width: { min: 120 } }
@@ -5,15 +5,15 @@
export async function submitString(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeString>
data: Partial<Models.ColumnString>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createStringAttribute(
.grids.createStringColumn(
databaseId,
collectionId,
tableId,
key,
data.size,
data.required,
@@ -23,15 +23,15 @@
}
export async function updateString(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeString>,
tableId: string,
data: Partial<Models.ColumnString>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateStringAttribute(
.grids.updateStringColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -42,10 +42,10 @@
</script>
<script lang="ts">
import { InputChoice, InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
import { createConservative } from '$lib/helpers/stores';
import { InputChoice, InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
export let data: Partial<Models.AttributeString> = {
export let data: Partial<Models.ColumnString> = {
required: false,
size: 0,
default: null,
@@ -67,7 +67,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeString>>({
} = createConservative<Partial<Models.ColumnString>>({
required: false,
array: false,
...data
@@ -104,9 +104,9 @@
bind:value={data.default} />
{/if}
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
Indicate whether this is a required column
</InputChoice>
<InputChoice id="array" label="Array" bind:value={data.array} disabled={data.required || editing}>
Indicate whether this attribute should act as an array, with the default value set as an empty
Indicate whether this column should act as an array, with the default value set as an empty
array.
</InputChoice>
@@ -17,19 +17,20 @@
import type { Models } from '@appwrite.io/console';
let data = $derived(page.data) as PageData;
let region = $derived(page.params.region);
let project = $derived(page.params.project);
let tableId = $derived(page.params.table);
let databaseId = $derived(page.params.database);
let collectionId = $derived(page.params.collection);
const collections = $derived(data.collections);
const tables = $derived(data.tables);
const sortedCollections = $derived.by(() =>
collections?.collections?.slice().sort((a, b) => a.name.localeCompare(b.name))
const sortedTables = $derived.by(() =>
tables?.tables?.slice().sort((a, b) => a.name.localeCompare(b.name))
);
const selectedCollection = $derived.by(() =>
sortedCollections?.find((collection: Models.Collection) => collection.$id === collectionId)
const selectedTable = $derived.by(() =>
sortedTables?.find((table: Models.Table) => table.$id === tableId)
);
let openBottomSheet = $state(false);
@@ -53,11 +54,11 @@
{data.database?.name}
</a>
<div class="collection-content">
{#if collections?.total}
{#if tables?.total}
<ul class="drop-list u-margin-inline-start-8 u-margin-block-start-8">
{#each sortedCollections as collection}
{@const href = `${base}/project-${region}-${project}/databases/database-${databaseId}/collection-${collection.$id}`}
{@const isSelected = collectionId === collection.$id}
{#each sortedTables as table}
{@const href = `${base}/project-${region}-${project}/databases/database-${databaseId}/table-${table.$id}`}
{@const isSelected = tableId === table.$id}
<Layout.Stack gap="m" direction="row" alignItems="center">
<div
@@ -76,7 +77,7 @@
? '--fgcolor-neutral-tertiary'
: '--fgcolor-neutral-weak'} />
<span class="text collection-name" data-private
>{collection.name}</span>
>{table.name}</span>
</a>
</li>
</Layout.Stack>
@@ -106,7 +107,7 @@
$showCreate = true;
$showSubNavigation = false;
}}>
Create collection
Create table
</Button>
</Layout.Stack>
</div>
@@ -128,7 +129,7 @@
onclick={() => {
openBottomSheet = !openBottomSheet;
}}>
<span class="orgName">{selectedCollection?.name}</span>
<span class="orgName">{selectedTable?.name}</span>
<Icon icon={IconChevronDown} size="s" />
</button>
</Layout.Stack>
@@ -140,20 +141,20 @@
bind:isOpen={openBottomSheet}
menu={{
top: {
items: sortedCollections.slice(0, 10).map((collection) => {
items: sortedTables.slice(0, 10).map((table) => {
return {
name: collection.name,
name: table.name,
leadingIcon: IconTable,
href: `${base}/project-${region}-${project}/databases/database-${databaseId}/collection-${collection.$id}`
href: `${base}/project-${region}-${project}/databases/database-${databaseId}/table-${table.$id}`
};
})
},
bottom:
sortedCollections.length > 10
sortedTables.length > 10
? {
items: [
{
name: 'All collections',
name: 'All tables',
leadingIcon: IconTable,
href: `${base}/project-${region}-${project}/databases/database-${databaseId}`
}
@@ -1,13 +1,14 @@
<script lang="ts" context="module">
import { writable } from 'svelte/store';
import type { Option } from './columns/store';
const createAttributeArgs = writable({
const createColumnArgs = writable({
showCreate: false,
selectedOption: null as Option['name'] | null
});
export const initCreateAttribute = (option: Option['name']) => {
createAttributeArgs.set({ showCreate: true, selectedOption: option });
export const initCreateColumn = (option: Option['name']) => {
createColumnArgs.set({ showCreate: true, selectedOption: option });
};
const showCreateIndex = writable(false);
@@ -22,7 +23,7 @@
import { realtime, sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import {
collection,
table,
columnsOrder,
databaseColumnSheetOptions,
databaseRowSheetOptions,
@@ -32,28 +33,27 @@
spreadsheetLoading
} from './store';
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
import CreateAttribute from './createAttribute.svelte';
import type { Option } from './attributes/store';
import { CreateAttributePanel } from '$lib/commandCenter/panels';
import CreateColumn from './createColumn.svelte';
import { CreateColumnPanel } from '$lib/commandCenter/panels';
import { database } from '../store';
import { project } from '../../../store';
import { page } from '$app/state';
import { base } from '$app/paths';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
import { IconEye, IconLockClosed, IconPlus, IconPuzzle } from '@appwrite.io/pink-icons-svelte';
import SideSheet from './layout/sidesheet.svelte';
import EditDocument from './editDocument.svelte';
import EditAttribute from './attributes/edit.svelte';
import EditRow from './editRow.svelte';
import EditColumn from './columns/edit.svelte';
import { Dialog, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Button, Seekbar } from '$lib/elements/forms';
import { generateFakeDocuments, generateAttributes } from '$lib/helpers/faker';
import { generateFakeRecords, generateColumns } from '$lib/helpers/faker';
import { addNotification } from '$lib/stores/notifications';
import { sleep } from '$lib/helpers/promises';
import CreateIndex from './indexes/createIndex.svelte';
let createIndex: CreateIndex;
let editDocument: EditDocument;
let createAttribute: CreateAttribute;
let editRow: EditRow;
let createColumn: CreateColumn;
let selectedOption: Option['name'] = 'String';
onMount(() => {
@@ -61,142 +61,143 @@
.forProject(page.params.region, page.params.project)
.subscribe(['project', 'console'], (response) => {
if (
response.events.includes('databases.*.collections.*.attributes.*') ||
response.events.includes('databases.*.collections.*.indexes.*')
response.events.includes('databases.*.tables.*.columns.*') ||
response.events.includes('databases.*.tables.*.indexes.*')
) {
invalidate(Dependencies.COLLECTION);
invalidate(Dependencies.TABLE);
}
});
});
// TODO: use route ids instead of pathname
$: $registerCommands([
{
label: 'Create document',
keys: page.url.pathname.endsWith($collection.$id) ? ['c'] : ['c', 'd'],
label: 'Create row',
keys: page.url.pathname.endsWith($table.$id) ? ['t'] : ['t', 'd'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/create`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/create`
);
},
icon: IconPlus,
group: 'documents'
group: 'rows'
},
{
label: 'Create attribute',
keys: page.url.pathname.endsWith('attributes') ? ['c'] : ['c', 'a'],
label: 'Create column',
keys: page.url.pathname.endsWith('columns') ? ['t'] : ['t', 'a'],
callback() {
addSubPanel(CreateAttributePanel);
addSubPanel(CreateColumnPanel);
},
icon: IconPlus,
group: 'attributes',
disabled: !$canWriteCollections
group: 'columns',
disabled: !$canWriteTables
},
{
label: 'Go to documents',
label: 'Go to rows',
keys: ['g', 'd'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}`
);
},
disabled: page.url.pathname.endsWith($collection.$id),
group: 'collections'
disabled: page.url.pathname.endsWith($table.$id),
group: 'tables'
},
{
label: 'Go to attributes',
label: 'Go to columns',
keys: ['g', 'a'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/attributes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/columns`
);
},
disabled: page.url.pathname.endsWith('attributes'),
group: 'collections'
disabled: page.url.pathname.endsWith('columns'),
group: 'tables'
},
{
label: 'Go to indexes',
keys: ['g', 'i'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/indexes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/indexes`
);
},
disabled: page.url.pathname.endsWith('indexes'),
group: 'collections'
group: 'tables'
},
{
label: 'Go to activity',
keys: ['g', 'c'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/activity`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/activity`
);
},
disabled: page.url.pathname.endsWith('activity'),
group: 'collections'
group: 'tables'
},
{
label: 'Go to usage',
keys: ['g', 'u'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/usage`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/usage`
);
},
disabled: page.url.pathname.endsWith('usage'),
group: 'collections'
group: 'tables'
},
{
label: 'Go to settings',
keys: ['g', 's'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/settings`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/settings`
);
},
disabled: page.url.pathname.endsWith('settings') || !$canWriteCollections,
group: 'collections'
disabled: page.url.pathname.endsWith('settings') || !$canWriteTables,
group: 'tables'
},
{
label: 'Display Name',
async callback() {
await goto(
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/collection-${$collection.$id}/settings#display-name`
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/table-${$table.$id}/settings#display-name`
);
},
group: 'collections',
group: 'tables',
disabled:
page.url.pathname.endsWith('display-name') ||
page.url.pathname.endsWith('settings') ||
!$canWriteCollections,
!$canWriteTables,
icon: IconEye
},
{
label: 'Permissions',
async callback() {
await goto(
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/collection-${$collection.$id}/settings#permissions`
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/table-${$table.$id}/settings#permissions`
);
},
group: 'collections',
group: 'tables',
disabled:
page.url.pathname.endsWith('permissions') ||
page.url.pathname.endsWith('settings') ||
!$canWriteCollections,
!$canWriteTables,
icon: IconPuzzle
},
{
label: 'Document security',
label: 'Row security',
async callback() {
await goto(
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/collection-${$collection.$id}/settings#document-security`
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/table-${$table.$id}/settings#row-security`
);
},
group: 'collections',
group: 'tables',
disabled:
page.url.pathname.endsWith('document-security') ||
page.url.pathname.endsWith('row-security') ||
page.url.pathname.endsWith('settings') ||
!$canWriteCollections,
!$canWriteTables,
icon: IconLockClosed
},
{
@@ -207,14 +208,14 @@
},
icon: IconPlus,
group: 'indexes',
disabled: !$canWriteCollections
disabled: !$canWriteTables
}
]);
$updateCommandGroupRanks({
attributes: 1000,
documents: 900,
collections: 800,
columns: 1000,
rows: 900,
tables: 800,
indexes: 700
});
@@ -222,16 +223,16 @@
$spreadsheetLoading = true;
$randomDataModalState.show = false;
let attributes = $collection.attributes;
let attributes = $table.columns;
if (!attributes.length) {
try {
attributes = await generateAttributes(
attributes = await generateColumns(
$project,
page.params.database,
page.params.collection
page.params.table
);
await invalidate(Dependencies.COLLECTION);
await invalidate(Dependencies.TABLE);
} catch (e) {
addNotification({
type: 'error',
@@ -246,17 +247,17 @@
await sleep(1250);
try {
const documents = generateFakeDocuments(attributes, $randomDataModalState.value);
const records = generateFakeRecords(attributes, $randomDataModalState.value);
await sdk
.forProject(page.params.region, page.params.project)
.databases.createDocuments(page.params.database, page.params.collection, documents);
.grids.createRows(page.params.database, page.params.table, records);
addNotification({
type: 'success',
message: 'Random data added successfully'
});
await invalidate(Dependencies.DOCUMENTS);
await invalidate(Dependencies.ROWS);
} catch (e) {
addNotification({
type: 'error',
@@ -271,7 +272,7 @@
</script>
<svelte:head>
<title>{$collection?.name ?? 'Collection'} - Appwrite</title>
<title>{$table?.name ?? 'Table'} - Appwrite</title>
</svelte:head>
<slot />
@@ -282,13 +283,13 @@
submit={{
text: 'Insert',
onClick: async () => {
await createAttribute?.submit();
await createColumn?.submit();
},
disabled: !selectedOption
}}>
<CreateAttribute
<CreateColumn
bind:selectedOption
bind:this={createAttribute}
bind:this={createColumn}
column={$showCreateAttributeSheet.column}
showCreate={$showCreateAttributeSheet.show}
direction={$showCreateAttributeSheet.direction}
@@ -308,10 +309,10 @@
disabled: $databaseColumnSheetOptions.disableSubmit,
onClick: () => $databaseColumnSheetOptions.submitAction()
}}>
<EditAttribute
<EditColumn
isModal={false}
showEdit={$databaseColumnSheetOptions.isEdit}
selectedAttribute={$databaseColumnSheetOptions.column} />
selectedColumn={$databaseColumnSheetOptions.column} />
</SideSheet>
<SideSheet
@@ -319,10 +320,10 @@
bind:show={$databaseRowSheetOptions.show}
submit={{
text: 'Update',
disabled: editDocument?.isDisabled(),
onClick: async () => await editDocument?.update()
disabled: editRow?.isDisabled(),
onClick: async () => await editRow?.update()
}}>
<EditDocument bind:document={$databaseRowSheetOptions.document} bind:this={editDocument} />
<EditRow bind:row={$databaseRowSheetOptions.row} bind:this={editRow} />
</SideSheet>
<SideSheet
@@ -335,7 +336,7 @@
<CreateIndex
bind:this={createIndex}
bind:showCreateIndex={$showCreateIndexSheet.show}
externalAttributeKey={$showCreateIndexSheet.column} />
externalColumnKey={$showCreateIndexSheet.column} />
</SideSheet>
<Dialog title="Generate random data" bind:open={$randomDataModalState.show}>
@@ -6,21 +6,20 @@ import Header from './header.svelte';
import { Query } from '@appwrite.io/console';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.COLLECTION);
depends(Dependencies.TABLE);
const [table, tables] = await Promise.all([
sdk.forProject(params.region, params.project).grids.getTable(params.database, params.table),
const [collection, collections] = await Promise.all([
sdk
.forProject(params.region, params.project)
.databases.getCollection(params.database, params.collection),
sdk
.forProject(params.region, params.project)
.databases.listCollections(params.database, [Query.orderDesc(''), Query.limit(100)])
.grids.listTables(params.database, [Query.orderDesc(''), Query.limit(100)])
]);
return {
header: Header,
breadcrumbs: Breadcrumbs,
collection,
collections
table,
tables
};
};
@@ -6,16 +6,16 @@
import type { Column, ColumnType } from '$lib/helpers/types';
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { canWriteCollections, canWriteDocuments } from '$lib/stores/roles';
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
import { Icon, Layout, Divider } from '@appwrite.io/pink-svelte';
import type { PageData } from './$types';
import {
collection,
columns,
table,
tableColumns,
isCsvImportInProgress,
showRecordsCreateSheet,
showCreateAttributeSheet,
type Attributes,
type Columns,
randomDataModalState
} from './store';
import SpreadSheet from './spreadsheet.svelte';
@@ -35,33 +35,32 @@
export let data: PageData;
let showImportCSV = false;
// todo: might need a type fix here.
const filterColumns = writable<Column[]>([]);
function createColumnsFromAttributes(
attributes: Attributes[],
selected: string[] = []
): Column[] {
return attributes.map((attribute) => ({
id: attribute.key,
title: attribute.key,
type: attribute.type as ColumnType,
hide: !!selected?.includes(attribute.key),
array: attribute?.array,
format: 'format' in attribute && attribute?.format === 'enum' ? attribute.format : null,
elements: 'elements' in attribute ? attribute.elements : null
function createTableColumns(columns: Columns[], selected: string[] = []): Column[] {
return columns.map((column) => ({
id: column.key,
title: column.key,
type: column.type as ColumnType,
hide: !!selected?.includes(column.key),
array: column?.array,
format: 'format' in column && column?.format === 'enum' ? column.format : null,
elements: 'elements' in column ? column.elements : null
}));
}
$: selected = preferences.getCustomCollectionColumns(page.params.collection);
$: selected = preferences.getCustomTableColumns(page.params.table);
$: if ($collection.attributes) {
const freshColumns = createColumnsFromAttributes($collection.attributes, selected);
columns.set(freshColumns);
$: if ($table.columns) {
const freshColumns = createTableColumns($table.columns, selected);
tableColumns.set(freshColumns);
filterColumns.set(freshColumns.filter((column) => !column.isAction));
}
$: hasAttributes = !!$collection.attributes.length;
$: hasValidAttributes = $collection?.attributes?.some((attr) => attr.status === 'available');
$: hasColumns = !!$table.columns.length;
$: hasValidColumns = $table?.columns?.some((col) => col.status === 'available');
async function onSelect(file: Models.File) {
$isCsvImportInProgress = true;
@@ -72,12 +71,12 @@
.migrations.createCsvMigration(
file.bucketId,
file.$id,
`${page.params.database}:${page.params.collection}`
`${page.params.database}:${page.params.table}`
);
addNotification({
type: 'success',
message: 'Documents import from csv has started'
message: 'Rows import from csv has started'
});
trackEvent(Submit.DatabaseImportCsv);
@@ -95,26 +94,27 @@
onDestroy(() => ($showCreateAttributeSheet.show = false));
</script>
{#key page.params.collection}
{#key page.params.table}
<Container expanded style="background: var(--bgcolor-neutral-primary)">
<Layout.Stack direction="column" gap="xl">
<Layout.Stack direction="row" justifyContent="space-between">
<!-- TODO: verify later - columns={tableColumns} -->
<Layout.Stack direction="row" gap="s">
<ViewSelector
onlyIcon
ui="new"
view={data.view}
{columns}
columns={tableColumns}
hideView
showAnyway
isCustomCollection />
isCustomTable />
<Filters
onlyIcon
query={data.query}
columns={filterColumns}
disabled={!(hasAttributes && hasValidAttributes)}
analyticsSource="database_documents" />
disabled={!(hasColumns && hasValidColumns)}
analyticsSource="database_rows" />
<!-- TODO: not yet implemented -->
<!-- <Button icon secondary>-->
@@ -122,21 +122,22 @@
<!-- </Button>-->
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector view={data.view} columns={tableColumns} hideView isCustomTable />
<Button
secondary
event={Click.DatabaseImportCsv}
disabled={!(hasAttributes && hasValidAttributes)}
disabled={!(hasColumns && hasValidColumns)}
on:click={() => (showImportCSV = true)}>
Import CSV
</Button>
{#if !$isSmallViewport}
<Button
secondary
disabled={!(hasAttributes && hasValidAttributes)}
disabled={!(hasColumns && hasValidColumns)}
on:click={() => ($showRecordsCreateSheet.show = true)}
event="create_document">
event="create_row">
<Icon icon={IconPlus} slot="start" size="s" />
Create document
Create row
</Button>
{/if}
</Layout.Stack>
@@ -144,27 +145,27 @@
{#if $isSmallViewport}
<Button
secondary
disabled={!(hasAttributes && hasValidAttributes)}
disabled={!(hasColumns && hasValidColumns)}
on:click={() => ($showRecordsCreateSheet.show = true)}
event="create_document">
event="create_row">
<Icon icon={IconPlus} slot="start" size="s" />
Create document
Create row
</Button>
{/if}
</Layout.Stack>
</Container>
<div class="databases-spreadsheet">
{#if hasAttributes && hasValidAttributes}
{#if data.documents.total}
{#if hasColumns && hasValidColumns}
{#if data.rows.total}
<Divider />
<SpreadSheet {data} bind:showRecordsCreateSheet={$showRecordsCreateSheet} />
{:else if $hasPageQueries}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2 u-bold">Sorry, we couldn't find any documents.</b>
<p>There are no documents that match your filters.</p>
<b class="body-text-2 u-bold">Sorry, we couldn't find any rows.</b>
<p>There are no rows that match your filters.</p>
</div>
<div class="u-flex common-section u-main-center">
<Button
@@ -173,7 +174,7 @@
queries.clearAll();
queries.apply();
trackEvent(Submit.FilterClear, {
source: 'database_documents'
source: 'database_rows'
});
}}>
Clear filters
@@ -184,8 +185,8 @@
{:else}
<EmptySheet
mode="records"
customColumns={createColumnsFromAttributes($collection.attributes, selected)}
showActions={$canWriteDocuments}
customColumns={createTableColumns($table.columns, selected)}
showActions={$canWriteRows}
actions={{
primary: {
onClick: () => {
@@ -203,7 +204,7 @@
<EmptySheet
mode="records"
title="You have no columns yet"
showActions={$canWriteCollections}
showActions={$canWriteTables}
actions={{
primary: {
text: 'Create column',
@@ -235,6 +236,6 @@
{/if}
<CreateRecord
collection={$collection}
table={$table}
bind:showSheet={$showRecordsCreateSheet.show}
bind:existingData={$showRecordsCreateSheet.document} />
bind:existingData={$showRecordsCreateSheet.row} />
@@ -4,12 +4,12 @@ import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';
import { queries, queryParamToMap } from '$lib/components/filters';
import { buildWildcardAttributesQuery } from './document-[document]/attributes/store';
import { buildWildcardColumnsQuery } from './row-[row]/columns/store';
export const load: PageLoad = async ({ params, depends, url, route, parent }) => {
const { collection } = await parent();
depends(Dependencies.DOCUMENT); /* TODO: we could just invalidate the documents maybe*/
depends(Dependencies.DOCUMENTS);
const { table } = await parent();
depends(Dependencies.ROW); /* TODO: we could just invalidate the rows maybe? */
depends(Dependencies.ROWS);
const page = getPage(url);
// TODO: apply pagination
@@ -20,7 +20,6 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
const paramQueries = url.searchParams.get('query');
const parsedQueries = queryParamToMap(paramQueries || '[]');
queries.set(parsedQueries);
return {
@@ -28,14 +27,14 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
limit,
view,
query,
documents: await sdk
rows: await sdk
.forProject(params.region, params.project)
.databases.listDocuments(params.database, params.collection, [
.grids.listRows(params.database, params.table, [
Query.limit(limit),
Query.offset(offset),
Query.orderDesc(''),
...parsedQueries.values(),
...buildWildcardAttributesQuery(collection)
...buildWildcardColumnsQuery(table)
])
};
};
@@ -14,7 +14,7 @@ export const load: PageLoad = async ({ params, url, route }) => {
limit,
logs: await sdk
.forProject(params.region, params.project)
.databases.listCollectionLogs(params.database, params.collection, [
.grids.listTableLogs(params.database, params.table, [
Query.limit(limit),
Query.offset(offset)
])
@@ -3,7 +3,7 @@
import { page } from '$app/state';
import { Breadcrumbs } from '$lib/layout';
import { database } from '../store';
import { collection } from './store';
import { table } from './store';
$: breadcrumbs = [
{
@@ -18,8 +18,8 @@
title: $database?.name
},
{
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}`,
title: $collection?.name
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}`,
title: $table?.name
}
];
</script>
@@ -1,7 +1,6 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { canWriteCollections } from '$lib/stores/roles';
import {
ActionMenu,
Badge,
@@ -15,19 +14,19 @@
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import { isRelationship, isString } from '../document-[document]/attributes/store';
import { isRelationship, isString } from '../row-[row]/columns/store';
import FailedModal from '../failedModal.svelte';
import {
attributes,
type Attributes,
columns,
type Columns,
indexes,
isCsvImportInProgress,
reorderItems,
showCreateIndexSheet
} from '../store';
import Delete from './deleteAttribute.svelte';
import EditAttribute from './edit.svelte';
import { attributeOptions } from './store';
import EditColumn from './edit.svelte';
import DeleteColumn from './deleteColumn.svelte';
import { columnOptions } from './store';
import {
IconArrowSmRight,
IconDotsHorizontal,
@@ -52,7 +51,7 @@
import { preferences } from '$lib/stores/preferences';
import { page } from '$app/state';
const updatedAttributes = $derived.by(() => {
const updatedColumnsForSheet = $derived.by(() => {
const baseAttrs = [
{
key: '$id',
@@ -66,7 +65,7 @@
selectable: boolean;
system: boolean;
},
...$attributes,
...$columns,
{
key: '$createdAt',
type: 'datetime',
@@ -100,23 +99,23 @@
let showDropdown = $state([]);
let showFailed = $state(false);
let showDelete = $state(false);
let selectedAttributes = $state([]);
let selectedAttribute: Attributes = $state(null);
let selectedColumns = $state([]);
let selectedColumn: Columns = $state(null);
let columnsOrder = $state([]);
const collectionId = page.params.collection;
const tableId = page.params.table;
let showEdit = $state(false);
let editAttribute: EditAttribute;
let editColumn: EditColumn;
const attributeFormatIcon = {
const columnFormatIcon = {
ip: IconLocationMarker,
url: IconLink,
email: IconLink,
enum: IconViewList
};
function getAttributeStatusBadge(status: string): ComponentProps<Badge>['type'] {
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
switch (status) {
case 'processing':
return 'warning';
@@ -131,11 +130,11 @@
const emptyCellsLimit = $derived($isSmallViewport ? 14 : 16);
const emptyCellsCount = $derived(
$attributes.length >= emptyCellsLimit ? 0 : emptyCellsLimit - $attributes.length
$columns.length >= emptyCellsLimit ? 0 : emptyCellsLimit - $columns.length
);
onMount(() => {
columnsOrder = preferences.getColumnOrder(collectionId);
columnsOrder = preferences.getColumnOrder(tableId);
});
onDestroy(() => ($showCreateAttributeSheet.show = false));
@@ -143,7 +142,7 @@
<Container expanded style="background: var(--bgcolor-neutral-primary)">
<Layout.Stack direction="row" justifyContent="flex-end">
{#if $canWriteCollections}
{#if updatedColumnsForSheet}
<Button
size="s"
secondary
@@ -166,7 +165,7 @@
allowSelection
height="100%"
emptyCells={emptyCellsCount}
bind:selectedRows={selectedAttributes}
bind:selectedRows={selectedColumns}
columns={[
{ id: 'key', width: { min: 200 } },
{ id: 'indexed', width: { min: 150 } },
@@ -181,23 +180,24 @@
<Spreadsheet.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each updatedAttributes as attribute, index}
{@const option = attributeOptions.find((option) => option.type === attribute.type)}
<!-- TODO: variable and terminology changes -->
{#each updatedColumnsForSheet as column, index}
{@const option = columnOptions.find((option) => option.type === column.type)}
{@const isSelectable =
attribute['system'] || attribute.type === 'relationship' ? 'disabled' : true}
<Spreadsheet.Row.Base {root} select={isSelectable} id={attribute.key}>
column['system'] || column.type === 'relationship' ? 'disabled' : true}
<Spreadsheet.Row.Base {root} select={isSelectable} id={column.key}>
<Spreadsheet.Cell column="key" {root} isEditable={false}>
<Layout.Stack direction="row" alignItems="center">
{#if isRelationship(attribute)}
{#if isRelationship(column)}
<Icon
size="s"
icon={attribute?.twoWay
icon={column?.twoWay
? IconSwitchHorizontal
: IconArrowSmRight} />
{:else if 'format' in attribute && attribute.format}
{@const icon = attributeFormatIcon[attribute?.format]}
{:else if 'format' in column && column.format}
{@const icon = columnFormatIcon[column?.format]}
<Icon {icon} size="s" />
{:else if attribute.key === '$id'}
{:else if column.key === '$id'}
<Icon icon={IconFingerPrint} size="s" />
{:else}
<Icon icon={option.icon} size="s" />
@@ -206,19 +206,19 @@
<Layout.Stack direction="row" alignItems="center" gap="s">
<Layout.Stack inline direction="row" alignItems="center" gap="xxs">
<span class="text u-trim-1" data-private>
{#if attribute.key === '$id' || attribute.key === '$createdAt' || attribute.key === '$updatedAt'}
{attribute['name']}
{#if attribute.key === '$id'}
{#if column.key === '$id' || column.key === '$createdAt' || column.key === '$updatedAt'}
{column['name']}
{#if column.key === '$id'}
<Badge
size="xs"
variant="secondary"
content="Primary key" />
{/if}
{:else}
{attribute.key}
{column.key}
{/if}
</span>
{#if isString(attribute) && attribute.encrypt}
{#if isString(column) && column.encrypt}
<Tooltip>
<Icon
size="s"
@@ -228,22 +228,22 @@
</Tooltip>
{/if}
</Layout.Stack>
{#if attribute.status !== 'available'}
{#if column.status !== 'available'}
<Badge
size="s"
variant="secondary"
content={attribute.status}
type={getAttributeStatusBadge(attribute.status)} />
{#if attribute.error}
content={column.status}
type={getColumnStatusBadge(column.status)} />
{#if column.error}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
error = attribute.error;
error = column.error;
showFailed = true;
}}>Details</Link.Button>
{/if}
{:else if attribute.required}
{:else if column.required}
<Badge size="xs" variant="secondary" content="required" />
{/if}
</Layout.Stack>
@@ -251,14 +251,14 @@
</Spreadsheet.Cell>
<Spreadsheet.Cell column="indexed" {root} isEditable={false}>
{@const isIndexed = $indexes.some((index) =>
index.attributes.includes(attribute.key)
index.columns.includes(column.key)
)}
<Selector.Checkbox size="s" checked={isIndexed} disabled />
</Spreadsheet.Cell>
<Spreadsheet.Cell column="default" {root} isEditable={false}>
{@const _default =
attribute?.default !== null && attribute?.default !== undefined
? attribute?.default
column?.default !== null && column?.default !== undefined
? column?.default
: null}
{#if _default === null}
@@ -281,31 +281,31 @@
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip" let:toggle>
{#if !attribute['system']}
{#if !column['system']}
<ActionMenu.Item.Button
leadingIcon={IconPencil}
on:click={(event) => {
toggle(event);
showEdit = true;
selectedAttribute = attribute;
selectedColumn = column;
showDropdown[index] = false;
}}>
Update
</ActionMenu.Item.Button>
{/if}
{#if !isRelationship(attribute)}
{#if !isRelationship(column)}
<ActionMenu.Item.Button
leadingIcon={IconPlus}
on:click={(event) => {
toggle(event);
showDropdown[index] = false;
$showCreateIndexSheet.show = true;
$showCreateIndexSheet.column = attribute.key;
$showCreateIndexSheet.column = column.key;
}}>
Create index
</ActionMenu.Item.Button>
{/if}
{#if attribute.status !== 'processing' && !attribute['system']}
{#if column.status !== 'processing' && !column['system']}
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
@@ -313,8 +313,8 @@
toggle(event);
showDelete = true;
showDropdown[index] = false;
selectedAttribute = attribute;
trackEvent(Click.DatabaseAttributeDelete);
selectedColumn = column;
trackEvent(Click.DatabaseColumnDelete);
}}>
Delete
</ActionMenu.Item.Button>
@@ -333,29 +333,30 @@
alignItems="center"
justifyContent="space-between">
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
{updatedAttributes.length} columns
{updatedColumnsForSheet.length} columns
</Typography.Text>
</Layout.Stack>
</svelte:fragment>
</Spreadsheet.Root>
</SpreadsheetContainer>
{#if selectedAttributes.length > 0}
<!-- TODO: terminologies -->
{#if selectedColumns.length > 0}
<div class="floating-action-bar">
<FloatingActionBar>
<svelte:fragment slot="start">
<div style:width="max-content">
<Layout.Stack direction="row" alignItems="center" gap="m">
<Badge content={selectedAttributes.length.toString()} />
<Badge content={selectedColumns.length.toString()} />
<span style:font-size="14px">
{selectedAttributes.length > 1 ? 'attributes' : 'attribute'}
{selectedColumns.length > 1 ? 'attributes' : 'attribute'}
selected
</span>
</Layout.Stack>
</div>
</svelte:fragment>
<svelte:fragment slot="end">
<Button text on:click={() => (selectedAttributes = [])}>Cancel</Button>
<Button text on:click={() => (selectedColumns = [])}>Cancel</Button>
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</FloatingActionBar>
@@ -363,10 +364,10 @@
{/if}
</div>
{#if selectedAttribute}
<Delete bind:showDelete {selectedAttribute} />
{:else if selectedAttributes && selectedAttributes.length}
<Delete bind:showDelete bind:selectedAttribute={selectedAttributes} />
{#if selectedColumn}
<DeleteColumn bind:showDelete {selectedColumn} />
{:else if selectedColumns && selectedColumns.length}
<DeleteColumn bind:showDelete bind:selectedColumn />
{/if}
<SideSheet
@@ -374,9 +375,9 @@
bind:show={showEdit}
submit={{
text: 'Update',
onClick: async () => await editAttribute.submit()
onClick: async () => await editColumn.submit()
}}>
<EditAttribute showEdit isModal={false} {selectedAttribute} bind:this={editAttribute} />
<EditColumn showEdit isModal={false} {selectedColumn} bind:this={editColumn} />
</SideSheet>
{#if showFailed}
@@ -5,15 +5,15 @@
export async function submitBoolean(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeBoolean>
data: Partial<Models.ColumnBoolean>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createBooleanAttribute(
.grids.createBooleanColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -22,15 +22,15 @@
}
export async function updateBoolean(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeBoolean>,
tableId: string,
data: Partial<Models.ColumnBoolean>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateBooleanAttribute(
.grids.updateBooleanColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -43,7 +43,7 @@
import { InputSelect } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeBoolean> = {
export let data: Partial<Models.ColumnBoolean> = {
required: false,
array: false,
default: null
@@ -66,7 +66,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeBoolean>>({
} = createConservative<Partial<Models.ColumnBoolean>>({
required: false,
array: false,
...data
@@ -93,11 +93,11 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
@@ -1,7 +1,7 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { ActionMenu, Icon, Popover } from '@appwrite.io/pink-svelte';
import { attributeOptions, type Option } from './store';
import { columnOptions, type Option } from './store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import CsvDisabled from '../csvDisabled.svelte';
import { isCsvImportInProgress } from '../store';
@@ -14,26 +14,26 @@
<CsvDisabled>
<Button size="s" disabled>
<Icon icon={IconPlus} slot="start" size="s" />
Create attribute
Create column
</Button>
</CsvDisabled>
{:else}
<Popover let:toggle padding="none" placement="bottom-start">
<slot {toggle}>
<Button secondary on:click={toggle} event="create_attribute">
<Button secondary on:click={toggle} event="create_column">
<Icon icon={IconPlus} slot="start" size="s" />
Create column
</Button>
</slot>
<ActionMenu.Root slot="tooltip">
{#each attributeOptions as attribute}
{#each columnOptions as column}
<ActionMenu.Item.Button
leadingIcon={attribute.icon}
leadingIcon={column.icon}
on:click={() => {
selectedOption = attribute.name;
selectedOption = column.name;
showCreate = true;
}}>
{attribute.name}
{column.name}
</ActionMenu.Item.Button>
{/each}
</ActionMenu.Root>
@@ -5,15 +5,15 @@
export async function submitDatetime(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeDatetime>
data: Partial<Models.ColumnDatetime>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createDatetimeAttribute(
.grids.createDatetimeColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -23,15 +23,15 @@
export async function updateDatetime(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeDatetime>,
tableId: string,
data: Partial<Models.ColumnDatetime>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateDatetimeAttribute(
.grids.updateDatetimeColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -44,7 +44,7 @@
import { InputDateTime } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeDatetime>;
export let data: Partial<Models.ColumnDatetime>;
import { createConservative } from '$lib/helpers/stores';
import { Selector } from '@appwrite.io/pink-svelte';
@@ -63,7 +63,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeDatetime>>({
} = createConservative<Partial<Models.ColumnDatetime>>({
required: false,
array: false,
...data
@@ -85,11 +85,11 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
@@ -3,41 +3,41 @@
import { page } from '$app/state';
import { InputChoice } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { collection } from '../store';
import type { Attributes } from '../store';
import { table } from '../store';
import type { Columns } from '../store';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { isRelationship } from '../document-[document]/attributes/store';
import { isRelationship } from '../row-[row]/columns/store';
import Confirm from '$lib/components/confirm.svelte';
import { Layout } from '@appwrite.io/pink-svelte';
import { Dependencies } from '$lib/constants';
let {
showDelete = $bindable(false),
selectedAttribute = $bindable(null)
selectedColumn = $bindable(null)
}: {
showDelete: boolean;
selectedAttribute: Attributes | string[];
selectedColumn: Columns | string[];
} = $props();
let checked = $state(false);
let error = $state<string | null>(null);
const selectedAttributes = $derived(
Array.isArray(selectedAttribute) ? selectedAttribute : [selectedAttribute]
const selectedColumns = $derived(
Array.isArray(selectedColumn) ? selectedColumn : [selectedColumn]
);
const selectedKeys = $derived(getKeys(selectedAttribute));
const selectedKeys = $derived(getKeys(selectedColumn));
const requiresTwoWayConfirm = $derived(
selectedAttributes.some((attr: string | Attributes) => {
typeof attr === 'string' ? false : isRelationship(attr) && attr.twoWay;
selectedColumns.some((column: string | Columns) => {
typeof column === 'string' ? false : isRelationship(column) && column.twoWay;
})
);
const isDeleteBtnDisabled = $derived(requiresTwoWayConfirm && !checked);
function getKeys(selected: Attributes | string[]): string[] {
function getKeys(selected: Columns | string[]): string[] {
return Array.isArray(selected) ? selected : [selected.key];
}
@@ -46,26 +46,27 @@
const client = sdk.forProject(page.params.region, page.params.project);
await Promise.all(
selectedAttributes.map((key) =>
client.databases.deleteAttribute(page.params.database, $collection.$id, key)
// TODO: check the type here
selectedColumns.map((key) =>
client.grids.deleteColumn(page.params.database, page.params.table, key)
)
);
trackEvent(Submit.AttributeDelete);
trackEvent(Submit.ColumnDelete);
addNotification({
type: 'success',
message:
selectedAttributes.length === 1
? 'Attribute has been deleted'
: `${selectedAttributes.length} attributes have been deleted`
selectedColumns.length === 1
? 'Column has been deleted'
: `${selectedColumns.length} columns have been deleted`
});
await invalidate(Dependencies.COLLECTION);
await invalidate(Dependencies.TABLE);
showDelete = false;
selectedAttribute = [];
selectedColumn = [];
} catch (e) {
error = e.message;
trackError(e, Submit.AttributeDelete);
trackError(e, Submit.ColumnDelete);
}
}
</script>
@@ -73,24 +74,24 @@
<Confirm
bind:open={showDelete}
onSubmit={handleDelete}
title="Delete attribute"
title="Delete column"
bind:error
disabled={isDeleteBtnDisabled}>
{#if selectedAttributes.length === 1}
{#if selectedColumns.length === 1}
<p>
Are you sure you want to delete <b data-private>{selectedKeys[0]}</b> from
<b data-private>{$collection?.name}</b>?
<b data-private>{$table?.name}</b>?
</p>
{:else}
<p>
Are you sure you want to delete <b data-private>{selectedKeys.join(', ')}</b> from
<b data-private>{$collection?.name}</b>?
<b data-private>{$table?.name}</b>?
</p>
{/if}
{#if requiresTwoWayConfirm}
<!-- not allowed on multi selections, safe to assume that this isn't a string! -->
{@const attribute = selectedAttribute[0]}
{@const attribute = selectedColumn[0]}
<Layout.Stack direction="column" gap="xl">
<p>
This is a two way relationship and the corresponding relationship will also be
@@ -8,22 +8,22 @@
import { Button, InputText } from '$lib/elements/forms';
import deepEqual from 'deep-equal';
import { addNotification } from '$lib/stores/notifications';
import { type Attributes, columnsOrder, databaseColumnSheetOptions } from '../store';
import { attributeOptions, type Option } from './store';
import { type Columns, columnsOrder, databaseColumnSheetOptions } from '../store';
import { columnOptions, type Option } from './store';
import { onMount } from 'svelte';
import { Layout } from '@appwrite.io/pink-svelte';
import { preferences } from '$lib/stores/preferences';
export let isModal = true;
export let showEdit = false;
export let selectedAttribute: Attributes;
export let selectedColumn: Columns;
let originalKey = '';
const databaseId = page.params.database;
const collectionId = page.params.collection;
const tableId = page.params.table;
let error: string;
let currentAttr: Attributes;
let currentColumn: Columns;
onMount(() => {
if (!isModal) {
@@ -34,56 +34,57 @@
}
});
$: option = attributeOptions.find((option) => {
if (selectedAttribute) {
if ('format' in selectedAttribute && selectedAttribute.format) {
return option?.format === selectedAttribute?.format;
$: option = columnOptions.find((option) => {
if (selectedColumn) {
if ('format' in selectedColumn && selectedColumn.format) {
return option?.format === selectedColumn?.format;
} else {
return option?.type === selectedAttribute?.type;
return option?.type === selectedColumn?.type;
}
}
}) as Option;
export async function submit() {
try {
await option.update(databaseId, collectionId, selectedAttribute, originalKey);
await invalidate(Dependencies.COLLECTION);
await option.update(databaseId, tableId, selectedColumn, originalKey);
await invalidate(Dependencies.TABLE);
if (isModal && !page.url.pathname.includes('attributes')) {
if (isModal && !page.url.pathname.includes('columns')) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${collectionId}/attributes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${tableId}/columns`
);
}
addNotification({
type: 'success',
message: `Attribute ${selectedAttribute.key} has been updated`
message: `Column ${selectedColumn.key} has been updated`
});
showEdit = false;
const oldKey = originalKey;
const newKey = selectedAttribute.key;
const newKey = selectedColumn.key;
// TODO: these stores need to be added.
if (oldKey !== newKey && $columnsOrder.includes(oldKey)) {
const updatedOrder = $columnsOrder.map((id) => (id === oldKey ? newKey : id));
columnsOrder.set(updatedOrder);
await preferences.saveColumnOrder(
page.data.organization.$id ?? page.data.project.teamId,
page.params.collection,
page.params.table,
updatedOrder
);
}
trackEvent(Submit.AttributeUpdate);
trackEvent(Submit.ColumnUpdate);
if (!isModal) {
invalidate(Dependencies.DOCUMENTS);
invalidate(Dependencies.ROWS);
$databaseColumnSheetOptions.show = false;
}
} catch (e) {
error = e.message;
trackError(e, Submit.AttributeUpdate);
trackError(e, Submit.ColumnUpdate);
if (!isModal) {
addNotification({
type: 'error',
@@ -93,20 +94,21 @@
}
}
// TODO: @itznotabug - runes?
$: onShow(showEdit);
$: title = `Update ${attributeOptions.find((v) => v.name === option.name)?.sentenceName ?? ''} attribute`;
$: title = `Update ${columnOptions.find((v) => v.name === option.name)?.sentenceName ?? ''} column`;
function onShow(show: boolean) {
if (show) {
currentAttr ??= { ...selectedAttribute };
originalKey = currentAttr.key;
currentColumn ??= { ...selectedColumn };
originalKey = currentColumn.key;
error = null;
} else {
currentAttr = null;
currentColumn = null;
}
}
$: $databaseColumnSheetOptions.disableSubmit = deepEqual(currentAttr, selectedAttribute);
$: $databaseColumnSheetOptions.disableSubmit = deepEqual(currentColumn, selectedColumn);
</script>
{#if isModal}
@@ -122,45 +124,45 @@
</div>
</svelte:fragment>
{#if selectedAttribute}
{#if selectedAttribute?.type !== 'relationship'}
{#if selectedColumn}
{#if selectedColumn?.type !== 'relationship'}
<InputText
id="key"
label="Attribute Key"
label="Column Key"
placeholder="Enter Key"
bind:value={selectedAttribute.key}
bind:value={selectedColumn.key}
autofocus />
{/if}
{#if option}
<svelte:component
this={option.component}
editing
bind:data={selectedAttribute}
bind:data={selectedColumn}
onclose={() => (option = null)} />
{/if}
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showEdit = false)}>Cancel</Button>
<Button submit disabled={deepEqual(currentAttr, selectedAttribute)}>Update</Button>
<Button submit disabled={deepEqual(currentColumn, selectedColumn)}>Update</Button>
</svelte:fragment>
</Modal>
{:else if selectedAttribute}
{:else if selectedColumn}
<Layout.Stack gap="xxxl">
<Layout.Stack gap="l">
{#if selectedAttribute?.type !== 'relationship'}
{#if selectedColumn?.type !== 'relationship'}
<InputText
id="key"
label="Attribute Key"
placeholder="Enter Key"
bind:value={selectedAttribute.key}
bind:value={selectedColumn.key}
autofocus />
{/if}
{#if option}
<svelte:component
this={option.component}
editing
bind:data={selectedAttribute}
bind:data={selectedColumn}
onclose={() => (option = null)} />
{/if}
</Layout.Stack>
@@ -5,15 +5,15 @@
export async function submitEmail(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeEmail>
data: Partial<Models.ColumnEmail>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createEmailAttribute(
.grids.createEmailColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -23,15 +23,15 @@
export async function updateEmail(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeEmail>,
tableId: string,
data: Partial<Models.ColumnEmail>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateEmailAttribute(
.grids.updateEmailColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -44,7 +44,7 @@
import { InputEmail } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeEmail>;
export let data: Partial<Models.ColumnEmail>;
import { createConservative } from '$lib/helpers/stores';
import { Selector } from '@appwrite.io/pink-svelte';
@@ -63,7 +63,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeEmail>>({
} = createConservative<Partial<Models.ColumnEmail>>({
required: false,
array: false,
...data
@@ -86,11 +86,11 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
@@ -5,15 +5,15 @@
export async function submitEnum(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeEnum>
data: Partial<Models.ColumnEnum>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createEnumAttribute(
.grids.createEnumColumn(
databaseId,
collectionId,
tableId,
key,
data.elements,
data.required,
@@ -24,15 +24,15 @@
export async function updateEnum(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeEnum>,
tableId: string,
data: Partial<Models.ColumnEnum>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateEnumAttribute(
.grids.updateEnumColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.elements,
data.required,
@@ -46,7 +46,7 @@
import { InputSelect, InputTags } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeEnum>;
export let data: Partial<Models.ColumnEnum>;
import { createConservative } from '$lib/helpers/stores';
import { Icon, Selector, Tooltip, Typography } from '@appwrite.io/pink-svelte';
@@ -66,7 +66,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeEnum>>({
} = createConservative<Partial<Models.ColumnEnum>>({
required: false,
array: false,
...data
@@ -117,11 +117,11 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
@@ -5,15 +5,15 @@
export async function submitFloat(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeFloat>
data: Partial<Models.ColumnFloat>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createFloatAttribute(
.grids.createFloatColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.min,
@@ -25,15 +25,15 @@
export async function updateFloat(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeFloat>,
tableId: string,
data: Partial<Models.ColumnFloat>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateFloatAttribute(
.grids.updateFloatColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -48,7 +48,7 @@
import { InputNumber } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeFloat> = {
export let data: Partial<Models.ColumnFloat> = {
required: false,
min: 0,
max: 0,
@@ -73,7 +73,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeFloat>>({
} = createConservative<Partial<Models.ColumnFloat>>({
required: false,
array: false,
...data
@@ -115,11 +115,11 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
@@ -5,15 +5,15 @@
export async function submitInteger(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeInteger>
data: Partial<Models.ColumnInteger>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createIntegerAttribute(
.grids.createIntegerColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.min,
@@ -25,15 +25,15 @@
export async function updateInteger(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeInteger>,
tableId: string,
data: Partial<Models.ColumnInteger>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateIntegerAttribute(
.grids.updateIntegerColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -49,7 +49,7 @@
export let editing = false;
export let data: Partial<Models.AttributeInteger> = {
export let data: Partial<Models.ColumnInteger> = {
required: false,
min: 0,
max: 0,
@@ -74,7 +74,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeInteger>>({
} = createConservative<Partial<Models.ColumnInteger>>({
required: false,
array: false,
...data
@@ -113,11 +113,11 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
@@ -5,15 +5,15 @@
export async function submitIp(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeIp>
data: Partial<Models.ColumnIp>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createIpAttribute(
.grids.createIpColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -22,15 +22,15 @@
}
export async function updateIp(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeIp>,
tableId: string,
data: Partial<Models.ColumnIp>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateIpAttribute(
.grids.updateIpColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -43,7 +43,7 @@
import { InputText } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeIp>;
export let data: Partial<Models.ColumnIp>;
import { createConservative } from '$lib/helpers/stores';
import { Selector } from '@appwrite.io/pink-svelte';
@@ -62,7 +62,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeIp>>({
} = createConservative<Partial<Models.ColumnIp>>({
required: false,
array: false,
...data
@@ -85,11 +85,11 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
@@ -6,9 +6,9 @@
export async function submitRelationship(
databaseId: string,
collectionId: string,
tableId: string,
_key: string,
data: Partial<Models.AttributeRelationship>
data: Partial<Models.ColumnRelationship>
) {
if (!isValueOfStringEnum(RelationshipType, data.relationType)) {
throw new Error(
@@ -21,10 +21,10 @@
await sdk
.forProject(page.params.region, page.params.project)
.databases.createRelationshipAttribute(
.grids.createRelationshipColumn(
databaseId,
collectionId,
data.relatedCollection,
tableId,
data.relatedTable,
data.relationType,
data.twoWay,
data.key,
@@ -35,8 +35,8 @@
export async function updateRelationship(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeRelationship>,
tableId: string,
data: Partial<Models.ColumnRelationship>,
originalKey?: string
) {
if (!isValueOfStringEnum(RelationMutate, data.onDelete)) {
@@ -45,9 +45,9 @@
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateRelationshipAttribute(
.grids.updateRelationshipColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.onDelete,
data.key !== originalKey ? data.key : undefined
@@ -59,7 +59,7 @@
import { InputText, InputSelect } from '$lib/elements/forms';
import { onMount } from 'svelte';
import { Box } from '$lib/components';
import { collection } from '../store';
import { table } from '../store';
import arrowOne from './arrow-one.svg';
import arrowTwo from './arrow-two.svg';
import { camelize } from '$lib/helpers/string';
@@ -69,7 +69,7 @@
// Props
export let editing = false;
export let data: Models.AttributeRelationship;
export let data: Models.ColumnRelationship;
// Constants
const databaseId = page.params.database;
@@ -81,47 +81,45 @@
];
const deleteOptions = [
{ value: 'setNull', label: 'Set NULL - set document ID as NULL in all related documents' },
{ value: 'cascade', label: 'Cascade - delete all related documents' },
{ value: 'restrict', label: 'Restrict - document can not be deleted' }
{ value: 'setNull', label: 'Set NULL - set row ID as NULL in all related rows' },
{ value: 'cascade', label: 'Cascade - delete all related rows' },
{ value: 'restrict', label: 'Restrict - row can not be deleted' }
];
// Variables
let way = 'one';
let search = undefined;
let collectionList: Models.CollectionList;
let originalCollectionList: Models.CollectionList;
let tableList: Models.TableList;
let originalTableList: Models.TableList;
// Lifecycle hooks
async function getCollections() {
async function getTables() {
const queries = [Query.limit(100)];
return sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections(databaseId, queries, search);
.grids.listTables(databaseId, queries, search);
}
const debouncedFetchCollections = debounce(async () => {
collectionList = await getCollections();
const debouncedFetchTables = debounce(async () => {
tableList = await getTables();
// reset search
search = undefined;
}, 500);
function updateKeyName() {
if (!editing) {
const collection = collectionList.collections.find(
(n) => n.$id === data.relatedCollection
);
data.key = camelize(collection.name);
const table = tableList.tables.find((n) => n.$id === data.relatedTable);
data.key = camelize(table.name);
}
}
onMount(async () => {
collectionList = await getCollections();
originalCollectionList = collectionList;
tableList = await getTables();
originalTableList = tableList;
});
// Reactive statements
$: collections = collectionList?.collections?.filter((n) => n.$id !== $collection.$id) ?? [];
$: tables = tableList?.tables?.filter((n) => n.$id !== $table.$id) ?? [];
$: if (editing) {
way = data.twoWay ? 'two' : 'one';
@@ -129,26 +127,26 @@
if (way === 'two') {
data.twoWay = true;
if (!data.twoWayKey) {
data.twoWayKey = camelize($collection.name);
data.twoWayKey = camelize($table.name);
}
} else {
data.twoWay = false;
}
}
$: search = data.relatedCollection || undefined;
$: search = data.relatedTable || undefined;
$: if (search) {
const exists = collectionList?.collections?.some((c) =>
const exists = tableList?.tables?.some((c) =>
c.$id.toLocaleLowerCase().includes(search.toLocaleLowerCase())
);
if (!exists) {
debouncedFetchCollections();
debouncedFetchTables();
}
} else if (!collectionList?.total) {
} else if (!tableList?.total) {
// reset to original list!
collectionList = originalCollectionList;
tableList = originalTableList;
}
</script>
@@ -159,7 +157,7 @@
name="one"
value="one"
icon={IconArrowSmRight}>
One Relation attribute within this collection
One Relation column within this table
</Card.Selector>
<Card.Selector
title="Two-way relationship"
@@ -167,23 +165,23 @@
name="two"
value="two"
icon={IconSwitchHorizontal}>
One Relation attribute within this collection and another within the related collection
One Relation column within this table and another within the related table
</Card.Selector>
</Layout.Stack>
<Input.ComboBox
required
id="related"
label="Related collection"
placeholder="Select a collection"
bind:value={data.relatedCollection}
label="Related table"
placeholder="Select a table"
bind:value={data.relatedTable}
on:change={updateKeyName}
options={collections?.map((n) => ({ value: n.$id, label: `${n.name} (${n.$id})` })) ?? []} />
options={tables?.map((n) => ({ value: n.$id, label: `${n.name} (${n.$id})` })) ?? []} />
{#if data?.relatedCollection}
{#if data?.relatedTable}
<InputText
id="key"
label="Attribute key"
label="Column key"
placeholder="Enter key"
bind:value={data.key}
helper="Allowed characters: a-z, A-Z, 0-9, -, ."
@@ -191,11 +189,11 @@
{#if way === 'two'}
<InputText
id="keyRelated"
label="Attribute key (related collection)"
label="Column key (related table)"
placeholder="Enter key"
bind:value={data.twoWayKey}
required
helper="Allowed characters: a-z, A-Z, 0-9, -, . Once created, attribute key cannot be
helper="Allowed characters: a-z, A-Z, 0-9, -, . Once created, column key cannot be
adjusted to maintain data integrity."
readonly={editing} />
{/if}
@@ -212,7 +210,7 @@
<div class="u-flex u-flex-vertical u-gap-16">
<Box>
<div class="u-flex u-align u-cross-center u-main-center u-gap-32">
<span data-private>{camelize($collection.name)}</span>
<span data-private>{camelize($table.name)}</span>
{#if data.twoWay}
<img src={arrowTwo} alt={'Two way relationship'} />
{:else}
@@ -224,7 +222,7 @@
{#if data.relationType}
<div>
<p class="u-text-center">
<b data-private>{camelize($collection.name)}</b> can contain {[
<b data-private>{camelize($table.name)}</b> can contain {[
'oneToOne',
'manyToOne'
].includes(data.relationType)
@@ -237,14 +235,14 @@
can belong to {['oneToOne', 'oneToMany'].includes(data.relationType)
? 'one'
: 'many'}
<b data-private>{camelize($collection.name)}</b>
<b data-private>{camelize($table.name)}</b>
</p>
</div>
{/if}
</div>
<InputSelect
id="deleting"
label="On deleting a document"
label="On deleting a row"
bind:value={data.onDelete}
required
placeholder="Select a deletion method"
@@ -9,7 +9,7 @@ import Ip, { submitIp, updateIp } from './ip.svelte';
import String, { submitString, updateString } from './string.svelte';
import Url, { submitUrl, updateUrl } from './url.svelte';
import Datetime, { submitDatetime, updateDatetime } from './datetime.svelte';
import type { Attributes } from '../store';
import type { Columns } from '../store';
import Relationship, { submitRelationship, updateRelationship } from './relationship.svelte';
import {
IconCalendar,
@@ -41,21 +41,21 @@ export type Option = {
type: 'string' | 'integer' | 'double' | 'boolean' | 'datetime' | 'relationship';
create: (
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Attributes>
data: Partial<Columns>
) => Promise<void>;
update: (
databaseId: string,
collectionId: string,
data: Partial<Attributes>,
tableId: string,
data: Partial<Columns>,
originalKey: string
) => Promise<void>;
format?: 'email' | 'ip' | 'url' | 'enum';
icon: ComponentType;
};
export const attributeOptions: Option[] = [
export const columnOptions: Option[] = [
{
name: 'String',
sentenceName: 'string',
@@ -5,15 +5,15 @@
export async function submitString(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeString>
data: Partial<Models.ColumnString>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createStringAttribute(
.grids.createStringColumn(
databaseId,
collectionId,
tableId,
key,
data.size,
data.required,
@@ -24,15 +24,15 @@
}
export async function updateString(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeString>,
tableId: string,
data: Partial<Models.ColumnString>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateStringAttribute(
.grids.updateStringColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -51,7 +51,7 @@
import { InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
import { Popover, Layout, Tag, Typography, Link } from '@appwrite.io/pink-svelte';
export let data: Partial<Models.AttributeString> = {
export let data: Partial<Models.ColumnString> = {
required: false,
size: 0,
array: false,
@@ -74,7 +74,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeString>>({
} = createConservative<Partial<Models.ColumnString>>({
required: false,
array: false,
...data
@@ -96,7 +96,7 @@
bind:value={data.size}
min={supportsStringEncryption && data.encrypt ? 150 : undefined}
helper={supportsStringEncryption && data.encrypt
? 'Encrypted string attributes require a minimum size of 150.'
? 'Encrypted string columns require a minimum size of 150.'
: undefined} />
<svelte:component
@@ -115,7 +115,7 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required." />
description="Indicate whether this column is required." />
<Selector.Checkbox
size="s"
@@ -123,7 +123,7 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
<Layout.Stack gap="xs" direction="column">
<div
@@ -159,15 +159,15 @@
</Layout.Stack>
</button>
<Typography.Text color="--fgcolor-neutral-tertiary">
Protect attribute against data leaks for best privacy compliance. Encrypted
attributes cannot be queried.
Protect column against data leaks for best privacy compliance. Encrypted
columns cannot be queried.
</Typography.Text>
</Layout.Stack>
<ActionMenu.Root width="180px" slot="tooltip">
<Typography.Text variant="m-500">
Available on Pro plan. <Link.Anchor href={$upgradeURL}>Upgrade</Link.Anchor>
to enable encrypted attributes.
to enable encrypted columns.
</Typography.Text>
</ActionMenu.Root>
</Popover>
@@ -5,15 +5,15 @@
export async function submitUrl(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeUrl>
data: Partial<Models.ColumnUrl>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createUrlAttribute(
.grids.createUrlColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -23,15 +23,15 @@
export async function updateUrl(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeUrl>,
tableId: string,
data: Partial<Models.ColumnUrl>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateUrlAttribute(
.grids.updateUrlColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -43,7 +43,7 @@
<script lang="ts">
import { InputURL } from '$lib/elements/forms';
export let data: Partial<Models.AttributeUrl>;
export let data: Partial<Models.ColumnUrl>;
export let editing = false;
import { createConservative } from '$lib/helpers/stores';
@@ -63,7 +63,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeUrl>>({
} = createConservative<Partial<Models.ColumnUrl>>({
required: false,
array: false,
...data
@@ -86,11 +86,11 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
@@ -1,13 +1,17 @@
<script lang="ts">
import { page } from '$app/state';
import { type Attributes, type ColumnDirection } from './store';
import { type Columns, type ColumnDirection } from './store';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Layout } from '@appwrite.io/pink-svelte';
import { InputSelect, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { attributeOptions, option, type Option } from './attributes/store';
import {
option,
columnOptions,
type Option
} from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/store';
import type { Column } from '$lib/helpers/types';
import { preferences } from '$lib/stores/preferences';
@@ -22,7 +26,7 @@
onColumnsReorder = null
}: {
showCreate: boolean;
column?: Attributes;
column?: Columns;
columnId?: string;
columns?: Column[];
columnsOrder?: string[];
@@ -31,19 +35,19 @@
onColumnsReorder?: (newOrder: string[]) => void;
} = $props();
const tableId = page.params.table;
const databaseId = page.params.database;
const collectionId = page.params.collection;
let key: string = $state(column?.key ?? null);
let data: Partial<Attributes> = $state({
let data: Partial<Columns> = $state({
required: column?.required ?? false,
array: column?.array ?? false,
default: column?.default ?? null,
...column
});
let AttributeComponent = $derived(
attributeOptions.find((option) => option.name === selectedOption).component
let ColumnComponent = $derived(
columnOptions.find((option) => option.name === selectedOption).component
);
function insertColumnInOrder() {
@@ -108,7 +112,7 @@
preferences.saveColumnOrder(
page.data.organization.$id ?? page.data.project.teamId,
collectionId,
tableId,
newOrder
);
@@ -118,24 +122,24 @@
export async function submit() {
try {
await $option.create(databaseId, collectionId, key, data);
await $option.create(databaseId, tableId, key, data);
columnId = key;
insertColumnInOrder();
await invalidate(Dependencies.COLLECTION);
await invalidate(Dependencies.TABLE);
addNotification({
type: 'success',
message: `Column ${key ?? data?.key} has been created`
});
trackEvent(Submit.AttributeCreate);
trackEvent(Submit.ColumnCreate);
} catch (e) {
addNotification({
type: 'error',
message: e.message
});
trackError(e, Submit.AttributeCreate);
trackError(e, Submit.ColumnCreate);
}
}
@@ -144,7 +148,7 @@
// correct view
if (selectedOption) {
$option = attributeOptions.find((option) => option.name === selectedOption);
$option = columnOptions.find((option) => option.name === selectedOption);
}
// cleanup
@@ -176,7 +180,7 @@
id="type"
label="Type"
bind:value={selectedOption}
options={attributeOptions.map((attr) => {
options={columnOptions.map((attr) => {
return {
label: attr.name,
value: attr.name,
@@ -187,6 +191,6 @@
</Layout.Stack>
{#if selectedOption}
<AttributeComponent bind:data onclose={() => ($option = null)} />
<ColumnComponent bind:data onclose={() => ($option = null)} />
{/if}
</Layout.Stack>
@@ -0,0 +1,138 @@
<script lang="ts">
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { writable } from 'svelte/store';
import ColumnForm from './row-[row]/columnForm.svelte';
import { Permissions } from '$lib/components/permissions';
import type { Columns } from './store';
import { ID, type Models } from '@appwrite.io/console';
import { Alert, Layout, Typography } from '@appwrite.io/pink-svelte';
import SideSheet from './layout/sidesheet.svelte';
let {
table,
showSheet = $bindable(false),
existingData = $bindable(null)
}: {
showSheet: boolean;
table: Models.Table;
existingData: Models.Document | null;
} = $props();
let isSubmitting = $state(false);
type CreateRow = {
id?: string;
row: object;
permissions: string[];
columns: Columns[];
};
function createRowWritable() {
const availableColumns = table.columns.filter((a) => a.status === 'available');
const initial = {
id: null,
row: availableColumns.reduce((acc, attr) => {
acc[attr.key] = attr.array ? [] : null;
return acc;
}, {}),
permissions: [],
columns: availableColumns
};
return writable<CreateRow>({ ...initial });
}
let createRow = createRowWritable();
$effect(() => {
if (showSheet) {
createRow = createRowWritable();
}
});
async function create() {
isSubmitting = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.grids.createRow(
page.params.database,
page.params.table,
$createRow.id ?? ID.unique(),
$createRow.row,
$createRow.permissions
);
showSheet = false;
addNotification({
message: 'Row has been created',
type: 'success'
});
// post op clear.
existingData = null;
trackEvent(Submit.RowCreate, {
customId: !!$createRow.id
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.RowCreate);
} finally {
isSubmitting = false;
}
}
</script>
{#if $createRow}
<div class="sheet-container">
<!-- TODO: add a ID badge-->
<SideSheet
spaced
title={`${existingData ? 'Duplicate' : 'Create'} record`}
bind:show={showSheet}
closeOnBlur={false}
submit={{
text: 'Create',
disabled: isSubmitting,
onClick: () => create()
}}>
<Layout.Stack gap="xxl">
<ColumnForm
bind:formValues={$createRow.row}
columns={$createRow.columns}
bind:customId={$createRow.id} />
<Layout.Stack gap="xl">
<Typography.Text>
Choose which permission scopes to grant your application. It is best
practice to allow only the permissions you need to meet your project goals.
</Typography.Text>
{#if table.rowSecurity}
<Alert.Inline status="info">
<svelte:fragment slot="title">Row security is enabled</svelte:fragment>
Users will be able to access this row if they have been granted
<b>either row or table permissions</b>.
</Alert.Inline>
<Permissions bind:permissions={$createRow.permissions} />
{:else}
<Alert.Inline status="info">
<svelte:fragment slot="title">Row security is disabled</svelte:fragment>
If you want to assign row permissions, navigate to Table settings and enable
row security. Otherwise, only table permissions will be used.
</Alert.Inline>
{/if}
</Layout.Stack>
</Layout.Stack>
</SideSheet>
</div>
{/if}
@@ -0,0 +1,120 @@
<script lang="ts">
import { symmetricDifference } from '$lib/helpers/array';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { type Writable, writable } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { invalidate } from '$app/navigation';
import { table, type Columns } from './store';
import ColumnItem from './row-[row]/columnItem.svelte';
import { isRelationship, isRelationshipToMany } from './row-[row]/columns/store';
import { deepClone } from '$lib/helpers/object';
const tableId = page.params.table;
const databaseId = page.params.database;
let {
row = $bindable()
}: {
row: Models.Row;
} = $props();
let work = $state<Writable<Models.Row> | null>(null);
function initWork() {
const prohibitedKeys = [
'$id',
'$collection',
'$tableId',
'$databaseId',
'$createdAt',
'$updatedAt'
];
const filteredKeys = Object.keys(row).filter((key) => {
return !prohibitedKeys.includes(key);
});
const result = filteredKeys.reduce((obj, key) => {
obj[key] = row[key];
return obj;
}, {});
return writable(deepClone(result as Models.Row));
}
$effect(() => {
if (row) work = initWork();
});
function compareAttributes(column: Columns, $work: Models.Row, $doc: Models.Row) {
if (!column) {
return false;
}
const workColumn = $work?.[column.key];
const currentColumn = $doc?.[column.key];
if (column.array) {
return !symmetricDifference(Array.from(workColumn), Array.from(currentColumn)).length;
}
if (isRelationship(column)) {
if (isRelationshipToMany(column as Models.ColumnRelationship)) {
const workIds = workColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
const relatedIds = currentColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
return !symmetricDifference(workIds, relatedIds).length;
} else {
const workId = typeof workColumn === 'string' ? workColumn : workColumn?.$id;
const relatedId =
typeof currentColumn === 'string' ? currentColumn : currentColumn?.$id;
return workId === relatedId;
}
}
return workColumn === currentColumn;
}
export async function update() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.grids.updateRow(databaseId, tableId, row.$id, $work, $work.$permissions);
invalidate(Dependencies.ROW);
trackEvent(Submit.RowUpdate);
addNotification({
message: 'Row has been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.RowUpdate);
}
}
export function isDisabled(): boolean {
if (!work || !$table?.columns?.length) return true;
return $table.columns.every((attribute) => compareAttributes(attribute, $work, row));
}
</script>
{#if $table.columns?.length && work}
{#each $table.columns as column}
{@const label = column.key}
<ColumnItem {column} bind:formValues={$work} {label} editing />
{/each}
{/if}
@@ -4,33 +4,30 @@
import { Id, Tab, Tabs } from '$lib/components';
import { isTabSelected } from '$lib/helpers/load';
import { Cover, CoverTitle } from '$lib/layout';
import { canWriteCollections } from '$lib/stores/roles';
import { collection } from './store';
import { canWriteTables } from '$lib/stores/roles';
import { table } from './store';
import { isTabletViewport } from '$lib/stores/viewport';
let databaseId = $derived.by(() => {
return page.params.database;
});
let collectionId = $derived.by(() => {
return page.params.collection;
});
const databaseId = $derived(page.params.database);
let path = $derived.by(() => {
return `${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${collectionId}`;
});
const tableId = $derived(page.params.table);
let tabs = $derived.by(() => {
return [
const path = $derived(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${tableId}`
);
const tabs = $derived(
[
{
href: path,
title: 'Documents',
event: 'documents',
title: 'Rows',
event: 'rows',
hasChildren: true
},
{
href: `${path}/attributes`,
title: 'Attributes',
event: 'attributes'
href: `${path}/columns`,
title: 'Columns',
event: 'columns'
},
{
href: `${path}/indexes`,
@@ -53,10 +50,10 @@
href: `${path}/settings`,
title: 'Settings',
event: 'settings',
disabled: !$canWriteCollections
disabled: !$canWriteTables
}
].filter((tab) => !tab.disabled);
});
].filter((tab) => !tab.disabled)
);
</script>
<div style:margin-top={$isTabletViewport ? '48px' : 0}>
@@ -64,10 +61,10 @@
<svelte:fragment slot="header">
<CoverTitle
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}`}>
{$collection?.name}
{$table?.name}
</CoverTitle>
{#key $collection?.$id}
<Id value={$collection?.$id}>{$collection?.$id}</Id>
{#key $table?.$id}
<Id value={$table?.$id}>{$table?.$id}</Id>
{/key}
</svelte:fragment>
@@ -1,13 +1,13 @@
<script lang="ts">
import { Container } from '$lib/layout';
import { collection } from '../store';
import { table } from '../store';
import Delete from './deleteIndex.svelte';
import CreateIndex from './createIndex.svelte';
import Overview from './overviewIndex.svelte';
import type { Models } from '@appwrite.io/console';
import { Button } from '$lib/elements/forms';
import FailedModal from '../failedModal.svelte';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
import {
ActionMenu,
Badge,
@@ -41,7 +41,7 @@
} = $props();
let showCreateIndex = $state(false);
let selectedIndex: Models.Index = $state(null);
let selectedIndex: Models.ColumnIndex = $state(null);
let selectedIndexes = $state([]);
let createIndex: CreateIndex;
@@ -60,7 +60,7 @@
{ id: 'actions', width: 40, isAction: true }
]);
function getAttributeStatusBadge(status: string): ComponentProps<Badge>['type'] {
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
switch (status) {
case 'processing':
return 'warning';
@@ -86,11 +86,11 @@
<Container expanded style="background: var(--bgcolor-neutral-primary)">
<Layout.Stack direction="row" justifyContent="flex-end">
{#if $canWriteCollections}
{#if $canWriteTables}
<Button
secondary
event="create_index"
disabled={!$collection?.attributes?.length}
disabled={!$table?.columns?.length}
on:click={() => (showCreateIndex = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Create index
@@ -100,8 +100,8 @@
</Container>
<div class="databases-spreadsheet">
{#if data.collection?.attributes?.length}
{#if data.collection.indexes.length}
{#if data.table?.columns?.length}
{#if data.table.indexes.length}
<SpreadsheetContainer>
<Spreadsheet.Root
let:root
@@ -121,7 +121,7 @@
{/if}
<Spreadsheet.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each data.collection.indexes as index}
{#each data.table.indexes as index}
<Spreadsheet.Row.Base {root} id={index.key}>
<Spreadsheet.Cell column="key" {root} isEditable={false}>
<Layout.Stack direction="row" alignItems="center">
@@ -131,7 +131,7 @@
size="s"
variant="secondary"
content={index.status}
type={getAttributeStatusBadge(index.status)} />
type={getColumnStatusBadge(index.status)} />
{#if index.error}
<Link.Button
variant="muted"
@@ -147,7 +147,7 @@
<Spreadsheet.Cell column="type" {root} isEditable={false}
>{index.type}</Spreadsheet.Cell>
<Spreadsheet.Cell column="columns" {root} isEditable={false}>
{index.attributes.join(', ')}
{index.columns.join(', ')}
</Spreadsheet.Cell>
<!-- <Spreadsheet.Cell column="orders" {root} isEditable={false}>-->
<!-- {index.orders}-->
@@ -190,7 +190,7 @@
actions={{
primary: {
onClick: () => (showCreateIndex = true),
disabled: !$collection?.attributes?.length
disabled: !$table?.columns?.length
}
}} />
{/if}
@@ -9,14 +9,14 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { IndexType } from '@appwrite.io/console';
import { isRelationship } from '../document-[document]/attributes/store';
import { collection, indexes } from '../store';
import { isRelationship } from '../row-[row]/columns/store';
import { table, indexes } from '../store';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
import { flags } from '$lib/flags';
export let showCreateIndex = false;
export let externalAttributeKey: string = null;
export let externalColumnKey: string = null;
const databaseId = page.params.database;
@@ -29,14 +29,14 @@
let selectedType = IndexType.Key;
let attributeOptions = $collection.attributes
.filter((attribute) => !isRelationship(attribute))
.map((attribute) => ({
value: attribute.key,
label: attribute.key
let columnOptions = $table.columns
.filter((column) => !isRelationship(column))
.map((column) => ({
value: column.key,
label: column.key
}));
let attributeList = [{ value: '', order: '', length: null }];
let columnList = [{ value: '', order: '', length: null }];
const showLengths = flags.showIndexLengths(page.data);
@@ -52,8 +52,8 @@
}
function initialize() {
attributeList = externalAttributeKey
? [{ value: externalAttributeKey, order: 'ASC', length: null }]
columnList = externalColumnKey
? [{ value: externalColumnKey, order: 'ASC', length: null }]
: [{ value: '', order: 'ASC', length: null }];
selectedType = IndexType.Key;
key = `index_${$indexes.length + 1}`;
@@ -64,31 +64,31 @@
key = generateIndexKey();
}
$: addAttributeDisabled = !attributeList.at(-1)?.value || !attributeList.at(-1)?.order;
$: addColumnDisabled = !columnList.at(-1)?.value || !columnList.at(-1)?.order;
export async function create() {
if (!(key && selectedType && !addAttributeDisabled)) {
if (!(key && selectedType && !addColumnDisabled)) {
return;
}
try {
await sdk.forProject(page.params.region, page.params.project).databases.createIndex(
await sdk.forProject(page.params.region, page.params.project).grids.createIndex(
databaseId,
$collection.$id,
$table.$id,
key,
selectedType,
attributeList.map((a) => a.value),
attributeList.map((a) => a.order),
attributeList.map((a) => (a.length ? Number(a.length) : null))
columnList.map((a) => a.value),
columnList.map((a) => a.order),
columnList.map((a) => (a.length ? Number(a.length) : null))
);
await Promise.allSettled([
invalidate(Dependencies.COLLECTION),
invalidate(Dependencies.TABLE),
invalidate(Dependencies.DATABASE)
]);
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${$collection.$id}/indexes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${$table.$id}/indexes`
);
addNotification({
@@ -106,11 +106,11 @@
}
}
function addAttribute() {
if (addAttributeDisabled) return;
function addColumn() {
if (addColumnDisabled) return;
// We assign instead of pushing to trigger Svelte's reactivity
attributeList = [...attributeList, { value: '', order: '', length: null }];
columnList = [...columnList, { value: '', order: '', length: null }];
}
</script>
@@ -118,7 +118,7 @@
<InputSelect required options={types} id="type" label="Index type" bind:value={selectedType} />
<Layout.Stack gap="s">
{#each attributeList as attribute, i}
{#each columnList as column, index}
<Layout.Stack direction="row">
<InputSelect
required
@@ -126,12 +126,12 @@
{ value: '$id', label: '$id' },
{ value: '$createdAt', label: '$createdAt' },
{ value: '$updatedAt', label: '$updatedAt' },
...attributeOptions
...columnOptions
]}
id={`attribute-${i}`}
label={i === 0 ? 'Column' : undefined}
id={`column-${index}`}
label={index === 0 ? 'Column' : undefined}
placeholder="Select column"
bind:value={attribute.value} />
bind:value={column.value} />
<InputSelect
options={[
@@ -139,26 +139,26 @@
{ value: 'DESC', label: 'DESC' }
]}
required
id={`order-${i}`}
label={i === 0 ? 'Order' : undefined}
bind:value={attribute.order}
placeholder="Select Order" />
id={`order-${index}`}
label={index === 0 ? 'Order' : undefined}
bind:value={column.order}
placeholder="Select order" />
{#if selectedType === IndexType.Key && showLengths}
<InputNumber
id={`length-${i}`}
label={i === 0 ? 'Length' : undefined}
placeholder="Enter Length"
bind:value={attribute.length} />
id={`length-${index}`}
label={index === 0 ? 'Length' : undefined}
placeholder="Enter length"
bind:value={column.length} />
{/if}
<Layout.Stack direction="row" alignItems="flex-end" inline>
<Button
icon
secondary
disabled={attributeList.length <= 1}
disabled={columnList.length <= 1}
on:click={() => {
attributeList = remove(attributeList, i);
columnList = remove(columnList, index);
}}>
<Icon icon={IconX} size="s" />
</Button>
@@ -166,7 +166,7 @@
</Layout.Stack>
{/each}
<div>
<Button compact on:click={addAttribute} disabled={addAttributeDisabled}>
<Button compact on:click={addColumn} disabled={addColumnDisabled}>
<Icon icon={IconPlus} slot="start" size="s" />
Add column
</Button>
@@ -1,6 +1,5 @@
<script lang="ts">
import { addNotification } from '$lib/stores/notifications';
import { collection } from '../store';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/state';
@@ -14,13 +13,13 @@
selectedIndex = $bindable(null)
}: {
showDelete: boolean;
selectedIndex: Models.Index | string[];
selectedIndex: Models.ColumnIndex | string[];
} = $props();
let error: string = $state(null);
let selectedKeys = $derived(getKeys(selectedIndex));
function getKeys(selected: Models.Index | string[]): string[] {
function getKeys(selected: Models.ColumnIndex | string[]): string[] {
return Array.isArray(selected) ? selected : [selected.key];
}
@@ -30,10 +29,10 @@
selectedKeys.map((key) =>
sdk
.forProject(page.params.region, page.params.project)
.databases.deleteIndex(page.params.database, $collection.$id, key)
.grids.deleteIndex(page.params.database, page.params.table, key)
)
);
await invalidate(Dependencies.COLLECTION);
await invalidate(Dependencies.TABLE);
showDelete = false;
addNotification({
type: 'success',
@@ -8,7 +8,7 @@
let {
selectedIndex = null
}: {
selectedIndex: Models.Index;
selectedIndex: Models.ColumnIndex;
} = $props();
const showLengths = flags.showIndexLengths(page.data);
@@ -17,8 +17,8 @@
<InputText
required
id="key"
label="Index Key"
placeholder="Enter Key"
label="Index key"
placeholder="Enter key"
value={selectedIndex.key}
readonly />
<InputText
@@ -29,14 +29,14 @@
value={selectedIndex.type}
readonly />
{#if selectedIndex?.attributes?.length}
{#each selectedIndex.attributes as attribute, i}
{#if selectedIndex?.columns?.length}
{#each selectedIndex.columns as column, i}
<Layout.Stack direction="row">
<InputText
required
label={i === 0 ? 'Column' : ''}
id={`value-${attribute}`}
value={attribute}
id={`value-${column}`}
value={column}
readonly />
<InputText
required
@@ -1,5 +1,7 @@
<script lang="ts">
import Helper from '$lib/elements/forms/helper.svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconChevronDown } from '@appwrite.io/pink-icons-svelte';
export let value: string;
export let id: string;
@@ -30,7 +32,7 @@
<select bind:this={element} {id} bind:value {required} on:invalid={handleInvalid}>
<slot />
</select>
<span class="icon-cheveron-down" aria-hidden="true"></span>
<Icon icon={IconChevronDown} size="s" />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
@@ -5,7 +5,12 @@
import { isSmallViewport } from '$lib/stores/viewport';
import { SortButton } from '$lib/components';
import type { Column } from '$lib/helpers/types';
import { columns, columnsOrder, showCreateAttributeSheet, spreadsheetLoading } from '../store';
import {
tableColumns,
columnsOrder,
showCreateAttributeSheet,
spreadsheetLoading
} from '../store';
type Mode = 'records' | 'indexes';
@@ -121,7 +126,7 @@
onclick={() => {
if (columnActionsById && mode === 'records') {
$showCreateAttributeSheet.show = true;
$showCreateAttributeSheet.columns = $columns;
$showCreateAttributeSheet.columns = $tableColumns;
$showCreateAttributeSheet.columnsOrder = $columnsOrder;
}
}}>
@@ -8,15 +8,15 @@
import { Table } from '@appwrite.io/pink-svelte';
export let show = false;
export let data: Partial<Models.Document>[];
export let selectedRelationship: Models.AttributeRelationship = null;
export let data: Partial<Models.Row>[];
export let selectedRelationship: Models.ColumnRelationship = null;
const databaseId = page.params.database;
const limit = 10;
$: args =
preferences
.getDisplayNames()
?.[selectedRelationship?.relatedCollection]?.filter((p) => p !== '$id') ?? [];
?.[selectedRelationship?.relatedTable]?.filter((p) => p !== '$id') ?? [];
$: if (!show) {
data = null;
@@ -36,7 +36,7 @@
{#snippet children(paginatedItems: typeof data)}
<Table.Root let:root columns={tableColumns}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="$id" {root}>Document ID</Table.Header.Cell>
<Table.Header.Cell column="$id" {root}>Row ID</Table.Header.Cell>
{#if args.length}
{#each args as arg}
<Table.Header.Cell column={arg} {root}>{arg}</Table.Header.Cell>
@@ -46,7 +46,7 @@
{#each paginatedItems as doc}
<Table.Row.Link
{root}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${selectedRelationship.relatedCollection}/document-${doc.$id}`}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${selectedRelationship.relatedTable}/row-${doc.$id}`}
on:click={() => (show = false)}>
<Table.Cell column="$id" {root}>
<Id value={doc.$id}>{doc.$id}</Id>
@@ -3,40 +3,35 @@ import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import type { Attributes } from '../store';
import { buildWildcardAttributesQuery } from './attributes/store';
import type { Columns } from '../store';
import { buildWildcardColumnsQuery } from './columns/store';
export const load: LayoutLoad = async ({ params, parent, depends }) => {
const { collection } = await parent();
depends(Dependencies.DOCUMENT);
const { table } = await parent();
depends(Dependencies.ROW);
const document = await sdk
const row = await sdk
.forProject(params.region, params.project)
.databases.getDocument(
params.database,
params.collection,
params.document,
buildWildcardAttributesQuery(collection)
);
.grids.getRow(params.database, params.table, params.row, buildWildcardColumnsQuery(table));
/**
* Sanitize DateTime to remove UTC Timezone section.
*/
collection.attributes.forEach((attribute) => {
const { type, key, array } = attribute as unknown as Attributes;
table.columns.forEach((column: Columns) => {
const { type, key, array } = column;
if (type === 'datetime') {
if (array) {
document[key] = document[key].map((n: string) => {
row[key] = row[key].map((n: string) => {
if (!n) {
return null;
}
return new Date(n).toISOString().slice(0, 23);
});
} else {
if (document[key]) {
document[key] = new Date(document[key]).toISOString().slice(0, 23);
if (row[key]) {
row[key] = new Date(row[key]).toISOString().slice(0, 23);
} else {
document[key] = null;
row[key] = null;
}
}
}
@@ -45,6 +40,6 @@ export const load: LayoutLoad = async ({ params, parent, depends }) => {
return {
header: Header,
breadcrumbs: Breadcrumbs,
document
row
};
};
@@ -11,36 +11,38 @@
import type { Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { invalidate } from '$app/navigation';
import { doc } from './store';
import { collection, type Attributes } from '../store';
import AttributeItem from './attributeItem.svelte';
import { isRelationship, isRelationshipToMany } from './attributes/store';
import { table, type Columns } from '../store';
import ColumnItem from './columnItem.svelte';
import { isRelationship, isRelationshipToMany } from './columns/store';
import { deepClone } from '$lib/helpers/object';
export let data;
const row = data.row;
const databaseId = page.params.database;
const collectionId = page.params.collection;
const documentId = page.params.document;
const tableId = page.params.table;
const rowId = page.params.row;
function initWork() {
const prohibitedKeys = [
'$id',
'$collection',
'$collectionId',
'$tableId',
'$databaseId',
'$createdAt',
'$updatedAt'
];
const filteredKeys = Object.keys($doc).filter((key) => {
const filteredKeys = Object.keys(row).filter((key) => {
return !prohibitedKeys.includes(key);
});
const result = filteredKeys.reduce((obj, key) => {
obj[key] = $doc[key];
obj[key] = row[key];
return obj;
}, {});
return writable(deepClone(result as Models.Document));
return writable(deepClone(result as Models.Row));
}
const work = initWork();
@@ -49,18 +51,12 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateDocument(
databaseId,
collectionId,
documentId,
$work,
$work.$permissions
);
.grids.updateRow(databaseId, tableId, rowId, $work, $work.$permissions);
invalidate(Dependencies.DOCUMENT);
trackEvent(Submit.DocumentUpdate);
await invalidate(Dependencies.ROW);
trackEvent(Submit.RowUpdate);
addNotification({
message: 'Document has been updated',
message: 'Row has been updated',
type: 'success'
});
} catch (error) {
@@ -68,46 +64,46 @@
message: error.message,
type: 'error'
});
trackError(error, Submit.DocumentUpdate);
trackError(error, Submit.RowUpdate);
}
}
function compareAttributes(
attribute: Attributes,
$work: Models.Document,
$doc: Models.Document
) {
if (!attribute) {
function compareColumns(column: Columns, related: Models.Row, row: Models.Row) {
if (!column) {
return false;
}
const workAttribute = $work?.[attribute.key];
const docAttribute = $doc?.[attribute.key];
const rowColumn = row?.[column.key];
const relatedColumn = related?.[column.key];
if (attribute.array) {
return !symmetricDifference(Array.from(workAttribute), Array.from(docAttribute)).length;
// undefined for a second, not sure why!
if (rowColumn === undefined || relatedColumn === undefined) {
return false;
}
if (isRelationship(attribute)) {
if (isRelationshipToMany(attribute as Models.AttributeRelationship)) {
const workIds = workAttribute.map((doc: string | Record<string, unknown>) =>
if (column.array) {
return !symmetricDifference(Array.from(relatedColumn), Array.from(rowColumn)).length;
}
if (isRelationship(column)) {
if (isRelationshipToMany(column as Models.ColumnRelationship)) {
const workIds = relatedColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
const relatedIds = docAttribute.map((doc: string | Record<string, unknown>) =>
const relatedIds = rowColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
return !symmetricDifference(workIds, relatedIds).length;
} else {
const workId =
typeof workAttribute === 'string' ? workAttribute : workAttribute?.$id;
const relatedId =
typeof docAttribute === 'string' ? docAttribute : docAttribute?.$id;
typeof relatedColumn === 'string' ? relatedColumn : relatedColumn?.$id;
const relatedId = typeof rowColumn === 'string' ? rowColumn : rowColumn?.$id;
return workId === relatedId;
}
}
return workAttribute === docAttribute;
return relatedColumn === rowColumn;
}
</script>
@@ -116,19 +112,19 @@
</svelte:head>
<Container>
{#if $collection?.attributes?.length}
{#each $collection.attributes as attribute}
{@const label = attribute.key}
{#if $table?.columns?.length}
{#each $table.columns as column}
{@const label = column.key}
<CardGrid>
<svelte:fragment slot="title">{label}</svelte:fragment>
<svelte:fragment slot="aside">
<AttributeItem {attribute} bind:formValues={$work} {label} editing />
<ColumnItem {column} bind:formValues={$work} {label} editing />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={compareAttributes(attribute, $work, $doc)}
disabled={compareColumns(column, $work, row)}
on:click={() => updateData()}>Update</Button>
</svelte:fragment>
</CardGrid>
@@ -14,7 +14,7 @@ export const load: PageLoad = async ({ params, url, route }) => {
limit,
logs: await sdk
.forProject(params.region, params.project)
.databases.listDocumentLogs(params.database, params.collection, params.document, [
.grids.listRowLogs(params.database, params.table, params.row, [
Query.limit(limit),
Query.offset(offset)
])
@@ -3,8 +3,8 @@
import { page } from '$app/state';
import { Breadcrumbs } from '$lib/layout';
import { database } from '../../store';
import { collection } from '../store';
import { doc } from './store';
import { table } from '../store';
import { row } from './store';
$: breadcrumbs = [
{
@@ -15,12 +15,12 @@
title: $database?.name
},
{
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}`,
title: $collection?.name
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}`,
title: $table?.name
},
{
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/document-${$doc?.$id}`,
title: $doc?.$id
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/row-${$row?.$id}`,
title: $row?.$id
}
];
</script>
@@ -0,0 +1,63 @@
<script lang="ts">
import type { Models } from '@appwrite.io/console';
import Boolean from './columns/boolean.svelte';
import Datetime from './columns/datetime.svelte';
import Enum from './columns/enum.svelte';
import Integer from './columns/integer.svelte';
import Relationship from './columns/relationship.svelte';
import String from './columns/string.svelte';
import Url from './columns/url.svelte';
export let id: string;
export let label: string;
export let optionalText: string | undefined = undefined;
export let value: string | number | boolean | null | string[];
export let editing = false;
export let column:
| Models.ColumnBoolean
| Models.ColumnEmail
| Models.ColumnEnum
| Models.ColumnFloat
| Models.ColumnInteger
| Models.ColumnIp
| Models.ColumnString
| Models.ColumnDatetime
| Models.ColumnUrl;
const columnsTypeMap = {
string: String,
integer: Integer,
double: Integer,
boolean: Boolean,
datetime: Datetime,
relationship: Relationship
};
const columnsFormatMap = {
ip: String,
url: Url,
email: String,
enum: Enum
};
</script>
{#if column.type in columnsTypeMap}
{#if 'format' in column && column.format}
<svelte:component
this={columnsFormatMap[column.format]}
{id}
{label}
{column}
{optionalText}
bind:value />
{:else}
<svelte:component
this={columnsTypeMap[column.type]}
{id}
{editing}
{label}
{column}
{optionalText}
bind:value />
{/if}
{/if}
@@ -1,22 +1,22 @@
<script lang="ts">
import CustomId from '$lib/components/customId.svelte';
import { Icon, Layout, Tag } from '@appwrite.io/pink-svelte';
import type { Attributes } from '../store';
import AttributeItem from './attributeItem.svelte';
import type { Columns } from '../store';
import ColumnItem from './columnItem.svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
export let attributes: Attributes[] = [];
export let columns: Columns[] = [];
export let formValues: object = {};
export let customId: string | null | undefined = undefined;
let showCustomId = false;
</script>
{#if attributes.length}
{#if columns.length}
<Layout.Stack>
{#each attributes as attribute}
{@const label = attribute.key}
<AttributeItem {attribute} bind:formValues {label} />
{#each columns as column}
{@const label = column.key}
<ColumnItem {column} bind:formValues {label} />
{/each}
{#if customId !== undefined}
@@ -24,11 +24,11 @@
<span>
<Tag size="s" on:click={() => (showCustomId = !showCustomId)}>
<Icon icon={IconPencil} slot="start" size="s" />
Document ID
Row ID
</Tag>
</span>
{:else}
<CustomId autofocus bind:show={showCustomId} name="Document" bind:id={customId} />
<CustomId autofocus bind:show={showCustomId} name="Row" bind:id={customId} />
{/if}
{/if}
</Layout.Stack>

Some files were not shown because too many files have changed in this diff Show More