Merge remote-tracking branch 'origin/main' into feat-signup-email-security

This commit is contained in:
harsh mahajan
2026-05-12 18:46:36 +05:30
13 changed files with 345 additions and 182 deletions
+16 -12
View File
@@ -8,7 +8,7 @@
import { database, checkForDatabaseBackupPolicies } from '$lib/stores/database';
import { newOrgModal, organization } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import { afterUpdate, onMount } from 'svelte';
import { onMount } from 'svelte';
import { loading } from '$routes/store';
import { requestedMigration } from '../store';
import Create from './createOrganization.svelte';
@@ -281,10 +281,12 @@
}
}
database.subscribe(async (database) => {
if (!database || !page.params.region || !page.params.project) return;
// the component checks `isCloud` internally.
await checkForDatabaseBackupPolicies(page.params.region, page.params.project, database);
onMount(() => {
return database.subscribe(async (database) => {
if (!database || !page.params.region || !page.params.project) return;
// the component checks `isCloud` internally.
await checkForDatabaseBackupPolicies(page.params.region, page.params.project, database);
});
});
let currentOrganizationId = null;
@@ -294,19 +296,23 @@
if (isCloud) {
currentOrganizationId = org.$id;
checkForEnterpriseTrial(org);
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
checkForUpgradingStatus(org);
await checkForNewDevUpgradePro(org);
if (org?.billingPlanDetails.requiresPaymentMethod) {
await paymentExpired(org);
await checkPaymentAuthorizationRequired(org);
const billingChecks: Promise<unknown>[] = [
checkForUsageLimit(org),
checkForNewDevUpgradePro(org)
];
if (org?.billingPlanDetails.requiresPaymentMethod) {
billingChecks.push(paymentExpired(org), checkPaymentAuthorizationRequired(org));
if (org?.billingTrialDays) {
calculateTrialDay(org);
}
}
await Promise.all(billingChecks);
$activeHeaderAlert = headerAlert.getExcluding('impersonation');
}
}
@@ -319,9 +325,7 @@
$registerSearchers(orgSearcher, projectsSearcher);
afterUpdate(() => {
$activeHeaderAlert = headerAlert.getExcluding('impersonation');
});
$: (void $headerAlert, ($activeHeaderAlert = headerAlert.getExcluding('impersonation')));
</script>
<CommandCenter />
+1 -25
View File
@@ -3,7 +3,7 @@ import { isCloud } from '$lib/system';
import type { LayoutLoad } from './$types';
import type { Account } from '$lib/stores/user';
import { Dependencies } from '$lib/constants';
import { Platform, Query, type Models } from '@appwrite.io/console';
import { Platform, type Models } from '@appwrite.io/console';
import { makePlansMap } from '$lib/helpers/billing';
import { plansInfo as plansInfoStore } from '$lib/stores/billing';
import { normalizeConsoleVariables } from '$lib/helpers/domains';
@@ -54,7 +54,6 @@ export const load: LayoutLoad = async ({ depends, parent, url }) => {
currentOrgId: undefined,
organizations,
consoleVariables,
allProjectsCount: 0,
plansInfo: plansInfo ?? null,
version: versionData?.version ?? null
};
@@ -107,28 +106,6 @@ export const load: LayoutLoad = async ({ depends, parent, url }) => {
preferences.organization ??
(organizations.teams.length > 0 ? organizations.teams[0].$id : undefined);
// Load projects for the current organization if one is selected
let projectsCount = 0;
if (currentOrgId) {
try {
projectsCount = (
await sdk.forConsole.projects.list({
queries: [
Query.equal('teamId', currentOrgId),
Query.limit(1),
Query.select(['$id'])
]
})
).total;
} catch (e) {
if (shouldRedirectToVerifyEmail(e)) {
redirect(303, verifyEmailUrl);
}
projectsCount = 0;
}
}
// just in case!
plansInfoStore.set(fallbackPlansInfoArray);
@@ -139,7 +116,6 @@ export const load: LayoutLoad = async ({ depends, parent, url }) => {
currentOrgId,
organizations,
consoleVariables,
allProjectsCount: projectsCount,
plansInfo: fallbackPlansInfoArray,
version: versionData?.version ?? null
};
@@ -99,11 +99,22 @@
return 'processing';
case 'failed':
return 'failed';
// pink-svelte's Status union has no 'skipped' — fall back to the
// neutral 'waiting' visual and override the label below.
case 'skipped':
return 'waiting';
default:
return 'waiting';
}
}
function getBackupStatusLabel(backup: Models.BackupArchive): string {
if (backup.status === 'skipped') {
return 'Skipped';
}
return capitalize(getBackupStatus(backup));
}
async function deleteSingleBackup(archiveId: string) {
try {
await sdk
@@ -231,7 +242,7 @@
</Table.Cell>
<Table.Cell column="status" {root}>
{@const backupStatus = getBackupStatus(backup)}
<Status status={backupStatus} label={capitalize(backupStatus)} />
<Status status={backupStatus} label={getBackupStatusLabel(backup)} />
<!--{#if backup.status === 'Failed'}-->
<!-- <span class="u-underline">Get support</span>-->
<!--{/if}-->
@@ -5,7 +5,15 @@
import type { Column, ColumnType } from '$lib/helpers/types';
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte';
import {
Icon,
Layout,
Divider,
Tooltip,
Selector,
Typography,
Dialog
} from '@appwrite.io/pink-svelte';
import type { PageProps } from './$types';
import FilePicker from '$lib/components/filePicker.svelte';
import { page } from '$app/state';
@@ -21,7 +29,7 @@
IconUpload,
IconDownload
} from '@appwrite.io/pink-icons-svelte';
import { type Models } from '@appwrite.io/console';
import { OnDuplicate, type Models } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
@@ -49,7 +57,11 @@
let isRefreshing = $state(false);
let showImportJson = $state(false);
let showImportOptions = $state(false);
let showCustomColumnsModal = $state(false);
let importOnDuplicate: OnDuplicate = $state(OnDuplicate.Fail);
let pendingFile: Models.File | null = $state(null);
let pendingLocalFile = $state(false);
let columnsError: string = $state(null);
let spreadsheet: SpreadSheet | null = $state(null);
@@ -74,17 +86,28 @@
return queryParam ? `${url}?query=${encodeURIComponent(queryParam)}` : url;
}
async function onSelect(file: Models.File, localFile = false) {
function onSelect(file: Models.File, localFile = false) {
pendingFile = file;
pendingLocalFile = localFile;
importOnDuplicate = OnDuplicate.Fail;
showImportOptions = true;
}
async function startImport() {
if (!pendingFile) return;
showImportOptions = false;
$isCollectionsJsonImportInProgress = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createJSONImport({
bucketId: file.bucketId,
fileId: file.$id,
bucketId: pendingFile.bucketId,
fileId: pendingFile.$id,
resourceId: `${page.params.database}:${page.params.collection}`,
internalFile: localFile
internalFile: pendingLocalFile,
onDuplicate: importOnDuplicate
});
addNotification({
@@ -101,6 +124,7 @@
});
} finally {
$isCollectionsJsonImportInProgress = false;
pendingFile = null;
}
}
@@ -343,6 +367,52 @@
}} />
{/if}
<Dialog title="Import options" bind:open={showImportOptions}>
<Layout.Stack gap="l">
<Typography.Text variant="m-400">
Choose how to handle documents that already exist in this collection.
</Typography.Text>
<Layout.Stack gap="m">
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Import aborts on the first document with a matching ID.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be silently skipped.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be updated with the imported data.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
<Button text on:click={() => (showImportOptions = false)}>Cancel</Button>
<Button on:click={startImport}>Start import</Button>
</Layout.Stack>
</svelte:fragment>
</Dialog>
<Modal
title="Custom columns"
bind:error={columnsError}
@@ -8,7 +8,16 @@
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
import { Icon, Layout, Divider, Tooltip, Typography, Link } from '@appwrite.io/pink-svelte';
import {
Dialog,
Icon,
Layout,
Divider,
Selector,
Tooltip,
Typography,
Link
} from '@appwrite.io/pink-svelte';
import type { PageData } from './$types';
import {
tableColumns,
@@ -34,7 +43,7 @@
IconUpload,
IconDownload
} from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import { OnDuplicate, type Models } from '@appwrite.io/console';
import CreateRow from '$database/table-[table]/rows/create.svelte';
import { onDestroy } from 'svelte';
import { isCloud } from '$lib/system';
@@ -56,6 +65,10 @@
let isRefreshing = false;
let showImportCSV = false;
let showImportOptions = false;
let importOnDuplicate: OnDuplicate = OnDuplicate.Fail;
let pendingFile: Models.File | null = null;
let pendingLocalFile = false;
// todo: might need a type fix here.
const filterColumns = writable<Column[]>([]);
@@ -107,17 +120,28 @@
$: disableButton = canShowSuggestionsSheet;
async function onSelect(file: Models.File, localFile = false) {
function onSelect(file: Models.File, localFile = false) {
pendingFile = file;
pendingLocalFile = localFile;
importOnDuplicate = OnDuplicate.Fail;
showImportOptions = true;
}
async function startImport() {
if (!pendingFile) return;
showImportOptions = false;
$isTablesCsvImportInProgress = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createCSVImport({
bucketId: file.bucketId,
fileId: file.$id,
bucketId: pendingFile.bucketId,
fileId: pendingFile.$id,
resourceId: `${page.params.database}:${page.params.table}`,
internalFile: localFile
internalFile: pendingLocalFile,
onDuplicate: importOnDuplicate
});
addNotification({
@@ -134,6 +158,7 @@
});
} finally {
$isTablesCsvImportInProgress = false;
pendingFile = null;
}
}
@@ -434,6 +459,52 @@
}} />
{/if}
<Dialog title="Import options" bind:open={showImportOptions}>
<Layout.Stack gap="l">
<Typography.Text variant="m-400">
Choose how to handle documents that already exist in this table.
</Typography.Text>
<Layout.Stack gap="m">
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Migration aborts on the first row with a matching ID.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be silently skipped.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be updated with the imported data.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
<Button text on:click={() => (showImportOptions = false)}>Cancel</Button>
<Button on:click={startImport}>Start import</Button>
</Layout.Stack>
</svelte:fragment>
</Dialog>
<CreateRow
{table}
bind:showSheet={$showRowCreateSheet.show}
@@ -62,6 +62,12 @@
.functions.deleteVariable({ functionId: data.function.$id, variableId });
await Promise.all([invalidate(Dependencies.VARIABLES), invalidate(Dependencies.FUNCTION)]);
};
const sdkListVariables = async (queries: string[]) =>
sdk.forProject(page.params.region, page.params.project).functions.listVariables({
functionId: data.function.$id,
queries
});
</script>
<Container>
@@ -105,9 +111,14 @@
{sdkCreateVariable}
{sdkUpdateVariable}
{sdkDeleteVariable}
{sdkListVariables}
isGlobal={false}
globalVariableList={data.globalVariables}
variableList={data.variables}
backendPagination
reloadPageOnPagination={false}
variablesOffset={data.variablesOffset}
variablesLimit={data.limit}
project={data.project}
analyticsSource="function_settings" />
<UpdateBuildCommand func={data.function} />
@@ -1,10 +1,16 @@
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
import { Query } from '@appwrite.io/console';
const VARIABLES_LIMIT = 100;
export const load = async ({ params, depends, parent }) => {
depends(Dependencies.VARIABLES);
depends(Dependencies.FUNCTION);
const limit = PAGE_LIMIT;
const variablesOffset = 0;
const { runtimesList, specificationsList, function: fn } = await parent();
const enabledSpecs = specificationsList?.specifications?.filter((s) => s.enabled) ?? [];
@@ -16,10 +22,13 @@ export const load = async ({ params, depends, parent }) => {
}
const [globalVariables, variables] = await Promise.all([
sdk.forProject(params.region, params.project).projectApi.listVariables(),
sdk
.forProject(params.region, params.project)
.functions.listVariables({ functionId: params.function })
sdk.forProject(params.region, params.project).projectApi.listVariables({
queries: [Query.limit(VARIABLES_LIMIT)]
}),
sdk.forProject(params.region, params.project).functions.listVariables({
functionId: params.function,
queries: [Query.limit(limit), Query.offset(variablesOffset)]
})
]);
// Conflicting variables first
@@ -43,6 +52,8 @@ export const load = async ({ params, depends, parent }) => {
return {
variables,
globalVariables,
limit,
variablesOffset,
runtimesList,
specificationsList,
function: fn
@@ -11,6 +11,7 @@
AppwriteMigrationResource,
FirebaseMigrationResource,
NHostMigrationResource,
OnDuplicate,
SupabaseMigrationResource
} from '@appwrite.io/console';
import { started } from '../stores';
@@ -23,6 +24,7 @@
Fieldset,
Icon,
Layout,
Selector,
Typography
} from '@appwrite.io/pink-svelte';
import { Link } from '$lib/elements';
@@ -38,6 +40,8 @@
import { capitalize } from '$lib/helpers/string';
import { page } from '$app/state';
let importOnDuplicate: OnDuplicate = OnDuplicate.Fail;
const onExit = () => {
resetImportStores();
};
@@ -46,6 +50,15 @@
try {
const resources = migrationFormToResources($formData, $provider.provider);
// Gate onDuplicate to Fail when databases isn't selected. The radios
// are only shown when databases.root is checked, but the local value
// persists across toggles — without this gate, deselecting databases
// after picking Overwrite/Skip would silently apply that mode to
// other resource types (users, teams, functions, etc.) on submit.
const importOptions = {
onDuplicate: $formData.databases.root ? importOnDuplicate : OnDuplicate.Fail
};
switch ($provider.provider) {
case 'appwrite': {
await sdk
@@ -54,7 +67,8 @@
resources: resources as AppwriteMigrationResource[],
endpoint: $provider.endpoint,
projectId: $provider.projectID,
apiKey: $provider.apiKey
apiKey: $provider.apiKey,
...importOptions
});
await invalidate(Dependencies.MIGRATIONS);
@@ -70,7 +84,8 @@
databaseHost: $provider.host,
username: $provider.username || 'postgres',
password: $provider.password,
port: $provider.port || 5432
port: $provider.port || 5432,
...importOptions
});
await invalidate(Dependencies.MIGRATIONS);
break;
@@ -80,7 +95,8 @@
.forProject(page.params.region, page.params.project)
.migrations.createFirebaseMigration({
resources: resources as FirebaseMigrationResource[],
serviceAccount: $provider.serviceAccount
serviceAccount: $provider.serviceAccount,
...importOptions
});
await invalidate(Dependencies.MIGRATIONS);
break;
@@ -95,7 +111,8 @@
adminSecret: $provider.adminSecret,
database: $provider.database || $provider.subdomain,
username: $provider.username || 'postgres',
password: $provider.password
password: $provider.password,
...importOptions
});
await invalidate(Dependencies.MIGRATIONS);
@@ -196,6 +213,46 @@
projectSdk={sdk.forProject(page.params.region, page.params.project)} />
</Layout.Stack>
</Fieldset>
{#if $formData.databases.root}
<Fieldset legend="Import options">
<Layout.Stack gap="m">
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Migration aborts on the first existing resource (database,
table, column, index, or row).
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing resources">
<svelte:fragment slot="description">
Existing resources are left untouched. Only resources missing on
the destination are created.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing resources">
<svelte:fragment slot="description">
Existing resources are updated to match the source. Schema drift
and row data are both reconciled.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Fieldset>
{/if}
{/if}
</Layout.Stack>
</Layout.Stack>
@@ -51,6 +51,12 @@
await Promise.all([invalidate(Dependencies.VARIABLES), invalidate(Dependencies.SITE)]);
};
const sdkListVariables = async (queries: string[]) =>
sdk.forProject(page.params.region, page.params.project).sites.listVariables({
siteId: page.params.site,
queries
});
onMount(async () => {
if (
page.url.searchParams.has('newInstallation') &&
@@ -75,9 +81,14 @@
{sdkCreateVariable}
{sdkUpdateVariable}
{sdkDeleteVariable}
{sdkListVariables}
isGlobal={false}
globalVariableList={data.globalVariables}
variableList={data.variables}
backendPagination
reloadPageOnPagination={false}
variablesOffset={data.variablesOffset}
variablesLimit={data.limit}
project={data.project}
product="site"
analyticsSource="site_settings" />
@@ -1,18 +1,26 @@
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
import { isCloud } from '$lib/system';
import { Query } from '@appwrite.io/console';
const VARIABLES_LIMIT = 100;
export const load = async ({ params, depends, parent }) => {
depends(Dependencies.VARIABLES);
depends(Dependencies.SITE);
const limit = PAGE_LIMIT;
const variablesOffset = 0;
const { site } = await parent();
const [globalVariables, variables, frameworks, installations, specificationsList] =
await Promise.all([
sdk.forProject(params.region, params.project).projectApi.listVariables(),
sdk
.forProject(params.region, params.project)
.sites.listVariables({ siteId: params.site }),
sdk.forProject(params.region, params.project).projectApi.listVariables({
queries: [Query.limit(VARIABLES_LIMIT)]
}),
sdk.forProject(params.region, params.project).sites.listVariables({
siteId: params.site,
queries: [Query.limit(limit), Query.offset(variablesOffset)]
}),
sdk.forProject(params.region, params.project).sites.listFrameworks(),
sdk.forProject(params.region, params.project).vcs.listInstallations(),
isCloud
@@ -51,6 +59,8 @@ export const load = async ({ params, depends, parent }) => {
frameworks,
variables,
globalVariables,
limit,
variablesOffset,
installations,
specificationsList
};
@@ -60,8 +60,13 @@
secret?: boolean
) => Promise<unknown>;
export let sdkDeleteVariable: (variableId: string) => Promise<unknown>;
export let sdkListVariables: (queries: string[]) => Promise<Models.VariableList> = (queries) =>
sdk.forProject(page.params.region, page.params.project).projectApi.listVariables({
queries
});
export let product: 'function' | 'site' = 'function';
export let backendPagination = false;
export let reloadPageOnPagination = true;
export let variablesOffset = 0;
export let variablesLimit = 10;
export let disabled = false;
@@ -76,20 +81,21 @@
let showDeleteModal = false;
let deleteError: string;
let fullVariableList: Models.VariableList | undefined = allVariableList;
let pagedVariableList: Models.VariableList = variableList;
let previousVariableList = variableList;
let offset = 0;
const limit = 10;
async function loadAllVariables() {
const projectSdk = sdk.forProject(page.params.region, page.params.project);
const variables = [...variableList.variables];
let nextOffset = variables.length;
const variables = backendPagination ? [] : [...variableList.variables];
let nextOffset = backendPagination ? 0 : variables.length;
let total = variableList.total;
while (nextOffset < total) {
const response = await projectSdk.projectApi.listVariables({
queries: [Query.limit(variablesLimit), Query.offset(nextOffset)]
});
const response = await sdkListVariables([
Query.limit(variablesLimit),
Query.offset(nextOffset)
]);
total = response.total;
@@ -106,7 +112,11 @@
}
async function ensureAllVariablesLoaded() {
if (fullVariableList && fullVariableList.total === variableList.total) return;
const total = backendPagination ? variableList.total : variableList.variables.length;
if (fullVariableList && fullVariableList.variables.length >= total) {
return;
}
fullVariableList = await loadAllVariables();
}
@@ -321,31 +331,35 @@
}
}
$: conflictVariables = globalVariableList
? globalVariableList.variables.filter((globalVariable) => {
return variableList.variables.find((variable) => {
return variable.key === globalVariable.key;
});
})
: [];
$: if (allVariableList && fullVariableList !== allVariableList) {
fullVariableList = allVariableList;
}
$: if (variableList !== previousVariableList) {
pagedVariableList = variableList;
if (backendPagination && !reloadPageOnPagination) {
variablesOffset = 0;
}
fullVariableList = undefined;
previousVariableList = variableList;
}
$: if (fullVariableList && fullVariableList.total !== variableList.total) {
$: if (fullVariableList && fullVariableList.total < variableList.total) {
fullVariableList = undefined;
}
$: editorVariableList = fullVariableList ?? allVariableList ?? variableList;
$: displayedVariables = backendPagination
? variableList.variables
? pagedVariableList.variables
: variableList.variables.slice(offset, offset + limit);
$: variableCount = backendPagination ? pagedVariableList.total : variableList.total;
$: conflictVariables = globalVariableList
? globalVariableList.variables.filter((globalVariable) => {
return displayedVariables.find((variable) => {
return variable.key === globalVariable.key;
});
})
: [];
$: hasConflictOnPage = globalVariableList
? displayedVariables.filter((variable) => {
@@ -368,6 +382,14 @@
];
async function handleVariablesPageChange() {
if (!reloadPageOnPagination) {
pagedVariableList = await sdkListVariables([
Query.limit(variablesLimit),
Query.offset(variablesOffset)
]);
return;
}
const nextUrl = new URL(page.url);
nextUrl.searchParams.set('variablesOffset', String(variablesOffset));
@@ -422,7 +444,7 @@
<Icon slot="start" icon={IconUpload} /> Import .env
</Button>
</Layout.Stack>
{#if variableList.total}
{#if variableCount}
<Button
secondary
{disabled}
@@ -436,7 +458,7 @@
{/if}
</Layout.Stack>
</Layout.Stack>
{@const sum = variableList.total}
{@const sum = variableCount}
{#if sum}
<Layout.Stack gap="l">
{#if conflictVariables.length > 0}
+3 -91
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Wizard } from '$lib/layout';
import { Icon, Input, Layout, Tag, Typography, Card, Upload } from '@appwrite.io/pink-svelte';
import { Icon, Input, Layout, Typography, Card, Upload } from '@appwrite.io/pink-svelte';
import { supportData, isSupportOnline } from './wizard/support/store';
import { onMount, onDestroy } from 'svelte';
import { sdk } from '$lib/stores/sdk';
@@ -24,46 +24,6 @@
let projectOptions = $state<Array<{ value: string; label: string }>>([]);
let files = $state<FileList | null>(null);
// Category options with display names
const categories = [
{ value: 'general', label: 'General' },
{ value: 'billing', label: 'Billing' },
{ value: 'technical', label: 'Technical' }
];
// Topic options based on category
const topicsByCategory = {
general: [
'Security',
'Compliance',
'Performance',
'Account',
'Project',
'Regions',
'Other'
],
billing: ['Invoice', 'Plans', 'Payment methods', 'Downgrade', 'Refund', 'Usage', 'Other'],
technical: [
'Auth',
'Databases',
'Storage',
'Functions',
'Realtime',
'Messaging',
'Migrations',
'Webhooks',
'SDKs',
'Console',
'Backups',
'Blocked project',
'Domains',
'Outage',
'Platforms',
'Sites',
'Other'
]
};
onMount(async () => {
// Filter projects by organization ID using server-side queries
const projectList = await sdk.forConsole.projects.list({
@@ -82,39 +42,20 @@
$supportData = {
message: null,
subject: null,
category: 'technical',
topic: undefined,
file: null
};
});
// Update topic options when category changes
const topicOptions = $derived(
($supportData.category ? topicsByCategory[$supportData.category] || [] : []).map(
(topic) => ({
value: topic.toLowerCase().trim().replace(/\s+/g, '-'),
label: topic
})
)
);
async function handleSubmit() {
// Create category-topic tag
const categoryTopicTag = $supportData.topic
? `${$supportData.category}-${$supportData.topic}`.toLowerCase()
: $supportData.category.toLowerCase();
const formData = new FormData();
formData.append('email', $user.email);
formData.append('subject', $supportData.subject);
formData.append('subject', $supportData.subject ?? '');
formData.append('firstName', ($user?.name || 'Unknown').slice(0, 40));
formData.append('message', $supportData.message);
formData.append('message', $supportData.message ?? '');
formData.append('tags[]', 'cloud');
formData.append('tags[]', categoryTopicTag);
formData.append(
'metaFields',
JSON.stringify({
category: $supportData.category,
orgId: $organization?.$id ?? '',
projectId: $supportData?.project ?? '',
billingPlan: $organization?.billingPlanId ?? ''
@@ -151,8 +92,6 @@
$supportData = {
message: null,
subject: null,
category: 'technical',
topic: undefined,
file: null,
project: null
};
@@ -190,33 +129,6 @@
>Please describe your request in detail. If applicable, include steps for
reproduction of any in-app issues.</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="s">
<Typography.Text color="--fgcolor-neutral-secondary"
>Choose a category</Typography.Text>
<Layout.Stack gap="s" direction="row">
{#each categories as category}
<Tag
on:click={() => {
if ($supportData.category !== category.value) {
$supportData.topic = undefined;
}
$supportData.category = category.value;
}}
selected={$supportData.category === category.value}
>{category.label}</Tag>
{/each}
</Layout.Stack>
</Layout.Stack>
{#if topicOptions.length > 0}
{#key $supportData.category}
<Input.ComboBox
id="topic"
label="Choose a topic"
placeholder="Select topic"
bind:value={$supportData.topic}
options={topicOptions} />
{/key}
{/if}
<Input.ComboBox
id="project"
label="Choose a project"
+5 -8
View File
@@ -1,18 +1,15 @@
import { writable } from 'svelte/store';
export type SupportData = {
message: string;
subject: string;
category: string;
topic?: string;
message: string | null;
subject: string | null;
file?: File | null;
project?: string;
project?: string | null;
};
export const supportData = writable<SupportData>({
message: '',
subject: '',
category: 'technical',
message: null,
subject: null,
file: null
});