mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat-SER-267-console-notification-for-grids-ui
This commit is contained in:
+7
-6
@@ -22,11 +22,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "^1.10.0",
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@c0da899",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@e6b154b",
|
||||
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@e6b154b",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4d9e4bf",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
@@ -83,8 +84,8 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.2.3",
|
||||
"vitest": "^3.0.0"
|
||||
"vite": "^7.0.6",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
@@ -94,5 +95,5 @@
|
||||
"svelte-preprocess"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.13.1"
|
||||
"packageManager": "pnpm@10.15.0"
|
||||
}
|
||||
|
||||
Generated
+363
-267
File diff suppressed because it is too large
Load Diff
@@ -148,9 +148,9 @@ export enum Click {
|
||||
ConnectRepositoryClick = 'click_connect_repository',
|
||||
CreditsRedeemClick = 'click_credits_redeem',
|
||||
CloudSignupClick = 'click_cloud_signup',
|
||||
DatabaseAttributeDelete = 'click_attribute_delete',
|
||||
DatabaseColumnDelete = 'click_column_delete',
|
||||
DatabaseIndexDelete = 'click_index_delete',
|
||||
DatabaseCollectionDelete = 'click_collection_delete',
|
||||
DatabaseTableDelete = 'click_table_delete',
|
||||
DatabaseDatabaseDelete = 'click_database_delete',
|
||||
DatabaseImportCsv = 'click_database_import_csv',
|
||||
DomainCreateClick = 'click_domain_create',
|
||||
@@ -265,6 +265,7 @@ export enum Submit {
|
||||
AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update',
|
||||
AuthMembershipPrivacyUpdate = 'submit_auth_membership_privacy_update',
|
||||
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
|
||||
AuthInvalidateSesssion = 'submit_auth_invalidate_session',
|
||||
SessionsLengthUpdate = 'submit_sessions_length_update',
|
||||
SessionsLimitUpdate = 'submit_sessions_limit_update',
|
||||
SessionDelete = 'submit_session_delete',
|
||||
@@ -273,22 +274,22 @@ export enum Submit {
|
||||
DatabaseDelete = 'submit_database_delete',
|
||||
DatabaseUpdateName = 'submit_database_update_name',
|
||||
DatabaseImportCsv = 'submit_database_import_csv',
|
||||
AttributeCreate = 'submit_attribute_create',
|
||||
AttributeUpdate = 'submit_attribute_update',
|
||||
AttributeDelete = 'submit_attribute_delete',
|
||||
DocumentCreate = 'submit_document_create',
|
||||
DocumentDelete = 'submit_document_delete',
|
||||
DocumentUpdate = 'submit_document_update',
|
||||
DocumentUpdatePermissions = 'submit_document_update_permissions',
|
||||
ColumnCreate = 'submit_column_create',
|
||||
ColumnUpdate = 'submit_column_update',
|
||||
ColumnDelete = 'submit_column_delete',
|
||||
RowCreate = 'submit_row_create',
|
||||
RowDelete = 'submit_row_delete',
|
||||
RowUpdate = 'submit_row_update',
|
||||
RowUpdatePermissions = 'submit_row_update_permissions',
|
||||
IndexCreate = 'submit_index_create',
|
||||
IndexDelete = 'submit_index_delete',
|
||||
CollectionCreate = 'submit_collection_create',
|
||||
CollectionDelete = 'submit_collection_delete',
|
||||
CollectionUpdateName = 'submit_collection_update_name',
|
||||
CollectionUpdatePermissions = 'submit_collection_update_permissions',
|
||||
CollectionUpdateSecurity = 'submit_collection_update_security',
|
||||
CollectionUpdateEnabled = 'submit_collection_update_enabled',
|
||||
CollectionUpdateDisplayNames = 'submit_collection_update_display_names',
|
||||
TableCreate = 'submit_row_create',
|
||||
TableDelete = 'submit_row_delete',
|
||||
TableUpdateName = 'submit_row_update_name',
|
||||
TableUpdatePermissions = 'submit_row_update_permissions',
|
||||
TableUpdateSecurity = 'submit_row_update_security',
|
||||
TableUpdateEnabled = 'submit_row_update_enabled',
|
||||
TableUpdateDisplayNames = 'submit_row_update_display_names',
|
||||
FunctionCreate = 'submit_function_create',
|
||||
FunctionDelete = 'submit_function_delete',
|
||||
FunctionUpdateName = 'submit_function_update_name',
|
||||
|
||||
@@ -28,10 +28,10 @@ const groups = [
|
||||
'integrations',
|
||||
'migrations',
|
||||
'users',
|
||||
'collections',
|
||||
'attributes',
|
||||
'tables',
|
||||
'columns',
|
||||
'indexes',
|
||||
'documents',
|
||||
'rows',
|
||||
'teams',
|
||||
'security',
|
||||
'buckets',
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
const examples = [
|
||||
'How to add platform in the console?',
|
||||
'How can I manage users, permissions, and access control in Appwrite?',
|
||||
'How can I set up database collections and documents in Appwrite?',
|
||||
'How can I set up database tables and rows in Appwrite?',
|
||||
'How do I configure and manage server-side functions in Appwrite?',
|
||||
'How to add custom domain in the console?'
|
||||
];
|
||||
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { initCreateAttribute } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
|
||||
import { attributeOptions } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/attributes/store';
|
||||
import { initCreateColumn } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte';
|
||||
import { columnOptions } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/store';
|
||||
import Template from './template.svelte';
|
||||
|
||||
let search = '';
|
||||
|
||||
let options = attributeOptions.map((option) => {
|
||||
let options = columnOptions.map((option) => {
|
||||
return {
|
||||
label: option.name,
|
||||
icon: option.icon,
|
||||
callback() {
|
||||
initCreateAttribute(option.name);
|
||||
initCreateColumn(option.name);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -37,16 +37,16 @@ export const DatabasesPanel: SubPanel = {
|
||||
component: Databases
|
||||
};
|
||||
|
||||
import Collections from './collections.svelte';
|
||||
export const CollectionsPanel: SubPanel = {
|
||||
name: 'Collections',
|
||||
component: Collections
|
||||
import Tables from './tables.svelte';
|
||||
export const TablesPanel: SubPanel = {
|
||||
name: 'Tables',
|
||||
component: Tables
|
||||
};
|
||||
|
||||
import CreateAttribute from './createAttribute.svelte';
|
||||
export const CreateAttributePanel: SubPanel = {
|
||||
name: 'Create Attribute',
|
||||
component: CreateAttribute
|
||||
import CreateColumn from './createColumn.svelte';
|
||||
export const CreateColumnPanel: SubPanel = {
|
||||
name: 'Create Column',
|
||||
component: CreateColumn
|
||||
};
|
||||
|
||||
import Users from './users.svelte';
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { initSearcher } from '../commands';
|
||||
import { collectionsSearcher } from '../searchers';
|
||||
import { tablesSearcher } from '../searchers';
|
||||
import Template from './template.svelte';
|
||||
|
||||
const { search, results } = initSearcher(collectionsSearcher);
|
||||
const { search, results } = initSearcher(tablesSearcher);
|
||||
</script>
|
||||
|
||||
<Template options={$results} bind:search={$search} />
|
||||
@@ -32,7 +32,9 @@ export const bucketSearcher = (async (query: string) => {
|
||||
const region = page.params.region;
|
||||
const { buckets } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.listBuckets([Query.orderDesc('$createdAt')]);
|
||||
.storage.listBuckets({
|
||||
queries: [Query.orderDesc('$createdAt')]
|
||||
});
|
||||
|
||||
const filtered = buckets.filter((bucket) => bucket.name.includes(query));
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { database } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export const collectionsSearcher = (async (query: string) => {
|
||||
const databaseId = get(database).$id;
|
||||
const { collections } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.databases.listCollections(databaseId);
|
||||
|
||||
return collections
|
||||
.filter((col) => col.name.toLowerCase().includes(query.toLowerCase()))
|
||||
.map(
|
||||
(col) =>
|
||||
({
|
||||
group: 'collections',
|
||||
label: col.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${col.$id}`
|
||||
);
|
||||
}
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
@@ -8,7 +8,7 @@ import { page } from '$app/state';
|
||||
export const dbSearcher = (async (query: string) => {
|
||||
const { databases } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.databases.list();
|
||||
.tablesDB.list();
|
||||
|
||||
return databases
|
||||
.filter((db) => db.name.toLowerCase().includes(query.toLowerCase()))
|
||||
|
||||
@@ -15,7 +15,11 @@ export const fileSearcher = (async (query: string) => {
|
||||
|
||||
const { files } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.listFiles($bucket.$id, [Query.orderDesc('')], query || undefined);
|
||||
.storage.listFiles({
|
||||
bucketId: $bucket.$id,
|
||||
queries: [Query.orderDesc('')],
|
||||
search: query || undefined
|
||||
});
|
||||
|
||||
return files.map((file) => ({
|
||||
label: file.name,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './databases';
|
||||
export * from './users';
|
||||
export * from './organizations';
|
||||
export * from './collections';
|
||||
export * from './tables';
|
||||
export * from './projects';
|
||||
export * from './teams';
|
||||
export * from './functions';
|
||||
|
||||
@@ -35,7 +35,7 @@ const getIcon = (message: Models.Message) => {
|
||||
export const messagesSearcher = (async (query: string) => {
|
||||
const { messages } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.listMessages([], query || undefined);
|
||||
.messaging.listMessages({ search: query || undefined });
|
||||
|
||||
return messages
|
||||
.filter((message) => getLabel(message).toLowerCase().includes(query.toLowerCase()))
|
||||
|
||||
@@ -7,10 +7,9 @@ import type { Searcher } from '../commands';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export const projectsSearcher = (async (query: string) => {
|
||||
const { projects } = await sdk.forConsole.projects.list([
|
||||
Query.equal('teamId', get(organization).$id),
|
||||
Query.orderDesc('')
|
||||
]);
|
||||
const { projects } = await sdk.forConsole.projects.list({
|
||||
queries: [Query.equal('teamId', get(organization).$id), Query.orderDesc('')]
|
||||
});
|
||||
|
||||
return projects
|
||||
.filter((project) => project.name.toLowerCase().includes(query.toLowerCase()))
|
||||
|
||||
@@ -13,7 +13,9 @@ const getIcon = (provider: string) => {
|
||||
export const providersSearcher = (async (query: string) => {
|
||||
const { providers } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.listProviders([], query || undefined);
|
||||
.messaging.listProviders({
|
||||
search: query || undefined
|
||||
});
|
||||
|
||||
return providers
|
||||
.filter((provider) => provider.name.toLowerCase().includes(query.toLowerCase()))
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export const tablesSearcher = (async (query: string) => {
|
||||
const databaseId = page.params.database;
|
||||
const { tables } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.tablesDB.listTables({ databaseId });
|
||||
|
||||
return tables
|
||||
.filter((table) => table.name.toLowerCase().includes(query.toLowerCase()))
|
||||
.map(
|
||||
(table) =>
|
||||
({
|
||||
group: 'tables',
|
||||
label: table.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${table.$id}`
|
||||
);
|
||||
}
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
@@ -17,9 +17,9 @@ const getTeamCommand = (team: Models.Team<Models.Preferences>, region: string, p
|
||||
}) satisfies Command;
|
||||
|
||||
export const teamSearcher = (async (query: string) => {
|
||||
const { teams } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.teams.list([], query);
|
||||
const { teams } = await sdk.forProject(page.params.region, page.params.project).teams.list({
|
||||
search: query
|
||||
});
|
||||
|
||||
if (teams.length === 1) {
|
||||
return [
|
||||
|
||||
@@ -8,7 +8,9 @@ import { page } from '$app/state';
|
||||
export const topicsSearcher = (async (query: string) => {
|
||||
const { topics } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.listTopics([], query || undefined);
|
||||
.messaging.listTopics({
|
||||
search: query || undefined
|
||||
});
|
||||
|
||||
return topics
|
||||
.filter((topic) => topic.name.toLowerCase().includes(query.toLowerCase()))
|
||||
|
||||
@@ -18,9 +18,9 @@ const getUserCommand = (user: Models.User<Models.Preferences>, region: string, p
|
||||
}) satisfies Command;
|
||||
|
||||
export const userSearcher = (async (query: string) => {
|
||||
const { users } = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.users.list([], query || undefined);
|
||||
const { users } = await sdk.forProject(page.params.region, page.params.project).users.list({
|
||||
search: query || undefined
|
||||
});
|
||||
|
||||
if (users.length === 1) {
|
||||
return [
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
export let size: AvatarProps['size'] = 'm';
|
||||
export let background: string | undefined = undefined;
|
||||
|
||||
$: src = sdk.forConsole.avatars.getInitials(name, 192, 192, background).toString();
|
||||
$: src = sdk.forConsole.avatars
|
||||
.getInitials({ name, width: 192, height: 192, background })
|
||||
.toString();
|
||||
</script>
|
||||
|
||||
<Avatar alt={name} {size} {src} />
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
}
|
||||
|
||||
function updateOrAddItem(payload: Payload) {
|
||||
// todo: @itznotabug - might need a change to $table?
|
||||
const { $id, status, $collection, policyId } = payload;
|
||||
if ($collection === 'archives' && policyId !== null) {
|
||||
return;
|
||||
@@ -117,9 +118,9 @@
|
||||
}
|
||||
|
||||
function backupName(item: BackupArchive | BackupRestoration, key: string) {
|
||||
const attribute = key === 'archives' ? '$createdAt' : 'startedAt';
|
||||
const column = key === 'archives' ? '$createdAt' : 'startedAt';
|
||||
|
||||
return toLocaleDate(item[attribute]);
|
||||
return toLocaleDate(item[column]);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -9,17 +9,22 @@
|
||||
upgradeURL
|
||||
} from '$lib/stores/billing';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { onMount } from 'svelte';
|
||||
import SelectProjectCloud from './selectProjectCloud.svelte';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
let showSelectProject: boolean = $state(false);
|
||||
|
||||
let selectedProjects: string[] = $state([]);
|
||||
onMount(() => {
|
||||
selectedProjects = page.data.organization?.projects || [];
|
||||
let showSelectProject: boolean = $state(false);
|
||||
|
||||
const organizationId = $derived(page.data.organization?.$id);
|
||||
|
||||
$effect(() => {
|
||||
if (organizationId) {
|
||||
selectedProjects = page.data.organization?.projects || [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<SelectProjectCloud bind:showSelectProject bind:selectedProjects />
|
||||
<SelectProjectCloud bind:showSelectProject bind:selectedProjects {organizationId} />
|
||||
|
||||
{#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { Alert, Button, Table } from '@appwrite.io/pink-svelte';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
import { Alert, Button, Skeleton, Table } from '@appwrite.io/pink-svelte';
|
||||
import { Modal } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { billingProjectsLimitDate } from '$lib/stores/billing';
|
||||
import { page } from '$app/state';
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
|
||||
let {
|
||||
showSelectProject = $bindable(false),
|
||||
selectedProjects = $bindable([])
|
||||
selectedProjects = $bindable([]),
|
||||
organizationId
|
||||
}: {
|
||||
showSelectProject: boolean;
|
||||
selectedProjects: string[];
|
||||
organizationId: string;
|
||||
} = $props();
|
||||
|
||||
let projects = $state<Array<Models.Project>>([]);
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let projectsLoadingError = $state<string | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
const currentOrgId = page.data.organization?.$id;
|
||||
projects =
|
||||
page.data.currentOrgId === currentOrgId ? page.data.allProjects?.projects || [] : [];
|
||||
});
|
||||
let error = $state<string | null>(null);
|
||||
let projects = $state<Array<Models.Project>>([]);
|
||||
|
||||
let projectsToArchive = $derived(
|
||||
projects.filter((project) => !selectedProjects.includes(project.$id))
|
||||
@@ -39,6 +36,7 @@
|
||||
projects[0].teamId,
|
||||
selectedProjects
|
||||
);
|
||||
|
||||
showSelectProject = false;
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
addNotification({
|
||||
@@ -69,47 +67,97 @@
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!showSelectProject) return;
|
||||
|
||||
const areProjectsLoaded = projects.length > 0;
|
||||
const teamIdInLoadedProjects = areProjectsLoaded ? projects[0].teamId : null;
|
||||
|
||||
if (organizationId != teamIdInLoadedProjects) {
|
||||
loading = true;
|
||||
|
||||
sdk.forConsole.projects
|
||||
.list({
|
||||
queries: [
|
||||
Query.equal('teamId', organizationId),
|
||||
Query.limit(1000) // Get all projects for organization
|
||||
]
|
||||
})
|
||||
.then((loadedProjects) => (projects = loadedProjects.projects))
|
||||
.catch((err) => (projectsLoadingError = err.message))
|
||||
.finally(() => (loading = false));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal bind:show={showSelectProject} title={'Manage projects'} onSubmit={updateSelected}>
|
||||
<Modal bind:show={showSelectProject} title="Manage projects" onSubmit={updateSelected}>
|
||||
<svelte:fragment slot="description">
|
||||
Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be
|
||||
blocked after this date.
|
||||
</svelte:fragment>
|
||||
{#if error}
|
||||
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
|
||||
{/if}
|
||||
<Table.Root
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows={selectedProjects}
|
||||
columns={[{ id: 'name' }, { id: 'created' }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each projects as project}
|
||||
<Table.Row.Base {root} id={project.$id}>
|
||||
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
|
||||
<Table.Cell column="created" {root}
|
||||
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{#if selectedProjects.length > $currentPlan?.projects}
|
||||
<div class="u-text-warning u-mb-4">
|
||||
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
|
||||
|
||||
{#if loading}
|
||||
<div class="skeleton-projects">
|
||||
<Table.Root let:root allowSelection columns={[{ id: 'name' }, { id: 'created' }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
|
||||
{#each Array.from({ length: 5 }) as _}
|
||||
<Table.Row.Base {root} select="disabled">
|
||||
<Table.Cell column="name" {root}>
|
||||
<Skeleton variant="line" height={20} width="80%" />
|
||||
</Table.Cell>
|
||||
<Table.Cell column="created" {root}>
|
||||
<Skeleton variant="line" height={20} width="80%" />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedProjects.length === $currentPlan?.projects}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
|
||||
<span>
|
||||
{@html formatProjectsToArchive()}
|
||||
will be archived.
|
||||
</span>
|
||||
</Alert.Inline>
|
||||
{:else if projectsLoadingError}
|
||||
<Alert.Inline status="error" title="Error">{projectsLoadingError}</Alert.Inline>
|
||||
{:else}
|
||||
{#if error}
|
||||
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
<Table.Root
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows={selectedProjects}
|
||||
columns={[{ id: 'name' }, { id: 'created' }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each projects as project}
|
||||
<Table.Row.Base {root} id={project.$id}>
|
||||
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
|
||||
<Table.Cell column="created" {root}
|
||||
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
{#if selectedProjects.length > $currentPlan?.projects}
|
||||
<div class="u-text-warning u-mb-4">
|
||||
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedProjects.length === $currentPlan?.projects}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
|
||||
<span>
|
||||
{@html formatProjectsToArchive()}
|
||||
will be archived.
|
||||
</span>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button.Button size="s" variant="secondary" on:click={() => (showSelectProject = false)}
|
||||
@@ -118,3 +166,13 @@
|
||||
>Save</Button.Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
/* disable the top select all selector button */
|
||||
.skeleton-projects :global([role='rowheader'] button) {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
background: var(--bgcolor-neutral-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
import PinkBackground from '$lib/images/pink-background.svg';
|
||||
import { bannerSpacing } from '$lib/layout/headerAlert.svelte';
|
||||
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
|
||||
export let variant: 'gradient' | 'image' = 'gradient';
|
||||
@@ -23,12 +24,22 @@
|
||||
const { header, sidebar, content } = queryLayoutElements();
|
||||
const headerHeight = header?.getBoundingClientRect().height || 0;
|
||||
const offset = alertHeight + (!$isTabletViewport && header ? headerHeight : 0);
|
||||
if (header) header.style.top = `${alertHeight}px`;
|
||||
|
||||
if (header) {
|
||||
header.style.top = `${alertHeight}px`;
|
||||
}
|
||||
|
||||
if (sidebar) {
|
||||
sidebar.style.top = `${offset}px`;
|
||||
sidebar.style.height = `calc(100vh - ${offset}px)`;
|
||||
|
||||
// for sidebar and sub-navigation!
|
||||
bannerSpacing.set(`${alertHeight}px`);
|
||||
}
|
||||
|
||||
if (content) {
|
||||
content.style.paddingBlockStart = `${alertHeight}px`;
|
||||
}
|
||||
if (content) content.style.paddingBlockStart = `${alertHeight}px`;
|
||||
};
|
||||
|
||||
onMount(setNavigationHeight);
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
const card = await submitStripeCard(name, page?.params?.organization ?? null);
|
||||
if (card && Object.hasOwn(card, 'id')) {
|
||||
if ((card as PaymentMethod).card.country === 'US') {
|
||||
if ((card as PaymentMethod).card?.country === 'US') {
|
||||
paymentMethod = card as PaymentMethod;
|
||||
showState = true;
|
||||
return;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { LabelCard } from '..';
|
||||
|
||||
export let billingPlan: Tier;
|
||||
export let anyOrgFree = false;
|
||||
export let isNewOrg = false;
|
||||
export let selfService = true;
|
||||
|
||||
@@ -22,12 +21,9 @@
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={anyOrgFree || !selfService}
|
||||
disabled={!selfService}
|
||||
value={BillingPlan.FREE}
|
||||
title={tierFree.name}
|
||||
tooltipShow={anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account."
|
||||
tooltipWidth="100%">
|
||||
title={tierFree.name}>
|
||||
<svelte:fragment slot="action">
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
|
||||
<Badge variant="secondary" size="xs" content="Current plan" />
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
import { base } from '$app/paths';
|
||||
import { currentPlan, newOrgModal } from '$lib/stores/organization';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { page } from '$app/state';
|
||||
|
||||
type Organization = {
|
||||
name: string;
|
||||
@@ -63,15 +65,15 @@
|
||||
}
|
||||
} = createMenu();
|
||||
|
||||
let isLoadingProjects = true;
|
||||
let isLoadingProjects = false;
|
||||
let loadedProjects: Models.ProjectList = { total: 0, projects: [] };
|
||||
|
||||
export let organizations: Organization[] = [];
|
||||
export let currentProject: Models.Project | null = null;
|
||||
export let projects: Promise<Models.ProjectList> = Promise.resolve(loadedProjects);
|
||||
|
||||
let projectsBottomSheetOpen = false;
|
||||
let organisationBottomSheetOpen = false;
|
||||
let projectsBottomSheet: Promise<SheetMenu | null> = null;
|
||||
|
||||
function createOrg() {
|
||||
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
|
||||
@@ -99,13 +101,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
async function createProjectsBottomSheet(organization: Organization): Promise<SheetMenu> {
|
||||
async function createProjectsBottomSheet(
|
||||
organization: Organization
|
||||
): Promise<SheetMenu | null> {
|
||||
if (!isOnProjects) return null;
|
||||
|
||||
isLoadingProjects = true;
|
||||
|
||||
// null on non-org/project path like `onboarding`.
|
||||
loadedProjects = (await projects) ?? loadedProjects;
|
||||
loadedProjects =
|
||||
(await sdk.forConsole.projects.list({
|
||||
queries: [
|
||||
Query.equal('teamId', organizationId ?? page.data.currentOrgId),
|
||||
Query.limit(5),
|
||||
Query.orderDesc('$updatedAt')
|
||||
]
|
||||
})) ?? loadedProjects;
|
||||
|
||||
for (const project of loadedProjects.projects) {
|
||||
project.region ??= 'default';
|
||||
}
|
||||
|
||||
isLoadingProjects = false;
|
||||
|
||||
const createProjectItem = {
|
||||
@@ -185,8 +201,6 @@
|
||||
|
||||
$: selectedOrg = organizations.find((org) => org.isSelected);
|
||||
|
||||
$: projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
|
||||
|
||||
$: organizationsBottomSheet = createOrganizationBottomSheet(selectedOrg);
|
||||
|
||||
$: correctPlanName =
|
||||
@@ -198,6 +212,20 @@
|
||||
: selectedOrg?.tierName; // fallback
|
||||
|
||||
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}`;
|
||||
|
||||
$: organizationId = currentProject?.teamId;
|
||||
|
||||
$: isOnProjects = page.route.id.includes('project-[region]-[project]');
|
||||
|
||||
$: shouldReloadProjects = isLoadingProjects
|
||||
? false
|
||||
: currentProject && loadedProjects.projects.length
|
||||
? loadedProjects.projects[0].teamId != currentProject.teamId
|
||||
: !loadedProjects.projects.length;
|
||||
|
||||
$: if (shouldReloadProjects) {
|
||||
projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={onResize} />
|
||||
@@ -380,7 +408,9 @@
|
||||
<BottomSheet.Menu bind:isOpen={organisationBottomSheetOpen} menu={organizationsBottomSheet} />
|
||||
|
||||
{#await projectsBottomSheet then menu}
|
||||
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
|
||||
{#if menu}
|
||||
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
|
||||
{/if}
|
||||
{/await}
|
||||
{/key}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
$: planLimit = getServiceLimit(serviceId) || Infinity;
|
||||
|
||||
$: limit = preferences.get(page.route)?.limit ?? CARD_LIMIT;
|
||||
$: limit = preferences.getForRoute(page.route)?.limit ?? CARD_LIMIT;
|
||||
|
||||
$: gridItemStyle = $isSmallViewport
|
||||
? undefined
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<div style:overflow="hidden">
|
||||
<div style:overflow="visible">
|
||||
<Layout.Stack {gap}>
|
||||
<slot name="aside" />
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts" context="module">
|
||||
<script lang="ts" module>
|
||||
const langArr = ['js', 'html', 'dart', 'kotlin', 'json', 'sh', 'yml', 'swift'] as const;
|
||||
export type Language = (typeof langArr)[number];
|
||||
|
||||
@@ -20,11 +20,31 @@
|
||||
import 'prismjs/plugins/autoloader/prism-autoloader';
|
||||
import 'prismjs/plugins/custom-class/prism-custom-class';
|
||||
import 'prismjs/plugins/line-numbers/prism-line-numbers';
|
||||
import { afterUpdate } from 'svelte';
|
||||
import { Copy } from '.';
|
||||
|
||||
export let label: string = null;
|
||||
export let labelIcon: 'code' | 'android' | 'flutter' | 'apple' = null;
|
||||
let {
|
||||
label = null,
|
||||
labelIcon = null,
|
||||
code,
|
||||
language,
|
||||
withLineNumbers = false,
|
||||
withCopy = false,
|
||||
noMargin = false,
|
||||
noBoxPadding = false,
|
||||
allowScroll = false,
|
||||
class: classes = ''
|
||||
}: {
|
||||
label?: string;
|
||||
labelIcon?: 'code' | 'android' | 'flutter' | 'apple';
|
||||
code: string;
|
||||
language: 'js' | 'html' | 'dart' | 'kotlin' | 'json' | 'sh' | 'yml' | 'swift';
|
||||
withLineNumbers?: boolean;
|
||||
withCopy?: boolean;
|
||||
noMargin?: boolean;
|
||||
noBoxPadding?: boolean;
|
||||
allowScroll?: boolean;
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
function getIcon(iconName: string) {
|
||||
switch (iconName) {
|
||||
@@ -40,22 +60,10 @@
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export let code: string;
|
||||
export let language: 'js' | 'html' | 'dart' | 'kotlin' | 'json' | 'sh' | 'yml' | 'swift';
|
||||
export let withLineNumbers = false;
|
||||
export let withCopy = false;
|
||||
export let noMargin = false;
|
||||
export let noBoxPadding = false;
|
||||
export let allowScroll = false;
|
||||
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
|
||||
Prism.plugins.customClass.prefix('prism-');
|
||||
|
||||
afterUpdate(async () => {
|
||||
Prism.highlightAll();
|
||||
});
|
||||
$effect(() => Prism.highlightAll());
|
||||
</script>
|
||||
|
||||
<section
|
||||
@@ -85,7 +93,7 @@
|
||||
class:line-numbers={withLineNumbers}><code>{code}</code></pre>
|
||||
</section>
|
||||
|
||||
<!-- svelte-ignore css-unused-selector -->
|
||||
<!-- svelte-ignore css_unused_selector -->
|
||||
<style lang="scss" global>
|
||||
@import 'prismjs/themes/prism.css';
|
||||
@import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
|
||||
|
||||
@@ -4,23 +4,49 @@
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { ActionMenu, Layout, Popover, Selector } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
ActionMenu,
|
||||
Divider,
|
||||
Layout,
|
||||
Popover,
|
||||
Selector,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
|
||||
let {
|
||||
columns,
|
||||
isCustomCollection = false,
|
||||
isCustomTable = false,
|
||||
ui = 'legacy',
|
||||
allowNoColumns = false,
|
||||
children
|
||||
showAnyway = false,
|
||||
children,
|
||||
onPreferencesUpdated = null
|
||||
}: {
|
||||
columns: Writable<Column[]>;
|
||||
isCustomCollection?: boolean;
|
||||
isCustomTable?: boolean;
|
||||
allowNoColumns?: boolean;
|
||||
ui?: 'legacy' | 'new';
|
||||
showAnyway?: boolean;
|
||||
children: Snippet<[toggle: () => void, selectedColumnsNumber: number]>;
|
||||
onPreferencesUpdated?: () => void;
|
||||
} = $props();
|
||||
|
||||
let search = $state('');
|
||||
let filteredColumns = $derived(
|
||||
$columns
|
||||
.filter((column) => !column.isAction && column.id !== '$sequence')
|
||||
.filter(
|
||||
(col) =>
|
||||
!col.exclude &&
|
||||
(col.title?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
col.id?.toLowerCase().includes(search.toLowerCase()))
|
||||
)
|
||||
);
|
||||
|
||||
let maxHeight = $state('none');
|
||||
let containerRef = $state<HTMLElement>(null);
|
||||
const collectionId = $derived(page.params.collection);
|
||||
const isNewStyle = ui === 'new';
|
||||
|
||||
const calcMaxHeight = () => {
|
||||
if (containerRef) {
|
||||
@@ -35,16 +61,17 @@
|
||||
const saveColumnPreferences = () => {
|
||||
const shownColumns = $columns.filter((n) => n.hide === true).map((n) => n.id);
|
||||
|
||||
if (isCustomCollection) {
|
||||
preferences.setCustomCollectionColumns(collectionId, shownColumns);
|
||||
if (isCustomTable) {
|
||||
onPreferencesUpdated?.();
|
||||
preferences.setCustomTableColumns(page.params.table, shownColumns);
|
||||
} else {
|
||||
preferences.setColumns(shownColumns);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (isCustomCollection) {
|
||||
const shownColumns = preferences.getCustomCollectionColumns(collectionId);
|
||||
if (isCustomTable) {
|
||||
const shownColumns = preferences.getCustomTableColumns(page.params.table);
|
||||
|
||||
columns.update((n) =>
|
||||
n.map((column) => {
|
||||
@@ -53,7 +80,7 @@
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const prefs = preferences.get(page.route);
|
||||
const prefs = preferences.getForRoute(page.route);
|
||||
|
||||
if (prefs?.columns) {
|
||||
columns.update((n) =>
|
||||
@@ -70,7 +97,7 @@
|
||||
|
||||
let selectedColumnsNumber = $derived(
|
||||
$columns.reduce((acc, column) => {
|
||||
if (column.hide) return acc;
|
||||
if (column.hide || column.isAction) return acc;
|
||||
|
||||
return ++acc;
|
||||
}, 0)
|
||||
@@ -78,45 +105,169 @@
|
||||
|
||||
function toggleColumn(column: Column) {
|
||||
columns.update((cols) =>
|
||||
cols.map((col) => {
|
||||
if (col.id === column.id) {
|
||||
column.hide = !column.hide;
|
||||
}
|
||||
return col;
|
||||
})
|
||||
cols.map((col) => (col.id === column.id ? { ...col, hide: !col.hide } : col))
|
||||
);
|
||||
|
||||
saveColumnPreferences();
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
columns.update((cols) =>
|
||||
cols.map((col) =>
|
||||
col.exclude
|
||||
? col
|
||||
: filteredColumns.some((fc) => fc.id === col.id)
|
||||
? { ...col, hide: false }
|
||||
: col
|
||||
)
|
||||
);
|
||||
saveColumnPreferences();
|
||||
}
|
||||
|
||||
function deselectAll() {
|
||||
columns.update((cols) => {
|
||||
const realColumns = cols.filter((col) => !col.exclude && !col.isAction);
|
||||
const filtered = filteredColumns.filter((col) => !col.exclude && !col.isAction);
|
||||
|
||||
if (filtered.length === 0) return cols;
|
||||
|
||||
const visibleRealColumns = realColumns.filter((col) => !col.hide);
|
||||
|
||||
if (!allowNoColumns && visibleRealColumns.length <= 1) {
|
||||
return cols;
|
||||
}
|
||||
|
||||
const willHideCount = filtered.filter((col) => !col.hide).length;
|
||||
|
||||
if (!allowNoColumns && visibleRealColumns.length - willHideCount < 1) {
|
||||
const [keep] = filtered;
|
||||
return cols.map((col) => {
|
||||
if (col.exclude || col.isAction) return col;
|
||||
if (!filtered.some((fc) => fc.id === col.id)) return col;
|
||||
|
||||
if (col.id === keep.id) {
|
||||
const stillVisible = cols
|
||||
.filter((c) => !c.exclude && !c.isAction)
|
||||
.filter((c) =>
|
||||
c.id === keep.id
|
||||
? true
|
||||
: filtered.some((fc) => fc.id === c.id)
|
||||
? false
|
||||
: !c.hide
|
||||
);
|
||||
if (stillVisible.length === 1 && typeof col.width === 'number') {
|
||||
return { ...col, hide: false, width: { min: col.width } };
|
||||
}
|
||||
return { ...col, hide: false };
|
||||
}
|
||||
|
||||
return { ...col, hide: true };
|
||||
});
|
||||
}
|
||||
|
||||
return cols.map((col) =>
|
||||
col.exclude || col.isAction
|
||||
? col
|
||||
: filtered.some((fc) => fc.id === col.id)
|
||||
? { ...col, hide: true }
|
||||
: col
|
||||
);
|
||||
});
|
||||
saveColumnPreferences();
|
||||
}
|
||||
|
||||
let visibleRealColumns = $derived(
|
||||
$columns.filter((col) => !col.exclude && !col.isAction && !col.hide)
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={calcMaxHeight} />
|
||||
|
||||
{#if $columns?.length}
|
||||
<Popover let:toggle placement="bottom-end" padding="none">
|
||||
{#if $columns?.length || showAnyway}
|
||||
{@const showActions = $columns.length > 1}
|
||||
{@const placement = isNewStyle ? 'bottom-start' : 'bottom-end'}
|
||||
<Popover let:toggle {placement} padding="none">
|
||||
{@render children(toggle, selectedColumnsNumber)}
|
||||
<svelte:fragment slot="tooltip">
|
||||
<div style:max-height={maxHeight} style:overflow="scroll" bind:this={containerRef}>
|
||||
<div bind:this={containerRef} class="actions-menu-wrapper" style:max-height={maxHeight}>
|
||||
<ActionMenu.Root>
|
||||
{#each $columns as column}
|
||||
{#if !column?.exclude}
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => toggleColumn(column)}
|
||||
disabled={allowNoColumns
|
||||
? false
|
||||
: selectedColumnsNumber <= 1 && column.hide !== true}>
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<Selector.Checkbox
|
||||
checked={!column.hide}
|
||||
size="s"
|
||||
on:click={() => toggleColumn(column)} />
|
||||
{column.title}
|
||||
{#if isNewStyle && showActions}
|
||||
<Layout.Stack gap="xs">
|
||||
<ActionMenu.Item.Input
|
||||
id="columns"
|
||||
placeholder="Search"
|
||||
bind:value={search} />
|
||||
|
||||
{#if filteredColumns.length > 0}
|
||||
<Layout.Stack
|
||||
gap="s"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
style="padding-block-end: 0.5rem">
|
||||
<Button size="xs" icon extraCompact on:click={selectAll}
|
||||
>Select all</Button>
|
||||
|
||||
<div style:height="1rem">
|
||||
<Divider vertical />
|
||||
</div>
|
||||
|
||||
<Button size="xs" icon extraCompact on:click={deselectAll}
|
||||
>Deselect all</Button>
|
||||
</Layout.Stack>
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div style:padding-inline="0.6rem" style:padding-block-end="0.5rem">
|
||||
<Typography.Text>No results found</Typography.Text>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<Layout.Stack
|
||||
gap="none"
|
||||
direction="column"
|
||||
class="filter-modal-actions-menu {showActions ? 'has-actions' : ''}">
|
||||
{#each filteredColumns as column}
|
||||
{#if !column?.exclude}
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => toggleColumn(column)}
|
||||
disabled={allowNoColumns
|
||||
? false
|
||||
: visibleRealColumns.length <= 1 && !column.hide}>
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
checked={!column.hide}
|
||||
on:change={() => toggleColumn(column)} />
|
||||
{column.title}
|
||||
</Layout.Stack>
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</ActionMenu.Root>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore css_unused_selector -->
|
||||
<style lang="scss">
|
||||
/* global because its on a tooltip slot, nested */
|
||||
:global(.actions-menu-wrapper) {
|
||||
overflow: scroll;
|
||||
margin-bottom: -0.35rem;
|
||||
|
||||
& :global(.filter-modal-actions-menu) {
|
||||
overflow: scroll;
|
||||
max-height: 150px;
|
||||
padding-bottom: unset;
|
||||
|
||||
&.has-actions {
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts" context="module">
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
type Consent = {
|
||||
key: string;
|
||||
accepted: Record<string, boolean>;
|
||||
@@ -19,8 +22,6 @@
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { Modal } from '.';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const key = new Date('2023-11-07');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import type { ComponentProps, Snippet } from 'svelte';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let value: string;
|
||||
export let event: string = null;
|
||||
export let eventContext = 'click_id_tag';
|
||||
export let tooltipDisabled = false;
|
||||
export let copyText: string = 'Click to copy';
|
||||
export type TooltipPlacement = NonNullable<ComponentProps<Tooltip>['placement']>;
|
||||
|
||||
let content = copyText;
|
||||
let {
|
||||
value,
|
||||
event = null,
|
||||
eventContext = 'click_id_tag',
|
||||
tooltipDisabled = false,
|
||||
tooltipPortal = false,
|
||||
tooltipDelay = 0,
|
||||
tooltipPlacement = undefined,
|
||||
copyText = 'Click to copy',
|
||||
children
|
||||
}: {
|
||||
value: string;
|
||||
event?: string;
|
||||
eventContext?: string;
|
||||
tooltipDisabled?: boolean;
|
||||
tooltipPortal?: boolean;
|
||||
tooltipDelay?: number;
|
||||
tooltipPlacement?: TooltipPlacement;
|
||||
copyText?: string;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
|
||||
let content = $state(copyText);
|
||||
|
||||
async function handleClick() {
|
||||
const success = await copy(value);
|
||||
@@ -34,17 +53,25 @@
|
||||
//TODO: remove this component
|
||||
</script>
|
||||
|
||||
<Tooltip disabled={tooltipDisabled}>
|
||||
<Tooltip
|
||||
disabled={tooltipDisabled}
|
||||
portal={tooltipPortal}
|
||||
delay={tooltipDelay}
|
||||
placement={tooltipPlacement}>
|
||||
<span
|
||||
data-private
|
||||
style:display="inline-flex"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
style:cursor="pointer"
|
||||
on:click|preventDefault|stopPropagation={handleClick}
|
||||
on:keyup={clickOnEnter}
|
||||
on:mouseenter={() => setTimeout(() => (content = copyText))}>
|
||||
<slot />
|
||||
onclick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleClick();
|
||||
}}
|
||||
onkeyup={clickOnEnter}
|
||||
onmouseenter={() => setTimeout(() => (content = copyText))}>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
<p slot="tooltip" let:showing>
|
||||
{#if showing}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
: '';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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<string, ImportItem>;
|
||||
@@ -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<ImportItemsMap> = writable(new Map());
|
||||
|
||||
async function showCompletionNotification(
|
||||
database: string,
|
||||
collection: string,
|
||||
payload: Payload
|
||||
) {
|
||||
async function showCompletionNotification(database: string, table: string, payload: Payload) {
|
||||
const isSuccess = payload.status === 'completed';
|
||||
const isError = !isSuccess && !!payload.errors;
|
||||
|
||||
@@ -48,20 +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 @@
|
||||
<header class="upload-box-header">
|
||||
<h4 class="upload-box-title">
|
||||
<Typography.Text variant="m-500">
|
||||
Importing documents ({$importItems.size})
|
||||
Importing rows ({$importItems.size})
|
||||
</Typography.Text>
|
||||
</h4>
|
||||
<button
|
||||
@@ -190,39 +196,54 @@
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#each [...$importItems.entries()] as [key, value] (key)}
|
||||
<div class="upload-box-content" class:is-open={isOpen}>
|
||||
<ul class="upload-box-list">
|
||||
<li class="upload-box-item">
|
||||
<section class="progress-bar u-width-full-line">
|
||||
<div
|
||||
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
|
||||
<Typography.Text>
|
||||
{@html text(value.status, value.collection)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar-container"
|
||||
class:is-danger={value.status === 'failed'}
|
||||
style="--graph-size:{graphSize(value.status)}%">
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="upload-box-content-list">
|
||||
{#each [...$importItems.entries()] as [key, value] (key)}
|
||||
<div class="upload-box-content" class:is-open={isOpen}>
|
||||
<ul class="upload-box-list">
|
||||
<li class="upload-box-item">
|
||||
<section class="progress-bar u-width-full-line">
|
||||
<div
|
||||
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
|
||||
<Typography.Text>
|
||||
{@html text(value.status, value.table)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar-container"
|
||||
class:is-danger={value.status === 'failed'}
|
||||
style="--graph-size:{graphSize(value.status)}%">
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.upload-box {
|
||||
display: flex;
|
||||
max-height: 320px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.upload-box-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.upload-box-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.upload-box-content-list {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.upload-box-content {
|
||||
min-width: 400px;
|
||||
max-width: 100vw;
|
||||
width: 304px;
|
||||
}
|
||||
|
||||
.upload-box-button {
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { InputId } from '$lib/elements/forms';
|
||||
import { InputProjectId } from '$lib/elements/forms';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { Card, Divider, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
export let show = false;
|
||||
export let name: string;
|
||||
export let id: string;
|
||||
export let autofocus = true;
|
||||
export let isProject = false;
|
||||
$: if (!show) {
|
||||
id = null;
|
||||
}
|
||||
|
||||
$: if (!id?.length) {
|
||||
id = null;
|
||||
}
|
||||
let {
|
||||
show = $bindable(false),
|
||||
name,
|
||||
id = $bindable(null),
|
||||
autofocus = true,
|
||||
isProject = false,
|
||||
required = true
|
||||
}: {
|
||||
show: boolean;
|
||||
name: string;
|
||||
id: string;
|
||||
autofocus?: boolean;
|
||||
isProject?: boolean;
|
||||
required?: boolean;
|
||||
} = $props();
|
||||
|
||||
$: if (show) {
|
||||
trackEvent(Click.ShowCustomIdClick);
|
||||
}
|
||||
$effect(() => {
|
||||
if (!show) {
|
||||
id = null;
|
||||
}
|
||||
|
||||
if (!id?.length) {
|
||||
id = null;
|
||||
}
|
||||
|
||||
if (show) {
|
||||
trackEvent(Click.ShowCustomIdClick);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
@@ -45,9 +59,9 @@
|
||||
<Divider />
|
||||
</span>
|
||||
{#if isProject}
|
||||
<InputProjectId bind:value={id} {autofocus} />
|
||||
<InputProjectId on:input bind:value={id} {autofocus} />
|
||||
{:else}
|
||||
<InputId required bind:value={id} {autofocus} />
|
||||
<InputId {required} on:input bind:value={id} {autofocus} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { menuOpen } from '$lib/components/menu/store';
|
||||
|
||||
export let time: string | null = null;
|
||||
export let canShowPopover: boolean = true;
|
||||
export let placement: ComponentProps<Popover>['placement'] = 'bottom';
|
||||
|
||||
function timeDifference(dateString: string): string {
|
||||
@@ -79,7 +80,7 @@
|
||||
<Popover let:show let:hide {placement} portal>
|
||||
<button
|
||||
on:mouseenter={() => {
|
||||
if (!$menuOpen) {
|
||||
if (!$menuOpen && canShowPopover) {
|
||||
setTimeout(show, 150);
|
||||
}
|
||||
}}
|
||||
@@ -89,54 +90,56 @@
|
||||
|
||||
<div
|
||||
let:hide
|
||||
let:showing
|
||||
slot="tooltip"
|
||||
role="tooltip"
|
||||
style:padding="1rem"
|
||||
style:margin="-1rem"
|
||||
on:mouseenter={() => (isMouseOverTooltip = true)}
|
||||
on:mouseleave={() => hidePopover(hide, false)}>
|
||||
<Layout.Stack gap="s" alignContent="flex-start">
|
||||
<!-- `Raw time` as per design -->
|
||||
<Typography.Caption color="--fgcolor-neutral-tertiary" variant="400">
|
||||
{#if $$slots.title}
|
||||
<slot name="title" />
|
||||
{:else}
|
||||
{timeToString}
|
||||
{/if}
|
||||
</Typography.Caption>
|
||||
{#if showing}
|
||||
<Layout.Stack gap="s" alignContent="flex-start">
|
||||
<!-- `Raw time` as per design -->
|
||||
<Typography.Caption color="--fgcolor-neutral-tertiary" variant="400">
|
||||
{#if $$slots.title}
|
||||
<slot name="title" />
|
||||
{:else}
|
||||
{timeToString}
|
||||
{/if}
|
||||
</Typography.Caption>
|
||||
|
||||
<!-- `Absolute time` as per design -->
|
||||
<Layout.Stack gap="xxs">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
<InteractiveText
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time, false, 'UTC')}
|
||||
value={toISOString(time)} />
|
||||
<!-- `Absolute time` as per design -->
|
||||
<Layout.Stack gap="xxs">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
<InteractiveText
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time, false, 'UTC')}
|
||||
value={toISOString(time)} />
|
||||
|
||||
<Badge variant="secondary" content="UTC" size="xs" />
|
||||
</Layout.Stack>
|
||||
<Badge variant="secondary" content="UTC" size="xs" />
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
<InteractiveText
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time)}
|
||||
value={toLocalDateTimeISO(time)} />
|
||||
<Badge variant="secondary" content="Local" size="xs" />
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
<InteractiveText
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time)}
|
||||
value={toLocalDateTimeISO(time)} />
|
||||
<Badge variant="secondary" content="Local" size="xs" />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
</Popover>{:else}
|
||||
<Typography.Text>{time === null || time === undefined ? 'null' : time}</Typography.Text>
|
||||
{/if}
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
...services.flatMap((s) => s.actions.map((a) => a.name)),
|
||||
...services.flatMap((s) => s.resources.flatMap((r) => r.actions.map((a) => a.name)))
|
||||
];
|
||||
const attributeNames = [
|
||||
...services.flatMap((s) => s.actions.flatMap((a) => a.attributes ?? [])),
|
||||
const columnNames = [
|
||||
...services.flatMap((s) => s.actions.flatMap((a) => a.columns ?? [])),
|
||||
...services.flatMap((s) =>
|
||||
s.resources.flatMap((r) => r.actions.flatMap((a) => a.attributes ?? []))
|
||||
s.resources.flatMap((r) => r.actions.flatMap((a) => a.columns ?? []))
|
||||
)
|
||||
];
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
const isService = (s: string) => serviceNames.includes(s);
|
||||
const isResource = (s: string) => resourceNames.includes(s);
|
||||
const isAction = (s: string) => actionNames.includes(s);
|
||||
const isAttribute = (s: string) => attributeNames.includes(s);
|
||||
const isColumn = (s: string) => columnNames.includes(s);
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
case 'service': {
|
||||
selected.resource = null;
|
||||
selected.action = null;
|
||||
selected.attribute = null;
|
||||
selected.column = null;
|
||||
break;
|
||||
}
|
||||
case 'resource': {
|
||||
@@ -78,25 +78,25 @@
|
||||
|
||||
// We need to check if the selected action name is still
|
||||
// on the available list, and if so, change the action to the
|
||||
// appropriate one, as it may contain different attributes
|
||||
// appropriate one, as it may contain different attributes/columns.
|
||||
const availableAction = availableActions.find(
|
||||
(a) => a.name === selected.action?.name
|
||||
);
|
||||
|
||||
if (!availableAction) {
|
||||
selected.action = null;
|
||||
selected.attribute = null;
|
||||
selected.column = null;
|
||||
} else {
|
||||
selected.action = availableAction;
|
||||
|
||||
if (!selected?.action?.attributes?.includes(selected.attribute)) {
|
||||
selected.attribute = null;
|
||||
if (!selected?.action?.columns?.includes(selected.column)) {
|
||||
selected.column = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
selected.attribute = null;
|
||||
selected.column = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -112,16 +112,13 @@
|
||||
if (isService(field)) {
|
||||
selected.service = services.find((s) => s.name === field);
|
||||
} else if (isResource(field)) {
|
||||
const resource = selected.service?.resources.find((r) => r.name === field);
|
||||
selected.resource = resource;
|
||||
selected.resource = selected.service?.resources.find((r) => r.name === field);
|
||||
} else if (isAction(field)) {
|
||||
const action =
|
||||
selected.action =
|
||||
selected.resource?.actions.find((a) => a.name === field) ||
|
||||
selected.service?.actions.find((a) => a.name === field);
|
||||
selected.action = action;
|
||||
} else if (isAttribute(field)) {
|
||||
const attribute = selected.action?.attributes?.find((a) => a === field);
|
||||
selected.attribute = attribute;
|
||||
} else if (isColumn(field)) {
|
||||
selected.column = selected.action?.columns?.find((a) => a === field);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +139,7 @@
|
||||
const secondToLastField = at(fields, index - 2);
|
||||
|
||||
if (index === 0 || isService(currField)) return 'service';
|
||||
if (isAttribute(currField) || isAction(prevField)) return 'attribute';
|
||||
if (isColumn(currField) || isAction(prevField)) return 'attribute';
|
||||
if (isAction(currField)) return 'action';
|
||||
if (isResource(currField)) return 'resource';
|
||||
if (isService(prevField) || isResource(prevField) || index === 1) {
|
||||
@@ -181,7 +178,7 @@
|
||||
service: null as EventService | null,
|
||||
resource: null as EventResource | null,
|
||||
action: null as EventAction | null,
|
||||
attribute: null as string | null
|
||||
column: null as string | null
|
||||
};
|
||||
let helper: string = null;
|
||||
let customInput: string = null;
|
||||
@@ -197,7 +194,7 @@
|
||||
services: services,
|
||||
resources: selected.service?.resources || [],
|
||||
actions: (selected.resource ? selected.resource?.actions : selected.service?.actions) || [],
|
||||
attributes: selected.action?.attributes || []
|
||||
columns: selected.action?.columns || []
|
||||
};
|
||||
|
||||
$: eventString = (function createEventString(): Array<{ value: string; description: string }> {
|
||||
@@ -211,8 +208,8 @@
|
||||
if (selected.service) {
|
||||
fields.push(selected.service.name, '*');
|
||||
}
|
||||
if (selected.resource?.name === 'documents') {
|
||||
fields.push('collections', '*');
|
||||
if (selected.resource?.name === 'rows') {
|
||||
fields.push('tables', '*');
|
||||
}
|
||||
if (selected.resource) {
|
||||
fields.push(selected.resource.name, '*');
|
||||
@@ -220,8 +217,8 @@
|
||||
if (selected.action) {
|
||||
fields.push(selected.action.name);
|
||||
}
|
||||
if (selected.attribute) {
|
||||
fields.push(selected.attribute);
|
||||
if (selected.column) {
|
||||
fields.push(selected.column);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,14 +286,14 @@
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
{#if !empty(available.attributes)}
|
||||
{#if !empty(available.columns)}
|
||||
<Layout.Stack gap="s">
|
||||
<Typography.Text variant="m-500">Choose an attribute (optional)</Typography.Text>
|
||||
<Layout.Stack gap="s" wrap="wrap" direction="row">
|
||||
{#each available.attributes as attribute}
|
||||
{#each available.columns as attribute}
|
||||
<Tag
|
||||
disabled={showInput}
|
||||
selected={selected.attribute === attribute}
|
||||
selected={selected.column === attribute}
|
||||
on:click={() => select('attribute', attribute)}>
|
||||
{attribute}
|
||||
</Tag>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
Card,
|
||||
Divider,
|
||||
Empty,
|
||||
Icon,
|
||||
Layout,
|
||||
Modal,
|
||||
Selector,
|
||||
@@ -24,23 +25,28 @@
|
||||
Table,
|
||||
ToggleButton,
|
||||
Tooltip,
|
||||
Typography
|
||||
Typography,
|
||||
Upload
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import Form from '$lib/elements/forms/form.svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { IconViewGrid, IconViewList } from '@appwrite.io/pink-icons-svelte';
|
||||
import { IconInfo, IconPlus, IconViewGrid, IconViewList } from '@appwrite.io/pink-icons-svelte';
|
||||
import { showCreateBucket } from '$routes/(console)/project-[region]-[project]/storage/+page.svelte';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
|
||||
export let show: boolean;
|
||||
export let mimeTypeQuery: string = 'image/';
|
||||
export let allowedExtension: string = '*';
|
||||
export let selectedBucket: string = null;
|
||||
export let selectedFile: string = null;
|
||||
export let onSelect: (e: Models.File) => void;
|
||||
export let onSelect: (e: Models.File, localFile: boolean) => void;
|
||||
export let gridImageDimensions: { imageHeight?: number; imageWidth?: number } = {
|
||||
imageHeight: 148
|
||||
};
|
||||
|
||||
export let showLocalFileBucket: boolean = false;
|
||||
export let localFileBucketTitle = 'Upload local file';
|
||||
let localFileBucketSelected = true;
|
||||
let localFile: FileList = null;
|
||||
let search = writable('');
|
||||
let searchEnabled = false;
|
||||
let fileSelector: HTMLInputElement;
|
||||
@@ -48,11 +54,22 @@
|
||||
let view: 'grid' | 'list' = 'list';
|
||||
|
||||
onMount(() => {
|
||||
const lastSelectedBucket = preferences.getKey('lastSelectedBucket', null);
|
||||
localFileBucketSelected = !lastSelectedBucket;
|
||||
|
||||
selectedBucket = currentBucket?.$id;
|
||||
});
|
||||
|
||||
function onSubmit() {
|
||||
onSelect(currentFile);
|
||||
async function onSubmit() {
|
||||
// localfile needs to be uploaded first
|
||||
if (localFileBucketSelected) {
|
||||
await uploadFile();
|
||||
}
|
||||
|
||||
// Save preference based on selection
|
||||
preferences.setKey('lastSelectedBucket', localFileBucketSelected ? null : selectedBucket);
|
||||
|
||||
onSelect(currentFile, localFileBucketSelected);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
@@ -60,7 +77,12 @@
|
||||
return (
|
||||
sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.getFilePreview(bucketId, fileId, size, size)
|
||||
.storage.getFilePreview({
|
||||
bucketId,
|
||||
fileId,
|
||||
width: size,
|
||||
height: size
|
||||
})
|
||||
.toString() + '&mode=admin'
|
||||
);
|
||||
}
|
||||
@@ -68,12 +90,24 @@
|
||||
async function uploadFile() {
|
||||
try {
|
||||
uploading = true;
|
||||
const file = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.createFile(selectedBucket, ID.unique(), fileSelector.files[0], [
|
||||
Permission.read(Role.any())
|
||||
]);
|
||||
search.set($search === null ? '' : null);
|
||||
let file = null;
|
||||
if (localFileBucketSelected) {
|
||||
file = await sdk.forConsoleIn(page.params.region).storage.createFile({
|
||||
bucketId: 'default',
|
||||
fileId: ID.unique(),
|
||||
file: localFile[0]
|
||||
});
|
||||
} else {
|
||||
file = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.createFile({
|
||||
bucketId: selectedBucket,
|
||||
fileId: ID.unique(),
|
||||
file: fileSelector.files[0],
|
||||
permissions: [Permission.read(Role.any())]
|
||||
});
|
||||
search.set($search === null ? '' : null);
|
||||
}
|
||||
selectFile(file);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -82,7 +116,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
function selectLocalBucket() {
|
||||
localFileBucketSelected = true;
|
||||
selectBucket(null);
|
||||
}
|
||||
|
||||
function selectBucket(bucket: Models.Bucket | null) {
|
||||
if (bucket) {
|
||||
localFileBucketSelected = false;
|
||||
localFile = null;
|
||||
}
|
||||
|
||||
search.set('');
|
||||
currentBucket = bucket;
|
||||
selectedBucket = bucket?.$id ?? null;
|
||||
@@ -91,8 +135,10 @@
|
||||
|
||||
function selectFile(file: Models.File) {
|
||||
currentFile = file;
|
||||
selectedBucket = currentFile.bucketId;
|
||||
selectedFile = currentFile.$id;
|
||||
if (!localFileBucketSelected) {
|
||||
selectedBucket = currentFile.bucketId;
|
||||
selectedFile = currentFile.$id;
|
||||
}
|
||||
}
|
||||
|
||||
function resetFile() {
|
||||
@@ -123,10 +169,24 @@
|
||||
const response = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.listBuckets();
|
||||
const bucket = response.buckets[0] ?? null;
|
||||
|
||||
const lastSelectedBucket = preferences.getKey('lastSelectedBucket', null);
|
||||
let preferredBucket = null;
|
||||
|
||||
if (lastSelectedBucket) {
|
||||
preferredBucket = response.buckets.find((bucket) => bucket.$id === lastSelectedBucket);
|
||||
}
|
||||
const bucket = preferredBucket ?? response.buckets[0] ?? null;
|
||||
|
||||
if (bucket) {
|
||||
currentBucket = bucket;
|
||||
selectedBucket = bucket.$id;
|
||||
if (preferredBucket) {
|
||||
localFileBucketSelected = false;
|
||||
}
|
||||
} else {
|
||||
// in case when the last bucket selected was deleted
|
||||
localFileBucketSelected = true;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -153,7 +213,11 @@
|
||||
currentBucket &&
|
||||
sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.listFiles(currentBucket.$id, getProperQuery(), $search || undefined)
|
||||
.storage.listFiles({
|
||||
bucketId: currentBucket.$id,
|
||||
queries: getProperQuery(),
|
||||
search: $search || undefined
|
||||
})
|
||||
.then((response) => {
|
||||
if ($search === '') {
|
||||
searchEnabled = response.total > 0;
|
||||
@@ -176,206 +240,268 @@
|
||||
<Layout.Stack direction={$isSmallViewport ? 'column' : 'row'} height="50vh" gap="none">
|
||||
<!-- min-width to avoid a layout-shift -->
|
||||
<aside>
|
||||
{#if !$isSmallViewport}
|
||||
<Typography.Caption variant="500">Buckets</Typography.Caption>
|
||||
{/if}
|
||||
<Layout.Stack direction="column" gap="l">
|
||||
{#if showLocalFileBucket}
|
||||
<div class="action-button" class:active-item={localFileBucketSelected}>
|
||||
<ActionMenu.Item.Button on:click={selectLocalBucket}>
|
||||
Local file
|
||||
</ActionMenu.Item.Button>
|
||||
</div>
|
||||
{/if}
|
||||
<Layout.Stack direction="column" gap="s">
|
||||
{#if !$isSmallViewport}
|
||||
<Typography.Caption variant="400" color="--fgcolor-neutral-tertiary"
|
||||
>Buckets</Typography.Caption>
|
||||
{/if}
|
||||
|
||||
{#await buckets}
|
||||
{#if $isSmallViewport}
|
||||
<!-- disabled state -->
|
||||
<div style:padding-block-start="1rem">
|
||||
<InputSelect
|
||||
required
|
||||
disabled
|
||||
id="bucket"
|
||||
value={null}
|
||||
options={[]}
|
||||
label="Bucket"
|
||||
placeholder="Loading buckets..." />
|
||||
</div>
|
||||
{/if}
|
||||
{:then response}
|
||||
{#if $isSmallViewport}
|
||||
<div style:padding-block-start="1rem">
|
||||
<InputSelect
|
||||
required
|
||||
id="bucket"
|
||||
label="Bucket"
|
||||
bind:value={selectedBucket}
|
||||
placeholder="Select bucket"
|
||||
on:change={(event) => {
|
||||
const bucketId = event.detail;
|
||||
const bucket = response.buckets.find(
|
||||
(bucket) => bucket.$id === bucketId
|
||||
);
|
||||
selectBucket(bucket);
|
||||
}}
|
||||
options={response.buckets.map((bucket) => {
|
||||
return {
|
||||
value: bucket.$id,
|
||||
label: `${bucket.name}`
|
||||
};
|
||||
})} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="action-menu-holder">
|
||||
<ActionMenu.Root width="180px">
|
||||
{#each response.buckets as bucket}
|
||||
{@const isSelected = bucket.$id === selectedBucket}
|
||||
<div class="action-button" class:active-item={isSelected}>
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => selectBucket(bucket)}>
|
||||
{bucket.name}
|
||||
</ActionMenu.Item.Button>
|
||||
</div>
|
||||
{:else}
|
||||
<ActionMenu.Item.Button
|
||||
>No buckets found</ActionMenu.Item.Button>
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{#await buckets}
|
||||
{#if $isSmallViewport}
|
||||
<!-- disabled state -->
|
||||
<div style:padding-block-start="1rem">
|
||||
<InputSelect
|
||||
required
|
||||
disabled
|
||||
id="bucket"
|
||||
value={null}
|
||||
options={[]}
|
||||
label="Bucket"
|
||||
placeholder="Loading buckets..." />
|
||||
</div>
|
||||
{/if}
|
||||
{:then response}
|
||||
{#if $isSmallViewport}
|
||||
<div style:padding-block-start="1rem">
|
||||
<InputSelect
|
||||
required
|
||||
id="bucket"
|
||||
label="Bucket"
|
||||
bind:value={selectedBucket}
|
||||
placeholder="Select bucket"
|
||||
on:change={(event) => {
|
||||
const bucketId = event.detail;
|
||||
const bucket = response.buckets.find(
|
||||
(bucket) => bucket.$id === bucketId
|
||||
);
|
||||
selectBucket(bucket);
|
||||
}}
|
||||
options={response.buckets.map((bucket) => {
|
||||
return {
|
||||
value: bucket.$id,
|
||||
label: `${bucket.name}`
|
||||
};
|
||||
})} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="action-menu-holder">
|
||||
<ActionMenu.Root width="180px">
|
||||
{#each response.buckets as bucket}
|
||||
{@const isSelected =
|
||||
!localFileBucketSelected &&
|
||||
bucket.$id === selectedBucket}
|
||||
<div
|
||||
class="action-button"
|
||||
class:active-item={isSelected}>
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => selectBucket(bucket)}>
|
||||
{bucket.name}
|
||||
</ActionMenu.Item.Button>
|
||||
</div>
|
||||
{:else}
|
||||
<Layout.Stack
|
||||
gap="xs"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
class="u-margin-inline-start-8">
|
||||
<Typography.Text
|
||||
align="center"
|
||||
variant="m-400"
|
||||
color="--fgcolor-neutral-tertiary"
|
||||
>No buckets found</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
style="gap: 3px; margin-block-start: 8px;">
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
<Button
|
||||
compact
|
||||
on:click={async () => {
|
||||
await goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/storage`
|
||||
);
|
||||
$showCreateBucket = true;
|
||||
}}>
|
||||
Create a bucket
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</aside>
|
||||
|
||||
<div style:padding-inline-start="1rem" style:opacity={$isSmallViewport ? 0 : 1}>
|
||||
<Divider vertical />
|
||||
</div>
|
||||
|
||||
<div class="files-section">
|
||||
<Layout.Stack gap="l">
|
||||
{#if $isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
type="file"
|
||||
tabindex="-1"
|
||||
class="u-hide"
|
||||
accept={extension}
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#await buckets then response}
|
||||
{#if response?.total}
|
||||
{#if currentBucket}
|
||||
<Layout.Stack>
|
||||
{#if !$isSmallViewport}
|
||||
{#key currentBucket?.$id}
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<Typography.Title size="s"
|
||||
>{currentBucket?.name}</Typography.Title>
|
||||
<Id value={currentBucket?.$id} event="bucket">
|
||||
{currentBucket?.$id}
|
||||
</Id>
|
||||
{#if localFileBucketSelected}
|
||||
<div class="files-section">
|
||||
<Layout.Stack gap="l">
|
||||
<Typography.Title size="s" color="--fgcolor-neutral-primary"
|
||||
>{localFileBucketTitle}</Typography.Title>
|
||||
<Upload.Dropzone bind:files={localFile} extensions={[allowedExtension]}>
|
||||
<Layout.Stack alignItems="center" gap="s">
|
||||
<Layout.Stack alignItems="center" gap="s">
|
||||
<Layout.Stack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
direction="row"
|
||||
gap="s">
|
||||
<Typography.Text variant="l-500">
|
||||
Drag and drop files here or click to upload
|
||||
</Typography.Text>
|
||||
<Tooltip>
|
||||
<Layout.Stack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
inline>
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
</Layout.Stack>
|
||||
{/key}
|
||||
{/if}
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<InputSearch
|
||||
placeholder="Search files"
|
||||
bind:value={$search}
|
||||
disabled={!searchEnabled} />
|
||||
<ToggleButton
|
||||
bind:active={view}
|
||||
buttons={[
|
||||
{
|
||||
id: 'list',
|
||||
label: 'list view',
|
||||
disabled: !searchEnabled,
|
||||
icon: IconViewList
|
||||
},
|
||||
{
|
||||
id: 'grid',
|
||||
label: 'grid view',
|
||||
disabled: !searchEnabled,
|
||||
icon: IconViewGrid
|
||||
}
|
||||
]} />
|
||||
{#if !$isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
type="file"
|
||||
tabindex="-1"
|
||||
class="u-hide"
|
||||
accept={extension}
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"
|
||||
></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
<svelte:fragment slot="tooltip"
|
||||
>{allowedExtension} files are allowed</svelte:fragment>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
<Typography.Caption variant="400"
|
||||
>Max file size: 10MB</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Upload.Dropzone>
|
||||
{#if localFile}
|
||||
<Upload.List
|
||||
files={Array.from(localFile).map((f) => {
|
||||
return {
|
||||
...f,
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
extension: f.type,
|
||||
removable: true
|
||||
};
|
||||
})}
|
||||
on:remove={() => (localFile = null)} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="files-section">
|
||||
<Layout.Stack gap="l">
|
||||
{#if $isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
type="file"
|
||||
tabindex="-1"
|
||||
class="u-hide"
|
||||
accept={extension}
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if files}
|
||||
{#await files}
|
||||
<Layout.Stack
|
||||
justifyContent="center"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
gap="xl"
|
||||
height="100%">
|
||||
<Spinner size="l" />
|
||||
<span>Loading files...</span>
|
||||
{#await buckets then response}
|
||||
{#if response?.total}
|
||||
{#if currentBucket}
|
||||
<Layout.Stack>
|
||||
{#if !$isSmallViewport}
|
||||
{#key currentBucket?.$id}
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<Typography.Title size="s"
|
||||
>{currentBucket?.name}</Typography.Title>
|
||||
<Id value={currentBucket?.$id} event="bucket">
|
||||
{currentBucket?.$id}
|
||||
</Id>
|
||||
</Layout.Stack>
|
||||
{/key}
|
||||
{/if}
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<InputSearch
|
||||
placeholder="Search files"
|
||||
bind:value={$search}
|
||||
disabled={!searchEnabled} />
|
||||
<ToggleButton
|
||||
bind:active={view}
|
||||
buttons={[
|
||||
{
|
||||
id: 'list',
|
||||
label: 'list view',
|
||||
disabled: !searchEnabled,
|
||||
icon: IconViewList
|
||||
},
|
||||
{
|
||||
id: 'grid',
|
||||
label: 'grid view',
|
||||
disabled: !searchEnabled,
|
||||
icon: IconViewGrid
|
||||
}
|
||||
]} />
|
||||
{#if !$isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
type="file"
|
||||
tabindex="-1"
|
||||
class="u-hide"
|
||||
accept={extension}
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"
|
||||
></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{:then response}
|
||||
{#if response?.files?.length}
|
||||
{#if view === 'grid'}
|
||||
{#if $isSmallViewport}
|
||||
<Layout.Stack gap="l">
|
||||
{#each response?.files as file}
|
||||
<Card.Selector
|
||||
radius="s"
|
||||
name="files"
|
||||
padding="xxs"
|
||||
imageHeight={32}
|
||||
imageWidth={32}
|
||||
imageRadius="xs"
|
||||
bind:group={selectedFile}
|
||||
title={truncatedFilename(file, 14)}
|
||||
value={file.$id}
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id,
|
||||
360
|
||||
)}
|
||||
on:click={() => selectFile(file)} />
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<Layout.Grid
|
||||
columnsXXS={1}
|
||||
columnsXS={2}
|
||||
columnsS={3}
|
||||
columns={4}>
|
||||
{#each response?.files as file}
|
||||
<div class="image-selector">
|
||||
</Layout.Stack>
|
||||
|
||||
{#if files}
|
||||
{#await files}
|
||||
<Layout.Stack
|
||||
justifyContent="center"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
gap="xl"
|
||||
height="100%">
|
||||
<Spinner size="l" />
|
||||
<span>Loading files...</span>
|
||||
</Layout.Stack>
|
||||
{:then response}
|
||||
{#if response?.files?.length}
|
||||
{#if view === 'grid'}
|
||||
{#if $isSmallViewport}
|
||||
<Layout.Stack gap="l">
|
||||
{#each response?.files as file}
|
||||
<Card.Selector
|
||||
radius="s"
|
||||
name="files"
|
||||
padding="xxs"
|
||||
imageWidth={gridImageDimensions.imageWidth}
|
||||
imageHeight={gridImageDimensions.imageHeight}
|
||||
imageHeight={32}
|
||||
imageWidth={32}
|
||||
imageRadius="xs"
|
||||
bind:group={selectedFile}
|
||||
title={truncatedFilename(
|
||||
@@ -390,166 +516,188 @@
|
||||
)}
|
||||
on:click={() =>
|
||||
selectFile(file)} />
|
||||
</div>
|
||||
{/each}
|
||||
</Layout.Grid>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if view === 'list'}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'filename', width: { min: 225 } },
|
||||
{ id: 'id', width: { min: 200 } },
|
||||
{ id: 'type', width: { min: 100 } },
|
||||
{ id: 'size', width: { min: 120 } },
|
||||
{ id: 'created', width: { min: 140 } }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="filename" {root}>
|
||||
Filename
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="id" {root}>
|
||||
ID
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="type" {root}>
|
||||
Type
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="size" {root}>
|
||||
Size
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="created" {root}>
|
||||
Created
|
||||
</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each response?.files as file (file.$id)}
|
||||
<Table.Row.Button
|
||||
{root}
|
||||
on:click={() => selectFile(file)}>
|
||||
<Table.Cell column="filename" {root}>
|
||||
<div
|
||||
class="u-inline-flex u-cross-center u-gap-12">
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
name="file"
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<Layout.Grid
|
||||
columnsXXS={1}
|
||||
columnsXS={2}
|
||||
columnsS={3}
|
||||
columns={4}>
|
||||
{#each response?.files as file}
|
||||
<div class="image-selector">
|
||||
<Card.Selector
|
||||
radius="s"
|
||||
name="files"
|
||||
padding="xxs"
|
||||
imageWidth={gridImageDimensions.imageWidth}
|
||||
imageHeight={gridImageDimensions.imageHeight}
|
||||
imageRadius="xs"
|
||||
bind:group={selectedFile}
|
||||
title={truncatedFilename(
|
||||
file,
|
||||
14
|
||||
)}
|
||||
value={file.$id}
|
||||
bind:group={selectedFile} />
|
||||
|
||||
<div class="preview-block">
|
||||
<img
|
||||
alt={file.name}
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id
|
||||
)} />
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
disabled={file.name.length <
|
||||
15}
|
||||
maxWidth="fit-content">
|
||||
<Typography.Text truncate>
|
||||
{truncatedFilename(
|
||||
file
|
||||
)}
|
||||
</Typography.Text>
|
||||
|
||||
<span slot="tooltip">
|
||||
{file.name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id,
|
||||
360
|
||||
)}
|
||||
on:click={() =>
|
||||
selectFile(file)} />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="id" {root}>
|
||||
<Id value={file.$id}>{file.$id}</Id>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="type" {root}>
|
||||
{file.mimeType}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="size" {root}>
|
||||
{calculateSize(file.sizeOriginal)}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="created" {root}>
|
||||
<DualTimeView
|
||||
time={file.$createdAt} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Button>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/each}
|
||||
</Layout.Grid>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if view === 'list'}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'filename', width: { min: 225 } },
|
||||
{ id: 'id', width: { min: 200 } },
|
||||
{ id: 'type', width: { min: 100 } },
|
||||
{ id: 'size', width: { min: 120 } },
|
||||
{ id: 'created', width: { min: 140 } }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell
|
||||
column="filename"
|
||||
{root}>
|
||||
Filename
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="id" {root}>
|
||||
ID
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="type" {root}>
|
||||
Type
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="size" {root}>
|
||||
Size
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell
|
||||
column="created"
|
||||
{root}>
|
||||
Created
|
||||
</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each response?.files as file (file.$id)}
|
||||
<Table.Row.Button
|
||||
{root}
|
||||
on:click={() => selectFile(file)}>
|
||||
<Table.Cell
|
||||
column="filename"
|
||||
{root}>
|
||||
<div
|
||||
class="u-inline-flex u-cross-center u-gap-12">
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
name="file"
|
||||
value={file.$id}
|
||||
bind:group={
|
||||
selectedFile
|
||||
} />
|
||||
|
||||
<div class="preview-block">
|
||||
<img
|
||||
alt={file.name}
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id
|
||||
)} />
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
disabled={file.name
|
||||
.length < 15}
|
||||
maxWidth="fit-content">
|
||||
<Typography.Text
|
||||
truncate>
|
||||
{truncatedFilename(
|
||||
file
|
||||
)}
|
||||
</Typography.Text>
|
||||
|
||||
<span slot="tooltip">
|
||||
{file.name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="id" {root}>
|
||||
<Id value={file.$id}
|
||||
>{file.$id}</Id>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="type" {root}>
|
||||
{file.mimeType}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="size" {root}>
|
||||
{calculateSize(
|
||||
file.sizeOriginal
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="created" {root}>
|
||||
<DualTimeView
|
||||
time={file.$createdAt} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Button>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/if}
|
||||
{:else if $search}
|
||||
<EmptySearch
|
||||
hidePages
|
||||
hidePagination
|
||||
bind:search={$search}
|
||||
target="files">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => ($search = '')}>
|
||||
Clear search
|
||||
</Button>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Card.Base padding="none">
|
||||
<Empty
|
||||
title="No files found within this bucket."
|
||||
description="Need a hand? Learn more in our documentation.">
|
||||
<slot name="actions" slot="actions">
|
||||
<Button
|
||||
text
|
||||
external
|
||||
size="s"
|
||||
event="empty_documentation"
|
||||
href="https://appwrite.io/docs/products/storage/upload-download"
|
||||
ariaLabel="create document"
|
||||
>Documentation</Button>
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() =>
|
||||
fileSelector.click()}
|
||||
>Upload file
|
||||
</Button>
|
||||
</slot>
|
||||
</Empty>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
{:else if $search}
|
||||
<EmptySearch
|
||||
hidePages
|
||||
hidePagination
|
||||
bind:search={$search}
|
||||
target="files">
|
||||
<Button secondary on:click={() => ($search = '')}>
|
||||
Clear search
|
||||
</Button>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Card.Base padding="none">
|
||||
<Empty
|
||||
title="No files found within this bucket."
|
||||
description="Need a hand? Learn more in our documentation.">
|
||||
<slot name="actions" slot="actions">
|
||||
<Button
|
||||
text
|
||||
external
|
||||
size="s"
|
||||
event="empty_documentation"
|
||||
href="https://appwrite.io/docs/products/storage/upload-download"
|
||||
ariaLabel="create document"
|
||||
>Documentation</Button>
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}
|
||||
>Upload file
|
||||
</Button>
|
||||
</slot>
|
||||
</Empty>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
{/await}
|
||||
{/await}
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<Card.Base padding="none">
|
||||
<Empty
|
||||
title="No buckets found"
|
||||
description="Need a hand? Learn more in our documentation.">
|
||||
<slot name="actions" slot="actions">
|
||||
<Button
|
||||
text
|
||||
external
|
||||
size="s"
|
||||
event="empty_documentation"
|
||||
href="https://appwrite.io/docs/products/storage/buckets"
|
||||
ariaLabel="create document">Documentation</Button>
|
||||
|
||||
<Button
|
||||
secondary
|
||||
on:click={async () => {
|
||||
await goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/storage`
|
||||
);
|
||||
$showCreateBucket = true;
|
||||
}}>
|
||||
Create bucket
|
||||
</Button>
|
||||
</slot>
|
||||
</Empty>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
{/await}
|
||||
</Layout.Stack>
|
||||
</div></Layout.Stack>
|
||||
|
||||
{/await}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button text on:click={closeModal}>Cancel</Button>
|
||||
<Button submit disabled={selectedBucket === null || selectedFile === null}
|
||||
<Button
|
||||
submit
|
||||
disabled={(selectedBucket === null && localFileBucketSelected === false) ||
|
||||
(selectedFile === null && localFile === null)}
|
||||
>Select
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { Click, Submit, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let query = '[]';
|
||||
export let onlyIcon = false;
|
||||
export let columns: Writable<Column[]>;
|
||||
export let disabled = false;
|
||||
export let fullWidthMobile = false;
|
||||
@@ -124,62 +125,74 @@
|
||||
<Popover let:toggle placement="bottom-start">
|
||||
<Button
|
||||
secondary
|
||||
icon={onlyIcon}
|
||||
class={onlyIcon ? 'width-fix' : undefined}
|
||||
on:click={(event) => {
|
||||
toggle(event);
|
||||
trackEvent(Click.FilterApplyClick, { source: analyticsSource });
|
||||
}}
|
||||
{disabled}>
|
||||
<Icon icon={IconFilterLine} slot="start" size="s" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
<span class="inline-tag">
|
||||
{applied}
|
||||
</span>
|
||||
<Icon icon={IconFilterLine} slot="start" size={onlyIcon ? 'm' : 's'} />
|
||||
{#if !onlyIcon}
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
<span class="inline-tag">
|
||||
{applied}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</Button>
|
||||
<svelte:fragment slot="tooltip">
|
||||
<div style:width="420px">
|
||||
<Layout.Stack gap="s">
|
||||
{#if displayQuickFilters}
|
||||
<slot name="quick" />
|
||||
{:else}
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{/if}
|
||||
<div
|
||||
class="u-flex u-cross-center u-margin-block-start-16"
|
||||
class:u-main-end={!quickFilters}
|
||||
class:u-main-space-between={quickFilters}>
|
||||
{#if quickFilters}
|
||||
<Button
|
||||
text
|
||||
on:click={() => (displayQuickFilters = !displayQuickFilters)}
|
||||
class="u-margin-block-end-auto">
|
||||
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
|
||||
</Button>
|
||||
|
||||
<svelte:fragment slot="tooltip" let:showing>
|
||||
{#if showing}
|
||||
<div style:width="420px">
|
||||
<Layout.Stack gap="s">
|
||||
{#if displayQuickFilters}
|
||||
<slot name="quick" />
|
||||
{:else}
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<!-- this needs to be portalled -->
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{/if}
|
||||
<div class="u-flex u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button size="s" text on:click={toggleDropdown}>Cancel</Button>
|
||||
{:else}
|
||||
<Button size="s" disabled={applied === 0} text on:click={clearAll}>
|
||||
Clear all
|
||||
<div
|
||||
class="u-flex u-cross-center u-margin-block-start-16"
|
||||
class:u-main-end={!quickFilters}
|
||||
class:u-main-space-between={quickFilters}>
|
||||
{#if quickFilters}
|
||||
<Button
|
||||
text
|
||||
on:click={() => (displayQuickFilters = !displayQuickFilters)}
|
||||
class="u-margin-block-end-auto">
|
||||
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button size="s" on:click={apply} disabled={isButtonDisabled}
|
||||
>Apply</Button>
|
||||
<div class="u-flex u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button size="s" text on:click={toggleDropdown}>Cancel</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="s"
|
||||
disabled={applied === 0}
|
||||
text
|
||||
on:click={clearAll}>
|
||||
Clear all
|
||||
</Button>
|
||||
{/if}
|
||||
<Button size="s" on:click={apply} disabled={isButtonDisabled}
|
||||
>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -238,3 +251,10 @@
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.width-fix) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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<TagValue, string>()) {
|
||||
}
|
||||
|
||||
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}**` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
</script>
|
||||
|
||||
{#key value}
|
||||
<Copy {value} {event}>
|
||||
<Copy {value} {event} {tooltipPortal} {tooltipDelay} {tooltipPlacement}>
|
||||
<Tag size="xs" variant="code">
|
||||
<Icon icon={IconDuplicate} size="s" slot="start" />
|
||||
<span
|
||||
|
||||
@@ -84,3 +84,4 @@ export { default as UsageCard } from './usageCard.svelte';
|
||||
export { default as ViewToggle } from './viewToggle.svelte';
|
||||
export { default as RegionEndpoint } from './regionEndpoint.svelte';
|
||||
export { default as ExpirationInput } from './expirationInput.svelte';
|
||||
export { default as SortButton, type SortDirection } from './sortButton.svelte';
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
export async function verify(challenge: Models.MfaChallenge, code: string) {
|
||||
try {
|
||||
if (challenge === null) {
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(
|
||||
AuthenticationFactor.Totp
|
||||
);
|
||||
challenge = await sdk.forConsole.account.createMFAChallenge({
|
||||
factor: AuthenticationFactor.Totp
|
||||
});
|
||||
}
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
await sdk.forConsole.account.updateMFAChallenge({
|
||||
challengeId: challenge.$id,
|
||||
otp: code
|
||||
});
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountLogin, { mfa_used: true });
|
||||
} catch (error) {
|
||||
@@ -46,7 +49,7 @@
|
||||
disabled = true;
|
||||
challengeType = factor;
|
||||
try {
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
challenge = await sdk.forConsole.account.createMFAChallenge({ factor });
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -63,9 +66,9 @@
|
||||
);
|
||||
if (enabledNonRecoveryFactors.length === 1) {
|
||||
if (factors.phone) {
|
||||
createChallenge(AuthenticationFactor.Phone);
|
||||
await createChallenge(AuthenticationFactor.Phone);
|
||||
} else if (factors.email) {
|
||||
createChallenge(AuthenticationFactor.Email);
|
||||
await createChallenge(AuthenticationFactor.Email);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
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<HTMLHeadElement> & {
|
||||
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<Models.ProjectList> = undefined;
|
||||
|
||||
let activeTheme = $app.theme;
|
||||
let shouldAnimateThemeToggle = false;
|
||||
@@ -138,7 +128,9 @@
|
||||
class="only-desktop">
|
||||
<img src={logo.src} alt={logo.alt} />
|
||||
</a>
|
||||
<Breadcrumbs {organizations} {projects} {currentProject} />
|
||||
|
||||
<Breadcrumbs {organizations} {currentProject} />
|
||||
|
||||
{#if page.route?.id?.includes('/project-[region]-[project]') && currentProject && currentProject.pingCount === 0}
|
||||
<div class="only-desktop" style:margin-inline-start="-16px">
|
||||
<Button.Anchor
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let sum: number;
|
||||
export let limit: number;
|
||||
export let offset: number;
|
||||
export let useCreateLink = true;
|
||||
|
||||
$: currentPage = Math.floor(offset / limit + 1);
|
||||
|
||||
@@ -18,6 +19,10 @@
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
$: paginationProps = useCreateLink
|
||||
? ({ type: 'link', createLink: getLink } as const)
|
||||
: ({ type: 'button', createLink: undefined as never } as const);
|
||||
</script>
|
||||
|
||||
<Pagination page={currentPage} total={sum} {limit} createLink={getLink} type="link" />
|
||||
<Pagination on:page {limit} total={sum} page={currentPage} {...paginationProps} />
|
||||
|
||||
@@ -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');
|
||||
</script>
|
||||
|
||||
<Layout.Stack direction="row" alignItems="center" wrap="wrap" justifyContent="space-between">
|
||||
<Limit {limit} sum={total} {name} />
|
||||
<Pagination {limit} {offset} sum={total} />
|
||||
<Layout.Stack wrap="wrap" {direction} {alignItems} justifyContent="space-between">
|
||||
{#if showLimit}
|
||||
<Limit {limit} sum={total} {name} />
|
||||
{/if}
|
||||
|
||||
<Pagination on:page {limit} {offset} sum={total} {useCreateLink} />
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
$: disabled = !value || $groups.has(value);
|
||||
</script>
|
||||
|
||||
<Modal title="Label" bind:show on:close={reset} onSubmit={create}>
|
||||
<Modal title="Label" bind:show on:close={reset} onSubmit={create} size="s">
|
||||
<svelte:fragment slot="description">
|
||||
Labels allow you to grant access to users with the specified label.
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
: '';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { page } from '$app/state';
|
||||
import { page } from '$app/stores';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { fade } from 'svelte/transition';
|
||||
import {
|
||||
Icon,
|
||||
Sidebar,
|
||||
@@ -37,6 +37,7 @@
|
||||
import { getSidebarState, updateSidebarState } from '$lib/helpers/sidebar';
|
||||
import { isTabletViewport, isSmallViewport } from '$lib/stores/viewport';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { bannerSpacing } from '$lib/layout/headerAlert.svelte';
|
||||
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
@@ -71,7 +72,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: state = $isTabletViewport ? 'closed' : getSidebarState();
|
||||
$: state = $isTabletViewport ? 'closed' : getSidebarState($page);
|
||||
|
||||
const projectOptions = [
|
||||
{ name: 'Auth', icon: IconUserGroup, slug: 'auth', category: 'build' },
|
||||
@@ -89,7 +90,7 @@
|
||||
];
|
||||
|
||||
const isSelected = (service: string): boolean => {
|
||||
return page.route.id?.includes(service);
|
||||
return $page.route.id?.includes(service);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -101,7 +102,7 @@
|
||||
<Sidebar.Base
|
||||
{...$$props}
|
||||
bind:state
|
||||
on:resize={(event) => updateSidebarState(event.detail)}
|
||||
on:resize={(event) => updateSidebarState($page, event.detail)}
|
||||
resizable>
|
||||
<div slot="top">
|
||||
<div class="only-mobile-tablet top">
|
||||
@@ -240,7 +241,9 @@
|
||||
<span slot="tooltip">{projectOption.name}</span>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
{#if project}
|
||||
{#if project && $isSmallViewport}
|
||||
<Divider />
|
||||
|
||||
<div class="mobile-tablet-settings">
|
||||
<Tooltip placement="right" disabled={state !== 'icons'}>
|
||||
<a
|
||||
@@ -354,7 +357,10 @@
|
||||
</div>
|
||||
|
||||
{#if subNavigation}
|
||||
<div class="sub-navigation" class:icons={state === 'icons'}>
|
||||
<div
|
||||
class="sub-navigation"
|
||||
class:icons={state === 'icons'}
|
||||
style:--banner-spacing={$bannerSpacing ? $bannerSpacing : undefined}>
|
||||
<svelte:component this={subNavigation} />
|
||||
</div>
|
||||
{/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script module lang="ts">
|
||||
export type SortDirection = 'asc' | 'desc' | 'default';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconChevronDown, IconChevronUp, IconSelector } from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { SortState } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store';
|
||||
|
||||
let {
|
||||
column,
|
||||
state,
|
||||
onSort = () => {},
|
||||
disabled = false
|
||||
}: {
|
||||
column: string;
|
||||
disabled?: boolean;
|
||||
onSort?: (query: string | null) => void | Promise<void>;
|
||||
state?: Writable<SortState>;
|
||||
} = $props();
|
||||
|
||||
function sort() {
|
||||
const isCurrent = $state.column === column;
|
||||
const nextDir: SortDirection = !isCurrent
|
||||
? 'asc'
|
||||
: $state.direction === 'asc'
|
||||
? 'desc'
|
||||
: $state.direction === 'desc'
|
||||
? 'default'
|
||||
: 'asc';
|
||||
|
||||
state?.set({
|
||||
direction: nextDir,
|
||||
column: nextDir === 'default' ? null : column
|
||||
});
|
||||
|
||||
if (nextDir === 'default') {
|
||||
onSort?.(null);
|
||||
} else if (nextDir === 'asc') {
|
||||
onSort?.(Query.orderAsc(column));
|
||||
} else {
|
||||
onSort?.(Query.orderDesc(column));
|
||||
}
|
||||
}
|
||||
|
||||
const direction = $derived(
|
||||
$state?.column === column && $state?.direction !== 'default' ? $state?.direction : ''
|
||||
);
|
||||
</script>
|
||||
|
||||
<Button extraCompact on:click={sort} class="hoverable-compact {direction}" {disabled}>
|
||||
<Icon
|
||||
size="s"
|
||||
icon={$state?.column !== column || $state?.direction === 'default'
|
||||
? IconSelector
|
||||
: $state?.direction === 'asc'
|
||||
? IconChevronUp
|
||||
: IconChevronDown}
|
||||
color="--fgcolor-neutral-weak" />
|
||||
</Button>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.hoverable-compact) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
--p-button-padding-inline: var(--space-0);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bgcolor-neutral-secondary);
|
||||
border: var(--border-width-s) solid var(--bgcolor-neutral-secondary);
|
||||
|
||||
& :global(i) {
|
||||
--icon-fill: var(--fgcolor-neutral-secondary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bgcolor-neutral-tertiary);
|
||||
border: var(--border-width-s) solid var(--bgcolor-neutral-tertiary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
& :global(i) {
|
||||
--icon-fill: var(--fgcolor-neutral-secondary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.hoverable-compact.asc),
|
||||
:global(.hoverable-compact.desc) {
|
||||
background-color: var(--bgcolor-neutral-secondary) !important;
|
||||
& :global(i) {
|
||||
--icon-fill: var(--fgcolor-neutral-secondary) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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<Column[]>;
|
||||
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<Column[]>;
|
||||
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);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !hideColumns && view === View.Table}
|
||||
<ColumnSelector {columns} {isCustomCollection} {allowNoColumns}>
|
||||
<ColumnSelector
|
||||
{ui}
|
||||
{columns}
|
||||
{showAnyway}
|
||||
{isCustomTable}
|
||||
{allowNoColumns}
|
||||
onPreferencesUpdated={updateBadgeState}>
|
||||
{#snippet children(toggle, selectedColumnsNumber)}
|
||||
<Button.Button
|
||||
size="s"
|
||||
icon={onlyIcon}
|
||||
onclick={toggle}
|
||||
variant="secondary"
|
||||
badge={selectedColumnsNumber.toString()}
|
||||
on:click={toggle}>
|
||||
disabled={!$columns.length && showAnyway}
|
||||
class={onlyIcon && !$isSmallViewport ? 'width-fix' : undefined}
|
||||
badge={showCountBadge ? selectedColumnsNumber.toString() : undefined}>
|
||||
<Icon slot="start" icon={IconViewBoards} />
|
||||
</Button.Button>
|
||||
{/snippet}
|
||||
@@ -32,3 +82,10 @@
|
||||
{#if !hideView}
|
||||
<ViewToggle bind:view />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.width-fix) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+54
-9
@@ -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<EventService> = [
|
||||
@@ -448,11 +493,11 @@ export const eventServices: Array<EventService> = [
|
||||
name: 'databases',
|
||||
resources: [
|
||||
{
|
||||
name: 'collections',
|
||||
name: 'tables',
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
name: 'rows',
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
}
|
||||
],
|
||||
@@ -479,7 +524,7 @@ export const eventServices: Array<EventService> = [
|
||||
name: 'memberships',
|
||||
actions: [
|
||||
{ name: 'create' },
|
||||
{ name: 'update', attributes: ['status'] },
|
||||
{ name: 'update', columns: ['status'] },
|
||||
{ name: 'delete' }
|
||||
]
|
||||
}
|
||||
@@ -495,7 +540,7 @@ export const eventServices: Array<EventService> = [
|
||||
],
|
||||
actions: [
|
||||
{ name: 'create' },
|
||||
{ name: 'update', attributes: ['email', 'name', 'password', 'status', 'prefs'] },
|
||||
{ name: 'update', columns: ['email', 'name', 'password', 'status', 'prefs'] },
|
||||
{ name: 'delete' }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import { Badge } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let {
|
||||
min = 1,
|
||||
max = 100,
|
||||
maxAllowed = max,
|
||||
value = $bindable(min),
|
||||
breakPoints = [],
|
||||
autoBreakpoints = true,
|
||||
breakpointCount = 7,
|
||||
snapToBreakpoints = true,
|
||||
snapThreshold = 3,
|
||||
disabled = false,
|
||||
id = ID.unique(),
|
||||
onchange
|
||||
}: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
maxAllowed?: number;
|
||||
value?: number;
|
||||
breakPoints?: number[];
|
||||
autoBreakpoints?: boolean;
|
||||
breakpointCount?: number;
|
||||
snapToBreakpoints?: boolean;
|
||||
snapThreshold?: number;
|
||||
disabled?: boolean;
|
||||
id?: string;
|
||||
onchange?: (value: number) => void;
|
||||
} = $props();
|
||||
|
||||
let isDragging = $state(false);
|
||||
let trackRef = $state<HTMLDivElement>();
|
||||
|
||||
const range = $derived(max - min);
|
||||
const clampedValue = $derived(Math.min(Math.max(value, min), maxAllowed));
|
||||
const relativeValue = $derived(clampedValue - min);
|
||||
const percentage = $derived((relativeValue / max) * 100);
|
||||
const maxAllowedPercentage = $derived(((maxAllowed - min) / range) * 100);
|
||||
|
||||
const calculatedBreakpoints = $derived(() => {
|
||||
if (!autoBreakpoints) return breakPoints;
|
||||
|
||||
const points = [];
|
||||
if (breakpointCount === 1) {
|
||||
points.push(min);
|
||||
} else {
|
||||
points.push(min);
|
||||
|
||||
if (breakpointCount > 2) {
|
||||
const step = range / (breakpointCount - 1);
|
||||
for (let i = 1; i < breakpointCount - 1; i++) {
|
||||
const rawValue = min + i * step;
|
||||
|
||||
let roundedValue: number;
|
||||
if (range <= 10) {
|
||||
roundedValue = Math.round(rawValue);
|
||||
} else if (range <= 100) {
|
||||
roundedValue = Math.round(rawValue / 5) * 5;
|
||||
} else if (range <= 1000) {
|
||||
roundedValue = Math.round(rawValue / 10) * 10;
|
||||
} else {
|
||||
roundedValue = Math.round(rawValue / 25) * 25;
|
||||
}
|
||||
|
||||
points.push(Math.max(min + 1, Math.min(max - 1, roundedValue)));
|
||||
}
|
||||
}
|
||||
|
||||
points.push(max);
|
||||
}
|
||||
|
||||
return points;
|
||||
});
|
||||
|
||||
const exactBreakpoints = $derived(() => {
|
||||
if (!autoBreakpoints) return breakPoints;
|
||||
|
||||
return calculatedBreakpoints();
|
||||
});
|
||||
|
||||
const debouncedOnChange = debounce((newValue: number) => {
|
||||
onchange?.(Math.round(newValue));
|
||||
}, 150);
|
||||
|
||||
function findClosestBreakpoint(value: number): number | null {
|
||||
if (!snapToBreakpoints || calculatedBreakpoints().length === 0) return null;
|
||||
|
||||
let closest = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
for (const breakpoint of calculatedBreakpoints()) {
|
||||
const distance = Math.abs(value - breakpoint);
|
||||
const distancePercentage = (distance / range) * 100;
|
||||
|
||||
if (distancePercentage <= snapThreshold && distance < closestDistance) {
|
||||
closest = breakpoint;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
isDragging = true;
|
||||
updateValue(event);
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
if (!isDragging) return;
|
||||
updateValue(event);
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isDragging = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
function handleTrackClick(event: MouseEvent) {
|
||||
if (disabled || isDragging) return;
|
||||
|
||||
updateValue(event);
|
||||
}
|
||||
|
||||
function updateValue(event: MouseEvent) {
|
||||
if (!trackRef) return;
|
||||
|
||||
const rect = trackRef.getBoundingClientRect();
|
||||
const clickX = event.clientX - rect.left;
|
||||
const newPercentage = Math.max(0, Math.min(100, (clickX / rect.width) * 100));
|
||||
let newValue = min + (newPercentage / 100) * (max - min);
|
||||
|
||||
const snapPoint = findClosestBreakpoint(newValue);
|
||||
if (snapPoint !== null) {
|
||||
newValue = snapPoint;
|
||||
}
|
||||
|
||||
const clampedNewValue = Math.max(min, Math.min(newValue, maxAllowed));
|
||||
const roundedValue = Math.round(clampedNewValue);
|
||||
|
||||
if (roundedValue !== value) {
|
||||
value = roundedValue;
|
||||
debouncedOnChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (isDragging) {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="seekbar">
|
||||
<div class="track" role="presentation" bind:this={trackRef} onclick={handleTrackClick}>
|
||||
{#if maxAllowed < max}
|
||||
<div
|
||||
class="disabled-area"
|
||||
style:left="{maxAllowedPercentage}%"
|
||||
style:width="{100 - maxAllowedPercentage}%">
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="progress" style:--progress-width="{percentage}%"></div>
|
||||
|
||||
<div class="thumb-container" style:left="{percentage}%">
|
||||
<div class="badge">
|
||||
<Badge variant="secondary" size="xs" content={Math.ceil(clampedValue).toString()} />
|
||||
</div>
|
||||
<div
|
||||
{id}
|
||||
role="slider"
|
||||
class="thumb"
|
||||
tabindex={disabled ? -1 : 0}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax={maxAllowed}
|
||||
aria-valuenow={Math.ceil(clampedValue)}
|
||||
onmousedown={handleMouseDown}
|
||||
onmouseenter={() => !disabled}
|
||||
onmouseleave={() => !isDragging}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if exactBreakpoints().length > 0}
|
||||
<div class="breakpoints">
|
||||
{#each exactBreakpoints() as breakPoint, index (index)}
|
||||
{@const breakPercentage = ((breakPoint - min) / range) * 100}
|
||||
<div
|
||||
class="breakpoint"
|
||||
class:is-major={index === 0 ||
|
||||
index === exactBreakpoints().length - 1 ||
|
||||
index === Math.floor(exactBreakpoints().length / 2)}
|
||||
style:left="{breakPercentage}%">
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:root {
|
||||
--seekbar-height: 4px;
|
||||
--seekbar-thumb-size: 16px;
|
||||
--seekbar-thumb-radius: 50%;
|
||||
--seekbar-track-radius: 13px;
|
||||
|
||||
--seekbar-breakpoint-height-small: 6px;
|
||||
--seekbar-breakpoint-height-medium: 6px;
|
||||
--seekbar-breakpoint-height-major: 12px;
|
||||
|
||||
--seekbar-thumb-color: var(--bgcolor-neutral-primary);
|
||||
--seekbar-track-color: var(--bgcolor-neutral-tertiary);
|
||||
--seekbar-progress-color: var(--bgcolor-neutral-invert);
|
||||
--seekbar-thumb-border-color: var(--fgcolor-accent-neutral);
|
||||
--seekbar-breakpoint-color: var(--bgcolor-neutral-invert-weaker);
|
||||
|
||||
// experimental, disabled seekbar progress color
|
||||
--seekbar-disabled-area-color: var(--overlay-neutral-pressed);
|
||||
}
|
||||
|
||||
.seekbar {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 1.25rem 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.breakpoints {
|
||||
position: absolute;
|
||||
top: calc(1.25rem + var(--seekbar-height) + 8px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.breakpoint {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
width: 1px;
|
||||
height: var(--seekbar-breakpoint-height-small);
|
||||
background-color: var(--seekbar-breakpoint-color);
|
||||
|
||||
&.is-major {
|
||||
height: var(--seekbar-breakpoint-height-major);
|
||||
}
|
||||
}
|
||||
|
||||
.track {
|
||||
position: relative;
|
||||
height: var(--seekbar-height);
|
||||
background-color: var(--seekbar-track-color);
|
||||
border-radius: var(--seekbar-track-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled-area {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
border-top-right-radius: var(--seekbar-track-radius);
|
||||
border-bottom-right-radius: var(--seekbar-track-radius);
|
||||
background-color: var(--seekbar-disabled-area-color);
|
||||
}
|
||||
|
||||
.progress {
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: var(--progress-width);
|
||||
background-color: var(--seekbar-progress-color);
|
||||
border-radius: var(--seekbar-track-radius);
|
||||
}
|
||||
|
||||
.thumb-container {
|
||||
top: 50%;
|
||||
position: absolute;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
.badge {
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
transform: translateX(-50%);
|
||||
|
||||
:global(.badge) {
|
||||
color: var(--fgcolor-on-invert) !important;
|
||||
background: var(--bgcolor-neutral-invert) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: var(--seekbar-thumb-size);
|
||||
height: var(--seekbar-thumb-size);
|
||||
border-radius: var(--seekbar-thumb-radius);
|
||||
background-color: var(--seekbar-thumb-color);
|
||||
border: 1px solid var(--seekbar-thumb-border-color);
|
||||
cursor: grab;
|
||||
}
|
||||
</style>
|
||||
@@ -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';
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</script>
|
||||
@@ -61,7 +74,8 @@
|
||||
helper={error || helper}
|
||||
state={error ? 'error' : 'default'}
|
||||
on:invalid={handleInvalid}
|
||||
on:input>
|
||||
on:input={coerceToNumber}
|
||||
on:change={coerceToNumber}>
|
||||
<svelte:fragment slot="info">
|
||||
<slot name="info" slot="info" />
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -51,4 +51,5 @@
|
||||
on:input
|
||||
bind:value>
|
||||
<slot name="info" slot="info" />
|
||||
<slot name="end" slot="end" />
|
||||
</Input.Textarea>
|
||||
|
||||
+1
-2
@@ -19,6 +19,5 @@ function isFlagEnabled(name: string) {
|
||||
}
|
||||
|
||||
export const flags = {
|
||||
showSites: isFlagEnabled('sites'),
|
||||
showIndexLengths: isFlagEnabled('index-lengths')
|
||||
showSites: isFlagEnabled('sites')
|
||||
};
|
||||
|
||||
+138
-138
@@ -4,188 +4,188 @@ import type { Models } from '@appwrite.io/console';
|
||||
export async function createRecord(record: Partial<Models.DnsRecord>, 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<Models.DnsRecord>, 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Columns[]> {
|
||||
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<string, string | number | boolean | Array<string | number | boolean>> = {
|
||||
$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, () => 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<string | number | boolean> {
|
||||
if (column.array) {
|
||||
const arraySize = faker.number.int({ min: 1, max: 5 });
|
||||
const items: Array<string | number | boolean> = [];
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -35,7 +35,7 @@ function updateNotificationPrefs(parsedPrefs: Record<string, NotificationPrefIte
|
||||
notificationPrefs: parsedPrefs
|
||||
};
|
||||
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
sdk.forConsole.account.updatePrefs({ prefs: newPrefs });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,5 +26,5 @@ export async function setHasOnboardingDismissed(projectId: string, account?: Acc
|
||||
onboardingDismissed: onboardingDismissed
|
||||
};
|
||||
|
||||
await sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
await sdk.forConsole.account.updatePrefs({ prefs: newPrefs });
|
||||
}
|
||||
|
||||
@@ -15,8 +15,15 @@ export function filterRegions(regions: Models.ConsoleRegion[]): RegionOption[] {
|
||||
return regions
|
||||
.filter((region) => 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) => ({
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
github: 'github.com',
|
||||
gitlab: 'gitlab.com',
|
||||
bitbucket: 'bitbucket.org'
|
||||
};
|
||||
|
||||
const host = hostMap[provider.toLowerCase()] ?? provider; // fallback
|
||||
|
||||
return `https://${host}/${owner}/${repo}`;
|
||||
}
|
||||
@@ -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<T extends Record<string, string>>(
|
||||
|
||||
@@ -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');
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 534 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 349 KiB |
@@ -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 } } : {}) }
|
||||
];
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Container
|
||||
{databasesScreen}
|
||||
{insideSideSheet}
|
||||
expanded={databasesScreen && !insideSideSheet}
|
||||
slotSpacing={databasesScreen && !insideSideSheet}>
|
||||
{#if logs.total}
|
||||
<Table.Root {columns} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="user" {root}>User</Table.Header.Cell>
|
||||
<Table.Header.Cell column="event" {root}>Event</Table.Header.Cell>
|
||||
<Table.Header.Cell column="location" {root}>Location</Table.Header.Cell>
|
||||
<Table.Header.Cell column="ip" {root}>IP</Table.Header.Cell>
|
||||
<Table.Header.Cell column="date" {root}>Date</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each logs.logs as log}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="user" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
{#if log.userEmail}
|
||||
{#if log.userName}
|
||||
<AvatarInitials size="xs" name={log.userName} />
|
||||
<Trim>{log.userName}</Trim>
|
||||
<div>
|
||||
<Table.Root {columns} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="user" {root}>User</Table.Header.Cell>
|
||||
<Table.Header.Cell column="event" {root}>Event</Table.Header.Cell>
|
||||
<Table.Header.Cell column="location" {root}>Location</Table.Header.Cell>
|
||||
<Table.Header.Cell column="ip" {root}>IP</Table.Header.Cell>
|
||||
<Table.Header.Cell column="date" {root}>Date</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each logs.logs as log}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="user" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
{#if log.userEmail}
|
||||
{#if log.userName}
|
||||
<AvatarInitials size="xs" name={log.userName} />
|
||||
<Trim>{log.userName}</Trim>
|
||||
{:else}
|
||||
<AvatarInitials size="xs" name={log.userEmail} />
|
||||
<Trim>{log.userEmail}</Trim>
|
||||
{/if}
|
||||
{:else}
|
||||
<AvatarInitials size="xs" name={log.userEmail} />
|
||||
<Trim>{log.userEmail}</Trim>
|
||||
<div class="avatar is-size-small">
|
||||
<span class="icon-anonymous" aria-hidden="true"></span>
|
||||
</div>
|
||||
<span class="text u-trim">{log.userName ?? 'Anonymous'}</span>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="event" {root}>
|
||||
{log.event}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="location" {root}>
|
||||
{#if log.countryCode !== '--'}
|
||||
{log.countryName}
|
||||
{:else}
|
||||
<div class="avatar is-size-small">
|
||||
<span class="icon-anonymous" aria-hidden="true"></span>
|
||||
</div>
|
||||
<span class="text u-trim">{log.userName ?? 'Anonymous'}</span>
|
||||
Unknown
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="event" {root}>
|
||||
{log.event}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="location" {root}>
|
||||
{#if log.countryCode !== '--'}
|
||||
{log.countryName}
|
||||
{:else}
|
||||
Unknown
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="ip" {root}>
|
||||
{log.ip}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="date" {root}>
|
||||
{toLocaleDateTime(log.time)}
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="ip" {root}>
|
||||
{log.ip}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="date" {root}>
|
||||
{toLocaleDateTime(log.time)}
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<PaginationWithLimit name="Logs" {limit} {offset} total={logs.total} />
|
||||
<PaginationWithLimit
|
||||
{limit}
|
||||
{offset}
|
||||
on:page
|
||||
name="Logs"
|
||||
total={logs.total}
|
||||
useCreateLink={useCreateLinkForPagination} />
|
||||
{:else}
|
||||
<Card.Base padding="none">
|
||||
<Empty
|
||||
@@ -78,7 +93,10 @@
|
||||
description="Need a hand? Learn more in our documentation."
|
||||
type="secondary">
|
||||
<svelte:fragment slot="actions">
|
||||
<Button external secondary href="https://appwrite.io/docs">
|
||||
<Button
|
||||
external
|
||||
secondary
|
||||
href="https://appwrite.io/docs/products/databases/databases">
|
||||
Documentation
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { IconChevronLeft } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Button, Icon, Layout } from '@appwrite.io/pink-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
href = null,
|
||||
collapsed = false,
|
||||
children
|
||||
}: {
|
||||
href?: string | null;
|
||||
collapsed?: boolean;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
const buttonSize = $derived(collapsed ? 'xs' : 's');
|
||||
const currentLineHeight = $derived(collapsed ? '130%' : '140%');
|
||||
const currentLetterSpacing = $derived(collapsed ? '0' : '-0.144px');
|
||||
const currentFontSize = $derived(collapsed ? 'var(--font-size-l)' : 'var(--font-size-xxxl)');
|
||||
</script>
|
||||
|
||||
<Layout.Stack justifyContent="center" alignItems="center" direction="row" gap="xs" inline>
|
||||
{#if href && !$isSmallViewport}
|
||||
<span style:position="relative">
|
||||
<Button.Anchor size={buttonSize} icon variant="text" {href} aria-label="page back">
|
||||
<Icon icon={IconChevronLeft} />
|
||||
</Button.Anchor>
|
||||
</span>
|
||||
{/if}
|
||||
<h1
|
||||
class="animated-title"
|
||||
style:font-size={currentFontSize}
|
||||
style:line-height={currentLineHeight}
|
||||
style:letter-spacing={currentLetterSpacing}>
|
||||
{@render children()}
|
||||
</h1>
|
||||
</Layout.Stack>
|
||||
|
||||
<style lang="scss">
|
||||
.animated-title {
|
||||
font-family: var(--font-family-brand), var(--sans-fallbacks);
|
||||
font-weight: 400;
|
||||
color: var(--fgcolor-neutral-primary);
|
||||
text-align: start;
|
||||
margin: 0;
|
||||
|
||||
transition:
|
||||
font-size 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
line-height 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
letter-spacing 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
|
||||
// TODO: needs better 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
|
||||
@@ -8,8 +17,16 @@
|
||||
: '';
|
||||
</script>
|
||||
|
||||
<div style:container-type="inline-size" class:overlap-cover={overlapCover}>
|
||||
<div class="console-container" {style}>
|
||||
<div style:container-type="inline-size" class:overlap-cover={overlapCover} {...$$restProps}>
|
||||
<div
|
||||
{style}
|
||||
class:expanded
|
||||
class:slotSpacing
|
||||
class:databasesScreen
|
||||
class:insideSideSheet
|
||||
class:expandHeightButton
|
||||
class="console-container"
|
||||
class:paddingInlineEnd={!paddingInlineEnd}>
|
||||
<Layout.Stack gap="l">
|
||||
<slot />
|
||||
</Layout.Stack>
|
||||
@@ -18,26 +35,92 @@
|
||||
|
||||
<style>
|
||||
.overlap-cover {
|
||||
z-index: 1;
|
||||
margin-block-start: -3.5rem;
|
||||
}
|
||||
:global(.console-container) {
|
||||
margin-block: var(--base-32);
|
||||
|
||||
&.insideSideSheet {
|
||||
margin-block: var(--base-20);
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
max-width: unset;
|
||||
margin-inline: 2.75rem;
|
||||
padding-inline-end: 0 !important;
|
||||
margin-block: var(--base-8) !important;
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
margin-inline: 0 !important;
|
||||
padding-inline: 53px !important;
|
||||
}
|
||||
|
||||
&.expandHeightButton {
|
||||
margin-inline-end: unset;
|
||||
margin-inline-start: 2.75rem;
|
||||
padding-inline-end: 0.5rem !important;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-inline: 4rem;
|
||||
|
||||
&.expandHeightButton {
|
||||
margin-inline: 0.5rem;
|
||||
margin-inline-start: 4rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-inline: 1rem;
|
||||
|
||||
&.expandHeightButton {
|
||||
margin-inline-start: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.slotSpacing {
|
||||
padding-block-start: var(--base-32) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.paddingInlineEnd {
|
||||
@media (min-width: 1024px) {
|
||||
padding-inline-end: 2.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.databasesScreen {
|
||||
@media (min-width: 1440px) {
|
||||
min-width: 1070px;
|
||||
/*max-width: 1144px;*/
|
||||
}
|
||||
|
||||
@media (min-width: 1728px) {
|
||||
min-width: 1070px;
|
||||
padding-inline: 196px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 360px) {
|
||||
margin-inline: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
margin-inline: auto;
|
||||
max-width: calc(944px - 11rem);
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
margin-inline: auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
margin-inline: auto;
|
||||
max-width: 1144px;
|
||||
}
|
||||
|
||||
@media (min-width: 1728px) {
|
||||
margin-inline: auto;
|
||||
max-width: 1200px;
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
users: 1,
|
||||
executions: 1
|
||||
};
|
||||
|
||||
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
|
||||
const limitedServices = [
|
||||
{ name: 'bandwidth', value: bandwidth },
|
||||
{ name: 'documents', value: documents },
|
||||
|
||||
@@ -1,33 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
|
||||
export let size: 'small' | 'medium' | 'large' | 'xl' = null;
|
||||
export let blocksize = '152px';
|
||||
export let expanded: boolean = false;
|
||||
export let animate: boolean = false;
|
||||
export let collapsed: boolean = false;
|
||||
|
||||
let isAnimating = false;
|
||||
let animationTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
$: style = size
|
||||
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
|
||||
: '';
|
||||
|
||||
$: marginTop = expanded && $isTabletViewport ? '48px' : undefined;
|
||||
|
||||
$: animatedBlocksize = animate && collapsed ? '65px' : blocksize;
|
||||
$: headerPaddingTop = animate && collapsed ? 'var(--base-16)' : 'var(--base-32)';
|
||||
$: headerPaddingBottom = animate && collapsed ? 'var(--base-8)' : 'var(--base-16)';
|
||||
|
||||
$: if (animate) {
|
||||
clearTimeout(animationTimeout);
|
||||
isAnimating = true;
|
||||
animationTimeout = setTimeout(() => {
|
||||
isAnimating = false;
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="top-cover-console" style:block-size={blocksize}>
|
||||
<div class="cover-container" {style}>
|
||||
<div
|
||||
class:animate
|
||||
class:expanded
|
||||
class:collapsed={animate && collapsed}
|
||||
class="top-cover-console"
|
||||
style:margin-top={marginTop}
|
||||
style:block-size={animatedBlocksize}
|
||||
style:padding-block-start={headerPaddingTop}
|
||||
style:padding-block-end={headerPaddingBottom}>
|
||||
<div
|
||||
class="cover-container"
|
||||
{style}
|
||||
class:expanded
|
||||
class:collapsed={animate && collapsed}
|
||||
class:animating={isAnimating}>
|
||||
<Layout.Stack direction="row" alignItems="baseline">
|
||||
<slot name="header" />
|
||||
</Layout.Stack>
|
||||
<slot />
|
||||
|
||||
{#if $$slots.default}
|
||||
<div class:expanded-slot={expanded}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.top-cover-console {
|
||||
container-type: inline-size;
|
||||
padding-block-start: var(--base-32);
|
||||
padding-block-end: var(--base-16);
|
||||
border-bottom: 1px solid var(--border-neutral, #2d2d31);
|
||||
background: var(--bgcolor-neutral-primary, #1d1d21);
|
||||
margin-left: -190px;
|
||||
padding-left: 190px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.animate {
|
||||
transition:
|
||||
block-size 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
padding-block-start 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
padding-block-end 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-bottom-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
border-bottom-color: var(--border-neutral, #2d2d31);
|
||||
}
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
position: relative;
|
||||
margin: 0 1rem;
|
||||
@@ -39,21 +91,40 @@
|
||||
@media (min-width: 360px) {
|
||||
margin-inline: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
margin-inline: auto;
|
||||
max-width: calc(944px - 11rem);
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
margin-inline: auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
margin-inline: auto;
|
||||
max-width: 1144px;
|
||||
}
|
||||
|
||||
@media (min-width: 1728px) {
|
||||
margin-inline: auto;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
margin-inline: 1rem !important;
|
||||
}
|
||||
|
||||
&.animating {
|
||||
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-slot {
|
||||
margin-inline: 3rem !important;
|
||||
@media (max-width: 768px) {
|
||||
margin-inline: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { IconChevronLeft } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Typography, Button, Icon, Layout } from '@appwrite.io/pink-svelte';
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
|
||||
<Layout.Stack justifyContent="center" alignItems="center" direction="row" gap="xs" inline>
|
||||
{#if href}
|
||||
<span
|
||||
style={$isTabletViewport ? 'position: relative' : 'position: absolute; left: -2.75rem'}>
|
||||
<span style:position="relative">
|
||||
<Button.Anchor size="s" icon variant="text" {href} aria-label="page back">
|
||||
<Icon icon={IconChevronLeft} />
|
||||
</Button.Anchor>
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
view = $bindable(),
|
||||
columns,
|
||||
hideColumns = false,
|
||||
isCustomCollection = false
|
||||
isCustomTable = false
|
||||
}: {
|
||||
show?: boolean;
|
||||
hideView?: boolean;
|
||||
view?: View;
|
||||
columns?: Writable<Column[]>;
|
||||
hideColumns?: boolean;
|
||||
isCustomCollection?: boolean;
|
||||
isCustomTable?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
{#if !hideColumns && $columns?.length}
|
||||
<Layout.Stack gap="xs">
|
||||
<Typography.Text>Columns</Typography.Text>
|
||||
<ColumnSelector {columns} {isCustomCollection}>
|
||||
<ColumnSelector {columns} {isCustomTable}>
|
||||
{#snippet children(toggle, selectedColumnsNumber)}
|
||||
<Button.Button
|
||||
size="s"
|
||||
|
||||
@@ -11,19 +11,24 @@
|
||||
Button,
|
||||
Badge
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const hideFooter = $derived.by(() => {
|
||||
const endings = ['table-[table]', 'table-[table]/columns', 'table-[table]/indexes'];
|
||||
return endings.some((end) => page.route.id?.endsWith(end));
|
||||
});
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<footer class:hide={hideFooter}>
|
||||
<Divider />
|
||||
<Layout.Stack direction={$isSmallViewport ? 'column-reverse' : 'row'}>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={$isSmallViewport ? 'm' : 'l'}
|
||||
inline={$isTabletViewport}
|
||||
justifyContent="flex-start">
|
||||
<Typography.Caption variant="400">
|
||||
ⓒ {currentYear} Appwrite. All rights reserved.
|
||||
@@ -54,63 +59,37 @@
|
||||
</Button.Anchor>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{#if isCloud && $isSmallViewport}
|
||||
<div class="extra-margin">
|
||||
<Layout.Stack direction="row" justifyContent={'flex-start'} alignItems="center">
|
||||
{#if $version}
|
||||
{#if isCloud}
|
||||
<Icon size="s" icon={IconCloud} />
|
||||
{/if}
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
href="https://github.com/appwrite/appwrite/releases"
|
||||
aria-label="Appwrite releases on Github"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Version {$version}
|
||||
</Link.Anchor>
|
||||
<span class="divider-wrapper">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
{/if}
|
||||
<Badge
|
||||
size="xs"
|
||||
type="success"
|
||||
variant="secondary"
|
||||
content="Generally Available"
|
||||
style="white-space: nowrap;" />
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
justifyContent={$isSmallViewport ? 'flex-start' : 'flex-end'}
|
||||
alignItems="center">
|
||||
{#if isCloud}
|
||||
{#if !$isSmallViewport}
|
||||
alignItems="center"
|
||||
wrap={$isSmallViewport ? 'wrap' : 'normal'}>
|
||||
{#if !$isSmallViewport}
|
||||
{#if isCloud}
|
||||
<Badge
|
||||
size="xs"
|
||||
type="success"
|
||||
variant="secondary"
|
||||
content="Generally Available"
|
||||
style="white-space: nowrap;" />
|
||||
|
||||
<Icon size="s" icon={IconCloud} />
|
||||
{#if $version}
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
href="https://github.com/appwrite/appwrite/releases"
|
||||
aria-label="Appwrite releases on Github"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Version {$version}
|
||||
</Link.Anchor>
|
||||
<span class="divider-wrapper">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if $version}
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
href="https://github.com/appwrite/appwrite/releases"
|
||||
aria-label="Appwrite releases on Github"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style="white-space: nowrap;">
|
||||
Version {$version}
|
||||
</Link.Anchor>
|
||||
<span class="divider-wrapper">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -157,6 +136,32 @@
|
||||
Cookies
|
||||
</Link.Anchor>
|
||||
{/if}
|
||||
{#if $isSmallViewport}
|
||||
{#if $version}
|
||||
<span class="divider-wrapper">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
href="https://github.com/appwrite/appwrite/releases"
|
||||
aria-label="Appwrite releases on Github"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style="white-space: nowrap;">
|
||||
Version {$version}
|
||||
</Link.Anchor>
|
||||
{#if isCloud}
|
||||
<Icon size="s" icon={IconCloud} />
|
||||
<Badge
|
||||
size="xs"
|
||||
type="success"
|
||||
variant="secondary"
|
||||
content="Generally Available"
|
||||
style="white-space: nowrap;" />
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</footer>
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
export let title: string;
|
||||
export let columns: Writable<Column[]>;
|
||||
export let view: View;
|
||||
export let isCustomCollection = false;
|
||||
export let isCustomTable = false;
|
||||
export let hideView = false;
|
||||
export let hideColumns = false;
|
||||
export let allowNoColumns = false;
|
||||
@@ -17,13 +17,7 @@
|
||||
<header class="grid-header">
|
||||
<Typography.Title size="m">{title}</Typography.Title>
|
||||
<div class="u-flex u-gap-16 u-contents-mobile">
|
||||
<ViewSelector
|
||||
{view}
|
||||
{columns}
|
||||
{isCustomCollection}
|
||||
{hideView}
|
||||
{hideColumns}
|
||||
{allowNoColumns} />
|
||||
<ViewSelector {view} {columns} {isCustomTable} {hideView} {hideColumns} {allowNoColumns} />
|
||||
<div class="grid-header-col-2">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
<script module lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const bannerSpacing = writable<string | null>(null);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
@@ -16,10 +22,15 @@
|
||||
if (header) {
|
||||
header.style.top = `${alertHeight}px`;
|
||||
}
|
||||
|
||||
if (sidebar) {
|
||||
sidebar.style.top = `${alertHeight + ($isTabletViewport ? 0 : header.getBoundingClientRect().height)}px`;
|
||||
sidebar.style.height = `calc(100vh - (${alertHeight + ($isTabletViewport ? 0 : header.getBoundingClientRect().height)}px))`;
|
||||
|
||||
// for sidebar and sub-navigation!
|
||||
bannerSpacing.set(`${alertHeight}px`);
|
||||
}
|
||||
|
||||
if (contentSection) {
|
||||
contentSection.style.paddingBlockStart = `${alertHeight}px`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { default as Container } from './container.svelte';
|
||||
export { default as Cover } from './cover.svelte';
|
||||
export { default as CoverTitle } from './coverTitle.svelte';
|
||||
export { default as AnimatedTitle } from './animatedTitle.svelte';
|
||||
export { default as Footer } from './footer.svelte';
|
||||
export { default as Navigation } from './navigation.svelte';
|
||||
export { default as Notifications } from './notifications.svelte';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
position: fixed;
|
||||
top: calc(var(--main-header-height) + 12px);
|
||||
right: 12px;
|
||||
z-index: 1000;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
+49
-13
@@ -22,7 +22,6 @@
|
||||
export let showFooter = true;
|
||||
export let showSideNavigation = false;
|
||||
export let selectedProject: Models.Project = null;
|
||||
export let projects: Promise<Models.ProjectList> = undefined;
|
||||
|
||||
let yOnMenuOpen: number;
|
||||
let showContentTransition = false;
|
||||
@@ -91,7 +90,13 @@
|
||||
alt: 'Logo Appwrite'
|
||||
},
|
||||
|
||||
avatar: sdk.forConsole.avatars.getInitials($user?.name, 80, 80).toString(),
|
||||
avatar: sdk.forConsole.avatars
|
||||
.getInitials({
|
||||
name: $user?.name,
|
||||
width: 80,
|
||||
height: 80
|
||||
})
|
||||
.toString(),
|
||||
|
||||
organizations: $organizationList.teams.map((org) => {
|
||||
const billingPlan = org['billingPlan'];
|
||||
@@ -104,8 +109,6 @@
|
||||
};
|
||||
}),
|
||||
|
||||
projects: projects,
|
||||
|
||||
currentProject: selectedProject
|
||||
};
|
||||
|
||||
@@ -141,13 +144,18 @@
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
isNewWizardStatusOpen.subscribe((value) => (showHeader = !value));
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={handleResize} />
|
||||
|
||||
<svelte:body use:style={$bodyStyle} />
|
||||
|
||||
{#if $activeHeaderAlert?.show && !$isNewWizardStatusOpen}
|
||||
<svelte:component this={$activeHeaderAlert.component} />
|
||||
{/if}
|
||||
|
||||
<main
|
||||
class:has-alert={$activeHeaderAlert?.show}
|
||||
class:is-open={$showSubNavigation}
|
||||
@@ -158,15 +166,20 @@
|
||||
{#if showHeader}
|
||||
<Navbar {...navbarProps} bind:sideBarIsOpen={$isSidebarOpen} bind:showAccountMenu />
|
||||
{/if}
|
||||
<Sidebar
|
||||
project={selectedProject}
|
||||
progressCard={progressCard()}
|
||||
avatar={navbarProps.avatar}
|
||||
bind:subNavigation
|
||||
bind:sideBarIsOpen={$isSidebarOpen}
|
||||
bind:showAccountMenu
|
||||
bind:state />
|
||||
|
||||
{#if !$isNewWizardStatusOpen}
|
||||
<Sidebar
|
||||
project={selectedProject}
|
||||
progressCard={progressCard()}
|
||||
avatar={navbarProps.avatar}
|
||||
bind:subNavigation
|
||||
bind:sideBarIsOpen={$isSidebarOpen}
|
||||
bind:showAccountMenu
|
||||
bind:state />
|
||||
{/if}
|
||||
|
||||
<SideNavigation bind:subNavigation />
|
||||
|
||||
<div
|
||||
class="content"
|
||||
class:has-transition={showContentTransition}
|
||||
@@ -174,7 +187,9 @@
|
||||
class:no-sidebar={!showSideNavigation}>
|
||||
<section class="main-content" data-test={showSideNavigation}>
|
||||
{#if $page.data?.header}
|
||||
<svelte:component this={$page.data.header} />
|
||||
<div class="layout-header">
|
||||
<svelte:component this={$page.data.header} />
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if showFooter}
|
||||
@@ -239,6 +254,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
:global(main:has(.databases-spreadsheet)) {
|
||||
/* avoids the unnecessary sheet slide animation */
|
||||
.has-transition {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.main-content {
|
||||
height: auto;
|
||||
padding-left: 210px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content:not(:has(.wide-screen-wrapper)) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import type { Campaign } from '$lib/stores/campaigns';
|
||||
import { Typography, Layout, Avatar } from '@appwrite.io/pink-svelte';
|
||||
import { getCampaignImageUrl } from '$routes/(public)/card/helpers';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
|
||||
export const imgLight = LoginLight;
|
||||
export const imgDark = LoginDark;
|
||||
@@ -44,7 +45,7 @@
|
||||
|
||||
<main class="grid-1-1 is-full-page" id="main">
|
||||
<section
|
||||
class={`u-flex u-flex-vertical ${variation !== 'default' ? 'u-cross-center u-main-center u-height-100-percent u-width-full-line side-bg' : 'side-default'}`}
|
||||
class={`u-flex u-flex-vertical ${variation !== 'default' ? 'u-cross-center u-main-center u-height-100-percent u-width-full-line side-bg' : !$isSmallViewport ? 'side-default' : ''}`}
|
||||
style:--url={variation !== 'default'
|
||||
? ''
|
||||
: `url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
|
||||
@@ -159,7 +160,6 @@
|
||||
<div class="u-margin-block-start-24">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<ul
|
||||
class="inline-links is-center is-with-sep u-margin-block-start-32"
|
||||
class:u-hide={!$$slots?.links}>
|
||||
@@ -167,18 +167,18 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="logo u-flex u-gap-16 u-margin-inline-auto is-only-mobile u-margin-block-start-32">
|
||||
class="logo u-flex u-gap-16 u-margin-inline-auto is-only-mobile u-margin-block-start-32 mobile-logo">
|
||||
<a href={base}>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={AppwriteLogoDark}
|
||||
width="93"
|
||||
width="120"
|
||||
class="u-block u-only-dark"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src={AppwriteLogoLight}
|
||||
width="93"
|
||||
width="120"
|
||||
class="u-block u-only-light"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
@@ -359,4 +359,26 @@
|
||||
padding-block-start: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// mobile logo positioned at bottom in mobile view
|
||||
.mobile-logo {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 100%;
|
||||
margin-block-start: 0 !important;
|
||||
}
|
||||
|
||||
@media #{devices.$break1} {
|
||||
.auth-container {
|
||||
margin-block-end: 5rem;
|
||||
margin-block-start: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -320,8 +320,10 @@ export function calculateTrialDay(org: Organization) {
|
||||
export async function checkForProjectsLimit(org: Organization, orgProjectCount?: number) {
|
||||
if (!isCloud) return;
|
||||
if (!org) return;
|
||||
|
||||
const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id);
|
||||
if (!plan) return;
|
||||
|
||||
if (plan.$id !== BillingPlan.FREE) return;
|
||||
if (org.projects?.length > 0) return;
|
||||
|
||||
@@ -369,6 +371,8 @@ export async function checkForUsageLimit(org: Organization) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
|
||||
const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {};
|
||||
const resources = [
|
||||
{ value: bandwidth, name: 'bandwidth' },
|
||||
@@ -381,6 +385,7 @@ export async function checkForUsageLimit(org: Organization) {
|
||||
const members = org.total;
|
||||
const plan = get(currentPlan);
|
||||
const membersOverflow =
|
||||
// `plan` can be null on `onboarding/create-organization` route.
|
||||
// nested null checks needed: GitHub Education plan have empty addons.
|
||||
members > plan?.addons?.seats?.limit
|
||||
? members - (plan?.addons?.seats?.limit || members)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user