diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 71bb39ae7..2a19d6fbe 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,6 +41,7 @@ jobs: "PUBLIC_CONSOLE_MODE=cloud" "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_APPWRITE_MULTI_REGION=true" + "PUBLIC_CONSOLE_EMAIL_VERIFICATION=true" "PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}" @@ -82,6 +83,7 @@ jobs: "PUBLIC_CONSOLE_MODE=cloud" "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_APPWRITE_MULTI_REGION=true" + "PUBLIC_CONSOLE_EMAIL_VERIFICATION=true" "PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" @@ -120,7 +122,8 @@ jobs: build-args: | "PUBLIC_CONSOLE_MODE=self-hosted" "PUBLIC_APPWRITE_MULTI_REGION=false" - "PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false" + "PUBLIC_CONSOLE_EMAIL_VERIFICATION=false" + "PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=true" "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" @@ -159,6 +162,7 @@ jobs: build-args: | "PUBLIC_CONSOLE_MODE=cloud" "PUBLIC_APPWRITE_MULTI_REGION=false" + "PUBLIC_CONSOLE_EMAIL_VERIFICATION=true" "PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false" "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" diff --git a/Dockerfile b/Dockerfile index 879a4bc65..5da6ccbf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN corepack prepare pnpm@10.0.0 --activate ADD ./package.json /app/package.json ADD ./pnpm-lock.yaml /app/pnpm-lock.yaml -RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm install --frozen-lockfile ADD ./build.js /app/build.js ADD ./tsconfig.json /app/tsconfig.json @@ -23,6 +23,7 @@ ADD ./static /app/static ARG PUBLIC_CONSOLE_MODE ARG PUBLIC_CONSOLE_FEATURE_FLAGS ARG PUBLIC_APPWRITE_MULTI_REGION +ARG PUBLIC_CONSOLE_EMAIL_VERIFICATION ARG PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS ARG PUBLIC_APPWRITE_ENDPOINT ARG PUBLIC_GROWTH_ENDPOINT @@ -35,6 +36,7 @@ ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE ENV PUBLIC_CONSOLE_FEATURE_FLAGS=$PUBLIC_CONSOLE_FEATURE_FLAGS ENV PUBLIC_APPWRITE_MULTI_REGION=$PUBLIC_APPWRITE_MULTI_REGION +ENV PUBLIC_CONSOLE_EMAIL_VERIFICATION=$PUBLIC_CONSOLE_EMAIL_VERIFICATION ENV PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=$PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN diff --git a/build.js b/build.js index affdf3ddd..df5933adc 100644 --- a/build.js +++ b/build.js @@ -28,6 +28,8 @@ async function main() { logEnv('MULTI REGION', env?.PUBLIC_APPWRITE_MULTI_REGION); logEnv('APPWRITE ENDPOINT', env?.PUBLIC_APPWRITE_ENDPOINT, 'relative'); logEnv('GROWTH ENDPOINT', env?.PUBLIC_GROWTH_ENDPOINT); + logEnv('CONSOLE EMAIL VERIFICATION', env?.PUBLIC_CONSOLE_EMAIL_VERIFICATION); + logEnv('CONSOLE MOCK AI SUGGESTIONS', env?.PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS); log(); logDelimiter(); await build(); diff --git a/e2e/steps/free-project.ts b/e2e/steps/free-project.ts index a8a3dadb9..cf0972e47 100644 --- a/e2e/steps/free-project.ts +++ b/e2e/steps/free-project.ts @@ -17,10 +17,22 @@ export async function createFreeProject(page: Page): Promise { await page.waitForURL(/\/organization-[^/]+/); await page.getByRole('button', { name: 'create project' }).first().click(); const dialog = page.locator('dialog[open]'); + await dialog.getByPlaceholder('Project name').fill('test project'); + + let region = 'fra'; // for fallback + const regionPicker = dialog.locator('button[role="combobox"]'); + if (await regionPicker.isVisible()) { + await regionPicker.click(); + await page.getByRole('option', { name: /New York/i }).click(); + + region = 'nyc'; + } + await dialog.getByRole('button', { name: 'create' }).click(); - await page.waitForURL(/\/project-fra-[^/]+/); - expect(page.url()).toContain('/console/project-fra-'); + + await page.waitForURL(new RegExp(`/project-${region}-[^/]+`)); + expect(page.url()).toContain(`/console/project-${region}-`); return getProjectIdFromUrl(page.url()); }); diff --git a/e2e/steps/pro-project.ts b/e2e/steps/pro-project.ts index 94cbb601f..3f3a7ec1f 100644 --- a/e2e/steps/pro-project.ts +++ b/e2e/steps/pro-project.ts @@ -50,10 +50,21 @@ export async function createProProject(page: Page): Promise { await page.waitForURL(/\/organization-[^/]+/); await page.getByRole('button', { name: 'create project' }).first().click(); const dialog = page.locator('dialog[open]'); + await dialog.getByPlaceholder('Project name').fill('test project'); + + let region = 'fra'; // for fallback + const regionPicker = dialog.locator('button[role="combobox"]'); + if (await regionPicker.isVisible()) { + await regionPicker.click(); + await page.getByRole('option', { name: /New York/i }).click(); + + region = 'nyc'; + } + await dialog.getByRole('button', { name: 'create' }).click(); - await page.waitForURL(/\/project-fra-[^/]+/); - expect(page.url()).toContain('/console/project-fra-'); + await page.waitForURL(new RegExp(`/project-${region}-[^/]+`)); + expect(page.url()).toContain(`/console/project-${region}-`); return getProjectIdFromUrl(page.url()); }); diff --git a/package.json b/package.json index eebc54597..8d1a0d1ba 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@f08cb74", + "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@50b60cc", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4", "@faker-js/faker": "^9.9.0", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", @@ -52,7 +52,7 @@ "@eslint/js": "^9.31.0", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.5", - "@playwright/test": "^1.51.1", + "@playwright/test": "^1.55.1", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.42.1", "@sveltejs/vite-plugin-svelte": "^5.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a4161ed8..463bb83f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,20 +12,20 @@ importers: specifier: ^1.1.24 version: 1.1.24(svelte@5.25.3)(zod@3.24.3) '@appwrite.io/console': - specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74 - version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74 + specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e + version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e '@appwrite.io/pink-icons': specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@50b60cc + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@50b60cc(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 @@ -97,8 +97,8 @@ importers: specifier: ^0.86.5 version: 0.86.5(svelte@5.25.3) '@playwright/test': - specifier: ^1.51.1 - version: 1.51.1 + specifier: ^1.55.1 + version: 1.56.1 '@sveltejs/adapter-static': specifier: ^3.0.8 version: 3.0.8(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))) @@ -260,8 +260,8 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74': - resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e': + resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e} version: 1.10.0 '@appwrite.io/pink-icons-svelte@2.0.0-RC.1': @@ -269,8 +269,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@50b60cc': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@50b60cc} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -284,8 +284,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -335,8 +335,8 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.25.9': @@ -352,8 +352,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -373,8 +373,8 @@ packages: resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} '@csstools/color-helpers@5.0.2': @@ -993,8 +993,8 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@playwright/test@1.51.1': - resolution: {integrity: sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==} + '@playwright/test@1.56.1': + resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} engines: {node: '>=18'} hasBin: true @@ -2607,8 +2607,8 @@ packages: magic-string@0.30.18: resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} magic-string@0.30.7: resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} @@ -2830,13 +2830,13 @@ packages: resolution: {integrity: sha512-hMhneYm3GCPyQon88SZrVJx+LlqhM1kZFQbuAgXPoh/Az2YvO1B6bitT9qlhpiTdJlsT5lsr3gPmzoVjb5CDXA==} engines: {node: '>=10'} - playwright-core@1.51.1: - resolution: {integrity: sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==} + playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} engines: {node: '>=18'} hasBin: true - playwright@1.51.1: - resolution: {integrity: sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==} + playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} engines: {node: '>=18'} hasBin: true @@ -3703,13 +3703,13 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@f08cb74': {} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e': {} '@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)': dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@077179c(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@50b60cc(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3722,7 +3722,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@077179c(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 @@ -3812,7 +3812,7 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.25.9': {} @@ -3825,9 +3825,9 @@ snapshots: dependencies: '@babel/types': 7.27.0 - '@babel/parser@7.28.4': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/runtime@7.27.0': dependencies: @@ -3856,10 +3856,10 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@babel/types@7.28.4': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@csstools/color-helpers@5.0.2': {} @@ -4453,9 +4453,9 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true - '@playwright/test@1.51.1': + '@playwright/test@1.56.1': dependencies: - playwright: 1.51.1 + playwright: 1.56.1 '@polka/url@1.0.0-next.28': {} @@ -5124,7 +5124,7 @@ snapshots: '@vue/compiler-core@3.5.13': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@vue/shared': 3.5.13 entities: 4.5.0 estree-walker: 2.0.2 @@ -5137,13 +5137,13 @@ snapshots: '@vue/compiler-sfc@3.5.13': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@vue/compiler-core': 3.5.13 '@vue/compiler-dom': 3.5.13 '@vue/compiler-ssr': 3.5.13 '@vue/shared': 3.5.13 estree-walker: 2.0.2 - magic-string: 0.30.19 + magic-string: 0.30.21 postcss: 8.5.6 source-map-js: 1.2.1 @@ -6285,7 +6285,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.19: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6486,11 +6486,11 @@ snapshots: plausible-tracker@0.3.9: {} - playwright-core@1.51.1: {} + playwright-core@1.56.1: {} - playwright@1.51.1: + playwright@1.56.1: dependencies: - playwright-core: 1.51.1 + playwright-core: 1.56.1 optionalDependencies: fsevents: 2.3.2 diff --git a/src/app.html b/src/app.html index 7ca4eefe8..1ce18283b 100644 --- a/src/app.html +++ b/src/app.html @@ -7,6 +7,30 @@ content="Appwrite is an open-source platform for building applications at any scale, using your preferred programming languages and tools." /> + + + + + + + + + + + + %sveltekit.head% diff --git a/src/lib/components/account/sendVerificationEmailModal.svelte b/src/lib/components/account/sendVerificationEmailModal.svelte index bbe676142..1385570f8 100644 --- a/src/lib/components/account/sendVerificationEmailModal.svelte +++ b/src/lib/components/account/sendVerificationEmailModal.svelte @@ -1,79 +1,195 @@ - - - - - To continue using Appwrite Cloud, please verify your email address. An email will be - sent to {get(user)?.email} - - - + + + diff --git a/src/lib/components/alerts/emailVerificationBanner.svelte b/src/lib/components/alerts/emailVerificationBanner.svelte deleted file mode 100644 index 96574d801..000000000 --- a/src/lib/components/alerts/emailVerificationBanner.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - -{#if shouldShowEmailBanner} - - - To avoid losing access to your projects, make sure {$user.email} is valid and up to date. Email - verification will be required soon. - - - - - - -{/if} diff --git a/src/lib/components/csvImportBox.svelte b/src/lib/components/csvImportBox.svelte index ec7a2c9a3..ac8561019 100644 --- a/src/lib/components/csvImportBox.svelte +++ b/src/lib/components/csvImportBox.svelte @@ -169,7 +169,7 @@ migrations.migrations.forEach(updateOrAddItem); }); - return sdk.forConsoleIn(page.params.region).client.subscribe('console', (response) => { + return sdk.forConsoleIn(page.params.region).realtime.subscribe('console', (response) => { if (!response.channels.includes(`projects.${getProjectId()}`)) return; if (response.events.includes('migrations.*')) { updateOrAddItem(response.payload as Payload); diff --git a/src/lib/components/customId.svelte b/src/lib/components/customId.svelte index a46307c81..66a56c2d8 100644 --- a/src/lib/components/customId.svelte +++ b/src/lib/components/customId.svelte @@ -27,10 +27,12 @@ id = null; } - if (!id?.length) { + if (id !== null && !id.length) { id = null; } + }); + $effect(() => { if (show) { trackEvent(Click.ShowCustomIdClick); } diff --git a/src/lib/components/git/connectRepoModal.svelte b/src/lib/components/git/connectRepoModal.svelte index 68acdab02..0f5eb76d4 100644 --- a/src/lib/components/git/connectRepoModal.svelte +++ b/src/lib/components/git/connectRepoModal.svelte @@ -13,6 +13,7 @@ import { Click, trackEvent } from '$lib/actions/analytics'; import RepositoryBehaviour from '$lib/components/git/repositoryBehaviour.svelte'; import { page } from '$app/state'; + import { connectGitHub } from '$lib/stores/git'; let { show = $bindable(false), @@ -121,7 +122,7 @@ {#if repositoryBehaviour === 'existing'} - + Missing a repository? check your permissions diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index 47afdf41a..b9d6e57ed 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -85,5 +85,5 @@ export { default as ViewToggle } from './viewToggle.svelte'; export { default as RegionEndpoint } from './regionEndpoint.svelte'; export { default as ExpirationInput } from './expirationInput.svelte'; export { default as EstimatedCard } from './estimatedCard.svelte'; -export { default as EmailVerificationBanner } from './alerts/emailVerificationBanner.svelte'; export { default as SortButton, type SortDirection } from './sortButton.svelte'; +export { default as SendVerificationEmailModal } from './account/sendVerificationEmailModal.svelte'; diff --git a/src/lib/components/modal.svelte b/src/lib/components/modal.svelte index 2eb562336..8181338f8 100644 --- a/src/lib/components/modal.svelte +++ b/src/lib/components/modal.svelte @@ -14,6 +14,7 @@ }; export let title = ''; export let hideFooter = false; + export let backdrop: boolean = true; let alert: HTMLElement; @@ -29,7 +30,7 @@
- + {#if error}
diff --git a/src/lib/components/permissions/row.svelte b/src/lib/components/permissions/row.svelte index 8706f77a7..dc2323849 100644 --- a/src/lib/components/permissions/row.svelte +++ b/src/lib/components/permissions/row.svelte @@ -1,11 +1,14 @@ @@ -48,70 +138,178 @@ {:else if role === 'any'}
Any
{:else} - - {role} -
- {#key showing} - {#await getData(role)} - - - - {:then data} - {@const isUser = role.startsWith('user')} - {@const isTeam = role.startsWith('team')} - {@const isAnonymous = !data.email && !data.phone && !data.name && isUser} - - - {#if isAnonymous} - - - - {:else if data.name} - - {:else} - - - - {/if} - - {data.name ?? data?.email ?? data?.phone ?? '-'} - - + + - - {#if isUser} - {#if data?.email} - Email: {data?.email} - {/if} - {#if data?.phone} - Phone: {data?.phone} - {/if} -
- - View user - - -
- {:else if isTeam} - Members: {data?.total} -
- - View team - - -
+
{/if} + + diff --git a/src/lib/elements/forms/inputDateTime.svelte b/src/lib/elements/forms/inputDateTime.svelte index 11bd78fd8..6a904cd53 100644 --- a/src/lib/elements/forms/inputDateTime.svelte +++ b/src/lib/elements/forms/inputDateTime.svelte @@ -4,7 +4,7 @@ export let id: string; export let label: string = ''; - export let value: string; + export let value: string | null; export let required = false; export let nullable = false; export let disabled = false; @@ -17,6 +17,7 @@ let error: string; let element: HTMLInputElement; + let previousValue: string | null = null; onMount(() => { if (element && autofocus) { @@ -24,14 +25,16 @@ } }); - let prevValue = ''; function handleNullChange(e: CustomEvent) { const isNull = e.detail; + if (isNull) { - prevValue = value; + if (value !== null) { + previousValue = value; + } value = null; } else { - value = prevValue; + value = previousValue; } } @@ -39,9 +42,7 @@ error = null; } - function onChange(event: CustomEvent) { - value = (event.target as HTMLInputElement).value; - } + $: isValueNull = value === null; @@ -51,20 +52,20 @@ {disabled} {readonly} {required} - {value} + bind:value {step} {type} helper={error} {leadingIcon} - on:change={onChange} autocomplete={autocomplete ? 'on' : 'off'}> - {#if nullable} - - {/if} + + {#if nullable} + + {/if} + diff --git a/src/lib/elements/forms/inputId.svelte b/src/lib/elements/forms/inputId.svelte index 909aaf094..9946c924a 100644 --- a/src/lib/elements/forms/inputId.svelte +++ b/src/lib/elements/forms/inputId.svelte @@ -12,11 +12,7 @@ const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => { event.preventDefault(); - - if (event.currentTarget.validity.patternMismatch) { - error = true; - return; - } + error = true; // show on any error }; $: if (value) { diff --git a/src/lib/elements/forms/inputProjectId.svelte b/src/lib/elements/forms/inputProjectId.svelte index c9628485c..f9b479ce5 100644 --- a/src/lib/elements/forms/inputProjectId.svelte +++ b/src/lib/elements/forms/inputProjectId.svelte @@ -12,11 +12,7 @@ const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => { event.preventDefault(); - - if (event.currentTarget.validity.patternMismatch) { - error = true; - return; - } + error = true; // show on any error }; $: if (value) { diff --git a/src/lib/helpers/faker.ts b/src/lib/helpers/faker.ts index 56987f879..421288bb3 100644 --- a/src/lib/helpers/faker.ts +++ b/src/lib/helpers/faker.ts @@ -198,15 +198,21 @@ function generateSingleValue( case 'integer': { const intAttr = column as Models.ColumnInteger; - const min = !isWithinSafeRange(intAttr.min) ? 0 : intAttr.min; - const max = !isWithinSafeRange(intAttr.max) ? 100 : intAttr.max; + const min = isWithinSafeRange(intAttr.min) ? intAttr.min : 0; + const fallbackMax = Math.max(min + 100, 100); + const max = isWithinSafeRange(intAttr.max) + ? intAttr.max + : Math.min(fallbackMax, Number.MAX_SAFE_INTEGER); return faker.number.int({ min, max }); } case 'double': { const floatAttr = column as Models.ColumnFloat; - const min = !isWithinSafeRange(floatAttr.min) ? 0 : floatAttr.min; - const max = !isWithinSafeRange(floatAttr.max) ? 100 : floatAttr.max; + const min = isWithinSafeRange(floatAttr.min) ? floatAttr.min : 0; + const fallbackMax = Math.max(min + 100, 100); + const max = isWithinSafeRange(floatAttr.max) + ? floatAttr.max + : Math.min(fallbackMax, Number.MAX_SAFE_INTEGER); const precision = 4; return faker.number.float({ min, max, fractionDigits: precision }); diff --git a/src/lib/helpers/numbers.ts b/src/lib/helpers/numbers.ts index ce1c0a0bb..a3ad303b5 100644 --- a/src/lib/helpers/numbers.ts +++ b/src/lib/helpers/numbers.ts @@ -18,21 +18,38 @@ export function toDecimals(num: number, decimals: number = 1): number { return parseFloat(num.toFixed(decimals)); } -export function formatNumberWithCommas(number: number): string { +export function formatNumberWithCommas(number: number, min: number = 0): string { if (isNaN(number)) return String(number); const formatter = new Intl.NumberFormat('en'); - return formatter.format(number); + return formatter.format(clampMin(number, min)); } -export function formatCurrency(number: number, locale = 'en-US', currency = 'USD'): string { +export function formatCurrency( + number: number, + locale = 'en-US', + currency = 'USD', + min: number = 0 +): string { if (isNaN(number)) return String(number); const formatter = new Intl.NumberFormat(locale, { style: 'currency', currency }); - return formatter.format(number); + return formatter.format(clampMin(number, min)); } export function isWithinSafeRange(val: number) { return Math.abs(val) < Number.MAX_SAFE_INTEGER; } + +/** + * Clamps a number to a minimum value + * + * @export + * @param {number} value + * @param {number} min + * @returns {number} + */ +export function clampMin(value: number, min: number = 0): number { + return Math.max(min, value || 0); +} diff --git a/src/lib/helpers/string.ts b/src/lib/helpers/string.ts index 91dd9a37c..6f867f86c 100644 --- a/src/lib/helpers/string.ts +++ b/src/lib/helpers/string.ts @@ -1,3 +1,5 @@ +import { clampMin } from './numbers'; + /** * Capitalizes the first letter of a string * @@ -45,8 +47,8 @@ const formatter = Intl.NumberFormat('en', { notation: 'compact' }); -export function formatNum(number: number): string { - return formatter.format(number); +export function formatNum(number: number, min: number = 0): string { + return formatter.format(clampMin(number, min)); } /** diff --git a/src/lib/images/promos/inversion-queries-dark.png b/src/lib/images/promos/inversion-queries-dark.png deleted file mode 100644 index 2b59c4ee2..000000000 Binary files a/src/lib/images/promos/inversion-queries-dark.png and /dev/null differ diff --git a/src/lib/images/promos/inversion-queries-light.png b/src/lib/images/promos/inversion-queries-light.png deleted file mode 100644 index 3063aca3c..000000000 Binary files a/src/lib/images/promos/inversion-queries-light.png and /dev/null differ diff --git a/src/lib/images/promos/spatial-columns-api-dark.png b/src/lib/images/promos/spatial-columns-api-dark.png deleted file mode 100644 index 232763d19..000000000 Binary files a/src/lib/images/promos/spatial-columns-api-dark.png and /dev/null differ diff --git a/src/lib/images/promos/spatial-columns-api-light.png b/src/lib/images/promos/spatial-columns-api-light.png deleted file mode 100644 index f62f20d4d..000000000 Binary files a/src/lib/images/promos/spatial-columns-api-light.png and /dev/null differ diff --git a/src/lib/images/promos/time-helper-queries-dark.png b/src/lib/images/promos/time-helper-queries-dark.png deleted file mode 100644 index 0adf4b231..000000000 Binary files a/src/lib/images/promos/time-helper-queries-dark.png and /dev/null differ diff --git a/src/lib/images/promos/time-helper-queries-light.png b/src/lib/images/promos/time-helper-queries-light.png deleted file mode 100644 index 466e6ac9f..000000000 Binary files a/src/lib/images/promos/time-helper-queries-light.png and /dev/null differ diff --git a/src/lib/images/promos/transactions-api-dark.png b/src/lib/images/promos/transactions-api-dark.png new file mode 100644 index 000000000..4fa749489 Binary files /dev/null and b/src/lib/images/promos/transactions-api-dark.png differ diff --git a/src/lib/images/promos/transactions-api-light.png b/src/lib/images/promos/transactions-api-light.png new file mode 100644 index 000000000..f9796c5e0 Binary files /dev/null and b/src/lib/images/promos/transactions-api-light.png differ diff --git a/src/lib/layout/footer.svelte b/src/lib/layout/footer.svelte index 2e4f9819c..54e8ec053 100644 --- a/src/lib/layout/footer.svelte +++ b/src/lib/layout/footer.svelte @@ -181,6 +181,11 @@ &.hide { display: none; } + + & :global(i) { + // because the `IconCloud` shows above floating action bars. + position: unset; + } } :global(main:has(.sub-navigation)) footer { diff --git a/src/lib/layout/notifications.svelte b/src/lib/layout/notifications.svelte index aa0132398..b6d6ffeff 100644 --- a/src/lib/layout/notifications.svelte +++ b/src/lib/layout/notifications.svelte @@ -11,13 +11,16 @@ {#each $notifications as notification (notification.id)} { return { label: button.name, - onClick: button.method + onClick: button.method, + isHtml: button.isHtml }; })} on:dismiss={() => dismissNotification(notification.id)} /> diff --git a/src/lib/layout/progress.svelte b/src/lib/layout/progress.svelte index a5b445fcc..8d34097f4 100644 --- a/src/lib/layout/progress.svelte +++ b/src/lib/layout/progress.svelte @@ -1,5 +1,4 @@ diff --git a/src/lib/layout/responsiveContainerHeader.svelte b/src/lib/layout/responsiveContainerHeader.svelte index 0f5358a02..29eed0f56 100644 --- a/src/lib/layout/responsiveContainerHeader.svelte +++ b/src/lib/layout/responsiveContainerHeader.svelte @@ -106,11 +106,11 @@ {#if hasSearch} {/if} +
+ {#if hasFilters && $columns?.length} {/if} - - {#if hasDisplaySettings} {/if} diff --git a/src/lib/layout/usageMultiple.svelte b/src/lib/layout/usageMultiple.svelte index 421b0892b..85f5091ca 100644 --- a/src/lib/layout/usageMultiple.svelte +++ b/src/lib/layout/usageMultiple.svelte @@ -4,7 +4,7 @@ import { Card, SecondaryTabs, SecondaryTabsItem } from '$lib/components'; import { page } from '$app/state'; import { type Models } from '@appwrite.io/console'; - import { formatNumberWithCommas } from '$lib/helpers/numbers'; + import { formatNumberWithCommas, clampMin } from '$lib/helpers/numbers'; import { Layout, Typography } from '@appwrite.io/pink-svelte'; export let title: string; @@ -41,7 +41,7 @@ {#if count} - {@const totalCount = total.reduce((a, b) => a + b, 0)} + {@const totalCount = clampMin(total.reduce((a, b) => a + b, 0))} {formatNumberWithCommas(totalCount)} diff --git a/src/lib/layout/wizardStep.svelte b/src/lib/layout/wizardStep.svelte index 4b64ae445..591fb62e8 100644 --- a/src/lib/layout/wizardStep.svelte +++ b/src/lib/layout/wizardStep.svelte @@ -21,14 +21,21 @@ -
- - {#if $$slots.subtitle} -

- -

- {/if} -
+ {#if $$slots.title || $$slots.subtitle} +
+ {#if $$slots.title} + + {/if} + {#if $$slots.subtitle} +

+ +

+ {/if} +
+ {/if}
@@ -42,4 +49,8 @@ padding-block-end: 0; border-block-end: none; } + + .only-subtitle { + margin-block-end: 0.5rem; + } diff --git a/src/lib/stores/navigation.ts b/src/lib/stores/navigation.ts deleted file mode 100644 index f35211058..000000000 --- a/src/lib/stores/navigation.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { writable } from 'svelte/store'; - -export const navigationCancelled = writable(false); diff --git a/src/lib/stores/notifications.ts b/src/lib/stores/notifications.ts index 97f6cfb36..c250820f7 100644 --- a/src/lib/stores/notifications.ts +++ b/src/lib/stores/notifications.ts @@ -1,13 +1,14 @@ import { writable } from 'svelte/store'; +import type { ComponentType } from 'svelte'; export type Notification = { id: number; - type: 'success' | 'error' | 'info' | 'warning'; + type?: 'success' | 'error' | 'info' | 'warning'; dismissible?: boolean; timeout?: number; message: string; title?: string; - icon?: string; + icon?: ComponentType; buttons?: Buttons[]; isHtml?: boolean; }; @@ -15,32 +16,52 @@ export type Notification = { export type Buttons = { method: () => void | Promise; name: string; + isHtml?: boolean; }; let counter = 0; +const activeTimeouts = new Map(); + export const notifications = writable([]); export const dismissNotification = (id: number) => { + const timeoutId = activeTimeouts.get(id); + + if (timeoutId) { + clearTimeout(timeoutId); + activeTimeouts.delete(id); + } + notifications.update((all) => all.filter((t) => t.id !== id)); }; export const dismissAllNotifications = () => { + activeTimeouts.forEach((timeoutId) => clearTimeout(timeoutId)); + activeTimeouts.clear(); + notifications.set([]); }; -export const addNotification = (notification: Omit) => { - const defaults = { +export const addNotification = (notification: Omit): number => { + const defaults: Notification = { id: counter++, type: 'info', dismissible: true, - timeout: 6000 + timeout: 6000, + ...notification }; - const n = { ...defaults, ...notification }; - notifications.update((all) => { - return [n, ...all.slice(0, 4)]; - }); + notifications.update((all) => [defaults, ...all.slice(0, 4)]); - if (n.timeout) setTimeout(() => dismissNotification(n.id), n.timeout); + if (defaults.timeout) { + const timeoutId = setTimeout(() => { + activeTimeouts.delete(defaults.id); + dismissNotification(defaults.id); + }, defaults.timeout); + + activeTimeouts.set(defaults.id, timeoutId); + } + + return defaults.id; }; diff --git a/src/lib/stores/preferences.ts b/src/lib/stores/preferences.ts index 723721410..fc6a864e2 100644 --- a/src/lib/stores/preferences.ts +++ b/src/lib/stores/preferences.ts @@ -224,6 +224,8 @@ function createPreferences() { delete teamPreferences?.displayNames?.[tableId]; delete teamPreferences?.columnOrder?.[tableId]; delete teamPreferences?.columnWidths?.[tableId]; + delete teamPreferences?.columnWidths?.[tableId + '#columns']; + delete teamPreferences?.columnWidths?.[tableId + '#indexes']; const removeTablePreferences = sdk.forConsole.teams.updatePrefs({ teamId: orgId, diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index 7e8f99355..d49468e24 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -21,7 +21,8 @@ import { Sites, Tokens, TablesDB, - Domains + Domains, + Realtime } from '@appwrite.io/console'; import { Billing } from '../sdk/billing'; import { Backups } from '../sdk/backups'; @@ -90,7 +91,8 @@ function createConsoleSdk(client: Client) { sources: new Sources(client), sites: new Sites(client), domains: new Domains(client), - storage: new Storage(client) + storage: new Storage(client), + realtime: new Realtime(client) }; } @@ -103,8 +105,8 @@ const clientProject = new Client(); const clientRealtime = new Client(); if (!building) { + scopedConsoleClient.setProject('console'); clientConsole.setEndpoint(endpoint).setProject('console'); - scopedConsoleClient.setMode(endpoint).setProject('console'); clientRealtime.setEndpoint(endpoint).setProject('console'); clientProject.setEndpoint(endpoint).setMode('admin'); @@ -185,6 +187,11 @@ export enum RuleTrigger { MANUAL = 'manual' } +/** + * Some type imports are broken on the SDK, this works correctly for the time being! + */ +export type AppwriteRealtimeSubscription = Awaited>; + export const createAdminClient = () => { return new Client().setEndpoint(getApiEndpoint()).setMode('admin').setProject(getProjectId()); }; diff --git a/src/lib/stores/sites.ts b/src/lib/stores/sites.ts index fcd321572..29d811332 100644 --- a/src/lib/stores/sites.ts +++ b/src/lib/stores/sites.ts @@ -26,6 +26,8 @@ export function getFrameworkIcon(framework: string) { return 'vite'; case framework.toLocaleLowerCase().includes('lynx'): return 'lynx'; + case framework.toLocaleLowerCase().includes('tanstack'): + return 'tanstack'; case framework.toLocaleLowerCase().includes('other'): return 'empty'; diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index a8e6e30c7..d3a77a8c4 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -45,7 +45,7 @@ import { headerAlert } from '$lib/stores/headerAlert'; import { UsageRates } from '$lib/components/billing'; import { canSeeProjects } from '$lib/stores/roles'; - import { BottomModalAlert, EmailVerificationBanner } from '$lib/components'; + import { BottomModalAlert } from '$lib/components'; import { IconAnnotation, IconBookOpen, @@ -346,8 +346,6 @@
- - {#if $wizard.show && $wizard.component} {:else if $wizard.cover} diff --git a/src/routes/(console)/bottomAlerts.ts b/src/routes/(console)/bottomAlerts.ts index fbbee52af..864daa6d3 100644 --- a/src/routes/(console)/bottomAlerts.ts +++ b/src/routes/(console)/bottomAlerts.ts @@ -1,76 +1,32 @@ import { isCloud } from '$lib/system'; import { isSameDay } from '$lib/helpers/date'; import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts'; -import SpatialColumnsLight from '$lib/images/promos/spatial-columns-api-light.png'; -import SpatialColumnsDark from '$lib/images/promos/spatial-columns-api-dark.png'; -import InversionQueriesDark from '$lib/images/promos/inversion-queries-dark.png'; -import InversionQueriesLight from '$lib/images/promos/inversion-queries-light.png'; -import TimeHelperQueriesDark from '$lib/images/promos/time-helper-queries-dark.png'; -import TimeHelperQueriesLight from '$lib/images/promos/time-helper-queries-light.png'; +import TransactionsApiDark from '$lib/images/promos/transactions-api-dark.png'; +import TransactionsApiLight from '$lib/images/promos/transactions-api-light.png'; const listOfPromotions: BottomModalAlertItem[] = []; if (isCloud) { - const spatialColumnsPromo: BottomModalAlertItem = { - id: 'modal:spatial_columns_announcement', + const transactionsApiPromo: BottomModalAlertItem = { + id: 'modal:transactions_api_announcement', src: { - dark: SpatialColumnsDark, - light: SpatialColumnsLight + dark: TransactionsApiDark, + light: TransactionsApiLight }, - title: 'Announcing API for spatial columns', - message: 'Store and query geo data directly in your database.', + title: 'Announcing Transactions API', + message: 'Ensure data consistency across tables with atomic, all-or-nothing commits.', plan: 'free', importance: 8, scope: 'project', cta: { text: 'Read announcement', - link: () => 'https://appwrite.io/blog/post/announcing-spatial-columns', + link: () => 'https://appwrite.io/blog/post/announcing-transactions-api', external: true, hideOnClick: true }, show: true }; - - const inversionQueriesPromo: BottomModalAlertItem = { - id: 'modal:inversion_queries_announcement', - src: { - dark: InversionQueriesDark, - light: InversionQueriesLight - }, - title: 'Announcing inversion queries', - message: 'New NOT operators to exclude data directly in queries.', - plan: 'free', - importance: 8, - scope: 'project', - cta: { - text: 'Read announcement', - link: () => 'https://appwrite.io/blog/post/announcing-inversion-queries', - external: true, - hideOnClick: true - }, - show: true - }; - - const timeHelperQueriesPromo: BottomModalAlertItem = { - id: 'modal:time_helper_queries_announcement', - src: { - dark: TimeHelperQueriesDark, - light: TimeHelperQueriesLight - }, - title: 'Announcing Time helper queries', - message: 'New before/after filters for simpler time-based queries.', - plan: 'free', - importance: 8, - scope: 'project', - cta: { - text: 'Read announcement', - link: () => 'https://appwrite.io/blog/post/announcing-time-helper-queries', - external: true, - hideOnClick: true - }, - show: true - }; - listOfPromotions.push(spatialColumnsPromo, inversionQueriesPromo, timeHelperQueriesPromo); + listOfPromotions.push(transactionsApiPromo); } export function addBottomModalAlerts() { diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index cd2417f29..947d507d1 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -83,12 +83,13 @@ } $: projectCreationDisabled = - (isCloud && getServiceLimit('projects') <= data.projects.total) || + (isCloud && getServiceLimit('projects', null, data.currentPlan) <= data.projects.total) || (isCloud && $readOnly && !GRACE_PERIOD_OVERRIDE) || !$canWriteProjects; - $: reachedProjectLimit = isCloud && getServiceLimit('projects') <= data.projects.total; - $: projectsLimit = getServiceLimit('projects'); + $: reachedProjectLimit = + isCloud && getServiceLimit('projects', null, data.currentPlan) <= data.projects.total; + $: projectsLimit = getServiceLimit('projects', null, data.currentPlan); $: $registerCommands([ { diff --git a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte index e6c787b0d..d8085642a 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte @@ -3,7 +3,7 @@ import { onMount } from 'svelte'; import { CardGrid, PaginationInline } from '$lib/components'; import { Button } from '$lib/elements/forms'; - import { toLocaleDate } from '$lib/helpers/date'; + import DualTimeView from '$lib/components/dualTimeView.svelte'; import { formatCurrency } from '$lib/helpers/numbers'; import type { Invoice, InvoiceList } from '$lib/sdk/billing'; import { getApiEndpoint, sdk } from '$lib/stores/sdk'; @@ -112,8 +112,9 @@ {#each invoiceList?.invoices as invoice (invoice.$id)} {@const status = invoice.status} - {toLocaleDate(invoice.dueAt)} + + + {@const isDanger = status === 'overdue' || diff --git a/src/routes/(console)/organization-[organization]/createProject.svelte b/src/routes/(console)/organization-[organization]/createProject.svelte index 340c66efa..1951eb240 100644 --- a/src/routes/(console)/organization-[organization]/createProject.svelte +++ b/src/routes/(console)/organization-[organization]/createProject.svelte @@ -16,7 +16,7 @@ const dispatch = createEventDispatcher(); - let id: string; + let id: string = ''; let error: string; let showCustomId = false; let disabled: boolean = false; @@ -28,7 +28,7 @@ disabled = true; showSubmissionLoader = true; const project = await sdk.forConsole.projects.create({ - projectId: id ?? ID.unique(), + projectId: id || ID.unique(), name, teamId }); diff --git a/src/routes/(console)/organization-[organization]/domains/+page.svelte b/src/routes/(console)/organization-[organization]/domains/+page.svelte index 4e98e3d80..ec6c271e0 100644 --- a/src/routes/(console)/organization-[organization]/domains/+page.svelte +++ b/src/routes/(console)/organization-[organization]/domains/+page.svelte @@ -4,7 +4,7 @@ import { EmptySearch, PaginationWithLimit, ViewSelector } from '$lib/components/index.js'; import { Button } from '$lib/elements/forms'; import Link from '$lib/elements/link.svelte'; - import { toLocaleDateTime } from '$lib/helpers/date'; + import DualTimeView from '$lib/components/dualTimeView.svelte'; import Container from '$lib/layout/container.svelte'; import { protocol } from '$routes/(console)/store.js'; import { @@ -103,9 +103,13 @@ {:else if column.id === 'nameservers'} {domain.nameservers || '-'} {:else if column.id === 'expiry_date'} - {domain?.expire ? toLocaleDateTime(domain.expire) : '-'} + {#if domain?.expire} + + {:else}-{/if} {:else if column.id === 'renewal'} - {domain?.renewal ? toLocaleDateTime(domain.renewal) : '-'} + {#if domain?.renewal} + + {:else}-{/if} {:else if column.id === 'auto_renewal'} {domain?.autoRenewal ? 'On' : 'Off'} {/if} diff --git a/src/routes/(console)/organization-[organization]/header.svelte b/src/routes/(console)/organization-[organization]/header.svelte index 1a81ae48c..38ec2a743 100644 --- a/src/routes/(console)/organization-[organization]/header.svelte +++ b/src/routes/(console)/organization-[organization]/header.svelte @@ -29,7 +29,7 @@ let areMembersLimited: boolean = $state(false); $effect(() => { - const limit = getServiceLimit('members') || Infinity; + const limit = getServiceLimit('members', null, page.data.currentPlan) || Infinity; const isLimited = limit !== 0 && limit < Infinity; areMembersLimited = isCloud && diff --git a/src/routes/(console)/project-[region]-[project]/auth/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/+page.svelte index 2bc7fb4fa..80f6ed5cc 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/+page.svelte @@ -15,7 +15,7 @@ SearchQuery } from '$lib/components'; import { Button } from '$lib/elements/forms'; - import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date'; + import DualTimeView from '$lib/components/dualTimeView.svelte'; import { Container } from '$lib/layout'; import type { Models } from '@appwrite.io/console'; import { writable } from 'svelte/store'; @@ -196,9 +196,13 @@ {user.labels.join(', ')} {:else if id === 'joined'} - {toLocaleDateTime(user.registration)} + {:else if id === 'lastActivity'} - {user.accessedAt ? toLocaleDate(user.accessedAt) : 'never'} + {#if user.accessedAt} + + {:else} + never + {/if} {:else} {user[id]} {/if} diff --git a/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte index 00d33151b..629c5bdda 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte @@ -6,7 +6,7 @@ import type { Models } from '@appwrite.io/console'; import { invalidate } from '$app/navigation'; import { base } from '$app/paths'; - import { toLocaleDateTime } from '$lib/helpers/date'; + import DualTimeView from '$lib/components/dualTimeView.svelte'; import type { PageData } from './$types'; import CreateMember from '../createMembership.svelte'; import DeleteMembership from '../deleteMembership.svelte'; @@ -117,7 +117,7 @@ {membership.roles} - {toLocaleDateTime(membership.joined)} +
{:else} diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/targets/table.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/targets/table.svelte index 48f88f76f..09aa63619 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/targets/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/targets/table.svelte @@ -3,7 +3,7 @@ import { Button } from '$lib/elements/forms'; import type { PageData } from './$types'; import { columns } from './store'; - import { toLocaleDateTime } from '$lib/helpers/date'; + import DualTimeView from '$lib/components/dualTimeView.svelte'; import ProviderType from '$routes/(console)/project-[region]-[project]/messaging/providerType.svelte'; import Provider from '$routes/(console)/project-[region]-[project]/messaging/provider.svelte'; import { sdk } from '$lib/stores/sdk'; @@ -83,7 +83,7 @@ {/if} {:else if column.id === '$createdAt'} - {toLocaleDateTime(target[column.id])} + {:else} {target[column.id]} {/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/+layout.svelte index 7e8347e4d..a94f13fec 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/+layout.svelte @@ -31,7 +31,10 @@ - Databases - Appwrite + + {#key page.url.pathname} + Databases - Appwrite + {/key} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte index 38ee34a9a..09289f76f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte @@ -10,7 +10,7 @@ FloatingActionBar, Popover } from '@appwrite.io/pink-svelte'; - import { IconCalendar, IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte'; + import { IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport'; import type { Column } from '$lib/helpers/types'; import { expandTabs } from '../table-[table]/store'; @@ -25,23 +25,21 @@ tableColumnSuggestions, basicColumnOptions, mockSuggestions, - createTableRequest + showIndexesSuggestions } from './store'; - import { addNotification } from '$lib/stores/notifications'; + import { addNotification, dismissNotification } from '$lib/stores/notifications'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { sleep } from '$lib/helpers/promises'; - import { invalidate, beforeNavigate, goto } from '$app/navigation'; - import { showCreateTable } from '../store'; - import { showSubNavigation } from '$lib/stores/layout'; - import { navigationCancelled } from '$lib/stores/navigation'; + import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { isWithinSafeRange } from '$lib/helpers/numbers'; import type { Columns } from '../table-[table]/store'; import { columnOptions } from '../table-[table]/columns/store'; import Options from './options.svelte'; import { InputSelect, InputText } from '$lib/elements/forms'; - import { Confirm } from '$lib/components'; - import { VARS } from '$lib/system'; + import { isCloud, VARS } from '$lib/system'; + + import IconAINotification from './icon/aiNotification.svelte'; let resizeObserver: ResizeObserver; let spreadsheetContainer: HTMLElement; @@ -51,18 +49,19 @@ let rangeOverlayEl: HTMLDivElement | null = null; let fadeBottomOverlayEl: HTMLDivElement | null = null; - let customColumns = $state([]); + let customColumns = $state< + (SuggestedColumnSchema & { elements?: []; isPlaceholder?: boolean })[] + >(Array.from({ length: 7 }, (_, index) => createPlaceholderColumn(index))); + let showFloatingBar = $state(true); let hasTransitioned = $state(false); let scrollAnimationFrame: number | null = null; - let confirmDismiss = $state(false); - let confirmNavigation = $state(false); - let pendingNavigationUrl: string | null = null; - let creatingColumns = $state(false); const baseColProps = { draggable: false, resizable: false }; + const NOTIFICATION_AND_MOCK_DELAY = 1250; + const getColumnWidth = (columnKey: string) => Math.max(180, columnKey.length * 8 + 60); const safeNumericValue = (value: number | undefined) => value !== undefined && isWithinSafeRange(value) ? value : undefined; @@ -128,12 +127,6 @@ rangeOverlayEl.style.display = 'block'; } - if (customColumns.length === 0) { - spreadsheetContainer.style.setProperty('--group-left', '40px'); - spreadsheetContainer.style.setProperty('--group-width', '100%'); - return; - } - // Custom columns mode: calculate precise overlay bounds const containerRect = spreadsheetContainer.getBoundingClientRect(); const getById = (id: string) => @@ -141,6 +134,31 @@ `[role="cell"][data-header="true"][data-column-id="${id}"]` ); + const hasRealColumns = customColumns.some((col) => !col.isPlaceholder); + if (!hasRealColumns) { + // For placeholders or no columns, position overlay to cover custom columns area + const idCell = getById('$id'); + const actionsCell = headerElement!.querySelector( + '[role="cell"][data-column-id="actions"]' + ); + + if (idCell && actionsCell) { + const idRect = idCell.getBoundingClientRect(); + const actionsRect = actionsCell.getBoundingClientRect(); + const left = Math.round(idRect.right - containerRect.left); + const actionsLeft = actionsRect.left - containerRect.left; + + const width = actionsLeft - left; + + spreadsheetContainer.style.setProperty('--group-left', `${left - 2}px`); + spreadsheetContainer.style.setProperty('--group-width', `${width + 2}px`); + } else { + spreadsheetContainer.style.setProperty('--group-left', '40px'); + spreadsheetContainer.style.setProperty('--group-width', 'calc(100% - 80px)'); + } + return; + } + // Calculate visible viewport bounds const scrollerRect = hScroller ? hScroller.getBoundingClientRect() : containerRect; const visibleRight = scrollerRect.right; @@ -190,27 +208,22 @@ const left = Math.round(startLeft - containerRect.left); - // maximum possible width of all custom columns - const totalFullWidth = customColumns.reduce( - (total, col) => total + getColumnWidth(col.key), - 0 - ); - // get the actions column and use its left border as the boundary const actionsCell = headerElement!.querySelector( '[role="cell"][data-column-id="actions"]' ); - const rawVisibleWidth = Math.round(visibleRight - idRect.right); - let maxAllowedWidth = rawVisibleWidth; - if (actionsCell) { - const actionsRect = actionsCell.getBoundingClientRect(); - const actionsLeft = actionsRect.left - containerRect.left; - maxAllowedWidth = Math.min(rawVisibleWidth, actionsLeft - left); + if (!actionsCell) { + if (rangeOverlayEl) { + rangeOverlayEl.style.display = 'none'; + } + return; } - // Set overlay width to not exceed actions column boundary - const width = Math.min(totalFullWidth, maxAllowedWidth); + const actionsRect = actionsCell.getBoundingClientRect(); + const actionsLeft = actionsRect.left - containerRect.left; + + const width = actionsLeft - left; // Apply overlay positioning spreadsheetContainer.style.setProperty('--group-left', `${left - 2}px`); @@ -219,7 +232,7 @@ // only for mobile, we can remove if not needed! const scrollToFirstCustomColumn = () => { - if (!$isSmallViewport || customColumns.length === 0) return; + if (!$isSmallViewport) return; if (!headerElement || !headerElement.isConnected) { headerElement = spreadsheetContainer.querySelector('[role="rowheader"]'); @@ -231,12 +244,19 @@ `[role="cell"][data-header="true"][data-column-id="${customColumns[0]?.key}"]` ); - if (firstCustomColumnCell && hScroller) { - const cellRect = firstCustomColumnCell.getBoundingClientRect(); - const scrollerRect = hScroller.getBoundingClientRect(); - const scrollLeft = hScroller.scrollLeft + cellRect.left - scrollerRect.left - 40; + const directAccessScroller = + hScroller ?? + findHorizontalScroller(headerElement) ?? + // internal spreadsheet root main container! + spreadsheetContainer.querySelector('.spreadsheet-container'); - hScroller.scrollTo({ + if (firstCustomColumnCell && directAccessScroller) { + const cellRect = firstCustomColumnCell.getBoundingClientRect(); + const scrollerRect = directAccessScroller.getBoundingClientRect(); + const scrollLeft = + directAccessScroller.scrollLeft + cellRect.left - scrollerRect.left - 40; + + directAccessScroller.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' }); @@ -260,25 +280,6 @@ }); }; - // Handle create table requests from subNavigation - const unsubscribeCreateTable = createTableRequest.subscribe((requested) => { - if (requested) { - if (customColumns.length > 0 && !creatingColumns) { - confirmNavigation = true; - pendingNavigationUrl = 'create-table'; - } else { - executeCreateTable(); - } - - createTableRequest.set(false); - } - }); - - function executeCreateTable() { - $showCreateTable = true; - $showSubNavigation = false; - } - const customSuggestedColumns = $derived.by(() => { return customColumns.map((col: SuggestedColumnSchema) => { const columnOption = getColumnOption(col.type, col.format); @@ -301,54 +302,54 @@ }); }); - const getRowColumns = (): Column[] => [ - { - id: '$id', - title: '$id', - type: 'string', - width: 180, - icon: IconFingerPrint, - ...baseColProps - }, - ...customSuggestedColumns, - ...(customColumns.length === 0 - ? [ - { - id: '$createdAt', - title: '$createdAt', - type: 'datetime' as Column['type'], - width: 180, - icon: IconCalendar, - ...baseColProps - }, - { - id: '$updatedAt', - title: '$updatedAt', - type: 'datetime' as Column['type'], - width: 180, - icon: IconCalendar, - ...baseColProps - } - ] - : []), - { - id: 'actions', - title: '', - type: 'string' as Column['type'], - width: 40, - isAction: true, - ...baseColProps - } - ]; + const getRowColumns = (): Column[] => { + const minColumnWidth = 180; + const fixedWidths = { id: minColumnWidth, actions: 40, selection: 40 }; - // Handle browser back/forward navigation - const handleBeforeUnload = (event: BeforeUnloadEvent) => { - if (customColumns.length > 0 && !creatingColumns) { - event.preventDefault(); - event.returnValue = - 'You have unsaved column suggestions. Are you sure you want to leave?'; - return event.returnValue; - } + // calculate base widths and total + const columnsWithBase = customSuggestedColumns.map((col) => ({ + ...col, + baseWidth: Math.max(minColumnWidth, getColumnWidth(col.id)) + })); + + const totalUsed = + fixedWidths.id + + fixedWidths.actions + + fixedWidths.selection + + columnsWithBase.reduce((sum, col) => sum + col.baseWidth, 0); + + // distribute excess space equally across custom columns + const viewportWidth = + spreadsheetContainer?.clientWidth || + (typeof window !== 'undefined' ? window.innerWidth : totalUsed); + + const extraPerColumn = + Math.max(0, viewportWidth - totalUsed) / (columnsWithBase.length || 1); + + const finalCustomColumns = columnsWithBase.map((col) => ({ + ...col, + width: { min: col.baseWidth + extraPerColumn } + })); + + return [ + { + id: '$id', + title: '$id', + type: 'string', + width: fixedWidths.id, + icon: IconFingerPrint, + ...baseColProps + }, + ...finalCustomColumns, + { + id: 'actions', + title: '', + type: 'string' as Column['type'], + width: fixedWidths.actions, + isAction: true, + ...baseColProps + } + ]; }; const spreadsheetColumns = $derived(getRowColumns()); @@ -364,15 +365,6 @@ await suggestColumns(); }); - beforeNavigate(({ cancel, to }) => { - if (customColumns.length > 0 && !creatingColumns) { - cancel(); - confirmNavigation = true; - $navigationCancelled = true; - pendingNavigationUrl = to?.url?.pathname || null; - } - }); - function resetSuggestionsStore(fullReset: boolean = true) { if ($tableColumnSuggestions.table?.id !== page.params.table) { return; @@ -391,6 +383,12 @@ async function suggestColumns() { $tableColumnSuggestions.thinking = true; + + if ($isSmallViewport) { + await tick(); + scrollToFirstCustomColumn(); + } + let suggestedColumns: { total: number; columns: ColumnInput[]; @@ -402,7 +400,7 @@ try { if (VARS.MOCK_AI_SUGGESTIONS) { /* animation */ - await sleep(1250); + await sleep(NOTIFICATION_AND_MOCK_DELAY); suggestedColumns = mockSuggestions; } else { suggestedColumns = (await sdk @@ -410,7 +408,8 @@ .console.suggestColumns({ databaseId: page.params.database, tableId: page.params.table, - context: $tableColumnSuggestions.context ?? undefined + context: $tableColumnSuggestions.context ?? undefined, + min: 6 })) as unknown as { total: number; columns: ColumnInput[]; @@ -423,21 +422,54 @@ total: suggestedColumns.total }); - customColumns = mapSuggestedColumns(suggestedColumns.columns); + const mappedColumns = mapSuggestedColumns(suggestedColumns.columns); - if (customColumns.length > 0) { - setTimeout(scrollToFirstCustomColumn, 100); - setTimeout(() => (hasTransitioned = true), 300); + // replace with actual columns and trim excess + if (mappedColumns.length < customColumns.length) { + customColumns = customColumns.slice(0, mappedColumns.length); } - } catch (error) { - addNotification({ - type: 'error', - message: error.message + + // replace existing placeholders and + // add any additional columns if needed + mappedColumns.forEach((column, index) => { + setTimeout(() => { + if (index < customColumns.length) { + // replace existing placeholder + customColumns[index] = { ...column, isPlaceholder: false }; + } else { + // new column directly if we have more than expected + // just added in case the max ever changes on backend! + customColumns.push({ ...column, isPlaceholder: false }); + } + + // recalculate overlay bounds + // after each column is populated! + requestAnimationFrame(() => updateOverlayBounds()); + }, index * 150); }); - trackError(error, Submit.ColumnSuggestions); - } finally { + if (mappedColumns.length > 0) { + setTimeout( + () => { + hasTransitioned = true; + // final recal after + // all animations complete + requestAnimationFrame(() => { + recalcAll(); + }); + }, + mappedColumns.length * 150 + 300 + ); + } + resetSuggestionsStore(false); + } catch (error) { + // remove completely! + resetSuggestionsStore(); + + // track & notify! + trackError(error, Submit.ColumnSuggestions); + addNotification({ type: 'error', message: error.message }); } } @@ -483,6 +515,31 @@ return !['$id', '$createdAt', '$updatedAt', 'actions'].includes(id); } + function showIndexSuggestionsNotification() { + // safeguard anyways! + if (!isCloud) return; + + setTimeout(() => { + const notifId = addNotification({ + isHtml: true, + title: 'Next step: add indexes', + message: 'See suggested indexes based on your columns', + dismissible: true, + icon: IconAINotification, + timeout: 10000, // ten seconds + buttons: [ + { + name: 'Create indexes', + method: () => { + dismissNotification(notifId); + showIndexesSuggestions.update(() => true); + } + } + ] + }); + }, NOTIFICATION_AND_MOCK_DELAY); + } + async function createColumns() { creatingColumns = true; const client = sdk.forProject(page.params.region, page.params.project); @@ -589,9 +646,13 @@ addNotification({ type: 'success', - message: 'Columns created successfully' + message: 'Columns created successfully', + timeout: NOTIFICATION_AND_MOCK_DELAY }); + // show index notification! + showIndexSuggestionsNotification(); + trackEvent(Submit.ColumnCreate, { type: 'suggestions' }); } catch (error) { trackError(error, Submit.ColumnCreate); @@ -603,6 +664,23 @@ } } + function createPlaceholderColumn( + index: number + ): SuggestedColumnSchema & { elements?: []; isPlaceholder?: boolean } { + return { + key: `column${index + 1}`, + type: 'string', + required: false, + default: null, + format: null, + size: undefined, + min: undefined, + max: undefined, + elements: undefined, + isPlaceholder: true + }; + } + onDestroy(() => { resizeObserver?.disconnect(); hScroller?.removeEventListener('scroll', recalcAllThrottled); @@ -612,15 +690,15 @@ customColumns = []; resetSuggestionsStore(); - unsubscribeCreateTable(); }); - +
0} + class:thinking={$tableColumnSuggestions.thinking} class="databases-spreadsheet spreadsheet-container-outer" style:--overlay-icon-color="#fd366e99" style:--non-overlay-icon-color="--fgcolor-neutral-weak"> @@ -643,7 +721,6 @@ bottomActionClick={() => {}}> {#each spreadsheetColumns as column, index (index)} - {@const isColumnInteractable = isCustomColumn(column.id)} {#if column.isAction} @@ -658,6 +735,8 @@ {@const columnIconColor = !columnObj?.type ? '--non-overlay-icon-color' : '--overlay-icon-color'} + {@const isColumnInteractable = + isCustomColumn(column.id) && !columnObj.isPlaceholder} - {column.title} + + {column.title} + - { - if ( - isColumnInteractable && - !$isTabletViewport - ) { - toggle(event); - } - }}> - - +
+ { + if ( + isColumnInteractable && + !$isTabletViewport + ) { + toggle(event); + } + }}> + {#if !columnObj?.isPlaceholder} + + {/if} + +
- + Thinking of column suggestions - + @@ -847,7 +941,7 @@
- {:else if customColumns.length > 0 && showFloatingBar} + {:else if customColumns.some((col) => !col.isPlaceholder) && showFloatingBar}
(confirmDismiss = true)} + on:click={() => { + customColumns = []; + resetSuggestionsStore(); + }} style="opacity: {creatingColumns ? '0' : '1'}" >Dismiss @@ -897,48 +994,13 @@ {/if}
- { - customColumns = []; - resetSuggestionsStore(); - }}> - Are you sure you want to dismiss these columns suggested by AI? This action is irreversible. - - - { - customColumns = []; - resetSuggestionsStore(); - confirmNavigation = false; - - if (pendingNavigationUrl) { - if (pendingNavigationUrl === 'create-table') { - executeCreateTable(); - } else { - goto(pendingNavigationUrl); - } - - pendingNavigationUrl = null; - } - }}> - You have unsaved column suggestions. If you leave this page, you'll lose these suggestions. Are - you sure you want to continue? - - diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/ai.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/ai.svelte index 27bc31198..1d51db168 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/ai.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/ai.svelte @@ -1,8 +1,18 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/aiNotification.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/aiNotification.svelte new file mode 100644 index 000000000..1fe702004 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/icon/aiNotification.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/indexes.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/indexes.svelte new file mode 100644 index 000000000..2f33a1af6 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/indexes.svelte @@ -0,0 +1,567 @@ + + +{#if !$isSmallViewport} + { + await applySuggestedIndexes(); + }}> + + {#await loadIndexSuggestions()} + + + {#each Array(3) as _, index} + {@const firstItem = index === 0} + + {@render fieldSkeleton({ label: 'Column', showLabel: firstItem })} + {@render fieldSkeleton({ label: 'Type', showLabel: firstItem })} + {@render fieldSkeleton({ label: 'Order', showLabel: firstItem })} + + +
+ +
+
+ {/each} +
+ {@render addIndexButton()} + + {:then suggestedIndexes} + + + {#each suggestedIndexes as index, count} + {@render indexEditForm({ index, count, isDesktop: true })} + {/each} + + + {@render addIndexButton()} + + {/await} + + + + + + + + + + + + +{/if} + +{#if $isSmallViewport} + await applySuggestedIndexes() + }} + cancel={{ + disabled: loadingSuggestions || creatingIndexes, + onClick: () => { + if (indexes.length > 0 && !creatingIndexes) { + confirmDismiss = true; + } else { + $showIndexesSuggestions = false; + } + } + }}> + {#if modalError} + + {/if} + + {#await loadIndexSuggestions()} + + {#each Array(3) as _} + {@render fieldSkeleton({ + label: undefined, + showLabel: false, + isDesktop: false + })} + {/each} + + {:then suggestedIndexes} + + {#each suggestedIndexes as index, count} + + {@render indexEditForm({ index, count, isDesktop: false })} + + {/each} + + {@render addIndexButton()} + + {/await} + +{/if} + +{#snippet fieldSkeleton({ label, showLabel, isDesktop = true })} + + {#if showLabel} + {label} + {/if} + + +{/snippet} + +{#snippet indexEditForm({ index, count, isDesktop = true })} + {@const firstItem = count === 0} + {#if isDesktop} + + syncIndexState(event, index)} + options={columnOptions} + placeholder="Select column" + required + disabled={creatingIndexes} /> + + + + + + {@render removeIndexButton({ count, isDesktop: true })} + + {:else} + + + + syncIndexState(event, index)} + options={columnOptions} + placeholder="Select column" + required /> + + + + + + + {@render removeIndexButton({ count, isDesktop: false })} + + {/if} +{/snippet} + +{#snippet addIndexButton()} + {#if indexes.length < MAX_INDEXES} + + + + {/if} +{/snippet} + +{#snippet removeIndexButton({ count, isDesktop = true })} + {#if isDesktop} +
+ +
+ {:else if indexes.length > 1} + + + + {/if} +{/snippet} + + + Are you sure you want to dismiss these suggested indexes? This action cannot be undone. + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte index b663588e9..31272972c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte @@ -1,42 +1,67 @@ - - + + - - Smart column suggestions - -
- -
-
+ {title} - Enable AI to suggest useful columns based on your table name + {subtitle}
+ + {#if featureActive} +
+ +
+ {/if}
- {#if $tableColumnSuggestions.enabled} + {#if !featureActive} + + + + {/if} + + + {#if $tableColumnSuggestions.enabled && featureActive}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts index e0432b6f2..f1792e391 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts @@ -1,4 +1,5 @@ import { writable } from 'svelte/store'; +import { IndexType } from '@appwrite.io/console'; import { columnOptions } from '../table-[table]/columns/store'; export type TableColumnSuggestions = { @@ -24,6 +25,20 @@ export type SuggestedColumnSchema = { format?: string | null; }; +export enum IndexOrder { + ASC = 'ASC', + DESC = 'DESC', + NONE = null +} + +export type SuggestedIndexSchema = { + key: string; + type: IndexType; + orders: IndexOrder; + columns: string[]; + lengths?: number[] | undefined; +}; + export const tableColumnSuggestions = writable({ enabled: false, context: null, @@ -31,7 +46,7 @@ export const tableColumnSuggestions = writable({ table: null }); -export const createTableRequest = writable(false); +export const showIndexesSuggestions = writable(false); export const mockSuggestions: { total: number; columns: ColumnInput[] } = { total: 7, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte index dc39232b0..fe77a776b 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte @@ -138,7 +138,10 @@ - Database - Appwrite + + {#key page.url.pathname} + Database - Appwrite + {/key} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte index 9fb56506a..6c6a8de18 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte @@ -23,6 +23,7 @@ import { trackEvent } from '$lib/actions/analytics'; import { Layout, Typography } from '@appwrite.io/pink-svelte'; import { page } from '$app/state'; + import IconQuestionMarkCircle from './components/questionIcon.svelte'; let policyCreateError: string; let totalPolicies: UserBackupPolicy[] = []; @@ -40,7 +41,7 @@ if (parsedCounter === showOnCount || !counter) { addNotification({ type: 'info', - icon: 'question-mark-circle', + icon: IconQuestionMarkCircle, message: 'How was your experience with our new Backups feature? Give us your feedback and help us improve!', timeout: 15000, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/components/questionIcon.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/components/questionIcon.svelte new file mode 100644 index 000000000..98e32e2bd --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/components/questionIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/createTable.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/createTable.svelte index 37906248f..78f692344 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/createTable.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/createTable.svelte @@ -19,6 +19,7 @@ } = $props(); const databaseId = page.params.database; + const isOnTablesPage = $derived(page.route?.id.endsWith('table-[table]')); let name = $state(''); let id: string = $state(null); @@ -27,6 +28,20 @@ let creatingTable = $state(false); + function enableThinkingModeForSuggestions(table: Models.Table) { + if ($tableColumnSuggestions.enabled) { + // if enabled, trigger thinking mode! + tableColumnSuggestions.update((store) => ({ + ...store, + thinking: true, + table: { + id: table.$id, + name: table.name + } + })); + } + } + async function createTable() { error = null; try { @@ -38,14 +53,6 @@ name }); - tableColumnSuggestions.update((store) => ({ - ...store, - table: { - id: table.$id, - name: table.name - } - })); - updateAndCleanup(); await onTableCreated(table); @@ -53,6 +60,9 @@ name = id = null; showCreate = false; creatingTable = false; + + // don't wait for UI to mount! + enableThinkingModeForSuggestions(table); } catch (e) { error = e.message; trackError(e, Submit.TableCreate); @@ -93,6 +103,15 @@ touchedId = false; } }); + + $effect(() => { + if (showCreate && isOnTablesPage && $tableColumnSuggestions.table) { + tableColumnSuggestions.update((store) => ({ + ...store, + table: null + })); + } + }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/delete.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/delete.svelte index 47a70368a..be59aa9ab 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/delete.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/delete.svelte @@ -8,7 +8,7 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { database } from './store'; - import { toLocaleDate } from '$lib/helpers/date'; + import DualTimeView from '$lib/components/dualTimeView.svelte'; import { type Models, Query } from '@appwrite.io/console'; import { Spinner, Table } from '@appwrite.io/pink-svelte'; @@ -126,7 +126,9 @@ {#each tableItems as table} {table.name} - {toLocaleDate(table.updatedAt)} + + + {/each} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte index e66924433..4b5354ffd 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte @@ -28,14 +28,13 @@ import { sdk } from '$lib/stores/sdk'; import { onMount } from 'svelte'; import { subNavigation } from '$lib/stores/database'; - import { createTableRequest, tableColumnSuggestions } from './(suggestions)/store'; - let data = $derived(page.data) as PageData; + const data = $derived(page.data) as PageData; - let region = $derived(page.params.region); - let project = $derived(page.params.project); - let tableId = $derived(page.params.table); - let databaseId = $derived(page.params.database); + const region = $derived(page.params.region); + const project = $derived(page.params.project); + const tableId = $derived(page.params.table); + const databaseId = $derived(page.params.database); let openBottomSheet = $state(false); @@ -56,10 +55,11 @@ const isMainDatabaseScreen = $derived(page.route.id.endsWith('database-[database]')); - // If banner open, `-1rem` to adjust banner size, else `-70.5px`. + // If banner open, adjust bottom position to account for banner container. // 70.5px is the size of the container of the banner holder and not just the banner! // Needed because things vary a bit much on how different browsers treat bottom layouts. - const bottomNavHeight = $derived(`calc(20% ${$bannerSpacing ? '- 1rem' : '- 70.5px'})`); + const bottomNavOffset = $derived($bannerSpacing ? '70.5px' : '0px'); + const tableContentPadding = $derived($bannerSpacing ? '210px' : '140px'); async function loadTables() { tables = await sdk.forProject(region, project).tablesDB.listTables({ @@ -93,7 +93,7 @@ {data.database?.name} -
+
{#if tables?.total}
    {#each sortedTables as table, index} @@ -147,25 +147,15 @@
- +
@@ -177,7 +167,7 @@ {@const href = `${base}/project-${region}-${project}/databases/database-${databaseId}/${action.href}`} -
  • +
  • @@ -191,7 +181,7 @@ {/each} - +
  • {:else if data?.database?.name && !isMainDatabaseScreen} @@ -271,11 +261,15 @@ overflow-x: hidden; min-height: 0; margin-bottom: auto; - padding-bottom: 16px; - scrollbar-width: thin; - scrollbar-color: var(--border-neutral, #ededf0) transparent; color: var(--fgcolor-neutral-secondary, #56565c); + /* hide scrollbars */ + scrollbar-width: none; + -ms-overflow-style: none; + + // scrollbar-width: thin; + // scrollbar-color: var(--border-neutral, #ededf0) transparent; + &::-webkit-scrollbar { width: 4px; height: 4px; @@ -304,6 +298,10 @@ font-size: var(--font-size-sm); color: var(--fgcolor-neutral-secondary); + &::-webkit-scrollbar { + display: none; + } + &:not(.bottom-nav)::before { content: ''; right: 99%; @@ -335,12 +333,16 @@ position: relative; padding-inline-end: 0.5rem; margin-inline-start: 0.5rem; - } - li:hover { - color: var(--fgcolor-neutral-primary); - border-radius: var(--border-radius-s, 6px); - background: var(--bgcolor-neutral-secondary); + &:hover { + color: var(--fgcolor-neutral-primary); + border-radius: var(--border-radius-s, 6px); + background: var(--bgcolor-neutral-secondary); + } + + &.bottom-nav-item:hover { + margin-inline-end: 1.25rem; + } } .table-name { @@ -382,8 +384,16 @@ line-height: 150%; /* 21px */ } + .bottom-nav-container { + right: 0; + bottom: 0; + left: 1.25rem; + position: absolute; + padding-block-end: 1rem; + } + .action-menu-divider { - margin-inline: -1.2rem; padding-block-end: 0.25rem; + margin-inline-start: -1.25rem; } diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte index fcc995b71..93034a2d5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte @@ -35,7 +35,8 @@ spreadsheetRenderKey, expandTabs, databaseRelatedRowSheetOptions, - rowPermissionSheet + rowPermissionSheet, + type Columns } from './store'; import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter'; import CreateColumn from './createColumn.svelte'; @@ -56,7 +57,6 @@ import { Button, Seekbar } from '$lib/elements/forms'; import { generateFakeRecords, generateColumns } from '$lib/helpers/faker'; import { addNotification } from '$lib/stores/notifications'; - import { sleep } from '$lib/helpers/promises'; import CreateIndex from './indexes/createIndex.svelte'; import { hash } from '$lib/helpers/string'; import { preferences } from '$lib/stores/preferences'; @@ -64,6 +64,10 @@ import { chunks } from '$lib/helpers/array'; import { Submit, trackEvent } from '$lib/actions/analytics'; + import IndexesSuggestions from '../(suggestions)/indexes.svelte'; + import { showIndexesSuggestions, tableColumnSuggestions } from '../(suggestions)'; + import type { RealtimeResponseEvent } from '@appwrite.io/console'; + let editRow: EditRow; let editRelatedRow: EditRelatedRow; let editRowPermissions: EditRowPermissions; @@ -79,6 +83,8 @@ */ let isWaterfallFromFaker = false; + let columnCreationHandler: ((response: RealtimeResponseEvent) => void) | null = null; + onMount(() => { expandTabs.set(preferences.getKey('tableHeaderExpanded', true)); @@ -89,7 +95,19 @@ response.events.includes('databases.*.tables.*.columns.*') || response.events.includes('databases.*.tables.*.indexes.*') ) { - if (!isWaterfallFromFaker) { + if (isWaterfallFromFaker) { + columnCreationHandler?.(response); + } + + // don't invalidate when - + // 1. from faker + // 2. ai columns creation + // 3. ai indexes creation + if ( + !isWaterfallFromFaker && + !$showIndexesSuggestions && + !$tableColumnSuggestions.table + ) { invalidate(Dependencies.TABLE); } } @@ -242,21 +260,76 @@ indexes: 700 }); + function setupColumnObserver() { + let expectedCount = 0; + let resolvePromise: () => void; + let timeout: ReturnType; + + const availableColumns = new Set(); + const waitPromise = new Promise((resolve) => (resolvePromise = resolve)); + + columnCreationHandler = (response) => { + const { events, payload } = response; + + if ( + events.includes('databases.*.tables.*.columns.*.create') || + events.includes('databases.*.tables.*.columns.*.update') + ) { + const asColumn = payload as Columns; + const columnId = asColumn.key; + const status = asColumn.status; + + if (status === 'available') { + availableColumns.add(columnId); + + if (expectedCount > 0 && availableColumns.size >= expectedCount) { + clearTimeout(timeout); + columnCreationHandler = null; + resolvePromise(); + } + } + } + }; + + // return function to start waiting! + const startWaiting = (count: number) => { + expectedCount = count; + + timeout = setTimeout(() => { + columnCreationHandler = null; + resolvePromise(); + }, 10000); + + if (availableColumns.size >= expectedCount) { + clearTimeout(timeout); + columnCreationHandler = null; + resolvePromise(); + } + }; + + return { startWaiting, waitPromise }; + } + async function createFakeData() { isWaterfallFromFaker = true; $spreadsheetLoading = true; $randomDataModalState.show = false; - let columns = $table.columns; + let columns = page.data.table.columns as Columns[]; const hasAnyRelationships = columns.some((column) => isRelationship(column)); const filteredColumns = columns.filter((col) => col.type !== 'relationship'); if (!filteredColumns.length) { try { + const { startWaiting, waitPromise } = setupColumnObserver(); columns = await generateColumns($project, page.params.database, page.params.table); + startWaiting(columns.length); + await waitPromise; await invalidate(Dependencies.TABLE); + columns = page.data.table.columns as Columns[]; + trackEvent(Submit.ColumnCreate, { type: 'faker' }); } catch (e) { addNotification({ @@ -268,9 +341,6 @@ } } - /* let the columns be processed! */ - await sleep(1250); - let rowIds = []; try { const { rows, ids } = generateFakeRecords(columns, $randomDataModalState.value); @@ -442,7 +512,7 @@ title="Row permissions" bind:show={$rowPermissionSheet.show} submit={{ - text: 'Create', + text: 'Update', disabled: editRowPermissions?.disableSubmit(), onClick: async () => editRowPermissions?.updatePermissions() }}> @@ -470,3 +540,5 @@
    + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte index b6d825c4f..df77376fa 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte @@ -6,7 +6,7 @@ import { Container } from '$lib/layout'; import { preferences } from '$lib/stores/preferences'; import { canWriteTables, canWriteRows } from '$lib/stores/roles'; - import { Icon, Layout, Divider } from '@appwrite.io/pink-svelte'; + import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte'; import type { PageData } from './$types'; import { table, @@ -31,6 +31,7 @@ import EmptySheet from './layout/emptySheet.svelte'; import CreateRow from './rows/create.svelte'; import { onDestroy } from 'svelte'; + import { isCloud } from '$lib/system'; import { Empty as SuggestionsEmptySheet, tableColumnSuggestions } from '../(suggestions)'; export let data: PageData; @@ -75,6 +76,12 @@ $: hasColumns = !!$table.columns.length; $: hasValidColumns = $table?.columns?.some((col) => col.status === 'available'); + $: canShowSuggestionsSheet = + // enabled, has table details + // and it matches current table + $tableColumnSuggestions.enabled && + $tableColumnSuggestions.table && + $tableColumnSuggestions.table.id === page.params.table; async function onSelect(file: Models.File, localFile = false) { $isCsvImportInProgress = true; @@ -114,21 +121,31 @@ - + +
    + +
    - + Columns +
    + + + + + Filters +
    - {:else if column.key !== '$sequence'} - + {:else if !isId}
    {#if selectedColumn} - + {:else if selectedColumns && selectedColumns.length} {/if} @@ -476,3 +581,13 @@ {#if showFailed} {/if} + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/boolean.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/boolean.svelte index cd09096a4..dbaf84823 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/boolean.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/boolean.svelte @@ -38,6 +38,9 @@ + + +
    + +
    + + + Required cannot be selected because array columns may contain more than one value. + +
    + + +
    + +
    + + + {#if editing} + Array cannot be selected to avoid data incompatibility. + {:else} + Array cannot be selected because required columns must be populated in all rows with a + single value. + {/if} + +
    diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/string.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/string.svelte index cc631ae82..e43c89da2 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/string.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/string.svelte @@ -44,6 +44,7 @@ import { currentPlan } from '$lib/stores/organization'; import { createConservative } from '$lib/helpers/stores'; import { ActionMenu, Selector } from '@appwrite.io/pink-svelte'; + import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte'; import { InputNumber, InputText, InputTextarea } from '$lib/elements/forms'; import { Popover, Layout, Tag, Typography, Link } from '@appwrite.io/pink-svelte'; @@ -105,21 +106,7 @@ disabled={data.required || data.array} nullable={!data.required && !data.array} /> - - - +
    import { InputURL } from '$lib/elements/forms'; + import { createConservative } from '$lib/helpers/stores'; + import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte'; export let data: Partial; export let editing = false; - import { createConservative } from '$lib/helpers/stores'; - import { Selector } from '@appwrite.io/pink-svelte'; - let savedDefault = data.default; function handleDefaultState(hideDefault: boolean) { @@ -64,6 +63,7 @@ array: false, ...data }); + $: listen(data); $: handleDefaultState($required || $array); @@ -76,17 +76,5 @@ bind:value={data.default} disabled={data.required || data.array} nullable={!data.required && !data.array} /> - - + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/+page.svelte index 53a4327e9..bce6d08c1 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/+page.svelte @@ -1,6 +1,6 @@ -
    +
    @@ -110,8 +124,19 @@ {#if footer} {@render footer?.()} {/if} - + + + - - {/if} - - + + {#if $canWriteProviders} + + + + {/if} + {#if data.providers.total} @@ -52,11 +47,7 @@ {:else if $hasPageQueries} {:else if data.search && data.search !== 'empty'} - -
    - Sorry, we couldn't find '{data.search}' -

    There are no providers that match your search.

    -
    + - @@ -227,7 +240,9 @@ hasDivider href={`https://appwrite.io/docs/products/messaging/${$provider}`} title="Read the guide in the docs" - icon={IconBookOpen} /> + icon={IconBookOpen} + target="_blank" + rel="noreferrer" /> { $newMemberModal = true; @@ -241,4 +256,9 @@ {#if $newMemberModal} {/if} + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/messaging/providers/createProviderDropdown.svelte b/src/routes/(console)/project-[region]-[project]/messaging/providers/createProviderDropdown.svelte index ea8f54839..b210dc61b 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/providers/createProviderDropdown.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/providers/createProviderDropdown.svelte @@ -9,7 +9,7 @@ import { ActionMenu, Popover } from '@appwrite.io/pink-svelte'; - + {#each Object.entries(providers) as [type, option]} diff --git a/src/routes/(console)/project-[region]-[project]/messaging/providers/popoverContent.svelte b/src/routes/(console)/project-[region]-[project]/messaging/providers/popoverContent.svelte index 7b9a1e723..ea0f6e270 100644 --- a/src/routes/(console)/project-[region]-[project]/messaging/providers/popoverContent.svelte +++ b/src/routes/(console)/project-[region]-[project]/messaging/providers/popoverContent.svelte @@ -1,30 +1,25 @@ -
    + {#each lines as line} -

    {@html line}

    +

    {@html line}

    {/each} {#if image} - + + + {/if} -
    + diff --git a/src/routes/(console)/verify-email/+page.ts b/src/routes/(console)/verify-email/+page.ts new file mode 100644 index 000000000..d97e73666 --- /dev/null +++ b/src/routes/(console)/verify-email/+page.ts @@ -0,0 +1,44 @@ +import { resolve } from '$app/paths'; +import { redirect } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; +import { Dependencies } from '$lib/constants'; +import { sdk } from '$lib/stores/sdk'; +import { addNotification } from '$lib/stores/notifications'; +import { VARS } from '$lib/system'; + +export const load: PageLoad = async ({ parent, depends, url }) => { + if (!VARS.EMAIL_VERIFICATION) { + redirect(303, resolve('/')); + } + + const { account } = await parent(); + depends(Dependencies.ACCOUNT); + + const user = url.searchParams.get('userId') ?? null; + const secret = url.searchParams.get('secret') ?? null; + + if (account && account.emailVerification === true) { + redirect(303, resolve('/')); + } else if (user && secret) { + try { + await sdk.forConsole.account.updateVerification({ + userId: user, + secret + }); + + addNotification({ + type: 'success', + message: 'Email has been verified', + timeout: 10000 // 10 seconds max, because if succeeded, loads takes some time! + }); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + } finally { + redirect(303, resolve('/')); + } + } + return {}; +}; diff --git a/src/routes/(console)/wizard/support/store.test.ts b/src/routes/(console)/wizard/support/store.test.ts new file mode 100644 index 000000000..a8b836983 --- /dev/null +++ b/src/routes/(console)/wizard/support/store.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it, beforeEach, vi } from 'vitest'; +import { isSupportOnline } from './store'; + +describe('isSupportOnline', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + // Helper function to create a mock date + const mockDate = (hour: number, day: number) => { + const mockDate = new Date(); + vi.spyOn(mockDate, 'getUTCDay').mockReturnValue(day); + vi.spyOn(mockDate, 'getUTCHours').mockReturnValue(hour); + vi.spyOn(global, 'Date').mockImplementation(() => mockDate); + return mockDate; + }; + + describe('weekday support hours (Monday-Friday)', () => { + it('should be online at 16:00 UTC (Monday)', () => { + mockDate(16, 1); // Monday, 16:00 UTC + expect(isSupportOnline()).toBe(true); + }); + + it('should be online at 20:00 UTC (Tuesday)', () => { + mockDate(20, 2); // Tuesday, 20:00 UTC + expect(isSupportOnline()).toBe(true); + }); + + it('should be online at 23:59 UTC (Wednesday)', () => { + mockDate(23, 3); // Wednesday, 23:59 UTC + expect(isSupportOnline()).toBe(true); + }); + + it('should be online at 23:00 UTC (Thursday)', () => { + mockDate(23, 4); // Thursday, 23:00 UTC + expect(isSupportOnline()).toBe(true); + }); + + it('should be online at 22:00 UTC (Friday)', () => { + mockDate(22, 5); // Friday, 22:00 UTC + expect(isSupportOnline()).toBe(true); + }); + }); + + describe('outside support hours on weekdays', () => { + it('should be offline at 15:59 UTC (Monday)', () => { + mockDate(15, 1); // Monday, 15:59 UTC + expect(isSupportOnline()).toBe(false); + }); + + it('should be offline at 00:00 UTC (Tuesday)', () => { + mockDate(0, 2); // Tuesday, 00:00 UTC + expect(isSupportOnline()).toBe(false); + }); + + it('should be offline at 10:00 UTC (Wednesday)', () => { + mockDate(10, 3); // Wednesday, 10:00 UTC + expect(isSupportOnline()).toBe(false); + }); + + it('should be offline at 14:00 UTC (Friday)', () => { + mockDate(14, 5); // Friday, 14:00 UTC + expect(isSupportOnline()).toBe(false); + }); + }); + + describe('weekends', () => { + it('should be offline on Saturday regardless of hour', () => { + mockDate(18, 6); // Saturday, 18:00 UTC (in support hours for weekdays) + expect(isSupportOnline()).toBe(false); + }); + + it('should be offline on Sunday regardless of hour', () => { + mockDate(0, 0); // Sunday, 00:00 UTC + expect(isSupportOnline()).toBe(false); + }); + + it('should be offline on Saturday at midnight', () => { + mockDate(0, 6); // Saturday, 00:00 UTC + expect(isSupportOnline()).toBe(false); + }); + + it('should be offline on Sunday at 16:00', () => { + mockDate(16, 0); // Sunday, 16:00 UTC + expect(isSupportOnline()).toBe(false); + }); + }); + + describe('edge cases', () => { + it('should handle boundary at 15:59 (just before support hours)', () => { + mockDate(15, 1); // Monday, 15:59 UTC + expect(isSupportOnline()).toBe(false); + }); + + it('should handle boundary at 16:00 (start of support hours)', () => { + mockDate(16, 1); // Monday, 16:00 UTC + expect(isSupportOnline()).toBe(true); + }); + + it('should handle late night hours on Friday', () => { + mockDate(23, 5); // Friday, 23:00 UTC + expect(isSupportOnline()).toBe(true); + }); + }); +}); diff --git a/src/routes/(console)/wizard/support/store.ts b/src/routes/(console)/wizard/support/store.ts index 535579f79..06f671b6a 100644 --- a/src/routes/(console)/wizard/support/store.ts +++ b/src/routes/(console)/wizard/support/store.ts @@ -20,15 +20,18 @@ export function isSupportOnline() { const day = currentDate.getUTCDay(); const hour = currentDate.getUTCHours(); - if (day === 1 || day === 6) { + // Support is offline on weekends (Sunday = 0, Saturday = 6) + if (day === 0 || day === 6) { return false; } - if (hour < 14) { - return false; + // Support hours are 16:00 UTC to 23:59 UTC on weekdays + // This roughly covers 09:00 PT to 17:00 PT (depending on PST/PDT) + if (hour >= 16) { + return true; } - return true; + return false; } export const showSupportModal = writable(false); diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 46b84cc98..5ee94b650 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -6,10 +6,10 @@ import { redirect } from '@sveltejs/kit'; import { Dependencies } from '$lib/constants'; import type { LayoutLoad } from './$types'; import { redirectTo } from './store'; -import { base } from '$app/paths'; +import { base, resolve } from '$app/paths'; import type { Account } from '$lib/stores/user'; import type { AppwriteException } from '@appwrite.io/console'; -import { isCloud } from '$lib/system'; +import { isCloud, VARS } from '$lib/system'; import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect'; export const ssr = false; @@ -29,6 +29,15 @@ export const load: LayoutLoad = async ({ depends, url, route }) => { } if (account) { + if (isCloud && !account.emailVerification && VARS.EMAIL_VERIFICATION) { + const isConsoleRoute = route.id?.startsWith('/(console)'); + const isVerifyEmailPage = url.pathname === resolve('/verify-email'); + + if (isConsoleRoute && !isVerifyEmailPage) { + redirect(303, resolve('/verify-email')); + } + } + return { account: account, organizations: !isCloud diff --git a/static/icons/dark/color/resend.svg b/static/icons/dark/color/resend.svg new file mode 100644 index 000000000..4371f60b3 --- /dev/null +++ b/static/icons/dark/color/resend.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/color/tanstack.svg b/static/icons/dark/color/tanstack.svg new file mode 100644 index 000000000..5823a237b --- /dev/null +++ b/static/icons/dark/color/tanstack.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/color/resend.svg b/static/icons/light/color/resend.svg new file mode 100644 index 000000000..67af6bf6d --- /dev/null +++ b/static/icons/light/color/resend.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/color/tanstack.svg b/static/icons/light/color/tanstack.svg new file mode 100644 index 000000000..76011176f --- /dev/null +++ b/static/icons/light/color/tanstack.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/logos/apple-touch-icon-120x120.png b/static/logos/apple-touch-icon-120x120.png new file mode 100644 index 000000000..b5e9c5ea1 Binary files /dev/null and b/static/logos/apple-touch-icon-120x120.png differ diff --git a/static/logos/apple-touch-icon-152x152.png b/static/logos/apple-touch-icon-152x152.png new file mode 100644 index 000000000..93a40e457 Binary files /dev/null and b/static/logos/apple-touch-icon-152x152.png differ diff --git a/static/logos/apple-touch-icon-167x167.png b/static/logos/apple-touch-icon-167x167.png new file mode 100644 index 000000000..9083bef9d Binary files /dev/null and b/static/logos/apple-touch-icon-167x167.png differ diff --git a/static/logos/apple-touch-icon-180x180.png b/static/logos/apple-touch-icon-180x180.png new file mode 100644 index 000000000..8e35bc723 Binary files /dev/null and b/static/logos/apple-touch-icon-180x180.png differ