Merge branch 'main' into fix-dat-701

This commit is contained in:
Darshan
2025-09-25 13:27:24 +05:30
140 changed files with 3634 additions and 732 deletions
+3 -1
View File
@@ -32,7 +32,9 @@ export default ts.config(
'svelte/no-at-html-tags': 'off',
'svelte/no-useless-mustaches': 'off',
'svelte/no-reactive-reassign': 'off',
'svelte/no-reactive-literals': 'off'
'svelte/no-reactive-literals': 'off',
// TODO: @itznotabug, this requires a big refactor!
'svelte/no-navigation-without-resolve': 'off'
}
},
{
+4 -4
View File
@@ -22,11 +22,11 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@d8326d0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@d8326d0",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1",
"@faker-js/faker": "^9.9.0",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
@@ -54,7 +54,7 @@
"@melt-ui/svelte": "^0.86.5",
"@playwright/test": "^1.51.1",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.36.2",
"@sveltejs/kit": "^2.42.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
+47 -40
View File
@@ -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@6031134
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562
'@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@d8326d0
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@d8326d0(svelte@5.25.3)
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1(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@d8326d0
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@d8326d0(svelte@5.25.3)
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1(svelte@5.25.3)
'@faker-js/faker':
specifier: ^9.9.0
version: 9.9.0
@@ -34,7 +34,7 @@ importers:
version: 2.11.8
'@sentry/sveltekit':
specifier: ^8.38.0
version: 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.36.3(@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)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
version: 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@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)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
'@stripe/stripe-js':
specifier: ^3.5.0
version: 3.5.0
@@ -101,10 +101,10 @@ importers:
version: 1.51.1
'@sveltejs/adapter-static':
specifier: ^3.0.8
version: 3.0.8(@sveltejs/kit@2.36.3(@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)))
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)))
'@sveltejs/kit':
specifier: ^2.36.2
version: 2.36.3(@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))
specifier: ^2.42.1
version: 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))
'@sveltejs/vite-plugin-svelte':
specifier: ^5.0.3
version: 5.0.3(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@6031134':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562}
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@d8326d0':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@d8326d0}
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1}
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@d8326d0':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@d8326d0}
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1}
version: 2.0.0-RC.2
peerDependencies:
svelte: ^4.0.0
@@ -352,8 +352,8 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.28.3':
resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==}
'@babel/parser@7.28.4':
resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==}
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.2':
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
'@babel/types@7.28.4':
resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
engines: {node: '>=6.9.0'}
'@csstools/color-helpers@5.0.2':
@@ -1264,8 +1264,8 @@ packages:
peerDependencies:
'@sveltejs/kit': ^2.0.0
'@sveltejs/kit@2.36.3':
resolution: {integrity: sha512-MVzwZz1GFznEQbL3f0i2v9AIc3lZH01izQj6XfIrthmZAwOzyXJCgXbLRss8vk//HfYsE4w6Tz+ukbf3rScPNQ==}
'@sveltejs/kit@2.42.1':
resolution: {integrity: sha512-SoWdb/OxEKHMlXZ78eNZa4pR1YdOGnUfcFj5NylJv+ZaTrnHK5xapw+7RJP2MiS1D4T+sEH25/tmMtmEl8p6VA==}
engines: {node: '>=18.13'}
hasBin: true
peerDependencies:
@@ -2607,6 +2607,9 @@ 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.7:
resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==}
engines: {node: '>=12'}
@@ -3050,8 +3053,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
seroval-plugins@1.3.2:
resolution: {integrity: sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==}
seroval-plugins@1.3.3:
resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==}
engines: {node: '>=10'}
peerDependencies:
seroval: ^1.0
@@ -3700,13 +3703,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562': {}
'@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@d8326d0(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3719,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@d8326d0(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1(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
@@ -3822,9 +3825,9 @@ snapshots:
dependencies:
'@babel/types': 7.27.0
'@babel/parser@7.28.3':
'@babel/parser@7.28.4':
dependencies:
'@babel/types': 7.28.2
'@babel/types': 7.28.4
'@babel/runtime@7.27.0':
dependencies:
@@ -3853,7 +3856,7 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/types@7.28.2':
'@babel/types@7.28.4':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
@@ -4671,14 +4674,14 @@ snapshots:
magic-string: 0.30.7
svelte: 5.25.3
'@sentry/sveltekit@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.36.3(@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)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))':
'@sentry/sveltekit@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@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)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))':
dependencies:
'@sentry/core': 8.55.0
'@sentry/node': 8.55.0
'@sentry/opentelemetry': 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)
'@sentry/svelte': 8.55.0(svelte@5.25.3)
'@sentry/vite-plugin': 2.22.6
'@sveltejs/kit': 2.36.3(@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))
'@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))
magic-string: 0.30.7
magicast: 0.2.8
sorcery: 1.0.0
@@ -4750,11 +4753,11 @@ snapshots:
dependencies:
acorn: 8.15.0
'@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.36.3(@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)))':
'@sveltejs/adapter-static@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)))':
dependencies:
'@sveltejs/kit': 2.36.3(@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))
'@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))
'@sveltejs/kit@2.36.3(@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))':
'@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))':
dependencies:
'@standard-schema/spec': 1.0.0
'@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0)
@@ -5121,7 +5124,7 @@ snapshots:
'@vue/compiler-core@3.5.13':
dependencies:
'@babel/parser': 7.28.3
'@babel/parser': 7.28.4
'@vue/shared': 3.5.13
entities: 4.5.0
estree-walker: 2.0.2
@@ -5134,13 +5137,13 @@ snapshots:
'@vue/compiler-sfc@3.5.13':
dependencies:
'@babel/parser': 7.28.3
'@babel/parser': 7.28.4
'@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.18
magic-string: 0.30.19
postcss: 8.5.6
source-map-js: 1.2.1
@@ -6282,6 +6285,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magic-string@0.30.19:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magic-string@0.30.7:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -6703,7 +6710,7 @@ snapshots:
semver@7.7.1: {}
seroval-plugins@1.3.2(seroval@1.3.2):
seroval-plugins@1.3.3(seroval@1.3.2):
dependencies:
seroval: 1.3.2
@@ -6786,7 +6793,7 @@ snapshots:
dependencies:
csstype: 3.1.3
seroval: 1.3.2
seroval-plugins: 1.3.2(seroval@1.3.2)
seroval-plugins: 1.3.3(seroval@1.3.2)
solid-swr-store@0.10.7(solid-js@1.9.5)(swr-store@0.10.6):
dependencies:
+4 -3
View File
@@ -6,7 +6,7 @@ import { user } from '$lib/stores/user';
import { ENV, MODE, VARS, isCloud } from '$lib/system';
import { AppwriteException } from '@appwrite.io/console';
import { browser } from '$app/environment';
import { getReferrerAndUtmSource } from '$lib/helpers/utm';
import { getReferrerAndUtmSource, getTrackedQueryParams } from '$lib/helpers/utm';
function plausible(domain: string): AnalyticsPlugin {
if (!browser) return { name: 'analytics-plugin-plausible' };
@@ -65,7 +65,7 @@ export function trackEvent(name: string, data: object = null): void {
};
}
data = { ...data, ...getReferrerAndUtmSource() };
data = { ...data, ...getReferrerAndUtmSource(), ...getTrackedQueryParams() };
if (ENV.DEV || ENV.PREVIEW) {
console.debug(`[Analytics] Event ${name} ${path}`, data);
@@ -429,5 +429,6 @@ export enum Submit {
RecordUpdate = 'submit_dns_record_update',
RecordDelete = 'submit_dns_record_delete',
SearchClear = 'submit_clear_search',
FrameworkDetect = 'submit_framework_detect'
FrameworkDetect = 'submit_framework_detect',
TestimonialView = 'testimonial_view'
}
+7 -4
View File
@@ -21,13 +21,16 @@ export const timer: Action<HTMLElement, Props> = (node, props) => {
}
function step() {
const diffInSeconds = Math.floor((new Date().getTime() - startDate.getTime()) / 1000);
const minutes = Math.floor(diffInSeconds / 60);
const elapsedSeconds = Math.max(
0,
Math.floor((new Date().getTime() - startDate.getTime()) / 1000)
);
const minutes = Math.floor(elapsedSeconds / 60);
if (minutes > 0) {
const seconds = diffInSeconds % 60;
const seconds = elapsedSeconds % 60;
node.textContent = `${minutes}m ${seconds}s`;
} else {
node.textContent = calculateTime(diffInSeconds);
node.textContent = calculateTime(elapsedSeconds);
}
frame = window.requestAnimationFrame(step);
}
@@ -0,0 +1,79 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { get } from 'svelte/store';
import { page } from '$app/state';
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Dependencies } from '$lib/constants';
import { onMount } from 'svelte';
let { show = $bindable(false) } = $props();
let creating = $state(false);
let emailSent = $state(false);
let cleanUrl = $derived(page.url.origin + page.url.pathname);
async function onSubmit() {
if (creating) return;
creating = true;
try {
await sdk.forConsole.account.createVerification({ url: cleanUrl });
addNotification({ message: 'Verification email has been sent', type: 'success' });
emailSent = true;
show = false;
} catch (error) {
addNotification({ message: error.message, type: 'error' });
} finally {
creating = false;
}
}
async function updateEmailVerification() {
const searchParams = page.url.searchParams;
const userId = searchParams.get('userId');
const secret = searchParams.get('secret');
if (userId && secret) {
try {
await sdk.forConsole.account.updateVerification({ userId, secret });
addNotification({
message: 'Email verified successfully',
type: 'success'
});
await Promise.all([
invalidate(Dependencies.ACCOUNT),
invalidate(Dependencies.FACTORS)
]);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
}
onMount(() => {
updateEmailVerification();
});
</script>
<Modal bind:show title="Send verification email" {onSubmit}>
<Card.Base variant="secondary" padding="s">
<Layout.Stack gap="m">
<Typography.Text gap="m">
To continue using Appwrite Cloud, please verify your email address. An email will be
sent to <Typography.Text variant="m-600" style="display: inline;"
>{get(user)?.email}</Typography.Text>
</Typography.Text>
</Layout.Stack>
</Card.Base>
<svelte:fragment slot="footer">
<Button submit disabled={creating}>{emailSent ? 'Resend email' : 'Send email'}</Button>
</svelte:fragment>
</Modal>
@@ -1,55 +1,26 @@
<script lang="ts">
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { Typography } from '@appwrite.io/pink-svelte';
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { user } from '$lib/stores/user';
import { wizard } from '$lib/stores/wizard';
import { page } from '$app/state';
const { emailBannerClosed, onEmailBannerClose } = $props<{
emailBannerClosed: boolean;
onEmailBannerClose: (closed: boolean) => void;
}>();
const isOnOnboarding = $derived(page.url?.pathname?.includes('/console/onboarding'));
import SendVerificationEmailModal from '../account/sendVerificationEmailModal.svelte';
import { page } from '$app/stores';
import { wizard, isNewWizardStatusOpen } from '$lib/stores/wizard';
import { isCloud } from '$lib/system';
const hasUser = $derived(!!$user);
const needsEmailVerification = $derived(hasUser && !$user.emailVerification);
const shouldShowNotificationBanner = $derived.by(() =>
shouldShowNotification('email-verification-banner')
);
const wizardNotActive = $derived(!$wizard.show && !$wizard.cover);
const bannerNotClosed = $derived(!emailBannerClosed);
const notOnOnboarding = $derived(!isOnOnboarding);
const notOnOnboarding = $derived(!$page.route.id.includes('/onboarding'));
const notOnWizard = $derived(!$wizard.show && !$isNewWizardStatusOpen);
const shouldShowEmailBanner = $derived(
hasUser &&
needsEmailVerification &&
shouldShowNotificationBanner &&
wizardNotActive &&
bannerNotClosed &&
notOnOnboarding
isCloud && hasUser && needsEmailVerification && notOnOnboarding && notOnWizard
);
function navigateToAccount() {
goto(`${base}/account`);
}
function handleDismiss() {
onEmailBannerClose(true);
hideNotification('email-verification-banner', { coolOffPeriod: 24 * 365 * 100 });
}
let showSendVerification = $state(false);
</script>
{#if shouldShowEmailBanner}
<HeaderAlert
type="warning"
title="Your email address needs to be verified"
dismissible
on:dismiss={handleDismiss}>
<HeaderAlert type="warning" title="Your email address needs to be verified">
<svelte:fragment>
To avoid losing access to your projects, make sure <Typography.Text
variant="m-500"
@@ -57,7 +28,9 @@
verification will be required soon.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button secondary size="s" on:click={navigateToAccount}>Update email address</Button>
<Button secondary size="s" on:click={() => (showSendVerification = true)}
>Verify email</Button>
</svelte:fragment>
</HeaderAlert>
<SendVerificationEmailModal bind:show={showSendVerification} />
{/if}
@@ -23,7 +23,7 @@
}
</script>
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes('/console/account')}
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes(base + '/account')}
<GradientBanner on:close={handleClose}>
<Layout.Stack
gap="m"
@@ -1,7 +1,7 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { formatNum } from '$lib/helpers/string';
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
import { Card, Layout, Tabs, Typography } from '@appwrite.io/pink-svelte';
export let downgrade = false;
@@ -9,29 +9,22 @@
let selectedTab: Tier = BillingPlan.FREE;
$: plan = $plansInfo.get(selectedTab);
const allTiers: Tier[] = [BillingPlan.FREE, BillingPlan.PRO, BillingPlan.SCALE];
$: visibleTiers = allTiers.filter((tier) => tier !== BillingPlan.SCALE);
</script>
<Card.Base>
<Layout.Stack>
<Tabs.Root stretch let:root>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.FREE}
on:click={() => (selectedTab = BillingPlan.FREE)}>
{tierFree.name}
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.PRO}
on:click={() => (selectedTab = BillingPlan.PRO)}>
{tierPro.name}
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.SCALE}
on:click={() => (selectedTab = BillingPlan.SCALE)}>
{tierScale.name}
</Tabs.Item.Button>
{#each visibleTiers as tier}
<Tabs.Item.Button
{root}
active={selectedTab === tier}
on:click={() => (selectedTab = tier)}>
{tierToPlan(tier).name}
</Tabs.Item.Button>
{/each}
</Tabs.Root>
<Typography.Text variant="m-600">{plan.name} plan</Typography.Text>
@@ -14,10 +14,13 @@
$: plans = Object.values(page.data.plans.plans) as Plan[];
$: currentPlanInList = plans.some((plan) => plan.$id === $currentPlan?.$id);
// experiment to remove scale plan temporarily
$: plansWithoutScale = plans.filter((plan) => plan.$id != BillingPlan.SCALE);
</script>
<Layout.Stack>
{#each plans as plan}
{#each plansWithoutScale as plan}
<LabelCard
name="plan"
bind:group={billingPlan}
+33 -10
View File
@@ -18,14 +18,15 @@
import { BottomSheet } from '$lib/components';
import { isSmallViewport } from '$lib/stores/viewport';
import { isCloud } from '$lib/system';
import { goto } from '$app/navigation';
import { goto, beforeNavigate } from '$app/navigation';
import { base } from '$app/paths';
import { currentPlan, newOrgModal, organization } from '$lib/stores/organization';
import { Click, trackEvent } from '$lib/actions/analytics';
import { type Models, Query } from '@appwrite.io/console';
import { ID, type Models, Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import { BillingPlan } from '$lib/constants';
import { onDestroy } from 'svelte';
type Organization = {
name: string;
@@ -34,11 +35,13 @@
isSelected: boolean;
};
const menubarBuilder = createMenubar();
const {
elements: { menubar },
builders: { createMenu }
} = createMenubar();
} = menubarBuilder;
const orgMenuBuilder = createMenu();
const {
elements: {
trigger: triggerOrganizations,
@@ -46,8 +49,9 @@
item: itemOrganizations,
separator: separatorOrganizations
},
builders: { createSubmenu: createSubmenuOrganizations, createMenuRadioGroup }
} = createMenu();
builders: { createSubmenu: createSubmenuOrganizations, createMenuRadioGroup },
states: orgMenuStates
} = orgMenuBuilder;
const {
elements: { radioGroup: radioGroupOrganizations }
@@ -57,14 +61,16 @@
elements: { subMenu: subMenuOrganizations, subTrigger: subTriggerOrganizations }
} = createSubmenuOrganizations();
const projectsMenuBuilder = createMenu();
const {
elements: {
trigger: triggerProjects,
menu: menuProjects,
item: itemProjects,
separator: separatorProjects
}
} = createMenu();
},
states: projectsMenuStates
} = projectsMenuBuilder;
let isLoadingProjects = false;
let loadedProjects: Models.ProjectList = { total: 0, projects: [] };
@@ -76,6 +82,23 @@
let organisationBottomSheetOpen = false;
let projectsBottomSheet: Promise<SheetMenu | null> = null;
function closeAllMenus() {
if (orgMenuStates?.open) {
orgMenuStates.open.set(false);
}
if (projectsMenuStates?.open) {
projectsMenuStates.open.set(false);
}
projectsBottomSheetOpen = false;
organisationBottomSheetOpen = false;
}
beforeNavigate(closeAllMenus);
onDestroy(closeAllMenus);
function createOrg() {
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
if (isCloud) {
@@ -105,6 +128,8 @@
async function createProjectsBottomSheet(
organization: Organization
): Promise<SheetMenu | null> {
const isOnProjects = page.route.id.includes('project-[region]-[project]');
if (!isOnProjects) return null;
isLoadingProjects = true;
@@ -212,12 +237,10 @@
? $currentPlan.name
: selectedOrg?.tierName; // fallback
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}`;
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}-${ID.unique()}`;
$: organizationId = currentProject?.teamId;
$: isOnProjects = page.route.id.includes('project-[region]-[project]');
$: shouldReloadProjects = isLoadingProjects
? false
: currentProject && loadedProjects.projects.length
+10 -1
View File
@@ -62,7 +62,7 @@
if (isSuccess) {
await invalidate(Dependencies.ROWS);
$spreadsheetRenderKey = hash(Date.now().toString());
spreadsheetRenderKey.set(hash(Date.now().toString()));
}
}
@@ -90,6 +90,15 @@
}
}
if (tableId && tableName === null) {
importItems.update((items) => {
const next = new Map(items);
next.delete(importData.$id);
return next;
});
return;
}
importItems.update((items) => {
const existing = items.get(importData.$id);
@@ -32,6 +32,6 @@
{#if dnsData}
<ActionMenu.Item.Anchor leadingIcon={IconDocumentText} href={dnsData}>
DNS Records
DNS records
</ActionMenu.Item.Anchor>
{/if}
+60 -3
View File
@@ -6,10 +6,11 @@
InputText,
InputTags,
InputSelectCheckbox,
InputDateTime
InputDateTime,
InputPoint
} from '$lib/elements/forms';
import { onMount, createEventDispatcher } from 'svelte';
import { operators, addFilter, queries, tags } from './store';
import { operators, addFilter, queries, tags, ValidOperators } from './store';
import type { Column } from '$lib/helpers/types';
import type { Writable } from 'svelte/store';
import { TagList } from '.';
@@ -18,6 +19,7 @@
let {
value = $bindable(null),
distanceValue = $bindable(null),
columns,
columnId = $bindable(null),
arrayValues = $bindable([]),
@@ -27,6 +29,7 @@
// We cast to any to not cause type errors in the input components
/* eslint @typescript-eslint/no-explicit-any: 'off' */
value?: any;
distanceValue?: number | null;
columns: Writable<Column[]>;
columnId?: string | null;
arrayValues?: string[];
@@ -61,21 +64,57 @@
}));
});
// Check if the current operator is a distance-based operator
let isDistanceOperator = $derived(
operatorKey &&
[
ValidOperators.DistanceEqual,
ValidOperators.DistanceNotEqual,
ValidOperators.DistanceGreaterThan,
ValidOperators.DistanceLessThan
].includes(operatorKey as ValidOperators)
);
onMount(() => {
value = column?.array ? [] : null;
if (column?.type === 'datetime') {
const now = new Date();
value = now.toISOString().slice(0, 16);
}
// Initialize spatial data with default values
if (column?.type === 'point') {
value = [0, 0];
} else if (column?.type === 'linestring') {
value = [
[0, 0],
[1, 1]
];
} else if (column?.type === 'polygon') {
value = [
[
[0, 0],
[1, 1],
[2, 2],
[0, 0]
]
];
}
});
const dispatch = createEventDispatcher<{ clear: void; apply: { applied: number } }>();
function addFilterAndReset() {
addFilter(columnsArray, columnId, operatorKey, value, arrayValues);
// For distance operators, pass the distance as a separate parameter
if (isDistanceOperator && distanceValue !== null && value !== null) {
addFilter(columnsArray, columnId, operatorKey, value, arrayValues, distanceValue);
} else {
addFilter(columnsArray, columnId, operatorKey, value, arrayValues);
}
columnId = null;
operatorKey = null;
value = null;
distanceValue = null;
arrayValues = [];
dispatch('apply', { applied: appliedTags.length });
if (singleCondition) {
@@ -143,11 +182,29 @@
{#key value}
<InputDateTime id="value" bind:value step={60} type="datetime-local" />
{/key}
{:else if column.type === 'point' || column.type === 'linestring' || column.type === 'polygon'}
<InputPoint
values={value || [0, 0]}
onChangePoint={(index, newValue) => {
if (!value) value = [0, 0];
value[index] = newValue;
}} />
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
{/if}
</ul>
{/if}
{#if isDistanceOperator}
<div class="u-margin-block-start-8">
<InputNumber
id="distance"
bind:value={distanceValue}
placeholder="Enter distance"
step={0.001}
required />
</div>
{/if}
{/if}
{#if !singleCondition}
<Button text disabled={isDisabled} class="u-margin-block-start-4" submit>
+23 -1
View File
@@ -36,6 +36,7 @@
/* eslint @typescript-eslint/no-explicit-any: 'off' */
let value: any = null;
let distanceValue: number | null = null;
let selectedColumn: string | null = null;
let operatorKey: string | null = null;
let arrayValues: string[] = [];
@@ -46,6 +47,16 @@
$: filtersAppliedCount = $tags.length;
// Check if the current operator is a distance-based operator
$: isDistanceOperator =
operatorKey &&
[
ValidOperators.DistanceEqual,
ValidOperators.DistanceNotEqual,
ValidOperators.DistanceGreaterThan,
ValidOperators.DistanceLessThan
].includes(operatorKey as ValidOperators);
beforeNavigate(() => {
showFiltersDesktop = false;
showFiltersMobile = false;
@@ -54,6 +65,8 @@
function clearAll() {
selectedColumn = null;
value = null;
distanceValue = null;
queries.clearAll();
if (clearOnClick) {
trackEvent(Submit.FilterClear, { source: analyticsSource });
@@ -73,9 +86,15 @@
value ||
arrayValues.length)
) {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
// For distance operators, pass the distance as a separate parameter
if (isDistanceOperator && distanceValue !== null && value !== null) {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues, distanceValue);
} else {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
}
selectedColumn = null;
value = null;
distanceValue = null;
operatorKey = null;
arrayValues = [];
}
@@ -97,6 +116,7 @@
$: if (!showFiltersDesktop && !showFiltersMobile) {
selectedColumn = null;
value = null;
distanceValue = null;
operatorKey = null;
arrayValues = [];
}
@@ -163,6 +183,7 @@
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:distanceValue
bind:arrayValues
{columns}
{singleCondition}
@@ -225,6 +246,7 @@
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:distanceValue
bind:arrayValues
{singleCondition}
on:apply={afterApply}
@@ -49,6 +49,8 @@ export function addFilterAndApply(
addStatusCodeFilter(value, colId, columns);
} else if (colId === '$createdAt' || colId === '$updatedAt' || colId === 'buildDuration') {
addDateFilter(value, colId, columns);
} else if (colId === 'duration') {
addDurationFilter(value, colId, columns);
} else {
addFilter(columns, colId, operator, value, arrayValues);
}
@@ -75,3 +77,7 @@ export function addDateFilter(value: string, colId: string, columns: Column[]) {
export function addSizeFilter(value: string, colId: string, columns: Column[]) {
addFilter(columns, colId, ValidOperators.GreaterThanOrEqual, value);
}
export function addDurationFilter(value: string, colId: string, columns: Column[]) {
addFilter(columns, colId, ValidOperators.GreaterThan, value);
}
+4 -12
View File
@@ -61,20 +61,12 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) {
const col = columns.find((c) => c.id === filter.id);
const timeTag = get(tags).find((tag) => tag.tag.includes(`**${filter.title}**`));
if (timeTag) {
const now = new Date();
const diff = now.getTime() - new Date(timeTag.value as string).getTime();
const ranges = col.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
const timeRange = ranges.find((range) => range.value === timeTag.value);
if (timeRange) {
const newTag = {
tag: `**${filter.title}** is **${dateRange.label}**`,
value: timeTag.value
tag: `**${filter.title}** is **${timeRange.label}**`,
value: timeRange.value
};
cleanOldTags(filter?.title);
+155 -14
View File
@@ -5,6 +5,7 @@ import { Query } from '@appwrite.io/console';
import { toLocaleDateTime } from '$lib/helpers/date';
import { derived, get, writable } from 'svelte/store';
import type { Column, ColumnType } from '$lib/helpers/types';
import { isSpatialType } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store';
export type TagValue = {
tag: string;
@@ -13,7 +14,7 @@ export type TagValue = {
export type Operator = {
toTag: (column: string, input?: string | number | string[], type?: string) => TagValue;
toQuery: (column: string, input?: string | number | string[]) => string;
toQuery: (column: string, input?: string | number | string[], distance?: number) => string;
types: ColumnType[];
hideInput?: boolean;
};
@@ -35,13 +36,14 @@ function initQueries(initialValue = new Map<TagValue, string>()) {
operator: Operator;
column: Column;
value: string | number | string[];
distance?: number;
};
function addFilter({ column, operator, value }: AddFilterArgs) {
function addFilter({ column, operator, value, distance }: AddFilterArgs) {
queries.update((map) => {
map.set(
operator.toTag(column.title, value, column?.type),
operator.toQuery(column.id, value)
operator.toQuery(column.id, value, distance)
);
return map;
});
@@ -101,15 +103,27 @@ export function addFilter(
columnId: string,
operatorKey: string,
value: any, // We cast to any to not cause type errors in the input components
arrayValues: string[] = []
arrayValues: string[] = [],
distance?: number
) {
const operator = operatorKey ? operators[operatorKey] : null;
const column = columns.find((c) => c.id === columnId) as Column;
if (!column || !operator) return;
if (column.array) {
queries.addFilter({ column, operator, value: arrayValues });
queries.addFilter({ column, operator, value: arrayValues, distance });
} else {
queries.addFilter({ column, operator, value: value ?? '' });
const isSpatialArrayWrappingRequired =
isSpatialType(column) &&
[
ValidOperators.Contains,
ValidOperators.Equal,
ValidOperators.NotEqual,
ValidOperators.NotContains
].includes(operatorKey as ValidOperators);
const preparedValue =
isSpatialArrayWrappingRequired && value != null ? [value] : (value ?? '');
queries.addFilter({ column, operator, value: preparedValue, distance });
}
}
@@ -124,7 +138,20 @@ export enum ValidOperators {
NotEqual = 'not equal',
IsNotNull = 'is not null',
IsNull = 'is null',
Contains = 'contains'
Contains = 'contains',
NotContains = 'not contains',
Crosses = 'crosses',
NotCrosses = 'not crosses',
DistanceEqual = 'distance equal',
DistanceNotEqual = 'distance not equal',
DistanceGreaterThan = 'distance greater than',
DistanceLessThan = 'distance less than',
Intersects = 'intersects',
NotIntersects = 'not intersects',
Overlaps = 'overlaps',
NotOverlaps = 'not overlaps',
Touches = 'touches',
NotTouches = 'not touches'
}
export enum ValidTypes {
@@ -134,13 +161,16 @@ export enum ValidTypes {
Boolean = 'boolean',
Datetime = 'datetime',
Relationship = 'relationship',
Enum = 'enum'
Enum = 'enum',
Point = 'point',
Line = 'linestring',
Polygon = 'polygon'
}
const operatorsDefault = new Map<
ValidOperators,
{
query: (col: string, input: string | number | string[]) => string;
query: (col: string, input: string | number | string[], distance?: number) => string;
types: ColumnType[];
hideInput?: boolean;
}
@@ -184,7 +214,10 @@ const operatorsDefault = new Map<
ValidTypes.Integer,
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Enum
ValidTypes.Enum,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
]
}
],
@@ -192,7 +225,15 @@ const operatorsDefault = new Map<
ValidOperators.NotEqual,
{
query: Query.notEqual,
types: [ValidTypes.String, ValidTypes.Integer, ValidTypes.Double, ValidTypes.Boolean]
types: [
ValidTypes.String,
ValidTypes.Integer,
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
]
}
],
[
@@ -205,7 +246,10 @@ const operatorsDefault = new Map<
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Datetime,
ValidTypes.Relationship
ValidTypes.Relationship,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
],
hideInput: true
}
@@ -220,7 +264,10 @@ const operatorsDefault = new Map<
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Datetime,
ValidTypes.Relationship
ValidTypes.Relationship,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
],
hideInput: true
}
@@ -235,9 +282,103 @@ const operatorsDefault = new Map<
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Datetime,
ValidTypes.Enum
ValidTypes.Enum,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
]
}
],
[
ValidOperators.NotContains,
{
query: Query.contains,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.Crosses,
{
query: Query.crosses,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.NotCrosses,
{
query: Query.notCrosses,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.DistanceEqual,
{
query: Query.distanceEqual,
types: [ValidTypes.Point]
}
],
[
ValidOperators.DistanceNotEqual,
{
query: Query.distanceNotEqual,
types: [ValidTypes.Point]
}
],
[
ValidOperators.DistanceGreaterThan,
{
query: Query.distanceGreaterThan,
types: [ValidTypes.Point]
}
],
[
ValidOperators.DistanceLessThan,
{
query: Query.distanceLessThan,
types: [ValidTypes.Point]
}
],
[
ValidOperators.Intersects,
{
query: Query.intersects,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.NotIntersects,
{
query: Query.notIntersects,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.Overlaps,
{
query: Query.overlaps,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.NotOverlaps,
{
query: Query.notOverlaps,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.Touches,
{
query: Query.touches,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.NotTouches,
{
query: Query.notTouches,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
]
]);
+7 -7
View File
@@ -11,7 +11,6 @@
Tabs,
Typography
} from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
let {
selectedLog,
@@ -23,29 +22,30 @@
let requestTab: 'parameters' | 'headers' = $state('parameters');
let parameters = $state([]);
const href =
product === 'site'
? 'https://appwrite.io/docs/products/sites/logs#log-details'
: 'https://appwrite.io/docs/products/functions/develop#logging';
onMount(() => {
// Make parameters reactive to selectedLog changes
let parameters = $derived.by(() => {
try {
// Add dummy base URL to parse relative paths
const url = new URL(selectedLog.requestPath, 'http://dummy.local');
if (url.search) {
parameters = Array.from(url.searchParams.entries()).map(([name, value]) => ({
return Array.from(url.searchParams.entries()).map(([name, value]) => ({
name,
value: decodeURIComponent(value)
}));
}
return [];
} catch (error) {
parameters = [];
return [];
}
});
onMount(() => {
// Update requestTab when parameters or selectedLog changes
$effect(() => {
if (parameters?.length) {
requestTab = 'parameters';
} else if (selectedLog.requestHeaders?.length) {
+105 -62
View File
@@ -1,21 +1,39 @@
<script context="module" lang="ts">
let inputDigitFields: InputDigits;
export async function verify(challenge: Models.MfaChallenge, code: string) {
export async function verify(
challenge: Models.MfaChallenge,
code: string,
challengeType: AuthenticationFactor = AuthenticationFactor.Totp,
factors?: Models.MfaFactors & { recoveryCode: boolean }
) {
try {
if (challenge === null) {
if (!challenge) {
// Validate that the challengeType is actually enabled
if (factors) {
const factorMap = {
[AuthenticationFactor.Totp]: factors.totp,
[AuthenticationFactor.Email]: factors.email,
[AuthenticationFactor.Phone]: factors.phone,
[AuthenticationFactor.Recoverycode]: factors.recoveryCode
};
if (!factorMap[challengeType]) {
throw new Error(`Authentication factor ${challengeType} is not enabled`);
}
}
challenge = await sdk.forConsole.account.createMFAChallenge({
factor: AuthenticationFactor.Totp
factor: challengeType
});
}
await sdk.forConsole.account.updateMFAChallenge({
challengeId: challenge.$id,
otp: code
});
await invalidate(Dependencies.ACCOUNT);
trackEvent(Submit.AccountLogin, { mfa_used: true });
} catch (error) {
inputDigitFields?.clearInputsAndRefocus();
trackError(error, Submit.AccountLogin);
throw error;
}
@@ -24,18 +42,17 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invalidate } from '$app/navigation';
import { Button, InputDigits, InputText } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
import { Button, InputText, InputDigits } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { Icon, Typography } from '@appwrite.io/pink-svelte';
import { IconChatAlt, IconDeviceMobile, IconMail } from '@appwrite.io/pink-icons-svelte';
export let factors: Models.MfaFactors & { recoveryCode: boolean };
/** If true, the form will be submitted automatically when the code is entered. */
export let showVerifyButton: boolean = true;
export let disabled: boolean = false;
export let challenge: Models.MfaChallenge;
@@ -43,13 +60,24 @@
let challengeType: AuthenticationFactor;
const enabledFactors = Object.entries(factors).filter(([_, enabled]) => enabled);
// Get enabled non-recovery factors for main factors
$: enabledMainFactors = [
factors.totp && 'totp',
factors.email && 'email',
factors.phone && 'phone'
].filter(Boolean);
async function createChallenge(factor: AuthenticationFactor) {
disabled = true;
challengeType = factor;
code = '';
try {
challenge = await sdk.forConsole.account.createMFAChallenge({ factor });
if (
factor !== AuthenticationFactor.Totp &&
factor !== AuthenticationFactor.Recoverycode
) {
challenge = await sdk.forConsole.account.createMFAChallenge({ factor });
}
} catch (error) {
addNotification({
type: 'error',
@@ -60,57 +88,70 @@
}
}
export async function verifyCurrent() {
try {
return await verify(challenge, code, challengeType, factors);
} catch (error) {
// Clear input fields on verification failure
code = '';
throw error;
}
}
onMount(async () => {
const enabledNonRecoveryFactors = enabledFactors.filter(
([factor, _]) => factor !== 'recoveryCode'
);
if (enabledNonRecoveryFactors.length === 1) {
if (factors.phone) {
await createChallenge(AuthenticationFactor.Phone);
} else if (factors.email) {
await createChallenge(AuthenticationFactor.Email);
} else if (factors.totp) {
challengeType = AuthenticationFactor.Totp;
}
if (enabledMainFactors.length === 0) {
return;
}
if (factors.totp) {
challengeType = AuthenticationFactor.Totp;
} else if (factors.email) {
await createChallenge(AuthenticationFactor.Email);
} else if (factors.phone) {
await createChallenge(AuthenticationFactor.Phone);
}
});
</script>
{#if challengeType === AuthenticationFactor.Recoverycode}
{#if enabledMainFactors.length === 0}
<!-- Skip MFA completely if no factors enabled -->
{:else if challengeType === AuthenticationFactor.Recoverycode}
<Typography.Text align="center">
Enter below one of the recovery codes you received when enabling MFA for this account.
Enter one of the recovery codes you received when enabling MFA.
</Typography.Text>
<InputText id="recovery-code" bind:value={code} required autofocus />
{:else}
{#if factors.totp && (challengeType === AuthenticationFactor.Totp || challengeType === null)}
<Typography.Text align="center"
>Enter below a 6-digit one-time code generated by your authentication app.</Typography.Text>
{:else if challengeType === AuthenticationFactor.Email}
<Typography.Text align="center"
>A 6-digit verification code was sent to your email, enter it below.</Typography.Text>
{:else if challengeType === AuthenticationFactor.Phone}
<Typography.Text align="center"
>A 6-digit verification code was sent to your phone, enter it below.</Typography.Text>
{/if}
{:else if challengeType === AuthenticationFactor.Totp}
<Typography.Text align="center">
Enter a 6-digit one-time code from your authenticator app.
</Typography.Text>
<InputDigits bind:value={code} required autofocus />
{:else if challengeType === AuthenticationFactor.Email}
<Typography.Text align="center">
A 6-digit verification code was sent to your email. Enter it below.
</Typography.Text>
<InputDigits bind:value={code} required autofocus />
{:else if challengeType === AuthenticationFactor.Phone}
<Typography.Text align="center">
A 6-digit verification code was sent to your phone. Enter it below.
</Typography.Text>
<InputDigits bind:value={code} required autofocus />
{/if}
{#if showVerifyButton}
{#if showVerifyButton && enabledMainFactors.length > 0}
<Button fullWidth submit {disabled}>Verify</Button>
{/if}
{#if enabledFactors.length > 1}
<span class="with-separators eyebrow-heading-3">or</span>
<div class="u-flex-vertical u-gap-8">
{#if factors.totp}
{#if (challengeType === null || challengeType === AuthenticationFactor.Totp || challengeType === AuthenticationFactor.Recoverycode) && factors.email}
<Button
secondary
fullWidth
{disabled}
on:click={() => createChallenge(AuthenticationFactor.Email)}>
<Icon icon={IconMail} slot="start" size="s" />
Email verification
</Button>
{:else if (challengeType === AuthenticationFactor.Recoverycode && !factors.email) || challengeType === AuthenticationFactor.Email}
{#if enabledMainFactors.length > 0}
{@const hasSecondaryOptions =
enabledMainFactors.length > 1 ||
(factors.recoveryCode && challengeType !== AuthenticationFactor.Recoverycode) ||
(enabledMainFactors.length === 1 && factors.recoveryCode)}
{#if hasSecondaryOptions}
<span class="with-separators eyebrow-heading-3">or</span>
<div class="u-flex-vertical u-gap-8">
<!-- Show non-active main factors -->
{#if factors.totp && challengeType !== AuthenticationFactor.Totp}
<Button
secondary
fullWidth
@@ -120,7 +161,7 @@
Authenticator app
</Button>
{/if}
{:else}
{#if factors.email && challengeType !== AuthenticationFactor.Email}
<Button
secondary
@@ -131,6 +172,7 @@
Email verification
</Button>
{/if}
{#if factors.phone && challengeType !== AuthenticationFactor.Phone}
<Button
secondary
@@ -141,15 +183,16 @@
Phone verification
</Button>
{/if}
{/if}
{#if factors.recoveryCode && challengeType !== AuthenticationFactor.Recoverycode}
<Button
text
fullWidth
{disabled}
on:click={() => createChallenge(AuthenticationFactor.Recoverycode)}>
<span class="text">Use recovery code</span>
</Button>
{/if}
</div>
{#if factors.recoveryCode && challengeType !== AuthenticationFactor.Recoverycode}
<Button
text
fullWidth
{disabled}
on:click={() => createChallenge(AuthenticationFactor.Recoverycode)}>
<span class="text">Use recovery code</span>
</Button>
{/if}
</div>
{/if}
{/if}
+7 -6
View File
@@ -42,6 +42,7 @@
import type { HTMLAttributes } from 'svelte/elements';
import type { Models } from '@appwrite.io/console';
import { noWidthTransition } from '$lib/stores/sidebar';
import { base } from '$app/paths';
type $$Props = HTMLAttributes<HTMLElement> & {
state?: 'closed' | 'open' | 'icons';
@@ -138,7 +139,7 @@
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
class="progress-card"
href={`/console/project-${project.region}-${project.$id}/get-started`}
href={`${base}/project-${project?.region}-${project?.$id}/get-started`}
on:click={() => {
trackEvent('click_menu_get_started');
sideBarIsOpen = false;
@@ -162,7 +163,7 @@
<Layout.Stack direction="column" gap="s">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/overview/platforms`}
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}
class="link"
class:active={isSelected('overview')}
on:click={() => {
@@ -192,7 +193,7 @@
{#each buildProjectOptions as projectOption}
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/${projectOption.slug}`}
href={`${base}/project-${project.region}-${project.$id}/${projectOption.slug}`}
class="link"
class:active={isSelected(projectOption.slug)}
on:click={() => {
@@ -223,7 +224,7 @@
{#each deployProjectOptions as projectOption}
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/${projectOption.slug}`}
href={`${base}/project-${project.region}-${project.$id}/${projectOption.slug}`}
class="link"
class:active={isSelected(projectOption.slug)}
on:click={() => {
@@ -253,7 +254,7 @@
<div class="mobile-tablet-settings">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/settings`}
href={`${base}/project-${project.region}-${project.$id}/settings`}
on:click={() => {
trackEvent('click_menu_settings');
sideBarIsOpen = false;
@@ -313,7 +314,7 @@
<div class="only-desktop">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/settings`}
href={`${base}/project-${project.region}-${project.$id}/settings`}
class="link"
on:click={() => {
trackEvent('click_menu_settings');
+58
View File
@@ -0,0 +1,58 @@
import DevKind from '$lib/images/testimonials/hassan.png';
import LangX from '$lib/images/testimonials/xue.webp';
import KCollect from '$lib/images/testimonials/ryan-oconner-testimonial.png';
import Phil from '$lib/images/testimonials/phil.jpg';
export interface Testimonial {
id: string;
logo: string;
headline: string;
blurb: string;
name: string;
title: string;
avatar: string;
}
export const testimonials: Testimonial[] = [
{
id: 'hassan-devkind',
logo: DevKind,
headline: 'DevKind reduced development time by 60% and lowered server costs by 40%',
blurb: 'A special thanks to Appwrite for providing robust features and seamless functionality.',
name: 'Hassan Ahmed',
title: 'Engineer at DevKind',
avatar: DevKind
},
{
id: 'xue-langx',
logo: LangX,
headline: 'LangX handled millions of requests using Appwrite',
blurb: 'With its comprehensive suite of services, Appwrite emerged as an ideal choice for my needs.',
name: 'Xue',
title: 'Founder at LangX',
avatar: LangX
},
{
id: 'ryan-kcollect',
logo: KCollect,
headline: 'K-Collect reduced infrastructure costs by 700%',
blurb: 'A major impact that Appwrite made was the amount of time and stress saved.',
name: "Ryan O'Connor",
title: 'Founder at K-Collect',
avatar: KCollect
},
{
id: 'phil-majikkids',
logo: Phil,
headline: 'With Appwrite, Majik Kids built an entire app with one developer.',
blurb: 'Just like a Swiss Army Knife you can choose and use the tools that you need with Appwrite.',
name: 'Phil McCluskey',
title: 'App Manager, Majik Kids',
avatar: Phil
}
];
export function getRandomTestimonial(): Testimonial {
const randomIndex = Math.floor(Math.random() * testimonials.length);
return testimonials[randomIndex];
}
+3
View File
@@ -31,3 +31,6 @@ export { default as InputDigits } from './inputDigits.svelte';
export { default as InputSelectCheckbox } from './inputSelectCheckbox.svelte';
export { default as InputFilePicker } from './inputFilePicker.svelte';
export { default as Seekbar } from './Seekbar.svelte';
export { default as InputPoint } from './inputPoint.svelte';
export { default as InputLine } from './inputLine.svelte';
export { default as InputPolygon } from './inputPolygon.svelte';
+61
View File
@@ -0,0 +1,61 @@
<script lang="ts">
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import InputPoint from './inputPoint.svelte';
import Button from './button.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { Snippet } from 'svelte';
type Props = {
values: number[][];
nullable?: boolean;
minDeletableIndex?: number;
allowLineDelete?: boolean;
onAddPoint: (index: number) => void;
onDeletePoint: (index: number) => void;
onChangePoint: (pointIndex: number, coordIndex: number, newValue: number) => void;
addLineButton?: Snippet;
};
let {
values,
nullable = false,
minDeletableIndex = 2,
allowLineDelete,
onAddPoint,
onDeletePoint,
onChangePoint,
addLineButton
}: Props = $props();
function isDeleteDisabled(index: number) {
let disable = index < minDeletableIndex;
if (allowLineDelete !== undefined) {
disable = disable && allowLineDelete;
}
return disable;
}
</script>
<Layout.Stack alignItems="flex-start" gap="xs">
<Layout.Stack>
{#each values as value, index}
<InputPoint
{nullable}
values={value}
deletePoints
disableDelete={isDeleteDisabled(index)}
onDeletePoint={() => onDeletePoint(index)}
onChangePoint={(coordIndex, newValue) =>
onChangePoint?.(index, coordIndex, newValue)} />
{/each}
</Layout.Stack>
{#if values}
<Layout.Stack direction="row" gap="s" alignItems="center">
<Button size="xs" compact on:click={() => onAddPoint(-1)} disabled={nullable}>
<Icon icon={IconPlus} size="s" /> Add coordinate
</Button>
{@render addLineButton?.()}
</Layout.Stack>
{/if}
</Layout.Stack>
+54
View File
@@ -0,0 +1,54 @@
<script lang="ts">
import { Input } from '@appwrite.io/pink-svelte';
import { Layout, Icon } from '@appwrite.io/pink-svelte';
import Button from './button.svelte';
import { IconX } from '@appwrite.io/pink-icons-svelte';
interface Props {
values: number[];
nullable?: boolean;
deletePoints?: boolean;
onDeletePoint?: () => void;
disableDelete?: boolean;
onChangePoint: (index: number, newValue: number) => void;
}
const nullableSkeletonShape: number[] = [0, 0];
let {
values,
nullable = false,
deletePoints = false,
disableDelete = false,
onDeletePoint,
onChangePoint
}: Props = $props();
</script>
<Layout.Stack>
<Layout.Stack direction="row" gap="m">
{#if nullable}
{#each nullableSkeletonShape as _, index}
<Input.Number id={`default-${index}`} placeholder="0" disabled />
{/each}
{:else}
{#each values as _, index}
<Input.Number
id={`point-${index}`}
placeholder="Enter value"
step={0.0001}
value={values[index]}
on:change={(e) => onChangePoint(index, Number.parseFloat(`${e.detail}`))} />
{/each}
{/if}
{#if deletePoints}
<Button
size="s"
secondary
disabled={nullable || disableDelete}
on:click={() => onDeletePoint?.()}>
<Icon icon={IconX} size="s" />
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
@@ -0,0 +1,59 @@
<script lang="ts">
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import Button from './button.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import InputLine from './inputLine.svelte';
type Props = {
values: number[][][];
nullable?: boolean;
minDeletableIndex?: number;
onAddPoint: (index: number) => void;
onAddLine?: (index: number) => void;
onDeletePoint: (index: number) => void;
onChangePoint: (
lineIndex: number,
pointIndex: number,
coordIndex: number,
newValue: number
) => void;
};
let {
values,
nullable = false,
minDeletableIndex = 4,
onAddPoint,
onAddLine,
onDeletePoint,
onChangePoint
}: Props = $props();
</script>
<Layout.Stack gap="s">
{#each values as value, index}
<Layout.Stack gap="xs">
<InputLine
values={value}
onAddPoint={() => onAddPoint(index)}
{nullable}
onDeletePoint={() => onDeletePoint(index)}
onChangePoint={(pointIndex, coordIndex, newValue) =>
onChangePoint(index, pointIndex, coordIndex, newValue)}
allowLineDelete={index < 2}
{minDeletableIndex}>
{#snippet addLineButton()}
{#if index === values.length - 1}
<Button
disabled={nullable}
size="xs"
compact
on:click={() => onAddLine?.(-1)}>
<Icon icon={IconPlus} size="s" /> Add line
</Button>
{/if}
{/snippet}
</InputLine>
</Layout.Stack>
{/each}
</Layout.Stack>
+53 -6
View File
@@ -3,6 +3,7 @@ import type { Columns } from '$routes/(console)/project-[region]-[project]/datab
import { ID, type Models } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { isWithinSafeRange } from '$lib/helpers/numbers';
import type { NestedNumberArray } from './types';
export async function generateColumns(
project: Models.Project,
@@ -42,7 +43,24 @@ export async function generateColumns(
size: 1000,
required: false
}),
client.tablesDB.createBooleanColumn({ databaseId, tableId, key: 'active', required: false })
client.tablesDB.createBooleanColumn({
databaseId,
tableId,
key: 'active',
required: false
}),
client.tablesDB.createPointColumn({
databaseId,
tableId,
key: 'location',
required: false
}),
client.tablesDB.createLineColumn({
databaseId,
tableId,
key: 'route',
required: false
})
]);
}
@@ -66,7 +84,10 @@ export function generateFakeRecords(
const id = ID.unique();
ids.push(id);
let row: Record<string, string | number | boolean | Array<string | number | boolean>> = {
let row: Record<
string,
string | number | boolean | Array<string | number | boolean | NestedNumberArray>
> = {
$id: id
};
@@ -78,7 +99,12 @@ export function generateFakeRecords(
age: faker.number.int({ min: 18, max: 80 }),
city: faker.location.city(),
description: faker.lorem.sentence(),
active: faker.datatype.boolean()
active: faker.datatype.boolean(),
location: [faker.location.longitude(), faker.location.latitude()],
route: Array.from({ length: 5 }, () => [
faker.location.longitude(),
faker.location.latitude()
])
};
} else {
for (const column of filteredColumns) {
@@ -116,10 +142,10 @@ function generateStringValue(key: string, maxLength: number): string {
function generateValueForColumn(
column: Columns
): string | number | boolean | null | Array<string | number | boolean> {
): string | number | boolean | null | Array<string | number | boolean | NestedNumberArray> {
if (column.array) {
const arraySize = faker.number.int({ min: 1, max: 5 });
const items: Array<string | number | boolean> = [];
const items: Array<string | number | NestedNumberArray | boolean> = [];
for (let i = 0; i < arraySize; i++) {
const itemAttribute = { ...column, array: false };
@@ -135,7 +161,9 @@ function generateValueForColumn(
return generateSingleValue(column);
}
function generateSingleValue(column: Columns): string | number | boolean | null {
function generateSingleValue(
column: Columns
): string | number | boolean | NestedNumberArray | null {
switch (column.type) {
case 'string': {
if ('format' in column && column.format) {
@@ -192,6 +220,25 @@ function generateSingleValue(column: Columns): string | number | boolean | null
return faker.date.recent({ days: 365 }).toISOString();
}
case 'point': {
return [faker.location.longitude(), faker.location.latitude()];
}
case 'linestring': {
return Array.from({ length: 5 }, () => [
faker.location.longitude(),
faker.location.latitude()
]);
}
case 'polygon': {
const points = Array.from({ length: 5 }, () => [
faker.location.longitude(),
faker.location.latitude()
]);
points.push(points[0]); // close polygon
return [points];
}
default: {
return faker.lorem.word();
}
+6 -2
View File
@@ -3,7 +3,9 @@ export function getNestedRootDirectory(repository: string): string | null {
return match ? match[1] : null;
}
export function getRepositoryInfo(repository: string): { owner: string; name: string } | null {
export function getRepositoryInfo(
repository: string
): { owner: string; name: string; url: string } | null {
const match = repository
.trim()
.match(/(?:^|\/\/|@)github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?(?:$|[/?#])/i);
@@ -13,7 +15,9 @@ export function getRepositoryInfo(repository: string): { owner: string; name: st
const owner = decodeURIComponent(match[1]);
const name = decodeURIComponent(match[2].replace(/\.git$/i, ''));
return { owner, name };
const url = `https://github.com/${owner}/${name}`;
return { owner, name, url };
}
export async function getLatestTag(owner: string, name: string): Promise<string | null> {
+23
View File
@@ -0,0 +1,23 @@
/**
* determines the badge color based on HTTP status code
*
* @param statusCode
* @returns badge color
*/
export function getBadgeTypeFromStatusCode(
statusCode: number
): 'error' | 'warning' | 'success' | undefined {
if (statusCode >= 500) {
return 'error';
}
if (statusCode >= 400) {
return 'warning';
}
if (statusCode === 0) {
return undefined;
}
return 'success';
}
+6 -1
View File
@@ -35,7 +35,10 @@ const columnTypes = [
'boolean',
'datetime',
'relationship',
'enum'
'enum',
'point',
'linestring',
'polygon'
] as const;
export type ColumnType = (typeof columnTypes)[number];
export type Column = PinkColumn & {
@@ -77,3 +80,5 @@ export type TableRootProp = {
addAvailableId: (id: string) => void;
removeAvailableId: (id: string) => void;
};
export type NestedNumberArray = Array<number | NestedNumberArray>;
+37
View File
@@ -1,3 +1,17 @@
const LINKEDIN_TRACKING_ID = 'li_fat_id';
const GOOGLE_TRACKING_ID = 'gclid';
const TWITTER_TRACKING_ID = 'twclid';
const REDDIT_TRACKING_ID = 'rdt_cid';
const TRACKED_QUERY_PARAMS = [
LINKEDIN_TRACKING_ID,
GOOGLE_TRACKING_ID,
TWITTER_TRACKING_ID,
REDDIT_TRACKING_ID
] as const;
type TrackedParams = Record<(typeof TRACKED_QUERY_PARAMS)[number], string>;
export function getReferrerAndUtmSource() {
if (sessionStorage) {
let values = {};
@@ -15,3 +29,26 @@ export function getReferrerAndUtmSource() {
}
return {};
}
export function getTrackedQueryParams(): Partial<TrackedParams> {
if (typeof window === 'undefined' || !window.location) {
return {};
}
try {
const urlParams = new URLSearchParams(window.location.search);
const params: Partial<TrackedParams> = {};
for (const paramName of TRACKED_QUERY_PARAMS) {
const value = urlParams.get(paramName);
if (value) {
params[paramName] = value;
}
}
return params;
} catch (error) {
console.warn('Failed to parse URL search parameters:', error);
return {};
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+1 -1
View File
@@ -205,7 +205,7 @@
<Navbar {...navbarProps} bind:sideBarIsOpen={$isSidebarOpen} bind:showAccountMenu />
{/if}
{#if !$isNewWizardStatusOpen}
{#if !$isNewWizardStatusOpen && isProjectPage}
<Sidebar
project={selectedProject}
progressCard={getProgressCard()}
+1 -1
View File
@@ -109,7 +109,7 @@
<Layout.Stack gap="s" direction="row">
{#if currentReview?.image}
<Avatar
src={getCampaignImageUrl(currentReview?.image)}
src={currentReview.image}
alt={currentReview.name}
size="m" />
{:else}
+1 -1
View File
@@ -620,7 +620,7 @@ export const billingURL = derived(
($page) => `${base}/organization-${$page.data?.organization?.$id}/billing`
);
export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account'];
export const hideBillingHeaderRoutes = [base + '/create-organization', base + '/account'];
export function calculateExcess(addon: AggregationTeam, plan: Plan) {
return {
+1 -1
View File
@@ -144,7 +144,7 @@ function createFeedbackStore() {
message,
email,
customFields,
firstname: name || 'Unknown',
firstname: (name || 'Unknown').slice(0, 40),
metaFields: {
source: get(feedback).source,
orgId,
+20 -1
View File
@@ -66,6 +66,19 @@ function safePrefsKey(widthPreferences: TeamPreferences['widths'], from: string,
}
}
function safePrefsKeyForOrder(
orderPreferences: TeamPreferences['order'],
from: string,
to: string
) {
if (orderPreferences.includes(from)) {
const index = orderPreferences.indexOf(from);
if (index !== -1) {
orderPreferences[index] = to;
}
}
}
// rare cases where the value was an array, probably due to PHP backend.
function ensureObjectProperty<T extends Record<string, unknown>, K extends keyof T>(
obj: T | null | undefined,
@@ -239,7 +252,9 @@ function createPreferences() {
},
getColumnOrder(tableId: string): TeamPreferences['order'] {
return teamPreferences?.columnOrder?.[tableId] ?? [];
const columnOrder = teamPreferences?.columnOrder?.[tableId] ?? [];
safePrefsKeyForOrder(columnOrder, '$uid', '$id');
return columnOrder;
},
async saveColumnOrder(orgId: string, tableId: string, columnIds: TeamPreferences['order']) {
@@ -247,6 +262,10 @@ function createPreferences() {
teamPreferences.columnOrder[tableId] = columnIds;
for (const tableOrder of Object.values(teamPreferences.columnOrder)) {
safePrefsKeyForOrder(tableOrder, '$id', '$uid');
}
await sdk.forConsole.teams.updatePrefs({
teamId: orgId,
prefs: teamPreferences
+7 -10
View File
@@ -59,7 +59,6 @@
import type { LayoutData } from './$types';
export let data: LayoutData;
let emailBannerClosed = false;
function kebabToSentenceCase(str: string) {
return str
@@ -83,7 +82,7 @@
keys: ['g', 'p'],
group: 'navigation',
disabled:
(page.url.pathname.includes('/console/organization-') &&
(page.url.pathname.includes(base + '/organization-') &&
!page.url.pathname.endsWith('/members') &&
!page.url.pathname.endsWith('/settings')) ||
!$canSeeProjects,
@@ -336,20 +335,18 @@
<Shell
showSideNavigation={page.url.pathname !== '/' &&
!page?.params.organization &&
!page.url.pathname.includes('/console/account') &&
!page.url.pathname.includes('/console/card') &&
!page.url.pathname.includes('/console/onboarding')}
showHeader={!page.url.pathname.includes('/console/onboarding/create-project')}
showFooter={!page.url.pathname.includes('/console/onboarding/create-project')}
!page.url.pathname.includes(base + '/account') &&
!page.url.pathname.includes(base + '/card') &&
!page.url.pathname.includes(base + '/onboarding')}
showHeader={!page.url.pathname.includes(base + '/onboarding/create-project')}
showFooter={!page.url.pathname.includes(base + '/onboarding/create-project')}
selectedProject={page.data?.project}>
<!-- <Header slot="header" />-->
<slot />
<Footer slot="footer" />
</Shell>
<EmailVerificationBanner
{emailBannerClosed}
onEmailBannerClose={(closed) => (emailBannerClosed = closed)} />
<EmailVerificationBanner />
{#if $wizard.show && $wizard.component}
<svelte:component this={$wizard.component} {...$wizard.props} />
@@ -89,25 +89,34 @@
id="address"
label="Street address"
placeholder="Enter street address"
autocomplete
required />
<InputText
bind:value={address2}
id="address2"
label="Address line 2"
autocomplete
placeholder="Unit number, floor, etc." />
<InputText
bind:value={city}
id="city"
label="City or suburb"
placeholder="Enter your city"
autocomplete
required />
<InputText
bind:value={state}
id="state"
label="State"
placeholder="Enter your state"
autocomplete
required />
<InputText bind:value={zip} id="zip" label="Postal code" placeholder="Enter postal code" />
<InputText
bind:value={zip}
id="zip"
label="Postal code"
placeholder="Enter postal code"
autocomplete />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
@@ -8,6 +8,7 @@
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { onMount } from 'svelte';
import { Badge, Layout } from '@appwrite.io/pink-svelte';
let email: string = null;
let emailPassword: string = null;
@@ -40,7 +41,14 @@
<Form onSubmit={updateEmail}>
<CardGrid>
<svelte:fragment slot="title">Email</svelte:fragment>
<svelte:fragment slot="title">
<Layout.Stack direction="row" gap="s" alignItems="center">
Email
{#if $user.emailVerification}
<Badge variant="secondary" type="success" size="s" content="verified" />
{/if}
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="aside">
<InputText
id="email"
+19 -7
View File
@@ -12,6 +12,7 @@
import { factors } from './store';
import MfaRecoveryCodes from './mfaRecoveryCodes.svelte';
import type { Models } from '@appwrite.io/console';
import { AuthenticationFactor } from '@appwrite.io/console';
import MfaRegenerateCodes from './mfaRegenerateCodes.svelte';
import { page } from '$app/state';
import { onMount } from 'svelte';
@@ -56,15 +57,11 @@
const userId = searchParams.get('userId');
const secret = searchParams.get('secret');
history.replaceState(null, '', cleanUrl);
if (userId && secret) {
history.replaceState(null, '', cleanUrl);
try {
await sdk.forConsole.account.updateVerification({ userId, secret });
addNotification({
message: 'Email verified successfully',
type: 'success'
});
// Don't show notification here - the modal will handle it
await Promise.all([
invalidate(Dependencies.ACCOUNT),
invalidate(Dependencies.FACTORS)
@@ -81,7 +78,20 @@
async function updateMfa() {
try {
await sdk.forConsole.account.updateMFA({ mfa: !$user.mfa });
await invalidate(Dependencies.ACCOUNT);
if (!$user.mfa && $user.emailVerification && !$factors.email) {
// Automatically set up email MFA when enabling MFA if the user has a verified email
// This provides a fallback authentication method without requiring user interaction
try {
await sdk.forConsole.account.createMFAChallenge({
factor: AuthenticationFactor.Email
});
} catch (emailError) {
// Silently ignore - email MFA is optional and shouldn't block MFA enablement
}
}
await Promise.all([invalidate(Dependencies.ACCOUNT), invalidate(Dependencies.FACTORS)]);
addNotification({
message: `Multi-factor authentication has been ${$user.mfa ? 'enabled' : 'disabled'}`,
type: 'success'
@@ -280,6 +290,7 @@
<Button
secondary
class="recovery-codes-button"
disabled={enabledMethods.length === 0}
on:click={() => (showRegenerateRecoveryCodes = true)}>
Regenerate
</Button>
@@ -287,6 +298,7 @@
<Button
secondary
class="recovery-codes-button"
disabled={enabledMethods.length === 0}
on:click={createRecoveryCodes}>View</Button>
{/if}
</Layout.Stack>
@@ -174,7 +174,7 @@
'',
clientSecret,
paymentMethodId,
`/console/apply-credit?${params}`
`${base}/apply-credit?${params}`
);
org = await sdk.forConsole.billing.validateOrganization(org.teamId, collaborators);
}
+10 -11
View File
@@ -1,8 +1,8 @@
import { isCloud } from '$lib/system';
import { isSameDay } from '$lib/helpers/date';
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
import TablesApiLight from '$lib/images/promos/tables-api-light.png';
import TablesApiDark from '$lib/images/promos/tables-api-dark.png';
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';
@@ -11,21 +11,20 @@ import TimeHelperQueriesLight from '$lib/images/promos/time-helper-queries-light
const listOfPromotions: BottomModalAlertItem[] = [];
if (isCloud) {
const tablesApiPromo: BottomModalAlertItem = {
id: 'modal:tables_api_announcement',
const spatialColumnsPromo: BottomModalAlertItem = {
id: 'modal:spatial_columns_announcement',
src: {
dark: TablesApiDark,
light: TablesApiLight
dark: SpatialColumnsDark,
light: SpatialColumnsLight
},
title: 'Announcing the new TablesDB UI and more!',
message:
'This is Appwrite Databases’ most significant update so far, introducing new terminology, a new UI, and a supporting TablesDB API.',
title: 'Announcing API for spatial columns',
message: 'Store and query geo data directly in your database.',
plan: 'free',
importance: 8,
scope: 'project',
cta: {
text: 'Read announcement',
link: () => 'https://apwr.dev/E64A90f',
link: () => 'https://appwrite.io/blog/post/announcing-spatial-columns',
external: true,
hideOnClick: true
},
@@ -71,7 +70,7 @@ if (isCloud) {
},
show: true
};
listOfPromotions.push(inversionQueriesPromo, timeHelperQueriesPromo, tablesApiPromo);
listOfPromotions.push(spatialColumnsPromo, inversionQueriesPromo, timeHelperQueriesPromo);
}
export function addBottomModalAlerts() {
@@ -143,7 +143,7 @@
'',
clientSecret,
paymentMethodId,
`/console/create-organization?${params}`
`${base}/create-organization?${params}`
);
await validate(org.teamId, collaborators);
}
@@ -82,13 +82,13 @@
<Loading {startAnimation} />
{:else}
<img
src="/console/images/appwrite-logo-light.svg"
src="{base}/images/appwrite-logo-light.svg"
width="120"
height="22"
class="u-only-light"
alt="Appwrite Logo" />
<img
src="/console/images/appwrite-logo-dark.svg"
src="{base}/images/appwrite-logo-dark.svg"
width="120"
height="22"
class="u-only-dark"
@@ -19,6 +19,8 @@
import { trackEvent, Click } from '$lib/actions/analytics';
import { type Models } from '@appwrite.io/console';
import { getServiceLimit, readOnly, upgradeURL } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { onMount, type ComponentType } from 'svelte';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
@@ -45,6 +47,7 @@
let addOrganization = false;
let showSelectProject = false;
let showCreateProjectCloud = false;
let freePlanAlertDismissed = false;
let searchQuery: SearchQuery;
@@ -80,10 +83,13 @@
}
$: projectCreationDisabled =
(isCloud && getServiceLimit('projects') <= data.allProjectsCount) ||
(isCloud && getServiceLimit('projects') <= data.projects.total) ||
(isCloud && $readOnly && !GRACE_PERIOD_OVERRIDE) ||
!$canWriteProjects;
$: reachedProjectLimit = isCloud && getServiceLimit('projects') <= data.projects.total;
$: projectsLimit = getServiceLimit('projects');
$: $registerCommands([
{
label: 'Create project',
@@ -97,7 +103,23 @@
}
]);
onMount(async () => checkPricingRefAndRedirect(page.url.searchParams));
function dismissFreePlanAlert() {
freePlanAlertDismissed = true;
const notificationId = `freePlanAlert_${data.organization.$id}`;
hideNotification(notificationId, { coolOffPeriod: 24 });
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'free_plan_info_alert_dismiss'
});
}
onMount(async () => {
checkPricingRefAndRedirect(page.url.searchParams);
const notificationId = `freePlanAlert_${data.organization.$id}`;
const shouldShow = shouldShowNotification(notificationId);
freePlanAlertDismissed = !shouldShow;
});
function findRegion(project: Models.Project) {
return $regionsStore.regions.find((region) => region.$id === project.region);
@@ -129,13 +151,27 @@
<SearchQuery bind:this={searchQuery} placeholder="Search by name or ID" />
{#if $canWriteProjects}
<Button
on:click={handleCreateProject}
event="create_project"
disabled={projectCreationDisabled}>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
{#if projectCreationDisabled && reachedProjectLimit}
<Tooltip placement="bottom">
<div>
<Button event="create_project" disabled>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
</div>
<span slot="tooltip">
You have reached your limit of {projectsLimit} projects.
</span>
</Tooltip>
{:else}
<Button
on:click={handleCreateProject}
event="create_project"
disabled={projectCreationDisabled}>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
{/if}
{/if}
</Layout.Stack>
@@ -162,6 +198,28 @@
</Alert.Inline>
{/if}
{#if isCloud && data.organization.billingPlan === BillingPlan.FREE && projectsToArchive.length === 0 && !freePlanAlertDismissed}
<Alert.Inline dismissible on:dismiss={dismissFreePlanAlert}>
<Typography.Text
>Your Free plan includes up to 2 projects and limited resources. Upgrade to unlock
more capacity and features.</Typography.Text>
<svelte:fragment slot="actions">
<Button
compact
size="s"
href={$upgradeURL}
on:click={() => {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'free_plan_info_alert'
});
}}>
Upgrade to Pro
</Button>
</svelte:fragment>
</Alert.Inline>
{/if}
{#if activeProjects.length > 0}
<CardContainer
disableEmpty={!$canWriteProjects}
@@ -4,11 +4,12 @@ import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
import type { PageLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
export const load: PageLoad = async ({ params, url, route, depends, parent }) => {
const { scopes } = await parent();
if (!scopes.includes('projects.read') && scopes.includes('billing.read')) {
return redirect(301, `/console/organization-${params.organization}/billing`);
return redirect(301, `${base}/organization-${params.organization}/billing`);
}
depends(Dependencies.ORGANIZATION);
@@ -5,12 +5,13 @@ import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { isCloud } from '$lib/system';
import { base } from '$app/paths';
export const load: PageLoad = async ({ parent, depends }) => {
const { organization, scopes, currentPlan, countryList, locale } = await parent();
if (!scopes.includes('billing.read')) {
return redirect(301, `/console/organization-${organization.$id}`);
return redirect(301, `${base}/organization-${organization.$id}`);
}
depends(Dependencies.PAYMENT_METHODS);
@@ -146,7 +146,7 @@
function getBillingData(currentPlan, currentAggregation, isSmallViewport) {
const projectsList = getProjectsList(currentAggregation);
const base = {
const basePlan = {
id: 'base-plan',
expandable: false,
cells: {
@@ -303,7 +303,7 @@
{
id: `usage-details`,
cells: {
item: `<a href="/console/project-${String(project.region || 'default')}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`,
item: `<a href="${base}/project-${String(project.region || 'default')}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`,
usage: '',
price: ''
}
@@ -311,7 +311,7 @@
]
}));
const noProjects = [];
return [base, ...addons, ...projects, ...noProjects];
return [basePlan, ...addons, ...projects, ...noProjects];
}
$: billingData = getBillingData(currentPlan, currentAggregation, $isSmallViewport);
@@ -277,7 +277,7 @@
'',
clientSecret,
paymentMethodId,
'/console/change-plan?' + params.toString()
base + '/change-plan?' + params.toString()
);
await validate(org.teamId, collaborators);
}
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
@@ -20,7 +21,7 @@
type: 'success',
message: 'Domain moved successfully'
});
await goto(`/console/organization-${selectedOrg}/domains/`);
await goto(`${base}/organization-${selectedOrg}/domains/`);
await invalidate(Dependencies.ORGANIZATION);
await invalidate(Dependencies.DOMAINS);
} catch (e) {
@@ -64,7 +64,7 @@
body: JSON.stringify({
subject: 'BAA Request',
email: email,
firstName: $user?.name ?? '',
firstName: ($user?.name ?? '').slice(0, 40),
message: `BAA request for ${$organization?.name ?? ''} (${$organization?.$id ?? ''})`,
tags: ['cloud'],
customFields: [
@@ -65,7 +65,7 @@
body: JSON.stringify({
subject: 'SOC-2 Request',
email: email,
firstName: $user?.name ?? '',
firstName: ($user?.name ?? '').slice(0, 40),
message: `SOC-2 request for ${$organization?.name ?? ''} (${$organization?.$id ?? ''})`,
tags: ['cloud'],
customFields: [
@@ -20,13 +20,26 @@
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import Create from './createUser.svelte';
import { Badge, Icon, Table, Layout, Typography } from '@appwrite.io/pink-svelte';
import {
Badge,
Icon,
Table,
Layout,
Typography,
FloatingActionBar
} from '@appwrite.io/pink-svelte';
import { Tag } from '@appwrite.io/pink-svelte';
import { IconDuplicate, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { canWriteUsers } from '$lib/stores/roles';
import type { Column } from '$lib/helpers/types';
import ViewSelector from '$lib/components/viewSelector.svelte';
import { View } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import Confirm from '$lib/components/confirm.svelte';
export let data;
@@ -46,11 +59,46 @@
}
]);
let selectedUsers: string[] = [];
let showDelete = false;
let deleting = false;
async function userCreated(event: CustomEvent<Models.User<Record<string, unknown>>>) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${event.detail.$id}`
);
}
async function handleDelete() {
showDelete = false;
deleting = true;
const promises = selectedUsers.map((userId) =>
sdk.forProject(page.params.region, page.params.project).users.delete(userId)
);
try {
await Promise.all(promises);
trackEvent(Submit.UserDelete, {
total: selectedUsers.length
});
addNotification({
type: 'success',
message: `${selectedUsers.length} user${selectedUsers.length > 1 ? 's' : ''} deleted`
});
invalidate(Dependencies.USERS);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.UserDelete);
} finally {
selectedUsers = [];
showDelete = false;
deleting = false;
}
}
</script>
<Container>
@@ -68,7 +116,11 @@
</Layout.Stack>
{#if data.users.total}
<Table.Root columns={$columns} let:root>
<Table.Root
columns={$columns}
allowSelection={$canWriteUsers}
bind:selectedRows={selectedUsers}
let:root>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title } (id)}
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
@@ -77,7 +129,8 @@
{#each data.users.users as user}
<Table.Row.Link
href={`${base}/project-${page.params.region}-${page.params.project}/auth/user-${user.$id}`}
{root}>
{root}
id={user.$id}>
{#each $columns as { id } (id)}
<Table.Cell column={id} {root}>
{#if id === '$id'}
@@ -175,6 +228,33 @@
allowCreate={$canWriteUsers}
on:click={() => showCreateUser.set(true)} />
{/if}
{#if selectedUsers.length > 0}
<FloatingActionBar>
<svelte:fragment slot="start">
<Badge content={selectedUsers.length.toString()} />
<span>
{selectedUsers.length > 1 ? 'users' : 'user'}
selected
</span>
</svelte:fragment>
<svelte:fragment slot="end">
<Button text on:click={() => (selectedUsers = [])}>Cancel</Button>
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</FloatingActionBar>
{/if}
</Container>
<Create bind:showCreate={$showCreateUser} on:created={userCreated} />
<Confirm title="Delete users" bind:open={showDelete} onSubmit={handleDelete} disabled={deleting}>
<Typography.Text>
Are you sure you want to delete <b>{selectedUsers.length}</b>
{selectedUsers.length > 1 ? 'users' : 'user'}?
</Typography.Text>
<Typography.Text>
This action is irreversible and will permanently remove the selected users and all their
data.
</Typography.Text>
</Confirm>
@@ -1,11 +1,13 @@
import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import { PAGE_LIMIT, Dependencies } from '$lib/constants';
import type { PageLoad } from './$types';
import { showCreateUser } from './+page.svelte';
export const load: PageLoad = async ({ url, route, params }) => {
export const load: PageLoad = async ({ url, route, params, depends }) => {
depends(Dependencies.USERS);
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, PAGE_LIMIT);
@@ -19,17 +19,72 @@
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import { canWriteTeams } from '$lib/stores/roles';
import { Icon, Layout, Table } from '@appwrite.io/pink-svelte';
import {
Icon,
Layout,
Table,
FloatingActionBar,
Badge,
Typography
} from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import Confirm from '$lib/components/confirm.svelte';
export let data;
const region = page.params.region;
const project = page.params.project;
let selectedTeams: string[] = [];
let showDelete = false;
let deleting = false;
const columns = writable([
{ id: 'name', title: 'Name', type: 'string', width: { min: 200, max: 300 } },
{ id: 'members', title: 'Members', type: 'string', width: { min: 120, max: 150 } },
{ id: 'created', title: 'Created', type: 'string', width: { min: 120, max: 180 } }
]);
const teamCreated = async (event: CustomEvent<Models.Team<Record<string, unknown>>>) => {
await goto(`${base}/project-${region}-${project}/auth/teams/team-${event.detail.$id}`);
};
async function handleDelete() {
showDelete = false;
deleting = true;
const promises = selectedTeams.map((teamId) =>
sdk.forProject(page.params.region, page.params.project).teams.delete(teamId)
);
try {
await Promise.all(promises);
trackEvent(Submit.TeamDelete, {
total: selectedTeams.length
});
addNotification({
type: 'success',
message: `${selectedTeams.length} team${selectedTeams.length > 1 ? 's' : ''} deleted`
});
invalidate(Dependencies.TEAMS);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.TeamDelete);
} finally {
selectedTeams = [];
showDelete = false;
deleting = false;
}
}
</script>
<Container>
@@ -46,32 +101,55 @@
</Layout.Stack>
{#if data.teams.total}
<Table.Root columns={3} let:root>
<Table.Root
columns={$columns}
allowSelection={$canWriteTeams}
bind:selectedRows={selectedTeams}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Name</Table.Header.Cell>
<Table.Header.Cell {root}>Members</Table.Header.Cell>
<Table.Header.Cell {root}>Created</Table.Header.Cell>
{#each $columns as { id, title }}
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
{/each}
</svelte:fragment>
{#each data.teams.teams as team}
{#each data.teams.teams as team (team.$id)}
<Table.Row.Link
{root}
href={`${base}/project-${region}-${project}/auth/teams/team-${team.$id}`}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center">
<AvatarInitials size="xs" name={team.name} />
<span class="u-trim">{team.name}</span>
</Layout.Stack>
</Table.Cell>
<Table.Cell {root}>
{team.total} members
</Table.Cell>
<Table.Cell {root}>
<DualTimeView time={team.$createdAt} />
</Table.Cell>
href={`${base}/project-${region}-${project}/auth/teams/team-${team.$id}`}
id={team.$id}>
{#each $columns as column}
<Table.Cell column={column.id} {root}>
{#if column.id === 'name'}
<Layout.Stack direction="row" alignItems="center">
<AvatarInitials size="xs" name={team.name} />
<span class="u-trim">{team.name}</span>
</Layout.Stack>
{:else if column.id === 'members'}
{team.total} members
{:else if column.id === 'created'}
<DualTimeView time={team.$createdAt} />
{/if}
</Table.Cell>
{/each}
</Table.Row.Link>
{/each}
</Table.Root>
{#if selectedTeams.length > 0}
<FloatingActionBar>
<svelte:fragment slot="start">
<Badge content={selectedTeams.length.toString()} />
<span>
{selectedTeams.length > 1 ? 'teams' : 'team'}
selected
</span>
</svelte:fragment>
<svelte:fragment slot="end">
<Button text on:click={() => (selectedTeams = [])}>Cancel</Button>
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</FloatingActionBar>
{/if}
<PaginationWithLimit
name="Teams"
limit={data.limit}
@@ -94,3 +172,14 @@
</Container>
<Create bind:showCreate={$showCreateTeam} on:created={teamCreated} />
<Confirm title="Delete teams" bind:open={showDelete} onSubmit={handleDelete} disabled={deleting}>
<Typography.Text>
Are you sure you want to delete <b>{selectedTeams.length}</b>
{selectedTeams.length > 1 ? 'teams' : 'team'}?
</Typography.Text>
<Typography.Text>
This action is irreversible and will permanently remove the selected teams and all their
memberships.
</Typography.Text>
</Confirm>
@@ -1,10 +1,12 @@
import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import { PAGE_LIMIT, Dependencies } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, route, params }) => {
export const load: PageLoad = async ({ url, route, params, depends }) => {
depends(Dependencies.TEAMS);
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, PAGE_LIMIT);
@@ -11,15 +11,28 @@
import CreateMember from '../createMembership.svelte';
import DeleteMembership from '../deleteMembership.svelte';
import { Dependencies } from '$lib/constants';
import { Click, trackEvent } from '$lib/actions/analytics';
import { Table, Layout, Icon } from '@appwrite.io/pink-svelte';
import { Click, trackEvent, Submit, trackError } from '$lib/actions/analytics';
import {
Table,
Layout,
Icon,
FloatingActionBar,
Badge,
Typography
} from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import Confirm from '$lib/components/confirm.svelte';
export let data: PageData;
let showCreate = false;
let showDelete = false;
let selectedMembership: Models.Membership;
let selectedMemberships: string[] = [];
let showBulkDelete = false;
let deleting = false;
const region = page.params.region;
const project = page.params.project;
@@ -27,6 +40,40 @@
async function memberCreated() {
invalidate(Dependencies.MEMBERSHIPS);
}
async function handleBulkDelete() {
showBulkDelete = false;
deleting = true;
const promises = selectedMemberships.map((membershipId) =>
sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
teamId: page.params.team,
membershipId
})
);
try {
await Promise.all(promises);
trackEvent(Submit.MembershipUpdate, {
total: selectedMemberships.length
});
addNotification({
type: 'success',
message: `${selectedMemberships.length} membership${selectedMemberships.length > 1 ? 's' : ''} deleted`
});
invalidate(Dependencies.MEMBERSHIPS);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.MembershipUpdate);
} finally {
selectedMemberships = [];
showBulkDelete = false;
deleting = false;
}
}
</script>
<Container>
@@ -40,6 +87,8 @@
{#if data.memberships.total}
<Table.Root
let:root
allowSelection
bind:selectedRows={selectedMemberships}
columns={[
{ id: 'name' },
{ id: 'roles' },
@@ -52,11 +101,12 @@
<Table.Header.Cell column="joined" {root}>Joined</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each data.memberships.memberships as membership}
{#each data.memberships.memberships as membership (membership.$id)}
{@const username = membership.userName ? membership.userName : '-'}
<Table.Row.Link
{root}
href={`${base}/project-${region}-${project}/auth/user-${membership.userId}`}>
href={`${base}/project-${region}-${project}/auth/user-${membership.userId}`}
id={membership.$id}>
<Table.Cell column="name" {root}>
<Layout.Stack direction="row" alignItems="center">
<AvatarInitials size="xs" name={username} />
@@ -85,6 +135,22 @@
{/each}
</Table.Root>
{#if selectedMemberships.length > 0}
<FloatingActionBar>
<svelte:fragment slot="start">
<Badge content={selectedMemberships.length.toString()} />
<span>
{selectedMemberships.length > 1 ? 'memberships' : 'membership'}
selected
</span>
</svelte:fragment>
<svelte:fragment slot="end">
<Button text on:click={() => (selectedMemberships = [])}>Cancel</Button>
<Button secondary on:click={() => (showBulkDelete = true)}>Delete</Button>
</svelte:fragment>
</FloatingActionBar>
{/if}
<PaginationWithLimit
name="Memberships"
limit={data.limit}
@@ -93,7 +159,7 @@
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find ‘{data.search}'</b>
<b>Sorry, we couldn't find '{data.search}'</b>
<p>There are no members that match your search.</p>
</div>
<Button
@@ -118,3 +184,17 @@
{selectedMembership}
bind:showDelete
on:deleted={() => invalidate(Dependencies.MEMBERSHIPS)} />
<Confirm
title="Delete memberships"
bind:open={showBulkDelete}
onSubmit={handleBulkDelete}
disabled={deleting}>
<Typography.Text>
Are you sure you want to delete <b>{selectedMemberships.length}</b>
{selectedMemberships.length > 1 ? 'memberships' : 'membership'}?
</Typography.Text>
<Typography.Text>
This action is irreversible and will remove the selected members from this team.
</Typography.Text>
</Confirm>
@@ -6,23 +6,81 @@
import { Container } from '$lib/layout';
import DeleteMembership from '../deleteMembership.svelte';
import type { Models } from '@appwrite.io/console';
import { trackEvent } from '$lib/actions/analytics';
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Table, Layout, Empty, Card } from '@appwrite.io/pink-svelte';
import {
Table,
Layout,
Empty,
Card,
FloatingActionBar,
Badge,
Typography
} from '@appwrite.io/pink-svelte';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import Confirm from '$lib/components/confirm.svelte';
export let data;
let selectedMembership: Models.Membership;
let showDelete = false;
let selectedMemberships: string[] = [];
let showBulkDelete = false;
let deleting = false;
const region = page.params.region;
const project = page.params.project;
async function handleBulkDelete() {
showBulkDelete = false;
deleting = true;
// Precompute a lookup map from membershipId to teamId for efficient access
const membershipIdToTeamId: Record<string, string> = {};
for (const m of data.memberships.memberships) {
membershipIdToTeamId[m.$id] = m.teamId;
}
const promises = selectedMemberships.map((membershipId) =>
sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
teamId: membershipIdToTeamId[membershipId] || '',
membershipId
})
);
try {
await Promise.all(promises);
trackEvent(Submit.MembershipUpdate, {
total: selectedMemberships.length
});
addNotification({
type: 'success',
message: `${selectedMemberships.length} membership${selectedMemberships.length > 1 ? 's' : ''} deleted`
});
invalidate(Dependencies.MEMBERSHIPS);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.MembershipUpdate);
} finally {
selectedMemberships = [];
showBulkDelete = false;
deleting = false;
}
}
</script>
<Container>
{#if data.memberships.total}
<Table.Root
let:root
allowSelection
bind:selectedRows={selectedMemberships}
columns={[
{ id: 'name' },
{ id: 'roles' },
@@ -38,7 +96,8 @@
{#each data.memberships.memberships as membership}
<Table.Row.Link
{root}
href={`${base}/project-${region}-${project}/auth/teams/team-${membership.teamId}`}>
href={`${base}/project-${region}-${project}/auth/teams/team-${membership.teamId}`}
id={membership.$id}>
<Table.Cell column="name" {root}>
<Layout.Stack direction="row" alignItems="center">
<AvatarInitials size="xs" name={membership.teamName} />
@@ -83,6 +142,36 @@
</Empty>
</Card.Base>
{/if}
{#if selectedMemberships.length > 0}
<FloatingActionBar>
<svelte:fragment slot="start">
<Badge content={selectedMemberships.length.toString()} />
<span>
{selectedMemberships.length > 1 ? 'memberships' : 'membership'}
selected
</span>
</svelte:fragment>
<svelte:fragment slot="end">
<Button text on:click={() => (selectedMemberships = [])}>Cancel</Button>
<Button secondary on:click={() => (showBulkDelete = true)}>Delete</Button>
</svelte:fragment>
</FloatingActionBar>
{/if}
</Container>
<DeleteMembership {selectedMembership} bind:showDelete />
<Confirm
title="Delete memberships"
bind:open={showBulkDelete}
onSubmit={handleBulkDelete}
disabled={deleting}>
<Typography.Text>
Are you sure you want to delete <b>{selectedMemberships.length}</b>
{selectedMemberships.length > 1 ? 'memberships' : 'membership'}?
</Typography.Text>
<Typography.Text>
This action is irreversible and will remove the user from the selected teams.
</Typography.Text>
</Confirm>
@@ -1,7 +1,10 @@
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
export const load: PageLoad = async ({ params, depends }) => {
depends(Dependencies.MEMBERSHIPS);
const codes = await sdk.forProject(params.region, params.project).locale.listCodes();
return {
localeCodes: codes.localeCodes,
@@ -12,7 +12,7 @@
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import CreateTable from './createTable.svelte';
import { showCreate } from './store';
import { showCreateTable } from './store';
import { TablesPanel } from '$lib/commandCenter/panels';
import { canWriteTables, canWriteDatabases } from '$lib/stores/roles';
import { showCreateBackup, showCreatePolicy } from './backups/store';
@@ -25,7 +25,7 @@
const databaseId = page.params.database;
async function handleCreate(event: CustomEvent<Models.Table>) {
$showCreate = false;
$showCreateTable = false;
await invalidate(Dependencies.DATABASE);
await goto(
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/table-${event.detail.$id}`
@@ -36,7 +36,7 @@
{
label: 'Create table',
callback() {
$showCreate = true;
$showCreateTable = true;
if (!page.url.pathname.endsWith(databaseId)) {
goto(
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}`
@@ -152,4 +152,4 @@
<slot />
<CreateTable bind:showCreate={$showCreate} on:created={handleCreate} />
<CreateTable bind:showCreate={$showCreateTable} on:created={handleCreate} />
@@ -2,7 +2,7 @@
import { EmptySearch, PaginationWithLimit, SearchQuery, ViewSelector } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { showCreate, tableViewColumns } from './store';
import { showCreateTable, tableViewColumns } from './store';
import Table from './table.svelte';
import Grid from './grid.svelte';
import type { PageData } from './$types';
@@ -33,7 +33,7 @@
hideView={!data.tables.total} />
{#if $canWriteTables}
<Button event="create_table" on:click={() => ($showCreate = true)}>
<Button event="create_table" on:click={() => ($showCreateTable = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Create table
</Button>
@@ -43,7 +43,7 @@
{#if data.tables.total}
{#if data.view === 'grid'}
<Grid {data} bind:showCreate={$showCreate} />
<Grid {data} bind:showCreate={$showCreateTable} />
{:else}
<Table {data} />
{/if}
@@ -77,13 +77,13 @@
href="https://appwrite.io/docs/products/databases/databases"
text
event="empty_documentation"
ariaLabel={`create table`}>Documentation</Button>
ariaLabel="create table">Documentation</Button>
{#if $canWriteTables}
<Button
secondary
on:click={() => {
$showCreate = true;
$showCreateTable = true;
}}>
Create table
</Button>
@@ -5,7 +5,7 @@ import { derived, writable } from 'svelte/store';
import { IconChartBar, IconCloudUpload, IconCog } from '@appwrite.io/pink-icons-svelte';
export const database = derived(page, ($page) => $page.data.database as Models.Database);
export const showCreate = writable(false);
export const showCreateTable = writable(false);
export const tableViewColumns = writable<Column[]>([
{ id: '$id', title: 'Table ID', type: 'string', width: 200 },
@@ -1,7 +1,7 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { showCreate, databaseSubNavigationItems } from './store';
import { showCreateTable, databaseSubNavigationItems } from './store';
import type { PageData } from './$types';
import { showSubNavigation } from '$lib/stores/layout';
import {
@@ -139,7 +139,7 @@
<Button
compact
on:click={() => {
$showCreate = true;
$showCreateTable = true;
$showSubNavigation = false;
}}>
Create table
@@ -40,7 +40,7 @@
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
import CreateColumn from './createColumn.svelte';
import { CreateColumnPanel } from '$lib/commandCenter/panels';
import { database } from '../store';
import { database, showCreateTable } from '../store';
import { project } from '../../../store';
import { page } from '$app/state';
import { base } from '$app/paths';
@@ -100,11 +100,7 @@
{
label: 'Create row',
keys: page.url.pathname.endsWith($table.$id) ? ['t'] : ['t', 'd'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/create`
);
},
callback: () => ($showCreateTable = true),
icon: IconPlus,
group: 'rows'
},
@@ -15,7 +15,7 @@
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import { isRelationship, isString } from '../rows/store';
import { isRelationship, isSpatialType, isString } from '../rows/store';
import FailedModal from '../failedModal.svelte';
import {
columns,
@@ -353,7 +353,7 @@
{#if _default === null}
<Badge variant="secondary" content="NULL" size="xs" />
{:else}
{_default}
{isSpatialType(column) ? JSON.stringify(_default) : _default}
{/if}
</Spreadsheet.Cell>
<Spreadsheet.Cell column="actions" {root} isEditable={false}>
@@ -0,0 +1,147 @@
<script module lang="ts">
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
export async function submitLine(
databaseId: string,
tableId: string,
key: string,
data: Partial<Models.ColumnLine>
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.createLineColumn({
databaseId,
tableId,
key,
required: data.required,
xdefault: data?.default
});
}
export async function updateLine(
databaseId: string,
tableId: string,
data: Partial<Models.ColumnLine>,
originalKey?: string
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.updateLineColumn({
databaseId,
tableId,
key: originalKey,
required: data.required,
xdefault: data?.default,
newKey: data.key !== originalKey ? data.key : undefined
});
}
</script>
<script lang="ts">
import { createConservative } from '$lib/helpers/stores';
import { Selector, Layout, Typography } from '@appwrite.io/pink-svelte';
import { InputLine } from '$lib/elements/forms';
import { getDefaultSpatialData } from '../store';
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnLine>;
editing?: boolean;
}
let { data = { required: false, default: null }, editing = false }: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
function handleDefaultState(hideDefault: boolean) {
if (hideDefault) {
savedDefault = data.default;
data.default = null;
} else {
data.default = savedDefault ?? getDefaultSpatialData('linestring');
}
}
function pushCoordinate() {
if (data.default) {
data.default.push(getDefaultSpatialData('point') as number[]);
data.default = [...data.default];
}
}
function deleteCoordinate(_: number) {
if (data.default && data.default.length > 2) {
data.default.pop();
data.default = [...data.default];
}
}
const {
stores: { required },
listen
} = createConservative<Partial<Models.ColumnLine>>({
required: false,
...data
});
$effect(() => {
listen(data);
});
$effect(() => {
data.required = $required;
if ($required) {
handleDefaultState(true);
}
});
onMount(() => {
if (!editing) {
savedDefault = getDefaultSpatialData('linestring');
defaultChecked = false;
$required = false;
}
});
</script>
<Selector.Checkbox
size="s"
id="required"
label="Required"
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
}}
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="default"
label="Default value"
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
$required = false;
handleDefaultState(false);
} else {
data.default = null;
}
}}
description="Enable to set a predefined value for this column" />
<Layout.Stack>
{#if defaultChecked}
<Layout.Stack direction="row" alignItems="center">
<Typography.Text variant="m-600">Default</Typography.Text>
<Typography.Caption variant="400">Optional</Typography.Caption>
</Layout.Stack>
{/if}
<InputLine
values={defaultChecked ? data.default : null}
onAddPoint={() => pushCoordinate()}
onDeletePoint={deleteCoordinate}
onChangePoint={(pointIndex: number, coordIndex: number, newValue: number) => {
if (data.default) {
data.default[pointIndex][coordIndex] = newValue;
data.default = [...data.default];
}
}} />
</Layout.Stack>
@@ -0,0 +1,136 @@
<script module lang="ts">
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
export async function submitPoint(
databaseId: string,
tableId: string,
key: string,
data: Partial<Models.ColumnPoint>
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.createPointColumn({
databaseId,
tableId,
key,
required: data.required,
xdefault: data?.default
});
}
export async function updatePoint(
databaseId: string,
tableId: string,
data: Partial<Models.ColumnPoint>,
originalKey?: string
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.updatePointColumn({
databaseId,
tableId,
key: originalKey,
required: data.required,
xdefault: data?.default,
newKey: data.key !== originalKey ? data.key : undefined
});
}
</script>
<script lang="ts">
import { InputPoint } from '$lib/elements/forms';
import { createConservative } from '$lib/helpers/stores';
import { Selector, Typography, Layout } from '@appwrite.io/pink-svelte';
import { getDefaultSpatialData } from '../store';
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnPoint>;
editing?: boolean;
}
let { data = { required: false, default: null }, editing }: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
function handleDefaultState(hideDefault: boolean) {
if (hideDefault) {
savedDefault = data.default;
data.default = null;
} else {
data.default = savedDefault ?? getDefaultSpatialData('point');
}
}
const {
stores: { required },
listen
} = createConservative<Partial<Models.ColumnPoint>>({
required: false,
...data
});
$effect(() => {
listen(data);
});
$effect(() => {
data.required = $required;
if ($required) {
handleDefaultState(true);
}
});
$effect(() => {
defaultChecked = data.default !== null;
});
onMount(() => {
if (!editing) {
savedDefault = getDefaultSpatialData('point');
defaultChecked = false;
$required = false;
}
});
</script>
<Selector.Checkbox
size="s"
id="required"
label="Required"
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
}}
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="default"
label="Default value"
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
$required = false;
handleDefaultState(false);
} else {
data.default = null;
}
}}
description="Enable to set a predefined value for this column" />
<Layout.Stack gap="xl">
{#if defaultChecked}
<Layout.Stack direction="row" alignItems="center">
<Typography.Text variant="m-600">Default</Typography.Text>
<Typography.Caption variant="400">Optional</Typography.Caption>
</Layout.Stack>
{/if}
<InputPoint
values={defaultChecked ? data.default : null}
onChangePoint={(index, newValue) => {
if (data.default) {
data.default[index] = newValue;
data.default = [...data.default];
}
}} />
</Layout.Stack>
@@ -0,0 +1,161 @@
<script module lang="ts">
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
export async function submitPolygon(
databaseId: string,
tableId: string,
key: string,
data: Partial<Models.ColumnPolygon>
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.createPolygonColumn({
databaseId,
tableId,
key,
required: data.required,
xdefault: data?.default
});
}
export async function updatePolygon(
databaseId: string,
tableId: string,
data: Partial<Models.ColumnPolygon>,
originalKey?: string
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.updatePolygonColumn({
databaseId,
tableId,
key: originalKey,
required: data.required,
xdefault: data?.default,
newKey: data.key !== originalKey ? data.key : undefined
});
}
</script>
<script lang="ts">
import { createConservative } from '$lib/helpers/stores';
import { Selector, Typography, Layout } from '@appwrite.io/pink-svelte';
import { InputPolygon } from '$lib/elements/forms';
import { getDefaultSpatialData, getSingleRingPolygon } from '../store';
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnPolygon>;
editing?: boolean;
}
let { data = { required: false, default: null }, editing = false }: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
function handleDefaultState(hideDefault: boolean) {
if (hideDefault) {
savedDefault = data.default;
data.default = null;
} else {
data.default = savedDefault ?? getDefaultSpatialData('polygon');
}
}
const {
stores: { required },
listen
} = createConservative<Partial<Models.ColumnPolygon>>({
required: false,
...data
});
function pushCoordinate(ringIndex: number) {
const ring = data.default?.at(ringIndex);
if (!ring) return;
const newPoint = getDefaultSpatialData('point') as number[];
ring.splice(ring.length - 1, 0, newPoint);
ring[ring.length - 1] = [...ring[0]];
data.default = [...(data.default || [])];
}
function pushLine() {
data.default = [...(data.default || []), getSingleRingPolygon()];
}
function deleteCoordinate(ringIndex: number) {
const ring = data.default?.at(ringIndex);
ring.splice(ring.length - 1, 1);
if (ring.length === 0) {
data.default.splice(ringIndex, 1);
}
data.default = [...(data.default || [])];
}
$effect(() => {
listen(data);
});
$effect(() => {
data.required = $required;
if ($required) {
handleDefaultState(true);
}
});
onMount(() => {
if (!editing) {
savedDefault = getDefaultSpatialData('polygon');
defaultChecked = false;
$required = false;
}
});
</script>
<Selector.Checkbox
size="s"
id="required"
label="Required"
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
else data.default = null;
}}
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="default"
label="Default value"
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
$required = false;
handleDefaultState(false);
} else {
data.default = null;
}
}}
description="Enable to set a predefined value for this column" />
<Layout.Stack gap="xl">
{#if defaultChecked}
<Layout.Stack direction="row" alignItems="center">
<Typography.Text variant="m-600">Default</Typography.Text>
<Typography.Caption variant="400">Optional</Typography.Caption>
</Layout.Stack>
{/if}
<InputPolygon
values={defaultChecked ? data.default : null}
onAddLine={pushLine}
onAddPoint={pushCoordinate}
onDeletePoint={deleteCoordinate}
onChangePoint={(lineIndex, pointIndex, coordIndex, newValue) => {
if (data.default && data.default[lineIndex] && data.default[lineIndex][pointIndex]) {
data.default[lineIndex][pointIndex][coordIndex] = newValue;
data.default = [...data.default];
}
}} />
</Layout.Stack>
@@ -138,7 +138,7 @@
$: search = data.relatedTable || undefined;
$: if (search) {
$: if (search && !editing) {
const exists = tableList?.tables?.some((c) =>
c.$id.toLocaleLowerCase().includes(search.toLocaleLowerCase())
);
@@ -191,6 +191,7 @@
bind:value={data.key}
helper="Allowed characters: a-z, A-Z, 0-9, -, ."
required />
{#if way === 'two'}
<InputText
id="keyRelated"
@@ -9,6 +9,9 @@ import Ip, { submitIp, updateIp } from './ip.svelte';
import String, { submitString, updateString } from './string.svelte';
import Url, { submitUrl, updateUrl } from './url.svelte';
import Datetime, { submitDatetime, updateDatetime } from './datetime.svelte';
import Point, { submitPoint, updatePoint } from './point.svelte';
import Line, { submitLine, updateLine } from './line.svelte';
import Polygon, { submitPolygon, updatePolygon } from './polygon.svelte';
import type { Columns } from '../store';
import Relationship, { submitRelationship, updateRelationship } from './relationship.svelte';
import {
@@ -20,7 +23,10 @@ import {
IconMail,
IconRelationship,
IconText,
IconToggle
IconToggle,
IconPoint,
IconLine,
IconPolygon
} from '@appwrite.io/pink-icons-svelte';
import type { ComponentType } from 'svelte';
@@ -35,10 +41,22 @@ export type Option = {
| 'IP'
| 'URL'
| 'Enum'
| 'Relationship';
| 'Relationship'
| 'Point'
| 'Line'
| 'Polygon';
sentenceName: string;
component: Component;
type: 'string' | 'integer' | 'double' | 'boolean' | 'datetime' | 'relationship';
type:
| 'string'
| 'integer'
| 'double'
| 'boolean'
| 'datetime'
| 'relationship'
| 'point'
| 'linestring'
| 'polygon';
create: (
databaseId: string,
tableId: string,
@@ -101,6 +119,33 @@ export const columnOptions: Option[] = [
update: updateDatetime,
icon: IconCalendar
},
{
name: 'Point',
sentenceName: 'point',
component: Point,
type: 'point',
create: submitPoint,
update: updatePoint,
icon: IconPoint
},
{
name: 'Line',
sentenceName: 'linestring',
component: Line,
type: 'linestring',
create: submitLine,
update: updateLine,
icon: IconLine
},
{
name: 'Polygon',
sentenceName: 'polygon',
component: Polygon,
type: 'polygon',
create: submitPolygon,
update: updatePolygon,
icon: IconPolygon
},
{
name: 'Email',
sentenceName: 'email',
@@ -9,7 +9,7 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { IndexType } from '@appwrite.io/console';
import { isRelationship } from '../rows/store';
import { isRelationship, isSpatialType } from '../rows/store';
import { table, indexes } from '../store';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
@@ -26,23 +26,51 @@
const databaseId = page.params.database;
let key = $state('');
let types = [
{ value: IndexType.Key, label: 'Key' },
{ value: IndexType.Unique, label: 'Unique' },
{ value: IndexType.Fulltext, label: 'Fulltext' }
];
let selectedType = $state(IndexType.Key);
let selectedType = $state<IndexType>(IndexType.Key);
let columnOptions = $table.columns
.filter((column) => !isRelationship(column))
.map((column) => ({
value: column.key,
label: column.key
}));
let columnOptions = $derived(
$table.columns
.filter((column) => {
if (selectedType === IndexType.Spatial) {
return isSpatialType(column); // keep only spatial
}
return !isRelationship(column) && !isSpatialType(column); // keep non-relationship and non-spatial
})
.map((column) => ({ value: column.key, label: column.key }))
);
let columnList = $state([{ value: '', order: '', length: null }]);
const types = [
{ value: IndexType.Key, label: 'Key' },
{ value: IndexType.Unique, label: 'Unique' },
{ value: IndexType.Fulltext, label: 'Fulltext' },
{ value: IndexType.Spatial, label: 'Spatial' }
];
// order options derived from selected type
let orderOptions = $derived.by(() =>
selectedType === IndexType.Spatial
? [
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' },
{ value: null, label: 'NONE' }
]
: [
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' }
]
);
// spatial type selected -> reset column list to single empty column
// and the column already is not spatial type
$effect(() => {
if (selectedType === IndexType.Spatial && !columnList.at(0).value) {
columnList = [{ value: '', order: null, length: null }];
}
});
function generateIndexKey() {
let indexKeys = $indexes.map((index) => index.key);
@@ -55,29 +83,41 @@
}
function initialize() {
const column = $table.columns.filter((column) => externalColumnKey === column.key);
const isSpatial = column.length && isSpatialType(column[0]);
const order = isSpatial ? null : 'ASC';
selectedType = isSpatial ? IndexType.Spatial : IndexType.Key;
columnList = externalColumnKey
? [{ value: externalColumnKey, order: 'ASC', length: null }]
: [{ value: '', order: 'ASC', length: null }];
selectedType = IndexType.Key;
? [{ value: externalColumnKey, order, length: null }]
: [{ value: '', order, length: null }];
key = `index_${$indexes.length + 1}`;
}
const addColumnDisabled = $derived(!columnList.at(-1)?.value || !columnList.at(-1)?.order);
const addColumnDisabled = $derived(
selectedType === IndexType.Spatial ||
!columnList.at(-1)?.value ||
(!columnList.at(-1)?.order && columnList.at(-1)?.order !== null)
);
const isOnIndexesPage = $derived(page.route.id?.endsWith('/indexes'));
const navigatorPathToIndexes = $derived(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${$table?.$id}/indexes`
);
let initializedForOpen = $state(false);
$effect(() => {
if (showCreateIndex) {
if (showCreateIndex && !initializedForOpen) {
initialize();
key = generateIndexKey();
initializedForOpen = true;
}
if (!showCreateIndex && initializedForOpen) {
initializedForOpen = false;
}
});
export async function create() {
if (!(key && selectedType && !addColumnDisabled)) {
if (!key || !selectedType || (selectedType !== IndexType.Spatial && addColumnDisabled)) {
addNotification({
type: 'error',
message: 'Selected column key or type invalid'
@@ -86,14 +126,15 @@
}
try {
const orders = columnList.map((a) => a.order).filter((order) => order !== null);
await sdk.forProject(page.params.region, page.params.project).tablesDB.createIndex({
databaseId,
tableId: $table.$id,
key,
type: selectedType,
columns: columnList.map((a) => a.value),
orders: columnList.map((a) => a.order),
lengths: columnList.map((a) => (a.length ? Number(a.length) : null))
lengths: columnList.map((a) => (a.length ? Number(a.length) : null)),
...(orders.length ? { orders } : {})
});
await Promise.allSettled([
@@ -151,9 +192,14 @@
<InputSelect
required
options={[
{ value: '$id', label: '$id' },
{ value: '$createdAt', label: '$createdAt' },
{ value: '$updatedAt', label: '$updatedAt' },
// allow system fields only for non-spatial index types
...(selectedType === IndexType.Spatial
? []
: [
{ value: '$id', label: '$id' },
{ value: '$createdAt', label: '$createdAt' },
{ value: '$updatedAt', label: '$updatedAt' }
]),
...columnOptions
]}
id={`column-${index}`}
@@ -162,10 +208,7 @@
bind:value={column.value} />
<InputSelect
options={[
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' }
]}
options={orderOptions}
required
id={`order-${index}`}
label={index === 0 ? 'Order' : undefined}
@@ -8,7 +8,7 @@
Typography
} from '@appwrite.io/pink-svelte';
import { IconCalendar, IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport';
import { SortButton } from '$lib/components';
import type { Column } from '$lib/helpers/types';
import {
@@ -156,7 +156,9 @@
const spreadsheetColumns = $derived(mode === 'rows' ? getRowColumns() : getIndexesColumns());
const emptyCells = $derived(($isSmallViewport ? 14 : 17) + (!$expandTabs ? 2 : 0));
const emptyCells = $derived(
($isSmallViewport ? 14 : $isTabletViewport ? 17 : 24) + (!$expandTabs ? 2 : 0)
);
</script>
<div
@@ -230,7 +232,6 @@
</SpreadsheetContainer>
{#if !$spreadsheetLoading}
<!-- Claude: Can this be truly centered without hacky left: xyz values? -->
<div
class="spreadsheet-fade-bottom"
data-collapsed-tabs={!$expandTabs}
@@ -360,6 +361,21 @@
bottom: 35%;
position: fixed;
@media (max-width: 768px) and (max-height: 768px) {
left: unset;
bottom: 12.5% !important;
}
@media (max-width: 768px) and (max-height: 1024px) {
left: unset;
bottom: 15% !important;
}
@media (max-width: 1024px) and (max-height: 1024px) {
left: unset;
bottom: 15%;
}
@media (max-width: 1024px) {
left: unset;
bottom: 30%;
@@ -1,8 +1,10 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Button, Form } from '$lib/elements/forms';
import { Badge, Divider, Layout, Sheet, Tag, Typography } from '@appwrite.io/pink-svelte';
import { Copy } from '$lib/components';
import { writable } from 'svelte/store';
import { Button, Form } from '$lib/elements/forms';
import { isTabletViewport } from '$lib/stores/viewport';
import { Badge, Divider, Layout, Sheet, Tag, Typography } from '@appwrite.io/pink-svelte';
let {
show = $bindable(false),
@@ -38,6 +40,9 @@
footer?: Snippet | null;
} = $props();
let form: Form;
let submitting = $state(writable(false));
let copyText = $state(undefined);
</script>
@@ -74,6 +79,8 @@
<Layout.Stack direction="column" justifyContent="space-evenly">
<Form
bind:this={form}
bind:isSubmitting={submitting}
onSubmit={async () => {
try {
const keepOpen = await submit?.onClick?.();
@@ -87,32 +94,38 @@
<Layout.Stack gap="xl" class="sheet-content">
{@render children?.()}
</Layout.Stack>
{#if submit}
<div class="sheet-footer">
<Layout.Stack gap="l">
<Divider />
<div class="sheet-footer-actions">
<Layout.Stack
gap="m"
direction="row"
justifyContent="flex-end"
alignItems="center">
{#if footer}
{@render footer?.()}
{/if}
<Button size="s" secondary on:click={() => (show = false)}
>Cancel</Button>
<Button size="s" submit disabled={submit.disabled}>
{submit.text}
</Button>
</Layout.Stack>
</div>
</Layout.Stack>
</div>
{/if}
</Form>
{#if submit}
<div class="sheet-footer">
<Layout.Stack gap="l">
<Divider />
<div class="sheet-footer-actions">
<Layout.Stack
gap="m"
direction="row"
justifyContent="flex-end"
alignItems="center">
{#if footer}
{@render footer?.()}
{/if}
<Button size="s" secondary on:click={() => (show = false)}
>Cancel</Button>
<Button
size="s"
submit
disabled={submit.disabled || $submitting}
forceShowLoader={$submitting && $isTabletViewport}
submissionLoader={$submitting && $isTabletViewport}
on:click={() => form?.triggerSubmit()}>
{submit.text}
</Button>
</Layout.Stack>
</div>
</Layout.Stack>
</div>
{/if}
</Layout.Stack>
</Sheet>
</div>
@@ -131,6 +144,14 @@
& :global(.sheet-content) {
// overflow-y: auto;
padding-bottom: 5rem;
@media (max-width: 768px) {
/*
* different mobile browsers handle bottom spaces differently,
* therefore, having extra bottom space doesn't hurt anyone imo!
*/
padding-bottom: 15rem;
}
}
}
@@ -141,6 +162,10 @@
position: absolute;
background: var(--bgcolor-neutral-primary);
@media (max-width: 768px) {
position: fixed;
}
& .sheet-footer-actions {
padding-inline: var(--space-8);
padding-block-end: var(--space-6);
@@ -7,6 +7,10 @@
import Relationship from './types/relationship.svelte';
import String from './types/string.svelte';
import Url from './types/url.svelte';
import Point from './types/point.svelte';
import Line from './types/line.svelte';
import Polygon from './types/polygon.svelte';
import { isSpatialType } from '../store';
export let id: string;
export let label: string;
@@ -24,7 +28,10 @@
| Models.ColumnIp
| Models.ColumnString
| Models.ColumnDatetime
| Models.ColumnUrl;
| Models.ColumnUrl
| Models.ColumnPoint
| Models.ColumnLine
| Models.ColumnPolygon;
const columnsTypeMap = {
string: String,
@@ -32,7 +39,10 @@
double: Integer,
boolean: Boolean,
datetime: Datetime,
relationship: Relationship
relationship: Relationship,
point: Point,
linestring: Line,
polygon: Polygon
};
const columnsFormatMap = {
@@ -59,7 +69,9 @@
{:else}
<!-- the `on:click` is from string > array mode for advanced edit button -->
<svelte:component
this={column.array ? columnsTypeMap['string'] : columnsTypeMap[column.type]}
this={column.array || (isSpatialType(column) && limited)
? columnsTypeMap['string']
: columnsTypeMap[column.type]}
{id}
{editing}
{limited}
@@ -5,11 +5,17 @@
import ColumnItem from './columnItem.svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
export let columns: Columns[] = [];
export let formValues: object = {};
export let customId: string | null | undefined = undefined;
let {
columns = [],
formValues = $bindable({}),
customId = $bindable(undefined)
}: {
columns: Columns[];
formValues: object;
customId: string | null | undefined;
} = $props();
let showCustomId = false;
let showCustomId = $state(false);
</script>
{#if columns.length}
@@ -26,23 +26,20 @@
let formStore = writable(formValues);
function removeArrayItem(key: string, index: number) {
const next = {
...$formStore,
[key]: $formStore[key].filter((_, i) => i !== index)
};
formStore.set(next);
onUpdateFormValues?.(next);
const currentArray = Array.isArray($formStore[key]) ? $formStore[key] : [];
const filteredArray = currentArray.filter((_: object, i: number) => i !== index);
formStore.update((values) => {
values[key] = filteredArray.length === 0 ? [] : filteredArray;
return values;
});
}
function addArrayItem(key: string) {
const next = {
...$formStore,
[key]: [...($formStore[key] ?? []), null]
};
formStore.set(next);
onUpdateFormValues?.(next);
const currentArray = Array.isArray($formStore[key]) ? $formStore[key] : null;
formStore.update((values) => {
values[key] = currentArray ? [...currentArray, null] : [null];
return values;
});
}
function getColumnType(column: Columns) {
@@ -57,8 +54,7 @@
case 'enum':
return 'Enum';
default:
'String';
break;
return 'String';
}
}
return `${capitalize(column.type)}${column.array ? '[]' : ''}`;
@@ -107,7 +103,7 @@
on:click />
{:else}
<Layout.Stack>
{#each [...($formStore[column.key]?.keys() ?? [])] as index}
{#each [...($formStore[column.key]?.keys() ?? [])] as index (index)}
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
<Column
{column}
@@ -1,6 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Models } from '@appwrite.io/console';
import { InputDateTime } from '$lib/elements/forms';
import { toLocalDateTimeISO } from '$lib/helpers/date';
import { IconCalendar } from '@appwrite.io/pink-icons-svelte';
export let id: string;
@@ -15,6 +17,12 @@
$: if (limited) {
label = undefined;
}
/**
* the value from backend will include timezone info,
* and that's not compatible with the component as it'll go empty.
*/
onMount(() => (value = value ? toLocalDateTimeISO(value) : value));
</script>
<InputDateTime
@@ -0,0 +1,67 @@
<script lang="ts">
import { InputLine } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import { Layout, Typography, Icon } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { getDefaultSpatialData } from '../../../store';
interface Props {
label: string;
value: number[][];
limited: boolean;
column: Models.ColumnLine;
}
let { label, value = $bindable(), limited = false, column }: Props = $props();
const defaultData = getDefaultSpatialData('linestring') as number[][];
function onAddPoint() {
value = [...(value || defaultData), getDefaultSpatialData('point') as number[]];
}
function onDeletePoint() {
if (value && value?.length > 2) value = value.slice(0, value.length - 1);
}
function handlePointChange(pointIndex: number, coordIndex: number, newValue: number) {
if (value && value[pointIndex]) {
value[pointIndex][coordIndex] = newValue;
value = [...value];
}
}
function handleAddDefault() {
value = getDefaultSpatialData('linestring') as number[][];
}
const nullable = $derived(!limited ? !column.required : false);
</script>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{#if nullable}
optional
{:else}
Line
{/if}
</Typography.Text>
</Layout.Stack>
{#if !value}
<Button secondary on:click={handleAddDefault}>
<Icon icon={IconPlus} slot="start" size="s" />
Add Line
</Button>
{/if}
</Layout.Stack>
<InputLine
values={value}
nullable={false}
{onAddPoint}
{onDeletePoint}
onChangePoint={handlePointChange}
minDeletableIndex={2} />
</Layout.Stack>
@@ -0,0 +1,53 @@
<script lang="ts">
import { InputPoint } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { getDefaultSpatialData } from '../../../store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
interface Props {
label: string;
value: number[];
limited: boolean;
column: Models.ColumnPoint;
}
let { label, value = $bindable(), limited = false, column }: Props = $props();
function handlePointChange(index: number, newValue: number) {
if (value) {
value[index] = newValue;
value = [...value];
}
}
function handleAddDefault() {
value = getDefaultSpatialData('point') as number[];
}
const nullable = $derived(!limited ? !column.required : false);
</script>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{#if nullable}
optional
{:else}
Point
{/if}
</Typography.Text>
</Layout.Stack>
{#if !value}
<Button secondary on:click={handleAddDefault}>
<Icon icon={IconPlus} slot="start" size="s" />
Add Point
</Button>
{/if}
</Layout.Stack>
<InputPoint values={value} nullable={false} onChangePoint={handlePointChange} />
</Layout.Stack>
@@ -0,0 +1,89 @@
<script lang="ts">
import type { Models } from '@appwrite.io/console';
import { Button } from '$lib/elements/forms';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Layout, Typography, Icon } from '@appwrite.io/pink-svelte';
import { getDefaultSpatialData, getSingleRingPolygon } from '../../../store';
import InputPolygon from '$lib/elements/forms/inputPolygon.svelte';
interface Props {
label: string;
value: number[][][];
limited: boolean;
column: Models.ColumnPolygon;
}
let { label, value = $bindable(), limited = false, column }: Props = $props();
function pushCoordinate(ringIndex: number) {
const ring = value[ringIndex];
if (!ring) return;
const newPoint = getDefaultSpatialData('point') as number[];
const newRing = [...ring];
newRing.splice(newRing.length - 1, 0, newPoint);
newRing[newRing.length - 1] = [...newRing[0]];
value = value.map((r, i) => (i === ringIndex ? newRing : r));
}
function pushLine() {
if (!value) return;
value = [...value, getSingleRingPolygon()];
}
function deleteCoordinate(ringIndex: number) {
if (!value) return;
value = value
.map((ring, i) => (i === ringIndex ? ring.slice(0, -1) : ring))
.filter((ring) => ring.length > 0);
}
function handlePointChange(
lineIndex: number,
pointIndex: number,
coordIndex: number,
newValue: number
) {
if (value && value[lineIndex] && value[lineIndex][pointIndex]) {
value[lineIndex][pointIndex][coordIndex] = newValue;
value = [...value];
}
}
function handleAddDefault() {
value = getDefaultSpatialData('polygon') as number[][][];
}
const nullable = $derived(!limited ? !column.required : false);
</script>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{#if nullable}
optional
{:else}
Polygon
{/if}
</Typography.Text>
</Layout.Stack>
{#if !value}
<Button secondary on:click={handleAddDefault}>
<Icon icon={IconPlus} slot="start" size="s" />
Add Polygon
</Button>
{/if}
</Layout.Stack>
<InputPolygon
values={value}
nullable={false}
onAddLine={pushLine}
onAddPoint={pushCoordinate}
onDeletePoint={deleteCoordinate}
onChangePoint={handlePointChange} />
</Layout.Stack>
@@ -7,7 +7,8 @@
import { type ComponentType, onMount } from 'svelte';
import { isRelationshipToMany } from '../../store';
import { IconPlus, IconRelationship, IconX } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Input, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { debounce } from '$lib/helpers/debounce';
type SelectOption = {
label: string;
@@ -33,7 +34,6 @@
let showInput = false;
let singleRel: string;
let search: string = null;
let loadingRelationships = false;
let newItemValue: string = '';
@@ -42,6 +42,23 @@
let limit = 10;
let offset = 0;
let searchNoResultsOption = undefined;
/**
* list of loaded rows to tune with searching!
*
* this is used when the view is not searching anything,
* therefore we don't have to make an API call again!
*
* Example:
* 1. initial rows are loaded -> cache the rows
* 2. user makes a search -> api call -> not results found
* 3. user clears the search -> this variable sets the rowsList back!
*
* `#3` allows us to not make a call to getRows again, saves time and bandwidth!
*/
let cachedRowsCopyList: Models.RowList<Models.Row>;
onMount(async () => {
if (isRelationshipToMany(column)) {
if (Array.isArray(value)) {
@@ -52,36 +69,51 @@
} else {
if (value && typeof value === 'object') {
row = value as Models.Row;
singleRel = row?.$id;
// set for combobox and select.
newItemValue = singleRel = row?.$id;
}
}
});
async function getRows(search: string = null) {
async function getRows() {
loadingRelationships = true;
// already includes the `$id`, dw!
const displayNames = preferences.getDisplayNames(column.relatedTable);
const queries = search
? [Query.select(displayNames), Query.startsWith('$id', search), Query.orderDesc('')]
: [Query.select(displayNames)];
const rows = await sdk
.forProject(page.params.region, page.params.project)
.tablesDB.listRows({
databaseId,
tableId: column.relatedTable,
queries
// limit `5` as `25` would look too much on sheet!
queries: [Query.select(displayNames), Query.limit(5)]
});
cachedRowsCopyList = rows;
loadingRelationships = false;
return rows;
}
function getAvailableOptions(excludeIndex?: number) {
return options?.filter((option) => {
async function searchRows(search: string = null) {
if (!search) return;
const displayNames = preferences.getDisplayNames(column.relatedTable);
const queries = [];
displayNames.forEach((name) => queries.push(Query.contains(name, [search])));
return await sdk.forProject(page.params.region, page.params.project).tablesDB.listRows({
databaseId,
tableId: column.relatedTable,
queries
});
}
function getAvailableOptions(excludeIndex?: number): SelectOption[] {
const source = options ?? [];
return source?.filter((option) => {
const otherItems =
excludeIndex !== undefined
? relatedList.filter((_, idx) => idx !== excludeIndex)
@@ -109,13 +141,13 @@
if (newItemValue) {
relatedList = [...relatedList, newItemValue];
value = relatedList;
newItemValue = '';
newItemValue = null;
showInput = false;
}
}
function cancelAddItem() {
newItemValue = '';
newItemValue = null;
showInput = false;
}
@@ -166,8 +198,42 @@
return baseOptions;
}
const debouncedSearch = debounce(async (searchTerm: string) => {
const availableOptions = getAvailableOptions();
const includesItem = availableOptions
.filter((opt) => {
const label = String(opt.label ?? '').toLowerCase();
const value = String(opt.value ?? '').toLowerCase();
return (
label.includes(searchTerm.toLowerCase()) ||
value.includes(searchTerm.toLowerCase())
);
})
.filter(Boolean);
if (!includesItem.length) {
try {
searchNoResultsOption = {
disabled: true,
message: 'Searching...'
};
const searchResults = await searchRows(searchTerm);
if (searchResults) {
rowList = searchResults;
}
} finally {
searchNoResultsOption = undefined;
}
} else if (cachedRowsCopyList) {
rowList = cachedRowsCopyList;
}
}, 500);
// Reactive statements
$: getRows(search).then((res) => (rowList = res));
$: if (!newItemValue && !cachedRowsCopyList) {
getRows().then((response) => (rowList = response));
}
$: paginatedItems = editing
? relatedList
@@ -193,6 +259,13 @@
$: if (limit && !isRelationshipToMany(column) && limited) {
label = undefined;
}
$: if (newItemValue) {
debouncedSearch(newItemValue);
} else if (!newItemValue && cachedRowsCopyList) {
rowList = cachedRowsCopyList;
searchNoResultsOption = undefined;
}
</script>
{#if isRelationshipToMany(column)}
@@ -297,22 +370,30 @@
<!-- Input for adding new items -->
{#if showInput}
<Layout.Stack direction="row">
<InputSelect
{id}
required
placeholder={`Select ${column.key}`}
bind:value={newItemValue}
options={getAvailableOptions()}
on:change={addNewItem}
leadingIcon={!limited ? IconRelationship : undefined} />
{@const availableOptions = getAvailableOptions()}
{@const noAvailableOptions = availableOptions.length <= 0}
{#key availableOptions}
<Layout.Stack direction="row">
<Input.ComboBox
{id}
required
on:change={addNewItem}
bind:value={newItemValue}
options={availableOptions}
disabled={noAvailableOptions}
placeholder={noAvailableOptions
? 'No related items available'
: `Select ${column.key}`}
noResultsOption={searchNoResultsOption}
leadingIcon={!limited ? IconRelationship : undefined} />
<div style:padding-block-start="0.5rem">
<Button icon extraCompact on:click={cancelAddItem}>
<Icon icon={IconX} size="s" />
</Button>
</div>
</Layout.Stack>
<div style:padding-block-start="0.5rem">
<Button icon extraCompact on:click={cancelAddItem}>
<Icon icon={IconX} size="s" />
</Button>
</div>
</Layout.Stack>
{/key}
{/if}
</Layout.Stack>
@@ -328,35 +409,77 @@
</Layout.Stack>
{:else}
<Layout.Stack direction="row" alignItems="center" gap="s">
<InputSelect
{id}
{options}
autofocus={limited}
bind:value={singleRel}
required={column.required}
label={limited ? undefined : label}
placeholder={`Select ${column.key}`}
on:change={() => {
if (singleRel === null) {
value = null;
} else {
value = rowList.rows.find((row) => row.$id === singleRel);
}
}}
leadingIcon={!limited ? IconRelationship : undefined} />
{#key options}
{#if limited}
<!-- for unlink badge -->
<Input.Select
{id}
{options}
autofocus={limited}
bind:value={newItemValue}
required={column.required}
label={limited ? undefined : label}
placeholder={`Select ${column.key}`}
noResultsOption={searchNoResultsOption}
on:change={() => {
if (newItemValue === null) {
value = null;
singleRel = null;
} else {
const selectedRow = rowList.rows.find(
(row) => row.$id === newItemValue
);
{#if !limited && singleRel}
<div style:padding-block-start="2.25rem">
<Button
icon
extraCompact
on:click={() => {
value = null;
singleRel = null;
}}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
if (selectedRow) {
value = selectedRow;
singleRel = newItemValue;
}
}
}}
leadingIcon={!limited ? IconRelationship : undefined} />
{:else}
{@const noOptions = options.length <= 0}
<Input.ComboBox
{id}
{options}
autofocus={limited}
bind:value={newItemValue}
required={column.required}
label={limited ? undefined : label}
disabled={noOptions}
placeholder={noOptions ? 'No related items available' : `Select ${column.key}`}
noResultsOption={searchNoResultsOption}
on:change={() => {
if (newItemValue === null) {
value = null;
singleRel = null;
} else {
const selectedRow = rowList.rows.find(
(row) => row.$id === newItemValue
);
if (selectedRow) {
value = selectedRow;
singleRel = newItemValue;
}
}
}}
leadingIcon={!limited ? IconRelationship : undefined} />
{/if}
{#if !limited && singleRel}
<div style:padding-block-start="2.25rem">
<Button
icon
extraCompact
on:click={() => {
value = null;
newItemValue = singleRel = null;
}}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
{/key}
</Layout.Stack>
{/if}
@@ -3,6 +3,7 @@
import { Layout, Link } from '@appwrite.io/pink-svelte';
import { IconText } from '@appwrite.io/pink-icons-svelte';
import { InputText, InputTextarea } from '$lib/elements/forms';
import { isSpatialType } from '../../store';
let {
id,
@@ -21,7 +22,10 @@
| Models.ColumnString
| Models.ColumnInteger
| Models.ColumnFloat
| Models.ColumnBoolean;
| Models.ColumnBoolean
| Models.ColumnPoint
| Models.ColumnLine
| Models.ColumnPolygon;
} = $props();
const autofocus = $derived(limited);
@@ -40,7 +44,6 @@
function parseValue(str: string | null): number | boolean | string | null {
const trimmed = str?.trim() ?? null;
if (!trimmed) return null;
switch (column.type) {
@@ -60,7 +63,10 @@
if (lower === 'false' || lower === '0') return false;
return null;
}
case 'point':
case 'linestring':
case 'polygon':
return trimmed;
case 'string':
default:
return trimmed;
@@ -68,7 +74,9 @@
}
$effect(() => {
if (array && Array.isArray(value)) {
if (isSpatialType(column) && Array.isArray(value)) {
stringValue = JSON.stringify(value);
} else if (array && Array.isArray(value)) {
stringValue = value.map(String).join(', ');
} else if (value !== null && value !== undefined) {
stringValue = String(value);
@@ -83,13 +91,14 @@
.split(',')
.map((item) => parseValue(item))
.filter((item) => item !== null);
if (JSON.stringify(newArray) !== JSON.stringify(value)) {
value = newArray as string[] | number[] | boolean[];
}
} else {
const parsedValue = parseValue(stringValue);
if (parsedValue !== value) {
const parsedValue = isSpatialType(column)
? JSON.parse(stringValue)
: parseValue(stringValue);
if (JSON.stringify(parsedValue) !== JSON.stringify(value)) {
value = parsedValue;
}
}
@@ -124,7 +133,7 @@
};
</script>
{#if columnSize >= 50 || array}
{#if columnSize >= 50 || array || isSpatialType(column)}
<InputTextarea
{id}
{label}
@@ -136,7 +145,7 @@
placeholder={getPlaceholder()}
leadingIcon={!limited ? IconText : undefined}>
<Layout.Stack direction="column" alignItems="flex-start" slot="end">
{#if array}
{#if array || isSpatialType(column)}
<Link.Button on:click size="s" variant="quiet">Advanced edit</Link.Button>
{/if}
</Layout.Stack>
@@ -7,7 +7,7 @@
import { writable } from 'svelte/store';
import ColumnForm from './columns/columnForm.svelte';
import { Permissions } from '$lib/components/permissions';
import { type Columns } from '../store';
import { type Columns, spreadsheetRenderKey } from '../store';
import { ID, type Models } from '@appwrite.io/console';
import { Alert, Layout, Typography, Selector } from '@appwrite.io/pink-svelte';
import SideSheet from '../layout/sidesheet.svelte';
@@ -15,6 +15,7 @@
import { Dependencies } from '$lib/constants';
import { tick } from 'svelte';
import { isRelationship, isRelationshipToMany } from './store';
import { hash } from '$lib/helpers/string';
type CreateRow = {
id?: string;
@@ -93,13 +94,15 @@
$createRow.row = prepareRowPayload($createRow);
try {
await sdk.forProject(page.params.region, page.params.project).tablesDB.createRow({
databaseId: page.params.database,
tableId: page.params.table,
rowId: $createRow.id ?? ID.unique(),
data: $createRow.row,
permissions: $createRow.permissions
});
const row = await sdk
.forProject(page.params.region, page.params.project)
.tablesDB.createRow({
databaseId: page.params.database,
tableId: page.params.table,
rowId: $createRow.id ?? ID.unique(),
data: $createRow.row,
permissions: $createRow.permissions
});
addNotification({
message: 'Row has been created',
@@ -109,7 +112,11 @@
trackEvent(Submit.RowCreate, {
customId: !!$createRow.id
});
await invalidate(Dependencies.ROW);
await invalidate(Dependencies.ROWS);
// re-render spreadsheet on addition!
spreadsheetRenderKey.set(hash(row.$id));
if (createMore) {
resetCreateRow();
@@ -1,4 +1,5 @@
import { page } from '$app/state';
import type { Column } from '$lib/helpers/types';
import type { Columns } from '../store';
import { type Models, Query } from '@appwrite.io/console';
@@ -26,6 +27,16 @@ export function isString(column: Columns): column is Models.ColumnString {
return column?.type === 'string';
}
export function isSpatialType(
column: Columns | Column
): column is Models.ColumnPoint | Models.ColumnLine | Models.ColumnPolygon {
if (!column) return false;
const spatialTypes = ['point', 'linestring', 'polygon'];
return spatialTypes.includes(column.type.toLowerCase());
}
/**
* Returns select queries for all main and related fields in a table.
*/
@@ -4,7 +4,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Confirm, Id, SortButton } from '$lib/components';
import { Dependencies, SPREADSHEET_PAGE_LIMIT } from '$lib/constants';
import { Button as ConsoleButton, InputChoice, InputSelect } from '$lib/elements/forms';
import { Button as ConsoleButton, InputSelect } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { preferences } from '$lib/stores/preferences';
import { sdk } from '$lib/stores/sdk';
@@ -16,6 +16,7 @@
buildWildcardColumnsQuery,
isRelationship,
isRelationshipToMany,
isSpatialType,
isString
} from './rows/store';
import {
@@ -66,10 +67,13 @@
IconDotsHorizontal,
IconFingerPrint,
IconHashtag,
IconLine,
IconLink,
IconLocationMarker,
IconMail,
IconPoint,
IconPlus,
IconPolygon,
IconRelationship,
IconSwitchHorizontal,
IconText,
@@ -78,7 +82,7 @@
} from '@appwrite.io/pink-icons-svelte';
import type { HeaderCellAction, RowCellAction } from './sheetOptions.svelte';
import SheetOptions from './sheetOptions.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport';
import SpreadsheetContainer from './layout/spreadsheet.svelte';
import EditRowCell from './rows/cell/edit.svelte';
import { copy } from '$lib/helpers/copy';
@@ -107,7 +111,13 @@
const organizationId = data.organization.$id ?? data.project.teamId;
const minimumWidth = 168;
const emptyCellsLimit = $isSmallViewport ? 12 : 18;
const emptyCellsLimit = $spreadsheetLoading
? 30
: $isSmallViewport
? 12
: $isTabletViewport
? 18
: 24;
let selectedRows = [];
let spreadsheetContainer: SpreadsheetContainer;
@@ -117,7 +127,6 @@
let showDelete = false;
let showColumnDelete = false;
let deleteConfirmationChecked = false;
let selectedRowForDelete: Models.Row['$id'] | null = null;
onMount(async () => {
@@ -305,6 +314,12 @@
return IconViewList;
case 'relationship':
return IconRelationship;
case 'point':
return IconPoint;
case 'linestring':
return IconLine;
case 'polygon':
return IconPolygon;
}
}
@@ -909,6 +924,10 @@
variant="secondary"
size="s" />
{/if}
{:else if isSpatialType(rowColumn) && row[columnId] !== null}
<Typography.Text truncate>
{JSON.stringify(row[columnId])}
</Typography.Text>
{:else}
{@const value = row[columnId]}
{@const formatted = formatColumn(row[columnId])}
@@ -1073,8 +1092,12 @@
</SpreadsheetContainer>
<Confirm
confirmDeletion
bind:open={showDelete}
onSubmit={handleDelete}
confirmDeletionLabel={relatedColumns?.length
? `Delete ${selectedRowForDelete !== null ? 'row' : 'rows'} from ${$table.name}`
: undefined}
title={selectedRows.length === 1 ? 'Delete Row' : 'Delete Rows'}>
{@const isSingle = selectedRowForDelete !== null}
@@ -1125,17 +1148,6 @@
<Layout.Stack direction="column" gap="m">
<Alert.Inline>To change the selection edit the relationship settings.</Alert.Inline>
<ul>
<InputChoice
id="delete"
label="Delete"
showLabel={false}
bind:value={deleteConfirmationChecked}>
Delete {isSingle ? 'row' : 'rows'} from
<span data-private>{$table.name}</span>
</InputChoice>
</ul>
</Layout.Stack>
{:else}
<p class="u-bold">This action is irreversible.</p>
@@ -1,5 +1,5 @@
import { page } from '$app/stores';
import type { Column } from '$lib/helpers/types';
import type { Column, ColumnType } from '$lib/helpers/types';
import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
import type { SortDirection } from '$lib/components';
@@ -15,6 +15,9 @@ export type Columns =
| Models.ColumnIp
| Models.ColumnString
| Models.ColumnUrl
| Models.ColumnPoint
| Models.ColumnLine
| Models.ColumnPolygon
| (Models.ColumnRelationship & { default?: never });
type Table = Omit<Models.Table, 'columns'> & {
@@ -188,3 +191,32 @@ export const PROHIBITED_ROW_KEYS = [
];
export const sheetHeightStore = writable('74.5vh');
export const getDefaultSpatialData = (
type: Extract<ColumnType, 'point' | 'linestring' | 'polygon'>
) => {
if (type === 'point') return [0, 0];
else if (type === 'linestring')
return [
[0, 0],
[0, 0]
];
else if (type === 'polygon')
return [
[
[0, 0],
[0, 0],
[0, 0],
[0, 0]
]
];
};
export const getSingleRingPolygon = () => {
return [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
];
};
@@ -270,7 +270,12 @@
href={`${base}/project-${page.params.region}-${page.params.project}/functions`}>
Cancel
</Button>
<Button submit disabled={!name || !runtime || !specification}>
<Button
submit
fullWidthMobile
submissionLoader
forceShowLoader={$isSubmitting}
disabled={!name || !runtime || !specification || $isSubmitting}>
Deploy function
</Button>
</Layout.Stack>
@@ -13,7 +13,7 @@ export const load: PageLoad = async ({ url, params, parent }) => {
redirect(302, `${base}/project-${params.region}-${params.project}/functions`);
}
const info = getRepositoryInfo(repository!);
const info = getRepositoryInfo(repository);
if (!info) {
redirect(302, `${base}/project-${params.region}-${params.project}/functions`);
}
@@ -41,8 +41,8 @@ export const load: PageLoad = async ({ url, params, parent }) => {
installations,
specificationsList,
repository: {
url: repository,
name: info.owner,
url: info.url,
name: info.name,
owner: info.owner,
rootDirectory: getNestedRootDirectory(repository)
}
@@ -78,7 +78,7 @@
{/snippet}
<Card padding="m" radius="m" {variant}>
<Layout.Stack gap="xxl">
<Layout.Stack gap="l">
<Layout.GridFraction start={4} end={6}>
<Layout.Stack>
<Layout.Stack direction="row" alignItems="center" gap="s" wrap="wrap">
@@ -17,9 +17,11 @@
} from '@appwrite.io/pink-svelte';
import { timeFromNow, toLocaleDateTime } from '$lib/helpers/date';
import { capitalize } from '$lib/helpers/string';
import { getBadgeTypeFromStatusCode } from '$lib/helpers/httpStatus';
import { Copy } from '$lib/components';
import { logStatusConverter } from './store';
import { LogsRequest, LogsResponse } from '$lib/components/logs';
import { timer } from '$lib/actions/timer';
export let selectedLogId: string;
export let logs: Models.Execution[];
@@ -98,11 +100,9 @@
<Badge
content={selectedLog.responseStatusCode.toString()}
variant="secondary"
type={selectedLog?.responseStatusCode >= 400
? 'error'
: selectedLog.responseStatusCode === 0
? undefined
: 'success'} />
type={getBadgeTypeFromStatusCode(
selectedLog.responseStatusCode
)} />
</span>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
@@ -140,7 +140,11 @@
Duration
</Typography.Text>
<Typography.Text variant="m-400">
{calculateTime(selectedLog.duration)}
{#if ['processing', 'waiting'].includes(selectedLog.status)}
<span use:timer={{ start: selectedLog.$createdAt }}></span>
{:else}
{calculateTime(selectedLog.duration)}
{/if}
</Typography.Text>
</Layout.Stack>
@@ -14,6 +14,7 @@
import Sheet from './sheet.svelte';
import { capitalize } from '$lib/helpers/string';
import { calculateTime } from '$lib/helpers/timeConversion';
import { getBadgeTypeFromStatusCode } from '$lib/helpers/httpStatus';
import { logStatusConverter } from './store';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { func } from '../store';
@@ -24,6 +25,7 @@
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { timer } from '$lib/actions/timer';
export let columns: Column[];
export let executions: Models.ExecutionList;
@@ -96,11 +98,7 @@
{:else if column.id === 'responseStatusCode'}
<Badge
variant="secondary"
type={log.responseStatusCode >= 400
? 'error'
: log.responseStatusCode === 0
? undefined
: 'success'}
type={getBadgeTypeFromStatusCode(log.responseStatusCode)}
content={log.responseStatusCode.toString()} />
{:else if column.id === 'requestMethod'}
<Typography.Code size="m">
@@ -124,7 +122,11 @@
</span>
</Tooltip>
{:else if column.id === 'duration'}
{calculateTime(log.duration)}
{#if ['processing', 'waiting'].includes(log.status)}
<span use:timer={{ start: log.$createdAt }}></span>
{:else}
{calculateTime(log.duration)}
{/if}
{/if}
</Table.Cell>
{/each}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 171 KiB

Some files were not shown because too many files have changed in this diff Show More