Merge branch 'main' of github.com:appwrite/console into feat-v2-regions

This commit is contained in:
Arman
2025-05-14 10:30:42 +02:00
11 changed files with 452 additions and 302 deletions
+101 -20
View File
@@ -4,7 +4,7 @@
import { app } from '$lib/stores/app';
import {
type BottomModalAlertItem,
bottomModalAlerts,
bottomModalAlertsConfig,
dismissBottomModalAlert,
hideAllModalAlerts
} from '$lib/stores/bottom-alerts';
@@ -16,6 +16,7 @@
import { project } from '$routes/(console)/project-[region]-[project]/store';
import { page } from '$app/state';
import { Click, trackEvent } from '$lib/actions/analytics';
import { goto } from '$app/navigation';
let currentIndex = 0;
let openModalOnMobile = false;
@@ -33,23 +34,27 @@
return alerts
.sort((a, b) => b.importance - a.importance)
.filter((alert) => {
return (
alert.show &&
shouldShowNotification(alert.id) &&
// if no scope > show in projects & org pages.
((!alert.scope && (isProjectPage || isOrganizationPage)) ||
// project scope, show only in project pages
(isProjectPage && alert.scope === 'project') ||
// organization scope, show only in organization pages
(isOrganizationPage && alert.scope === 'organization'))
);
if (!alert.show || !shouldShowNotification(alert.id)) return false;
switch (alert.scope) {
case 'everywhere':
return true;
case 'project':
return isProjectPage;
case 'organization':
return isOrganizationPage;
default:
return false;
}
});
}
$: filteredModalAlerts = filterModalAlerts($bottomModalAlerts, page.route.id);
$: filteredModalAlerts = filterModalAlerts($bottomModalAlertsConfig.alerts, page.route.id);
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
$: hasOnlyPrimaryCta = typeof currentModalAlert?.learnMore === 'undefined';
function handleClose() {
filteredModalAlerts.forEach((alert) => {
const modalAlert = alert;
@@ -67,9 +72,48 @@
currentIndex = (currentIndex - 1 + filteredModalAlerts.length) % filteredModalAlerts.length;
}
function getMobileWindowConfig(): {
html: boolean;
cta: boolean;
title: string;
message: string;
} {
const config = $bottomModalAlertsConfig?.mobileSingleLayout;
const visibleAlerts = $bottomModalAlertsConfig.alerts.filter((a) => a.show);
const fallback = {
title: 'New features available',
message: 'Explore new features to enhance your projects and improve security.'
};
const shouldApplyConfig = config?.enabled === true && visibleAlerts.length === 1;
return {
cta: !!(shouldApplyConfig && config.cta),
html: !!(shouldApplyConfig && config.isHtml),
title: shouldApplyConfig && config.title ? config.title : fallback.title,
message: shouldApplyConfig && config.message ? config.message : fallback.message
};
}
function triggerMobileWindowLink() {
handleClose();
const url = $bottomModalAlertsConfig.mobileSingleLayout.cta.link({
organization: $organization,
project: $project
});
if ($bottomModalAlertsConfig.mobileSingleLayout.cta.external) {
window.open(url, '_blank');
} else {
goto(url);
}
}
function showUpgrade() {
const plan = currentModalAlert.plan;
const organizationPlan = $organization.billingPlan;
const organizationPlan = $organization?.billingPlan;
switch (plan) {
case 'free':
return false;
@@ -87,7 +131,7 @@
});
</script>
{#if filteredModalAlerts.length > 0 && currentModalAlert}
{#if filteredModalAlerts.length > 0 && currentModalAlert && !$page.url.pathname.includes('console/onboarding')}
{@const shouldShowUpgrade = showUpgrade()}
<div class="main-alert-wrapper is-not-mobile">
<div class="alert-container">
@@ -164,8 +208,8 @@
<div
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
<Button
secondary
class="button"
secondary={!hasOnlyPrimaryCta}
class={`${hasOnlyPrimaryCta ? 'only-primary-cta' : ''}`}
href={shouldShowUpgrade
? $upgradeURL
: currentModalAlert.cta.link({
@@ -175,6 +219,12 @@
external={!!currentModalAlert.cta.external}
fullWidthMobile
on:click={() => {
if (currentModalAlert.cta?.hideOnClick === true) {
// be careful of this one.
// once clicked, its gone!
handleClose();
}
trackEvent(Click.PromoClick, {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
@@ -280,7 +330,7 @@
<div
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
<Button
secondary
secondary={!hasOnlyPrimaryCta}
class="button"
href={shouldShowUpgrade
? $upgradeURL
@@ -322,17 +372,32 @@
</article>
</div>
{:else}
{@const mobileConfig = getMobileWindowConfig()}
<button
class:showing={!openModalOnMobile}
class="card notification-card u-width-full-line"
on:click={() => (openModalOnMobile = true)}>
on:click={() => {
if (mobileConfig.cta) {
// navigate manually!
triggerMobileWindowLink();
} else {
openModalOnMobile = true;
}
}}>
<div class="u-flex-vertical u-gap-4">
<div class="u-flex u-cross-center u-main-space-between">
<h3 class="body-text-2 u-bold">New features available</h3>
<h3 class="body-text-2 u-bold">{mobileConfig.title}</h3>
<button on:click={hideAllModalAlerts}>
<span class="icon-x" />
</button>
</div>
<span class="u-width-fit-content">
Explore new features to enhance your projects and improve security.
{#if mobileConfig.html}
{@html mobileConfig.message}
{:else}
{mobileConfig.message}
{/if}
</span>
</div>
</button>
@@ -377,6 +442,22 @@
border: hsl(var(--color-neutral-80)) solid 1px;
}
:global(.main-alert-wrapper .only-primary-cta) {
width: 100%;
text-align: center;
justify-content: center;
}
.showcase-image.u-only-light {
border-radius: 8px;
border: 0.795px solid var(--border-neutral-strong, #d8d8db);
}
.showcase-image.u-only-dark {
border-radius: 8px;
border: 0.795px solid var(--border-neutral-strong, #414146);
}
.u-gap-10 {
gap: 0.625rem;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

+46 -7
View File
@@ -5,10 +5,28 @@ import type { Models } from '@appwrite.io/console';
type BottomModalAlertAction = {
text: string;
hideOnClick?: boolean;
link: (ctx: { organization: Organization; project: Models.Project }) => string;
external?: boolean;
};
/**
* Special layout settings for mobile when exactly one alert is shown.
*
* Applied only if `enabled` is `true` and the number of visible alerts is exactly 1.
*
* Useful when you want to display the alert directly in the floating window,
* without opening a separate modal.
*/
export type MobileSingleAlertLayoutConfig = {
title: string;
message: string;
enabled: boolean;
isHtml?: boolean;
// because message is the text!
cta: Omit<BottomModalAlertAction, 'text'>;
};
export type BottomModalAlertItem = {
id: string;
title: string;
@@ -23,18 +41,36 @@ export type BottomModalAlertItem = {
importance?: number;
closed?: () => void;
scope?: 'organization' | 'project';
scope: 'organization' | 'project' | 'everywhere';
notificationHideOptions?: NotificationCoolOffOptions;
};
export const bottomModalAlerts = writable<BottomModalAlertItem[]>([]);
type BottomModalAlertState = {
alerts: BottomModalAlertItem[];
mobileSingleLayout?: MobileSingleAlertLayoutConfig;
};
export const bottomModalAlertsConfig = writable<BottomModalAlertState>({ alerts: [] });
export const hideAllModalAlerts = () => {
bottomModalAlerts.update((all) => all.map((t) => ({ ...t, show: false })));
bottomModalAlertsConfig.update((state) => ({
...state,
alerts: state.alerts.map((t) => ({ ...t, show: false }))
}));
};
export const setMobileSingleAlertLayout = (config: MobileSingleAlertLayoutConfig) => {
bottomModalAlertsConfig.update((state) => ({
...state,
mobileSingleLayout: config
}));
};
export const dismissBottomModalAlert = (id: string) => {
bottomModalAlerts.update((all) => all.filter((t) => t.id !== id));
bottomModalAlertsConfig.update((state) => ({
...state,
alerts: state.alerts.filter((t) => t.id !== id)
}));
};
export const showBottomModalAlert = (notification: BottomModalAlertItem) => {
@@ -45,8 +81,11 @@ export const showBottomModalAlert = (notification: BottomModalAlertItem) => {
...notification
};
bottomModalAlerts.update((all) => {
if (all.some((t) => t.id === notification.id)) return all;
return [...all, defaults as BottomModalAlertItem];
bottomModalAlertsConfig.update((state) => {
if (state.alerts.some((t) => t.id === notification.id)) return state;
return {
...state,
alerts: [...state.alerts, defaults as BottomModalAlertItem]
};
});
};
+33 -19
View File
@@ -1,32 +1,46 @@
import { base } from '$app/paths';
import BackupsDark from '$lib/images/backups/promo/backups-dark.png';
import BackupsLight from '$lib/images/backups/promo/backups-light.png';
import Init3Promo from '$lib/images/promo-init3.png';
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
import {
type BottomModalAlertItem,
setMobileSingleAlertLayout,
showBottomModalAlert
} from '$lib/stores/bottom-alerts';
import { isCloud } from '$lib/system';
const listOfPromotions: BottomModalAlertItem[] = [];
if (isCloud) {
const title = 'Join Init 19-23 May';
const message =
'This release will change the way you build with Appwrite forever. Register for Init and join the giveaway.';
const callToAction = {
external: true,
hideOnClick: true,
text: 'Claim your ticket',
link: () => 'https://apwr.dev/clcon'
};
listOfPromotions.push({
id: 'modal:databaseBackups',
id: 'modal:init3',
src: {
dark: BackupsDark,
light: BackupsLight
dark: Init3Promo,
light: Init3Promo
},
title: 'Database Backups are available now',
message: 'Protect your data and ensure quick recovery with our new backups',
plan: 'pro',
scope: 'project',
title,
message,
plan: 'free',
importance: 8,
cta: {
text: 'Try now',
link: ({ project }) => `${base}/project-${project.region}-${project.$id}/databases`
},
learnMore: {
text: 'Learn more',
link: () => 'https://appwrite.io/docs/products/databases/backups'
}
scope: 'everywhere',
cta: callToAction
});
// there's only one item.
setMobileSingleAlertLayout({
title,
message,
enabled: true,
cta: callToAction
});
}
@@ -1,10 +1,9 @@
<script lang="ts">
import { page } from '$app/state';
import { Empty, EmptySearch, PaginationWithLimit } from '$lib/components';
import { Filters, hasPageQueries, queries } from '$lib/components/filters';
import ViewSelector from '$lib/components/viewSelector.svelte';
import { Button } from '$lib/elements/forms';
import type { ColumnType } from '$lib/helpers/types';
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';
@@ -15,14 +14,16 @@
import CreateAttribute from './createAttribute.svelte';
import { collection, columns, isCsvImportInProgress } from './store';
import Table from './table.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { base } from '$app/paths';
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { writable } from 'svelte/store';
import FilePicker from '$lib/components/filePicker.svelte';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { isSmallViewport } from '$lib/stores/viewport';
import { base } from '$app/paths';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
export let data: PageData;
@@ -30,6 +31,8 @@
let showCreateAttribute = false;
let selectedAttribute: Option['name'] = null;
const filterColumns = writable<Column[]>([]);
$: selected = preferences.getCustomCollectionColumns(page.params.collection);
$: columns.set(
$collection.attributes.map((attribute) => ({
@@ -42,6 +45,16 @@
elements: 'elements' in attribute ? attribute.elements : null
}))
);
$: filterColumns.set([
...$columns,
...['$id', '$createdAt', '$updatedAt'].map((id) => ({
id,
title: id,
show: true,
type: (id === '$id' ? 'string' : 'datetime') as ColumnType
}))
]);
$: hasAttributes = !!$collection.attributes.length;
$: hasValidAttributes = $collection?.attributes?.some((attr) => attr.status === 'available');
@@ -1,140 +1,138 @@
<script lang="ts">
import { CardGrid, BoxAvatar } from '$lib/components';
import { Container } from '$lib/layout';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { doc } from './store';
import { addNotification } from '$lib/stores/notifications';
import { toLocaleDateTime } from '$lib/helpers/date';
import Delete from './delete.svelte';
import { symmetricDifference } from '$lib/helpers/array';
import { Permissions } from '$lib/components/permissions';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { collection } from '../store';
import { Alert, Link } from '@appwrite.io/pink-svelte';
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import { CardGrid } from '$lib/components';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { writable } from 'svelte/store';
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 { deepClone } from '$lib/helpers/object';
let showDelete = false;
let permissions = $doc?.$permissions;
let arePermsDisabled = true;
let showPermissionAlert = true;
const databaseId = page.params.database;
const collectionId = page.params.collection;
const documentId = page.params.document;
const editing = true;
async function updatePermissions() {
function initWork() {
const prohibitedKeys = [
'$id',
'$collection',
'$collectionId',
'$databaseId',
'$createdAt',
'$updatedAt'
];
const filteredKeys = Object.keys($doc).filter((key) => {
return !prohibitedKeys.includes(key);
});
const result = filteredKeys.reduce((obj, key) => {
obj[key] = $doc[key];
return obj;
}, {});
return writable(deepClone(result as Models.Document));
}
const work = initWork();
async function updateData() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateDocument(
$doc.$databaseId,
$doc.$collectionId,
$doc.$id,
$doc.data,
permissions
databaseId,
collectionId,
documentId,
$work,
$work.$permissions
);
await invalidate(Dependencies.DOCUMENT);
arePermsDisabled = true;
invalidate(Dependencies.DOCUMENT);
trackEvent(Submit.DocumentUpdate);
addNotification({
message: 'Permissions have been updated',
message: 'Document has been updated',
type: 'success'
});
trackEvent(Submit.DocumentUpdatePermissions);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DocumentUpdatePermissions);
trackError(error, Submit.DocumentUpdate);
}
}
$: if (permissions) {
if (symmetricDifference(permissions, $doc.$permissions).length) {
arePermsDisabled = false;
} else arePermsDisabled = true;
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;
}
</script>
<svelte:head>
<title>Document - Appwrite</title>
<title>Data - Appwrite</title>
</svelte:head>
<Container>
<CardGrid>
<svelte:fragment slot="title">Metadata</svelte:fragment>
<svelte:fragment slot="aside">
<div>
<p>Created: {toLocaleDateTime($doc.$createdAt)}</p>
<p>Last updated: {toLocaleDateTime($doc.$updatedAt)}</p>
</div>
</svelte:fragment>
</CardGrid>
<CardGrid>
<svelte:fragment slot="title">Permissions</svelte:fragment>
A user requires appropriate permissions at either the <b>collection level</b> or
<b>document level</b> to access a document. If no permissions are configured, no user can
access the document
<Link.Anchor
href="https://appwrite.io/docs/products/databases/permissions"
target="_blank"
rel="noopener noreferrer">Learn more</Link.Anchor>
<svelte:fragment slot="aside">
{#if $collection.documentSecurity}
{#if showPermissionAlert}
<Alert.Inline
status="info"
dismissible
on:dismiss={() => (showPermissionAlert = false)}>
<svelte:fragment slot="title">Document security is enabled</svelte:fragment>
<p class="text">
Users will be able to access this document if they have been granted <b
>either document or collection permissions.
</b>
</p>
</Alert.Inline>
{/if}
{#if permissions}
<Permissions bind:permissions />
{/if}
{:else}
<Alert.Inline status="info">
<svelte:fragment slot="title">Document security is disabled</svelte:fragment>
<p class="text">
If you want to assign document permissions. Go to <Link.Anchor
href={`./settings`}>Collection settings</Link.Anchor> and enable document
security. Otherwise, only collection permissions will be used.
</p>
</Alert.Inline>
{/if}
</svelte:fragment>
{#if $collection?.attributes?.length}
{#each $collection.attributes as attribute}
{@const label = attribute.key}
<CardGrid>
<svelte:fragment slot="title">{label}</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={arePermsDisabled}
on:click={() => {
updatePermissions();
}}>Update</Button>
</svelte:fragment>
</CardGrid>
<CardGrid>
<svelte:fragment slot="title">Delete document</svelte:fragment>
The document will be permanently deleted, including all the data within it. This action is irreversible.
<svelte:fragment slot="aside">
<BoxAvatar>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$doc.$id}</h6>
<svelte:fragment slot="aside">
<AttributeItem {attribute} bind:formValues={$work} {label} {editing} />
</svelte:fragment>
<p>
Last updated: {toLocaleDateTime($doc.$updatedAt)}
</p>
</BoxAvatar>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
<svelte:fragment slot="actions">
<Button
disabled={compareAttributes(attribute, $work, $doc)}
on:click={() => updateData()}>Update</Button>
</svelte:fragment>
</CardGrid>
{/each}
{/if}
</Container>
<Delete bind:showDelete />
@@ -1,137 +0,0 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { CardGrid } from '$lib/components';
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { writable } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { doc } from '../store';
import { collection, type Attributes } from '../../store';
import { Container } from '$lib/layout';
import AttributeItem from '../attributeItem.svelte';
import { symmetricDifference } from '$lib/helpers/array';
import { isRelationship, isRelationshipToMany } from '../attributes/store';
import { deepClone } from '$lib/helpers/object';
const databaseId = page.params.database;
const collectionId = page.params.collection;
const documentId = page.params.document;
const editing = true;
function initWork() {
const prohibitedKeys = [
'$id',
'$collection',
'$collectionId',
'$databaseId',
'$createdAt',
'$updatedAt'
];
const filteredKeys = Object.keys($doc).filter((key) => {
return !prohibitedKeys.includes(key);
});
const result = filteredKeys.reduce((obj, key) => {
obj[key] = $doc[key];
return obj;
}, {});
return writable(deepClone(result as Models.Document));
}
const work = initWork();
async function updateData() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateDocument(
databaseId,
collectionId,
documentId,
$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);
}
}
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;
}
</script>
<svelte:head>
<title>Data - Appwrite</title>
</svelte:head>
<Container>
{#if $collection?.attributes?.length}
{#each $collection.attributes as attribute}
{@const label = attribute.key}
<CardGrid>
<svelte:fragment slot="title">{label}</svelte:fragment>
<svelte:fragment slot="aside">
<AttributeItem {attribute} bind:formValues={$work} {label} {editing} />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={compareAttributes(attribute, $work, $doc)}
on:click={() => updateData()}>Update</Button>
</svelte:fragment>
</CardGrid>
{/each}
{/if}
</Container>
@@ -14,20 +14,20 @@
const tabs = [
{
href: path,
title: 'Overview',
event: 'overview'
},
{
href: `${path}/data`,
title: 'Data',
event: 'data',
hasChildren: true
event: 'data'
},
{
href: `${path}/activity`,
title: 'Activity',
event: 'activity',
hasChildren: true
},
{
href: `${path}/settings`,
title: 'Settings',
event: 'settings',
hasChildren: true
}
];
</script>
@@ -0,0 +1,142 @@
<script lang="ts">
import { CardGrid, BoxAvatar, Heading, Alert } from '$lib/components';
import { Container } from '$lib/layout';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { doc } from '../store';
import { addNotification } from '$lib/stores/notifications';
import { toLocaleDateTime } from '$lib/helpers/date';
import Delete from '../delete.svelte';
import { symmetricDifference } from '$lib/helpers/array';
import { Permissions } from '$lib/components/permissions';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { collection } from '../../store';
import { page } from '$app/stores';
let showDelete = false;
let permissions = $doc?.$permissions;
let arePermsDisabled = true;
let showPermissionAlert = true;
async function updatePermissions() {
try {
await sdk
.forProject($page.params.region, $page.params.project)
.databases.updateDocument(
$doc.$databaseId,
$doc.$collectionId,
$doc.$id,
$doc.data,
permissions
);
await invalidate(Dependencies.DOCUMENT);
arePermsDisabled = true;
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
trackEvent(Submit.DocumentUpdatePermissions);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DocumentUpdatePermissions);
}
}
$: if (permissions) {
arePermsDisabled = !symmetricDifference(permissions, $doc.$permissions).length;
}
</script>
<svelte:head>
<title>Document - Appwrite</title>
</svelte:head>
<Container>
<CardGrid>
<Heading tag="h2" size="7">Metadata</Heading>
<svelte:fragment slot="aside">
<div>
<p>Created: {toLocaleDateTime($doc.$createdAt)}</p>
<p>Last updated: {toLocaleDateTime($doc.$updatedAt)}</p>
</div>
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Permissions</Heading>
<p>
A user requires appropriate permissions at either the <b>collection level</b> or
<b>document level</b> to access a document. If no permissions are configured, no user
can access the document
<a
href="https://appwrite.io/docs/products/databases/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more about database permissions</a
>.
</p>
<svelte:fragment slot="aside">
{#if $collection.documentSecurity}
{#if showPermissionAlert}
<Alert type="info" dismissible on:dismiss={() => (showPermissionAlert = false)}>
<svelte:fragment slot="title">Document security is enabled</svelte:fragment>
<p class="text">
Users will be able to access this document if they have been granted <b
>either document or collection permissions.
</b>
</p>
</Alert>
{/if}
{#if permissions}
<Permissions bind:permissions />
{/if}
{:else}
<Alert type="info">
<svelte:fragment slot="title">Document security is disabled</svelte:fragment>
<p class="text">
If you want to assign document permissions. Go to Collection settings and
enable document security. Otherwise, only collection permissions will be
used.
</p>
</Alert>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={arePermsDisabled}
on:click={() => {
updatePermissions();
}}>Update</Button>
</svelte:fragment>
</CardGrid>
<CardGrid danger>
<Heading tag="h6" size="7">Delete document</Heading>
<p>
The document will be permanently deleted, including all the data within it. This action
is irreversible.
</p>
<svelte:fragment slot="aside">
<BoxAvatar>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$doc.$id}</h6>
</svelte:fragment>
<p>
Last updated: {toLocaleDateTime($doc.$updatedAt)}
</p>
</BoxAvatar>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
</Container>
<Delete bind:showDelete />