diff --git a/.env.example b/.env.example index fc29a596b..5a16f982f 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ PUBLIC_CONSOLE_MODE=self-hosted -PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths +PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths PUBLIC_APPWRITE_MULTI_REGION=false PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1 PUBLIC_STRIPE_KEY= diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cfd944fa4..294dae87c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -79,7 +79,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | "PUBLIC_CONSOLE_MODE=cloud" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" "PUBLIC_APPWRITE_MULTI_REGION=true" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" @@ -118,7 +118,7 @@ jobs: build-args: | "PUBLIC_CONSOLE_MODE=self-hosted" "PUBLIC_APPWRITE_MULTI_REGION=false" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" publish-cloud-no-regions: @@ -156,6 +156,6 @@ jobs: build-args: | "PUBLIC_CONSOLE_MODE=cloud" "PUBLIC_APPWRITE_MULTI_REGION=false" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" diff --git a/package.json b/package.json index 71e75ba6a..5e1d8fb76 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f", + "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606", + "@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", "@stripe/stripe-js": "^3.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9da094eea..6b127b486 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606 - version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606(svelte@5.25.3) + specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53 + version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53(svelte@5.25.3) '@popperjs/core': specifier: ^2.11.8 version: 2.11.8 @@ -261,8 +261,8 @@ packages: resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f} version: 1.9.0 - '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed': - resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed} + '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8': + resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -282,8 +282,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606': - resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606} + '@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53': + resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -1266,6 +1266,14 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tanstack/svelte-virtual@3.13.12': + resolution: {integrity: sha512-SsOinCktvpMFbdSW61X6jeUNaJvD1tmt30AFVSLe1FQxGxjdPkZo0gAsoVPqM9FFZqidvvXQ4gpadDaTz/yGxA==} + peerDependencies: + svelte: ^3.48.0 || ^4.0.0 || ^5.0.0 + + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -3638,7 +3646,7 @@ snapshots: '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f': {} - '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3655,12 +3663,13 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53(svelte@5.25.3)': dependencies: - '@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed(svelte@5.25.3) + '@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8(svelte@5.25.3) '@floating-ui/dom': 1.6.13 '@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3) '@melt-ui/svelte': 0.86.6(svelte@5.25.3) + '@tanstack/svelte-virtual': 3.13.12(svelte@5.25.3) ansicolor: 2.0.3 d3: 7.9.0 fuse.js: 7.1.0 @@ -4711,6 +4720,13 @@ snapshots: dependencies: tslib: 2.8.1 + '@tanstack/svelte-virtual@3.13.12(svelte@5.25.3)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + svelte: 5.25.3 + + '@tanstack/virtual-core@3.13.12': {} + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 diff --git a/src/lib/commandCenter/panels/ai.svelte b/src/lib/commandCenter/panels/ai.svelte index b764cfd76..5181a99a2 100644 --- a/src/lib/commandCenter/panels/ai.svelte +++ b/src/lib/commandCenter/panels/ai.svelte @@ -148,7 +148,12 @@ })} clearOnCallback={false} on:keydown={(e) => { - if (e.detail.key !== 'Escape') { + const showingExamples = !$isLoading && !answer; + if (e.detail.key === 'Enter' && $input.trim()) { + e.detail.cancel(); + return; + } + if (e.detail.key !== 'Escape' && !showingExamples) { e.detail.cancel(); } }} @@ -302,15 +307,27 @@ } } - :global(.answer ul), - :global(.answer ol) { - gap: 1rem; + :global(.answer ul) { + padding-inline-start: 1rem; + list-style-type: disc; display: grid; + gap: 1rem; + } + + :global(.answer ol) { + padding-inline-start: 1.1rem; + list-style-type: decimal; + display: grid; + gap: 1rem; } :global(.answer a) { text-decoration: underline; } + + :global(.answer a:hover) { + opacity: 0.8; + } } .experimental { diff --git a/src/lib/components/billing/alerts/paymentMandate.svelte b/src/lib/components/billing/alerts/paymentMandate.svelte index b55f5d20b..08d9207d7 100644 --- a/src/lib/components/billing/alerts/paymentMandate.svelte +++ b/src/lib/components/billing/alerts/paymentMandate.svelte @@ -12,7 +12,7 @@ $organization.$id, $paymentMissingMandate.$id ); - await confirmSetup(method.clientSecret, method.$id); + await confirmSetup(method.clientSecret, method.providerMethodId); } diff --git a/src/lib/components/billing/alerts/projectsLimit.svelte b/src/lib/components/billing/alerts/projectsLimit.svelte new file mode 100644 index 000000000..cd3a0a3ee --- /dev/null +++ b/src/lib/components/billing/alerts/projectsLimit.svelte @@ -0,0 +1,51 @@ + + + + +{#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)} + + + Choose which projects to keep before {toLocaleDate(billingProjectsLimitDate)} or upgrade + to Pro. Projects over the limit will be blocked after this date. + + + + + + +{/if} diff --git a/src/lib/components/billing/alerts/selectProjectCloud.svelte b/src/lib/components/billing/alerts/selectProjectCloud.svelte new file mode 100644 index 000000000..6a44161b3 --- /dev/null +++ b/src/lib/components/billing/alerts/selectProjectCloud.svelte @@ -0,0 +1,117 @@ + + + + + Choose which two projects to keep. Projects over the limit will be blocked after this date. + + {#if error} + {error} + {/if} + + + Project Name + Created + + {#each projects as project} + + {project.name} + {toLocaleDateTime(project.$createdAt)} + + {/each} + + {#if selectedProjects.length > $currentPlan?.projects} +
+ You can only select {$currentPlan?.projects} projects. Please deselect others to continue. +
+ {/if} + {#if selectedProjects.length === $currentPlan?.projects} + + + {@html formatProjectsToArchive()} + will be archived. + + + {/if} + + (showSelectProject = false)} + >Cancel + Save + +
diff --git a/src/lib/components/bottomModalAlert.svelte b/src/lib/components/bottomModalAlert.svelte index 406b40222..1c0f24a9f 100644 --- a/src/lib/components/bottomModalAlert.svelte +++ b/src/lib/components/bottomModalAlert.svelte @@ -3,7 +3,6 @@ import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications'; import { app } from '$lib/stores/app'; import { - type BottomModalAlertAction, type BottomModalAlertItem, bottomModalAlertsConfig, dismissBottomModalAlert, @@ -20,18 +19,18 @@ import { goto } from '$app/navigation'; import { Typography } from '@appwrite.io/pink-svelte'; - let currentIndex = 0; - let openModalOnMobile = false; + let currentIndex = $state(0); + let openModalOnMobile = $state(false); - function getPageScope(pathname: string) { - const isProjectPage = pathname.includes('project-[region]-[project]'); - const isOrganizationPage = pathname.includes('organization-[organization]'); + function getPageScope(route: string) { + const isProjectPage = route.includes('project-[region]-[project]'); + const isOrganizationPage = route.includes('organization-[organization]'); return { isProjectPage, isOrganizationPage }; } - function filterModalAlerts(alerts: BottomModalAlertItem[], pathname: string) { - const { isProjectPage, isOrganizationPage } = getPageScope(pathname); + function filterModalAlerts(alerts: BottomModalAlertItem[], route: string) { + const { isProjectPage, isOrganizationPage } = getPageScope(route); return alerts .sort((a, b) => b.importance - a.importance) @@ -51,19 +50,32 @@ }); } - $: filteredModalAlerts = filterModalAlerts($bottomModalAlertsConfig.alerts, page.route.id); + const bottomModalAlerts = $derived($bottomModalAlertsConfig.alerts); - $: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem; + const filteredModalAlerts = $derived(filterModalAlerts(bottomModalAlerts, page.route.id)); - $: hasOnlyPrimaryCta = typeof currentModalAlert?.learnMore === 'undefined'; + const currentModalAlert = $derived(filteredModalAlerts[currentIndex] as BottomModalAlertItem); - function handleClose() { - filteredModalAlerts.forEach((alert) => { + const hasOnlyPrimaryCta = $derived(typeof currentModalAlert?.learnMore === 'undefined'); + + const isOnOnboarding = $derived(page.route.id.includes('(console)/onboarding')); + + function handleClose(id: string | null = null) { + const alerts = !id + ? filteredModalAlerts + : filteredModalAlerts.filter((alert) => alert.id === id); + + alerts.forEach((alert) => { const modalAlert = alert; dismissBottomModalAlert(modalAlert.id); hideNotification(modalAlert.id, { coolOffPeriod: 24 * 365 }); if (modalAlert.closed) modalAlert.closed(); }); + + // reset `currentIndex` if we removed the last item + if (currentIndex >= filteredModalAlerts.length - 1) { + currentIndex = Math.max(0, filteredModalAlerts.length - 2); + } } function showNext() { @@ -114,8 +126,13 @@ } // the button component cannot have both href and on:click! - function triggerWindowLink(alertAction: BottomModalAlertAction, event?: string) { + function triggerWindowLink(alert: BottomModalAlertItem, event?: string) { + const alertAction = alert.cta; const shouldShowUpgrade = showUpgrade(); + + // for correct event tracking after removal + const currentModalId = currentModalAlert.id; + const url = shouldShowUpgrade ? $upgradeURL : alertAction.link({ @@ -130,14 +147,12 @@ } if (alertAction?.hideOnClick === true) { - // be careful of this one. - // once clicked, its gone! - handleClose(); + handleClose(alert.id); // gone! } trackEvent(Click.PromoClick, { - promo: currentModalAlert.id, - type: shouldShowUpgrade ? 'upgrade' : (event ?? `cta_click_${currentModalAlert.id}`) + promo: currentModalId, + type: shouldShowUpgrade ? 'upgrade' : (event ?? `cta_click_${currentModalId}`) }); } @@ -156,12 +171,10 @@ } } - onMount(() => { - addBottomModalAlerts(); - }); + onMount(addBottomModalAlerts); -{#if filteredModalAlerts.length > 0 && currentModalAlert && !page.url.pathname.includes('console/onboarding')} +{#if !isOnOnboarding && filteredModalAlerts.length > 0 && currentModalAlert} {@const shouldShowUpgrade = showUpgrade()}
@@ -170,7 +183,7 @@ @@ -243,7 +261,7 @@ fullWidthMobile secondary={!hasOnlyPrimaryCta} class={`${hasOnlyPrimaryCta ? 'only-primary-cta' : ''}`} - on:click={() => triggerWindowLink(currentModalAlert.cta)}> + on:click={() => triggerWindowLink(currentModalAlert)}> {currentModalAlert.cta.text} @@ -276,7 +294,7 @@
diff --git a/src/lib/components/columnSelector.svelte b/src/lib/components/columnSelector.svelte index 8d0e1769a..54d8ace9e 100644 --- a/src/lib/components/columnSelector.svelte +++ b/src/lib/components/columnSelector.svelte @@ -32,7 +32,7 @@ }; const saveColumnPreferences = () => { - const shownColumns = $columns.filter((n) => n.hide !== true).map((n) => n.id); + const shownColumns = $columns.filter((n) => n.hide === true).map((n) => n.id); if (isCustomTable) { preferences.setCustomTableColumns(page.params.table, shownColumns); @@ -45,22 +45,22 @@ if (isCustomTable) { const shownColumns = preferences.getCustomTableColumns(page.params.table); - columns.update((columns) => { - return columns.map((column) => { - column.hide = !shownColumns.includes(column.id); + columns.update((n) => + n.map((column) => { + column.hide = shownColumns?.includes(column.id) ?? false; return column; - }); - }); + }) + ); } else { const prefs = preferences.get(page.route); - if (prefs?.columns && prefs.columns.length > 0) { - columns.update((cols) => { - return cols.map((column) => { - column.hide = !prefs.columns.includes(column.id); + if (prefs?.columns) { + columns.update((n) => + n.map((column) => { + column.hide = prefs.columns?.includes(column.id) ?? false; return column; - }); - }); + }) + ); } } @@ -79,7 +79,7 @@ columns.update((cols) => cols.map((col) => { if (col.id === column.id) { - col.hide = !column.hide; + column.hide = !column.hide; } return col; }) @@ -90,6 +90,7 @@ + {#if $columns?.length} {@render children(toggle, selectedColumnsNumber)} diff --git a/src/lib/components/dualTimeView.svelte b/src/lib/components/dualTimeView.svelte index 810e8e50f..644aabe67 100644 --- a/src/lib/components/dualTimeView.svelte +++ b/src/lib/components/dualTimeView.svelte @@ -10,7 +10,7 @@ import { Badge, InteractiveText, Layout, Popover, Typography } from '@appwrite.io/pink-svelte'; import { menuOpen } from '$lib/components/menu/store'; - export let time: string = ''; + export let time: string | null = null; export let placement: ComponentProps['placement'] = 'bottom'; function timeDifference(dateString: string): string { @@ -75,64 +75,68 @@ $: timeToString = time ? timeDifference(time) : 'Invalid time'; - - +{#if time} + + -
(isMouseOverTooltip = true)} - on:mouseleave={() => hidePopover(hide, false)}> - - - - {#if $$slots.title} - - {:else} - {timeToString} - {/if} - +
(isMouseOverTooltip = true)} + on:mouseleave={() => hidePopover(hide, false)}> + + + + {#if $$slots.title} + + {:else} + {timeToString} + {/if} + - - - - + + + + - - + + - - - + + + + - -
- +
+
+{:else} + {time === null || time === undefined ? 'null' : time} +{/if} diff --git a/src/lib/components/git/productionBranchFieldset.svelte b/src/lib/components/git/productionBranchFieldset.svelte index 7518e7e41..2820ee962 100644 --- a/src/lib/components/git/productionBranchFieldset.svelte +++ b/src/lib/components/git/productionBranchFieldset.svelte @@ -90,5 +90,5 @@ {#if show} - + {/if} diff --git a/src/lib/components/git/repositories.svelte b/src/lib/components/git/repositories.svelte index 39f260a99..f67a5a0c3 100644 --- a/src/lib/components/git/repositories.svelte +++ b/src/lib/components/git/repositories.svelte @@ -23,7 +23,8 @@ import { page } from '$app/state'; import Card from '../card.svelte'; import SkeletonRepoList from './skeletonRepoList.svelte'; - import { onMount, untrack } from 'svelte'; + import { onMount, untrack, onDestroy } from 'svelte'; + import { debounce } from '$lib/helpers/debounce'; let { action = $bindable('select'), @@ -52,6 +53,28 @@ loadInstallations(); }); + const debouncedLoadRepositories = debounce( + async (installationId: string, searchTerm: string) => { + isLoadingRepositories = true; + try { + await loadRepositories(installationId, searchTerm); + } finally { + isLoadingRepositories = false; + } + }, + 300 + ); + + $effect(() => { + if (selectedInstallation && search !== undefined) { + debouncedLoadRepositories(selectedInstallation, search); + } + }); + + onDestroy(() => { + debouncedLoadRepositories.cancel(); + }); + async function loadInstallations() { if (installationList) { if (installationList.installations.length) { @@ -71,7 +94,7 @@ .vcs.listInstallations(); if (installations.length) { if (!selectedInstallation) { - untrack(() => (selectedInstallation = installationList.installations[0].$id)); + untrack(() => (selectedInstallation = installations[0].$id)); } installation.set(installations.find((entry) => entry.$id === selectedInstallation)); } @@ -80,22 +103,6 @@ } async function loadRepositories(installationId: string, search: string) { - if ( - !$repositories || - $repositories.installationId !== installationId || - $repositories.search !== search - ) { - await fetchRepos(installationId, search); - } - - if ($repositories.repositories.length && action === 'select') { - selectedRepository = $repositories.repositories[0].id; - $repository = $repositories.repositories[0]; - } - return $repositories.repositories; - } - - async function fetchRepos(installationId: string, search: string) { if (product === 'functions') { $repositories.repositories = ( (await sdk @@ -117,9 +124,9 @@ )) as unknown as Models.ProviderRepositoryFrameworkList ).frameworkProviderRepositories; } - $repositories.search = search; $repositories.installationId = installationId; + return $repositories.repositories; } selectedRepository; @@ -168,10 +175,7 @@ installationsMap.find((entry) => entry.$id === selectedInstallation) ); - isLoadingRepositories = true; - loadRepositories(selectedInstallation, search).then(() => { - isLoadingRepositories = false; - }); + debouncedLoadRepositories.cancel(); }} bind:value={selectedInstallation} /> {#if isLoadingRepositories} + {:else if $repositories?.repositories?.length} + + {#snippet children( + paginatedItems: Models.ProviderRepositoryRuntime[] & + Models.ProviderRepositoryFramework[] + )} + + {#each paginatedItems as repo} + + + + {#if action === 'select'} + repository.set(repo)} + value={repo.id} /> + {/if} + {#if product === 'sites'} + {#if repo?.framework && repo.framework !== 'other'} + + + + {:else} + + {/if} + {:else} + {@const iconName = repo?.runtime + ? repo.runtime.split('-')[0] + : undefined} + + {#if iconName} + + {/if} + + {/if} + + + {repo.name} + + {#if repo.private} + + {/if} + {#if !$isSmallViewport} + + {/if} + + {#if action === 'button'} + connect(repo)}> + Connect + + {/if} + + + + {/each} + + {/snippet} + + {:else if search} + + + {#if search} + + {/if} + + {:else} - {#await loadRepositories(selectedInstallation, search)} - - {:then response} - {#if response?.length} - - {#snippet children( - paginatedItems: Models.ProviderRepositoryRuntime[] & - Models.ProviderRepositoryFramework[] - )} - - {#each paginatedItems as repo} - - - - {#if action === 'select'} - repository.set(repo)} - value={repo.id} /> - {/if} - {#if product === 'sites'} - {#if repo?.framework && repo.framework !== 'other'} - - - - {:else} - - {/if} - {:else} - {@const iconName = repo?.runtime - ? repo.runtime.split('-')[0] - : undefined} - - - - {/if} - - - - {repo.name} - - {#if repo.private} - - {/if} - {#if !$isSmallViewport} - - {/if} - - {#if action === 'button'} - connect(repo)}> - Connect - - {/if} - - - - - {/each} - - {/snippet} - - {:else if search} - - - {#if search} - - {/if} - - - {:else} - - - - No repositories available - - - - {/if} - {/await} + + + + No repositories available + + + {/if} {/if} diff --git a/src/lib/components/git/selectRootModal.svelte b/src/lib/components/git/selectRootModal.svelte index 4972d58f7..7caa5b58d 100644 --- a/src/lib/components/git/selectRootModal.svelte +++ b/src/lib/components/git/selectRootModal.svelte @@ -22,6 +22,7 @@ export let show = false; export let rootDir: string; export let product: 'sites' | 'functions' = 'functions'; + export let branch: string; let isLoading = true; let directories: Directory[] = [ @@ -42,7 +43,7 @@ try { const content = await sdk .forProject(page.params.region, page.params.project) - .vcs.getRepositoryContents($installation.$id, $repository.id, currentPath); + .vcs.getRepositoryContents($installation.$id, $repository.id, currentPath, branch); directories[0].fileCount = content.contents?.length ?? 0; directories[0].children = content.contents .filter((e) => e.isDirectory) @@ -82,7 +83,7 @@ try { const content = await sdk .forProject(page.params.region, page.params.project) - .vcs.getRepositoryContents($installation.$id, $repository.id, path); + .vcs.getRepositoryContents($installation.$id, $repository.id, path, branch); const fileCount = content.contents?.length ?? 0; const contentDirectories = content.contents.filter((e) => e.isDirectory); diff --git a/src/lib/components/gridItem1.svelte b/src/lib/components/gridItem1.svelte index 5fe8012a8..89b2d3bb2 100644 --- a/src/lib/components/gridItem1.svelte +++ b/src/lib/components/gridItem1.svelte @@ -14,7 +14,7 @@
- + diff --git a/src/lib/components/permissions/actions.svelte b/src/lib/components/permissions/actions.svelte index 4bdbb7637..132ee5b40 100644 --- a/src/lib/components/permissions/actions.svelte +++ b/src/lib/components/permissions/actions.svelte @@ -12,7 +12,6 @@ export let showTeam: boolean; export let showLabel: boolean; export let showCustom: boolean; - export let hideOnClick: boolean = false; export let groups: Writable>; const dispatch = createEventDispatcher(); @@ -25,7 +24,7 @@ { - if (hideOnClick) hide(e); + hide(e); dispatch('create', ['any']); }}> Any @@ -33,7 +32,7 @@ { - if (hideOnClick) hide(e); + hide(e); dispatch('create', ['guests']); }}> All guests @@ -41,7 +40,7 @@ { - if (hideOnClick) hide(e); + hide(e); dispatch('create', ['users']); }}> All users @@ -49,22 +48,22 @@ { showUser = true; - if (hideOnClick) hide(e); + hide(e); }}>Select users { showTeam = true; - if (hideOnClick) hide(e); + hide(e); }}>Select teams { showLabel = true; - if (hideOnClick) hide(e); + hide(e); }}>Label { showCustom = true; - if (hideOnClick) hide(e); + hide(e); }}>Custom permission diff --git a/src/lib/components/permissions/permissions.svelte b/src/lib/components/permissions/permissions.svelte index be7cbf74f..135f1506d 100644 --- a/src/lib/components/permissions/permissions.svelte +++ b/src/lib/components/permissions/permissions.svelte @@ -22,7 +22,6 @@ import { Card } from '$lib/components'; export let withCreate = false; - export let hideOnClick = false; export let permissions: string[] = []; let showUser = false; @@ -197,7 +196,6 @@ bind:showTeam bind:showUser {groups} - {hideOnClick} on:create={create} let:toggle> + + + {/if} - {#if showTitle} - Create your project - {/if} - - - - {#if !showCustomId} -
- { - showCustomId = true; - }}> Project ID -
- {/if} - -
- {#if isCloud && regions.length > 0} - - - Region cannot be changed after creation - + + + {#if !showCustomId} +
+ { + showCustomId = true; + }}> Project ID +
{/if} +
+ {#if isCloud && regions.length > 0} + + + Region cannot be changed after creation + + {/if}
-
- + {@render submit?.()} +
diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 2d835eea8..b1d3b5809 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -313,6 +313,7 @@ export type Plan = { webhooks: number; users: number; teams: number; + projects: number; databases: number; databasesAllowEncrypt: boolean; buckets: number; @@ -592,6 +593,25 @@ export class Billing { ); } + async updateSelectedProjects( + organizationId: string, + projects: string[] + ): Promise { + const path = `/organizations/${organizationId}/projects`; + const params = { + projects + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async setOrganizationPaymentMethod( organizationId: string, paymentMethodId: string diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 91823ea78..2159ab768 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -39,6 +39,7 @@ import { sdk } from './sdk'; import { user } from './user'; import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte'; import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte'; +import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte'; import EnterpriseTrial from '$routes/(console)/organization-[organization]/enterpriseTrial.svelte'; export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1'; @@ -68,6 +69,7 @@ export const roles = [ export const teamStatusReadonly = 'readonly'; export const billingLimitOutstandingInvoice = 'outstanding_invoice'; +export const billingProjectsLimitDate = '2024-09-01'; export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList); export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList); @@ -287,6 +289,7 @@ export function checkForEnterpriseTrial(org: Organization) { } export function calculateEnterpriseTrial(org: Organization) { + if (!org || !org.billingNextInvoiceDate) return 0; const endDate = new Date(org.billingNextInvoiceDate); const startDate = new Date(org.billingCurrentInvoiceDate); const today = new Date(); @@ -314,6 +317,24 @@ export function calculateTrialDay(org: Organization) { return days; } +export async function checkForProjectsLimit(org: Organization, projects: number) { + if (!isCloud) return; + if (!org || !projects) 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; + + if (plan.projects > 0 && projects > plan.projects) { + headerAlert.add({ + id: 'projectsLimitReached', + component: ProjectsLimit, + show: true, + importance: 12 + }); + } +} + export async function checkForUsageLimit(org: Organization) { if (org?.status === teamStatusReadonly && org?.remarks === billingLimitOutstandingInvoice) { headerAlert.add({ diff --git a/src/lib/stores/organization.ts b/src/lib/stores/organization.ts index 71b29944f..79f218d8f 100644 --- a/src/lib/stores/organization.ts +++ b/src/lib/stores/organization.ts @@ -34,6 +34,7 @@ export type Organization = Models.Team> & { billingInvoiceId: string; status: string; remarks: string; + projects: string[]; }; export type OrganizationList = { @@ -61,5 +62,4 @@ export const organizationList = derived( export const organization = derived(page, ($page) => $page.data?.organization as Organization); export const currentPlan = derived(page, ($page) => $page.data?.currentPlan as Plan); export const members = derived(page, ($page) => $page.data.members as Models.MembershipList); - export const regions = writable({ total: 0, regions: [] }); diff --git a/src/lib/stores/preferences.ts b/src/lib/stores/preferences.ts index fbde7885c..da285e849 100644 --- a/src/lib/stores/preferences.ts +++ b/src/lib/stores/preferences.ts @@ -180,7 +180,7 @@ function createPreferences() { names: TeamPreferences['names'] ) => { let teamPrefs: Models.Preferences; - update((n) => { + await updateAndSync((n) => { if (!n?.displayNames) { n ??= {}; n.displayNames ??= {}; diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index 452847d49..1ee4d941e 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -18,6 +18,7 @@ checkForMarkedForDeletion, checkForMissingPaymentMethod, checkForNewDevUpgradePro, + checkForProjectsLimit, checkForUsageLimit, checkPaymentAuthorizationRequired, paymentExpired, @@ -296,6 +297,7 @@ if (currentOrganizationId === org.$id) return; if (isCloud) { currentOrganizationId = org.$id; + checkForProjectsLimit(org, data.projects?.projects?.length || 0); checkForEnterpriseTrial(org); await checkForUsageLimit(org); checkForMarkedForDeletion(org); diff --git a/src/routes/(console)/+layout.ts b/src/routes/(console)/+layout.ts index 6c46118c8..9680cdf5a 100644 --- a/src/routes/(console)/+layout.ts +++ b/src/routes/(console)/+layout.ts @@ -4,6 +4,7 @@ import type { LayoutLoad } from './$types'; import { Dependencies } from '$lib/constants'; import type { Tier } from '$lib/stores/billing'; import type { Plan, PlanList } from '$lib/sdk/billing'; +import { Query } from '@appwrite.io/console'; export const load: LayoutLoad = async ({ depends, parent }) => { const { organizations } = await parent(); @@ -28,6 +29,20 @@ export const load: LayoutLoad = async ({ depends, parent }) => { preferences.organization ?? (organizations.teams.length > 0 ? organizations.teams[0].$id : undefined); + // Load projects for the current organization if one is selected + let projects = null; + if (currentOrgId) { + try { + projects = await sdk.forConsole.projects.list([ + Query.equal('teamId', currentOrgId), + Query.limit(1000) // Get all projects for organization + ]); + } catch (e) { + // Handle error silently - projects might not be accessible + projects = {}; + } + } + return { plansInfo, roles: [], @@ -36,7 +51,8 @@ export const load: LayoutLoad = async ({ depends, parent }) => { currentOrgId, organizations, consoleVariables, - version: versionData?.version ?? null + version: versionData?.version ?? null, + projects }; }; diff --git a/src/routes/(console)/account/payments/addressModal.svelte b/src/routes/(console)/account/payments/addressModal.svelte index 4c64e3cf1..9bdcc2784 100644 --- a/src/routes/(console)/account/payments/addressModal.svelte +++ b/src/routes/(console)/account/payments/addressModal.svelte @@ -56,7 +56,7 @@ if (organization) { org = await sdk.forConsole.billing.setBillingAddress(organization, response.$id); trackEvent(Submit.OrganizationBillingAddressUpdate); - await invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.ORGANIZATIONS); } await invalidate(Dependencies.ADDRESS); diff --git a/src/routes/(console)/bottomAlerts.ts b/src/routes/(console)/bottomAlerts.ts index 5016b9c07..ab0336576 100644 --- a/src/routes/(console)/bottomAlerts.ts +++ b/src/routes/(console)/bottomAlerts.ts @@ -1,11 +1,76 @@ import { isCloud } from '$lib/system'; import { isSameDay } from '$lib/helpers/date'; import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts'; +import AutoIncrementDark from '$lib/images/promos/auto-increment-dark.png'; +import AutoIncrementLight from '$lib/images/promos/auto-increment-light.png'; +import EncryptedDatabasesDark from '$lib/images/promos/encrypted-attribute-dark.png'; +import EncryptedDatabasesLight from '$lib/images/promos/encrypted-attribute-light.png'; +import OptInRelationDark from '$lib/images/promos/opt-relation-dark.png'; +import OptInRelationLight from '$lib/images/promos/opt-relation-light.png'; const listOfPromotions: BottomModalAlertItem[] = []; if (isCloud) { - /* add promos here */ + const encryptedAttributePromo: BottomModalAlertItem = { + id: 'modal:encrypted_attribute_announcement', + src: { + dark: EncryptedDatabasesDark, + light: EncryptedDatabasesLight + }, + title: 'Introducing Encrypted string attribute support', + message: 'Encrypt string attributes at rest, directly from the Appwrite Console.', + plan: 'free', + importance: 8, + scope: 'project', + cta: { + text: 'Read announcement', + link: () => 'https://appwrite.io/blog/post/announcing-encrypted-string-attributes', + external: true, + hideOnClick: true + }, + show: true + }; + const autoIncrementPromo: BottomModalAlertItem = { + id: 'modal:auto_increment_announcement', + src: { + dark: AutoIncrementDark, + light: AutoIncrementLight + }, + title: 'Announcing Auto-increment support', + message: + 'Get a built-in numeric identifier that increases predictably with every new document added.', + plan: 'free', + importance: 8, + scope: 'project', + cta: { + text: 'Read announcement', + link: () => 'https://appwrite.io/blog/post/announcing-auto-increment-support', + external: true, + hideOnClick: true + }, + show: true + }; + const optInRelationPromo: BottomModalAlertItem = { + id: 'modal:opt_in_relation_announcement', + src: { + dark: OptInRelationDark, + light: OptInRelationLight + }, + title: 'Introducing Opt-in relationship loading', + message: + 'Gain full control over which related documents to fetch and drastically reduce payload sizes.', + plan: 'free', + importance: 8, + scope: 'project', + cta: { + text: 'Read announcement', + link: () => 'https://appwrite.io/blog/post/announcing-opt-in-relationship-loading', + external: true, + hideOnClick: true + }, + show: false + }; + listOfPromotions.push(optInRelationPromo, autoIncrementPromo, encryptedAttributePromo); } export function addBottomModalAlerts() { diff --git a/src/routes/(console)/create-organization/+page.ts b/src/routes/(console)/create-organization/+page.ts index 9aba3aefd..690baa2bb 100644 --- a/src/routes/(console)/create-organization/+page.ts +++ b/src/routes/(console)/create-organization/+page.ts @@ -6,7 +6,7 @@ import type { Organization } from '$lib/stores/organization'; export const load: PageLoad = async ({ url, parent, depends }) => { const { organizations } = await parent(); - depends(Dependencies.CREATE_ORGANIZATION); + depends(Dependencies.ORGANIZATIONS); const [coupon, paymentMethods] = await Promise.all([ getCoupon(url), diff --git a/src/routes/(console)/onboarding/create-organization/+page.svelte b/src/routes/(console)/onboarding/create-organization/+page.svelte index b765cbd6a..b29ff6f44 100644 --- a/src/routes/(console)/onboarding/create-organization/+page.svelte +++ b/src/routes/(console)/onboarding/create-organization/+page.svelte @@ -50,7 +50,7 @@ // fixes an edge case where // the org is not available for some reason! - await invalidate(Dependencies.CREATE_ORGANIZATION); + await invalidate(Dependencies.ORGANIZATIONS); } isLoading = false; } diff --git a/src/routes/(console)/onboarding/create-project/+page.svelte b/src/routes/(console)/onboarding/create-project/+page.svelte index 5121889d6..80fed1137 100644 --- a/src/routes/(console)/onboarding/create-project/+page.svelte +++ b/src/routes/(console)/onboarding/create-project/+page.svelte @@ -15,7 +15,7 @@ import { user } from '$lib/stores/user'; let isLoading = false; - let id: string; + let id: string = ID.unique(); let startAnimation = false; let projectName = 'Appwrite project'; let region = Region.Fra; @@ -99,14 +99,18 @@ bind:projectName bind:id bind:region - on:submit={createProject}> - + showTitle={true}> + {#snippet submit()} - + Create - + {/snippet} {/if} diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 448d7b12e..560abb2eb 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -21,11 +21,11 @@ import { loading } from '$routes/store'; import { ID, Region, type Models } from '@appwrite.io/console'; import { openImportWizard } from '../project-[region]-[project]/settings/migrations/(import)'; - import { readOnly } from '$lib/stores/billing'; + import { billingProjectsLimitDate, readOnly } from '$lib/stores/billing'; import { onMount, type ComponentType } from 'svelte'; import { canWriteProjects } from '$lib/stores/roles'; import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect'; - import { Badge, Icon, Typography } from '@appwrite.io/pink-svelte'; + import { Badge, Icon, Typography, Alert, Tag, Tooltip } from '@appwrite.io/pink-svelte'; import { IconAndroid, IconApple, @@ -37,13 +37,16 @@ } from '@appwrite.io/pink-icons-svelte'; import { getPlatformInfo } from '$lib/helpers/platform'; import CreateProjectCloud from './createProjectCloud.svelte'; - import { regions as regionsStore } from '$lib/stores/organization'; + import { currentPlan, regions as regionsStore } from '$lib/stores/organization'; + import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte'; + import { toLocaleDate } from '$lib/helpers/date'; export let data; let showCreate = false; let showCreateProjectCloud = false; let addOrganization = false; + let showSelectProject = false; function filterPlatforms(platforms: { name: string; icon: string }[]) { return platforms.filter( @@ -117,8 +120,25 @@ function findRegion(project: Models.Project) { return $regionsStore.regions.find((region) => region.$id === project.region); } + + function isSetToArchive(project: Models.Project): boolean { + if (!isCloud) return false; + if (data.organization.projects?.length === 0) return false; + if (!project || !project.$id) return false; + return !data.organization.projects?.includes(project.$id); + } + + function formatName(name: string, limit: number = 19) { + return name ? (name.length > limit ? `${name.slice(0, limit)}...` : name) : '-'; + } + + $: projectsToArchive = data.projects.projects.filter( + (project) => !data.organization.projects?.includes(project.$id) + ); + +
Projects @@ -144,6 +164,27 @@
+ {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects} + + + {#each projectsToArchive as project, index}{@const text = `${project.name}`} + {@html text}{index == projectsToArchive.length - 2 + ? ', and ' + : index < projectsToArchive.length - 1 + ? ', ' + : ''} + {/each} + will be archived + + + + + + {/if} + {#if data.projects.total} getPlatformInfo(platform.type)) )} + {@const formatted = isSetToArchive(project) + ? formatName(project.name) + : project.name} {project?.platforms?.length ? project?.platforms?.length : 'No'} apps - {project.name} + + {formatted} + + {project.name} + + + + + + {#if isSetToArchive(project)} + { + event.preventDefault(); + showSelectProject = true; + }}>Set to archive + {/if} {#each platforms.slice(0, 2) as platform} @@ -210,4 +273,8 @@ - + diff --git a/src/routes/(console)/organization-[organization]/billing/+page.svelte b/src/routes/(console)/organization-[organization]/billing/+page.svelte index 59a4f64d9..ae2674465 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.svelte +++ b/src/routes/(console)/organization-[organization]/billing/+page.svelte @@ -1,6 +1,5 @@ @@ -93,7 +93,7 @@ {#if $failedInvoice} {#if $failedInvoice?.lastError} - The scheduled payment for {$organization.name} failed due to following error: {$failedInvoice.lastError} + The scheduled payment for {organization.name} failed due to following error: {$failedInvoice.lastError} {:else} - + {/if} - 0} /> + 0} /> diff --git a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte index b98c45bb2..14407a2e7 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte @@ -5,7 +5,7 @@ import { CardGrid, CreditCardBrandImage, CreditCardInfo } from '$lib/components'; import { BillingPlan, Dependencies } from '$lib/constants'; import { addNotification } from '$lib/stores/notifications'; - import { organization } from '$lib/stores/organization'; + import { type Organization } from '$lib/stores/organization'; import { Button } from '$lib/elements/forms'; import { hasStripePublicKey, isCloud } from '$lib/system'; import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing'; @@ -34,6 +34,7 @@ IconTrash } from '@appwrite.io/pink-icons-svelte'; + export let organization: Organization; export let methods: PaymentList; let showPayment = false; @@ -47,12 +48,12 @@ async function addPaymentMethod(paymentMethodId: string) { try { await sdk.forConsole.billing.setOrganizationPaymentMethod( - $organization.$id, + organization.$id, paymentMethodId ); addNotification({ type: 'success', - message: `A new payment method has been added to ${$organization.name}` + message: `A new payment method has been added to ${organization.name}` }); trackEvent(Submit.OrganizationPaymentUpdate); invalidate(Dependencies.ORGANIZATION); @@ -68,12 +69,12 @@ async function addBackupPaymentMethod(paymentMethodId: string) { try { await sdk.forConsole.billing.setOrganizationPaymentMethodBackup( - $organization.$id, + organization.$id, paymentMethodId ); addNotification({ type: 'success', - message: `A new payment method has been added to ${$organization.name}` + message: `A new payment method has been added to ${organization.name}` }); invalidate(Dependencies.ORGANIZATION); } catch (error) { @@ -85,15 +86,15 @@ } } - $: if ($organization?.backupPaymentMethodId) { + $: if (organization?.backupPaymentMethodId) { sdk.forConsole.billing - .getOrganizationPaymentMethod($organization.$id, $organization.backupPaymentMethodId) + .getOrganizationPaymentMethod(organization.$id, organization.backupPaymentMethodId) .then((res) => (backupPaymentMethod = res)); } - $: if ($organization?.paymentMethodId) { + $: if (organization?.paymentMethodId) { sdk.forConsole.billing - .getOrganizationPaymentMethod($organization.$id, $organization.paymentMethodId) + .getOrganizationPaymentMethod(organization.$id, organization.paymentMethodId) .then((res) => (defaultPaymentMethod = res)); } @@ -112,7 +113,7 @@ Payment methods View or update your organization payment methods here. - {#if $organization?.paymentMethodId} + {#if organization?.paymentMethodId} - {#if $organization?.backupPaymentMethodId} + {#if organization?.backupPaymentMethodId} @@ -209,9 +210,9 @@ {/if} - {#if !$organization?.backupPaymentMethodId} + {#if !organization?.backupPaymentMethodId} {@const filteredPaymentMethods = methods.paymentMethods.filter( - (o) => !!o.last4 && o.$id !== $organization?.paymentMethodId + (o) => !!o.last4 && o.$id !== organization?.paymentMethodId )}
@@ -264,7 +265,7 @@ {/if} {:else} {@const filteredPaymentMethods = methods.paymentMethods.filter( - (o) => !!o.last4 && o.$id !== $organization?.backupPaymentMethodId + (o) => !!o.last4 && o.$id !== organization?.backupPaymentMethodId )} @@ -319,15 +320,15 @@ bind:show={showEdit} /> {/if} {#if isCloud && hasStripePublicKey} - + {/if} {#if showDelete && isCloud && hasStripePublicKey} {@const hasOtherMethod = isSelectedBackup - ? !!$organization?.paymentMethodId - : !!$organization?.backupPaymentMethodId} + ? !!organization?.paymentMethodId + : !!organization?.backupPaymentMethodId} + disabled={organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod} /> {/if} diff --git a/src/routes/(console)/organization-[organization]/billing/removeAddress.svelte b/src/routes/(console)/organization-[organization]/billing/removeAddress.svelte index 72d8eb100..0a913b851 100644 --- a/src/routes/(console)/organization-[organization]/billing/removeAddress.svelte +++ b/src/routes/(console)/organization-[organization]/billing/removeAddress.svelte @@ -20,7 +20,7 @@ message: `The billing address has been removed from ${$organization.name}` }); trackEvent(Submit.OrganizationBillingAddressDelete); - invalidate(Dependencies.ORGANIZATION); + invalidate(Dependencies.ORGANIZATIONS); show = false; } catch (e) { error = e.message; diff --git a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte index faf49fc8c..9adbb514e 100644 --- a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte +++ b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte @@ -3,7 +3,7 @@ import { FakeModal } from '$lib/components'; import { Button } from '$lib/elements/forms'; import { sdk } from '$lib/stores/sdk'; - import { organization } from '$lib/stores/organization'; + import type { Organization } from '$lib/stores/organization'; import { Dependencies } from '$lib/constants'; import { submitStripeCard } from '$lib/stores/stripe'; import { onMount } from 'svelte'; @@ -12,6 +12,7 @@ import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { PaymentBoxes } from '$lib/components/billing'; + export let organization: Organization; export let show = false; export let isBackup = false; export let methods: PaymentList; @@ -21,12 +22,12 @@ let selectedPaymentMethodId: string; onMount(async () => { - if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) { + if (!organization.paymentMethodId && !organization.backupPaymentMethodId) { selectedPaymentMethodId = methods?.total ? methods.paymentMethods[0].$id : null; } else { selectedPaymentMethodId = isBackup - ? $organization.backupPaymentMethodId - : $organization.paymentMethodId; + ? organization.backupPaymentMethodId + : organization.paymentMethodId; // If the selected payment method does not belong to the current user, select the first one. if ( methods?.total && @@ -40,7 +41,7 @@ async function handleSubmit() { try { if (selectedPaymentMethodId === '$new') { - const method = await submitStripeCard(name, $organization.$id); + const method = await submitStripeCard(name, organization.$id); selectedPaymentMethodId = method.$id; } isBackup @@ -68,7 +69,7 @@ async function addPaymentMethod(paymentMethodId: string) { try { await sdk.forConsole.billing.setOrganizationPaymentMethod( - $organization.$id, + organization.$id, paymentMethodId ); } catch (e) { @@ -79,7 +80,7 @@ async function addBackupPaymentMethod(paymentMethodId: string) { try { await sdk.forConsole.billing.setOrganizationPaymentMethodBackup( - $organization.$id, + organization.$id, paymentMethodId ); } catch (e) { @@ -97,18 +98,18 @@ methods={filteredMethods} bind:name bind:group={selectedPaymentMethodId} - defaultMethod={$organization?.paymentMethodId} - backupMethod={$organization?.backupPaymentMethodId} + defaultMethod={organization?.paymentMethodId} + backupMethod={organization?.backupPaymentMethodId} disabledCondition={isBackup - ? $organization.paymentMethodId - : $organization.backupPaymentMethodId} /> + ? organization.paymentMethodId + : organization.backupPaymentMethodId} /> diff --git a/src/routes/(console)/organization-[organization]/createProject.svelte b/src/routes/(console)/organization-[organization]/createProject.svelte index 5e70a1acd..856447cab 100644 --- a/src/routes/(console)/organization-[organization]/createProject.svelte +++ b/src/routes/(console)/organization-[organization]/createProject.svelte @@ -21,10 +21,12 @@ let showCustomId = false; let disabled: boolean = false; let name: string = 'New project'; + let showSubmissionLoader = false; async function create() { try { disabled = true; + showSubmissionLoader = true; const project = await sdk.forConsole.projects.create(id ?? ID.unique(), name, teamId); show = false; dispatch('created', project); @@ -40,7 +42,9 @@ } catch (e) { error = e.message; trackError(e, Submit.ProjectCreate); + } finally { disabled = false; + showSubmissionLoader = false; } } @@ -62,6 +66,10 @@ - + diff --git a/src/routes/(console)/organization-[organization]/createProjectCloud.svelte b/src/routes/(console)/organization-[organization]/createProjectCloud.svelte index 0557c1a4b..a0236076f 100644 --- a/src/routes/(console)/organization-[organization]/createProjectCloud.svelte +++ b/src/routes/(console)/organization-[organization]/createProjectCloud.svelte @@ -2,16 +2,18 @@ import { sdk } from '$lib/stores/sdk'; import { onDestroy } from 'svelte'; import { goto, invalidate } from '$app/navigation'; - import { page } from '$app/state'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { ID, type Models, Region as ConsoleRegion, Region } from '@appwrite.io/console'; import { base } from '$app/paths'; import CreateProject from '$lib/layout/createProject.svelte'; import { Modal } from '$lib/components'; + import { currentPlan } from '$lib/stores/organization'; import { Button } from '$lib/elements/forms'; import { Dependencies } from '$lib/constants'; + export let teamId: string; export let showCreateProjectCloud: boolean; + export let projects: number; export let regions: Array = []; let id: string = null; @@ -20,7 +22,6 @@ let region: ConsoleRegion = Region.Fra; let showSubmissionLoader = false; - const teamId = page.params.organization; async function create() { let project: Models.Project; @@ -60,11 +61,18 @@ title={'Create project'} onSubmit={create} bind:error> - + diff --git a/src/routes/(console)/organization-[organization]/domains/+page.svelte b/src/routes/(console)/organization-[organization]/domains/+page.svelte index b2726e561..4251c80e9 100644 --- a/src/routes/(console)/organization-[organization]/domains/+page.svelte +++ b/src/routes/(console)/organization-[organization]/domains/+page.svelte @@ -196,7 +196,7 @@ + +
+ +
+
+ {organization?.billingPlan === BillingPlan.FREE + ? 'Upgrade to add more members' + : `You've reached the members limit for the ${ + tierToPlan(organization?.billingPlan)?.name + } plan`} +
+
{/if}
diff --git a/src/routes/(console)/organization-[organization]/members/+page.svelte b/src/routes/(console)/organization-[organization]/members/+page.svelte index c906ece51..abc2360b3 100644 --- a/src/routes/(console)/organization-[organization]/members/+page.svelte +++ b/src/routes/(console)/organization-[organization]/members/+page.svelte @@ -31,8 +31,11 @@ Icon, Typography, Popover, - ActionMenu + ActionMenu, + Tooltip } from '@appwrite.io/pink-svelte'; + import { BillingPlan } from '$lib/constants'; + import { tierToPlan } from '$lib/stores/billing'; export let data; @@ -41,6 +44,13 @@ let showEdit = false; let showDropdown = []; + // Calculate if button should be disabled and tooltip should show + $: memberCount = data.organizationMembers?.total ?? 0; + $: isFreeWithMembers = $organization?.billingPlan === BillingPlan.FREE && memberCount >= 1; + $: isButtonDisabled = isCloud + ? isFreeWithMembers || !$currentPlan?.addons?.seats?.supported + : false; + const resend = async (member: Models.Membership) => { try { await sdk.forConsole.teams.createMembership( @@ -70,14 +80,25 @@ Members - newMemberModal.set(true)} - disabled={isCloud ? !$currentPlan?.addons?.seats?.supported : false}> - - Invite - + +
+ newMemberModal.set(true)} + disabled={isButtonDisabled}> + + Invite + +
+
+ {$organization?.billingPlan === BillingPlan.FREE + ? 'Upgrade to add more members' + : `You've reached the members limit for the ${ + tierToPlan($organization?.billingPlan)?.name + } plan`} +
+
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
-
-

URI

- -
+ diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index 1e318f3ce..85fcdb91d 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -65,11 +65,9 @@ To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. -
-

URI

- -
+ - - - {/if} + + Upgrade your plan to ensure your data stays safe and backed up. + + + + {:else} ([]); $: selected = preferences.getCustomTableColumns(page.params.table); + $: tableColumns.set( $table.columns.map((column) => ({ id: column.key, title: column.key, type: column.type as ColumnType, - hide: !selected?.includes(column.key), + hide: !!selected?.includes(column.key), array: column?.array, format: 'format' in column && column?.format === 'enum' ? column.format : null, elements: 'elements' in column ? column.elements : null @@ -51,13 +51,13 @@ ...['$id', '$createdAt', '$updatedAt'].map((id) => ({ id, title: id, - hide: false, + show: true, type: (id === '$id' ? 'string' : 'datetime') as ColumnType })) ]); $: hasColumns = !!$table.columns.length; - $: hasValidColumns = $table?.columns?.some((attr) => attr.status === 'available'); + $: hasValidColumns = $table?.columns?.some((col) => col.status === 'available'); async function onSelect(file: Models.File) { $isCsvImportInProgress = true; @@ -100,15 +100,13 @@ analyticsSource="database_rows" /> - {#if flags.showCsvImport(data)} - - {/if} + {#if !$isSmallViewport} - - Indicate whether this column is encrypted. Encrypted columns cannot be - queried. - - + + + + Protect attribute against data leaks for best privacy compliance. Encrypted + attributes cannot be queried. + + - - - Available on Pro plan. Upgrade - to enable encrypted columns. - - - - - - -{/if} + + + Available on Pro plan. Upgrade + to enable encrypted columns. + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte index be9dfee94..d0a8d3bf6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte @@ -13,7 +13,6 @@ import { base } from '$app/paths'; import type { Columns } from './store'; import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; - import { preferences } from '$lib/stores/preferences'; export let showCreate = false; export let selectedOption: Option['name'] = null; @@ -29,17 +28,10 @@ }; let error: string; - async function updateTableColumns() { - const selectedColumns = preferences.getCustomTableColumns(tableId); - selectedColumns.push(key ?? data?.key); - await preferences.setCustomTableColumns(tableId, selectedColumns); - await invalidate(Dependencies.TABLE); - } - async function submit() { try { await $option.create(databaseId, tableId, key, data); - await updateTableColumns(); + await invalidate(Dependencies.TABLE); if (!page.url.pathname.includes('columns')) { await goto( diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/relationshipsModal.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/relationshipsModal.svelte index 90504e08a..b834a00c7 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/relationshipsModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/relationshipsModal.svelte @@ -22,18 +22,22 @@ data = null; selectedRelationship = null; } + + $: tableColumns = [ + // take full width if no display names set + { id: '$id', width: args.length ? 200 : undefined }, + ...args.map((id) => ({ id })) + ]; {#if data?.length} {#snippet children(paginatedItems: typeof data)} - ({ id }))]}> + Row ID - {#if args?.length} + {#if args.length} {#each args as arg} {arg} {/each} @@ -47,7 +51,7 @@ {doc.$id} - {#if args?.length} + {#if args.length} {#each args as arg} {doc[arg]} {/each} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/boolean.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/boolean.svelte index 06db73dbf..cf26aa742 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/boolean.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/boolean.svelte @@ -12,6 +12,7 @@ + ].filter(Boolean)} /> diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/email.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/email.svelte index 3e62153e8..eeb7a1334 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/email.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/email.svelte @@ -6,14 +6,12 @@ export let label: string; export let value: string | null; export let column: Models.ColumnEmail; - - $: nullable = column.required ? false : !value; + nullable={!column.required} /> diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/integer.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/integer.svelte index d189e3f02..d25b85d9f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/integer.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/integer.svelte @@ -6,16 +6,14 @@ export let label: string; export let value: number; export let column: Models.ColumnInteger; - - $: nullable = column.required ? false : !value; + required={column.required} + nullable={!column.required} + step={column.type === 'double' ? 'any' : 1} /> diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/relationship.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/relationship.svelte index 3ce113e28..624c1bd4f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/relationship.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/relationship.svelte @@ -116,9 +116,29 @@ $: totalCount = relatedList?.length ?? 0; $: options = - rowList?.rows?.map((n) => { - const data = displayNames.filter((name) => name !== '$id').map((name) => n?.[name]); - return { value: n.$id, label: n.$id, data }; + rowList?.rows?.map((row) => { + const names = displayNames.filter((name) => name !== '$id'); + const values = names + .map((name) => row?.[name]) + // always supposed to be a string but just being a bit safe here + .filter((value) => value != null && typeof value === 'string' && value !== ''); + + const displayValues = !editing + ? values + : // on non edit routes like create, there's enough space! + values.map((value) => (value.length > 5 ? value.slice(0, 5) + '...' : value)); + + const label = !values.length + ? row.$id + : // values are in `$id (a | b)` format + // previously used to have a `$id a | b`. + `${row.$id} (${displayValues.join(' | ')})`; + + return { + label, + value: row.$id, + data: names.map((name) => row?.[name]) + }; }) ?? []; $: hasItems = totalCount > 0; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/string.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/string.svelte index 516dbba00..fd6678b8f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/string.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/string.svelte @@ -6,26 +6,24 @@ export let label: string; export let value: string; export let column: Models.ColumnString; - - $: nullable = column.required ? false : !value; {#if column.size >= 50} + required={column.required} + nullable={!column.required} /> {:else} + required={column.required} + nullable={!column.required} /> {/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/url.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/url.svelte index 73f7f6003..c160e8a30 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/url.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/row-[row]/columns/url.svelte @@ -6,8 +6,12 @@ export let label: string; export let value: string | null; export let column: Models.ColumnUrl; - - $: nullable = column.required ? false : !value; - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/settings/displayName.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/settings/displayName.svelte index 0f10ca8c2..dc8601514 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/settings/displayName.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/settings/displayName.svelte @@ -17,10 +17,23 @@ const tableId = page.params.table; let names: string[] = [...(preferences.getDisplayNames()?.[tableId] ?? [])]; + function getDisplayNames() { + return [...(preferences.getDisplayNames()?.[tableId] ?? [])].filter( + (name) => name !== '$id' + ); // edge case with `$id`? got saved during tests! + } + + let names: string[] = $state(getDisplayNames()); + async function updateDisplayName() { try { - await preferences.setDisplayNames($organization.$id, tableId, names); - names = [...(preferences.getDisplayNames()?.[tableId] ?? [])]; + // $state makes proxy, + // structuredClone doesn't work + const regularArray = [...names]; + + await preferences.setDisplayNames($organization.$id, tableId, regularArray); + names = getDisplayNames(); + await invalidate(Dependencies.TEAM); addNotification({ message: 'Display names have been updated', @@ -36,23 +49,34 @@ } } - $: options = ($columns as Models.ColumnString[]) - .filter( - (attr) => - attr.type === 'string' && !attr?.array && !names?.some((name) => name === attr.key) - ) - .map((attr) => { - return { + function getValidAttributes() { + return ($attributes as Models.AttributeString[]).filter( + (attr) => attr.type === 'string' && !attr?.array + ); + } + + function getOptions(index: number) { + const current = names?.[index]; + return getValidAttributes() + .filter((attr) => !names?.includes(attr.key) || attr.key === current) + .map((attr) => ({ value: attr.key, label: attr.key - }; - }); + })); + } - $: addAttributeDisabled = names?.length >= 5 || (names?.length && !names[names?.length - 1]); + const addAttributeDisabled = $derived( + names?.length >= 5 || (names?.length && !names[names?.length - 1]) + ); - $: updateBtnDisabled = + const updateBtnDisabled = $derived( !symmetricDifference(names, preferences.getDisplayNames()?.[tableId] ?? [])?.length || - (names?.length && !last(names)); + (names?.length && !last(names)) + ); + + const hasExhaustedOptions = $derived( + getValidAttributes().length === names.filter(Boolean).length + );
@@ -72,19 +96,22 @@ {#if names?.length} - {#each names as name, i} + {#each names as name, index} + {@const options = getOptions(index)} + {@const disabled = + (!!names[index] && names.length > index + 1) || hasExhaustedOptions} i + 1} - {options} /> + {options} + {disabled} + bind:value={names[index]} + placeholder="Select column" /> - + + + {#if !hasExhaustedOptions} +
+ +
+ {/if}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/table.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/table.svelte index d601a2515..580e0cadd 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/table.svelte @@ -10,7 +10,7 @@ import { preferences } from '$lib/stores/preferences'; import { sdk } from '$lib/stores/sdk'; import type { Models } from '@appwrite.io/console'; - import { afterUpdate, onMount } from 'svelte'; + import { onMount } from 'svelte'; import type { PageData } from './$types'; import { isRelationship, isRelationshipToMany, isString } from './row-[row]/columns/store'; import RelationshipsModal from './relationshipsModal.svelte'; @@ -28,36 +28,21 @@ } from '@appwrite.io/pink-svelte'; import { toLocaleDateTime } from '$lib/helpers/date'; import DualTimeView from '$lib/components/dualTimeView.svelte'; - import { flags } from '$lib/flags'; export let data: PageData; const databaseId = page.params.database; const tableId = page.params.table; + + let displayNames = {}; let showRelationships = false; let selectedRelationship: Models.ColumnRelationship = null; let relationshipData: Partial[]; - let displayNames = {}; onMount(async () => { displayNames = preferences.getDisplayNames(); - updateMaxWidth(); }); - afterUpdate(() => updateMaxWidth()); - - function updateMaxWidth() { - const tableCells = Array.from(document.querySelectorAll('.less-width-truncated')); - - const visibleColumnsCount = $tableColumns.filter((col) => !col.hide).length; - const newMaxWidth = Math.max(50 - (visibleColumnsCount - 1) * 5, 25); - - tableCells.forEach((cell) => { - const cellItem = cell as HTMLElement; - cellItem.style.maxWidth = `${newMaxWidth}vw`; - }); - } - function formatArray(array: unknown[]) { if (array.length === 0) return '[ ]'; @@ -104,7 +89,7 @@ id: column.key, title: column.key, type: column.type as ColumnType, - hide: !selected?.includes(column.key), + hide: !!selected?.includes(column.key), array: column?.array, width: { min: 168 }, format: 'format' in column && column?.format === 'enum' ? column.format : null, @@ -164,8 +149,6 @@ ) as Models.ColumnRelationship[]; let checked = false; - - const showEncrypt = flags.showAttributeEncrypt(data); Timestamp {toLocaleDateTime(datetime, true)} - {:else if isEncryptedAttribute && showEncrypt} + {:else if isEncryptedAttribute} + diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateScopes.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateScopes.svelte index 0442d0bc2..991feefbb 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateScopes.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateScopes.svelte @@ -11,7 +11,7 @@ import { func } from '../store'; import { isValueOfStringEnum } from '$lib/helpers/types'; import { Runtime } from '@appwrite.io/console'; - import Scopes from '$routes/(console)/project-[region]-[project]/overview/keys/scopes.svelte'; + import Scopes from '$routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte'; import { symmetricDifference } from '$lib/helpers/array'; import { Link } from '$lib/elements'; diff --git a/src/routes/(console)/project-[region]-[project]/messaging/providers/settingsFormInput.svelte b/src/routes/(console)/project-[region]-[project]/messaging/providers/settingsFormInput.svelte index 411420a6b..afa62b44f 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/providers/settingsFormInput.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/providers/settingsFormInput.svelte @@ -24,8 +24,7 @@ APNSProviderParams } from './store'; import TooltipPopover from './tooltipPopover.svelte'; - import { Icon, Tooltip, Upload, Layout, Typography } from '@appwrite.io/pink-svelte'; - import { IconInfo } from '@appwrite.io/pink-icons-svelte'; + import { Upload, Layout, Typography } from '@appwrite.io/pink-svelte'; import { InvalidFileType, removeFile } from '$lib/helpers/files'; import { addNotification } from '$lib/stores/notifications'; @@ -44,18 +43,6 @@ | FCMProviderParams | APNSProviderParams >; - const popover = input.popover ? PopoverContent : null; - const popoverProps = getPopoverProps(input); - - function getPopoverProps(input: ProviderInput) { - if (!input.popover) { - return {}; - } - return { - lines: input.popover, - image: input.popoverImage - }; - } function handleInvalid(e: CustomEvent) { const reason = e.detail?.reason ?? ''; @@ -72,6 +59,14 @@ }); } } + + $: popover = input.popover ? PopoverContent : null; + $: popoverProps = !input.popover + ? {} + : { + lines: input.popover, + image: input.popoverImage + }; {#if input.type === 'text'} @@ -122,25 +117,15 @@ {:else if input.type === 'file'} - - - - - Drag and drop service account JSON here or click to upload - - - - - - Only .json files allowed - - - Max file size 10MB - + + + Drag and drop your {input.label} here or click to upload + + {#if files[input.name]?.length} diff --git a/src/routes/(console)/project-[region]-[project]/messaging/providers/store.ts b/src/routes/(console)/project-[region]-[project]/messaging/providers/store.ts index 4d7d47402..985d93215 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/providers/store.ts +++ b/src/routes/(console)/project-[region]-[project]/messaging/providers/store.ts @@ -85,7 +85,7 @@ export const providers: ProvidersMap = { ], configure: [ { - label: 'Service account JSON (.json file)', + label: 'service account JSON (.json file)', name: 'serviceAccountJSON', type: 'file', allowedFileExtensions: ['json'], @@ -164,7 +164,7 @@ export const providers: ProvidersMap = { } }, { - label: 'Auth key (.p8 file)', + label: 'auth key (.p8 file)', name: 'authKey', type: 'file', allowedFileExtensions: ['p8'], diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte index 3378bfe09..ccefd0f9c 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte @@ -13,7 +13,7 @@ import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { addNotification } from '$lib/stores/notifications'; import { writable } from 'svelte/store'; - import Scopes from '../keys/scopes.svelte'; + import Scopes from '../api-keys/scopes.svelte'; import { page } from '$app/state'; const projectId = page.params.project; @@ -41,7 +41,7 @@ trackEvent(Submit.KeyCreate); await goto( - `${base}/project-${page.params.region}-${page.params.project}/overview/keys/${$id}` + `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/${$id}` ); addNotification({ message: `API key has been created`, @@ -59,7 +59,7 @@ Platforms API keys + event="api-keys" + href={`${path}/api-keys`} + selected={isTabSelected('api-keys')}>API keys Dev keys diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/+page.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/+page.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/overview/keys/+page.svelte rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/+page.svelte diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/+page.ts b/src/routes/(console)/project-[region]-[project]/overview/api-keys/+page.ts similarity index 100% rename from src/routes/(console)/project-[region]-[project]/overview/keys/+page.ts rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/+page.ts diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/[key]/+page.ts b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/+page.ts similarity index 100% rename from src/routes/(console)/project-[region]-[project]/overview/keys/[key]/+page.ts rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/+page.ts diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/[key]/+page@project-[region]-[project].svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/+page@project-[region]-[project].svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/overview/keys/[key]/+page@project-[region]-[project].svelte rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/+page@project-[region]-[project].svelte diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/[key]/breadcrumbs.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/breadcrumbs.svelte similarity index 90% rename from src/routes/(console)/project-[region]-[project]/overview/keys/[key]/breadcrumbs.svelte rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/breadcrumbs.svelte index 00fa0e8f7..80b95a333 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/keys/[key]/breadcrumbs.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/breadcrumbs.svelte @@ -16,11 +16,11 @@ title: $project?.name }, { - href: `${base}/project-${page.params.region}-${page.params.project}/overview/keys`, + href: `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys`, title: 'API keys' }, { - href: `${base}/project-${page.params.region}-${page.params.project}/overview/keys/${$key?.$id}`, + href: `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/${$key?.$id}`, title: $key?.name } ]; diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/[key]/header.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/header.svelte similarity index 92% rename from src/routes/(console)/project-[region]-[project]/overview/keys/[key]/header.svelte rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/header.svelte index 0fd354e94..0b4b527d6 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/keys/[key]/header.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/header.svelte @@ -9,7 +9,7 @@ - + {$key?.name} diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/[key]/store.ts b/src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/store.ts similarity index 100% rename from src/routes/(console)/project-[region]-[project]/overview/keys/[key]/store.ts rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/store.ts diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/action.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/action.svelte similarity index 92% rename from src/routes/(console)/project-[region]-[project]/overview/keys/action.svelte rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/action.svelte index ba746e54a..20cb3a227 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/keys/action.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/action.svelte @@ -9,7 +9,7 @@ {#if $canWriteKeys} diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/checkbox.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/checkbox.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/overview/keys/checkbox.svelte rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/checkbox.svelte diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/create/+page.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/create/+page.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/overview/keys/create/+page.svelte rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/create/+page.svelte diff --git a/src/routes/(console)/project-[region]-[project]/overview/keys/scopes.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/overview/keys/scopes.svelte rename to src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard.svelte b/src/routes/(console)/project-[region]-[project]/overview/onboard.svelte index 3a7d7d5a5..818a6adc6 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/onboard.svelte @@ -43,9 +43,12 @@ trackEvent(Click.KeyCreateClick, { source: 'onboarding' }); - goto(`${base}/project-${page.params.region}-${page.params.project}/overview/keys/create`, { - replaceState: true - }); + goto( + `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/create`, + { + replaceState: true + } + ); } export let platforms: Models.Platform[] = []; diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/createWeb.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/createWeb.svelte index 82d2c479a..2a75050fa 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/platforms/createWeb.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/createWeb.svelte @@ -44,7 +44,7 @@ AngularFrameworkIcon, JavascriptFrameworkIcon } from './components/index'; - import { hostnameRegex } from '$lib/helpers/string'; + import { extendedHostnameRegex } from '$lib/helpers/string'; export let key; @@ -143,7 +143,7 @@ ${prefix}APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.p $: selectedFrameworkIcon = selectedFramework ? selectedFramework.icon : NoFrameworkIcon; async function createWebPlatform() { - hostnameError = hostname !== '' ? !new RegExp(hostnameRegex).test(hostname) : null; + hostnameError = hostname !== '' ? !new RegExp(extendedHostnameRegex).test(hostname) : null; if (hostnameError) { return; diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte index af7f27d0d..b9affbece 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte @@ -49,8 +49,8 @@ diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte index 3d642cd9e..2c02dc197 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte @@ -25,10 +25,6 @@ $: $updateCommandGroupRanks({ webhooks: 20, domains: 10 }); - - Webhooks - Appwrite - - diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte index b3a8b8a16..2a88dc960 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/manual/+page.svelte @@ -209,9 +209,11 @@ >Only .tar.gz files allowed
- Max file size: {readableMaxSize.value + - readableMaxSize.unit} + {#if maxSize > 0} + Max file size: {readableMaxSize.value + + readableMaxSize.unit} + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/createManualDeploymentModal.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/createManualDeploymentModal.svelte index a428f7cc9..a8843f2fa 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/createManualDeploymentModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/createManualDeploymentModal.svelte @@ -95,9 +95,11 @@ >Only .tar.gz files allowed
- Max file size: {readableMaxSize.value + - readableMaxSize.unit} + {#if maxSize > 0} + Max file size: {readableMaxSize.value + + readableMaxSize.unit} + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateRepository.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateRepository.svelte index 4d9579b2d..7c0c376da 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateRepository.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateRepository.svelte @@ -269,5 +269,9 @@ {/if} {#if showSelectRoot} - + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte index 8ff56517e..9f8275a41 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte @@ -17,6 +17,9 @@ export let site: Models.Site; export let specs: Models.SpecificationList; let specification = site.specification; + let originalSpecification = site.specification; + + $: originalSpecification = site.specification; async function updateLogging() { try { @@ -42,7 +45,9 @@ site?.providerRootDirectory || undefined, specification || undefined ); - await invalidate(Dependencies.FUNCTION); + await invalidate(Dependencies.SITE); + originalSpecification = specification; + addNotification({ type: 'success', message: 'Resource limits have been updated' @@ -92,7 +97,7 @@ - + diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 21fe42dde..46b84cc98 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -16,7 +16,7 @@ export const ssr = false; export const load: LayoutLoad = async ({ depends, url, route }) => { depends(Dependencies.ACCOUNT); - depends(Dependencies.CREATE_ORGANIZATION); + depends(Dependencies.ORGANIZATIONS); const [account, error] = (await sdk.forConsole.account .get()