Merge branch 'main' into fix-dat-701
@@ -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'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -205,7 +205,7 @@
|
||||
<Navbar {...navbarProps} bind:sideBarIsOpen={$isSidebarOpen} bind:showAccountMenu />
|
||||
{/if}
|
||||
|
||||
{#if !$isNewWizardStatusOpen}
|
||||
{#if !$isNewWizardStatusOpen && isProjectPage}
|
||||
<Sidebar
|
||||
project={selectedProject}
|
||||
progressCard={getProgressCard()}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -144,7 +144,7 @@ function createFeedbackStore() {
|
||||
message,
|
||||
email,
|
||||
customFields,
|
||||
firstname: name || 'Unknown',
|
||||
firstname: (name || 'Unknown').slice(0, 40),
|
||||
metaFields: {
|
||||
source: get(feedback).source,
|
||||
orgId,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
After Width: | Height: | Size: 171 KiB |