diff --git a/deno.lock b/deno.lock index fae6177..5293913 100644 --- a/deno.lock +++ b/deno.lock @@ -9,15 +9,16 @@ "jsr:@std/path@^1.1.1": "1.1.1", "jsr:@std/uuid@*": "1.0.9", "jsr:@std/uuid@^1.0.9": "1.0.9", - "npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.2__picomatch@4.0.3", - "npm:@sveltejs/vite-plugin-svelte@^6.1.1": "6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3", + "npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0", + "npm:@sveltejs/vite-plugin-svelte@^6.1.1": "6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0", "npm:@tsconfig/svelte@^5.0.4": "5.0.4", + "npm:sass-embedded@^1.91.0": "1.91.0", "npm:svelte-check@^4.3.1": "4.3.1_svelte@5.38.1__acorn@8.15.0_typescript@5.8.3", "npm:svelte-language-server@~0.17.19": "0.17.19_prettier@3.3.3_svelte@4.2.20_typescript@5.9.2", "npm:svelte@^5.37.3": "5.38.1_acorn@8.15.0", "npm:typescript@~5.8.3": "5.8.3", "npm:uuid@^11.1.0": "11.1.0", - "npm:vite@^7.1.1": "7.1.2_picomatch@4.0.3", + "npm:vite@^7.1.1": "7.1.2_picomatch@4.0.3_sass-embedded@1.91.0", "npm:zod@^4.0.17": "4.0.17" }, "jsr": { @@ -61,10 +62,19 @@ "@jridgewell/trace-mapping" ] }, + "@bufbuild/protobuf@2.7.0": { + "integrity": "sha512-qn6tAIZEw5i/wiESBF4nQxZkl86aY4KoO0IkUa2Lh+rya64oTOdJQFlZuMwI1Qz9VBJQrQC4QlSA2DNek5gCOA==" + }, "@deno/vite-plugin@1.0.5_vite@7.1.2__picomatch@4.0.3": { "integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==", "dependencies": [ - "vite" + "vite@7.1.2_picomatch@4.0.3" + ] + }, + "@deno/vite-plugin@1.0.5_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0": { + "integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==", + "dependencies": [ + "vite@7.1.2_picomatch@4.0.3_sass-embedded@1.91.0" ] }, "@emmetio/abbreviation@2.3.3": { @@ -369,6 +379,96 @@ "@jridgewell/sourcemap-codec" ] }, + "@parcel/watcher-android-arm64@2.5.1": { + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@parcel/watcher-darwin-arm64@2.5.1": { + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@parcel/watcher-darwin-x64@2.5.1": { + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@parcel/watcher-freebsd-x64@2.5.1": { + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@parcel/watcher-linux-arm-glibc@2.5.1": { + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@parcel/watcher-linux-arm-musl@2.5.1": { + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@parcel/watcher-linux-arm64-glibc@2.5.1": { + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@parcel/watcher-linux-arm64-musl@2.5.1": { + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@parcel/watcher-linux-x64-glibc@2.5.1": { + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@parcel/watcher-linux-x64-musl@2.5.1": { + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@parcel/watcher-win32-arm64@2.5.1": { + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@parcel/watcher-win32-ia32@2.5.1": { + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@parcel/watcher-win32-x64@2.5.1": { + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@parcel/watcher@2.5.1": { + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dependencies": [ + "detect-libc", + "is-glob", + "micromatch", + "node-addon-api" + ], + "optionalDependencies": [ + "@parcel/watcher-android-arm64", + "@parcel/watcher-darwin-arm64", + "@parcel/watcher-darwin-x64", + "@parcel/watcher-freebsd-x64", + "@parcel/watcher-linux-arm-glibc", + "@parcel/watcher-linux-arm-musl", + "@parcel/watcher-linux-arm64-glibc", + "@parcel/watcher-linux-arm64-musl", + "@parcel/watcher-linux-x64-glibc", + "@parcel/watcher-linux-x64-musl", + "@parcel/watcher-win32-arm64", + "@parcel/watcher-win32-ia32", + "@parcel/watcher-win32-x64" + ], + "scripts": true + }, "@rollup/rollup-android-arm-eabi@4.46.2": { "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", "os": ["android"], @@ -478,23 +578,45 @@ "@sveltejs/vite-plugin-svelte-inspector@5.0.0_@sveltejs+vite-plugin-svelte@6.1.1__svelte@5.38.1___acorn@8.15.0__vite@7.1.2___picomatch@4.0.3_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3": { "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", "dependencies": [ - "@sveltejs/vite-plugin-svelte", + "@sveltejs/vite-plugin-svelte@6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3", "debug", "svelte@5.38.1_acorn@8.15.0", - "vite" + "vite@7.1.2_picomatch@4.0.3" + ] + }, + "@sveltejs/vite-plugin-svelte-inspector@5.0.0_@sveltejs+vite-plugin-svelte@6.1.1__svelte@5.38.1___acorn@8.15.0__vite@7.1.2___picomatch@4.0.3_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0": { + "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte@6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0", + "debug", + "svelte@5.38.1_acorn@8.15.0", + "vite@7.1.2_picomatch@4.0.3_sass-embedded@1.91.0" ] }, "@sveltejs/vite-plugin-svelte@6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3": { "integrity": "sha512-vB0Vq47Js7C11L2JrwhncIAoDNkdKDPI500SjLSb34X48dDcsSH5JpLl0cHT0sfO997BrzAS6PKjiZEey/S0VQ==", "dependencies": [ - "@sveltejs/vite-plugin-svelte-inspector", + "@sveltejs/vite-plugin-svelte-inspector@5.0.0_@sveltejs+vite-plugin-svelte@6.1.1__svelte@5.38.1___acorn@8.15.0__vite@7.1.2___picomatch@4.0.3_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3", "debug", "deepmerge", "kleur", "magic-string", "svelte@5.38.1_acorn@8.15.0", - "vite", - "vitefu" + "vite@7.1.2_picomatch@4.0.3", + "vitefu@1.1.1_vite@7.1.2__picomatch@4.0.3" + ] + }, + "@sveltejs/vite-plugin-svelte@6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0": { + "integrity": "sha512-vB0Vq47Js7C11L2JrwhncIAoDNkdKDPI500SjLSb34X48dDcsSH5JpLl0cHT0sfO997BrzAS6PKjiZEey/S0VQ==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte-inspector@5.0.0_@sveltejs+vite-plugin-svelte@6.1.1__svelte@5.38.1___acorn@8.15.0__vite@7.1.2___picomatch@4.0.3_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0", + "debug", + "deepmerge", + "kleur", + "magic-string", + "svelte@5.38.1_acorn@8.15.0", + "vite@7.1.2_picomatch@4.0.3_sass-embedded@1.91.0", + "vitefu@1.1.1_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0" ] }, "@tsconfig/svelte@5.0.4": { @@ -527,6 +649,15 @@ "axobject-query@4.1.0": { "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": [ + "fill-range" + ] + }, + "buffer-builder@0.2.0": { + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==" + }, "chokidar@4.0.3": { "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dependencies": [ @@ -546,6 +677,9 @@ "periscopic" ] }, + "colorjs.io@0.5.2": { + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==" + }, "css-tree@2.3.1": { "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "dependencies": [ @@ -565,6 +699,10 @@ "deepmerge@4.3.1": { "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, + "detect-libc@1.0.3": { + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "bin": true + }, "emmet@2.4.11": { "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", "dependencies": [ @@ -626,10 +764,16 @@ "fdir@6.4.6_picomatch@4.0.3": { "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dependencies": [ - "picomatch" + "picomatch@4.0.3" ], "optionalPeers": [ - "picomatch" + "picomatch@4.0.3" + ] + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": [ + "to-regex-range" ] }, "fsevents@2.3.3": { @@ -640,6 +784,24 @@ "globrex@0.1.2": { "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "immutable@5.1.3": { + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==" + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, "is-reference@3.0.3": { "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dependencies": [ @@ -673,6 +835,13 @@ "mdn-data@2.0.30": { "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, + "micromatch@4.0.8": { + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": [ + "braces", + "picomatch@2.3.1" + ] + }, "mri@1.2.0": { "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" }, @@ -690,6 +859,9 @@ "tslib" ] }, + "node-addon-api@7.1.1": { + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, "pascal-case@3.1.2": { "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dependencies": [ @@ -708,6 +880,9 @@ "picocolors@1.1.1": { "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, + "picomatch@2.3.1": { + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, "picomatch@4.0.3": { "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" }, @@ -763,12 +938,158 @@ ], "bin": true }, + "rxjs@7.8.2": { + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dependencies": [ + "tslib" + ] + }, "sade@1.8.1": { "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dependencies": [ "mri" ] }, + "sass-embedded-all-unknown@1.91.0": { + "integrity": "sha512-AXC1oPqDfLnLtcoxM+XwSnbhcQs0TxAiA5JDEstl6+tt6fhFLKxdyl1Hla39SFtxvMfB2QDUYE3Dmx49O59vYg==", + "dependencies": [ + "sass" + ], + "cpu": ["!arm", "!arm64", "!riscv64", "!x64"] + }, + "sass-embedded-android-arm64@1.91.0": { + "integrity": "sha512-I8Eeg2CeVcZIhXcQLNEY6ZBRF0m7jc818/fypwMwvIdbxGWBekTzc3aKHTLhdBpFzGnDIyR4s7oB0/OjIpzD1A==", + "os": ["android"], + "cpu": ["arm64"] + }, + "sass-embedded-android-arm@1.91.0": { + "integrity": "sha512-DSh1V8TlLIcpklAbn4NINEFs3yD2OzVTbawEXK93IH990upoGNFVNRTstFQ/gcvlbWph3Y3FjAJvo37zUO485A==", + "os": ["android"], + "cpu": ["arm"] + }, + "sass-embedded-android-riscv64@1.91.0": { + "integrity": "sha512-qmsl1a7IIJL0fCOwzmRB+6nxeJK5m9/W8LReXUrdgyJNH5RyxChDg+wwQPVATFffOuztmWMnlJ5CV2sCLZrXcQ==", + "os": ["android"], + "cpu": ["riscv64"] + }, + "sass-embedded-android-x64@1.91.0": { + "integrity": "sha512-/wN0HBLATOVSeN3Tzg0yxxNTo1IQvOxxxwFv7Ki/1/UCg2AqZPxTpNoZj/mn8tUPtiVogMGbC8qclYMq1aRZsQ==", + "os": ["android"], + "cpu": ["x64"] + }, + "sass-embedded-darwin-arm64@1.91.0": { + "integrity": "sha512-gQ6ScInxAN+BDUXy426BSYLRawkmGYlHpQ9i6iOxorr64dtIb3l6eb9YaBV8lPlroUnugylmwN2B3FU9BuPfhA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "sass-embedded-darwin-x64@1.91.0": { + "integrity": "sha512-DSvFMtECL2blYVTFMO5fLeNr5bX437Lrz8R47fdo5438TRyOkSgwKTkECkfh3YbnrL86yJIN2QQlmBMF17Z/iw==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "sass-embedded-linux-arm64@1.91.0": { + "integrity": "sha512-OnKCabD7f420ZEC/6YI9WhCVGMZF+ybZ5NbAB9SsG1xlxrKbWQ1s7CIl0w/6RDALtJ+Fjn8+mrxsxqakoAkeuA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "sass-embedded-linux-arm@1.91.0": { + "integrity": "sha512-ppAZLp3eZ9oTjYdQDf4nM7EehDpkxq5H1hE8FOrx8LpY7pxn6QF+SRpAbRjdfFChRw0K7vh+IiCnQEMp7uLNAg==", + "os": ["linux"], + "cpu": ["arm"] + }, + "sass-embedded-linux-musl-arm64@1.91.0": { + "integrity": "sha512-VfbPpID1C5TT7rukob6CKgefx/TsLE+XZieMNd00hvfJ8XhqPr5DGvSMCNpXlwaedzTirbJu357m+n2PJI9TFQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "sass-embedded-linux-musl-arm@1.91.0": { + "integrity": "sha512-znEsNC2FurPF9+XwQQ6e/fVoic3e5D3/kMB41t/bE8byJVRdaPhkdsszt3pZUE56nNGYoCuieSXUkk7VvyPHsw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "sass-embedded-linux-musl-riscv64@1.91.0": { + "integrity": "sha512-ZfLGldKEEeZjuljKks835LTq7jDRI3gXsKKXXgZGzN6Yymd4UpBOGWiDQlWsWTvw5UwDU2xfFh0wSXbLGHTjVA==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "sass-embedded-linux-musl-x64@1.91.0": { + "integrity": "sha512-4kSiSGPKFMbLvTRbP/ibyiKheOA3fwsJKWU0SOuekSPmybMdrhNkTm0REp6+nehZRE60kC3lXmEV4a7w8Jrwyg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "sass-embedded-linux-riscv64@1.91.0": { + "integrity": "sha512-Y3Fj94SYYvMX9yo49T78yBgBWXtG3EyYUT5K05XyCYkcdl1mVXJSrEmqmRfe4vQGUCaSe/6s7MmsA9Q+mQez7Q==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "sass-embedded-linux-x64@1.91.0": { + "integrity": "sha512-XwIUaE7pQP/ezS5te80hlyheYiUlo0FolQ0HBtxohpavM+DVX2fjwFm5LOUJHrLAqP+TLBtChfFeLj1Ie4Aenw==", + "os": ["linux"], + "cpu": ["x64"] + }, + "sass-embedded-unknown-all@1.91.0": { + "integrity": "sha512-Bj6v7ScQp/HtO91QBy6ood9AArSIN7/RNcT4E7P9QoY3o+e6621Vd28lV81vdepPrt6u6PgJoVKmLNODqB6Q+A==", + "dependencies": [ + "sass" + ], + "os": ["!android", "!darwin", "!linux", "!win32"] + }, + "sass-embedded-win32-arm64@1.91.0": { + "integrity": "sha512-yDCwTiPRex03i1yo7LwiAl1YQ21UyfOxPobD7UjI8AE8ZcB0mQ28VVX66lsZ+qm91jfLslNFOFCD4v79xCG9hA==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "sass-embedded-win32-x64@1.91.0": { + "integrity": "sha512-wiuMz/cx4vsk6rYCnNyoGE5pd73aDJ/zF3qJDose3ZLT1/vV943doJE5pICnS/v5DrUqzV6a1CNq4fN+xeSgFQ==", + "os": ["win32"], + "cpu": ["x64"] + }, + "sass-embedded@1.91.0": { + "integrity": "sha512-VTckYcH1AglrZ3VpPETilTo3Ef472XKwP13lrNfbOHSR6Eo5p27XTkIi+6lrCbuhBFFGAmy+4BRoLaeFUgn+eg==", + "dependencies": [ + "@bufbuild/protobuf", + "buffer-builder", + "colorjs.io", + "immutable", + "rxjs", + "supports-color", + "sync-child-process", + "varint" + ], + "optionalDependencies": [ + "sass-embedded-all-unknown", + "sass-embedded-android-arm", + "sass-embedded-android-arm64", + "sass-embedded-android-riscv64", + "sass-embedded-android-x64", + "sass-embedded-darwin-arm64", + "sass-embedded-darwin-x64", + "sass-embedded-linux-arm", + "sass-embedded-linux-arm64", + "sass-embedded-linux-musl-arm", + "sass-embedded-linux-musl-arm64", + "sass-embedded-linux-musl-riscv64", + "sass-embedded-linux-musl-x64", + "sass-embedded-linux-riscv64", + "sass-embedded-linux-x64", + "sass-embedded-unknown-all", + "sass-embedded-win32-arm64", + "sass-embedded-win32-x64" + ], + "bin": true + }, + "sass@1.91.0": { + "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", + "dependencies": [ + "chokidar", + "immutable", + "source-map-js" + ], + "optionalDependencies": [ + "@parcel/watcher" + ], + "bin": true + }, "semver@7.7.2": { "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "bin": true @@ -776,6 +1097,12 @@ "source-map-js@1.2.1": { "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, + "supports-color@8.1.1": { + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": [ + "has-flag" + ] + }, "svelte-check@4.3.1_svelte@5.38.1__acorn@8.15.0_typescript@5.8.3": { "integrity": "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==", "dependencies": [ @@ -861,11 +1188,26 @@ "zimmerframe" ] }, + "sync-child-process@1.0.2": { + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "dependencies": [ + "sync-message-port" + ] + }, + "sync-message-port@1.1.3": { + "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==" + }, "tinyglobby@0.2.14_picomatch@4.0.3": { "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dependencies": [ "fdir", - "picomatch" + "picomatch@4.0.3" + ] + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" ] }, "tslib@2.8.1": { @@ -889,12 +1231,15 @@ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "bin": true }, + "varint@6.0.0": { + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==" + }, "vite@7.1.2_picomatch@4.0.3": { "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", "dependencies": [ "esbuild", "fdir", - "picomatch", + "picomatch@4.0.3", "postcss", "rollup", "tinyglobby" @@ -904,13 +1249,41 @@ ], "bin": true }, + "vite@7.1.2_picomatch@4.0.3_sass-embedded@1.91.0": { + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", + "dependencies": [ + "esbuild", + "fdir", + "picomatch@4.0.3", + "postcss", + "rollup", + "sass-embedded", + "tinyglobby" + ], + "optionalDependencies": [ + "fsevents" + ], + "optionalPeers": [ + "sass-embedded" + ], + "bin": true + }, "vitefu@1.1.1_vite@7.1.2__picomatch@4.0.3": { "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dependencies": [ - "vite" + "vite@7.1.2_picomatch@4.0.3" ], "optionalPeers": [ - "vite" + "vite@7.1.2_picomatch@4.0.3" + ] + }, + "vitefu@1.1.1_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0": { + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dependencies": [ + "vite@7.1.2_picomatch@4.0.3_sass-embedded@1.91.0" + ], + "optionalPeers": [ + "vite@7.1.2_picomatch@4.0.3_sass-embedded@1.91.0" ] }, "vscode-css-languageservice@6.3.7": { @@ -981,6 +1354,7 @@ "npm:@deno/vite-plugin@^1.0.5", "npm:@sveltejs/vite-plugin-svelte@^6.1.1", "npm:@tsconfig/svelte@^5.0.4", + "npm:sass-embedded@^1.91.0", "npm:svelte-check@^4.3.1", "npm:svelte-language-server@~0.17.19", "npm:svelte@^5.37.3", diff --git a/interim-models/migrations/20250528233517_lenses.up.sql b/interim-models/migrations/20250528233517_lenses.up.sql index 7934f3a..18dd9f9 100644 --- a/interim-models/migrations/20250528233517_lenses.up.sql +++ b/interim-models/migrations/20250528233517_lenses.up.sql @@ -16,6 +16,6 @@ create table if not exists fields ( lens_id uuid not null references lenses(id) on delete cascade, name text not null, label text, - field_type jsonb not null, + presentation jsonb not null, width_px int not null default 200 ); diff --git a/interim-models/src/encodable.rs b/interim-models/src/encodable.rs new file mode 100644 index 0000000..ba4c6c5 --- /dev/null +++ b/interim-models/src/encodable.rs @@ -0,0 +1,45 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::Postgres; +use uuid::Uuid; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(tag = "t", content = "c")] +pub enum Encodable { + Text(Option), + Timestamp(Option>), + Uuid(Option), +} + +impl Encodable { + // TODO: Can something similar be achieved with a generic return type? + /// Bind this as a parameter to a sqlx query. + pub fn bind_onto<'a>( + self, + query: sqlx::query::Query<'a, Postgres, ::Arguments<'a>>, + ) -> sqlx::query::Query<'a, Postgres, ::Arguments<'a>> { + match self { + Self::Text(value) => query.bind(value), + Self::Timestamp(value) => query.bind(value), + Self::Uuid(value) => query.bind(value), + } + } + + /// Transform the contained value into a serde_json::Value. + pub fn inner_as_value(&self) -> serde_json::Value { + let serialized = serde_json::to_value(self).unwrap(); + #[derive(Deserialize)] + struct Tagged { + c: serde_json::Value, + } + let deserialized: Tagged = serde_json::from_value(serialized).unwrap(); + deserialized.c + } + + pub fn is_none(&self) -> bool { + match self { + Self::Text(None) | Self::Timestamp(None) | Self::Uuid(None) => true, + Self::Text(_) | Self::Timestamp(_) | Self::Uuid(_) => false, + } + } +} diff --git a/interim-models/src/expression.rs b/interim-models/src/expression.rs index 78bebef..a8dd32c 100644 --- a/interim-models/src/expression.rs +++ b/interim-models/src/expression.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use interim_pgtypes::escape_identifier; use serde::{Deserialize, Serialize}; -use crate::field::Encodable; +use crate::encodable::Encodable; #[derive(Clone, Debug, PartialEq)] pub struct QueryFragment { diff --git a/interim-models/src/field.rs b/interim-models/src/field.rs index afff827..c7d8d98 100644 --- a/interim-models/src/field.rs +++ b/interim-models/src/field.rs @@ -1,4 +1,3 @@ -use chrono::{DateTime, Utc}; use derive_builder::Builder; use interim_pgtypes::pg_attribute::PgAttribute; use serde::{Deserialize, Serialize}; @@ -7,45 +6,48 @@ use thiserror::Error; use uuid::Uuid; use crate::client::AppDbClient; +use crate::encodable::Encodable; +use crate::presentation::Presentation; -pub const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S"; - +/// A materialization of a database column, fit for consumption by an end user. +/// +/// There may be zero or more fields per column/attribute in a Postgres view. +/// There may in some case also be fields with no underlying column, if it has +/// been removed or altered. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Field { + /// Internal ID for application use. pub id: Uuid, + + /// Name of the database column. pub name: String, + + /// Optional human friendly label. pub label: Option, - pub field_type: sqlx::types::Json, + + /// Refer to documentation for `Presentation`. + pub presentation: sqlx::types::Json, + + /// Width of UI table column in pixels. pub width_px: i32, } impl Field { - pub fn insertable_builder() -> InsertableFieldBuilder { + /// Constructs a brand new field to be inserted into the application db. + pub fn insert() -> InsertableFieldBuilder { InsertableFieldBuilder::default() } - pub fn default_from_attr(attr: &PgAttribute) -> Self { - Self { + /// Generate a default field config based on an existing column's name and + /// type. + pub fn default_from_attr(attr: &PgAttribute) -> Option { + Presentation::default_from_attr(attr).map(|presentation| Self { id: Uuid::now_v7(), name: attr.attname.clone(), label: None, - field_type: sqlx::types::Json(FieldType::default_from_attr(attr)), + presentation: sqlx::types::Json(presentation), width_px: 200, - } - } - - pub fn webc_tag(&self) -> &str { - match self.field_type.0 { - FieldType::InterimUser {} => "cell-interim-user", - FieldType::Text {} => "cell-text", - FieldType::Timestamp { .. } => "cell-timestamp", - FieldType::Uuid {} => "cell-uuid", - FieldType::Unknown => "cell-unknown", - } - } - - pub fn webc_custom_attrs(&self) -> Vec<(String, String)> { - vec![] + }) } pub fn get_value_encodable(&self, row: &PgRow) -> Result { @@ -54,6 +56,7 @@ impl Field { .or(Err(ParseError::FieldNotFound))?; let type_info = value_ref.type_info(); let ty = type_info.name(); + dbg!(&ty); Ok(match ty { "TEXT" | "VARCHAR" => { Encodable::Text( as Decode>::decode(value_ref).unwrap()) @@ -84,7 +87,7 @@ select id, name, label, - field_type as "field_type: sqlx::types::Json", + presentation as "presentation: sqlx::types::Json", width_px from fields where lens_id = $1 @@ -96,59 +99,13 @@ where lens_id = $1 } } -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "t", content = "c")] -pub enum FieldType { - InterimUser {}, - Text {}, - Timestamp { - format: String, - }, - Uuid {}, - /// A special variant for when the field type is not specified and cannot be - /// inferred. This isn't represented as an error, because we still want to - /// be able to define display behavior via the .render() method. - Unknown, -} - -impl FieldType { - pub fn default_from_attr(attr: &PgAttribute) -> Self { - match attr.regtype.as_str() { - "text" => Self::Text {}, - "timestamp" => Self::Timestamp { - format: RFC_3339_S.to_owned(), - }, - "uuid" => Self::Uuid {}, - _ => Self::Unknown, - } - } - - /// Returns a SQL fragment for the default data type for creating or - /// altering a backing column, such as "integer", or "timestamptz". Returns - /// None if the field type is Unknown. - pub fn attr_data_type_fragment(&self) -> Option<&'static str> { - match self { - Self::InterimUser {} | Self::Text {} => Some("text"), - Self::Timestamp { .. } => Some("timestamptz"), - Self::Uuid { .. } => Some("uuid"), - Self::Unknown => None, - } - } -} - -#[derive(Clone, Debug, Error)] -#[error("field type is unknown")] -pub struct FieldTypeUnknownError {} - -// -------- Insertable -------- - #[derive(Builder, Clone, Debug)] pub struct InsertableField { lens_id: Uuid, name: String, #[builder(default)] label: Option, - field_type: FieldType, + presentation: Presentation, #[builder(default = 200)] width_px: i32, } @@ -159,20 +116,20 @@ impl InsertableField { Field, r#" insert into fields -(id, lens_id, name, label, field_type, width_px) +(id, lens_id, name, label, presentation, width_px) values ($1, $2, $3, $4, $5, $6) returning id, name, label, - field_type as "field_type: sqlx::types::Json", + presentation as "presentation: sqlx::types::Json", width_px "#, Uuid::now_v7(), self.lens_id, self.name, self.label, - sqlx::types::Json::<_>(self.field_type) as sqlx::types::Json, + sqlx::types::Json::<_>(self.presentation) as sqlx::types::Json, self.width_px, ) .fetch_one(&mut *app_db.conn) @@ -184,14 +141,12 @@ impl InsertableFieldBuilder { pub fn default_from_attr(attr: &PgAttribute) -> Self { Self { name: Some(attr.attname.clone()), - field_type: Some(FieldType::default_from_attr(attr)), + presentation: Presentation::default_from_attr(attr), ..Self::default() } } } -// -------- Errors -------- - /// Error when parsing a sqlx value to JSON #[derive(Debug, Error)] pub enum ParseError { @@ -202,45 +157,3 @@ pub enum ParseError { #[error("unknown postgres type")] UnknownType, } - -// -------- Encodable -------- -// TODO this should probably be moved to another crate - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -#[serde(tag = "t", content = "c")] -pub enum Encodable { - Text(Option), - Timestamp(Option>), - Uuid(Option), -} - -impl Encodable { - pub fn bind_onto<'a>( - self, - query: sqlx::query::Query<'a, Postgres, ::Arguments<'a>>, - ) -> sqlx::query::Query<'a, Postgres, ::Arguments<'a>> { - match self { - Self::Text(value) => query.bind(value), - Self::Timestamp(value) => query.bind(value), - Self::Uuid(value) => query.bind(value), - } - } - - /// Transform the contained value into a serde_json::Value. - pub fn inner_as_value(&self) -> serde_json::Value { - let serialized = serde_json::to_value(self).unwrap(); - #[derive(Deserialize)] - struct Tagged { - c: serde_json::Value, - } - let deserialized: Tagged = serde_json::from_value(serialized).unwrap(); - deserialized.c - } - - pub fn is_none(&self) -> bool { - match self { - Self::Text(None) | Self::Timestamp(None) | Self::Uuid(None) => true, - Self::Text(_) | Self::Timestamp(_) | Self::Uuid(_) => false, - } - } -} diff --git a/interim-models/src/lib.rs b/interim-models/src/lib.rs index f90e3c2..e10c0ca 100644 --- a/interim-models/src/lib.rs +++ b/interim-models/src/lib.rs @@ -1,8 +1,10 @@ pub mod base; pub mod client; +pub mod encodable; pub mod expression; pub mod field; pub mod lens; +pub mod presentation; pub mod rel_invitation; pub mod user; diff --git a/interim-models/src/presentation.rs b/interim-models/src/presentation.rs new file mode 100644 index 0000000..a187d4b --- /dev/null +++ b/interim-models/src/presentation.rs @@ -0,0 +1,61 @@ +use interim_pgtypes::pg_attribute::PgAttribute; +use serde::{Deserialize, Serialize}; + +pub const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S"; + +/// Struct defining how a field's is displayed and how it accepts input in UI. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "t", content = "c")] +pub enum Presentation { + Array { inner: Box }, + Dropdown { allow_custom: bool }, + Text { input_mode: TextInputMode }, + Timestamp { format: String }, + Uuid {}, +} + +impl Presentation { + /// Returns a SQL fragment for the default data type for creating or + /// altering a backing column, such as "integer", or "timestamptz". + pub fn attr_data_type_fragment(&self) -> String { + match self { + Self::Array { inner } => format!("{0}[]", inner.attr_data_type_fragment()), + Self::Dropdown { .. } | Self::Text { .. } => "text".to_owned(), + Self::Timestamp { .. } => "timestamptz".to_owned(), + Self::Uuid { .. } => "uuid".to_owned(), + } + } + + /// Generate the default presentation based on an existing column's type. + /// Returns None if no default presentation exists. + pub fn default_from_attr(attr: &PgAttribute) -> Option { + match attr.regtype.to_lowercase().as_str() { + "text" => Some(Self::Text { + input_mode: TextInputMode::MultiLine {}, + }), + "timestamp" => Some(Self::Timestamp { + format: RFC_3339_S.to_owned(), + }), + "uuid" => Some(Self::Uuid {}), + _ => None, + } + } + + /// Bet the web component tag name to use for rendering a UI cell. + pub fn cell_webc_tag(&self) -> String { + match self { + Self::Array { .. } => todo!(), + Self::Dropdown { .. } => "cell-dropdown".to_owned(), + Self::Text { .. } => "cell-text".to_owned(), + Self::Timestamp { .. } => "cell-timestamp".to_owned(), + Self::Uuid {} => "cell-uuid".to_owned(), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "t", content = "c")] +pub enum TextInputMode { + SingleLine {}, + MultiLine {}, +} diff --git a/interim-models/src/selection.rs b/interim-models/src/selection.rs deleted file mode 100644 index 444bc65..0000000 --- a/interim-models/src/selection.rs +++ /dev/null @@ -1,107 +0,0 @@ -use derive_builder::Builder; -use interim_pgtypes::pg_attribute::PgAttribute; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use sqlx::{PgExecutor, query_as}; -use uuid::Uuid; - -use crate::field::{Field, FieldType}; - -#[derive(Clone, Debug, Serialize)] -pub struct Selection { - pub id: Uuid, - pub attr_filters: sqlx::types::Json>, - pub label: Option, - pub field_type: Option>, - pub visible: bool, - pub width_px: i32, -} - -impl Selection { - pub fn insertable_builder() -> InsertableSelectionBuilder { - InsertableSelectionBuilder::default() - } - - pub fn resolve_fields_from_attrs(&self, all_attrs: &[PgAttribute]) -> Vec { - if self.visible { - let mut filtered_attrs = all_attrs.to_owned(); - for attr_filter in self.attr_filters.0.clone() { - filtered_attrs.retain(|attr| attr_filter.matches(attr)); - } - filtered_attrs - .into_iter() - .map(|attr| Field { - name: attr.attname.clone(), - label: self.label.clone(), - field_type: self - .field_type - .clone() - .unwrap_or_else(|| sqlx::types::Json(FieldType::default_from_attr(&attr))), - width_px: self.width_px, - }) - .collect() - } else { - vec![] - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum AttrFilter { - NameEq(String), - NameMatches(String), - TypeEq(String), -} - -impl AttrFilter { - pub fn matches(&self, attr: &PgAttribute) -> bool { - match self { - Self::NameEq(name) => &attr.attname == name, - Self::NameMatches(pattern) => Regex::new(pattern) - .map(|re| re.is_match(&attr.attname)) - .unwrap_or(false), - Self::TypeEq(_) => todo!("attr type filter is not yet implemented"), - } - } -} - -#[derive(Builder, Clone, Debug)] -pub struct InsertableSelection { - lens_id: Uuid, - attr_filters: Vec, - #[builder(default, setter(strip_option))] - label: Option, - #[builder(default, setter(strip_option))] - field_type: Option, - #[builder(default = true)] - visible: bool, -} - -impl InsertableSelection { - pub async fn insert<'a, E: PgExecutor<'a>>(self, app_db: E) -> Result { - query_as!( - Selection, - r#" -insert into lens_selections -(id, lens_id, attr_filters, label, field_type, visible) -values ($1, $2, $3, $4, $5, $6) -returning - id, - attr_filters as "attr_filters: sqlx::types::Json>", - label, - field_type as "field_type?: sqlx::types::Json", - visible, - width_px -"#, - Uuid::now_v7(), - self.lens_id, - sqlx::types::Json::<_>(self.attr_filters) as sqlx::types::Json>, - self.label, - self.field_type.map(|value| sqlx::types::Json::<_>(value)) - as Option>, - self.visible, - ) - .fetch_one(app_db) - .await - } -} diff --git a/interim-pgtypes/src/pg_attribute.rs b/interim-pgtypes/src/pg_attribute.rs index 867395f..abb0745 100644 --- a/interim-pgtypes/src/pg_attribute.rs +++ b/interim-pgtypes/src/pg_attribute.rs @@ -17,6 +17,10 @@ pub struct PgAttribute { pub attlen: i16, /// The number of the column. Ordinary columns are numbered from 1 up. System columns, such as ctid, have (arbitrary) negative numbers. pub attnum: i16, + /// Number of dimensions, if the column is an array type; otherwise 0. + /// (Presently, the number of dimensions of an array is not enforced, so any + /// nonzero value effectively means “it's an array”.) + pub attndims: i16, /// This represents a not-null constraint. pub attnotnull: Option, /// This column has a default expression or generation expression, in which case there will be a corresponding entry in the pg_attrdef catalog that actually defines the expression. (Check attgenerated to determine whether this is a default or a generation expression.) @@ -66,6 +70,7 @@ select atttypid::regtype::text as "regtype!", attlen, attnum, + attndims, attnotnull as "attnotnull?", atthasdef, atthasmissing, @@ -102,6 +107,7 @@ select atttypid::regtype::text as "regtype!", a.attlen as attlen, a.attnum as attnum, + a.attndims as attndims, a.attnotnull as "attnotnull?", a.atthasdef as atthasdef, a.atthasmissing as atthasmissing, diff --git a/interim-server/src/field.rs b/interim-server/src/field_info.rs similarity index 100% rename from interim-server/src/field.rs rename to interim-server/src/field_info.rs diff --git a/interim-server/src/main.rs b/interim-server/src/main.rs index 773fd2a..3a0f55e 100644 --- a/interim-server/src/main.rs +++ b/interim-server/src/main.rs @@ -15,7 +15,7 @@ mod auth; mod base_pooler; mod base_user_perms; mod cli; -mod field; +mod field_info; mod middleware; mod navbar; mod navigator; diff --git a/interim-server/src/router.rs b/interim-server/src/router.rs index 446dc9f..4584e2c 100644 --- a/interim-server/src/router.rs +++ b/interim-server/src/router.rs @@ -69,10 +69,6 @@ pub fn new_router(state: AppState) -> Router<()> { "/d/{base_id}/r/{class_oid}/l/{lens_id}/get-data", get(routes::lenses::get_data_page_get), ) - // .route( - // "/d/{base_id}/r/{class_oid}/l/{lens_id}/add-selection", - // post(routes::lenses::add_selection_page_post), - // ) .route( "/d/{base_id}/r/{class_oid}/l/{lens_id}/add-column", post(routes::lenses::add_column_page_post), diff --git a/interim-server/src/routes/lens_index.rs b/interim-server/src/routes/lens_index.rs index 1712326..87b7822 100644 --- a/interim-server/src/routes/lens_index.rs +++ b/interim-server/src/routes/lens_index.rs @@ -43,7 +43,7 @@ pub async fn lens_page_get( let attr_names: Vec = attrs.iter().map(|attr| attr.attname.clone()).collect(); #[derive(Template)] - #[template(path = "lens0_2.html")] + #[template(path = "lens.html")] struct ResponseTemplate { attr_names: Vec, filter: Option, diff --git a/interim-server/src/routes/lens_insert.rs b/interim-server/src/routes/lens_insert.rs index adf69f6..dc8b4e1 100644 --- a/interim-server/src/routes/lens_insert.rs +++ b/interim-server/src/routes/lens_insert.rs @@ -5,7 +5,7 @@ use axum::{ response::Response, }; use axum_extra::extract::Form; -use interim_models::{field::Encodable, lens::Lens}; +use interim_models::{encodable::Encodable, lens::Lens}; use interim_pgtypes::{escape_identifier, pg_class::PgClass}; use sqlx::{postgres::types::Oid, query}; diff --git a/interim-server/src/routes/lens_set_filter.rs b/interim-server/src/routes/lens_set_filter.rs index d9e9a0b..2d804a4 100644 --- a/interim-server/src/routes/lens_set_filter.rs +++ b/interim-server/src/routes/lens_set_filter.rs @@ -9,7 +9,7 @@ use super::LensPagePath; #[derive(Deserialize)] pub struct FormBody { - filter_expression: String, + filter_expression: Option, } pub async fn lens_set_filter_page_post( @@ -23,7 +23,8 @@ pub async fn lens_set_filter_page_post( let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?; - let filter: Option = serde_json::from_str(&body.filter_expression)?; + let filter: Option = + serde_json::from_str(&body.filter_expression.unwrap_or("null".to_owned()))?; Lens::update() .id(lens.id) .filter(filter) diff --git a/interim-server/src/routes/lenses.rs b/interim-server/src/routes/lenses.rs index d3dcbf3..66e2681 100644 --- a/interim-server/src/routes/lenses.rs +++ b/interim-server/src/routes/lenses.rs @@ -9,8 +9,10 @@ use axum::{ use axum_extra::extract::Form; use interim_models::{ base::Base, - field::{Encodable, Field, FieldType, InsertableFieldBuilder, RFC_3339_S}, + encodable::Encodable, + field::{Field, InsertableFieldBuilder}, lens::{Lens, LensDisplayType}, + presentation::{Presentation, RFC_3339_S}, }; use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass}; use serde::{Deserialize, Serialize}; @@ -25,8 +27,7 @@ use crate::{ app_error::{AppError, bad_request}, app_state::AppDbConn, base_pooler::{BasePooler, RoleAssignment}, - field::FieldInfo, - navbar::{NavLocation, Navbar, RelLocation}, + field_info::FieldInfo, navigator::Navigator, settings::Settings, user::CurrentUser, @@ -232,7 +233,8 @@ pub async fn get_data_page_get( for row in rows.iter() { let mut pkey_values: HashMap = HashMap::new(); for attr in pkey_attrs.clone() { - let field = Field::default_from_attr(&attr); + let field = Field::default_from_attr(&attr) + .ok_or(anyhow::anyhow!("unsupported primary key column type"))?; pkey_values.insert(field.name.clone(), field.get_value_encodable(row)?); } let pkey = serde_json::to_string(&pkey_values)?; @@ -265,21 +267,21 @@ pub async fn get_data_page_get( pub struct AddColumnPageForm { name: String, label: String, - field_type: String, + presentation_tag: String, timestamp_format: Option, } -fn try_field_type_from_form(form: &AddColumnPageForm) -> Result { - let serialized = match form.field_type.as_str() { +fn try_presentation_from_form(form: &AddColumnPageForm) -> Result { + let serialized = match form.presentation_tag.as_str() { "Timestamp" => { json!({ - "t": form.field_type, + "t": form.presentation_tag, "c": { "format": form.timestamp_format.clone().unwrap_or(RFC_3339_S.to_owned()), }, }) } - _ => json!({"t": form.field_type}), + _ => json!({"t": form.presentation_tag}), }; serde_json::from_value(serialized).or(Err(bad_request!("unable to parse field type"))) } @@ -307,10 +309,8 @@ pub async fn add_column_page_post( .fetch_one(&mut base_client) .await?; - let field_type = try_field_type_from_form(&form)?; - let data_type_fragment = field_type.attr_data_type_fragment().ok_or(bad_request!( - "cannot create column with type specified as Unknown" - ))?; + let presentation = try_presentation_from_form(&form)?; + let data_type_fragment = presentation.attr_data_type_fragment(); query(&format!( r#" @@ -324,7 +324,7 @@ add column if not exists {1} {2} .execute(base_client.get_conn()) .await?; - Field::insertable_builder() + Field::insert() .lens_id(lens.id) .name(form.name) .label(if form.label.is_empty() { @@ -332,7 +332,7 @@ add column if not exists {1} {2} } else { Some(form.label) }) - .field_type(field_type) + .presentation(presentation) .build()? .insert(&mut app_db) .await?; diff --git a/interim-server/src/routes/relations.rs b/interim-server/src/routes/relations.rs index 41a6373..37382d2 100644 --- a/interim-server/src/routes/relations.rs +++ b/interim-server/src/routes/relations.rs @@ -63,6 +63,7 @@ pub async fn list_relations_page( let all_rels = PgClass::with_kind_in([PgRelKind::OrdinaryTable]) .fetch_all(&mut client) .await?; + dbg!(&all_rels); let accessible_rels: Vec = all_rels .into_iter() .filter(|rel| { diff --git a/interim-server/templates/base_config.html b/interim-server/templates/base_config.html index e0c57e1..9f07bb9 100644 --- a/interim-server/templates/base_config.html +++ b/interim-server/templates/base_config.html @@ -8,7 +8,7 @@
- +
diff --git a/interim-server/templates/lens.html b/interim-server/templates/lens.html index 3581028..874bb26 100644 --- a/interim-server/templates/lens.html +++ b/interim-server/templates/lens.html @@ -3,81 +3,18 @@ {% block main %}
-
+
+ +
{{ navbar | safe }}
- - - - - {% for field in fields %} - - {% endfor %} - - - - - {% for (i, row) in rows.iter().enumerate() %} - {# TODO: store primary keys in a Vec separate from rows #} - - {% for (j, field) in fields.iter().enumerate() %} - {# Setting max-width is required for overflow to work properly. #} - - {% endfor %} - - {% endfor %} - - {% for (i, field) in fields.iter().enumerate() %} - - -
- {{ field.label.clone().unwrap_or(field.name.clone()) }} - - -
- {% match field.get_value_encodable(row) %} - {% when Ok with (encodable) %} - <{{ field.webc_tag() | safe }} - {% for (k, v) in field.webc_custom_attrs() %} - {{ k }}="{{ v }}" - {% endfor %} - row="{{ i }}" - column="{{ j }}" - value="{{ encodable | json }}" - class="cell" - > - {{ encodable.inner_as_value() | json }} - {{ err }} - {% endmatch %} -
- <{{ field.webc_tag() | safe }} - {% for (k, v) in field.webc_custom_attrs() %} - {{ k }}="{{ v }}" - {% endfor %} - row="{{ pkeys.len() }}" - column="{{ i }}" - class="cell" - insertable="true" - value="{{ field.field_type.default_for_insert()? | json }}" - > - - {% endfor %} -
- -
+
- - - - - + + + {% endblock %} + diff --git a/interim-server/templates/lens0_2.html b/interim-server/templates/lens0_2.html deleted file mode 100644 index 6c762e7..0000000 --- a/interim-server/templates/lens0_2.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} - -{% block main %} - -
-
- -
-
- {{ navbar | safe }} -
-
- -
-
- - -{% endblock %} - diff --git a/interim-server/templates/navbar.html b/interim-server/templates/navbar.html index 872c66e..2efcce0 100644 --- a/interim-server/templates/navbar.html +++ b/interim-server/templates/navbar.html @@ -83,5 +83,5 @@ {% endfor -%} - + diff --git a/mise.toml b/mise.toml index b1e1268..80755d9 100644 --- a/mise.toml +++ b/mise.toml @@ -22,11 +22,6 @@ run = "deno run -A npm:vite build" dir = "./svelte" sources = ["svelte/src/**/*.ts", "svelte/src/**/*.svelte"] -[tasks.build-gleam] -run = "sh build.sh" -dir = "./webc" -sources = ["webc/src/**/*.gleam", "webc/src/**/*.mjs"] - [tasks.build-css] run = "sass sass/:css_dist/" sources = ["sass/**/*.scss"] diff --git a/package.json b/package.json index c28ea40..9732f23 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@deno/vite-plugin": "^1.0.5", "@sveltejs/vite-plugin-svelte": "^6.1.1", + "sass-embedded": "^1.91.0", "svelte-language-server": "^0.17.19", "uuid": "^11.1.0", "vite": "^7.1.1", diff --git a/sass/_field-adder.scss b/sass/_field-adder.scss new file mode 100644 index 0000000..0dc19af --- /dev/null +++ b/sass/_field-adder.scss @@ -0,0 +1,30 @@ +@use 'globals'; +@use 'viewer-shared'; + +.field-adder { + &__container { + align-items: stretch; + display: flex; + } + + &__header-lookalike { + @include viewer-shared.th; + border-bottom-style: dashed; + border-right-style: dashed; + } + + &__label-input { + @include globals.reset-input; + } + + &__popover:popover-open { + @include globals.popover; + padding: 1rem; + } + + &__summary-buttons { + align-items: center; + display: flex; + } +} + diff --git a/sass/_globals.scss b/sass/_globals.scss index bab13cc..0bb3a85 100644 --- a/sass/_globals.scss +++ b/sass/_globals.scss @@ -106,3 +106,15 @@ $hover-lightness-scale-factor: -10%; @mixin rounded { border-radius: $border-radius-rounded; } + +@mixin popover { + @include rounded; + inset: unset; + border: $popover-border; + margin: 0; + margin-top: 0.25rem; + position: relative; + display: block; + background: #fff; + box-shadow: $popover-shadow; +} diff --git a/sass/field_adder/_styles.scss b/sass/field_adder/_styles.scss deleted file mode 100644 index 3a37cd6..0000000 --- a/sass/field_adder/_styles.scss +++ /dev/null @@ -1,48 +0,0 @@ -@use '../globals'; -@use '../viewer-shared'; - -field-adder { - height: 100%; - - &::part(expander) { - @include globals.reset-button; - width: 100%; - height: 100%; - } - - &::part(th-lookalike) { - @include viewer-shared.th; - border-right-style: dashed; - border-bottom-style: dashed; - border-left: none; - display: flex; - } - - &::part(label-input) { - appearance: none; - background: none; - border: none; - outline: none; - font-weight: inherit; - } - - &::part(config-popover) { - @include globals.rounded-sm; - position: fixed; - inset: unset; - margin: 0; - width: 20rem; - padding: 1rem; - font-weight: normal; - border: globals.$popover-border; - filter: drop-shadow(globals.$popover-shadow); - - &:popover-open { - display: block; - } - } - - &::part(form-section-header) { - margin: 0; - } -} diff --git a/sass/main.scss b/sass/main.scss index 6e6e648..9364afa 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -2,6 +2,7 @@ @use 'globals'; @use 'modern-normalize'; +@use 'forms'; html { font-family: "Averia Serif Libre", "Open Sans", "Helvetica Neue", Arial, sans-serif; @@ -52,6 +53,10 @@ button, input[type="submit"] { &--secondary { @include globals.button-secondary; } + + &--clear { + @include globals.button-clear; + } } .page-grid { @@ -151,18 +156,8 @@ button, input[type="submit"] { &__popover { &:popover-open { - @include globals.rounded; - inset: unset; - border: globals.$popover-border; - margin: 0; - margin-top: 0.25rem; - position: fixed; - display: flex; - flex-direction: column; + @include globals.popover; width: 16rem; - padding: 0; - background: #fff; - box-shadow: globals.$popover-shadow; // FIXME: This makes button border radius work correctly, but also hides // the outline that appears when each button is focused, particularly // when there is only one button present. @@ -178,3 +173,23 @@ button, input[type="submit"] { text-align: left; } } + +.combobox { + &__popover:popover-open { + @include globals.popover; + padding: 0; + } + + &__completion { + @include globals.reset-button; + display: block; + padding: 0.5rem; + font-weight: normal; + text-align: left; + width: 100%; + + &:hover, &:focus { + background: #0000001f; + } + } +} diff --git a/sass/navbar.scss b/sass/navbar.scss index ba8ccbe..7d6af84 100644 --- a/sass/navbar.scss +++ b/sass/navbar.scss @@ -1,4 +1,5 @@ @use 'globals'; +@use 'collapsible_menu'; $background-current-item: #0001; diff --git a/sass/viewer.scss b/sass/viewer.scss index 9aa30b1..336d5c0 100644 --- a/sass/viewer.scss +++ b/sass/viewer.scss @@ -1,6 +1,7 @@ @use 'globals'; @use 'sass:color'; @use 'condition-editor'; +@use 'field-adder'; $table-border-color: #ccc; @@ -31,19 +32,6 @@ $table-border-color: #ccc; align-items: stretch; } - &__header { - display: flex; - flex: none; - align-items: center; - border: solid 1px #ccc; - border-top: none; - border-left: none; - font-family: "Funnel Sans"; - background: #0001; - padding: 0.5rem; - text-align: left; - } - &__main { grid-area: main; overflow-y: auto; @@ -52,13 +40,16 @@ $table-border-color: #ccc; &__row { align-items: stretch; display: flex; + height: 2.25rem; } &__cell { - flex: none; + align-items: stretch; border: solid 1px $table-border-color; border-left: none; border-top: none; + display: flex; + flex: none; padding: 0; &--insertable { @@ -79,6 +70,8 @@ $table-border-color: #ccc; &__container { align-items: center; display: flex; + flex: none; + width: 100%; &--selected { outline: 3px solid #37f; @@ -94,7 +87,7 @@ $table-border-color: #ccc; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - padding: 0.5rem; + padding: 0 0.5rem; } &--uuid { @@ -102,15 +95,21 @@ $table-border-color: #ccc; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - padding: 0.5rem; + padding: 0 0.5rem; } &--null { - color: color.scale(#000, $lightness: 50%, $space: hsl); + align-items: center; + color: color.scale(#000, $lightness: 65%, $space: hsl); + display: flex; font-family: globals.$font-family-data; font-style: oblique; - text-align: center; - padding: 0.5rem; + justify-content: center; + padding: 0 0.25rem; + + svg path { + fill: currentColor; + } } } @@ -121,7 +120,48 @@ $table-border-color: #ccc; padding: 0 0.25rem; svg path { - stroke: currentColor; + fill: currentColor; + } + } +} + +.field-header { + &__container { + align-items: center; + background: #0001; + border: solid 1px #ccc; + border-top: none; + border-left: none; + display: flex; + flex: none; + font-family: globals.$font-family-data; + justify-content: space-between; + padding: 0.25rem; + text-align: left; + } + + &__label { + padding: 0.25rem; + } + + &__type-indicator { + @include globals.button-clear; + align-items: center; + display: flex; + height: 2rem; + justify-content: center; + padding: 0; + width: 2rem; + + svg path { + fill: currentColor; + } + } + + &__popover { + &:popover-open { + @include globals.popover; + padding: 1rem; } } } @@ -155,7 +195,7 @@ $table-border-color: #ccc; padding: 0 1rem; svg path { - stroke: currentColor; + fill: currentColor; } } } diff --git a/svelte/src/collapsible-menu.webc.svelte b/svelte/src/collapsible-menu.webc.svelte new file mode 100644 index 0000000..b1ab6ef --- /dev/null +++ b/svelte/src/collapsible-menu.webc.svelte @@ -0,0 +1,51 @@ + + + + + + +
+ +
+ +
+
diff --git a/svelte/src/combobox.svelte b/svelte/src/combobox.svelte new file mode 100644 index 0000000..7c61aee --- /dev/null +++ b/svelte/src/combobox.svelte @@ -0,0 +1,90 @@ + + +
+ +
+ {#each completions as completion} + + {/each} +
+
diff --git a/svelte/src/editor-state.svelte.ts b/svelte/src/editor-state.svelte.ts index 68872b6..6909498 100644 --- a/svelte/src/editor-state.svelte.ts +++ b/svelte/src/editor-state.svelte.ts @@ -1,11 +1,12 @@ import * as uuid from "uuid"; -import { type Encodable, type FieldType } from "./field.svelte.ts"; +import { type Encodable } from "./encodable.svelte.ts"; +import { type Presentation } from "./presentation.svelte.ts"; type Assert<_T extends true> = void; // This should be a discriminated union type, but TypeScript isn't -// sophisticated enough to discriminate based on the nested field_type's tag, +// sophisticated enough to discriminate based on the nested presentation's tag, // causing a huge pain in the ass. export type EditorState = { date_value: string; @@ -54,16 +55,16 @@ export function editor_state_from_encodable(value: Encodable): EditorState { export function encodable_from_editor_state( value: EditorState, - field_type: FieldType, + presentation: Presentation, ): Encodable | undefined { - if (field_type.t === "Text") { + if (presentation.t === "Text") { return { t: "Text", c: value.text_value }; } - if (field_type.t === "Timestamp") { + if (presentation.t === "Timestamp") { // FIXME throw new Error("not yet implemented"); } - if (field_type.t === "Uuid") { + if (presentation.t === "Uuid") { try { return { t: "Uuid", c: uuid.stringify(uuid.parse(value.text_value)) }; } catch { @@ -71,6 +72,6 @@ export function encodable_from_editor_state( return undefined; } } - type _ = Assert; + type _ = Assert; throw new Error("this should be unreachable"); } diff --git a/svelte/src/encodable-editor.svelte b/svelte/src/encodable-editor.svelte index 7e2af8e..2328902 100644 --- a/svelte/src/encodable-editor.svelte +++ b/svelte/src/encodable-editor.svelte @@ -39,7 +39,7 @@ onclick={handle_type_selector_menu_button_click} type="button" > - {field_info.field.field_type.t} + {field_info.field.presentation.t}
- {assignable_field_info.field.field_type.t} + {assignable_field_info.field.presentation.t} {/each}
{/if}
- {#if field_info.field.field_type.t === "Text" || field_info.field.field_type.t === "Uuid"} + {#if field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"} - {:else if field_info.field.field_type.t === "Timestamp"} + {:else if field_info.field.presentation.t === "Timestamp"} {/if} diff --git a/svelte/src/encodable.svelte.ts b/svelte/src/encodable.svelte.ts new file mode 100644 index 0000000..c8edf64 --- /dev/null +++ b/svelte/src/encodable.svelte.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +type Assert<_T extends true> = void; + +// -------- Encodable -------- // + +export const all_encodable_tags = [ + "Text", + "Timestamp", + "Uuid", +] as const; + +// Type checking to ensure that all valid enum tags are included. +type _ = Assert< + Encodable["t"] extends (typeof all_encodable_tags)[number] ? true : false +>; + +const encodable_text_schema = z.object({ + t: z.literal("Text"), + c: z.string().nullish().transform((x) => x ?? undefined), +}); + +const encodable_timestamp_schema = z.object({ + t: z.literal("Timestamp"), + c: z.coerce.date().nullish().transform((x) => x ?? undefined), +}); + +const encodable_uuid_schema = z.object({ + t: z.literal("Uuid"), + c: z.string().nullish().transform((x) => x ?? undefined), +}); + +export const encodable_schema = z.union([ + encodable_text_schema, + encodable_timestamp_schema, + encodable_uuid_schema, +]); + +export type Encodable = z.infer; diff --git a/svelte/src/expression-editor.webc.svelte b/svelte/src/expression-editor.webc.svelte index 270c964..f450942 100644 --- a/svelte/src/expression-editor.webc.svelte +++ b/svelte/src/expression-editor.webc.svelte @@ -20,20 +20,21 @@ type EditorState, encodable_from_editor_state, } from "./editor-state.svelte"; - import { type FieldInfo, type FieldType } from "./field.svelte"; + import { type FieldInfo } from "./field.svelte"; + import { type Presentation } from "./presentation.svelte"; - const ASSIGNABLE_FIELD_TYPES: FieldType[] = [ - { t: "Text", c: {} }, + const ASSIGNABLE_PRESENTATIONS: Presentation[] = [ + { t: "Text", c: { input_mode: { t: "MultiLine", c: {} } } }, { t: "Timestamp", c: {} }, { t: "Uuid", c: {} }, ]; - const ASSIGNABLE_FIELDS: FieldInfo[] = ASSIGNABLE_FIELD_TYPES.map( - (field_type) => ({ + const ASSIGNABLE_FIELDS: FieldInfo[] = ASSIGNABLE_PRESENTATIONS.map( + (presentation) => ({ field: { id: "", label: "", name: "", - field_type, + presentation, width_px: -1, }, not_null: true, @@ -59,7 +60,7 @@ if (value?.t === "Literal" && editor_field_info) { const encodable_value = encodable_from_editor_state( editor_state, - editor_field_info.field.field_type, + editor_field_info.field.presentation, ); if (encodable_value) { value.c = encodable_value; diff --git a/svelte/src/expression.svelte.ts b/svelte/src/expression.svelte.ts index b51bd81..6c953e3 100644 --- a/svelte/src/expression.svelte.ts +++ b/svelte/src/expression.svelte.ts @@ -5,7 +5,7 @@ import cube_icon from "../assets/heroicons/20/solid/cube.svg?raw"; import cube_transparent_icon from "../assets/heroicons/20/solid/cube-transparent.svg?raw"; import hashtag_icon from "../assets/heroicons/20/solid/hashtag.svg?raw"; import variable_icon from "../assets/heroicons/20/solid/variable.svg?raw"; -import { encodable_schema } from "./field.svelte.ts"; +import { encodable_schema } from "./encodable.svelte.ts"; export const all_expression_types = [ "Comparison", diff --git a/svelte/src/field-adder.webc.svelte b/svelte/src/field-adder.webc.svelte new file mode 100644 index 0000000..afbfba7 --- /dev/null +++ b/svelte/src/field-adder.webc.svelte @@ -0,0 +1,128 @@ + + + + + + + + +
+
+
+ + col + .toLocaleLowerCase("en-US") + .includes(label_value.toLocaleLowerCase("en-US")), + )} + search_input_class="field-adder__label-input" + /> +
+
+ +
+ + +
+
+ +
+ + +
diff --git a/svelte/src/field-details.svelte b/svelte/src/field-details.svelte new file mode 100644 index 0000000..db048f8 --- /dev/null +++ b/svelte/src/field-details.svelte @@ -0,0 +1,135 @@ + + + + +

Field Details

+ + + +{#if presentation?.t === "Text"} + +{/if} diff --git a/svelte/src/field-header.svelte b/svelte/src/field-header.svelte index 3e6fb05..8d5463d 100644 --- a/svelte/src/field-header.svelte +++ b/svelte/src/field-header.svelte @@ -1,19 +1,77 @@
-
{field.field.label ?? field.field.name}
+
+ {field.field.label ?? field.field.name} +
+
+ +
+ +
+
diff --git a/svelte/src/field.svelte.ts b/svelte/src/field.svelte.ts index bd6d577..61c7d21 100644 --- a/svelte/src/field.svelte.ts +++ b/svelte/src/field.svelte.ts @@ -1,106 +1,13 @@ import { z } from "zod"; -type Assert<_T extends true> = void; - -// -------- Encodable -------- // - -export const all_encodable_types = [ - "Text", - "Timestamp", - "Uuid", -] as const; - -// Type checking to ensure that all valid enum tags are included. -type _1 = Assert< - Encodable["t"] extends (typeof all_encodable_types)[number] ? true : false ->; - -const encodable_text_schema = z.object({ - t: z.literal("Text"), - c: z.string().nullish().transform((x) => x ?? undefined), -}); - -const encodable_timestamp_schema = z.object({ - t: z.literal("Timestamp"), - c: z.coerce.date().nullish().transform((x) => x ?? undefined), -}); - -const encodable_uuid_schema = z.object({ - t: z.literal("Uuid"), - c: z.string().nullish().transform((x) => x ?? undefined), -}); - -export const encodable_schema = z.union([ - encodable_text_schema, - encodable_timestamp_schema, - encodable_uuid_schema, -]); - -export type Encodable = z.infer; - -// -------- FieldType -------- // - -export const all_field_types = [ - "Text", - "Timestamp", - "Uuid", -] as const; - -// Type checking to ensure that all valid enum tags are included. -type _2 = Assert< - FieldType["t"] extends (typeof all_field_types)[number] ? true : false ->; - -const field_type_text_schema = z.object({ - t: z.literal("Text"), - c: z.unknown(), -}); - -export type FieldTypeText = z.infer; - -const field_type_timestamp_schema = z.object({ - t: z.literal("Timestamp"), - c: z.unknown(), -}); - -export type FieldTypeTimestamp = z.infer; - -const field_type_uuid_schema = z.object({ - t: z.literal("Uuid"), - c: z.unknown(), -}); - -export type FieldTypeUuid = z.infer; - -export const field_type_schema = z.union([ - field_type_text_schema, - field_type_timestamp_schema, - field_type_uuid_schema, -]); - -export type FieldType = z.infer; - -export function get_empty_encodable_for(field_type: FieldType): Encodable { - if (field_type.t === "Timestamp") { - return { t: "Timestamp", c: undefined }; - } - if (field_type.t === "Text") { - return { t: "Text", c: undefined }; - } - if (field_type.t === "Uuid") { - return { t: "Uuid", c: undefined }; - } - type _ = Assert; - throw new Error("this should be unreachable"); -} - -// -------- Field -------- // +import { type Encodable } from "./encodable.svelte.ts"; +import { presentation_schema } from "./presentation.svelte.ts"; export const field_schema = z.object({ id: z.string(), name: z.string(), label: z.string().nullish().transform((x) => x ?? undefined), - field_type: field_type_schema, + presentation: presentation_schema, width_px: z.number(), }); diff --git a/svelte/src/presentation.svelte.ts b/svelte/src/presentation.svelte.ts new file mode 100644 index 0000000..62b7193 --- /dev/null +++ b/svelte/src/presentation.svelte.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; + +import { type Encodable } from "./encodable.svelte.ts"; + +type Assert<_T extends true> = void; + +export const all_presentation_tags = [ + "Text", + "Timestamp", + "Uuid", +] as const; + +// Type checking to ensure that all valid enum tags are included. +type _PresentationTagsAssertion = Assert< + Presentation["t"] extends (typeof all_presentation_tags)[number] ? true + : false +>; + +export const all_text_input_modes = [ + "SingleLine", + "MultiLine", +] as const; + +// Type checking to ensure that all valid enum tags are included. +type _TextInputModesAssertion = Assert< + TextInputMode["t"] extends (typeof all_text_input_modes)[number] ? true + : false +>; + +const text_input_mode_schema = z.union([ + z.object({ + t: z.literal("SingleLine"), + c: z.object({}), + }), + z.object({ + t: z.literal("MultiLine"), + c: z.object({}), + }), +]); + +export type TextInputMode = z.infer; + +const presentation_text_schema = z.object({ + t: z.literal("Text"), + c: z.object({ + input_mode: text_input_mode_schema, + }), +}); + +export type PresentationText = z.infer; + +const presentation_timestamp_schema = z.object({ + t: z.literal("Timestamp"), + c: z.unknown(), +}); + +export type PresentationTimestamp = z.infer< + typeof presentation_timestamp_schema +>; + +const presentation_uuid_schema = z.object({ + t: z.literal("Uuid"), + c: z.unknown(), +}); + +export type PresentationUuid = z.infer; + +export const presentation_schema = z.union([ + presentation_text_schema, + presentation_timestamp_schema, + presentation_uuid_schema, +]); + +export type Presentation = z.infer; + +export function get_empty_encodable_for(presentation: Presentation): Encodable { + if (presentation.t === "Timestamp") { + return { t: "Timestamp", c: undefined }; + } + if (presentation.t === "Text") { + return { t: "Text", c: undefined }; + } + if (presentation.t === "Uuid") { + return { t: "Uuid", c: undefined }; + } + type _ = Assert; + throw new Error("this should be unreachable"); +} diff --git a/svelte/src/table-viewer.webc.svelte b/svelte/src/table-viewer.webc.svelte index da7a3a1..7ed298c 100644 --- a/svelte/src/table-viewer.webc.svelte +++ b/svelte/src/table-viewer.webc.svelte @@ -1,29 +1,42 @@ - +