Merge pull request #2143 from appwrite/1.8.x

1.8.x
This commit is contained in:
Jake Barnby authored and GitHub committed 2025-07-22 23:19:55 +12:00
commit 6de1ec9f78
128 files changed
+1701 -1645

No files matched your search

+5 -5
View File
@@ -22,7 +22,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@8836b0c",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@935b30f",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
"@appwrite.io/pink-legacy": "^1.0.3",
@@ -57,20 +57,20 @@
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.4",
"@testing-library/svelte": "^5.2.8",
"@testing-library/user-event": "^14.6.1",
"@types/deep-equal": "^1.0.4",
"@types/prismjs": "^1.26.5",
"@types/remarkable": "^2.0.8",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"@vitest/ui": "^3.0.9",
"@vitest/ui": "^3.2.4",
"color": "^5.0.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.0",
"eslint-plugin-svelte": "^3.3.3",
"globals": "^16.0.0",
"jsdom": "^26.0.0",
"jsdom": "^26.1.0",
"kleur": "^4.1.5",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
@@ -94,5 +94,5 @@
"svelte-preprocess"
]
},
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
"packageManager": "pnpm@10.13.1"
}
+66 -89
View File
@@ -12,8 +12,8 @@ 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@8836b0c
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@8836b0c
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@935b30f
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@935b30f
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
@@ -112,8 +112,8 @@ importers:
specifier: ^6.6.3
version: 6.6.3
'@testing-library/svelte':
specifier: ^5.2.4
version: 5.2.7(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))(vitest@3.0.9)
specifier: ^5.2.8
version: 5.2.8(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))(vitest@3.0.9)
'@testing-library/user-event':
specifier: ^14.6.1
version: 14.6.1(@testing-library/dom@10.4.0)
@@ -133,8 +133,8 @@ importers:
specifier: ^8.28.0
version: 8.28.0(eslint@9.31.0)(typescript@5.8.2)
'@vitest/ui':
specifier: ^3.0.9
version: 3.0.9(vitest@3.0.9)
specifier: ^3.2.4
version: 3.2.4(vitest@3.0.9)
color:
specifier: ^5.0.0
version: 5.0.0
@@ -151,8 +151,8 @@ importers:
specifier: ^16.0.0
version: 16.0.0
jsdom:
specifier: ^26.0.0
version: 26.0.0
specifier: ^26.1.0
version: 26.1.0
kleur:
specifier: ^4.1.5
version: 4.1.5
@@ -194,7 +194,7 @@ importers:
version: 6.2.3(@types/node@22.13.14)(sass@1.86.0)
vitest:
specifier: ^3.0.0
version: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.9)(jsdom@26.0.0)(sass@1.86.0)
version: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0)
packages:
@@ -257,8 +257,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@8836b0c':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@8836b0c}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@935b30f':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@935b30f}
version: 1.9.0
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8':
@@ -1282,8 +1282,8 @@ packages:
resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==}
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
'@testing-library/svelte@5.2.7':
resolution: {integrity: sha512-aGhUaFmEXEVost4QOsbHUUbHLwi7ZZRRxAHFDO2Cmr0BZD3/3+XvaYEPq70Rdw0NRNjdqZHdARBEcrCOkPuAqw==}
'@testing-library/svelte@5.2.8':
resolution: {integrity: sha512-ucQOtGsJhtawOEtUmbR4rRh53e6RbM1KUluJIXRmh6D4UzxR847iIqqjRtg9mHNFmGQ8Vkam9yVcR5d1mhIHKA==}
engines: {node: '>= 10'}
peerDependencies:
svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0
@@ -1478,6 +1478,9 @@ packages:
'@vitest/pretty-format@3.0.9':
resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==}
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/runner@3.0.9':
resolution: {integrity: sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==}
@@ -1487,14 +1490,17 @@ packages:
'@vitest/spy@3.0.9':
resolution: {integrity: sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==}
'@vitest/ui@3.0.9':
resolution: {integrity: sha512-FpZD4aIv/qNpwkV3XbLV6xldWFHMgoNWAJEgg5GmpObmAOLAErpYjew9dDwXdYdKOS3iZRKdwI+P3JOJcYeUBg==}
'@vitest/ui@3.2.4':
resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==}
peerDependencies:
vitest: 3.0.9
vitest: 3.2.4
'@vitest/utils@3.0.9':
resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==}
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
'@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
@@ -1625,9 +1631,6 @@ packages:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autolinker@3.16.2:
resolution: {integrity: sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==}
@@ -1750,10 +1753,6 @@ packages:
resolution: {integrity: sha512-16BlyiuyLq3MLxpRWyOTiWsO3ii/eLQLJUQXBSNcxMBBSnyt1ee9YUdaozQp03ifwm5woztEZGDbk9RGVuCsdw==}
engines: {node: '>=18'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@@ -1970,10 +1969,6 @@ packages:
delaunator@5.0.1:
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -2037,10 +2032,6 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.25.1:
resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==}
engines: {node: '>=18'}
@@ -2183,6 +2174,14 @@ packages:
picomatch:
optional: true
fdir@6.4.6:
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -2212,10 +2211,6 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
form-data@4.0.2:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
forwarded-parse@2.1.2:
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
@@ -2485,8 +2480,8 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
jsdom@26.0.0:
resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==}
jsdom@26.1.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
engines: {node: '>=18'}
peerDependencies:
canvas: ^3.0.0
@@ -2554,6 +2549,9 @@ packages:
loupe@3.1.3:
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
loupe@3.1.4:
resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -2615,14 +2613,6 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
min-indent@1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
@@ -3256,8 +3246,8 @@ packages:
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyglobby@0.2.12:
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
tinyglobby@0.2.14:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
tinypool@1.0.2:
@@ -3661,7 +3651,7 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@8836b0c': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@935b30f': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8(svelte@5.25.3)':
dependencies:
@@ -4765,13 +4755,13 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
'@testing-library/svelte@5.2.7(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))(vitest@3.0.9)':
'@testing-library/svelte@5.2.8(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))(vitest@3.0.9)':
dependencies:
'@testing-library/dom': 10.4.0
svelte: 5.25.3
optionalDependencies:
vite: 6.2.3(@types/node@22.13.14)(sass@1.86.0)
vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.9)(jsdom@26.0.0)(sass@1.86.0)
vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0)
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)':
dependencies:
@@ -5015,6 +5005,10 @@ snapshots:
dependencies:
tinyrainbow: 2.0.0
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.0.9':
dependencies:
'@vitest/utils': 3.0.9
@@ -5030,16 +5024,16 @@ snapshots:
dependencies:
tinyspy: 3.0.2
'@vitest/ui@3.0.9(vitest@3.0.9)':
'@vitest/ui@3.2.4(vitest@3.0.9)':
dependencies:
'@vitest/utils': 3.0.9
'@vitest/utils': 3.2.4
fflate: 0.8.2
flatted: 3.3.3
pathe: 2.0.3
sirv: 3.0.1
tinyglobby: 0.2.12
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.9)(jsdom@26.0.0)(sass@1.86.0)
vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0)
'@vitest/utils@3.0.9':
dependencies:
@@ -5047,6 +5041,12 @@ snapshots:
loupe: 3.1.3
tinyrainbow: 2.0.0
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
loupe: 3.1.4
tinyrainbow: 2.0.0
'@vue/compiler-core@3.5.13':
dependencies:
'@babel/parser': 7.27.0
@@ -5198,8 +5198,6 @@ snapshots:
dependencies:
tslib: 2.8.1
asynckit@0.4.0: {}
autolinker@3.16.2:
dependencies:
tslib: 2.8.1
@@ -5332,10 +5330,6 @@ snapshots:
color-convert: 3.1.0
color-string: 2.0.1
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
comma-separated-tokens@2.0.3: {}
commander@7.2.0: {}
@@ -5580,8 +5574,6 @@ snapshots:
dependencies:
robust-predicates: 3.0.2
delayed-stream@1.0.0: {}
dequal@2.0.3: {}
detect-libc@1.0.3:
@@ -5640,13 +5632,6 @@ snapshots:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.25.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.1
@@ -5828,6 +5813,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fdir@6.4.6(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
fflate@0.8.2: {}
file-entry-cache@8.0.0:
@@ -5858,13 +5847,6 @@ snapshots:
dependencies:
is-callable: 1.2.7
form-data@4.0.2:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
forwarded-parse@2.1.2: {}
framesync@6.1.2:
@@ -6132,12 +6114,11 @@ snapshots:
dependencies:
argparse: 2.0.1
jsdom@26.0.0:
jsdom@26.1.0:
dependencies:
cssstyle: 4.3.0
data-urls: 5.0.0
decimal.js: 10.5.0
form-data: 4.0.2
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
@@ -6203,6 +6184,8 @@ snapshots:
loupe@3.1.3: {}
loupe@3.1.4: {}
lru-cache@10.4.3: {}
lru-cache@5.1.1:
@@ -6271,12 +6254,6 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
min-indent@1.0.1: {}
minimatch@3.1.2:
@@ -6897,9 +6874,9 @@ snapshots:
tinyexec@0.3.2: {}
tinyglobby@0.2.12:
tinyglobby@0.2.14:
dependencies:
fdir: 6.4.3(picomatch@4.0.2)
fdir: 6.4.6(picomatch@4.0.2)
picomatch: 4.0.2
tinypool@1.0.2: {}
@@ -7063,7 +7040,7 @@ snapshots:
optionalDependencies:
vite: 6.2.3(@types/node@22.13.14)(sass@1.86.0)
vitest@3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.9)(jsdom@26.0.0)(sass@1.86.0):
vitest@3.0.9(@types/node@22.13.14)(@vitest/ui@3.2.4)(jsdom@26.1.0)(sass@1.86.0):
dependencies:
'@vitest/expect': 3.0.9
'@vitest/mocker': 3.0.9(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
@@ -7087,8 +7064,8 @@ snapshots:
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.13.14
'@vitest/ui': 3.0.9(vitest@3.0.9)
jsdom: 26.0.0
'@vitest/ui': 3.2.4(vitest@3.0.9)
jsdom: 26.1.0
transitivePeerDependencies:
- jiti
- less
+16 -16
View File
@@ -148,9 +148,9 @@ export enum Click {
ConnectRepositoryClick = 'click_connect_repository',
CreditsRedeemClick = 'click_credits_redeem',
CloudSignupClick = 'click_cloud_signup',
DatabaseAttributeDelete = 'click_attribute_delete',
DatabaseColumnDelete = 'click_column_delete',
DatabaseIndexDelete = 'click_index_delete',
DatabaseCollectionDelete = 'click_collection_delete',
DatabaseTableDelete = 'click_table_delete',
DatabaseDatabaseDelete = 'click_database_delete',
DatabaseImportCsv = 'click_database_import_csv',
DomainCreateClick = 'click_domain_create',
@@ -273,22 +273,22 @@ export enum Submit {
DatabaseDelete = 'submit_database_delete',
DatabaseUpdateName = 'submit_database_update_name',
DatabaseImportCsv = 'submit_database_import_csv',
AttributeCreate = 'submit_attribute_create',
AttributeUpdate = 'submit_attribute_update',
AttributeDelete = 'submit_attribute_delete',
DocumentCreate = 'submit_document_create',
DocumentDelete = 'submit_document_delete',
DocumentUpdate = 'submit_document_update',
DocumentUpdatePermissions = 'submit_document_update_permissions',
ColumnCreate = 'submit_column_create',
ColumnUpdate = 'submit_column_update',
ColumnDelete = 'submit_column_delete',
RowCreate = 'submit_row_create',
RowDelete = 'submit_row_delete',
RowUpdate = 'submit_row_update',
RowUpdatePermissions = 'submit_row_update_permissions',
IndexCreate = 'submit_index_create',
IndexDelete = 'submit_index_delete',
CollectionCreate = 'submit_collection_create',
CollectionDelete = 'submit_collection_delete',
CollectionUpdateName = 'submit_collection_update_name',
CollectionUpdatePermissions = 'submit_collection_update_permissions',
CollectionUpdateSecurity = 'submit_collection_update_security',
CollectionUpdateEnabled = 'submit_collection_update_enabled',
CollectionUpdateDisplayNames = 'submit_collection_update_display_names',
TableCreate = 'submit_row_create',
TableDelete = 'submit_row_delete',
TableUpdateName = 'submit_row_update_name',
TableUpdatePermissions = 'submit_row_update_permissions',
TableUpdateSecurity = 'submit_row_update_security',
TableUpdateEnabled = 'submit_row_update_enabled',
TableUpdateDisplayNames = 'submit_row_update_display_names',
FunctionCreate = 'submit_function_create',
FunctionDelete = 'submit_function_delete',
FunctionUpdateName = 'submit_function_update_name',
+3 -3
View File
@@ -28,10 +28,10 @@ const groups = [
'integrations',
'migrations',
'users',
'collections',
'attributes',
'tables',
'columns',
'indexes',
'documents',
'rows',
'teams',
'security',
'buckets',
+1 -1
View File
@@ -27,7 +27,7 @@
const examples = [
'How to add platform in the console?',
'How can I manage users, permissions, and access control in Appwrite?',
'How can I set up database collections and documents in Appwrite?',
'How can I set up database tables and rows in Appwrite?',
'How do I configure and manage server-side functions in Appwrite?',
'How to add custom domain in the console?'
];
@@ -1,16 +1,16 @@
<script lang="ts">
import { initCreateAttribute } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
import { attributeOptions } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/attributes/store';
import { initCreateColumn } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte';
import { columnOptions } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/store';
import Template from './template.svelte';
let search = '';
let options = attributeOptions.map((option) => {
let options = columnOptions.map((option) => {
return {
label: option.name,
icon: option.icon,
callback() {
initCreateAttribute(option.name);
initCreateColumn(option.name);
}
};
});
+8 -8
View File
@@ -37,16 +37,16 @@ export const DatabasesPanel: SubPanel = {
component: Databases
};
import Collections from './collections.svelte';
export const CollectionsPanel: SubPanel = {
name: 'Collections',
component: Collections
import Tables from './tables.svelte';
export const TablesPanel: SubPanel = {
name: 'Tables',
component: Tables
};
import CreateAttribute from './createAttribute.svelte';
export const CreateAttributePanel: SubPanel = {
name: 'Create Attribute',
component: CreateAttribute
import CreateColumn from './createColumn.svelte';
export const CreateColumnPanel: SubPanel = {
name: 'Create Column',
component: CreateColumn
};
import Users from './users.svelte';
@@ -1,9 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { collectionsSearcher } from '../searchers';
import { tablesSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(collectionsSearcher);
const { search, results } = initSearcher(tablesSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -1,29 +0,0 @@
import { goto } from '$app/navigation';
import { database } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
import { page } from '$app/state';
export const collectionsSearcher = (async (query: string) => {
const databaseId = get(database).$id;
const { collections } = await sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections(databaseId);
return collections
.filter((col) => col.name.toLowerCase().includes(query.toLowerCase()))
.map(
(col) =>
({
group: 'collections',
label: col.name,
callback: () => {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${col.$id}`
);
}
}) as const
);
}) satisfies Searcher;
+1 -1
View File
@@ -1,7 +1,7 @@
export * from './databases';
export * from './users';
export * from './organizations';
export * from './collections';
export * from './tables';
export * from './projects';
export * from './teams';
export * from './functions';
+27
View File
@@ -0,0 +1,27 @@
import { goto } from '$app/navigation';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
import { page } from '$app/state';
export const tablesSearcher = (async (query: string) => {
const databaseId = page.params.database;
const { tables } = await sdk
.forProject(page.params.region, page.params.project)
.tables.list(databaseId);
return tables
.filter((table) => table.name.toLowerCase().includes(query.toLowerCase()))
.map(
(table) =>
({
group: 'tables',
label: table.name,
callback: () => {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${table.$id}`
);
}
}) as const
);
}) satisfies Searcher;
+3 -2
View File
@@ -57,6 +57,7 @@
}
function updateOrAddItem(payload: Payload) {
// todo: @itznotabug - might need a change to $table?
const { $id, status, $collection, policyId } = payload;
if ($collection === 'archives' && policyId !== null) {
return;
@@ -117,9 +118,9 @@
}
function backupName(item: BackupArchive | BackupRestoration, key: string) {
const attribute = key === 'archives' ? '$createdAt' : 'startedAt';
const column = key === 'archives' ? '$createdAt' : 'startedAt';
return toLocaleDate(item[attribute]);
return toLocaleDate(item[column]);
}
onMount(() => {
+6 -7
View File
@@ -8,19 +8,18 @@
let {
columns,
isCustomCollection = false,
isCustomTable = false,
allowNoColumns = false,
children
}: {
columns: Writable<Column[]>;
isCustomCollection?: boolean;
isCustomTable?: boolean;
allowNoColumns?: boolean;
children: Snippet<[toggle: () => void, selectedColumnsNumber: number]>;
} = $props();
let maxHeight = $state('none');
let containerRef = $state<HTMLElement>(null);
const collectionId = $derived(page.params.collection);
const calcMaxHeight = () => {
if (containerRef) {
@@ -35,16 +34,16 @@
const saveColumnPreferences = () => {
const shownColumns = $columns.filter((n) => n.hide === true).map((n) => n.id);
if (isCustomCollection) {
preferences.setCustomCollectionColumns(collectionId, shownColumns);
if (isCustomTable) {
preferences.setCustomTableColumns(page.params.table, shownColumns);
} else {
preferences.setColumns(shownColumns);
}
};
onMount(() => {
if (isCustomCollection) {
const shownColumns = preferences.getCustomCollectionColumns(collectionId);
if (isCustomTable) {
const shownColumns = preferences.getCustomTableColumns(page.params.table);
columns.update((n) =>
n.map((column) => {
+18 -22
View File
@@ -13,7 +13,7 @@
type ImportItem = {
status: string;
collection?: string;
table?: string;
};
type ImportItemsMap = Map<string, ImportItem>;
@@ -22,15 +22,11 @@
* Keeps a track of the active and ongoing csv migrations.
*
* The structure is as follows -
* `{ migrationId: { status: status, collection: collection } }`
* `{ migrationId: { status: status, table: table } }`
*/
const importItems: Writable<ImportItemsMap> = writable(new Map());
async function showCompletionNotification(
database: string,
collection: string,
payload: Payload
) {
async function showCompletionNotification(database: string, table: string, payload: Payload) {
const isSuccess = payload.status === 'completed';
const isError = !isSuccess && !!payload.errors;
@@ -48,20 +44,20 @@
const type = isSuccess ? 'success' : 'error';
const message = isError ? errorMessage : 'CSV import finished successfully.';
const url = `${base}/project-${page.params.region}-${page.params.project}/databases/database-${database}/collection-${collection}`;
const url = `${base}/project-${page.params.region}-${page.params.project}/databases/database-${database}/table-${table}`;
addNotification({
type,
message,
isHtml: true,
buttons:
isSuccess && collection !== page.params.collection
? [{ name: 'View documents', method: () => goto(url) }]
isSuccess && table !== page.params.table
? [{ name: 'View rows', method: () => goto(url) }]
: undefined
});
if (isSuccess) {
await invalidate(Dependencies.DOCUMENTS);
await invalidate(Dependencies.ROWS);
}
}
@@ -70,19 +66,19 @@
const status = importData.status;
const resourceId = importData.resourceId ?? '';
const [databaseId, collectionId] = resourceId.split(':') ?? [];
const [databaseId, tableId] = resourceId.split(':') ?? [];
const current = $importItems.get(importData.$id);
let collectionName = current?.collection ?? null;
let tableName = current?.table ?? null;
if (!collectionName && collectionId) {
if (!tableName && tableId) {
try {
const collection = await sdk
const table = await sdk
.forProject(page.params.region, page.params.project)
.databases.getCollection(databaseId, collectionId);
collectionName = collection.name;
.tables.get(databaseId, tableId);
tableName = table.name;
} catch {
collectionName = null;
tableName = null;
}
}
@@ -99,12 +95,12 @@
if (shouldSkip) return items;
const next = new Map(items);
next.set(importData.$id, { status, collection: collectionName ?? undefined });
next.set(importData.$id, { status, table: tableName ?? undefined });
return next;
});
if (status === 'completed' || status === 'failed') {
await showCompletionNotification(databaseId, collectionId, importData);
await showCompletionNotification(databaseId, tableId, importData);
}
}
@@ -172,7 +168,7 @@
<header class="upload-box-header">
<h4 class="upload-box-title">
<Typography.Text variant="m-500">
Importing documents ({$importItems.size})
Importing rows ({$importItems.size})
</Typography.Text>
</h4>
<button
@@ -198,7 +194,7 @@
<div
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<Typography.Text>
{@html text(value.status, value.collection)}
{@html text(value.status, value.table)}
</Typography.Text>
</div>
<div
+24 -27
View File
@@ -26,10 +26,10 @@
...services.flatMap((s) => s.actions.map((a) => a.name)),
...services.flatMap((s) => s.resources.flatMap((r) => r.actions.map((a) => a.name)))
];
const attributeNames = [
...services.flatMap((s) => s.actions.flatMap((a) => a.attributes ?? [])),
const columnNames = [
...services.flatMap((s) => s.actions.flatMap((a) => a.columns ?? [])),
...services.flatMap((s) =>
s.resources.flatMap((r) => r.actions.flatMap((a) => a.attributes ?? []))
s.resources.flatMap((r) => r.actions.flatMap((a) => a.columns ?? []))
)
];
@@ -37,7 +37,7 @@
const isService = (s: string) => serviceNames.includes(s);
const isResource = (s: string) => resourceNames.includes(s);
const isAction = (s: string) => actionNames.includes(s);
const isAttribute = (s: string) => attributeNames.includes(s);
const isColumn = (s: string) => columnNames.includes(s);
const dispatch = createEventDispatcher();
@@ -69,7 +69,7 @@
case 'service': {
selected.resource = null;
selected.action = null;
selected.attribute = null;
selected.column = null;
break;
}
case 'resource': {
@@ -79,25 +79,25 @@
// We need to check if the selected action name is still
// on the available list, and if so, change the action to the
// appropriate one, as it may contain different attributes
// appropriate one, as it may contain different attributes/columns.
const availableAction = availableActions.find(
(a) => a.name === selected.action?.name
);
if (!availableAction) {
selected.action = null;
selected.attribute = null;
selected.column = null;
} else {
selected.action = availableAction;
if (!selected?.action?.attributes?.includes(selected.attribute)) {
selected.attribute = null;
if (!selected?.action?.columns?.includes(selected.column)) {
selected.column = null;
}
}
break;
}
case 'action': {
selected.attribute = null;
selected.column = null;
break;
}
}
@@ -113,16 +113,13 @@
if (isService(field)) {
selected.service = services.find((s) => s.name === field);
} else if (isResource(field)) {
const resource = selected.service?.resources.find((r) => r.name === field);
selected.resource = resource;
selected.resource = selected.service?.resources.find((r) => r.name === field);
} else if (isAction(field)) {
const action =
selected.action =
selected.resource?.actions.find((a) => a.name === field) ||
selected.service?.actions.find((a) => a.name === field);
selected.action = action;
} else if (isAttribute(field)) {
const attribute = selected.action?.attributes?.find((a) => a === field);
selected.attribute = attribute;
} else if (isColumn(field)) {
selected.column = selected.action?.columns?.find((a) => a === field);
}
}
}
@@ -143,7 +140,7 @@
const secondToLastField = at(fields, index - 2);
if (index === 0 || isService(currField)) return 'service';
if (isAttribute(currField) || isAction(prevField)) return 'attribute';
if (isColumn(currField) || isAction(prevField)) return 'attribute';
if (isAction(currField)) return 'action';
if (isResource(currField)) return 'resource';
if (isService(prevField) || isResource(prevField) || index === 1) {
@@ -182,7 +179,7 @@
service: null as EventService | null,
resource: null as EventResource | null,
action: null as EventAction | null,
attribute: null as string | null
column: null as string | null
};
let helper: string = null;
let customInput: string = null;
@@ -198,7 +195,7 @@
services: services,
resources: selected.service?.resources || [],
actions: (selected.resource ? selected.resource?.actions : selected.service?.actions) || [],
attributes: selected.action?.attributes || []
columns: selected.action?.columns || []
};
$: eventString = (function createEventString(): Array<{ value: string; description: string }> {
@@ -212,8 +209,8 @@
if (selected.service) {
fields.push(selected.service.name, '*');
}
if (selected.resource?.name === 'documents') {
fields.push('collections', '*');
if (selected.resource?.name === 'rows') {
fields.push('tables', '*');
}
if (selected.resource) {
fields.push(selected.resource.name, '*');
@@ -221,8 +218,8 @@
if (selected.action) {
fields.push(selected.action.name);
}
if (selected.attribute) {
fields.push(selected.attribute);
if (selected.column) {
fields.push(selected.column);
}
}
@@ -290,14 +287,14 @@
</Layout.Stack>
{/if}
{#if !empty(available.attributes)}
{#if !empty(available.columns)}
<Layout.Stack gap="s">
<Typography.Text variant="m-500">Choose an attribute (optional)</Typography.Text>
<Layout.Stack gap="s" wrap="wrap" direction="row">
{#each available.attributes as attribute}
{#each available.columns as attribute}
<Pill
disabled={showInput}
selected={selected.attribute === attribute}
selected={selected.column === attribute}
button
on:click={() => select('attribute', attribute)}>
{attribute}
+2 -2
View File
@@ -499,7 +499,7 @@
size="s"
event="empty_documentation"
href="https://appwrite.io/docs/products/storage/upload-download"
ariaLabel="create document"
ariaLabel="upload download"
>Documentation</Button>
<Button
secondary
@@ -526,7 +526,7 @@
size="s"
event="empty_documentation"
href="https://appwrite.io/docs/products/storage/buckets"
ariaLabel="create document">Documentation</Button>
ariaLabel="create buckets">Documentation</Button>
<Button
secondary
+10 -10
View File
@@ -12,8 +12,8 @@ export type TagValue = {
};
export type Operator = {
toTag: (attribute: string, input?: string | number | string[], type?: string) => TagValue;
toQuery: (attribute: string, input?: string | number | string[]) => string;
toTag: (column: string, input?: string | number | string[], type?: string) => TagValue;
toQuery: (column: string, input?: string | number | string[]) => string;
types: ColumnType[];
hideInput?: boolean;
};
@@ -134,7 +134,7 @@ export enum ValidTypes {
const operatorsDefault = new Map<
ValidOperators,
{
query: (attr: string, input: string | number | string[]) => string;
query: (col: string, input: string | number | string[]) => string;
types: ColumnType[];
hideInput?: boolean;
}
@@ -249,8 +249,8 @@ export function generateDefaultOperators() {
operatorsDefault.forEach((operator, operatorName) => {
operators[operatorName] = {
toQuery: operator.query,
toTag: (attribute, input = null, type = null) => {
return generateTag(attribute, operatorName, input, type);
toTag: (column, input = null, type = null) => {
return generateTag(column, operatorName, input, type);
},
types: operator.types,
hideInput: operator.hideInput
@@ -259,24 +259,24 @@ export function generateDefaultOperators() {
return operators;
}
export function generateTag(attribute: string, operatorName: string, input = null, type = null) {
export function generateTag(column: string, operatorName: string, input = null, type = null) {
if (input === null) {
return {
value: '',
tag: `**${attribute}** ${operatorName}`
tag: `**${column}** ${operatorName}`
};
} else if (Array.isArray(input) && input.length > 2) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
tag: `**${column}** ${operatorName} **${formatArray(input)}** `
};
} else if (type === ValidTypes.Datetime) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
tag: `**${column}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
};
} else {
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
return { value: input, tag: `**${column}** ${operatorName} **${input}**` };
}
}
+2 -2
View File
@@ -9,14 +9,14 @@
export let columns: Writable<Column[]>;
export let view: View;
export let isCustomCollection = false;
export let isCustomTable = false;
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
</script>
{#if !hideColumns && view === View.Table}
<ColumnSelector {columns} {isCustomCollection} {allowNoColumns}>
<ColumnSelector {columns} {isCustomTable} {allowNoColumns}>
{#snippet children(toggle, selectedColumnsNumber)}
<Button.Button
size="s"
+52 -9
View File
@@ -38,9 +38,9 @@ export enum Dependencies {
MEMBERSHIPS = 'dependency:memberships',
DATABASE = 'dependency:database',
DATABASES = 'dependency:databases',
COLLECTION = 'dependency:collection',
DOCUMENT = 'dependency:document',
DOCUMENTS = 'dependency:documents',
TABLE = 'dependency:table',
ROW = 'dependency:row',
ROWS = 'dependency:rows',
BUCKET = 'dependency:bucket',
FILE = 'dependency:file',
FILE_TOKENS = 'dependency:file_tokens',
@@ -64,7 +64,7 @@ export enum Dependencies {
WEBHOOK = 'dependency:webhook',
WEBHOOKS = 'dependency:webhooks',
MIGRATIONS = 'dependency:migrations',
COLLECTIONS = 'dependency:collections',
TABLES = 'dependency:tables',
BACKUPS = 'dependency:backups',
RUNTIMES = 'dependency:runtimes',
CONSOLE_VARIABLES = 'dependency:console_variables',
@@ -92,6 +92,8 @@ export const defaultScopes: string[] = [
'teams.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'projects.read',
@@ -111,6 +113,8 @@ export const defaultScopes: string[] = [
'databases.write',
'collections.read',
'collections.write',
'tables.read',
'tables.write',
'buckets.read',
'buckets.write',
'functions.read',
@@ -141,6 +145,9 @@ export const defaultScopes: string[] = [
export const defaultRoles: string[] = ['owner'];
// TODO: remove old terminology scopes.
// these are kept for backwards compatibility with keys and events that already exists.
// for the new ones, we use the new terminology.
export const scopes: {
scope: string;
description: string;
@@ -201,6 +208,18 @@ export const scopes: {
category: 'Database',
icon: 'database'
},
{
scope: 'tables.read',
description: "Access to read your project's database tables",
category: 'Database',
icon: 'database'
},
{
scope: 'tables.write',
description: "Access to create, update, and delete your project's database tables",
category: 'Database',
icon: 'database'
},
{
scope: 'attributes.read',
description: "Access to read your project's database collection's attributes",
@@ -214,6 +233,18 @@ export const scopes: {
category: 'Database',
icon: 'database'
},
{
scope: 'columns.read',
description: "Access to read your project's database table's columns",
category: 'Database',
icon: 'database'
},
{
scope: 'columns.write',
description: "Access to create, update, and delete your project's database table's columns",
category: 'Database',
icon: 'database'
},
{
scope: 'indexes.read',
description: "Access to read your project's database collection's indexes",
@@ -239,6 +270,18 @@ export const scopes: {
category: 'Database',
icon: 'database'
},
{
scope: 'rows.read',
description: "Access to read your project's database rows",
category: 'Database',
icon: 'database'
},
{
scope: 'rows.write',
description: "Access to create, update, and delete your project's database rows",
category: 'Database',
icon: 'database'
},
{
scope: 'files.read',
description: "Access to read your project's storage files and preview images",
@@ -430,7 +473,7 @@ export type EventResource = {
export type EventAction = {
name: string;
attributes?: string[];
columns?: string[];
};
export const eventServices: Array<EventService> = [
@@ -448,11 +491,11 @@ export const eventServices: Array<EventService> = [
name: 'databases',
resources: [
{
name: 'collections',
name: 'tables',
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
},
{
name: 'documents',
name: 'rows',
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
}
],
@@ -479,7 +522,7 @@ export const eventServices: Array<EventService> = [
name: 'memberships',
actions: [
{ name: 'create' },
{ name: 'update', attributes: ['status'] },
{ name: 'update', columns: ['status'] },
{ name: 'delete' }
]
}
@@ -495,7 +538,7 @@ export const eventServices: Array<EventService> = [
],
actions: [
{ name: 'create' },
{ name: 'update', attributes: ['email', 'name', 'password', 'status', 'prefs'] },
{ name: 'update', columns: ['email', 'name', 'password', 'status', 'prefs'] },
{ name: 'delete' }
]
},
+3
View File
@@ -35,6 +35,7 @@
let showDropdown = false;
// TODO: remove the default billing limits when backend is updated with billing code
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {
bandwidth: 1,
documents: 1,
@@ -42,6 +43,8 @@
users: 1,
executions: 1
};
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
const limitedServices = [
{ name: 'bandwidth', value: bandwidth },
{ name: 'documents', value: documents },
+3 -3
View File
@@ -13,14 +13,14 @@
view = $bindable(),
columns,
hideColumns = false,
isCustomCollection = false
isCustomTable = false
}: {
show?: boolean;
hideView?: boolean;
view?: View;
columns?: Writable<Column[]>;
hideColumns?: boolean;
isCustomCollection?: boolean;
isCustomTable?: boolean;
} = $props();
</script>
@@ -35,7 +35,7 @@
{#if !hideColumns && $columns?.length}
<Layout.Stack gap="xs">
<Typography.Text>Columns</Typography.Text>
<ColumnSelector {columns} {isCustomCollection}>
<ColumnSelector {columns} {isCustomTable}>
{#snippet children(toggle, selectedColumnsNumber)}
<Button.Button
size="s"
+2 -8
View File
@@ -8,7 +8,7 @@
export let title: string;
export let columns: Writable<Column[]>;
export let view: View;
export let isCustomCollection = false;
export let isCustomTable = false;
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
@@ -17,13 +17,7 @@
<header class="grid-header">
<Typography.Title size="m">{title}</Typography.Title>
<div class="u-flex u-gap-16 u-contents-mobile">
<ViewSelector
{view}
{columns}
{isCustomCollection}
{hideView}
{hideColumns}
{allowNoColumns} />
<ViewSelector {view} {columns} {isCustomTable} {hideView} {hideColumns} {allowNoColumns} />
<div class="grid-header-col-2">
<slot />
</div>
+3
View File
@@ -366,6 +366,8 @@ export async function checkForUsageLimit(org: Organization) {
return;
}
}
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {};
const resources = [
{ value: bandwidth, name: 'bandwidth' },
@@ -378,6 +380,7 @@ export async function checkForUsageLimit(org: Organization) {
const members = org.total;
const plan = get(currentPlan);
const membersOverflow =
// `plan` can be null on `onboarding/create-organization` route.
// nested null checks needed: GitHub Education plan have empty addons.
members > plan?.addons?.seats?.limit
? members - (plan?.addons?.seats?.limit || members)
+13 -12
View File
@@ -9,7 +9,7 @@ const initialFormData = {
},
databases: {
root: false,
documents: false
rows: false
},
functions: {
root: false,
@@ -45,18 +45,19 @@ export const ResourcesFriendly = {
'environment-variable': { singular: 'Environment Variable', plural: 'Environment Variables' },
deployment: { singular: 'Deployment', plural: 'Deployments' },
database: { singular: 'Database', plural: 'Databases' },
collection: { singular: 'Collection', plural: 'Collections' },
table: { singular: 'Table', plural: 'Tables' },
index: { singular: 'Index', plural: 'Indexes' },
attribute: { singular: 'Attribute', plural: 'Attributes' },
document: { singular: 'Document', plural: 'Documents' }
column: { singular: 'Column', plural: 'Columns' },
row: { singular: 'Row', plural: 'Rows' }
};
const resources = Object.keys(ResourcesFriendly);
type Resource = (typeof resources)[number];
// @todo: @itznotabug - check if other resources are correct and work fine!
export const providerResources: Record<Provider, Resource[]> = {
appwrite: [...resources],
appwrite: [...resources], // new terminology, others are ok?
supabase: [
'user',
'database',
@@ -91,12 +92,12 @@ export const migrationFormToResources = (
}
if (formData.databases.root) {
addResource('database');
addResource('collection');
addResource('attribute');
addResource('index');
addResource('table');
addResource('column');
addResource('columnIndex');
}
if (formData.databases.documents) {
addResource('document');
if (formData.databases.rows) {
addResource('row');
}
if (formData.functions.root) {
addResource('function');
@@ -150,8 +151,8 @@ export const resourcesToMigrationForm = (
if (resources.includes('database')) {
formData.databases.root = true;
}
if (includesAll(resources, ['collection', 'attribute', 'document'])) {
formData.databases.documents = true;
if (includesAll(resources, ['table', 'column', 'row'])) {
formData.databases.rows = true;
}
if (resources.includes('function') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.root = true;
+1
View File
@@ -42,6 +42,7 @@ export type OrganizationList = {
total: number;
};
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
export type BillingLimits = {
bandwidth: number;
documents: number;
+15 -10
View File
@@ -20,9 +20,13 @@ type TeamPreferences = {
type PreferencesStore = {
[key: string]: Preferences;
// kept for backwards compatibility
collections?: {
[key: string]: Preferences['columns'];
};
tables?: {
[key: string]: Preferences['columns'];
};
displayNames?: {
[key: string]: TeamPreferences['names'];
};
@@ -106,8 +110,8 @@ function createPreferences() {
}
);
},
getCustomCollectionColumns: (collectionId: string): Preferences['columns'] => {
return preferences?.collections?.[collectionId] ?? [];
getCustomTableColumns: (tableId: string): Preferences['columns'] => {
return preferences?.tables?.[tableId] ?? preferences?.collections?.[tableId] ?? [];
},
setLimit: (limit: Preferences['limit']) =>
updateAndSync((n) => {
@@ -148,14 +152,15 @@ function createPreferences() {
return n;
}),
setCustomCollectionColumns: (collectionId: string, columns: Preferences['columns']) =>
setCustomTableColumns: (tableId: string, columns: Preferences['columns']) =>
updateAndSync((n) => {
if (!n?.collections?.[collectionId]) {
n ??= {};
n.collections ??= {};
}
n ??= {};
n.collections ??= {};
n.tables ??= {};
n.collections[collectionId] = Array.from(new Set(columns));
n.tables[tableId] = Array.from(new Set(columns));
// let's not double save
// n.collections[tableId] = Array.from(new Set(columns));
return n;
}),
loadTeamPrefs: async (id: string) => {
@@ -172,7 +177,7 @@ function createPreferences() {
},
setDisplayNames: async (
orgId: string,
collectionId: string,
tableId: string,
names: TeamPreferences['names']
) => {
let teamPrefs: Models.Preferences;
@@ -183,7 +188,7 @@ function createPreferences() {
}
teamPrefs = n;
n.displayNames[collectionId] = names;
n.displayNames[tableId] = names;
return n;
});
+6 -4
View File
@@ -19,13 +19,15 @@ export const canWritePlatforms = derived(scopes, ($scopes) => $scopes.includes('
export const canWriteTargets = derived(scopes, ($scopes) => $scopes.includes('targets.write'));
export const canWriteUsers = derived(scopes, ($scopes) => $scopes.includes('users.write'));
export const canWriteTeams = derived(scopes, ($scopes) => $scopes.includes('teams.write'));
export const canWriteCollections = derived(scopes, ($scopes) =>
$scopes.includes('collections.write')
);
export const canWriteTables = derived(scopes, ($scopes) => {
return $scopes.includes('collections.write') || $scopes.includes('tables.write');
});
export const canWriteSites = derived(scopes, ($scopes) => $scopes.includes('sites.write'));
export const canWriteDomains = derived(scopes, ($scopes) => $scopes.includes('domains.write')); //TODO: check if correct
export const canWriteDocuments = derived(scopes, ($scopes) => $scopes.includes('documents.write'));
export const canWriteRows = derived(scopes, ($scopes) => {
return $scopes.includes('documents.write') || $scopes.includes('rows.write');
});
export const canWriteExecutions = derived(scopes, ($scopes) =>
$scopes.includes('executions.write')
);
+3 -1
View File
@@ -21,6 +21,7 @@ import {
Vcs,
Sites,
Tokens,
Tables,
Domains
} from '@appwrite.io/console';
import { Billing } from '../sdk/billing';
@@ -122,7 +123,8 @@ const sdkForProject = {
vcs: new Vcs(clientProject),
proxy: new Proxy(clientProject),
migrations: new Migrations(clientProject),
sites: new Sites(clientProject)
sites: new Sites(clientProject),
tables: new Tables(clientProject)
};
export const realtime = {
@@ -7,6 +7,7 @@ import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, params, route, depends }) => {
depends(Dependencies.ORGANIZATION);
depends(Dependencies.MEMBERS);
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, PAGE_LIMIT);
@@ -6,6 +6,7 @@
import { capitalize } from '$lib/helpers/string';
import { currentPlan } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
function getResource(id: string) {
id = id.replace('/(console)/project-[region]-[project]/', '');
let parts = id.split('/');
@@ -39,6 +40,7 @@
>{'status' in page.error
? page.error.status || 'Invalid Argument'
: 'Invalid Argument'}</Typography.Title>
<p class="body-text-2 u-bold u-margin-block-start-4">{page.error.message}</p>
<Typography.Title>{page.error.message}</Typography.Title>
{/if}
</Container>
@@ -82,6 +82,7 @@
const database = await sdk
.forProject(page.params.region, page.params.project)
.databases.create(databaseId, name);
await createPolicies(databaseId);
showCreate = false;
@@ -8,13 +8,13 @@
registerSearchers,
updateCommandGroupRanks
} from '$lib/commandCenter';
import { collectionsSearcher } from '$lib/commandCenter/searchers';
import { tablesSearcher } from '$lib/commandCenter/searchers';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import CreateCollection from './createCollection.svelte';
import CreateTable from './createTable.svelte';
import { showCreate } from './store';
import { CollectionsPanel } from '$lib/commandCenter/panels';
import { canWriteCollections, canWriteDatabases } from '$lib/stores/roles';
import { TablesPanel } from '$lib/commandCenter/panels';
import { canWriteTables, canWriteDatabases } from '$lib/stores/roles';
import { showCreateBackup, showCreatePolicy } from './backups/store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { currentPlan } from '$lib/stores/organization';
@@ -23,17 +23,17 @@
const project = page.params.project;
const databaseId = page.params.database;
async function handleCreate(event: CustomEvent<Models.Collection>) {
async function handleCreate(event: CustomEvent<Models.Table>) {
$showCreate = false;
await invalidate(Dependencies.DATABASE);
await goto(
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/collection-${event.detail.$id}`
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/table-${event.detail.$id}`
);
}
$: $registerCommands([
{
label: 'Create collection',
label: 'Create table',
callback() {
$showCreate = true;
if (!page.url.pathname.endsWith(databaseId)) {
@@ -43,7 +43,7 @@
}
},
keys: page.url.pathname.endsWith(databaseId) ? ['c'] : ['c', 'c'],
disabled: page.url.pathname.includes('collection-') || !$canWriteCollections,
disabled: page.url.pathname.includes('table-') || !$canWriteTables,
group: 'databases',
icon: IconPlus
},
@@ -80,14 +80,14 @@
disabled: !isCloud || !$currentPlan.backupsEnabled
},
{
label: 'Go to collections',
label: 'Go to tables',
callback() {
goto(
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}`
);
},
disabled:
page.url.pathname.endsWith(databaseId) || page.url.pathname.includes('collection-'),
page.url.pathname.endsWith(databaseId) || page.url.pathname.includes('table-'),
keys: ['g', 'c'],
group: 'databases'
},
@@ -98,8 +98,7 @@
`${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/usage`
);
},
disabled:
page.url.pathname.includes('/usage') || page.url.pathname.includes('collection-'),
disabled: page.url.pathname.includes('/usage') || page.url.pathname.includes('table-'),
keys: ['g', 'u'],
group: 'databases'
},
@@ -111,7 +110,7 @@
);
},
disabled:
page.url.pathname.includes('/backups') || page.url.pathname.includes('collection-'),
page.url.pathname.includes('/backups') || page.url.pathname.includes('table-'),
keys: ['g', 'b'],
group: 'databases'
},
@@ -124,24 +123,24 @@
},
disabled:
page.url.pathname.includes('/settings') ||
page.url.pathname.includes('collection-') ||
page.url.pathname.includes('table-') ||
!$canWriteDatabases,
keys: ['g', 's'],
group: 'databases'
},
{
label: 'Find collections',
label: 'Find tables',
callback: () => {
addSubPanel(CollectionsPanel);
addSubPanel(TablesPanel);
},
group: 'databases',
rank: -1
}
]);
$registerSearchers(collectionsSearcher);
$registerSearchers(tablesSearcher);
$: $updateCommandGroupRanks({ collections: 10 });
$: $updateCommandGroupRanks({ tables: 10 });
</script>
<svelte:head>
@@ -150,4 +149,4 @@
<slot />
<CreateCollection bind:showCreate={$showCreate} on:created={handleCreate} />
<CreateTable bind:showCreate={$showCreate} on:created={handleCreate} />
@@ -2,11 +2,11 @@
import { Empty, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { columns, showCreate } from './store';
import { tableViewColumns, showCreate } from './store';
import Table from './table.svelte';
import Grid from './grid.svelte';
import type { PageData } from './$types';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
@@ -16,18 +16,18 @@
<Container>
<ResponsiveContainerHeader
bind:view={data.view}
{columns}
columns={tableViewColumns}
hasSearch
searchPlaceholder="Search by name or ID">
{#if $canWriteCollections}
<Button on:click={() => ($showCreate = true)} event="create_collection">
{#if $canWriteTables}
<Button on:click={() => ($showCreate = true)} event="create_table">
<Icon icon={IconPlus} slot="start" size="s" />
Create collection
Create table
</Button>
{/if}
</ResponsiveContainerHeader>
{#if data.collections.total}
{#if data.tables.total}
{#if data.view === 'grid'}
<Grid {data} bind:showCreate={$showCreate} />
{:else}
@@ -35,16 +35,16 @@
{/if}
<PaginationWithLimit
name="Collections"
name="Tables"
limit={data.limit}
offset={data.offset}
total={data.collections.total} />
total={data.tables.total} />
{:else}
<Empty
single
allowCreate={$canWriteCollections}
href="https://appwrite.io/docs/products/databases/collections"
target="collection"
allowCreate={$canWriteTables}
href="https://appwrite.io/docs/products/databases/tables"
target="table"
on:click={() => ($showCreate = true)} />
{/if}
</Container>
@@ -5,15 +5,16 @@ import type { PageLoad } from './$types';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
export const load: PageLoad = async ({ params, url, route, depends }) => {
depends(Dependencies.COLLECTIONS);
depends(Dependencies.TABLES);
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, CARD_LIMIT);
const view = getView(url, route, View.Grid);
const offset = pageToOffset(page, limit);
const collections = await sdk
const tables = await sdk
.forProject(params.region, params.project)
.databases.listCollections(
.tables.list(
params.database,
[Query.limit(limit), Query.offset(offset), Query.orderDesc('')],
search || undefined
@@ -24,6 +25,6 @@ export const load: PageLoad = async ({ params, url, route, depends }) => {
limit,
search,
view,
collections
tables
};
};
@@ -0,0 +1,41 @@
import { base } from '$app/paths';
import type { PageLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { AppwriteException } from '@appwrite.io/console';
const LEGACY_ROUTE_MAPPINGS = [
{ pattern: /^collection-([^/]+)/, replacement: 'table-$1' },
{ pattern: /^document-([^/]+)/, replacement: 'row-$1' },
{ pattern: /^attribute-([^/]+)/, replacement: 'column-$1' }
] as const;
function isLegacyRoute(segments: string[]): boolean {
return segments.some((segment) =>
LEGACY_ROUTE_MAPPINGS.some((mapping) => mapping.pattern.test(segment))
);
}
function rewriteLegacySegments(segments: string[]): string[] {
return segments.map((segment) => {
for (const mapping of LEGACY_ROUTE_MAPPINGS) {
if (mapping.pattern.test(segment)) {
return segment.replace(mapping.pattern, mapping.replacement);
}
}
return segment;
});
}
export const load: PageLoad = async ({ params, url }) => {
const restSegments = params.rest ? params.rest.split('/').filter(Boolean) : [];
const baseUrl = `${base}/project-${params.region}-${params.project}/databases/database-${params.database}`;
if (isLegacyRoute(restSegments)) {
const rewrittenSegments = rewriteLegacySegments(restSegments);
const newPath = `${baseUrl}/${rewrittenSegments.join('/')}`;
redirect(308, newPath + url.search);
}
throw new AppwriteException('Not Found', 404);
};
@@ -1,63 +0,0 @@
<script lang="ts">
import type { Models } from '@appwrite.io/console';
import Boolean from './attributes/boolean.svelte';
import Datetime from './attributes/datetime.svelte';
import Enum from './attributes/enum.svelte';
import Integer from './attributes/integer.svelte';
import Relationship from './attributes/relationship.svelte';
import String from './attributes/string.svelte';
import Url from './attributes/url.svelte';
export let id: string;
export let label: string;
export let optionalText: string | undefined = undefined;
export let value: string | number | boolean | null | string[];
export let editing = false;
export let attribute:
| Models.AttributeBoolean
| Models.AttributeEmail
| Models.AttributeEnum
| Models.AttributeFloat
| Models.AttributeInteger
| Models.AttributeIp
| Models.AttributeString
| Models.AttributeDatetime
| Models.AttributeUrl;
const attributesTypeMap = {
string: String,
integer: Integer,
double: Integer,
boolean: Boolean,
datetime: Datetime,
relationship: Relationship
};
const attributesFormatMap = {
ip: String,
url: Url,
email: String,
enum: Enum
};
</script>
{#if attribute.type in attributesTypeMap}
{#if 'format' in attribute && attribute.format}
<svelte:component
this={attributesFormatMap[attribute.format]}
{id}
{label}
{attribute}
{optionalText}
bind:value />
{:else}
<svelte:component
this={attributesTypeMap[attribute.type]}
{id}
{editing}
{label}
{attribute}
{optionalText}
bind:value />
{/if}
{/if}
@@ -1,37 +0,0 @@
import { type Models, Query } from '@appwrite.io/console';
import type { Attributes } from '../../store';
export function isRelationshipToMany(attribute: Models.AttributeRelationship) {
if (!attribute) return false;
if (!attribute?.relationType) return false;
if (attribute?.side === 'child') {
return !['oneToOne', 'oneToMany'].includes(attribute?.relationType);
} else {
return !['oneToOne', 'manyToOne'].includes(attribute?.relationType);
}
}
export function isRelationship(attribute: Attributes): attribute is Models.AttributeRelationship {
if (!attribute) return false;
return attribute?.type === 'relationship';
}
export function isString(attribute: Attributes): attribute is Models.AttributeString {
if (!attribute) return false;
return attribute?.type === 'string';
}
/**
* Returns select queries for all main and related fields in a collection.
*/
export function buildWildcardAttributesQuery(
collection: Models.Collection | null = null
): string[] {
return [
...(collection?.attributes
?.filter((attr) => isRelationship(attr))
?.map((attr) => Query.select([`${attr.key}.*`])) ?? []),
Query.select(['*'])
];
}
@@ -1,72 +0,0 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, InputSwitch } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { collection } from '../store';
const databaseId = page.params.database;
let collectionDocumentSecurity: boolean = null;
onMount(() => {
collectionDocumentSecurity ??= $collection.documentSecurity;
});
async function updateSecurity() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateCollection(
databaseId,
$collection.$id,
$collection.name,
$collection.$permissions,
collectionDocumentSecurity,
$collection.enabled
);
await invalidate(Dependencies.COLLECTION);
addNotification({
message: 'Security has been updated',
type: 'success'
});
trackEvent(Submit.CollectionUpdateSecurity);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.CollectionUpdateSecurity);
}
}
</script>
<CardGrid>
<svelte:fragment slot="title">Document security</svelte:fragment>
<svelte:fragment slot="aside">
<InputSwitch
bind:value={collectionDocumentSecurity}
id="security"
label="Document security" />
<p class="text">
When document security is enabled, users will be able to access documents for which they
have been granted <b>either document or collection permissions</b>.
</p>
<p class="text">
If document security is disabled, users can access documents <b
>only if they have collection permissions</b
>. Document permissions will be ignored.
</p>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={collectionDocumentSecurity === $collection.documentSecurity}
on:click={updateSecurity}>Update</Button>
</svelte:fragment>
</CardGrid>
@@ -1,30 +0,0 @@
import { page } from '$app/stores';
import type { Column } from '$lib/helpers/types';
import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
export type Attributes =
| Models.AttributeBoolean
| Models.AttributeEmail
| Models.AttributeEnum
| Models.AttributeFloat
| Models.AttributeInteger
| Models.AttributeIp
| Models.AttributeString
| Models.AttributeUrl
| (Models.AttributeRelationship & { default?: never });
type Collection = Omit<Models.Collection, 'attributes'> & {
attributes: Array<Attributes>;
};
export const collection = derived(page, ($page) => $page.data.collection as Collection);
export const attributes = derived(
page,
($page) => $page.data.collection.attributes as Attributes[]
);
export const indexes = derived(page, ($page) => $page.data.collection.indexes as Models.Index[]);
export const columns = writable<Column[]>([]);
export const isCsvImportInProgress = writable(false);
@@ -24,22 +24,24 @@
const create = async () => {
error = null;
try {
const collection = await sdk
const table = await sdk
.forProject(page.params.region, page.params.project)
.databases.createCollection(databaseId, id ? id : ID.unique(), name);
.tables.create(databaseId, id ? id : ID.unique(), name);
showCreate = false;
dispatch('created', collection);
dispatch('created', table);
addNotification({
type: 'success',
message: `${name} has been created`
});
name = id = null;
trackEvent(Submit.CollectionCreate, {
trackEvent(Submit.TableCreate, {
customId: !!id
});
} catch (e) {
error = e.message;
trackError(e, Submit.CollectionCreate);
trackError(e, Submit.TableCreate);
}
};
@@ -51,11 +53,11 @@
}
</script>
<Modal title="Create collection" size="m" bind:show={showCreate} onSubmit={create} bind:error>
<Modal title="Create table" size="m" bind:show={showCreate} onSubmit={create} bind:error>
<InputText
id="name"
label="Name"
placeholder="Enter collection name"
placeholder="Enter table name"
bind:value={name}
autofocus
required />
@@ -66,10 +68,10 @@
size="s"
on:click={() => {
showCustomId = true;
}}><Icon icon={IconPencil} /> Collection ID</Tag>
}}><Icon icon={IconPencil} /> Table ID</Tag>
</div>
{/if}
<CustomId autofocus bind:show={showCustomId} name="Collection" bind:id />
<CustomId autofocus bind:show={showCustomId} name="Table" bind:id />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
@@ -18,17 +18,17 @@
let confirmedDeletion = false;
let error = null;
let isLoadingDocumentsCount = false;
let isLoadingRowsCount = false;
$: collectionItems = [];
let collections: Models.CollectionList = null;
$: tableItems = [];
let tables: Models.TableList = null;
function buildQueries(): string[] {
const queries = [Query.orderDesc('$updatedAt')];
if (collectionItems.length > 0) {
if (tableItems.length > 0) {
queries.push(Query.limit(25));
queries.push(Query.offset(collectionItems.length));
queries.push(Query.offset(tableItems.length));
} else {
queries.push(Query.limit(3));
}
@@ -36,32 +36,32 @@
return queries;
}
async function listCollections() {
async function listTables() {
// let's just wait...
if (isLoadingDocumentsCount) return;
if (isLoadingRowsCount) return;
isLoadingDocumentsCount = true;
isLoadingRowsCount = true;
try {
const queries = buildQueries();
collections = await sdk
tables = await sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections(databaseId, queries);
.tables.list(databaseId, queries);
const collectionPromises = collections.collections.map(async (collection) => {
const tablePromises = tables.tables.map(async (table) => {
return {
id: collection.$id,
name: collection.name,
updatedAt: collection.$updatedAt
id: table.$id,
name: table.name,
updatedAt: table.$updatedAt
};
});
collectionItems = [...collectionItems, ...(await Promise.all(collectionPromises))];
tableItems = [...tableItems, ...(await Promise.all(tablePromises))];
} catch (err) {
error = true;
} finally {
isLoadingDocumentsCount = false;
isLoadingRowsCount = false;
}
}
@@ -88,24 +88,24 @@
/* reset data on modal close */
$: if (!showDelete) {
collections = null;
collectionItems = [];
tables = null;
tableItems = [];
} else {
listCollections();
listTables();
}
</script>
<Modal title="Delete database" bind:show={showDelete} onSubmit={handleDelete}>
<p class="text" slot="description">
{#if collectionItems.length > 0}
The following collections and all data associated with <b>{$database.name}</b>, will be
{#if tableItems.length > 0}
The following tables and all data associated with <b>{$database.name}</b>, will be
permanently deleted.
{:else}
Are you sure you want to delete <b>{$database.name}</b>?
{/if}
</p>
{#if isLoadingDocumentsCount}
{#if isLoadingRowsCount}
<div class="u-flex u-main-center">
<Spinner />
</div>
@@ -113,36 +113,36 @@
<p class="text">
Are you sure you want to delete <b>{$database.name}</b>?
</p>
{:else if collectionItems.length > 0}
{:else if tableItems.length > 0}
<div class="u-flex-vertical u-gap-16">
<Table.Root columns={2} let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Collection</Table.Header.Cell>
<Table.Header.Cell {root}>Table</Table.Header.Cell>
<Table.Header.Cell {root}>Last Updated</Table.Header.Cell>
</svelte:fragment>
{#each collectionItems as collection}
{#each tableItems as table}
<Table.Row.Base {root}>
<Table.Cell {root}>{collection.name}</Table.Cell>
<Table.Cell {root}>{toLocaleDate(collection.updatedAt)}</Table.Cell>
<Table.Cell {root}>{table.name}</Table.Cell>
<Table.Cell {root}>{toLocaleDate(table.updatedAt)}</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{#if collectionItems.length < collections.total}
{#if tableItems.length < tables.total}
<div class="u-flex u-gap-16 u-cross-center">
<button class="u-underline" on:click={listCollections} type="button">
<button class="u-underline" on:click={listTables} type="button">
Show more
</button>
{#if isLoadingDocumentsCount}
{#if isLoadingRowsCount}
<div class="loader is-small"></div>
{/if}
</div>
{:else if collectionItems.length > 25}
{:else if tableItems.length > 25}
<button
class="u-underline"
on:click={() => {
collectionItems = collectionItems.slice(0, 3);
tableItems = tableItems.slice(0, 3);
}}
type="button">
Show less
@@ -151,7 +151,7 @@
</div>
{/if}
{#if !isLoadingDocumentsCount}
{#if !isLoadingRowsCount}
<p class="text" data-private>
<b>
Once deleted, this database and its backups cannot be restored. This action is
@@ -162,7 +162,7 @@
<div class="input-check-box-friction">
<InputCheckbox
required
id="delete_policy"
id="delete_database"
bind:checked={confirmedDeletion}
label="I understand and confirm" />
</div>
@@ -2,9 +2,9 @@
import { base } from '$app/paths';
import { page } from '$app/state';
import { CardContainer, GridItem1, Id } from '$lib/components';
import { Pill } from '$lib/elements';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
import type { PageData } from './$types';
import { Badge } from '@appwrite.io/pink-svelte';
export let data: PageData;
export let showCreate = false;
const projectId = page.params.project;
@@ -12,20 +12,20 @@
</script>
<CardContainer
disableEmpty={!$canWriteCollections}
total={data.collections.total}
disableEmpty={!$canWriteTables}
total={data.tables.total}
on:click={() => (showCreate = true)}
event="collection">
{#each data.collections.collections as collection}
event="table">
{#each data.tables.tables as table}
<GridItem1
href={`${base}/project-${page.params.region}-${projectId}/databases/database-${databaseId}/collection-${collection.$id}`}>
<svelte:fragment slot="title">{collection.name}</svelte:fragment>
href={`${base}/project-${page.params.region}-${projectId}/databases/database-${databaseId}/table-${table.$id}`}>
<svelte:fragment slot="title">{table.name}</svelte:fragment>
<svelte:fragment slot="status">
{#if !collection.enabled}
<Pill>disabled</Pill>
{#if !table.enabled}
<Badge variant="secondary" content="disabled" />
{/if}</svelte:fragment>
<Id value={collection.$id}>{collection.$id}</Id>
<Id value={table.$id}>{table.$id}</Id>
</GridItem1>
{/each}
</CardContainer>
@@ -13,8 +13,8 @@
const tabs = [
{
href: path,
title: 'Collections',
event: 'collections',
title: 'Tables',
event: 'tables',
hasChildren: true
},
{
@@ -13,6 +13,7 @@
import Delete from '../delete.svelte';
import { database } from '../store';
import { Query } from '@appwrite.io/console';
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
let showDelete = false;
let showError: false | 'name' | 'email' | 'password' = false;
@@ -24,10 +25,10 @@
databaseName ??= $database.name;
});
async function loadCollectionCount() {
async function loadTableCount() {
const { total } = await sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections($database.$id, [Query.limit(1)]);
.tables.list($database.$id, [Query.limit(1)]);
return total;
}
@@ -95,21 +96,21 @@
<CardGrid>
<svelte:fragment slot="title">Delete database</svelte:fragment>
The database will be permanently deleted, including all collections within it. This action
is irreversible.
The database will be permanently deleted, including all tables within it. This action is
irreversible.
<svelte:fragment slot="aside">
<BoxAvatar>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$database.name}</h6>
<span class="u-flex u-gap-8">
{#await loadCollectionCount()}
<div class="loader is-small"></div>
{:then count}
{count}
{/await}
Collections
</span>
<Layout.Stack direction="column" gap="xxs">
<h6 class="u-bold u-trim-1">{$database.name}</h6>
<Layout.Stack direction="row" gap="s">
{#await loadTableCount()}
<Skeleton variant="line" width="100%" height={19.5} />
{:then count}
{count} {count === 1 ? 'Table' : 'Tables'}
{/await}
</Layout.Stack>
</Layout.Stack>
</svelte:fragment>
</BoxAvatar>
</svelte:fragment>
@@ -6,8 +6,8 @@ import { derived, writable } from 'svelte/store';
export const database = derived(page, ($page) => $page.data.database as Models.Database);
export const showCreate = writable(false);
export const columns = writable<Column[]>([
{ id: '$id', title: 'Collection ID', type: 'string', width: 200 },
export const tableViewColumns = writable<Column[]>([
{ id: '$id', title: 'Table ID', type: 'string', width: 200 },
{ id: 'name', title: 'Name', type: 'string', width: { min: 120 } },
{ id: '$createdAt', title: 'Created', type: 'datetime', width: { min: 120 } },
{ id: '$updatedAt', title: 'Updated', type: 'datetime', width: { min: 120 } }
@@ -5,15 +5,15 @@
export async function submitString(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeString>
data: Partial<Models.ColumnString>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createStringAttribute(
.tables.createStringColumn(
databaseId,
collectionId,
tableId,
key,
data.size,
data.required,
@@ -23,15 +23,15 @@
}
export async function updateString(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeString>,
tableId: string,
data: Partial<Models.ColumnString>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateStringAttribute(
.tables.updateStringColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -42,10 +42,10 @@
</script>
<script lang="ts">
import { InputChoice, InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
import { createConservative } from '$lib/helpers/stores';
import { InputChoice, InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
export let data: Partial<Models.AttributeString> = {
export let data: Partial<Models.ColumnString> = {
required: false,
size: 0,
default: null,
@@ -67,7 +67,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeString>>({
} = createConservative<Partial<Models.ColumnString>>({
required: false,
array: false,
...data
@@ -1,10 +1,13 @@
<script lang="ts" context="module">
import { writable } from 'svelte/store';
import type { Option } from './columns/store';
const createAttributeArgs = writable({
showCreate: false,
selectedOption: null as Option['name'] | null
});
export const initCreateAttribute = (option: Option['name']) => {
export const initCreateColumn = (option: Option['name']) => {
createAttributeArgs.set({ showCreate: true, selectedOption: option });
};
@@ -19,18 +22,16 @@
import { Dependencies } from '$lib/constants';
import { realtime } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { collection } from './store';
import { table } from './store';
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
import CreateAttribute from './createAttribute.svelte';
import { writable } from 'svelte/store';
import type { Option } from './attributes/store';
import { CreateAttributePanel } from '$lib/commandCenter/panels';
import CreateAttribute from './createColumn.svelte';
import { CreateColumnPanel } from '$lib/commandCenter/panels';
import { database } from '../store';
import { project } from '$routes/(console)/project-[region]-[project]/store';
import { page } from '$app/state';
import CreateIndex from './indexes/createIndex.svelte';
import { base } from '$app/paths';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
import { IconEye, IconLockClosed, IconPlus, IconPuzzle } from '@appwrite.io/pink-icons-svelte';
onMount(() => {
@@ -38,142 +39,143 @@
.forProject(page.params.region, page.params.project)
.subscribe(['project', 'console'], (response) => {
if (
response.events.includes('databases.*.collections.*.attributes.*') ||
response.events.includes('databases.*.collections.*.indexes.*')
response.events.includes('databases.*.tables.*.columns.*') ||
response.events.includes('databases.*.tables.*.indexes.*')
) {
invalidate(Dependencies.COLLECTION);
invalidate(Dependencies.TABLE);
}
});
});
// TODO: use route ids instead of pathname
$: $registerCommands([
{
label: 'Create document',
keys: page.url.pathname.endsWith($collection.$id) ? ['c'] : ['c', 'd'],
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}/collection-${$collection?.$id}/create`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/create`
);
},
icon: IconPlus,
group: 'documents'
group: 'rows'
},
{
label: 'Create attribute',
keys: page.url.pathname.endsWith('attributes') ? ['c'] : ['c', 'a'],
label: 'Create column',
keys: page.url.pathname.endsWith('columns') ? ['t'] : ['t', 'a'],
callback() {
addSubPanel(CreateAttributePanel);
addSubPanel(CreateColumnPanel);
},
icon: IconPlus,
group: 'attributes',
disabled: !$canWriteCollections
group: 'columns',
disabled: !$canWriteTables
},
{
label: 'Go to documents',
label: 'Go to rows',
keys: ['g', 'd'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}`
);
},
disabled: page.url.pathname.endsWith($collection.$id),
group: 'collections'
disabled: page.url.pathname.endsWith($table.$id),
group: 'tables'
},
{
label: 'Go to attributes',
label: 'Go to columns',
keys: ['g', 'a'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/attributes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/columns`
);
},
disabled: page.url.pathname.endsWith('attributes'),
group: 'collections'
disabled: page.url.pathname.endsWith('columns'),
group: 'tables'
},
{
label: 'Go to indexes',
keys: ['g', 'i'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/indexes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/indexes`
);
},
disabled: page.url.pathname.endsWith('indexes'),
group: 'collections'
group: 'tables'
},
{
label: 'Go to activity',
keys: ['g', 'c'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/activity`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/activity`
);
},
disabled: page.url.pathname.endsWith('activity'),
group: 'collections'
group: 'tables'
},
{
label: 'Go to usage',
keys: ['g', 'u'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/usage`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/usage`
);
},
disabled: page.url.pathname.endsWith('usage'),
group: 'collections'
group: 'tables'
},
{
label: 'Go to settings',
keys: ['g', 's'],
callback() {
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/settings`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/settings`
);
},
disabled: page.url.pathname.endsWith('settings') || !$canWriteCollections,
group: 'collections'
disabled: page.url.pathname.endsWith('settings') || !$canWriteTables,
group: 'tables'
},
{
label: 'Display Name',
async callback() {
await goto(
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/collection-${$collection.$id}/settings#display-name`
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/table-${$table.$id}/settings#display-name`
);
},
group: 'collections',
group: 'tables',
disabled:
page.url.pathname.endsWith('display-name') ||
page.url.pathname.endsWith('settings') ||
!$canWriteCollections,
!$canWriteTables,
icon: IconEye
},
{
label: 'Permissions',
async callback() {
await goto(
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/collection-${$collection.$id}/settings#permissions`
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/table-${$table.$id}/settings#permissions`
);
},
group: 'collections',
group: 'tables',
disabled:
page.url.pathname.endsWith('permissions') ||
page.url.pathname.endsWith('settings') ||
!$canWriteCollections,
!$canWriteTables,
icon: IconPuzzle
},
{
label: 'Document security',
label: 'Row security',
async callback() {
await goto(
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/collection-${$collection.$id}/settings#document-security`
`${base}/project-${$project.region}-${$project.$id}/databases/database-${$database.$id}/table-${$table.$id}/settings#row-security`
);
},
group: 'collections',
group: 'tables',
disabled:
page.url.pathname.endsWith('document-security') ||
page.url.pathname.endsWith('row-security') ||
page.url.pathname.endsWith('settings') ||
!$canWriteCollections,
!$canWriteTables,
icon: IconLockClosed
},
{
@@ -184,20 +186,20 @@
},
icon: IconPlus,
group: 'indexes',
disabled: !$canWriteCollections
disabled: !$canWriteTables
}
]);
$updateCommandGroupRanks({
attributes: 1000,
documents: 900,
collections: 800,
columns: 1000,
rows: 900,
tables: 800,
indexes: 700
});
</script>
<svelte:head>
<title>{$collection?.name ?? 'Collection'} - Appwrite</title>
<title>{$table?.name ?? 'Table'} - Appwrite</title>
</svelte:head>
<slot />
@@ -7,22 +7,21 @@ import SubNavigation from './subNavigation.svelte';
import { Query } from '@appwrite.io/console';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.COLLECTION);
depends(Dependencies.TABLE);
const [table, allTables] = await Promise.all([
sdk.forProject(params.region, params.project).tables.get(params.database, params.table),
const [collection, allCollections] = await Promise.all([
sdk
.forProject(params.region, params.project)
.databases.getCollection(params.database, params.collection),
sdk
.forProject(params.region, params.project)
.databases.listCollections(params.database, [Query.orderDesc(''), Query.limit(100)])
.tables.list(params.database, [Query.orderDesc(''), Query.limit(100)])
]);
return {
header: Header,
breadcrumbs: Breadcrumbs,
subNavigation: SubNavigation,
collection,
allCollections
table,
allTables
};
};
@@ -6,13 +6,13 @@
import type { Column, ColumnType } from '$lib/helpers/types';
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { canWriteCollections, canWriteDocuments } from '$lib/stores/roles';
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
import { Card, Icon, Layout, Empty as PinkEmpty } from '@appwrite.io/pink-svelte';
import type { PageData } from './$types';
import CreateAttributeDropdown from './attributes/createAttributeDropdown.svelte';
import type { Option } from './attributes/store';
import CreateAttribute from './createAttribute.svelte';
import { collection, columns, isCsvImportInProgress } from './store';
import CreateColumnDropdown from './columns/createColumnDropdown.svelte';
import type { Option } from './columns/store';
import CreateColumn from './createColumn.svelte';
import { table, tableColumns, isCsvImportInProgress } from './store';
import Table from './table.svelte';
import { writable } from 'svelte/store';
import FilePicker from '$lib/components/filePicker.svelte';
@@ -28,26 +28,26 @@
export let data: PageData;
let showImportCSV = false;
let showCreateAttribute = false;
let selectedAttribute: Option['name'] = null;
let showCreateColumn = false;
let selectedColumn: Option['name'] = null;
const filterColumns = writable<Column[]>([]);
$: selected = preferences.getCustomCollectionColumns(page.params.collection);
$: selected = preferences.getCustomTableColumns(page.params.table);
$: columns.set(
$collection.attributes.map((attribute) => ({
id: attribute.key,
title: attribute.key,
type: attribute.type as ColumnType,
hide: !!selected?.includes(attribute.key),
array: attribute?.array,
format: 'format' in attribute && attribute?.format === 'enum' ? attribute.format : null,
elements: 'elements' in attribute ? attribute.elements : null
$: tableColumns.set(
$table.columns.map((column) => ({
id: column.key,
title: column.key,
type: column.type as ColumnType,
hide: !!selected?.includes(column.key),
array: column?.array,
format: 'format' in column && column?.format === 'enum' ? column.format : null,
elements: 'elements' in column ? column.elements : null
}))
);
$: filterColumns.set([
...$columns,
...$tableColumns,
...['$id', '$createdAt', '$updatedAt'].map((id) => ({
id,
title: id,
@@ -56,8 +56,8 @@
}))
]);
$: hasAttributes = !!$collection.attributes.length;
$: hasValidAttributes = $collection?.attributes?.some((attr) => attr.status === 'available');
$: hasColumns = !!$table.columns.length;
$: hasValidColumns = $table?.columns?.some((col) => col.status === 'available');
async function onSelect(file: Models.File) {
$isCsvImportInProgress = true;
@@ -68,12 +68,12 @@
.migrations.createCsvMigration(
file.bucketId,
file.$id,
`${page.params.database}:${page.params.collection}`
`${page.params.database}:${page.params.table}`
);
addNotification({
type: 'success',
message: 'Documents import from csv has started'
message: 'Rows import from csv has started'
});
trackEvent(Submit.DatabaseImportCsv);
@@ -89,61 +89,61 @@
}
</script>
{#key page.params.collection}
{#key page.params.table}
<Container>
<Layout.Stack direction="column" gap="xl">
<Layout.Stack direction="row" justifyContent="space-between">
<Filters
query={data.query}
columns={filterColumns}
disabled={!(hasAttributes && hasValidAttributes)}
analyticsSource="database_documents" />
disabled={!(hasColumns && hasValidColumns)}
analyticsSource="database_rows" />
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector view={data.view} {columns} hideView isCustomCollection />
<ViewSelector view={data.view} columns={tableColumns} hideView isCustomTable />
<Button
secondary
event={Click.DatabaseImportCsv}
disabled={!(hasAttributes && hasValidAttributes)}
disabled={!(hasColumns && hasValidColumns)}
on:click={() => (showImportCSV = true)}>
Import CSV
</Button>
{#if !$isSmallViewport}
<Button
disabled={!(hasAttributes && hasValidAttributes)}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/create`}
event="create_document">
disabled={!(hasColumns && hasValidColumns)}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${page.params.table}/create`}
event="create_row">
<Icon icon={IconPlus} slot="start" size="s" />
Create document
Create row
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
{#if $isSmallViewport}
<Button
disabled={!(hasAttributes && hasValidAttributes)}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/create`}
event="create_document">
disabled={!(hasColumns && hasValidColumns)}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${page.params.table}/create`}
event="create_row">
<Icon icon={IconPlus} slot="start" size="s" />
Create document
Create row
</Button>
{/if}
</Layout.Stack>
{#if hasAttributes && hasValidAttributes}
{#if data.documents.total}
{#if hasColumns && hasValidColumns}
{#if data.rows.total}
<Table {data} />
<PaginationWithLimit
name="Documents"
name="Rows"
limit={data.limit}
offset={data.offset}
total={data.documents.total} />
total={data.rows.total} />
{:else if $hasPageQueries}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2 u-bold">Sorry, we couldn't find any documents.</b>
<p>There are no documents that match your filters.</p>
<b class="body-text-2 u-bold">Sorry, we couldn't find any rows.</b>
<p>There are no rows that match your filters.</p>
</div>
<div class="u-flex common-section u-main-center">
<Button
@@ -152,7 +152,7 @@
queries.clearAll();
queries.apply();
trackEvent(Submit.FilterClear, {
source: 'database_documents'
source: 'database_rows'
});
}}>
Clear filters
@@ -162,24 +162,24 @@
</EmptySearch>
{:else}
<Empty
allowCreate={$canWriteDocuments}
allowCreate={$canWriteRows}
single
href="https://appwrite.io/docs/products/databases/documents"
target="document">
href="https://appwrite.io/docs/products/databases/rows"
target="row">
<svelte:fragment slot="actions">
<Button
external
href="https://appwrite.io/docs/products/databases/documents"
href="https://appwrite.io/docs/products/databases/rows"
text
event="empty_documentation"
size="s"
ariaLabel="create document">Documentation</Button>
ariaLabel="create row">Documentation</Button>
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/create`}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${page.params.table}/create`}
secondary
disabled={!$canWriteDocuments}
disabled={!$canWriteRows}
size="s">
Create document
Create row
</Button>
</svelte:fragment>
</Empty>
@@ -187,24 +187,24 @@
{:else}
<Card.Base padding="none">
<PinkEmpty
title="Create an attribute to get started."
title="Create a column to get started."
description="Need a hand? Learn more in our documentation.">
<slot name="actions" slot="actions">
<Button
external
href="https://appwrite.io/docs/products/databases/collections#attributes"
href="https://appwrite.io/docs/products/databases/tables#columns"
text
event="empty_documentation"
size="s">Documentation</Button>
{#if $canWriteCollections}
<CreateAttributeDropdown
bind:selectedOption={selectedAttribute}
bind:showCreate={showCreateAttribute}
{#if $canWriteTables}
<CreateColumnDropdown
bind:selectedOption={selectedColumn}
bind:showCreate={showCreateColumn}
let:toggle>
<Button secondary event="create_attribute" on:click={toggle}>
Create attribute
<Button secondary event="create_column" on:click={toggle}>
Create column
</Button>
</CreateAttributeDropdown>
</CreateColumnDropdown>
{/if}
</slot>
</PinkEmpty>
@@ -213,10 +213,8 @@
</Container>
{/key}
{#if showCreateAttribute}
<CreateAttribute
bind:showCreate={showCreateAttribute}
bind:selectedOption={selectedAttribute} />
{#if showCreateColumn}
<CreateColumn bind:showCreate={showCreateColumn} bind:selectedOption={selectedColumn} />
{/if}
{#if showImportCSV}
@@ -4,11 +4,11 @@ import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';
import { queries, queryParamToMap } from '$lib/components/filters';
import { buildWildcardAttributesQuery } from './document-[document]/attributes/store';
import { buildWildcardColumnsQuery } from './row-[row]/columns/store';
export const load: PageLoad = async ({ params, depends, url, route, parent }) => {
const { collection } = await parent();
depends(Dependencies.DOCUMENTS);
const { table } = await parent();
depends(Dependencies.ROWS);
const page = getPage(url);
const limit = getLimit(url, route, PAGE_LIMIT);
@@ -18,7 +18,6 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
const paramQueries = url.searchParams.get('query');
const parsedQueries = queryParamToMap(paramQueries || '[]');
queries.set(parsedQueries);
return {
@@ -26,14 +25,14 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
limit,
view,
query,
documents: await sdk
rows: await sdk
.forProject(params.region, params.project)
.databases.listDocuments(params.database, params.collection, [
.tables.listRows(params.database, params.table, [
Query.limit(limit),
Query.offset(offset),
Query.orderDesc(''),
...parsedQueries.values(),
...buildWildcardAttributesQuery(collection)
...buildWildcardColumnsQuery(table)
])
};
};
@@ -14,7 +14,7 @@ export const load: PageLoad = async ({ params, url, route }) => {
limit,
logs: await sdk
.forProject(params.region, params.project)
.databases.listCollectionLogs(params.database, params.collection, [
.tables.listLogs(params.database, params.table, [
Query.limit(limit),
Query.offset(offset)
])
@@ -3,7 +3,7 @@
import { page } from '$app/state';
import { Breadcrumbs } from '$lib/layout';
import { database } from '../store';
import { collection } from './store';
import { table } from './store';
$: breadcrumbs = [
{
@@ -18,8 +18,8 @@
title: $database?.name
},
{
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}`,
title: $collection?.name
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}`,
title: $table?.name
}
];
</script>
@@ -4,7 +4,7 @@
import { Empty } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
import {
ActionMenu,
Badge,
@@ -16,15 +16,15 @@
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import Create from '../createAttribute.svelte';
import { isRelationship, isString } from '../document-[document]/attributes/store';
import Create from '../createColumn.svelte';
import { isRelationship, isString } from '../row-[row]/columns/store';
import FailedModal from '../failedModal.svelte';
import CreateIndex from '../indexes/createIndex.svelte';
import { attributes, type Attributes, isCsvImportInProgress } from '../store';
import CreateAttributeDropdown from './createAttributeDropdown.svelte';
import Delete from './deleteAttribute.svelte';
import { columns, type Columns, isCsvImportInProgress } from '../store';
import CreateColumnDropdown from './createColumnDropdown.svelte';
import Delete from './deleteColumn.svelte';
import Edit from './edit.svelte';
import { attributeOptions, type Option } from './store';
import { columnOptions, type Option } from './store';
import {
IconArrowSmRight,
IconDotsHorizontal,
@@ -45,7 +45,7 @@
let showDropdown = [];
let selectedOption: Option['name'] = null;
let selectedAttribute: Attributes = null;
let selectedColumn: Columns = null;
let showCreate = false;
let showDelete = false;
let showEdit = false;
@@ -53,14 +53,14 @@
let showFailed = false;
let error = '';
const attributeFormatIcon = {
const columnFormatIcon = {
ip: IconLocationMarker,
url: IconLink,
email: IconLink,
enum: IconViewList
};
function getAttributeStatusBadge(status: string): ComponentProps<Badge>['type'] {
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
switch (status) {
case 'processing':
return 'warning';
@@ -76,13 +76,13 @@
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Title>Attributes</Typography.Title>
{#if $canWriteCollections}
<CreateAttributeDropdown bind:selectedOption bind:showCreate />
<Typography.Title>Columns</Typography.Title>
{#if $canWriteTables}
<CreateColumnDropdown bind:selectedOption bind:showCreate />
{/if}
</Layout.Stack>
{#if $attributes.length}
{#if $columns.length}
<Table.Root
let:root
columns={[
@@ -97,27 +97,27 @@
<Table.Header.Cell column="default" {root}>Default value</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each $attributes as attribute, index}
{@const option = attributeOptions.find((option) => option.type === attribute.type)}
{#each $columns as column, index}
{@const option = columnOptions.find((option) => option.type === column.type)}
<Table.Row.Base {root}>
<Table.Cell column="key" {root}>
<Layout.Stack direction="row" alignItems="center">
{#if isRelationship(attribute)}
{#if isRelationship(column)}
<Icon
size="s"
icon={attribute?.twoWay
icon={column?.twoWay
? IconSwitchHorizontal
: IconArrowSmRight} />
{:else if 'format' in attribute && attribute.format}
{@const icon = attributeFormatIcon[attribute?.format]}
{:else if 'format' in column && column.format}
{@const icon = columnFormatIcon[column?.format]}
<Icon {icon} size="s" />
{:else}
<Icon icon={option.icon} size="s" />
{/if}
<Layout.Stack direction="row" alignItems="center" gap="s">
<Layout.Stack inline direction="row" alignItems="center" gap="xxs">
<span class="text u-trim-1" data-private>{attribute.key}</span>
{#if isString(attribute) && attribute.encrypt}
<span class="text u-trim-1" data-private>{column.key}</span>
{#if isString(column) && column.encrypt}
<Tooltip>
<Icon
size="s"
@@ -127,49 +127,49 @@
</Tooltip>
{/if}
</Layout.Stack>
{#if attribute.status !== 'available'}
{#if column.status !== 'available'}
<Badge
size="s"
variant="secondary"
content={attribute.status}
type={getAttributeStatusBadge(attribute.status)} />
{#if attribute.error}
content={column.status}
type={getColumnStatusBadge(column.status)} />
{#if column.error}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
error = attribute.error;
error = column.error;
showFailed = true;
}}>Details</Link.Button>
{/if}
{:else if attribute.required}
{:else if column.required}
<Badge size="xs" variant="secondary" content="required" />
{/if}
</Layout.Stack>
</Layout.Stack>
</Table.Cell>
<Table.Cell column="type" {root}>
{#if 'format' in attribute && attribute.format}
<span class="u-capitalize">{attribute.format}</span>
{#if 'format' in column && column.format}
<span class="u-capitalize">{column.format}</span>
{:else}
<p>
<span class="u-capitalize">{attribute.type}</span>
{#if isRelationship(attribute)}
<span class="u-capitalize">{column.type}</span>
{#if isRelationship(column)}
<span>
with <a
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${attribute?.relatedCollection}`}
><b data-private>{attribute?.key}</b></a>
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${column?.relatedTable}`}
><b data-private>{column?.key}</b></a>
</span>
{/if}
</p>
{/if}
<span>
{attribute.array ? '[]' : ''}
{column.array ? '[]' : ''}
</span>
</Table.Cell>
<Table.Cell column="default" {root}>
{attribute?.default !== null && attribute?.default !== undefined
? attribute?.default
{column?.default !== null && column?.default !== undefined
? column?.default
: '-'}
</Table.Cell>
<Table.Cell column="actions" {root}>
@@ -190,32 +190,32 @@
on:click={(event) => {
toggle(event);
showEdit = true;
selectedAttribute = attribute;
selectedColumn = column;
showDropdown[index] = false;
}}>
Update
</ActionMenu.Item.Button>
{#if !isRelationship(attribute)}
{#if !isRelationship(column)}
<ActionMenu.Item.Button
leadingIcon={IconPlus}
on:click={(event) => {
toggle(event);
selectedAttribute = attribute;
selectedColumn = column;
showCreateIndex = true;
showDropdown[index] = false;
}}>
Create index
</ActionMenu.Item.Button>
{/if}
{#if attribute.status !== 'processing'}
{#if column.status !== 'processing'}
<ActionMenu.Item.Button
leadingIcon={IconTrash}
on:click={(event) => {
toggle(event);
showDelete = true;
showDropdown[index] = false;
selectedAttribute = attribute;
trackEvent(Click.DatabaseAttributeDelete);
selectedColumn = column;
trackEvent(Click.DatabaseColumnDelete);
}}>
Delete
</ActionMenu.Item.Button>
@@ -228,20 +228,20 @@
{/each}
</Table.Root>
{:else}
<Empty single target="attribute">
<Empty single target="column">
<svelte:fragment slot="actions">
<Button
external
href="https://appwrite.io/docs/products/databases/collections#attributes"
href="https://appwrite.io/docs/products/databases/tables#columns"
text
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
{#if $canWriteCollections}
<CreateAttributeDropdown bind:selectedOption bind:showCreate let:toggle>
<Button secondary event="create_attribute" on:click={toggle}>
Create attribute
ariaLabel={'create column'}>Documentation</Button>
{#if $canWriteTables}
<CreateColumnDropdown bind:selectedOption bind:showCreate let:toggle>
<Button secondary event="create_column" on:click={toggle}>
Create column
</Button>
</CreateAttributeDropdown>
</CreateColumnDropdown>
{/if}
</svelte:fragment>
</Empty>
@@ -252,14 +252,14 @@
<Create bind:showCreate bind:selectedOption />
{/if}
{#if showDelete}
<Delete bind:showDelete {selectedAttribute} />
<Delete bind:showDelete {selectedColumn} />
{/if}
{#if showEdit}
<Edit bind:showEdit {selectedAttribute} />
<Edit bind:showEdit {selectedColumn} />
{/if}
{#if showCreateIndex}
<CreateIndex bind:showCreateIndex externalAttribute={selectedAttribute} />
<CreateIndex bind:showCreateIndex externalColumn={selectedColumn} />
{/if}
{#if showFailed}
<FailedModal bind:show={showFailed} title="Create attribute" header="Creation failed" {error} />
<FailedModal bind:show={showFailed} title="Create column" header="Creation failed" {error} />
{/if}
@@ -5,15 +5,15 @@
export async function submitBoolean(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeBoolean>
data: Partial<Models.ColumnBoolean>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createBooleanAttribute(
.tables.createBooleanColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -22,15 +22,15 @@
}
export async function updateBoolean(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeBoolean>,
tableId: string,
data: Partial<Models.ColumnBoolean>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateBooleanAttribute(
.tables.updateBooleanColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -43,7 +43,7 @@
import { InputSelect } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeBoolean> = {
export let data: Partial<Models.ColumnBoolean> = {
required: false,
array: false,
default: null
@@ -66,7 +66,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeBoolean>>({
} = createConservative<Partial<Models.ColumnBoolean>>({
required: false,
array: false,
...data
@@ -1,13 +1,13 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { ActionMenu, Icon, Popover } from '@appwrite.io/pink-svelte';
import { attributeOptions, type Option } from './store';
import { columnOptions, type Option } from './store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import CsvDisabled from '../csvDisabled.svelte';
import { isCsvImportInProgress } from '../store';
export let selectedOption: Option['name'] = null;
export let showCreate = false;
export let selectedOption: Option['name'] = null;
</script>
{#if $isCsvImportInProgress}
@@ -22,11 +22,11 @@
<slot {toggle}>
<Button on:click={toggle} event="create_attribute">
<Icon icon={IconPlus} slot="start" size="s" />
Create attribute
Create column
</Button>
</slot>
<ActionMenu.Root slot="tooltip">
{#each attributeOptions as attribute}
{#each columnOptions as attribute}
<ActionMenu.Item.Button
leadingIcon={attribute.icon}
on:click={() => {
@@ -5,15 +5,15 @@
export async function submitDatetime(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeDatetime>
data: Partial<Models.ColumnDatetime>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createDatetimeAttribute(
.tables.createDatetimeColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -23,15 +23,15 @@
export async function updateDatetime(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeDatetime>,
tableId: string,
data: Partial<Models.ColumnDatetime>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateDatetimeAttribute(
.tables.updateDatetimeColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -44,7 +44,7 @@
import { InputDateTime } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeDatetime>;
export let data: Partial<Models.ColumnDatetime>;
import { createConservative } from '$lib/helpers/stores';
import { Selector } from '@appwrite.io/pink-svelte';
@@ -63,7 +63,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeDatetime>>({
} = createConservative<Partial<Models.ColumnDatetime>>({
required: false,
array: false,
...data
@@ -4,15 +4,16 @@
import { page } from '$app/state';
import { InputChoice } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { collection } from '../store';
import type { Attributes } from '../store';
import { table } from '../store';
import type { Columns } from '../store';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { isRelationship } from '../document-[document]/attributes/store';
import { isRelationship } from '../row-[row]/columns/store';
import Confirm from '$lib/components/confirm.svelte';
export let showDelete = false;
export let selectedAttribute: Attributes;
export let selectedColumn: Columns;
const databaseId = page.params.database;
let error: string;
@@ -22,38 +23,37 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.deleteAttribute(databaseId, $collection.$id, selectedAttribute.key);
.tables.deleteColumn(databaseId, $table.$id, selectedColumn.key);
showDelete = false;
addNotification({
type: 'success',
message: `Attribute has been deleted`
message: `Column has been deleted`
});
trackEvent(Submit.AttributeDelete);
trackEvent(Submit.ColumnDelete);
await goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${page.params.collection}/attributes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${page.params.table}/columns`
);
} catch (e) {
error = e.message;
trackError(e, Submit.AttributeDelete);
trackError(e, Submit.ColumnDelete);
}
}
$: isDeleteBtnDisabled =
isRelationship(selectedAttribute) && selectedAttribute?.twoWay && !checked;
$: isDeleteBtnDisabled = isRelationship(selectedColumn) && selectedColumn?.twoWay && !checked;
</script>
<Confirm
bind:open={showDelete}
onSubmit={handleDelete}
title="Delete attribute"
title="Delete column"
bind:error
disabled={isDeleteBtnDisabled}>
<p>
Are you sure you want to delete <b data-private>{selectedAttribute?.key}</b> from
<b data-private>{$collection?.name}</b>?
Are you sure you want to delete <b data-private>{selectedColumn?.key}</b> from
<b data-private>{$table?.name}</b>?
</p>
{#if isRelationship(selectedAttribute) && selectedAttribute?.twoWay}
{#if isRelationship(selectedColumn) && selectedColumn?.twoWay}
<div class="u-flex u-flex-vertical u-gap-24">
<p>
This is a two way relationship and the corresponding relationship will also be
@@ -62,8 +62,8 @@
<p><b>This action is irreversible.</b></p>
<ul>
<InputChoice id="delete" label="Delete" showLabel={false} bind:value={checked}>
Delete relationship between <b data-private>{selectedAttribute.key}</b> to
<b data-private>{selectedAttribute.twoWayKey}</b>
Delete relationship between <b data-private>{selectedColumn.key}</b> to
<b data-private>{selectedColumn.twoWayKey}</b>
</InputChoice>
</ul>
</div>
@@ -8,60 +8,60 @@
import { Button, InputText } from '$lib/elements/forms';
import deepEqual from 'deep-equal';
import { addNotification } from '$lib/stores/notifications';
import type { Attributes } from '../store';
import { attributeOptions, type Option } from './store';
import type { Columns } from '../store';
import { columnOptions, type Option } from './store';
export let showEdit = false;
export let selectedAttribute: Attributes;
export let selectedColumn: Columns;
const databaseId = page.params.database;
const collectionId = page.params.collection;
const tableId = page.params.table;
let originalKey = '';
let error: string;
let currentAttr: Attributes;
let currentColumn: Columns;
$: option = attributeOptions.find((option) => {
if (selectedAttribute) {
if ('format' in selectedAttribute && selectedAttribute.format) {
return option?.format === selectedAttribute?.format;
$: option = columnOptions.find((option) => {
if (selectedColumn) {
if ('format' in selectedColumn && selectedColumn.format) {
return option?.format === selectedColumn?.format;
} else {
return option?.type === selectedAttribute?.type;
return option?.type === selectedColumn?.type;
}
}
}) as Option;
async function submit() {
try {
await option.update(databaseId, collectionId, selectedAttribute, originalKey);
await invalidate(Dependencies.COLLECTION);
if (!page.url.pathname.includes('attributes')) {
await option.update(databaseId, tableId, selectedColumn, originalKey);
await invalidate(Dependencies.TABLE);
if (!page.url.pathname.includes('columns')) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${collectionId}/attributes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${tableId}/columns`
);
}
addNotification({
type: 'success',
message: `Attribute ${selectedAttribute.key} has been updated`
message: `Column ${selectedColumn.key} has been updated`
});
showEdit = false;
trackEvent(Submit.AttributeUpdate);
trackEvent(Submit.ColumnUpdate);
} catch (e) {
error = e.message;
trackError(e, Submit.AttributeUpdate);
trackError(e, Submit.ColumnUpdate);
}
}
$: onShow(showEdit);
$: title = `Update ${attributeOptions.find((v) => v.name === option.name)?.sentenceName ?? ''} attribute`;
$: title = `Update ${columnOptions.find((v) => v.name === option.name)?.sentenceName ?? ''} attribute`;
function onShow(show: boolean) {
if (show) {
currentAttr ??= { ...selectedAttribute };
originalKey = currentAttr.key;
currentColumn ??= { ...selectedColumn };
originalKey = currentColumn.key;
error = null;
} else {
currentAttr = null;
currentColumn = null;
}
}
</script>
@@ -78,26 +78,26 @@
</div>
</svelte:fragment>
{#if selectedAttribute}
{#if selectedAttribute?.type !== 'relationship'}
{#if selectedColumn}
{#if selectedColumn?.type !== 'relationship'}
<InputText
id="key"
label="Attribute Key"
label="Column Key"
placeholder="Enter Key"
bind:value={selectedAttribute.key}
bind:value={selectedColumn.key}
autofocus />
{/if}
{#if option}
<svelte:component
this={option.component}
editing
bind:data={selectedAttribute}
bind:data={selectedColumn}
onclose={() => (option = null)} />
{/if}
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showEdit = false)}>Cancel</Button>
<Button submit disabled={deepEqual(currentAttr, selectedAttribute)}>Update</Button>
<Button submit disabled={deepEqual(currentColumn, selectedColumn)}>Update</Button>
</svelte:fragment>
</Modal>
@@ -5,15 +5,15 @@
export async function submitEmail(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeEmail>
data: Partial<Models.ColumnEmail>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createEmailAttribute(
.tables.createEmailColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -23,15 +23,15 @@
export async function updateEmail(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeEmail>,
tableId: string,
data: Partial<Models.ColumnEmail>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateEmailAttribute(
.tables.updateEmailColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -44,7 +44,7 @@
import { InputEmail } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeEmail>;
export let data: Partial<Models.ColumnEmail>;
import { createConservative } from '$lib/helpers/stores';
import { Selector } from '@appwrite.io/pink-svelte';
@@ -63,7 +63,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeEmail>>({
} = createConservative<Partial<Models.ColumnEmail>>({
required: false,
array: false,
...data
@@ -5,15 +5,15 @@
export async function submitEnum(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeEnum>
data: Partial<Models.ColumnEnum>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createEnumAttribute(
.tables.createEnumColumn(
databaseId,
collectionId,
tableId,
key,
data.elements,
data.required,
@@ -24,15 +24,15 @@
export async function updateEnum(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeEnum>,
tableId: string,
data: Partial<Models.ColumnEnum>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateEnumAttribute(
.tables.updateEnumColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.elements,
data.required,
@@ -46,7 +46,7 @@
import { InputSelect, InputTags } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeEnum>;
export let data: Partial<Models.ColumnEnum>;
import { createConservative } from '$lib/helpers/stores';
import { Icon, Selector, Tooltip, Typography } from '@appwrite.io/pink-svelte';
@@ -66,7 +66,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeEnum>>({
} = createConservative<Partial<Models.ColumnEnum>>({
required: false,
array: false,
...data
@@ -5,15 +5,15 @@
export async function submitFloat(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeFloat>
data: Partial<Models.ColumnFloat>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createFloatAttribute(
.tables.createFloatColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.min,
@@ -25,15 +25,15 @@
export async function updateFloat(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeFloat>,
tableId: string,
data: Partial<Models.ColumnFloat>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateFloatAttribute(
.tables.updateFloatColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -48,7 +48,7 @@
import { InputNumber } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeFloat> = {
export let data: Partial<Models.ColumnFloat> = {
required: false,
min: 0,
max: 0,
@@ -73,7 +73,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeFloat>>({
} = createConservative<Partial<Models.ColumnFloat>>({
required: false,
array: false,
...data
@@ -5,15 +5,15 @@
export async function submitInteger(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeInteger>
data: Partial<Models.ColumnInteger>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createIntegerAttribute(
.tables.createIntegerColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.min,
@@ -25,15 +25,15 @@
export async function updateInteger(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeInteger>,
tableId: string,
data: Partial<Models.ColumnInteger>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateIntegerAttribute(
.tables.updateIntegerColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -49,7 +49,7 @@
export let editing = false;
export let data: Partial<Models.AttributeInteger> = {
export let data: Partial<Models.ColumnInteger> = {
required: false,
min: 0,
max: 0,
@@ -74,7 +74,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeInteger>>({
} = createConservative<Partial<Models.ColumnInteger>>({
required: false,
array: false,
...data
@@ -5,15 +5,15 @@
export async function submitIp(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeIp>
data: Partial<Models.ColumnIp>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createIpAttribute(
.tables.createIpColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -22,15 +22,15 @@
}
export async function updateIp(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeIp>,
tableId: string,
data: Partial<Models.ColumnIp>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateIpAttribute(
.tables.updateIpColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -43,7 +43,7 @@
import { InputText } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeIp>;
export let data: Partial<Models.ColumnIp>;
import { createConservative } from '$lib/helpers/stores';
import { Selector } from '@appwrite.io/pink-svelte';
@@ -62,7 +62,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeIp>>({
} = createConservative<Partial<Models.ColumnIp>>({
required: false,
array: false,
...data
@@ -6,9 +6,9 @@
export async function submitRelationship(
databaseId: string,
collectionId: string,
tableId: string,
_key: string,
data: Partial<Models.AttributeRelationship>
data: Partial<Models.ColumnRelationship>
) {
if (!isValueOfStringEnum(RelationshipType, data.relationType)) {
throw new Error(
@@ -21,10 +21,10 @@
await sdk
.forProject(page.params.region, page.params.project)
.databases.createRelationshipAttribute(
.tables.createRelationshipColumn(
databaseId,
collectionId,
data.relatedCollection,
tableId,
data.relatedTable,
data.relationType,
data.twoWay,
data.key,
@@ -35,8 +35,8 @@
export async function updateRelationship(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeRelationship>,
tableId: string,
data: Partial<Models.ColumnRelationship>,
originalKey?: string
) {
if (!isValueOfStringEnum(RelationMutate, data.onDelete)) {
@@ -45,9 +45,9 @@
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateRelationshipAttribute(
.tables.updateRelationshipColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.onDelete,
data.key !== originalKey ? data.key : undefined
@@ -59,7 +59,7 @@
import { InputText, InputSelect } from '$lib/elements/forms';
import { onMount } from 'svelte';
import { Box } from '$lib/components';
import { collection } from '../store';
import { table } from '../store';
import arrowOne from './arrow-one.svg';
import arrowTwo from './arrow-two.svg';
import { camelize } from '$lib/helpers/string';
@@ -69,7 +69,7 @@
// Props
export let editing = false;
export let data: Models.AttributeRelationship;
export let data: Models.ColumnRelationship;
// Constants
const databaseId = page.params.database;
@@ -81,47 +81,45 @@
];
const deleteOptions = [
{ value: 'setNull', label: 'Set NULL - set document ID as NULL in all related documents' },
{ value: 'cascade', label: 'Cascade - delete all related documents' },
{ value: 'restrict', label: 'Restrict - document can not be deleted' }
{ value: 'setNull', label: 'Set NULL - set row ID as NULL in all related rows' },
{ value: 'cascade', label: 'Cascade - delete all related rows' },
{ value: 'restrict', label: 'Restrict - row can not be deleted' }
];
// Variables
let way = 'one';
let search = undefined;
let collectionList: Models.CollectionList;
let originalCollectionList: Models.CollectionList;
let tableList: Models.TableList;
let originalTableList: Models.TableList;
// Lifecycle hooks
async function getCollections() {
async function getTables() {
const queries = [Query.limit(100)];
return sdk
.forProject(page.params.region, page.params.project)
.databases.listCollections(databaseId, queries, search);
.tables.list(databaseId, queries, search);
}
const debouncedFetchCollections = debounce(async () => {
collectionList = await getCollections();
const debouncedFetchTables = debounce(async () => {
tableList = await getTables();
// reset search
search = undefined;
}, 500);
function updateKeyName() {
if (!editing) {
const collection = collectionList.collections.find(
(n) => n.$id === data.relatedCollection
);
const collection = tableList.tables.find((n) => n.$id === data.relatedTable);
data.key = camelize(collection.name);
}
}
onMount(async () => {
collectionList = await getCollections();
originalCollectionList = collectionList;
tableList = await getTables();
originalTableList = tableList;
});
// Reactive statements
$: collections = collectionList?.collections?.filter((n) => n.$id !== $collection.$id) ?? [];
$: tables = tableList?.tables?.filter((n) => n.$id !== $table.$id) ?? [];
$: if (editing) {
way = data.twoWay ? 'two' : 'one';
@@ -129,26 +127,26 @@
if (way === 'two') {
data.twoWay = true;
if (!data.twoWayKey) {
data.twoWayKey = camelize($collection.name);
data.twoWayKey = camelize($table.name);
}
} else {
data.twoWay = false;
}
}
$: search = data.relatedCollection || undefined;
$: search = data.relatedTable || undefined;
$: if (search) {
const exists = collectionList?.collections?.some((c) =>
const exists = tableList?.tables?.some((c) =>
c.$id.toLocaleLowerCase().includes(search.toLocaleLowerCase())
);
if (!exists) {
debouncedFetchCollections();
debouncedFetchTables();
}
} else if (!collectionList?.total) {
} else if (!tableList?.total) {
// reset to original list!
collectionList = originalCollectionList;
tableList = originalTableList;
}
</script>
@@ -159,7 +157,7 @@
name="one"
value="one"
icon={IconArrowSmRight}>
One Relation attribute within this collection
One Relation column within this table
</Card.Selector>
<Card.Selector
title="Two-way relationship"
@@ -167,23 +165,23 @@
name="two"
value="two"
icon={IconSwitchHorizontal}>
One Relation attribute within this collection and another within the related collection
One Relation column within this table and another within the related table
</Card.Selector>
</Layout.Stack>
<Input.ComboBox
required
id="related"
label="Related collection"
placeholder="Select a collection"
bind:value={data.relatedCollection}
label="Related table"
placeholder="Select a table"
bind:value={data.relatedTable}
on:change={updateKeyName}
options={collections?.map((n) => ({ value: n.$id, label: `${n.name} (${n.$id})` })) ?? []} />
options={tables?.map((n) => ({ value: n.$id, label: `${n.name} (${n.$id})` })) ?? []} />
{#if data?.relatedCollection}
{#if data?.relatedTable}
<InputText
id="key"
label="Attribute key"
label="Column key"
placeholder="Enter key"
bind:value={data.key}
helper="Allowed characters: a-z, A-Z, 0-9, -, ."
@@ -191,11 +189,11 @@
{#if way === 'two'}
<InputText
id="keyRelated"
label="Attribute key (related collection)"
label="Column key (related table)"
placeholder="Enter key"
bind:value={data.twoWayKey}
required
helper="Allowed characters: a-z, A-Z, 0-9, -, . Once created, attribute key cannot be
helper="Allowed characters: a-z, A-Z, 0-9, -, . Once created, column key cannot be
adjusted to maintain data integrity."
readonly={editing} />
{/if}
@@ -212,7 +210,7 @@
<div class="u-flex u-flex-vertical u-gap-16">
<Box>
<div class="u-flex u-align u-cross-center u-main-center u-gap-32">
<span data-private>{camelize($collection.name)}</span>
<span data-private>{camelize($table.name)}</span>
{#if data.twoWay}
<img src={arrowTwo} alt={'Two way relationship'} />
{:else}
@@ -224,7 +222,7 @@
{#if data.relationType}
<div>
<p class="u-text-center">
<b data-private>{camelize($collection.name)}</b> can contain {[
<b data-private>{camelize($table.name)}</b> can contain {[
'oneToOne',
'manyToOne'
].includes(data.relationType)
@@ -237,14 +235,14 @@
can belong to {['oneToOne', 'oneToMany'].includes(data.relationType)
? 'one'
: 'many'}
<b data-private>{camelize($collection.name)}</b>
<b data-private>{camelize($table.name)}</b>
</p>
</div>
{/if}
</div>
<InputSelect
id="deleting"
label="On deleting a document"
label="On deleting a row"
bind:value={data.onDelete}
required
placeholder="Select a deletion method"
@@ -9,7 +9,7 @@ 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 type { Attributes } from '../store';
import type { Columns } from '../store';
import Relationship, { submitRelationship, updateRelationship } from './relationship.svelte';
import {
IconCalendar,
@@ -41,21 +41,21 @@ export type Option = {
type: 'string' | 'integer' | 'double' | 'boolean' | 'datetime' | 'relationship';
create: (
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Attributes>
data: Partial<Columns>
) => Promise<void>;
update: (
databaseId: string,
collectionId: string,
data: Partial<Attributes>,
tableId: string,
data: Partial<Columns>,
originalKey: string
) => Promise<void>;
format?: 'email' | 'ip' | 'url' | 'enum';
icon: ComponentType;
};
export const attributeOptions: Option[] = [
export const columnOptions: Option[] = [
{
name: 'String',
sentenceName: 'string',
@@ -5,15 +5,15 @@
export async function submitString(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeString>
data: Partial<Models.ColumnString>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createStringAttribute(
.tables.createStringColumn(
databaseId,
collectionId,
tableId,
key,
data.size,
data.required,
@@ -24,15 +24,15 @@
}
export async function updateString(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeString>,
tableId: string,
data: Partial<Models.ColumnString>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateStringAttribute(
.tables.updateStringColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -51,7 +51,7 @@
import { InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
import { Popover, Layout, Tag, Typography, Link } from '@appwrite.io/pink-svelte';
export let data: Partial<Models.AttributeString> = {
export let data: Partial<Models.ColumnString> = {
required: false,
size: 0,
array: false,
@@ -74,7 +74,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeString>>({
} = createConservative<Partial<Models.ColumnString>>({
required: false,
array: false,
...data
@@ -96,7 +96,7 @@
bind:value={data.size}
min={supportsStringEncryption && data.encrypt ? 150 : undefined}
helper={supportsStringEncryption && data.encrypt
? 'Encrypted string attributes require a minimum size of 150.'
? 'Encrypted string columns require a minimum size of 150.'
: undefined} />
<svelte:component
@@ -115,7 +115,7 @@
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required." />
description="Indicate whether this column is required." />
<Selector.Checkbox
size="s"
@@ -123,7 +123,7 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute is an array. Defaults to an empty array." />
description="Indicate whether this column is an array. Defaults to an empty array." />
<Layout.Stack gap="xs" direction="column">
<div
@@ -167,7 +167,7 @@
<ActionMenu.Root width="180px" slot="tooltip">
<Typography.Text variant="m-500">
Available on Pro plan. <Link.Anchor href={$upgradeURL}>Upgrade</Link.Anchor>
to enable encrypted attributes.
to enable encrypted columns.
</Typography.Text>
</ActionMenu.Root>
</Popover>
@@ -5,15 +5,15 @@
export async function submitUrl(
databaseId: string,
collectionId: string,
tableId: string,
key: string,
data: Partial<Models.AttributeUrl>
data: Partial<Models.ColumnUrl>
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.createUrlAttribute(
.tables.createUrlColumn(
databaseId,
collectionId,
tableId,
key,
data.required,
data.default,
@@ -23,15 +23,15 @@
export async function updateUrl(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeUrl>,
tableId: string,
data: Partial<Models.ColumnUrl>,
originalKey?: string
) {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateUrlAttribute(
.tables.updateUrlColumn(
databaseId,
collectionId,
tableId,
originalKey,
data.required,
data.default,
@@ -43,7 +43,7 @@
<script lang="ts">
import { InputURL } from '$lib/elements/forms';
export let data: Partial<Models.AttributeUrl>;
export let data: Partial<Models.ColumnUrl>;
export let editing = false;
import { createConservative } from '$lib/helpers/stores';
@@ -63,7 +63,7 @@
const {
stores: { required, array },
listen
} = createConservative<Partial<Models.AttributeUrl>>({
} = createConservative<Partial<Models.ColumnUrl>>({
required: false,
array: false,
...data
@@ -10,10 +10,10 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { writable } from 'svelte/store';
import AttributeForm from '../document-[document]/attributeForm.svelte';
import ColumnForm from '../row-[row]/columnForm.svelte';
import { Permissions } from '$lib/components/permissions';
import type { PageData } from './$types';
import type { Attributes } from '../store';
import type { Columns } from '../store';
import { ID } from '@appwrite.io/console';
export let data: PageData;
@@ -24,68 +24,66 @@
type CreateDocument = {
id?: string;
document: object;
row: object;
permissions: string[];
attributes: Attributes[];
columns: Columns[];
};
function createDocumentWritable() {
const availableAttributes = (data.collection.attributes as unknown as Attributes[]).filter(
function createRowWritable() {
const availableColumns = (data.table.columns as unknown as Columns[]).filter(
(a) => a.status === 'available'
);
const initial = {
id: null,
document: availableAttributes.reduce((acc, attr) => {
row: availableColumns.reduce((acc, attr) => {
acc[attr.key] = attr.array ? [] : null;
return acc;
}, {}),
permissions: [],
attributes: availableAttributes
columns: availableColumns
};
const store = writable<CreateDocument>({ ...initial });
return store;
return writable<CreateDocument>({ ...initial });
}
const createDocument = createDocumentWritable();
const createRow = createRowWritable();
async function create() {
try {
const { $id } = await sdk
.forProject(page.params.region, page.params.project)
.databases.createDocument(
.tables.createRow(
page.params.database,
page.params.collection,
$createDocument.id ?? ID.unique(),
$createDocument.document,
$createDocument.permissions
page.params.table,
$createRow.id ?? ID.unique(),
$createRow.row,
$createRow.permissions
);
addNotification({
message: 'Document has been created',
message: 'Row has been created',
type: 'success'
});
trackEvent(Submit.DocumentCreate, {
customId: !!$createDocument.id
trackEvent(Submit.RowCreate, {
customId: !!$createRow.id // todo: @itznotabug - change store name
});
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/document-${$id}`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${page.params.table}/row-${$id}`
);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DocumentCreate);
trackError(error, Submit.RowCreate);
}
}
</script>
<Wizard
title="Create document"
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/`}
title="Create row"
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${page.params.table}`}
bind:showExitModal
column
columnSize="s"
@@ -93,10 +91,10 @@
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
<Layout.Stack gap="xxl">
<Fieldset legend="Data">
<AttributeForm
bind:formValues={$createDocument.document}
attributes={$createDocument.attributes}
bind:customId={$createDocument.id} />
<ColumnForm
bind:formValues={$createRow.row}
columns={$createRow.columns}
bind:customId={$createRow.id} />
</Fieldset>
<Fieldset legend="Permissions">
@@ -105,21 +103,18 @@
Choose which permission scopes to grant your application. It is best
practice to allow only the permissions you need to meet your project goals.
</Typography.Text>
{#if data.collection.documentSecurity}
{#if data.table.rowSecurity}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is enabled</svelte:fragment>
Users will be able to access this document if they have been granted
<b>either document or collection permissions</b>.
<svelte:fragment slot="title">Row security is enabled</svelte:fragment>
Users will be able to access this row if they have been granted
<b>either row or table permissions</b>.
</Alert.Inline>
<Permissions bind:permissions={$createDocument.permissions} />
<Permissions bind:permissions={$createRow.permissions} />
{:else}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is disabled</svelte:fragment>
If you want to assign document permissions, navigate to Collection settings
and enable document security. Otherwise, only collection permissions will
be used.
<svelte:fragment slot="title">Row security is disabled</svelte:fragment>
If you want to assign row permissions, navigate to Table settings and enable
row security. Otherwise, only table permissions will be used.
</Alert.Inline>
{/if}
</Layout.Stack>
@@ -1,22 +1,27 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { option, attributeOptions, type Option } from './attributes/store';
import {
option,
columnOptions,
type Option
} from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/store';
import { Button, InputText } from '$lib/elements/forms';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { page } from '$app/state';
import { addNotification } from '$lib/stores/notifications';
import { base } from '$app/paths';
import type { Attributes } from './store';
import type { Columns } from './store';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
export let showCreate = false;
export let selectedOption: Option['name'] = null;
$: tableId = page.params.table;
const databaseId = page.params.database;
$: collectionId = page.params.collection;
let key: string = null;
let data: Partial<Attributes> = {
let data: Partial<Columns> = {
required: false,
array: false,
default: null
@@ -25,28 +30,28 @@
async function submit() {
try {
await $option.create(databaseId, collectionId, key, data);
await invalidate(Dependencies.COLLECTION);
await $option.create(databaseId, tableId, key, data);
await invalidate(Dependencies.TABLE);
if (!page.url.pathname.includes('attributes')) {
if (!page.url.pathname.includes('columns')) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${collectionId}/attributes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${tableId}/columns`
);
}
addNotification({
type: 'success',
message: `Attribute ${key ?? data?.key} has been created`
message: `Column ${key ?? data?.key} has been created`
});
showCreate = false;
trackEvent(Submit.AttributeCreate);
trackEvent(Submit.ColumnCreate);
} catch (e) {
error = e.message;
trackError(e, Submit.AttributeCreate);
trackError(e, Submit.ColumnCreate);
}
}
$: if (selectedOption) {
$option = attributeOptions.find((option) => option.name === selectedOption);
$option = columnOptions.find((option) => option.name === selectedOption);
}
$: if (!showCreate) {
@@ -61,14 +66,14 @@
};
}
$: title = `Create ${attributeOptions.find((option) => option.name === selectedOption)?.sentenceName ?? ''} attribute`;
$: title = `Create ${columnOptions.find((option) => option.name === selectedOption)?.sentenceName ?? ''} column`;
</script>
<Modal {error} bind:show={showCreate} onSubmit={submit} {title}>
{#if selectedOption !== 'Relationship'}
<InputText
id="key"
label="Attribute key"
label="Column key"
placeholder="Enter key"
bind:value={key}
autofocus
@@ -4,33 +4,30 @@
import { Id, Tab, Tabs } from '$lib/components';
import { isTabSelected } from '$lib/helpers/load';
import { Cover, CoverTitle } from '$lib/layout';
import { canWriteCollections } from '$lib/stores/roles';
import { collection } from './store';
import { canWriteTables } from '$lib/stores/roles';
import { table } from './store';
import { isTabletViewport } from '$lib/stores/viewport';
let databaseId = $derived.by(() => {
return page.params.database;
});
let collectionId = $derived.by(() => {
return page.params.collection;
});
const databaseId = $derived(page.params.database);
let path = $derived.by(() => {
return `${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${collectionId}`;
});
const tableId = $derived(page.params.table);
let tabs = $derived.by(() => {
return [
const path = $derived(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${tableId}`
);
const tabs = $derived(
[
{
href: path,
title: 'Documents',
event: 'documents',
title: 'Rows',
event: 'rows',
hasChildren: true
},
{
href: `${path}/attributes`,
title: 'Attributes',
event: 'attributes'
href: `${path}/columns`,
title: 'Columns',
event: 'columns'
},
{
href: `${path}/indexes`,
@@ -53,10 +50,10 @@
href: `${path}/settings`,
title: 'Settings',
event: 'settings',
disabled: !$canWriteCollections
disabled: !$canWriteTables
}
].filter((tab) => !tab.disabled);
});
].filter((tab) => !tab.disabled)
);
</script>
<div style:margin-top={$isTabletViewport ? '48px' : 0}>
@@ -64,10 +61,10 @@
<svelte:fragment slot="header">
<CoverTitle
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}`}>
{$collection?.name}
{$table?.name}
</CoverTitle>
{#key $collection?.$id}
<Id value={$collection?.$id}>{$collection?.$id}</Id>
{#key $table?.$id}
<Id value={$table?.$id}>{$table?.$id}</Id>
{/key}
</svelte:fragment>
@@ -1,17 +1,17 @@
<script lang="ts">
import { Empty } from '$lib/components';
import { Container } from '$lib/layout';
import { collection } from '../store';
import { table } from '../store';
import Delete from './deleteIndex.svelte';
import Create from './createIndex.svelte';
import Overview from './overviewIndex.svelte';
import CreateAttribute from '../createAttribute.svelte';
import CreateColumn from '../createColumn.svelte';
import type { Models } from '@appwrite.io/console';
import { Button } from '$lib/elements/forms';
import CreateAttributeDropdown from '../attributes/createAttributeDropdown.svelte';
import type { Option } from '../attributes/store';
import CreateColumnDropdown from '../columns/createColumnDropdown.svelte';
import type { Option } from '../columns/store';
import FailedModal from '../failedModal.svelte';
import { canWriteCollections } from '$lib/stores/roles';
import { canWriteTables } from '$lib/stores/roles';
import {
ActionMenu,
Badge,
@@ -33,16 +33,16 @@
export let data;
let selectedIndex: Models.Index = null;
let showCreateIndex = false;
let showOverview = false;
let showDelete = false;
let showCreateAttribute = false;
let selectedAttribute: Option['name'] = null;
let showFailed = false;
let error = '';
let showFailed = false;
let showDelete = false;
let showOverview = false;
let showCreateIndex = false;
let showCreateColumn = false;
let selectedColumn: Option['name'] = null;
let selectedIndex: Models.ColumnIndex = null;
function getAttributeStatusBadge(status: string): ComponentProps<Badge>['type'] {
function getColumnStatusBadge(status: string): ComponentProps<Badge>['type'] {
switch (status) {
case 'processing':
return 'warning';
@@ -59,10 +59,10 @@
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Title>Indexes</Typography.Title>
{#if $canWriteCollections}
{#if $canWriteTables}
<Button
event="create_index"
disabled={!$collection?.attributes?.length}
disabled={!$table?.columns?.length}
on:click={() => (showCreateIndex = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Create index
@@ -70,14 +70,14 @@
{/if}
</Layout.Stack>
{#if data.collection?.attributes?.length}
{#if data.collection.indexes.length}
{#if data.table?.columns?.length}
{#if data.table.indexes.length}
<Table.Root
let:root
columns={[
{ id: 'key' },
{ id: 'type' },
{ id: 'attributes' },
{ id: 'columns' },
{ id: 'orders' },
{ id: 'lengths' },
{ id: 'actions', width: 40 }
@@ -85,12 +85,12 @@
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="key" {root}>Key</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
<Table.Header.Cell column="attributes" {root}>Attributes</Table.Header.Cell>
<Table.Header.Cell column="columns" {root}>Columns</Table.Header.Cell>
<Table.Header.Cell column="orders" {root}>Orders</Table.Header.Cell>
<Table.Header.Cell column="lengths" {root}>Lengths</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each data.collection.indexes as index}
{#each data.table.indexes as index}
<Table.Row.Base {root}>
<Table.Cell column="key" {root}>
<Layout.Stack direction="row" alignItems="center">
@@ -100,7 +100,7 @@
size="s"
variant="secondary"
content={index.status}
type={getAttributeStatusBadge(index.status)} />
type={getColumnStatusBadge(index.status)} />
{#if index.error}
<Link.Button
variant="muted"
@@ -114,8 +114,8 @@
</Layout.Stack>
</Table.Cell>
<Table.Cell column="type" {root}>{index.type}</Table.Cell>
<Table.Cell column="attributes" {root}>
{index.attributes}
<Table.Cell column="columns" {root}>
{index.columns}
</Table.Cell>
<Table.Cell column="orders" {root}>
{index.orders}
@@ -150,30 +150,30 @@
</Table.Root>
{:else}
<Empty
allowCreate={$canWriteCollections}
allowCreate={$canWriteTables}
single
href="https://appwrite.io/docs/products/databases/collections#indexes"
href="https://appwrite.io/docs/products/databases/tables#indexes"
target="index"
on:click={() => (showCreateIndex = true)} />
{/if}
{:else}
<Empty single target="attribute">
<Empty single target="index">
<svelte:fragment slot="actions">
<Button
external
href="https://appwrite.io/docs/products/databases/collections#attributes"
href="https://appwrite.io/docs/products/databases/tables#columns"
text
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
{#if $canWriteCollections}
<CreateAttributeDropdown
bind:selectedOption={selectedAttribute}
bind:showCreate={showCreateAttribute}
ariaLabel="create index">Documentation</Button>
{#if $canWriteTables}
<CreateColumnDropdown
bind:selectedOption={selectedColumn}
bind:showCreate={showCreateColumn}
let:toggle>
<Button secondary event="create_attribute" on:click={toggle}>
Create attribute
<Button secondary event="create_column" on:click={toggle}>
Create column
</Button>
</CreateAttributeDropdown>
</CreateColumnDropdown>
{/if}
</svelte:fragment>
</Empty>
@@ -187,10 +187,8 @@
<Overview bind:showOverview {selectedIndex} />
{/if}
{#if showCreateAttribute}
<CreateAttribute
bind:showCreate={showCreateAttribute}
bind:selectedOption={selectedAttribute} />
{#if showCreateColumn}
<CreateColumn bind:showCreate={showCreateColumn} bind:selectedOption={selectedColumn} />
{/if}
<FailedModal bind:show={showFailed} title="Create index" header="Creation failed" {error} />
@@ -10,14 +10,14 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { IndexType } from '@appwrite.io/console';
import { isRelationship } from '../document-[document]/attributes/store';
import { type Attributes, collection, indexes } from '../store';
import { isRelationship } from '../row-[row]/columns/store';
import { type Columns, table, indexes } from '../store';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { flags } from '$lib/flags';
export let showCreateIndex = false;
export let externalAttribute: Attributes = null;
export let externalColumn: Columns = null;
const databaseId = page.params.database;
@@ -30,14 +30,14 @@
];
let selectedType = IndexType.Key;
let attributeOptions = $collection.attributes
.filter((attribute) => !isRelationship(attribute))
.map((attribute) => ({
value: attribute.key,
label: attribute.key
let columnOptions = $table.columns
.filter((column) => !isRelationship(column))
.map((column) => ({
value: column.key,
label: column.key
}));
let attributeList = [{ value: '', order: '', length: null }];
let columnList = [{ value: '', order: '', length: null }];
const showLengths = flags.showIndexLengths(page.data);
@@ -53,8 +53,8 @@
}
function initialize() {
attributeList = externalAttribute
? [{ value: externalAttribute.key, order: 'ASC', length: null }]
columnList = externalColumn
? [{ value: externalColumn.key, order: 'ASC', length: null }]
: [{ value: '', order: 'ASC', length: null }];
selectedType = IndexType.Key;
key = `index_${$indexes.length + 1}`;
@@ -66,32 +66,32 @@
key = generateIndexKey();
}
$: addAttributeDisabled = !attributeList.at(-1)?.value || !attributeList.at(-1)?.order;
$: addColumnDisabled = !columnList.at(-1)?.value || !columnList.at(-1)?.order;
async function create() {
if (!(key && selectedType && !addAttributeDisabled)) {
if (!(key && selectedType && !addColumnDisabled)) {
error = 'All fields are required';
return;
}
try {
await sdk.forProject(page.params.region, page.params.project).databases.createIndex(
await sdk.forProject(page.params.region, page.params.project).tables.createIndex(
databaseId,
$collection.$id,
$table.$id,
key,
selectedType,
attributeList.map((a) => a.value),
attributeList.map((a) => a.order),
attributeList.map((a) => (a.length ? Number(a.length) : null))
columnList.map((a) => a.value),
columnList.map((a) => a.order),
columnList.map((a) => (a.length ? Number(a.length) : null))
);
await Promise.allSettled([
invalidate(Dependencies.COLLECTION),
invalidate(Dependencies.TABLE),
invalidate(Dependencies.DATABASE)
]);
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${$collection.$id}/indexes`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${$table.$id}/indexes`
);
addNotification({
@@ -106,11 +106,11 @@
}
}
function addAttribute() {
if (addAttributeDisabled) return;
function addColumn() {
if (addColumnDisabled) return;
// We assign instead of pushing to trigger Svelte's reactivity
attributeList = [...attributeList, { value: '', order: '', length: null }];
columnList = [...columnList, { value: '', order: '', length: null }];
}
</script>
@@ -125,7 +125,7 @@
<InputSelect required options={types} id="type" label="Index type" bind:value={selectedType} />
<Layout.Stack gap="s">
{#each attributeList as attribute, i}
{#each columnList as column, index}
<Layout.Stack direction="row">
<InputSelect
required
@@ -133,12 +133,12 @@
{ value: '$id', label: '$id' },
{ value: '$createdAt', label: '$createdAt' },
{ value: '$updatedAt', label: '$updatedAt' },
...attributeOptions
...columnOptions
]}
id={`attribute-${i}`}
label={i === 0 ? 'Attribute' : undefined}
placeholder="Select Attribute"
bind:value={attribute.value} />
id={`column-${index}`}
label={index === 0 ? 'Column' : undefined}
placeholder="Select column"
bind:value={column.value} />
<InputSelect
options={[
@@ -146,25 +146,25 @@
{ value: 'DESC', label: 'DESC' }
]}
required
id={`order-${i}`}
label={i === 0 ? 'Order' : undefined}
bind:value={attribute.order}
placeholder="Select Order" />
id={`order-${index}`}
label={index === 0 ? 'Order' : undefined}
bind:value={column.order}
placeholder="Select order" />
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
{#if selectedType === IndexType.Key && showLengths}
<InputNumber
id={`length-${i}`}
label={i === 0 ? 'Length' : undefined}
placeholder="Enter Length"
bind:value={attribute.length} />
id={`length-${index}`}
label={index === 0 ? 'Length' : undefined}
placeholder="Enter length"
bind:value={column.length} />
{/if}
<Button
icon
compact
disabled={attributeList.length <= 1}
disabled={columnList.length <= 1}
on:click={() => {
attributeList = remove(attributeList, i);
columnList = remove(columnList, index);
}}>
<span class="icon-x" aria-hidden="true"></span>
</Button>
@@ -172,9 +172,9 @@
</Layout.Stack>
{/each}
<div>
<Button compact on:click={addAttribute} disabled={addAttributeDisabled}>
<Button compact on:click={addColumn} disabled={addColumnDisabled}>
<Icon icon={IconPlus} slot="start" size="s" />
Add attribute
Add column
</Button>
</div>
</Layout.Stack>
@@ -1,6 +1,6 @@
<script lang="ts">
import { addNotification } from '$lib/stores/notifications';
import { collection } from '../store';
import { table } from '../store';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/state';
@@ -11,17 +11,17 @@
import { Typography } from '@appwrite.io/pink-svelte';
export let showDelete = false;
export let selectedIndex: Models.Index;
export let selectedIndex: Models.ColumnIndex;
const databaseId = page.params.database;
let error: string;
const databaseId = page.params.database;
async function handleDelete() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.deleteIndex(databaseId, $collection.$id, selectedIndex.key);
await invalidate(Dependencies.COLLECTION);
.tables.deleteIndex(databaseId, $table.$id, selectedIndex.key);
await invalidate(Dependencies.TABLE);
showDelete = false;
addNotification({
type: 'success',
@@ -37,6 +37,6 @@
<Confirm onSubmit={handleDelete} title="Delete index" bind:open={showDelete} bind:error>
<Typography.Text>
Are you sure you want to delete <b>{selectedIndex.key}</b> from <b>{$collection.name}</b>?
Are you sure you want to delete <b>{selectedIndex.key}</b> from <b>{$table.name}</b>?
</Typography.Text>
</Confirm>
@@ -7,7 +7,7 @@
import { flags } from '$lib/flags';
export let showOverview = false;
export let selectedIndex: Models.Index = null;
export let selectedIndex: Models.ColumnIndex = null;
const showLengths = flags.showIndexLengths(page.data);
</script>
@@ -16,8 +16,8 @@
<InputText
required
id="key"
label="Index Key"
placeholder="Enter Key"
label="Index key"
placeholder="Enter key"
value={selectedIndex.key}
readonly />
<InputText
@@ -28,14 +28,14 @@
value={selectedIndex.type}
readonly />
{#if selectedIndex?.attributes?.length}
{#each selectedIndex.attributes as attribute, i}
{#if selectedIndex?.columns?.length}
{#each selectedIndex.columns as column, i}
<Layout.Stack direction="row">
<InputText
required
label={i === 0 ? 'Attribute' : ''}
id={`value-${attribute}`}
value={attribute}
label={i === 0 ? 'Column' : ''}
id={`value-${column}`}
value={column}
readonly />
<InputText
required
@@ -1,5 +1,7 @@
<script lang="ts">
import Helper from '$lib/elements/forms/helper.svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconChevronDown } from '@appwrite.io/pink-icons-svelte';
export let value: string;
export let id: string;
@@ -30,7 +32,7 @@
<select bind:this={element} {id} bind:value {required} on:invalid={handleInvalid}>
<slot />
</select>
<span class="icon-cheveron-down" aria-hidden="true"></span>
<Icon icon={IconChevronDown} size="s" />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
@@ -9,14 +9,14 @@
export let show = false;
export let data: Partial<Models.Document>[];
export let selectedRelationship: Models.AttributeRelationship = null;
export let selectedRelationship: Models.ColumnRelationship = null;
const databaseId = page.params.database;
const limit = 10;
$: args =
preferences
.getDisplayNames()
?.[selectedRelationship?.relatedCollection]?.filter((p) => p !== '$id') ?? [];
?.[selectedRelationship?.relatedTable]?.filter((p) => p !== '$id') ?? [];
$: if (!show) {
data = null;
@@ -36,7 +36,7 @@
{#snippet children(paginatedItems: typeof data)}
<Table.Root let:root columns={tableColumns}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="$id" {root}>Document ID</Table.Header.Cell>
<Table.Header.Cell column="$id" {root}>Row ID</Table.Header.Cell>
{#if args.length}
{#each args as arg}
<Table.Header.Cell column={arg} {root}>{arg}</Table.Header.Cell>
@@ -46,7 +46,7 @@
{#each paginatedItems as doc}
<Table.Row.Link
{root}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${selectedRelationship.relatedCollection}/document-${doc.$id}`}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${selectedRelationship.relatedTable}/row-${doc.$id}`}
on:click={() => (show = false)}>
<Table.Cell column="$id" {root}>
<Id value={doc.$id}>{doc.$id}</Id>
@@ -3,40 +3,35 @@ import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import type { Attributes } from '../store';
import { buildWildcardAttributesQuery } from './attributes/store';
import type { Columns } from '../store';
import { buildWildcardColumnsQuery } from './columns/store';
export const load: LayoutLoad = async ({ params, parent, depends }) => {
const { collection } = await parent();
depends(Dependencies.DOCUMENT);
depends(Dependencies.ROW);
const { table } = await parent();
const document = await sdk
const row = await sdk
.forProject(params.region, params.project)
.databases.getDocument(
params.database,
params.collection,
params.document,
buildWildcardAttributesQuery(collection)
);
.tables.getRow(params.database, params.table, params.row, buildWildcardColumnsQuery(table));
/**
* Sanitize DateTime to remove UTC Timezone section.
*/
collection.attributes.forEach((attribute) => {
const { type, key, array } = attribute as unknown as Attributes;
table.columns.forEach((column) => {
const { type, key, array } = column as unknown as Columns;
if (type === 'datetime') {
if (array) {
document[key] = document[key].map((n: string) => {
row[key] = row[key].map((n: string) => {
if (!n) {
return null;
}
return new Date(n).toISOString().slice(0, 23);
});
} else {
if (document[key]) {
document[key] = new Date(document[key]).toISOString().slice(0, 23);
if (row[key]) {
row[key] = new Date(row[key]).toISOString().slice(0, 23);
} else {
document[key] = null;
row[key] = null;
}
}
}
@@ -45,6 +40,6 @@ export const load: LayoutLoad = async ({ params, parent, depends }) => {
return {
header: Header,
breadcrumbs: Breadcrumbs,
document
row
};
};
@@ -11,36 +11,38 @@
import type { Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { invalidate } from '$app/navigation';
import { doc } from './store';
import { collection, type Attributes } from '../store';
import AttributeItem from './attributeItem.svelte';
import { isRelationship, isRelationshipToMany } from './attributes/store';
import { table, type Columns } from '../store';
import ColumnItem from './columnItem.svelte';
import { isRelationship, isRelationshipToMany } from './columns/store';
import { deepClone } from '$lib/helpers/object';
export let data;
const row = data.row;
const databaseId = page.params.database;
const collectionId = page.params.collection;
const documentId = page.params.document;
const tableId = page.params.table;
const rowId = page.params.row;
function initWork() {
const prohibitedKeys = [
'$id',
'$collection',
'$collectionId',
'$tableId',
'$databaseId',
'$createdAt',
'$updatedAt'
];
const filteredKeys = Object.keys($doc).filter((key) => {
const filteredKeys = Object.keys(row).filter((key) => {
return !prohibitedKeys.includes(key);
});
const result = filteredKeys.reduce((obj, key) => {
obj[key] = $doc[key];
obj[key] = row[key];
return obj;
}, {});
return writable(deepClone(result as Models.Document));
return writable(deepClone(result as Models.Row));
}
const work = initWork();
@@ -49,18 +51,12 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.updateDocument(
databaseId,
collectionId,
documentId,
$work,
$work.$permissions
);
.tables.updateRow(databaseId, tableId, rowId, $work, $work.$permissions);
invalidate(Dependencies.DOCUMENT);
trackEvent(Submit.DocumentUpdate);
invalidate(Dependencies.ROW);
trackEvent(Submit.RowUpdate);
addNotification({
message: 'Document has been updated',
message: 'Row has been updated',
type: 'success'
});
} catch (error) {
@@ -68,46 +64,46 @@
message: error.message,
type: 'error'
});
trackError(error, Submit.DocumentUpdate);
trackError(error, Submit.RowUpdate);
}
}
function compareAttributes(
attribute: Attributes,
$work: Models.Document,
$doc: Models.Document
) {
if (!attribute) {
function compareColumns(column: Columns, related: Models.Row, row: Models.Row) {
if (!column) {
return false;
}
const workAttribute = $work?.[attribute.key];
const docAttribute = $doc?.[attribute.key];
const rowColumn = row?.[column.key];
const relatedColumn = related?.[column.key];
if (attribute.array) {
return !symmetricDifference(Array.from(workAttribute), Array.from(docAttribute)).length;
// undefined for a second, not sure why!
if (rowColumn === undefined || relatedColumn === undefined) {
return false;
}
if (isRelationship(attribute)) {
if (isRelationshipToMany(attribute as Models.AttributeRelationship)) {
const workIds = workAttribute.map((doc: string | Record<string, unknown>) =>
if (column.array) {
return !symmetricDifference(Array.from(relatedColumn), Array.from(rowColumn)).length;
}
if (isRelationship(column)) {
if (isRelationshipToMany(column as Models.ColumnRelationship)) {
const workIds = relatedColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
const relatedIds = docAttribute.map((doc: string | Record<string, unknown>) =>
const relatedIds = rowColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
return !symmetricDifference(workIds, relatedIds).length;
} else {
const workId =
typeof workAttribute === 'string' ? workAttribute : workAttribute?.$id;
const relatedId =
typeof docAttribute === 'string' ? docAttribute : docAttribute?.$id;
typeof relatedColumn === 'string' ? relatedColumn : relatedColumn?.$id;
const relatedId = typeof rowColumn === 'string' ? rowColumn : rowColumn?.$id;
return workId === relatedId;
}
}
return workAttribute === docAttribute;
return relatedColumn === rowColumn;
}
</script>
@@ -116,19 +112,19 @@
</svelte:head>
<Container>
{#if $collection?.attributes?.length}
{#each $collection.attributes as attribute}
{@const label = attribute.key}
{#if $table?.columns?.length}
{#each $table.columns as column}
{@const label = column.key}
<CardGrid>
<svelte:fragment slot="title">{label}</svelte:fragment>
<svelte:fragment slot="aside">
<AttributeItem {attribute} bind:formValues={$work} {label} editing />
<ColumnItem {column} bind:formValues={$work} {label} editing />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={compareAttributes(attribute, $work, $doc)}
disabled={compareColumns(column, $work, row)}
on:click={() => updateData()}>Update</Button>
</svelte:fragment>
</CardGrid>
@@ -14,7 +14,7 @@ export const load: PageLoad = async ({ params, url, route }) => {
limit,
logs: await sdk
.forProject(params.region, params.project)
.databases.listDocumentLogs(params.database, params.collection, params.document, [
.tables.listRowLogs(params.database, params.table, params.row, [
Query.limit(limit),
Query.offset(offset)
])
@@ -3,8 +3,8 @@
import { page } from '$app/state';
import { Breadcrumbs } from '$lib/layout';
import { database } from '../../store';
import { collection } from '../store';
import { doc } from './store';
import { table } from '../store';
import { row } from './store';
$: breadcrumbs = [
{
@@ -15,12 +15,12 @@
title: $database?.name
},
{
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}`,
title: $collection?.name
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}`,
title: $table?.name
},
{
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/collection-${$collection?.$id}/document-${$doc?.$id}`,
title: $doc?.$id
href: `${base}/project-${page.params.region}-${page.params.project}/databases/database-${$database?.$id}/table-${$table?.$id}/row-${$row?.$id}`,
title: $row?.$id
}
];
</script>
@@ -0,0 +1,63 @@
<script lang="ts">
import type { Models } from '@appwrite.io/console';
import Boolean from './columns/boolean.svelte';
import Datetime from './columns/datetime.svelte';
import Enum from './columns/enum.svelte';
import Integer from './columns/integer.svelte';
import Relationship from './columns/relationship.svelte';
import String from './columns/string.svelte';
import Url from './columns/url.svelte';
export let id: string;
export let label: string;
export let optionalText: string | undefined = undefined;
export let value: string | number | boolean | null | string[];
export let editing = false;
export let column:
| Models.ColumnBoolean
| Models.ColumnEmail
| Models.ColumnEnum
| Models.ColumnFloat
| Models.ColumnInteger
| Models.ColumnIp
| Models.ColumnString
| Models.ColumnDatetime
| Models.ColumnUrl;
const columnsTypeMap = {
string: String,
integer: Integer,
double: Integer,
boolean: Boolean,
datetime: Datetime,
relationship: Relationship
};
const columnsFormatMap = {
ip: String,
url: Url,
email: String,
enum: Enum
};
</script>
{#if column.type in columnsTypeMap}
{#if 'format' in column && column.format}
<svelte:component
this={columnsFormatMap[column.format]}
{id}
{label}
{column}
{optionalText}
bind:value />
{:else}
<svelte:component
this={columnsTypeMap[column.type]}
{id}
{editing}
{label}
{column}
{optionalText}
bind:value />
{/if}
{/if}
@@ -1,22 +1,22 @@
<script lang="ts">
import CustomId from '$lib/components/customId.svelte';
import { Icon, Layout, Tag } from '@appwrite.io/pink-svelte';
import type { Attributes } from '../store';
import AttributeItem from './attributeItem.svelte';
import type { Columns } from '../store';
import ColumnItem from './columnItem.svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
export let attributes: Attributes[] = [];
export let columns: Columns[] = [];
export let formValues: object = {};
export let customId: string | null | undefined = undefined;
let showCustomId = false;
</script>
{#if attributes.length}
{#if columns.length}
<Layout.Stack>
{#each attributes as attribute}
{@const label = attribute.key}
<AttributeItem {attribute} bind:formValues {label} />
{#each columns as column}
{@const label = column.key}
<ColumnItem {column} bind:formValues {label} />
{/each}
{#if customId !== undefined}
@@ -24,11 +24,11 @@
<span>
<Tag size="s" on:click={() => (showCustomId = !showCustomId)}>
<Icon icon={IconPencil} slot="start" size="s" />
Document ID
Row ID
</Tag>
</span>
{:else}
<CustomId autofocus bind:show={showCustomId} name="Document" bind:id={customId} />
<CustomId autofocus bind:show={showCustomId} name="Row" bind:id={customId} />
{/if}
{/if}
</Layout.Stack>
@@ -3,10 +3,10 @@
import { capitalize } from '$lib/helpers/string';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import Attribute from './attribute.svelte';
import type { Attributes } from '../store';
import Column from './column.svelte';
import type { Columns } from '../store';
export let attribute: Attributes;
export let column: Columns;
export let formValues: object = {};
export let label: string;
export let editing = false;
@@ -25,9 +25,9 @@
};
}
function getAttributeType(attribute: Attributes) {
if ('format' in attribute) {
switch (attribute.format) {
function getColumnType(column: Columns) {
if ('format' in column) {
switch (column.format) {
case 'ip':
return 'IP';
case 'email':
@@ -41,41 +41,41 @@
break;
}
}
return `${capitalize(attribute.type)}${attribute.array ? '[]' : ''}`;
return `${capitalize(column.type)}${column.array ? '[]' : ''}`;
}
</script>
{#if attribute.array}
{#if formValues[attribute.key]?.length === 0}
{#if column.array}
{#if formValues[column.key]?.length === 0}
<Layout.Stack direction="row" alignContent="space-between">
<Layout.Stack gap="xxs" direction="row" alignItems="center">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{getAttributeType(attribute)}
{getColumnType(column)}
</Typography.Text>
</Layout.Stack>
<Button secondary on:click={() => addArrayItem(attribute.key)}>
<Button secondary on:click={() => addArrayItem(column.key)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add item
</Button>
</Layout.Stack>
{:else}
<Layout.Stack>
{#each [...formValues[attribute.key].keys()] as index}
{#each [...(formValues[column.key]?.keys() ?? [])] as index}
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
<Attribute
{attribute}
id={`${attribute.key}-${index}`}
optionalText={index === 0 ? getAttributeType(attribute) : undefined}
<Column
{column}
id={`${column.key}-${index}`}
optionalText={index === 0 ? getColumnType(column) : undefined}
label={index === 0 ? label : ''}
bind:value={formValues[attribute.key][index]} />
<Button text icon on:click={() => removeArrayItem(attribute.key, index)}>
bind:value={formValues[column.key][index]} />
<Button text icon on:click={() => removeArrayItem(column.key, index)}>
<span class="icon-x" aria-hidden="true"></span>
</Button>
</Layout.Stack>
{/each}
<div>
<Button secondary on:click={() => addArrayItem(attribute.key)}>
<Button secondary on:click={() => addArrayItem(column.key)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add item
</Button>
@@ -83,11 +83,11 @@
</Layout.Stack>
{/if}
{:else}
<Attribute
{editing}
{attribute}
<Column
{label}
optionalText={getAttributeType(attribute)}
id={attribute.key}
bind:value={formValues[attribute.key]} />
{editing}
{column}
id={column.key}
optionalText={getColumnType(column)}
bind:value={formValues[column.key]} />
{/if}
@@ -5,7 +5,7 @@
export let id: string;
export let label: string;
export let value: boolean;
export let attribute: Models.AttributeBoolean;
export let column: Models.ColumnBoolean;
export let optionalText: string | undefined = undefined;
</script>
@@ -15,9 +15,9 @@
bind:value
{optionalText}
placeholder="Select a value"
required={attribute.required}
required={column.required}
options={[
!attribute.required && { label: 'NULL', value: null },
!column.required && { label: 'NULL', value: null },
{ label: 'True', value: true },
{ label: 'False', value: false }
].filter(Boolean)} />
@@ -5,7 +5,7 @@
export let id: string;
export let label: string;
export let value: string;
export let attribute: Models.AttributeDatetime;
export let column: Models.ColumnDatetime;
export let type: 'date' | 'time' | 'datetime-local' = 'datetime-local';
</script>
@@ -14,5 +14,5 @@
{label}
{type}
bind:value
required={attribute.required}
nullable={!attribute.required} />
required={column.required}
nullable={!column.required} />
@@ -5,7 +5,7 @@
export let id: string;
export let label: string;
export let value: string | null;
export let attribute: Models.AttributeUrl;
export let column: Models.ColumnEmail;
</script>
<InputEmail
@@ -13,5 +13,5 @@
{label}
bind:value
placeholder="Enter URL"
required={attribute.required}
nullable={!attribute.required} />
required={column.required}
nullable={!column.required} />
@@ -5,17 +5,17 @@
export let id: string;
export let label: string;
export let value: string;
export let attribute: Models.AttributeEnum;
export let column: Models.ColumnEnum;
export let optionalText: string | undefined = undefined;
$: options = [
...attribute.elements.map((element) => {
...column.elements.map((element) => {
return {
label: element,
value: element
};
}),
!attribute.required && {
!column.required && {
label: 'NULL',
value: null
}
@@ -28,5 +28,5 @@
{id}
{label}
{optionalText}
required={attribute.required}
required={column.required}
placeholder="Select a value" />
@@ -5,15 +5,15 @@
export let id: string;
export let label: string;
export let value: number;
export let attribute: Models.AttributeInteger;
export let column: Models.ColumnInteger;
</script>
<InputNumber
{id}
{label}
bind:value
min={attribute.min}
max={attribute.max}
required={attribute.required}
nullable={!attribute.required}
step={attribute.type === 'double' ? 'any' : 1} />
min={column.min}
max={column.max}
required={column.required}
nullable={!column.required}
step={column.type === 'double' ? 'any' : 1} />
@@ -5,7 +5,7 @@
import { sdk } from '$lib/stores/sdk';
import { Query, type Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { doc } from '../store';
import { row } from '../store';
import { isRelationshipToMany } from './store';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
@@ -15,12 +15,12 @@
export let editing = false;
export let value: string | string[];
export let attribute: Models.AttributeRelationship;
export let column: Models.ColumnRelationship;
export let optionalText: string | undefined = undefined;
const databaseId = page.params.database;
let documentList: Models.DocumentList<Models.Document>;
let rowList: Models.RowList<Models.Row>;
let search: string = null;
let displayNames = ['$id'];
let relatedList: string[] = [];
@@ -32,38 +32,38 @@
onMount(async () => {
if (value) {
if (isRelationshipToMany(attribute)) {
if (isRelationshipToMany(column)) {
relatedList = (value as string[]).slice();
} else {
singleRel = value as string;
}
}
if (editing && $doc?.[attribute.key]) {
if ($doc[attribute.key]?.length) {
if (editing && $row?.[column.key]) {
if ($row[column.key]?.length) {
relatedList =
$doc[attribute.key]?.map((d: Record<string, unknown>) => {
$row[column.key]?.map((d: Record<string, unknown>) => {
return d?.$id;
}) ?? [];
} else {
singleRel = $doc[attribute.key]?.$id;
singleRel = $row[column.key]?.$id;
}
}
displayNames = preferences.getDisplayNames()?.[attribute?.relatedCollection] ?? ['$id'];
displayNames = preferences.getDisplayNames()?.[column?.relatedTable] ?? ['$id'];
if (!displayNames?.includes('$id')) {
displayNames.unshift('$id');
}
});
async function getDocuments(search: string = null) {
async function getRows(search: string = null) {
const queries = search
? [Query.select(['$id']), Query.startsWith('$id', search), Query.orderDesc('')]
: [Query.select(['$id'])];
return await sdk
.forProject(page.params.region, page.params.project)
.databases.listDocuments(databaseId, attribute.relatedCollection, queries);
.tables.listRows(databaseId, column.relatedTable, queries);
}
function getAvailableOptions(excludeIndex?: number) {
@@ -105,7 +105,7 @@
}
// Reactive statements
$: getDocuments(search).then((res) => (documentList = res));
$: getRows(search).then((res) => (rowList = res));
$: paginatedItems = editing
? relatedList
@@ -117,10 +117,10 @@
$: totalCount = relatedList?.length ?? 0;
$: options =
documentList?.documents?.map((document) => {
rowList?.rows?.map((row) => {
const names = displayNames.filter((name) => name !== '$id');
const values = names
.map((name) => document?.[name])
.map((name) => row?.[name])
// always supposed to be a string but just being a bit safe here
.filter((value) => value != null && typeof value === 'string' && value !== '');
@@ -130,15 +130,15 @@
values.map((value) => (value.length > 5 ? value.slice(0, 5) + '...' : value));
const label = !values.length
? document.$id
? row.$id
: // values are in `$id (a | b)` format
// previously used to have a `$id a | b`.
`${document.$id} (${displayValues.join(' | ')})`;
`${row.$id} (${displayValues.join(' | ')})`;
return {
label,
value: document.$id,
data: names.map((name) => document?.[name])
value: row.$id,
data: names.map((name) => row?.[name])
};
}) ?? [];
@@ -150,7 +150,7 @@
$: showEmptyInput = editing && totalCount === 0 && !showInput;
</script>
{#if isRelationshipToMany(attribute)}
{#if isRelationshipToMany(column)}
<Layout.Stack gap="xxl">
<Layout.Stack gap="m">
<Layout.Stack direction="row" alignContent="space-between">
@@ -178,7 +178,7 @@
required
{options}
bind:value={relatedList[0]}
placeholder={`Select ${attribute.key}`}
placeholder={`Select ${column.key}`}
on:change={() => {
if (!relatedList[0]) relatedList = [''];
updateRelatedList();
@@ -196,7 +196,7 @@
required
options={getAvailableOptions(actualIndex)}
bind:value={relatedList[actualIndex]}
placeholder={`Select ${attribute.key}`}
placeholder={`Select ${column.key}`}
on:change={updateRelatedList} />
{#if relatedList[actualIndex]}
<div style:padding-block-start="0.5rem">
@@ -244,7 +244,7 @@
<InputSelect
{id}
required
placeholder={`Select ${attribute.key}`}
placeholder={`Select ${column.key}`}
bind:value={newItemValue}
options={getAvailableOptions()}
on:change={addNewItem} />
@@ -272,8 +272,8 @@
{id}
{label}
{options}
required={attribute.required}
placeholder={`Select ${attribute.key}`}
required={column.required}
placeholder={`Select ${column.key}`}
bind:value={singleRel}
on:change={() => (value = singleRel)} />
{/if}
@@ -0,0 +1,35 @@
import { type Models, Query } from '@appwrite.io/console';
import type { Columns } from '../../store';
export function isRelationshipToMany(column: Models.ColumnRelationship) {
if (!column) return false;
if (!column?.relationType) return false;
if (column?.side === 'child') {
return !['oneToOne', 'oneToMany'].includes(column?.relationType);
} else {
return !['oneToOne', 'manyToOne'].includes(column?.relationType);
}
}
export function isRelationship(column: Columns): column is Models.ColumnRelationship {
if (!column) return false;
return column?.type === 'relationship';
}
export function isString(column: Columns): column is Models.ColumnString {
if (!column) return false;
return column?.type === 'string';
}
/**
* Returns select queries for all main and related fields in a table.
*/
export function buildWildcardColumnsQuery(table: Models.Table | null = null): string[] {
return [
...(table?.columns
?.filter((col) => isRelationship(col))
?.map((col) => Query.select([`${col.key}.*`])) ?? []),
Query.select(['*'])
];
}
@@ -5,25 +5,25 @@
export let id: string;
export let label: string;
export let value: string;
export let attribute: Models.AttributeString;
export let column: Models.ColumnString;
</script>
{#if attribute.size >= 50}
{#if column.size >= 50}
<InputTextarea
{id}
{label}
bind:value
placeholder="Enter string"
maxlength={attribute.size}
required={attribute.required}
nullable={!attribute.required} />
maxlength={column.size}
required={column.required}
nullable={!column.required} />
{:else}
<InputText
{id}
{label}
bind:value
placeholder="Enter string"
maxlength={attribute.size}
required={attribute.required}
nullable={!attribute.required} />
maxlength={column.size}
required={column.required}
nullable={!column.required} />
{/if}
@@ -5,7 +5,7 @@
export let id: string;
export let label: string;
export let value: string | null;
export let attribute: Models.AttributeUrl;
export let column: Models.ColumnUrl;
</script>
<InputURL
@@ -13,5 +13,5 @@
{label}
bind:value
placeholder="Enter URL"
required={attribute.required}
nullable={!attribute.required} />
required={column.required}
nullable={!column.required} />
@@ -7,9 +7,9 @@
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Alert, Selector, Table } from '@appwrite.io/pink-svelte';
import { attributes, collection } from '../store';
import { isRelationship, isRelationshipToMany } from './attributes/store';
import { Alert, Layout, Selector, Table } from '@appwrite.io/pink-svelte';
import { columns, table } from '../store';
import { isRelationship, isRelationshipToMany } from './columns/store';
import type { Models } from '@appwrite.io/console';
export let showDelete = false;
@@ -20,32 +20,32 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.databases.deleteDocument(databaseId, page.params.collection, page.params.document);
.tables.deleteRow(databaseId, page.params.table, page.params.row);
showDelete = false;
addNotification({
type: 'success',
message: `Document has been deleted`
message: `Row has been deleted`
});
trackEvent(Submit.DocumentDelete);
trackEvent(Submit.RowDelete);
await goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}`
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${page.params.table}`
);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.DocumentDelete);
trackError(error, Submit.RowDelete);
}
};
enum Deletion {
'setNull' = 'Set document ID as NULL in all related documents',
'cascade' = 'All related documents will be deleted',
'restrict' = 'Document cannot be deleted'
'setNull' = 'Set row ID as NULL in all related rows',
'cascade' = 'All related rows will be deleted',
'restrict' = 'Row cannot be deleted'
}
$: relAttributes = $attributes?.filter(
$: relColumns = $columns?.filter(
(attribute) =>
isRelationship(attribute) &&
// One-to-One are always included
@@ -56,18 +56,16 @@
(attribute.relationType === 'manyToOne' && attribute.side === 'child') ||
// Many-to-Many: Only include if the parent is being deleted
(isRelationshipToMany(attribute) && attribute.side === 'parent'))
) as Models.AttributeRelationship[];
) as Models.ColumnRelationship[];
</script>
<Confirm title="Delete document" onSubmit={handleDelete} bind:open={showDelete}>
<Confirm title="Delete row" onSubmit={handleDelete} bind:open={showDelete}>
<p data-private>
Are you sure you want to delete <b
>the document from <span data-private>{$collection.name}</span></b
>?
Are you sure you want to delete <b>the row from <span data-private>{$table.name}</span></b>?
</p>
{#if relAttributes?.length}
<p class="text">This document contains the following relationships:</p>
{#if relColumns?.length}
<p class="text">This row contains the following relationships:</p>
<Table.Root
let:root
columns={[
@@ -80,7 +78,7 @@
<Table.Header.Cell column="setting" {root}>Setting</Table.Header.Cell>
<Table.Header.Cell column="desc" {root} />
</svelte:fragment>
{#each relAttributes as attr}
{#each relColumns as attr}
<Table.Row.Base {root}>
<Table.Cell column="relations" {root}>
<span class="u-flex u-cross-center u-gap-8">
@@ -101,18 +99,16 @@
</Table.Row.Base>
{/each}
</Table.Root>
<div class="u-flex u-flex-vertical u-gap-16">
<Layout.Stack gap="l">
<Alert.Inline
title="To change these deletion behaviors edit the relationship settings." />
<Selector.Checkbox
id="delete"
label={`Delete document from ${$collection.name}`}
bind:checked />
</div>
<Selector.Checkbox id="delete" label="Delete row from ${$table.name}" bind:checked />
</Layout.Stack>
{/if}
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button danger submit disabled={relAttributes?.length && !checked}>Delete</Button>
<Button danger submit disabled={relColumns?.length && !checked}>Delete</Button>
</svelte:fragment>
</Confirm>
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff. Show more