setTimeout(() => (content = copyText))}>
-
+ onclick={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ handleClick();
+ }}
+ onkeyup={clickOnEnter}
+ onmouseenter={() => setTimeout(() => (content = copyText))}>
+ {@render children?.()}
{#if showing}
diff --git a/src/lib/components/creditCardBrandImage.svelte b/src/lib/components/creditCardBrandImage.svelte
index 43e8dadc0..18f4d2474 100644
--- a/src/lib/components/creditCardBrandImage.svelte
+++ b/src/lib/components/creditCardBrandImage.svelte
@@ -10,7 +10,11 @@
export let height = 16;
$: ccImage = isValueOfStringEnum(CreditCard, brand)
- ? sdk.forConsole.avatars.getCreditCard(brand, width, height)
+ ? sdk.forConsole.avatars.getCreditCard({
+ code: brand,
+ width,
+ height
+ })
: '';
diff --git a/src/lib/components/csvImportBox.svelte b/src/lib/components/csvImportBox.svelte
index 7b3fcc920..8ff2838ab 100644
--- a/src/lib/components/csvImportBox.svelte
+++ b/src/lib/components/csvImportBox.svelte
@@ -11,9 +11,13 @@
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import { type Models, type Payload, Query } from '@appwrite.io/console';
+ // re-render the key for sheet UI.
+ import { hash } from '$lib/helpers/string';
+ import { spreadsheetRenderKey } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store';
+
type ImportItem = {
status: string;
- collection?: string;
+ table?: string;
};
type ImportItemsMap = Map;
@@ -22,15 +26,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 = 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 +48,21 @@
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);
+ $spreadsheetRenderKey = hash(Date.now().toString());
}
}
@@ -70,19 +71,22 @@
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;
+ .tablesDB.getTable({
+ databaseId,
+ tableId
+ });
+ tableName = table.name;
} catch {
- collectionName = null;
+ tableName = null;
}
}
@@ -99,12 +103,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);
}
}
@@ -146,10 +150,12 @@
onMount(() => {
sdk.forProject(page.params.region, page.params.project)
- .migrations.list([
- Query.equal('source', 'CSV'),
- Query.equal('status', ['pending', 'processing'])
- ])
+ .migrations.list({
+ queries: [
+ Query.equal('source', 'CSV'),
+ Query.equal('status', ['pending', 'processing'])
+ ]
+ })
.then((migrations) => {
migrations.migrations.forEach(updateOrAddItem);
});
@@ -172,7 +178,7 @@
- {#each [...$importItems.entries()] as [key, value] (key)}
-
-
- -
-
-
-
- {@html text(value.status, value.collection)}
-
-
-
-
-
-
-
-
- {/each}
+
+ {#each [...$importItems.entries()] as [key, value] (key)}
+
+
+ -
+
+
+
+ {@html text(value.status, value.table)}
+
+
+
+
+
+
+
+
+ {/each}
+
{/if}
diff --git a/src/lib/components/filters/store.ts b/src/lib/components/filters/store.ts
index 6be45f6a0..1561b2d30 100644
--- a/src/lib/components/filters/store.ts
+++ b/src/lib/components/filters/store.ts
@@ -1,10 +1,10 @@
import { goto } from '$app/navigation';
-import { derived, get, writable } from 'svelte/store';
import { page } from '$app/stores';
import deepEqual from 'deep-equal';
-import type { Column, ColumnType } from '$lib/helpers/types';
import { Query } from '@appwrite.io/console';
import { toLocaleDateTime } from '$lib/helpers/date';
+import { derived, get, writable } from 'svelte/store';
+import type { Column, ColumnType } from '$lib/helpers/types';
export type TagValue = {
tag: string;
@@ -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;
};
@@ -59,9 +59,15 @@ function initQueries(initialValue = new Map()) {
}
function apply() {
- const queryParam = mapToQueryParams(get(queries));
+ const usableQueries = get(queries);
const currentLocation = window.location.pathname;
- goto(`${currentLocation}?query=${queryParam}`, { noScroll: true });
+
+ if (usableQueries.size) {
+ const queryParam = mapToQueryParams(get(queries));
+ goto(`${currentLocation}?query=${queryParam}`, { noScroll: true });
+ } else {
+ goto(currentLocation, { noScroll: true });
+ }
}
return {
@@ -134,7 +140,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 +255,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 +265,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}**` };
}
}
diff --git a/src/lib/components/git/connectRepoModal.svelte b/src/lib/components/git/connectRepoModal.svelte
index bf2685c65..68acdab02 100644
--- a/src/lib/components/git/connectRepoModal.svelte
+++ b/src/lib/components/git/connectRepoModal.svelte
@@ -56,7 +56,11 @@
if (repositoryBehaviour === 'new') {
const repo = await sdk
.forProject(page.params.region, page.params.project)
- .vcs.createRepository($installation.$id, repositoryName, repositoryPrivate);
+ .vcs.createRepository({
+ installationId: $installation.$id,
+ name: repositoryName,
+ xprivate: repositoryPrivate
+ });
repository.set(repo);
selectedRepository = repo.id;
}
diff --git a/src/lib/components/git/productionBranchFieldset.svelte b/src/lib/components/git/productionBranchFieldset.svelte
index de059a73c..addd40557 100644
--- a/src/lib/components/git/productionBranchFieldset.svelte
+++ b/src/lib/components/git/productionBranchFieldset.svelte
@@ -18,13 +18,19 @@
async function loadBranches() {
const repo = await sdk
.forProject(page.params.region, page.params.project)
- .vcs.getRepository(installationId, repositoryId);
+ .vcs.getRepository({
+ installationId,
+ providerRepositoryId: repositoryId
+ });
branch = repo.defaultBranch ?? 'main';
const { branches } = await sdk
.forProject(page.params.region, page.params.project)
- .vcs.listRepositoryBranches(installationId, repositoryId);
+ .vcs.listRepositoryBranches({
+ installationId,
+ providerRepositoryId: repositoryId
+ });
return sortBranches(branches);
}
diff --git a/src/lib/components/git/repositories.svelte b/src/lib/components/git/repositories.svelte
index f67a5a0c3..260576f32 100644
--- a/src/lib/components/git/repositories.svelte
+++ b/src/lib/components/git/repositories.svelte
@@ -107,21 +107,21 @@
$repositories.repositories = (
(await sdk
.forProject(page.params.region, page.params.project)
- .vcs.listRepositories(
+ .vcs.listRepositories({
installationId,
- VCSDetectionType.Runtime,
- search || undefined
- )) as unknown as Models.ProviderRepositoryRuntimeList
+ type: VCSDetectionType.Runtime,
+ search: search || undefined
+ })) as unknown as Models.ProviderRepositoryRuntimeList
).runtimeProviderRepositories; //TODO: remove forced cast after backend fixes
} else {
$repositories.repositories = (
(await sdk
.forProject(page.params.region, page.params.project)
- .vcs.listRepositories(
+ .vcs.listRepositories({
installationId,
- VCSDetectionType.Framework,
- search || undefined
- )) as unknown as Models.ProviderRepositoryFrameworkList
+ type: VCSDetectionType.Framework,
+ search: search || undefined
+ })) as unknown as Models.ProviderRepositoryFrameworkList
).frameworkProviderRepositories;
}
$repositories.search = search;
diff --git a/src/lib/components/git/selectRootModal.svelte b/src/lib/components/git/selectRootModal.svelte
index 7caa5b58d..bc817623e 100644
--- a/src/lib/components/git/selectRootModal.svelte
+++ b/src/lib/components/git/selectRootModal.svelte
@@ -43,7 +43,12 @@
try {
const content = await sdk
.forProject(page.params.region, page.params.project)
- .vcs.getRepositoryContents($installation.$id, $repository.id, currentPath, branch);
+ .vcs.getRepositoryContents({
+ installationId: $installation.$id,
+ providerRepositoryId: $repository.id,
+ providerRootDirectory: currentPath,
+ providerReference: branch
+ });
directories[0].fileCount = content.contents?.length ?? 0;
directories[0].children = content.contents
.filter((e) => e.isDirectory)
@@ -83,7 +88,12 @@
try {
const content = await sdk
.forProject(page.params.region, page.params.project)
- .vcs.getRepositoryContents($installation.$id, $repository.id, path, branch);
+ .vcs.getRepositoryContents({
+ installationId: $installation.$id,
+ providerRepositoryId: $repository.id,
+ providerRootDirectory: path,
+ providerReference: branch
+ });
const fileCount = content.contents?.length ?? 0;
const contentDirectories = content.contents.filter((e) => e.isDirectory);
@@ -101,12 +111,15 @@
}));
const runtime = await sdk
.forProject(page.params.region, page.params.project)
- .vcs.createRepositoryDetection(
- $installation.$id,
- $repository.id,
- product === 'sites' ? VCSDetectionType.Framework : VCSDetectionType.Runtime,
- path
- );
+ .vcs.createRepositoryDetection({
+ installationId: $installation.$id,
+ providerRepositoryId: $repository.id,
+ type:
+ product === 'sites'
+ ? VCSDetectionType.Framework
+ : VCSDetectionType.Runtime,
+ providerRootDirectory: path
+ });
if (product === 'sites') {
currentDir.children.forEach((dir) => {
dir.thumbnailUrl = $iconPath(runtime.framework, 'color');
diff --git a/src/lib/components/id.svelte b/src/lib/components/id.svelte
index a2e19a8c8..c756ee8f6 100644
--- a/src/lib/components/id.svelte
+++ b/src/lib/components/id.svelte
@@ -45,13 +45,18 @@
import { Icon, Tag } from '@appwrite.io/pink-svelte';
import { Copy } from '.';
import { IconDuplicate } from '@appwrite.io/pink-icons-svelte';
+ import type { TooltipPlacement } from '$lib/components/copy.svelte';
export let value: string;
export let event: string = null;
+
+ export let tooltipPortal = false;
+ export let tooltipDelay: number = 0;
+ export let tooltipPlacement: TooltipPlacement = undefined;
{#key value}
-
+
import type { HTMLAttributes } from 'svelte/elements';
- export type NavbarProject = {
- name: string;
- $id: string;
- region: string;
- isSelected: boolean;
- platformCount: number;
- pingCount: number;
- };
-
export type BaseNavbarProps = HTMLAttributes & {
logo: {
src: string;
@@ -106,7 +97,6 @@
export let sideBarIsOpen: $$Props['sideBarIsOpen'] = false;
export let showAccountMenu = false;
export let currentProject: Models.Project = undefined;
- export let projects: Promise = undefined;
let activeTheme = $app.theme;
let shouldAnimateThemeToggle = false;
@@ -138,7 +128,9 @@
class="only-desktop">
-
+
+
+
{#if page.route?.id?.includes('/project-[region]-[project]') && currentProject && currentProject.pingCount === 0}
-
+
diff --git a/src/lib/components/paginationWithLimit.svelte b/src/lib/components/paginationWithLimit.svelte
index 089019adc..7cf78f6b5 100644
--- a/src/lib/components/paginationWithLimit.svelte
+++ b/src/lib/components/paginationWithLimit.svelte
@@ -3,13 +3,29 @@
import Pagination from './pagination.svelte';
import { Layout } from '@appwrite.io/pink-svelte';
- export let limit: number;
- export let offset: number;
- export let total: number;
- export let name: string;
+ let {
+ limit,
+ offset,
+ total,
+ name,
+ useCreateLink = true
+ }: {
+ limit: number;
+ offset: number;
+ total: number;
+ name: string;
+ useCreateLink?: boolean;
+ } = $props();
+
+ const showLimit = $derived(!!useCreateLink);
+ const direction = $derived(showLimit ? 'row' : 'column');
+ const alignItems = $derived(showLimit ? 'center' : 'flex-end');
-
-
-
+
+ {#if showLimit}
+
+ {/if}
+
+
diff --git a/src/lib/components/permissions/label.svelte b/src/lib/components/permissions/label.svelte
index 74ed9db0c..415d66379 100644
--- a/src/lib/components/permissions/label.svelte
+++ b/src/lib/components/permissions/label.svelte
@@ -30,7 +30,7 @@
$: disabled = !value || $groups.has(value);
-
+
Labels allow you to grant access to users with the specified label.
diff --git a/src/lib/components/permissions/row.svelte b/src/lib/components/permissions/row.svelte
index 31a6fd4db..8706f77a7 100644
--- a/src/lib/components/permissions/row.svelte
+++ b/src/lib/components/permissions/row.svelte
@@ -29,13 +29,13 @@
if (role === 'user') {
const user = await sdk
.forProject(page.params.region, page.params.project)
- .users.get(id);
+ .users.get({ userId: id });
return user;
}
if (role === 'team') {
const team = await sdk
.forProject(page.params.region, page.params.project)
- .teams.get(id);
+ .teams.get({ teamId: id });
return team;
}
}
diff --git a/src/lib/components/permissions/team.svelte b/src/lib/components/permissions/team.svelte
index c5d21e5f5..02867a7fd 100644
--- a/src/lib/components/permissions/team.svelte
+++ b/src/lib/components/permissions/team.svelte
@@ -45,9 +45,10 @@
async function request() {
if (!show) return;
isLoading = true;
- results = await sdk
- .forProject(page.params.region, page.params.project)
- .teams.list([Query.limit(5), Query.offset(offset)], search || undefined);
+ results = await sdk.forProject(page.params.region, page.params.project).teams.list({
+ queries: [Query.limit(5), Query.offset(offset)],
+ search: search || undefined
+ });
isLoading = false;
}
diff --git a/src/lib/components/permissions/user.svelte b/src/lib/components/permissions/user.svelte
index 64f687a69..bf051e76d 100644
--- a/src/lib/components/permissions/user.svelte
+++ b/src/lib/components/permissions/user.svelte
@@ -48,9 +48,10 @@
async function request() {
if (!show) return;
isLoading = true;
- results = await sdk
- .forProject(page.params.region, page.params.project)
- .users.list([Query.limit(5), Query.offset(offset)], search || undefined);
+ results = await sdk.forProject(page.params.region, page.params.project).users.list({
+ queries: [Query.limit(5), Query.offset(offset)],
+ search: search || undefined
+ });
isLoading = false;
}
diff --git a/src/lib/components/regionEndpoint.svelte b/src/lib/components/regionEndpoint.svelte
index adcd0e0d6..aeaef43de 100644
--- a/src/lib/components/regionEndpoint.svelte
+++ b/src/lib/components/regionEndpoint.svelte
@@ -11,7 +11,12 @@
$: flagSrc =
region && isValueOfStringEnum(Flag, region.flag)
- ? sdk.forConsole.avatars.getFlag(region.flag, 30, 20, 100)
+ ? sdk.forConsole.avatars.getFlag({
+ code: region.flag,
+ width: 30,
+ height: 20,
+ quality: 100
+ })
: '';
diff --git a/src/lib/components/sidebar.svelte b/src/lib/components/sidebar.svelte
index 9c128d556..65b18ae1a 100644
--- a/src/lib/components/sidebar.svelte
+++ b/src/lib/components/sidebar.svelte
@@ -1,7 +1,7 @@
@@ -101,7 +102,7 @@
updateSidebarState(event.detail)}
+ on:resize={(event) => updateSidebarState($page, event.detail)}
resizable>
@@ -240,7 +241,9 @@
{projectOption.name}
{/each}
- {#if project}
+ {#if project && $isSmallViewport}
+
+
{#if subNavigation}
-
+
{/if}
@@ -608,8 +614,9 @@
.sub-navigation {
margin-top: 48px;
+
@media (min-width: 1024px) {
- margin-top: 0;
+ margin-top: var(--banner-spacing, 0px);
width: 400px;
height: calc(100vh - 48px);
display: flex;
@@ -623,6 +630,17 @@
&.icons {
width: 266px;
transition: width 0.3s linear;
+
+ & :global(nav) {
+ margin-top: var(--banner-spacing);
+ }
+ }
+ }
+
+ /* when in small or tablet viewport and there's a banner */
+ @media (max-width: 1023px) {
+ & :global(header) {
+ margin-top: var(--banner-spacing);
}
}
}
diff --git a/src/lib/components/sortButton.svelte b/src/lib/components/sortButton.svelte
new file mode 100644
index 000000000..d202cf664
--- /dev/null
+++ b/src/lib/components/sortButton.svelte
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
diff --git a/src/lib/components/viewSelector.svelte b/src/lib/components/viewSelector.svelte
index 02f4dcbf2..7523c6f67 100644
--- a/src/lib/components/viewSelector.svelte
+++ b/src/lib/components/viewSelector.svelte
@@ -6,23 +6,73 @@
import { IconViewBoards } from '@appwrite.io/pink-icons-svelte';
import ViewToggle from './viewToggle.svelte';
import ColumnSelector from './columnSelector.svelte';
+ import { isSmallViewport } from '$lib/stores/viewport';
+ import { preferences } from '$lib/stores/preferences';
+ import { page } from '$app/state';
+ import { hash } from '$lib/helpers/string';
- export let columns: Writable
;
- export let view: View;
- export let isCustomCollection = false;
- export let hideView = false;
- export let hideColumns = false;
- export let allowNoColumns = false;
+ interface Props {
+ onlyIcon?: boolean;
+ ui?: 'legacy' | 'new';
+ columns: Writable;
+ view: View;
+ isCustomTable?: boolean;
+ hideView?: boolean;
+ hideColumns?: boolean;
+ allowNoColumns?: boolean;
+ showAnyway?: boolean;
+ }
+
+ let {
+ onlyIcon = false,
+ ui = 'legacy',
+ columns,
+ view = $bindable(),
+ isCustomTable = false,
+ hideView = false,
+ hideColumns = false,
+ allowNoColumns = false,
+ showAnyway = false
+ }: Props = $props();
+
+ let showCountBadge = $state(false);
+ const preferenceKey = $derived(getPreferenceKey());
+
+ function getPreferenceKey(): string {
+ const tableId = page.params.table;
+ const organizationId = page.data.organization?.$id ?? page.data.project?.teamId;
+ return hash([organizationId, tableId].filter(Boolean).join('#'));
+ }
+
+ function updateBadgeState() {
+ if (!preferences.getKey(preferenceKey, false)) {
+ showCountBadge = true;
+ preferences.setKey(preferenceKey, true);
+ }
+ }
+
+ $effect(() => {
+ showCountBadge = !onlyIcon || !!preferences.getKey(preferenceKey, false);
+ });
{#if !hideColumns && view === View.Table}
-
+
{#snippet children(toggle, selectedColumnsNumber)}
+ disabled={!$columns.length && showAnyway}
+ class={onlyIcon && !$isSmallViewport ? 'width-fix' : undefined}
+ badge={showCountBadge ? selectedColumnsNumber.toString() : undefined}>
{/snippet}
@@ -32,3 +82,10 @@
{#if !hideView}
{/if}
+
+
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 7414a2ffa..a2d138457 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,4 +1,5 @@
export const PAGE_LIMIT = 12; // default page limit
+export const SPREADSHEET_PAGE_LIMIT = 50; // default sheet page limit
export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
export const NEW_DEV_PRO_UPGRADE_COUPON = 'appw50';
@@ -6,10 +7,12 @@ export const NEW_DEV_PRO_UPGRADE_COUPON = 'appw50';
export const REGION_FRA = 'fra';
export const REGION_SYD = 'syd';
export const REGION_NYC = 'nyc';
+export const REGION_SFO = 'sfo';
export const SUBDOMAIN_FRA = 'fra.';
export const SUBDOMAIN_SYD = 'syd.';
export const SUBDOMAIN_NYC = 'nyc.';
+export const SUBDOMAIN_SFO = 'sfo.';
export enum Dependencies {
FACTORS = 'dependency:factors',
@@ -38,9 +41,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 +67,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 +95,8 @@ export const defaultScopes: string[] = [
'teams.write',
'documents.read',
'documents.write',
+ 'rows.read',
+ 'rows.write',
'files.read',
'files.write',
'projects.read',
@@ -111,6 +116,8 @@ export const defaultScopes: string[] = [
'databases.write',
'collections.read',
'collections.write',
+ 'tables.read',
+ 'tables.write',
'buckets.read',
'buckets.write',
'functions.read',
@@ -141,6 +148,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 +210,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 +235,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 +272,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 +475,7 @@ export type EventResource = {
export type EventAction = {
name: string;
- attributes?: string[];
+ columns?: string[];
};
export const eventServices: Array = [
@@ -448,11 +493,11 @@ export const eventServices: Array = [
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 +524,7 @@ export const eventServices: Array = [
name: 'memberships',
actions: [
{ name: 'create' },
- { name: 'update', attributes: ['status'] },
+ { name: 'update', columns: ['status'] },
{ name: 'delete' }
]
}
@@ -495,7 +540,7 @@ export const eventServices: Array = [
],
actions: [
{ name: 'create' },
- { name: 'update', attributes: ['email', 'name', 'password', 'status', 'prefs'] },
+ { name: 'update', columns: ['email', 'name', 'password', 'status', 'prefs'] },
{ name: 'delete' }
]
},
diff --git a/src/lib/elements/flag.svelte b/src/lib/elements/flag.svelte
index 9fc0f2dcf..5a4809db3 100644
--- a/src/lib/elements/flag.svelte
+++ b/src/lib/elements/flag.svelte
@@ -14,7 +14,12 @@
export function getFlag(country: string, width: number, height: number, quality: number) {
if (!isValueOfStringEnum(Flag, country)) return '';
return sdk.forConsole.avatars
- .getFlag(country, width * 2, height * 2, quality)
+ .getFlag({
+ code: country,
+ width: width * 2,
+ height: height * 2,
+ quality
+ })
?.toString()
?.replace('&project=console', '&mode=admin');
}
diff --git a/src/lib/elements/forms/Seekbar.svelte b/src/lib/elements/forms/Seekbar.svelte
new file mode 100644
index 000000000..7fa184e8e
--- /dev/null
+++ b/src/lib/elements/forms/Seekbar.svelte
@@ -0,0 +1,315 @@
+
+
+
+
+ {#if maxAllowed < max}
+
+
+ {/if}
+
+
+
+
+
+
+
+
!disabled}
+ onmouseleave={() => !isDragging}>
+
+
+
+
+ {#if exactBreakpoints().length > 0}
+
+ {#each exactBreakpoints() as breakPoint, index (index)}
+ {@const breakPercentage = ((breakPoint - min) / range) * 100}
+
+
+ {/each}
+
+ {/if}
+
+
+
diff --git a/src/lib/elements/forms/index.ts b/src/lib/elements/forms/index.ts
index 938532339..20b970669 100644
--- a/src/lib/elements/forms/index.ts
+++ b/src/lib/elements/forms/index.ts
@@ -30,3 +30,4 @@ export { default as InputTime } from './inputTime.svelte';
export { default as InputDigits } from './inputDigits.svelte';
export { default as InputSelectCheckbox } from './inputSelectCheckbox.svelte';
export { default as InputFilePicker } from './inputFilePicker.svelte';
+export { default as Seekbar } from './Seekbar.svelte';
diff --git a/src/lib/elements/forms/inputId.svelte b/src/lib/elements/forms/inputId.svelte
index 5c8511b28..84d9263d6 100644
--- a/src/lib/elements/forms/inputId.svelte
+++ b/src/lib/elements/forms/inputId.svelte
@@ -31,6 +31,7 @@
maxlength={36}
autocomplete="off"
bind:value
+ on:input
on:invalid={handleInvalid}
state={error ? 'error' : 'default'}
helper="Allowed characters: alphanumeric, non-leading hyphen, underscore, period" />
diff --git a/src/lib/elements/forms/inputNumber.svelte b/src/lib/elements/forms/inputNumber.svelte
index 71d08161f..2abf5228a 100644
--- a/src/lib/elements/forms/inputNumber.svelte
+++ b/src/lib/elements/forms/inputNumber.svelte
@@ -18,6 +18,19 @@
let error: string;
+ function coerceToNumber(event: Event & { currentTarget: EventTarget & HTMLInputElement }) {
+ const raw = event.currentTarget?.value ?? '';
+ if (raw === '') {
+ value = nullable ? null : (undefined as unknown as number);
+ return;
+ }
+
+ const parsed = Number(raw);
+ if (Number.isFinite(parsed)) {
+ value = parsed;
+ }
+ }
+
const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
event.preventDefault();
@@ -39,7 +52,7 @@
error = event.currentTarget.validationMessage;
};
- $: if (value) {
+ $: if (value !== null && value !== undefined && !Number.isNaN(value)) {
error = null;
}
@@ -61,7 +74,8 @@
helper={error || helper}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
- on:input>
+ on:input={coerceToNumber}
+ on:change={coerceToNumber}>
diff --git a/src/lib/elements/forms/inputProjectId.svelte b/src/lib/elements/forms/inputProjectId.svelte
index 0fa91b895..2f0552e2f 100644
--- a/src/lib/elements/forms/inputProjectId.svelte
+++ b/src/lib/elements/forms/inputProjectId.svelte
@@ -30,6 +30,7 @@
maxlength={36}
autocomplete="off"
bind:value
+ on:input
on:invalid={handleInvalid}
state={error ? 'error' : 'default'}
helper="Allowed characters: lowercase alphanumeric and non-leading hyphen" />
diff --git a/src/lib/elements/forms/inputSelect.svelte b/src/lib/elements/forms/inputSelect.svelte
index 6d8fd19c0..7e36cee97 100644
--- a/src/lib/elements/forms/inputSelect.svelte
+++ b/src/lib/elements/forms/inputSelect.svelte
@@ -6,6 +6,7 @@
export let label: string | undefined = undefined;
export let value: string | number | boolean | null;
export let helper: string | undefined = undefined;
+ export let autofocus: boolean | undefined = undefined;
export let optionalText: string | number | boolean | null | undefined = undefined;
export let placeholder = '';
export let required = false;
@@ -54,6 +55,7 @@
{optionalText}
{placeholder}
{disabled}
+ {autofocus}
helper={error ?? helper}
{required}
state={error ? 'error' : 'default'}
diff --git a/src/lib/elements/forms/inputTextarea.svelte b/src/lib/elements/forms/inputTextarea.svelte
index d6fc25d18..9550900f3 100644
--- a/src/lib/elements/forms/inputTextarea.svelte
+++ b/src/lib/elements/forms/inputTextarea.svelte
@@ -51,4 +51,5 @@
on:input
bind:value>
+
diff --git a/src/lib/flags.ts b/src/lib/flags.ts
index 50b902b95..70bba582a 100644
--- a/src/lib/flags.ts
+++ b/src/lib/flags.ts
@@ -19,6 +19,5 @@ function isFlagEnabled(name: string) {
}
export const flags = {
- showSites: isFlagEnabled('sites'),
- showIndexLengths: isFlagEnabled('index-lengths')
+ showSites: isFlagEnabled('sites')
};
diff --git a/src/lib/helpers/domains.ts b/src/lib/helpers/domains.ts
index d8a65c745..840eb4b73 100644
--- a/src/lib/helpers/domains.ts
+++ b/src/lib/helpers/domains.ts
@@ -4,188 +4,188 @@ import type { Models } from '@appwrite.io/console';
export async function createRecord(record: Partial, domainId: string) {
switch (record.type) {
case 'A':
- return await sdk.forConsole.domains.createRecordA(
+ return await sdk.forConsole.domains.createRecordA({
domainId,
- record.name,
- record.value,
- record.ttl,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record?.comment || undefined
+ });
case 'AAAA':
- return await sdk.forConsole.domains.createRecordAAAA(
+ return await sdk.forConsole.domains.createRecordAAAA({
domainId,
- record.name,
- record.value,
- record.ttl,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record?.comment || undefined
+ });
case 'CNAME':
- return await sdk.forConsole.domains.createRecordCNAME(
+ return await sdk.forConsole.domains.createRecordCNAME({
domainId,
- record.name,
- record.value,
- record.ttl,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record?.comment || undefined
+ });
case 'MX':
- return await sdk.forConsole.domains.createRecordMX(
+ return await sdk.forConsole.domains.createRecordMX({
domainId,
- record.name,
- record.value,
- record.ttl,
- record.priority || 10,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ priority: record.priority || 10,
+ comment: record?.comment || undefined
+ });
case 'TXT':
- return await sdk.forConsole.domains.createRecordTXT(
+ return await sdk.forConsole.domains.createRecordTXT({
domainId,
- record.name,
- record.ttl,
- record.value,
- record?.comment || undefined
- );
+ name: record.name!,
+ ttl: record.ttl!,
+ value: record.value!,
+ comment: record?.comment || undefined
+ });
case 'NS':
- return await sdk.forConsole.domains.createRecordNS(
+ return await sdk.forConsole.domains.createRecordNS({
domainId,
- record.name,
- record.value,
- record.ttl,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record?.comment || undefined
+ });
case 'SRV':
- return await sdk.forConsole.domains.createRecordSRV(
+ return await sdk.forConsole.domains.createRecordSRV({
domainId,
- record.name,
- record.value,
- record.ttl,
- record.priority,
- record.weight,
- record.port,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ priority: record.priority!,
+ weight: record.weight!,
+ port: record.port!,
+ comment: record?.comment || undefined
+ });
case 'CAA':
- return await sdk.forConsole.domains.createRecordCAA(
+ return await sdk.forConsole.domains.createRecordCAA({
domainId,
- record.name,
- record.value,
- record.ttl,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record?.comment || undefined
+ });
case 'HTTPS':
- return await sdk.forConsole.domains.createRecordHTTPS(
+ return await sdk.forConsole.domains.createRecordHTTPS({
domainId,
- record.name,
- record.value,
- record.ttl,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record?.comment || undefined
+ });
case 'ALIAS':
- return await sdk.forConsole.domains.createRecordAlias(
+ return await sdk.forConsole.domains.createRecordAlias({
domainId,
- record.name,
- record.value,
- record.ttl,
- record?.comment || undefined
- );
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record?.comment || undefined
+ });
}
}
export async function updateRecord(record: Partial, domainId: string) {
switch (record.type) {
case 'A':
- return await sdk.forConsole.domains.updateRecordA(
+ return await sdk.forConsole.domains.updateRecordA({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record.comment || undefined
+ });
case 'AAAA':
- return await sdk.forConsole.domains.updateRecordAAAA(
+ return await sdk.forConsole.domains.updateRecordAAAA({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record.comment || undefined
+ });
case 'CNAME':
- return await sdk.forConsole.domains.updateRecordCNAME(
+ return await sdk.forConsole.domains.updateRecordCNAME({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record.comment || undefined
+ });
case 'MX':
- return await sdk.forConsole.domains.updateRecordMX(
+ return await sdk.forConsole.domains.updateRecordMX({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.priority,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ priority: record.priority!,
+ comment: record.comment || undefined
+ });
case 'TXT':
- return await sdk.forConsole.domains.updateRecordTXT(
+ return await sdk.forConsole.domains.updateRecordTXT({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record.comment || undefined
+ });
case 'NS':
- return await sdk.forConsole.domains.updateRecordNS(
+ return await sdk.forConsole.domains.updateRecordNS({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record.comment || undefined
+ });
case 'SRV':
- return await sdk.forConsole.domains.updateRecordSRV(
+ return await sdk.forConsole.domains.updateRecordSRV({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.priority,
- record.weight,
- record.port,
- record?.comment || undefined
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ priority: record.priority!,
+ weight: record.weight!,
+ port: record.port!,
+ comment: record.comment || undefined
+ });
case 'CAA':
- return await sdk.forConsole.domains.updateRecordCAA(
+ return await sdk.forConsole.domains.updateRecordCAA({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record.comment || undefined
+ });
case 'HTTPS':
- return await sdk.forConsole.domains.updateRecordHTTPS(
+ return await sdk.forConsole.domains.updateRecordHTTPS({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record.comment || undefined
+ });
case 'ALIAS':
- return await sdk.forConsole.domains.updateRecordAlias(
+ return await sdk.forConsole.domains.updateRecordAlias({
domainId,
- record.$id,
- record.name,
- record.value,
- record.ttl,
- record.comment
- );
+ recordId: record.$id!,
+ name: record.name!,
+ value: record.value!,
+ ttl: record.ttl!,
+ comment: record.comment || undefined
+ });
}
}
diff --git a/src/lib/helpers/faker.ts b/src/lib/helpers/faker.ts
new file mode 100644
index 000000000..8cfc2348e
--- /dev/null
+++ b/src/lib/helpers/faker.ts
@@ -0,0 +1,197 @@
+import { faker } from '@faker-js/faker';
+import type { Columns } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store';
+import { ID, type Models } from '@appwrite.io/console';
+import { sdk } from '$lib/stores/sdk';
+
+export async function generateColumns(
+ project: Models.Project,
+ databaseId: string,
+ tableId: string
+): Promise {
+ const client = sdk.forProject(project.region, project.$id);
+
+ return await Promise.all([
+ client.tablesDB.createStringColumn({
+ databaseId,
+ tableId,
+ key: 'name',
+ size: 255,
+ required: false
+ }),
+ client.tablesDB.createEmailColumn({ databaseId, tableId, key: 'email', required: false }),
+ client.tablesDB.createIntegerColumn({
+ databaseId,
+ tableId,
+ key: 'age',
+ required: false,
+ min: 18,
+ max: 80
+ }),
+ client.tablesDB.createStringColumn({
+ databaseId,
+ tableId,
+ key: 'city',
+ size: 100,
+ required: false
+ }),
+ client.tablesDB.createStringColumn({
+ databaseId,
+ tableId,
+ key: 'description',
+ size: 1000,
+ required: false
+ }),
+ client.tablesDB.createBooleanColumn({ databaseId, tableId, key: 'active', required: false })
+ ]);
+}
+
+export function generateFakeRecords(
+ columns: Columns[],
+ count: number
+): {
+ ids: string[];
+ rows: Models.Row[];
+} {
+ if (count <= 0) return { ids: [], rows: [] };
+
+ const filteredColumns = columns.filter((col) => col.type !== 'relationship');
+
+ const ids: string[] = [];
+ const rows: Models.Row[] = [];
+
+ for (let i = 0; i < count; i++) {
+ const id = ID.unique();
+ ids.push(id);
+
+ let row: Record> = {
+ $id: id
+ };
+
+ if (filteredColumns.length === 0) {
+ row = {
+ $id: id,
+ name: faker.person.fullName(),
+ email: faker.internet.email(),
+ age: faker.number.int({ min: 18, max: 80 }),
+ city: faker.location.city(),
+ description: faker.lorem.sentence(),
+ active: faker.datatype.boolean()
+ };
+ } else {
+ for (const column of filteredColumns) {
+ row[column.key] = generateValueForColumn(column);
+ }
+ }
+
+ rows.push(row as unknown as Models.Row);
+ }
+
+ return { ids, rows };
+}
+
+function generateStringValue(key: string, maxLength: number): string {
+ const lowerKey = key.toLowerCase();
+
+ const generators: Record string> = {
+ name: () => faker.person.fullName(),
+ email: () => faker.internet.email(),
+ title: () => faker.lorem.words({ min: 1, max: 3 }),
+ description: () => faker.lorem.paragraph(),
+ content: () => faker.lorem.paragraph(),
+ address: () => faker.location.streetAddress(),
+ city: () => faker.location.city(),
+ country: () => faker.location.country(),
+ company: () => faker.company.name()
+ };
+
+ const matchingKey = Object.keys(generators).find((pattern) => lowerKey.includes(pattern));
+
+ const text = matchingKey ? generators[matchingKey]() : faker.lorem.words({ min: 1, max: 5 });
+
+ return text.length <= maxLength ? text : text.substring(0, maxLength);
+}
+
+function generateValueForColumn(
+ column: Columns
+): string | number | boolean | null | Array {
+ if (column.array) {
+ const arraySize = faker.number.int({ min: 1, max: 5 });
+ const items: Array = [];
+
+ for (let i = 0; i < arraySize; i++) {
+ const itemAttribute = { ...column, array: false };
+ const item = generateSingleValue(itemAttribute);
+ if (item !== null) {
+ items.push(item);
+ }
+ }
+
+ return items;
+ }
+
+ return generateSingleValue(column);
+}
+
+function generateSingleValue(column: Columns): string | number | boolean | null {
+ switch (column.type) {
+ case 'string': {
+ if ('format' in column && column.format) {
+ switch (column.format) {
+ case 'email': {
+ return faker.internet.email();
+ }
+
+ case 'ip': {
+ return faker.internet.ip();
+ }
+
+ case 'url': {
+ return faker.internet.url();
+ }
+ }
+ return '';
+ } else {
+ const stringAttr = column as Models.ColumnString;
+ const maxLength = Math.min(stringAttr.size ?? 255, 1000);
+ return generateStringValue(column.key, maxLength);
+ }
+ }
+
+ case 'integer': {
+ const intAttr = column as Models.ColumnInteger;
+ const min = intAttr.min ?? 0;
+ const max = intAttr.max ?? 10000;
+ return faker.number.int({ min, max });
+ }
+
+ case 'float': {
+ const floatAttr = column as Models.ColumnFloat;
+ const min = floatAttr.min ?? 0;
+ const max = floatAttr.max ?? 1000000;
+ const precision = 2;
+ return parseFloat(
+ faker.number.float({ min, max, fractionDigits: precision }).toFixed(precision)
+ );
+ }
+
+ case 'boolean': {
+ return faker.datatype.boolean();
+ }
+
+ case 'datetime': {
+ return faker.date.recent({ days: 365 }).toISOString();
+ }
+
+ case 'enum': {
+ const enumAttr = column as Models.ColumnEnum;
+ if (enumAttr.elements && enumAttr.elements.length > 0) {
+ return faker.helpers.arrayElement(enumAttr.elements);
+ }
+ return null;
+ }
+
+ default: {
+ return faker.lorem.word();
+ }
+ }
+}
diff --git a/src/lib/helpers/flag.ts b/src/lib/helpers/flag.ts
index 0fb3254a6..5eb12dafd 100644
--- a/src/lib/helpers/flag.ts
+++ b/src/lib/helpers/flag.ts
@@ -4,5 +4,7 @@ import { sdk } from '$lib/stores/sdk';
export function getFlagUrl(countryCode: string) {
if (!isValueOfStringEnum(Flag, countryCode)) return '';
- return sdk.forConsole.avatars.getFlag(countryCode, 22, 15, 100)?.toString();
+ return sdk.forConsole.avatars
+ .getFlag({ code: countryCode, width: 22, height: 15, quality: 100 })
+ ?.toString();
}
diff --git a/src/lib/helpers/load.ts b/src/lib/helpers/load.ts
index 3dd28f126..82330293e 100644
--- a/src/lib/helpers/load.ts
+++ b/src/lib/helpers/load.ts
@@ -10,7 +10,9 @@ export function getPage(url: URL): number {
}
export function getLimit(url: URL, route: Page['route'], fallback: number): number {
- return Number(url.searchParams.get('limit') ?? preferences.get(route).limit ?? fallback);
+ return Number(
+ url.searchParams.get('limit') ?? preferences.getForRoute(route).limit ?? fallback
+ );
}
export enum View {
@@ -19,7 +21,7 @@ export enum View {
}
export function getView(url: URL, route: Page['route'], fallback: View, overwrite?: View): View {
- const view = url.searchParams.get('view') ?? preferences.get(route).view;
+ const view = url.searchParams.get('view') ?? preferences.getForRoute(route).view;
return view === View.Grid
? View.Grid
@@ -29,7 +31,7 @@ export function getView(url: URL, route: Page['route'], fallback: View, overwrit
}
export function getColumns(route: Page['route'], fallback: string[]): string[] {
- return preferences.get(route).columns ?? fallback;
+ return preferences.getForRoute(route).columns ?? fallback;
}
export function getSearch(url: URL): string | undefined {
diff --git a/src/lib/helpers/logout.ts b/src/lib/helpers/logout.ts
index 0e5e9507f..e25666b75 100644
--- a/src/lib/helpers/logout.ts
+++ b/src/lib/helpers/logout.ts
@@ -6,7 +6,7 @@ import { base } from '$app/paths';
import { uploader } from '$lib/stores/uploader';
export async function logout(redirect: boolean = true) {
- await sdk.forConsole.account.deleteSession('current');
+ await sdk.forConsole.account.deleteSession({ sessionId: 'current' });
await invalidate(Dependencies.ACCOUNT);
uploader.reset();
trackEvent(Submit.AccountLogout);
diff --git a/src/lib/helpers/notifications.ts b/src/lib/helpers/notifications.ts
index 65b40d932..7264d5303 100644
--- a/src/lib/helpers/notifications.ts
+++ b/src/lib/helpers/notifications.ts
@@ -35,7 +35,7 @@ function updateNotificationPrefs(parsedPrefs: Record region.$id !== 'default')
.sort((a, b) => {
- if (a.disabled && !b.disabled) return 1;
- if (!a.disabled && b.disabled) return -1;
+ // Check if regions are truly available (not disabled and available)
+ const aAvailable = !a.disabled && a.available;
+ const bAvailable = !b.disabled && b.available;
+
+ // Prioritize truly available regions
+ if (aAvailable && !bAvailable) return -1;
+ if (!aAvailable && bAvailable) return 1;
+
+ // Within the same availability group, sort alphabetically
return a.name.localeCompare(b.name);
})
.map((region) => ({
diff --git a/src/lib/helpers/sidebar.ts b/src/lib/helpers/sidebar.ts
index b563f5b33..2609928de 100644
--- a/src/lib/helpers/sidebar.ts
+++ b/src/lib/helpers/sidebar.ts
@@ -1,10 +1,18 @@
import { get } from 'svelte/store';
import { user } from '$lib/stores/user';
import { sdk } from '$lib/stores/sdk';
+import type { Page } from '@sveltejs/kit';
const userPreferences = () => get(user)?.prefs;
-export function updateSidebarState(state: 'closed' | 'open' | 'icons') {
+// for spreadsheet, closed with icons
+const isInDatabasesRoute = (page: Page) => {
+ return page.route.id?.includes('databases/database-[database]');
+};
+
+export function updateSidebarState(page: Page, state: 'closed' | 'open' | 'icons') {
+ if (isInDatabasesRoute(page)) return;
+
if (state === 'open' || state === 'icons') {
const currentPrefs = userPreferences();
@@ -13,11 +21,13 @@ export function updateSidebarState(state: 'closed' | 'open' | 'icons') {
sidebarState: state
};
- sdk.forConsole.account.updatePrefs(newPrefs);
+ sdk.forConsole.account.updatePrefs({ prefs: newPrefs });
}
}
-export function getSidebarState(): 'open' | 'icons' {
+export function getSidebarState(page: Page): 'open' | 'icons' {
+ if (isInDatabasesRoute(page)) return 'icons';
+
const currentPrefs = userPreferences();
if (currentPrefs && currentPrefs.sidebarState) {
if (currentPrefs.sidebarState === 'open' || currentPrefs.sidebarState === 'icons') {
diff --git a/src/lib/helpers/string.ts b/src/lib/helpers/string.ts
index 31e5f905b..c138a7483 100644
--- a/src/lib/helpers/string.ts
+++ b/src/lib/helpers/string.ts
@@ -60,3 +60,12 @@ export const hostnameRegex = String.raw`(\*)|(\*\.)?(?!-)[A-Za-z0-9\-]+([\-\.]{1
* Supports domains, localhost, wildcards, ip-addresses and Chrome extension IDs!
*/
export const extendedHostnameRegex = String.raw`(\*)|(\*\.)?((?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}|localhost|(\d{1,3}\.){3}\d{1,3}|[a-z0-9]{32})`;
+
+export function hash(input: string | string[], delimiter: string = ','): string {
+ const str = Array.isArray(input) ? input.join(delimiter) : input;
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = ((hash << 5) - hash + str.charCodeAt(i)) & 0xffffffff;
+ }
+ return Math.abs(hash).toString(36);
+}
diff --git a/src/lib/helpers/templateSource.ts b/src/lib/helpers/templateSource.ts
new file mode 100644
index 000000000..d24b67e77
--- /dev/null
+++ b/src/lib/helpers/templateSource.ts
@@ -0,0 +1,26 @@
+import type { Models } from '@appwrite.io/console';
+
+/**
+ * Build VCS repo URL from the template response model.
+ * Example (GitHub): https://github.com/appwrite/templates-for-sites
+ */
+export function getTemplateSourceUrl(
+ t: Models.TemplateSite | Models.TemplateFunction
+): string | null {
+ const owner = t.providerOwner;
+ const repo = t.providerRepositoryId;
+ const provider = t.vcsProvider; // e.g., "github"
+
+ if (!owner || !repo || !provider) return null;
+
+ // Map provider → host (extend if needed)
+ const hostMap: Record = {
+ github: 'github.com',
+ gitlab: 'gitlab.com',
+ bitbucket: 'bitbucket.org'
+ };
+
+ const host = hostMap[provider.toLowerCase()] ?? provider; // fallback
+
+ return `https://${host}/${owner}/${repo}`;
+}
diff --git a/src/lib/helpers/types.ts b/src/lib/helpers/types.ts
index 961c992d0..abb4c133a 100644
--- a/src/lib/helpers/types.ts
+++ b/src/lib/helpers/types.ts
@@ -1,3 +1,4 @@
+import type { ComponentType } from 'svelte';
import type { Writable } from 'svelte/store';
export type PinkColumn = {
@@ -11,6 +12,7 @@ export type PinkColumn = {
min: number;
}
| number;
+ minimumWidth?: number;
hide?: boolean;
};
@@ -45,6 +47,14 @@ export type Column = PinkColumn & {
exclude?: boolean;
elements?: string[] | { value: string | number; label: string }[];
encrypt?: boolean;
+ icon?: ComponentType;
+ isPrimary?: boolean;
+ isEditable?: boolean;
+ fixed?: boolean;
+ resizable?: boolean;
+ draggable?: boolean;
+ resizedWidth?: number;
+ isAction?: boolean;
};
export function isValueOfStringEnum>(
diff --git a/src/lib/helpers/waitlist.ts b/src/lib/helpers/waitlist.ts
index 78765de13..d0ff50e85 100644
--- a/src/lib/helpers/waitlist.ts
+++ b/src/lib/helpers/waitlist.ts
@@ -8,7 +8,7 @@ export const joinWaitlistSites = (user: Account) => {
joinWaitlistSites: true
};
- sdk.forConsole.account.updatePrefs(newPrefs);
+ sdk.forConsole.account.updatePrefs({ prefs: newPrefs });
if (sessionStorage) {
sessionStorage.setItem('joinWaitlistSites', 'true');
diff --git a/src/lib/images/promos/timestamp-overrides-dark.png b/src/lib/images/promos/timestamp-overrides-dark.png
new file mode 100644
index 000000000..569abb8e0
Binary files /dev/null and b/src/lib/images/promos/timestamp-overrides-dark.png differ
diff --git a/src/lib/images/promos/timestamp-overrides-light.png b/src/lib/images/promos/timestamp-overrides-light.png
new file mode 100644
index 000000000..adf0c88ec
Binary files /dev/null and b/src/lib/images/promos/timestamp-overrides-light.png differ
diff --git a/src/lib/layout/activity.svelte b/src/lib/layout/activity.svelte
index f78f3bbe1..dce36d66f 100644
--- a/src/lib/layout/activity.svelte
+++ b/src/lib/layout/activity.svelte
@@ -7,70 +7,85 @@
import Button from '$lib/elements/forms/button.svelte';
import type { PinkColumn } from '$lib/helpers/types';
- export let logs: Models.LogList;
- export let offset = 0;
export let limit = 0;
+ export let offset = 0;
+ export let logs: Models.LogList;
+ export let insideSideSheet = false;
+ export let databasesScreen = false;
+ export let useCreateLinkForPagination = true;
const columns: PinkColumn[] = [
- { id: 'user' },
- { id: 'event' },
- { id: 'location' },
- { id: 'ip' },
- { id: 'date' }
+ { id: 'user', ...(insideSideSheet ? { width: 140 } : {}) },
+ { id: 'event', ...(insideSideSheet ? { width: 125 } : {}) },
+ { id: 'location', ...(insideSideSheet ? { width: 100 } : {}) },
+ { id: 'ip', ...(insideSideSheet ? { width: { min: 150 } } : {}) },
+ { id: 'date', ...(insideSideSheet ? { width: { min: 200 } } : {}) }
];
-
+
{#if logs.total}
-
-
- User
- Event
- Location
- IP
- Date
-
- {#each logs.logs as log}
-
-
-
- {#if log.userEmail}
- {#if log.userName}
-
- {log.userName}
+
+
+
+ User
+ Event
+ Location
+ IP
+ Date
+
+ {#each logs.logs as log}
+
+
+
+ {#if log.userEmail}
+ {#if log.userName}
+
+ {log.userName}
+ {:else}
+
+ {log.userEmail}
+ {/if}
{:else}
-
- {log.userEmail}
+
+
+
+ {log.userName ?? 'Anonymous'}
{/if}
+
+
+
+ {log.event}
+
+
+ {#if log.countryCode !== '--'}
+ {log.countryName}
{:else}
-
-
-
- {log.userName ?? 'Anonymous'}
+ Unknown
{/if}
-
-
-
- {log.event}
-
-
- {#if log.countryCode !== '--'}
- {log.countryName}
- {:else}
- Unknown
- {/if}
-
-
- {log.ip}
-
-
- {toLocaleDateTime(log.time)}
-
-
- {/each}
-
+
+
+ {log.ip}
+
+
+ {toLocaleDateTime(log.time)}
+
+
+ {/each}
+
+
-
+
{:else}
-
diff --git a/src/lib/layout/animatedTitle.svelte b/src/lib/layout/animatedTitle.svelte
new file mode 100644
index 000000000..867c3212b
--- /dev/null
+++ b/src/lib/layout/animatedTitle.svelte
@@ -0,0 +1,58 @@
+
+
+
+ {#if href && !$isSmallViewport}
+
+
+
+
+
+ {/if}
+
+ {@render children()}
+
+
+
+
diff --git a/src/lib/layout/container.svelte b/src/lib/layout/container.svelte
index abe25a4a9..8bb2983dd 100644
--- a/src/lib/layout/container.svelte
+++ b/src/lib/layout/container.svelte
@@ -1,6 +1,15 @@
-
-
+
+
@@ -18,26 +35,92 @@
diff --git a/src/lib/layout/coverTitle.svelte b/src/lib/layout/coverTitle.svelte
index ce87e70e5..2cfecde2a 100644
--- a/src/lib/layout/coverTitle.svelte
+++ b/src/lib/layout/coverTitle.svelte
@@ -1,5 +1,5 @@
@@ -35,7 +35,7 @@
{#if !hideColumns && $columns?.length}
Columns
-
+
{#snippet children(toggle, selectedColumnsNumber)}
{
+ const endings = ['table-[table]', 'table-[table]/columns', 'table-[table]/indexes'];
+ return endings.some((end) => page.route.id?.endsWith(end));
+ });
-
@@ -172,15 +177,20 @@
flex-direction: column;
gap: var(--gap-l);
margin-inline: 2rem;
+
+ &.hide {
+ display: none;
+ }
}
:global(main:has(.sub-navigation)) footer {
@media (min-width: 1024px) {
- margin-inline-start: -1.5rem;
margin-inline-end: 2rem;
+ margin-inline-start: -1.5rem;
}
}
- .extra-margin {
- margin-block-start: var(--space-2, 4px);
+
+ :global(main:has(.wide-screen-wrapper)) footer {
+ margin-inline-start: 2rem !important;
}
diff --git a/src/lib/layout/gridHeader.svelte b/src/lib/layout/gridHeader.svelte
index 9b66018bb..c3532f1e0 100644
--- a/src/lib/layout/gridHeader.svelte
+++ b/src/lib/layout/gridHeader.svelte
@@ -8,7 +8,7 @@
export let title: string;
export let columns: Writable
;
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 @@