mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch '1.8.x' into 'spreadsheet' ‼️.
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
Generated
+9
-9
@@ -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: {}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -28,10 +28,10 @@ const groups = [
|
||||
'integrations',
|
||||
'migrations',
|
||||
'users',
|
||||
'collections',
|
||||
'attributes',
|
||||
'tables',
|
||||
'columns',
|
||||
'indexes',
|
||||
'documents',
|
||||
'rows',
|
||||
'teams',
|
||||
'security',
|
||||
'buckets',
|
||||
|
||||
@@ -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?'
|
||||
];
|
||||
|
||||
+4
-4
@@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
+2
-2
@@ -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;
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}**` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+18
-19
@@ -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} />
|
||||
|
||||
+3
-1
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
+8
-11
@@ -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
-4
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
-149
@@ -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}
|
||||
-63
@@ -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}
|
||||
-37
@@ -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(['*'])
|
||||
];
|
||||
}
|
||||
-132
@@ -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}
|
||||
-72
@@ -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>
|
||||
+10
-8
@@ -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
|
||||
+35
-35
@@ -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>
|
||||
|
||||
+11
-11
@@ -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>
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@
|
||||
const tabs = [
|
||||
{
|
||||
href: path,
|
||||
title: 'Collections',
|
||||
event: 'collections',
|
||||
title: 'Tables',
|
||||
event: 'tables',
|
||||
hasChildren: true
|
||||
},
|
||||
{
|
||||
|
||||
+16
-15
@@ -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>
|
||||
|
||||
+2
-2
@@ -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 } }
|
||||
|
||||
+13
-13
@@ -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>
|
||||
|
||||
+19
-18
@@ -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}`
|
||||
}
|
||||
|
||||
+75
-74
@@ -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}>
|
||||
+7
-8
@@ -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
|
||||
};
|
||||
};
|
||||
+47
-46
@@ -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} />
|
||||
+7
-8
@@ -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)
|
||||
])
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -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
-3
@@ -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>
|
||||
+61
-60
@@ -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}
|
||||
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 468 B |
|
Before Width: | Height: | Size: 837 B After Width: | Height: | Size: 837 B |
+12
-12
@@ -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." />
|
||||
+7
-7
@@ -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>
|
||||
+12
-12
@@ -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." />
|
||||
+26
-25
@@ -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
|
||||
+37
-35
@@ -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>
|
||||
+12
-12
@@ -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." />
|
||||
+12
-12
@@ -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." />
|
||||
+12
-12
@@ -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." />
|
||||
+12
-12
@@ -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." />
|
||||
+12
-12
@@ -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." />
|
||||
+45
-47
@@ -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"
|
||||
+6
-6
@@ -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',
|
||||
+16
-16
@@ -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>
|
||||
+12
-12
@@ -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." />
|
||||
+19
-15
@@ -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>
|
||||
+138
@@ -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}
|
||||
+120
@@ -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}
|
||||
+21
-24
@@ -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>
|
||||
|
||||
+12
-12
@@ -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}
|
||||
+39
-39
@@ -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>
|
||||
+4
-5
@@ -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',
|
||||
+7
-7
@@ -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
|
||||
+3
-1
@@ -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>
|
||||
+7
-2
@@ -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;
|
||||
}
|
||||
}}>
|
||||
+5
-5
@@ -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>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<svelte:head>
|
||||
<title>Document - Appwrite</title>
|
||||
<title>Row - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
+13
-18
@@ -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
|
||||
};
|
||||
};
|
||||
+40
-44
@@ -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>
|
||||
+1
-1
@@ -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)
|
||||
])
|
||||
+6
-6
@@ -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>
|
||||
+63
@@ -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}
|
||||
+9
-9
@@ -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
Reference in New Issue
Block a user