Merge branch 'main' into feat-add-view-certiicate-logs

This commit is contained in:
Khushboo Verma
2025-09-04 17:54:28 +05:30
29 changed files with 376 additions and 210 deletions
+97 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { DropList, GridItem1, CardContainer } from '$lib/components';
import { Button, InputText } from '$lib/elements/forms';
import { DropList, GridItem1, CardContainer, Modal } from '$lib/components';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import {
Badge,
Icon,
@@ -9,7 +10,8 @@
Accordion,
ActionMenu,
Popover,
Layout
Layout,
Divider
} from '@appwrite.io/pink-svelte';
import {
IconAndroid,
@@ -21,7 +23,8 @@
IconInfo,
IconDotsHorizontal,
IconInboxIn,
IconSwitchHorizontal
IconSwitchHorizontal,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import { getPlatformInfo } from '$lib/helpers/platform';
import { Status, type Models } from '@appwrite.io/console';
@@ -33,7 +36,7 @@
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Modal } from '$lib/components';
import { isSmallViewport } from '$lib/stores/viewport';
import { isCloud } from '$lib/system';
import { regions as regionsStore } from '$lib/stores/organization';
@@ -53,6 +56,17 @@
let readOnlyInfoOpen = $state<Record<string, boolean>>({});
let showUnarchiveModal = $state(false);
let projectToUnarchive = $state<Models.Project | null>(null);
let showDeleteModal = $state(false);
let projectToDelete = $state<Models.Project | null>(null);
let deleteProjectName = $state<string | null>(null);
let deleteError = $state<string | null>(null);
function resetDeleteState() {
showDeleteModal = false;
projectToDelete = null;
deleteProjectName = null;
deleteError = null;
}
function filterPlatforms(platforms: { name: string; icon: string }[]) {
return platforms.filter(
@@ -103,6 +117,12 @@
showUnarchiveModal = true;
}
function handleDeleteProject(project: Models.Project) {
projectToDelete = project;
deleteProjectName = null;
showDeleteModal = true;
}
// Confirm unarchive action
async function confirmUnarchive() {
if (!projectToUnarchive) return;
@@ -141,6 +161,29 @@
projectToUnarchive = null;
}
async function confirmDelete() {
if (!projectToDelete) return;
try {
await sdk.forConsoleIn(projectToDelete.region).projects.delete({
projectId: projectToDelete.$id
});
await invalidate(Dependencies.ORGANIZATION);
trackEvent(Submit.ProjectDelete);
addNotification({
type: 'success',
message: `${projectToDelete.name} has been deleted`
});
resetDeleteState();
} catch (error) {
deleteError = error.message;
trackError(error, Submit.ProjectDelete);
}
}
function findRegion(project: Models.Project) {
return $regionsStore.regions.find((region) => region.$id === project.region);
}
@@ -226,6 +269,14 @@
leadingIcon={IconSwitchHorizontal}
on:click={() => handleMigrateProject(project)}
>Migrate project</ActionMenu.Item.Button>
<div class="action-menu-divider">
<Divider />
</div>
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
on:click={() => handleDeleteProject(project)}
>Delete project</ActionMenu.Item.Button>
</ActionMenu.Root>
</Popover>
</div>
@@ -275,10 +326,51 @@
</svelte:fragment>
</Modal>
<!-- Delete Confirmation Modal -->
<Modal
size="s"
bind:show={showDeleteModal}
title="Delete project"
onSubmit={confirmDelete}
bind:error={deleteError}>
<svelte:fragment slot="description">
The archived project <strong>{projectToDelete?.name}</strong> will be deleted along with all
of its metadata, stats, and other resources.
<b>This action is irreversible.</b>
</svelte:fragment>
<InputText
label={`Enter "${projectToDelete?.name}" to continue`}
placeholder="Enter name"
id="delete-project-name"
autofocus
required
bind:value={deleteProjectName} />
<svelte:fragment slot="footer">
<Button
text
on:click={() => {
resetDeleteState();
}}>Cancel</Button>
<Button
submissionLoader
submit
disabled={(deleteProjectName ?? '') !== projectToDelete?.name}>
Delete
</Button>
</svelte:fragment>
</Modal>
<style>
.archive-projects-margin-top {
margin-top: 36px;
}
.action-menu-divider {
margin-inline: -1rem;
padding-block-start: 0.25rem;
padding-block-end: 0.25rem;
}
.archive-projects-margin {
margin-top: 16px;
@@ -23,8 +23,8 @@
{/if}
</Layout.Stack>
<Typography.Text variant="m-400">
Add the following nameservers on your DNS provider. Note that DNS changes may take time to
propagate fully.
Add the following nameservers on your DNS provider. Note that changes may take up to 48
hours to propagate fully.
</Typography.Text>
</Layout.Stack>
+1 -1
View File
@@ -63,7 +63,7 @@ function initQueries(initialValue = new Map<TagValue, string>()) {
const currentLocation = window.location.pathname;
if (usableQueries.size) {
const queryParam = mapToQueryParams(get(queries));
const queryParam = mapToQueryParams(usableQueries);
goto(`${currentLocation}?query=${queryParam}`, { noScroll: true });
} else {
goto(currentLocation, { noScroll: true });
+1 -1
View File
@@ -21,7 +21,7 @@
const url = new URL(pageStore.url);
const previousLimit = Number(url.searchParams.get('limit'));
url.searchParams.set('limit', limit.toString());
preferences.setLimit(limit);
await preferences.setLimit(limit);
if (url.searchParams.has('page')) {
const page = Number(url.searchParams.get('page'));
-2
View File
@@ -250,8 +250,6 @@
</Tooltip>
{/each}
{#if project && $isSmallViewport}
<Divider />
<div class="mobile-tablet-settings">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

+50 -16
View File
@@ -1,34 +1,56 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
// TODO: needs better props
let {
expanded = false,
slotSpacing = false,
overlapCover = false,
paddingInlineEnd = true,
paddingInlineEndDouble = false,
insideSideSheet = false,
databasesScreen = false,
databasesMainScreen = false,
expandHeightButton = false,
size = null,
children,
...restProps
}: {
expanded?: boolean;
slotSpacing?: boolean;
overlapCover?: boolean;
paddingInlineEnd?: boolean;
paddingInlineEndDouble?: boolean;
insideSideSheet?: boolean;
databasesScreen?: boolean;
databasesMainScreen?: boolean;
expandHeightButton?: boolean;
children?: Snippet;
size?: 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | null;
} & HTMLAttributes<HTMLDivElement> = $props();
export let expanded = false;
export let slotSpacing = false;
export let overlapCover = false;
export let paddingInlineEnd = true;
export let insideSideSheet = false;
export let databasesScreen = false;
export let expandHeightButton = false;
export let size: 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' = null;
$: style = size
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
: '';
const style = $derived(
size
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
: ''
);
</script>
<div style:container-type="inline-size" class:overlap-cover={overlapCover} {...$$restProps}>
<div style:container-type="inline-size" class:overlap-cover={overlapCover} {...restProps}>
<div
{style}
class:expanded
class:slotSpacing
class:databasesScreen
class:insideSideSheet
class:databasesScreen
class:expandHeightButton
class:databasesMainScreen
class="console-container"
class:paddingInlineEndDouble
class:paddingInlineEnd={!paddingInlineEnd}>
<Layout.Stack gap="l">
<slot />
{@render children?.()}
</Layout.Stack>
</div>
</div>
@@ -90,6 +112,12 @@
}
}
&.paddingInlineEndDouble {
@media (min-width: 1024px) {
padding-inline-end: calc(2 * 2.75rem) !important;
}
}
&.databasesScreen {
@media (min-width: 1440px) {
min-width: 1070px;
@@ -102,6 +130,12 @@
}
}
&.databasesMainScreen {
@media (min-width: 1440px) {
max-width: 1200px;
}
}
@media (min-width: 360px) {
margin-inline: 1rem;
}
+8
View File
@@ -7,6 +7,7 @@
export let expanded: boolean = false;
export let animate: boolean = false;
export let collapsed: boolean = false;
export let databasesMainScreen: boolean = false;
let isAnimating = false;
let animationTimeout: ReturnType<typeof setTimeout>;
@@ -43,6 +44,7 @@
class="cover-container"
{style}
class:expanded
class:databasesMainScreen
class:collapsed={animate && collapsed}
class:animating={isAnimating}>
<Layout.Stack direction="row" alignItems="baseline">
@@ -119,6 +121,12 @@
&.animating {
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
&.databasesMainScreen {
@media (min-width: 1440px) {
max-width: 1200px;
}
}
}
.expanded-slot {
+8
View File
@@ -18,6 +18,7 @@
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import { getSidebarState, isInDatabasesRoute, updateSidebarState } from '$lib/helpers/sidebar';
import { isTabletViewport } from '$lib/stores/viewport';
export let showHeader = true;
export let showFooter = true;
@@ -100,8 +101,15 @@
*
* This needs to be handled like this because
* the setup around the sidebar is very tightly configured with 2 states sync.
*
* The sidebar is **always closed** on mobile and tablet devices!
*/
afterNavigate((navigation) => {
if ($isTabletViewport) {
state = 'closed';
return;
}
const isEnteringDatabase = isInDatabasesRoute(navigation.to.route);
const isLeavingDatabase =
isInDatabasesRoute(navigation.from.route) && !isInDatabasesRoute(navigation.to.route);
+10 -11
View File
@@ -3,8 +3,8 @@ import { isSameDay } from '$lib/helpers/date';
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
import TablesApiLight from '$lib/images/promos/tables-api-light.png';
import TablesApiDark from '$lib/images/promos/tables-api-dark.png';
import TimestampOverridesDark from '$lib/images/promos/timestamp-overrides-dark.png';
import TimestampOverridesLight from '$lib/images/promos/timestamp-overrides-light.png';
import TimeHelperQueriesDark from '$lib/images/promos/time-helper-queries-dark.png';
import TimeHelperQueriesLight from '$lib/images/promos/time-helper-queries-light.png';
import OptInRelationDark from '$lib/images/promos/opt-relation-dark.png';
import OptInRelationLight from '$lib/images/promos/opt-relation-light.png';
@@ -32,21 +32,20 @@ if (isCloud) {
show: true
};
const timestampOverridesPromo: BottomModalAlertItem = {
id: 'modal:timestamp_overrides_announcement',
const timeHelperQueriesPromo: BottomModalAlertItem = {
id: 'modal:time_helper_queries_announcement',
src: {
dark: TimestampOverridesDark,
light: TimestampOverridesLight
dark: TimeHelperQueriesDark,
light: TimeHelperQueriesLight
},
title: 'Announcing Timestamp Overrides',
message:
'Move historical data into Appwrite without losing context or disrupting chronological accuracy.',
title: 'Announcing Time helper queries',
message: 'New before/after filters for simpler time-based queries.',
plan: 'free',
importance: 8,
scope: 'project',
cta: {
text: 'Read announcement',
link: () => 'https://appwrite.io/blog/post/announcing-timestamp-overrides',
link: () => 'https://appwrite.io/blog/post/announcing-time-helper-queries',
external: true,
hideOnClick: true
},
@@ -73,7 +72,7 @@ if (isCloud) {
},
show: true
};
listOfPromotions.push(optInRelationPromo, tablesApiPromo, timestampOverridesPromo);
listOfPromotions.push(timeHelperQueriesPromo, optInRelationPromo, tablesApiPromo);
}
export function addBottomModalAlerts() {
@@ -81,7 +81,7 @@
$: projectCreationDisabled =
(isCloud && getServiceLimit('projects') <= data.allProjectsCount) ||
($readOnly && !GRACE_PERIOD_OVERRIDE) ||
(isCloud && $readOnly && !GRACE_PERIOD_OVERRIDE) ||
!$canWriteProjects;
$: $registerCommands([
@@ -106,12 +106,14 @@
function isSetToArchive(project: Models.Project): boolean {
if (!isCloud) return false;
if (!project || !project.$id) return false;
return project.status !== 'active';
return project.status === 'archived';
}
$: projectsToArchive = data.projects.projects.filter((project) => project.status !== 'active');
$: projectsToArchive = isCloud
? data.projects.projects.filter((project) => project.status === 'archived')
: [];
$: activeProjects = data.projects.projects.filter((project) => project.status === 'active');
$: activeProjects = data.projects.projects.filter((project) => project.status !== 'archived');
function clearSearch() {
searchQuery?.clearInput();
}
@@ -1,5 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { onMount } from 'svelte';
import { CardGrid, PaginationInline } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
@@ -28,6 +29,7 @@
IconRefresh
} from '@appwrite.io/pink-icons-svelte';
let limit = $state(5);
let offset = $state(0);
let isLoadingInvoices = $state(false);
let invoiceList: InvoiceList = $state({
@@ -35,16 +37,29 @@
total: 0
});
const limit = 5;
const endpoint = getApiEndpoint();
const hasPaymentError = $derived(invoiceList?.invoices.some((invoice) => invoice?.lastError));
async function request() {
/**
* Special case handling for the first page!
*
* As per Damodar - `there is some logic to **hide current cycle invoice** in the endpoint`.
*
* Due to this, the first page always loads `limit - 1` invoices which is inconsistent!
* Therefore, we load `limit + 1` to counter that so the returned invoices are consistent.
*/
onMount(() => request(true));
async function request(patchQuery: boolean = false) {
isLoadingInvoices = true;
invoiceList = await sdk.forConsole.billing.listInvoices(page.params.organization, [
Query.limit(limit),
Query.offset(offset),
Query.orderDesc('$createdAt')
Query.orderDesc('$createdAt'),
// first page extra must have an extra limit!
Query.limit(patchQuery ? limit + 1 : limit),
// so an invoice isn't repeated on 2nd page!
Query.offset(patchQuery ? offset : offset + 1)
]);
isLoadingInvoices = false;
@@ -62,17 +77,11 @@
}
});
$effect(() => {
if (offset !== null) {
request();
}
});
const columns = $derived([
{ id: 'dueDate', width: { min: 120 } },
{ id: 'status', width: { min: hasPaymentError ? 200 : 100 } },
{ id: 'amount', width: { min: 120 } },
{ id: 'action', width: 40 }
{ id: 'actions', width: 40 }
]);
</script>
@@ -86,11 +95,11 @@
<Table.Header.Cell column="dueDate" {root}>Due date</Table.Header.Cell>
<Table.Header.Cell column="status" {root}>Status</Table.Header.Cell>
<Table.Header.Cell column="amount" {root}>Amount due</Table.Header.Cell>
<Table.Header.Cell column="action" {root} />
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#if isLoadingInvoices}
{#each Array.from({ length: 2 }).keys() as index (index)}
{#each Array.from({ length: 5 }).keys() as index (index)}
<Table.Row.Base {root}>
{#each columns as column}
<Table.Cell column={column.id} {root}>
@@ -99,91 +108,95 @@
{/each}
</Table.Row.Base>
{/each}
{/if}
{#each invoiceList?.invoices as invoice}
{@const status = invoice.status}
<Table.Row.Base {root}>
<Table.Cell column="dueDate" {root}>
{toLocaleDate(invoice.dueAt)}
</Table.Cell>
<Table.Cell column="status" {root}>
{@const isDanger =
status === 'overdue' ||
status === 'failed' ||
status === 'requires_authentication'}
{@const isSuccess = status === 'paid' || status === 'succeeded'}
{@const isWarning = status === 'pending'}
<Layout.Stack direction="row" gap="s">
<Badge
variant="secondary"
content={status === 'requires_authentication'
? 'failed'
: status}
type={isDanger
? 'error'
: isWarning
? 'warning'
: isSuccess
? 'success'
: undefined} />
{#if invoice?.lastError}
<Popover let:toggle>
<Link.Button on:click={toggle}>Details</Link.Button>
<svelte:fragment slot="tooltip">
The scheduled payment has failed.
<Link.Button on:click={() => retryPayment(invoice)}
>Try again
</Link.Button>
</svelte:fragment>
</Popover>
{/if}
</Layout.Stack>
</Table.Cell>
<Table.Cell column="amount" {root}>
{formatCurrency(invoice.grossAmount)}
</Table.Cell>
<Table.Cell column="status" {root}>
<Popover let:toggle placement="bottom-start" padding="none">
<Button text icon ariaLabel="more options" on:click={toggle}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<!-- todo: add missing event -->
<ActionMenu.Item.Anchor
leadingIcon={IconExternalLink}
external
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
View invoice
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor
leadingIcon={IconDownload}
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
Download PDF
</ActionMenu.Item.Anchor>
{#if status === 'overdue' || status === 'failed' || status === 'abandoned'}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={() => {
retryPayment(invoice);
trackEvent(`click_retry_payment`, {
from: 'button',
source: 'billing_invoice_menu'
});
}}>
Retry payment
</ActionMenu.Item.Button>
{:else}
{#each invoiceList?.invoices as invoice (invoice.$id)}
{@const status = invoice.status}
<Table.Row.Base {root}>
<Table.Cell column="dueDate" {root}
>{toLocaleDate(invoice.dueAt)}</Table.Cell>
<Table.Cell column="status" {root}>
{@const isDanger =
status === 'overdue' ||
status === 'failed' ||
status === 'requires_authentication'}
{@const isSuccess = status === 'paid' || status === 'succeeded'}
{@const isWarning = status === 'pending'}
<Layout.Stack direction="row" gap="s">
<Badge
variant="secondary"
content={status === 'requires_authentication'
? 'failed'
: status}
type={isDanger
? 'error'
: isWarning
? 'warning'
: isSuccess
? 'success'
: undefined} />
{#if invoice?.lastError}
<Popover let:toggle>
<Link.Button on:click={toggle}>Details</Link.Button>
<svelte:fragment slot="tooltip">
The scheduled payment has failed.
<Link.Button on:click={() => retryPayment(invoice)}
>Try again
</Link.Button>
</svelte:fragment>
</Popover>
{/if}
</ActionMenu.Root>
</Popover>
</Table.Cell>
</Table.Row.Base>
{/each}
</Layout.Stack>
</Table.Cell>
<Table.Cell column="amount" {root}>
{formatCurrency(invoice.grossAmount)}
</Table.Cell>
<Table.Cell column="actions" {root}>
<Popover let:toggle placement="bottom-start" padding="none">
<Button text icon ariaLabel="more options" on:click={toggle}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<!-- todo: add missing event -->
<ActionMenu.Item.Anchor
leadingIcon={IconExternalLink}
external
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
View invoice
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor
leadingIcon={IconDownload}
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
Download PDF
</ActionMenu.Item.Anchor>
{#if status === 'overdue' || status === 'failed' || status === 'abandoned'}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={() => {
retryPayment(invoice);
trackEvent(`click_retry_payment`, {
from: 'button',
source: 'billing_invoice_menu'
});
}}>
Retry payment
</ActionMenu.Item.Button>
{/if}
</ActionMenu.Root>
</Popover>
</Table.Cell>
</Table.Row.Base>
{/each}
{/if}
</Table.Root>
{#if invoiceList.total > limit}
{#if invoiceList.total >= limit}
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<p class="text">Total results: {invoiceList.total}</p>
<PaginationInline {limit} bind:offset total={invoiceList.total} hidePages />
<PaginationInline
{limit}
hidePages
bind:offset
total={invoiceList.total}
on:change={() => request()} />
</Layout.Stack>
{/if}
{:else}
@@ -84,8 +84,8 @@
{/if}
</Layout.Stack>
<Typography.Text variant="m-400">
Add the following nameservers on your DNS provider. Note that DNS changes may take
time to propagate fully.
Add the following nameservers on your DNS provider. Note that changes may take up to
48 hours to propagate fully.
</Typography.Text>
</Layout.Stack>
@@ -64,8 +64,8 @@
>{selectedDomain.domain}</Typography.Text>
</Layout.Stack>
<Typography.Text variant="m-400">
Add the following nameservers on your DNS provider. Note that DNS changes may take time
to propagate fully.
Add the following nameservers on your DNS provider. Note that changes may take up to 48
hours to propagate fully.
</Typography.Text>
</Layout.Stack>
@@ -18,7 +18,7 @@
const databaseId = page.params.database;
</script>
<Container paddingInlineEnd={false}>
<Container databasesMainScreen>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
@@ -177,7 +177,7 @@
});
</script>
<Container size="xxl">
<Container size="xxl" databasesMainScreen>
<div class="u-flex u-gap-32 u-flex-vertical-mobile">
{#if !isDisabled}
<div class="u-flex-vertical u-gap-16 policies-holder-card">
@@ -38,7 +38,7 @@
].filter((tab) => !tab.disabled);
</script>
<Cover expanded>
<Cover databasesMainScreen>
<svelte:fragment slot="header">
<CoverTitle href={`${base}/project-${page.params.region}-${projectId}/databases`}>
{$database.name}
@@ -61,7 +61,7 @@
</script>
{#if $database}
<Container>
<Container databasesMainScreen>
<CardGrid>
<svelte:fragment slot="title">{$database.name}</svelte:fragment>
<svelte:fragment slot="aside">
@@ -134,7 +134,7 @@
</div>
</section>
</Sidebar.Base>
{:else}
{:else if data?.database?.name}
<Navbar.Base>
<div slot="left">
<Layout.Stack direction="row" alignItems="center" gap="s">
@@ -30,6 +30,7 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
view,
query,
currentSort,
parsedQueries,
rows: await sdk.forProject(params.region, params.project).tablesDB.listRows({
databaseId: params.database,
tableId: params.table,
@@ -10,11 +10,13 @@
row = $bindable(null),
onChange = null,
onRevert = null,
noInlineEdit = false,
openSideSheet = null,
onRowStructureUpdate = null
}: {
row: Models.Row;
column: Columns;
noInlineEdit?: boolean;
openSideSheet?: () => void;
onChange?: (row: Models.DefaultRow) => void;
onRevert?: (row: Models.DefaultRow) => void;
@@ -27,6 +29,11 @@
onMount(() => {
original = structuredClone(row);
if (noInlineEdit) {
openSideSheet?.();
return;
}
const trigger = wrapperEl.querySelector('button.input') as HTMLButtonElement;
if (trigger) {
trigger.click();
@@ -62,5 +69,5 @@
fromSpreadsheet
label={undefined}
bind:formValues={row}
on:click={openSideSheet} />
on:click={() => openSideSheet?.()} />
</div>
@@ -15,7 +15,7 @@
label = undefined;
}
$: if (limited) {
$: {
column.min = isWithinSafeRange(column.min) ? column.min : Number.MIN_SAFE_INTEGER;
column.max = isWithinSafeRange(column.max) ? column.max : Number.MAX_SAFE_INTEGER;
}
@@ -2,8 +2,12 @@ import { page } from '$app/state';
import type { Columns } from '../store';
import { type Models, Query } from '@appwrite.io/console';
export function isRelationshipToMany(column: Models.ColumnRelationship) {
if (!column) return false;
export function isRelationshipToMany(col: Columns) {
if (!col) return false;
if (!isRelationship(col)) return false;
const column = col as Models.ColumnRelationship;
if (!column?.relationType) return false;
if (column?.side === 'child') {
return !['oneToOne', 'oneToMany'].includes(column?.relationType);
@@ -87,6 +87,7 @@
import { hash } from '$lib/helpers/string';
import { formatNumberWithCommas } from '$lib/helpers/numbers';
import { chunks } from '$lib/helpers/array';
import { mapToQueryParams } from '$lib/components/filters/store';
export let data: PageData;
export let showRowCreateSheet: {
@@ -308,26 +309,31 @@
async function sort(query: string | null) {
$spreadsheetLoading = true;
const url = new URL(page.url);
const parsedQueries = data.parsedQueries;
if (query === null) {
if (parsedQueries.size > 0) {
for (const [tagValue, queryString] of parsedQueries.entries()) {
if (queryString.includes('orderAsc') || queryString.includes('orderDesc')) {
parsedQueries.delete(tagValue);
}
}
}
if (query !== null) {
const { attribute, method } = JSON.parse(query);
const tagValue = {
tag: `${attribute} ${method}`,
value: attribute
};
parsedQueries.set(tagValue, query);
}
if (parsedQueries.size === 0) {
url.searchParams.delete('query');
} else {
// compatible with `load` func!
const { attribute, method } = JSON.parse(query);
url.searchParams.set(
'query',
JSON.stringify([
[
{
tag: `${attribute} ${method}`,
value: attribute
},
query
]
])
);
url.searchParams.set('query', mapToQueryParams(parsedQueries));
}
// save > navigate > restore!
@@ -459,6 +465,12 @@
}
}
function openSideSheetForRelationsToMany(tableId: string, rows: string | Models.Row[]) {
$databaseRelatedRowSheetOptions.tableId = tableId;
$databaseRelatedRowSheetOptions.rows = rows;
$databaseRelatedRowSheetOptions.show = true;
}
async function onSelectSheetOption(
action: HeaderCellAction | RowCellAction,
columnId: string,
@@ -605,6 +617,9 @@
return false;
}
const parsedQueries = data.parsedQueries;
const filterQueries = parsedQueries.size ? data.parsedQueries.values() : [];
$paginatedRowsLoading = true;
const loadedRows = await sdk
.forProject(page.params.region, page.params.project)
@@ -615,6 +630,7 @@
getCorrectOrderQuery(),
Query.limit(SPREADSHEET_PAGE_LIMIT),
Query.offset(pageToOffset(pageNumber, SPREADSHEET_PAGE_LIMIT)),
...filterQueries /* filter queries */,
...buildWildcardColumnsQuery($table)
]
});
@@ -885,18 +901,10 @@
{/if}
{:else}
{@const itemsNum = row[columnId]?.length}
<Button.Button
variant="extra-compact"
disabled={!itemsNum}
badge={itemsNum ?? 0}
on:click={() => {
$databaseRelatedRowSheetOptions.show = true;
$databaseRelatedRowSheetOptions.rows =
row[columnId];
$databaseRelatedRowSheetOptions.tableId = columnId;
}}>
Items
</Button.Button>
Items <Badge
content={itemsNum}
variant="secondary"
size="s" />
{/if}
{:else}
{@const value = row[columnId]}
@@ -946,11 +954,20 @@
{row}
column={rowColumn}
onRowStructureUpdate={updateRowContents}
noInlineEdit={isRelationshipToMany(rowColumn)}
onChange={(row) => paginatedRows.update(index, row)}
onRevert={(row) => paginatedRows.update(index, row)}
openSideSheet={() => {
close(); /* closes the editor */
onSelectSheetOption('update', null, 'row', row);
if (isRelationshipToMany(rowColumn)) {
openSideSheetForRelationsToMany(
columnId,
row[columnId]
);
} else {
onSelectSheetOption('update', null, 'row', row);
}
}} />
</svelte:fragment>
</Spreadsheet.Cell>
@@ -1,14 +1,8 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Container, Usage, UsageMultiple } from '$lib/layout';
import { Layout } from '@appwrite.io/pink-svelte';
import { Container, UsageMultiple } from '$lib/layout';
export let data;
$: total = data.tablesTotal;
$: count = data.tables;
$: reads = data.databaseReads;
$: readsTotal = data.databaseReadsTotal;
@@ -16,26 +10,15 @@
$: writesTotal = data.databaseWritesTotal;
</script>
<Container>
<Layout.Stack gap="l">
<Usage
path={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/usage`}
{total}
{count}
countMetadata={{
legend: 'Tables',
title: 'Total tables'
}} />
<UsageMultiple
title="Reads and writes"
showHeader={false}
total={[readsTotal, writesTotal]}
count={[reads, writes]}
legendNumberFormat="abbreviate"
legendData={[
{ name: 'Reads', value: readsTotal },
{ name: 'Writes', value: writesTotal }
]} />
</Layout.Stack>
<Container databasesMainScreen>
<UsageMultiple
title="Reads and writes"
showHeader={false}
total={[readsTotal, writesTotal]}
count={[reads, writes]}
legendNumberFormat="abbreviate"
legendData={[
{ name: 'Reads', value: readsTotal },
{ name: 'Writes', value: writesTotal }
]} />
</Container>
@@ -25,8 +25,8 @@
<Badge variant="secondary" type="warning" content="Pending verification" />
</Layout.Stack>
<Typography.Text variant="m-400">
Add the following nameservers on your DNS provider. Note that DNS changes may
take time to propagate fully.
Add the following nameservers on your DNS provider. Note that changes may take
up to 48 hours to propagate fully.
</Typography.Text>
</Layout.Stack>