From 307018953750124efa9ecf1d9563abd3f4502c49 Mon Sep 17 00:00:00 2001 From: Aitor Alonso Date: Mon, 20 Apr 2026 21:58:54 +0200 Subject: [PATCH 01/21] fix: fixes 4186 Some database providers, like supabase, provides two URLs to access the database. A main URL behind pgBouncer's for load balancing, and a direct separated URL to perform migrations (running DDL operations), as does not support advisory locks or multi-statement DDL required by migrations. Before v3.1.0, adding `directUrl` along with `url` to `prisma/schema.prisma` was enought. Now, I'm making a commit to use direct url when set --- scripts/check-db.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/check-db.js b/scripts/check-db.js index 8c806cd8f..aaa128af5 100644 --- a/scripts/check-db.js +++ b/scripts/check-db.js @@ -67,7 +67,8 @@ async function checkDatabaseVersion() { async function applyMigration() { if (!process.env.SKIP_DB_MIGRATION) { - console.log(execSync('prisma migrate deploy').toString()); + const directUrl = process.env.DIRECT_DATABASE_URL || process.env.DATABASE_URL; + console.log(execSync('prisma migrate deploy', { env: { ...process.env, DATABASE_URL: directUrl } }).toString()); success('Database is up to date.'); } From cb9a94f89be50f92e4b449f0489dca55ad526395 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:09:07 +0000 Subject: [PATCH 02/21] Bump uuid from 13.0.0 to 14.0.0 Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.0 to 14.0.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0) --- updated-dependencies: - dependency-name: uuid dependency-version: 14.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 59 +++++--------------------------------------------- 2 files changed, 6 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 1d6fc31ee..8e6985cd2 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "serialize-error": "^12.0.0", "thenby": "^1.3.4", "ua-parser-js": "^2.0.9", - "uuid": "^13.0.0", + "uuid": "^14.0.0", "zod": "^4.3.6", "zustand": "^5.0.12" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 740288d7a..3f80dc4a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,8 +192,8 @@ importers: specifier: ^2.0.9 version: 2.0.9 uuid: - specifier: ^13.0.0 - version: 13.0.0 + specifier: ^14.0.0 + version: 14.0.0 zod: specifier: ^4.3.6 version: 4.3.6 @@ -313,8 +313,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - dist: {} - packages: '@ampproject/remapping@2.3.0': @@ -520,28 +518,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@2.4.10': resolution: {integrity: sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.10': resolution: {integrity: sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@2.4.10': resolution: {integrity: sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@2.4.10': resolution: {integrity: sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==} @@ -1108,105 +1102,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1409,28 +1387,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.4': resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.4': resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.4': resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.4': resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} @@ -1485,42 +1459,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -1925,79 +1893,66 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -2162,28 +2117,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.15.11': resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.15.11': resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.15.11': resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.15.11': resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} @@ -6005,8 +5956,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true uuid@8.3.2: @@ -12171,7 +12122,7 @@ snapshots: util-deprecate@1.0.2: {} - uuid@13.0.0: {} + uuid@14.0.0: {} uuid@8.3.2: {} From 1e711d79e1a64ee2cb5944370caa1a10cb0039f0 Mon Sep 17 00:00:00 2001 From: Cobal Date: Sat, 25 Apr 2026 14:30:31 +0200 Subject: [PATCH 03/21] =?UTF-8?q?=F0=9F=8C=90=20fix(i18n):=20add=20missing?= =?UTF-8?q?=20'saved'=20labels=20for=20german=20and=20swiss=20locales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/intl/messages/de-CH.json | 1 + public/intl/messages/de-DE.json | 1 + 2 files changed, 2 insertions(+) diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json index fecef5cc1..019336190 100644 --- a/public/intl/messages/de-CH.json +++ b/public/intl/messages/de-CH.json @@ -338,6 +338,7 @@ "unique-events": "Einzigartige Ereignisse", "unique-visitors": "Einzigartigi Bsuecher", "uniqueCustomers": "Einzigartigi Kunde", + "saved": "Gspeichert", "unknown": "Unbekannt", "untitled": "Unbennant", "update": "Aktualisieren", diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json index e5be06cb9..3038b4a31 100644 --- a/public/intl/messages/de-DE.json +++ b/public/intl/messages/de-DE.json @@ -338,6 +338,7 @@ "unique-events": "Einzigartige Ereignisse", "unique-visitors": "Einzigartige Besucher", "uniqueCustomers": "Einzigartige Kunden", + "saved": "Gespeichert", "unknown": "Unbekannt", "untitled": "Unbenannt", "update": "Aktualisieren", From 17a00f62a449ec9b537528aed89c72f7a1c01fb4 Mon Sep 17 00:00:00 2001 From: JLUpengjiaji Date: Wed, 29 Apr 2026 14:57:24 +0800 Subject: [PATCH 04/21] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E7=9A=84=E6=90=9C=E7=B4=A2=E6=9D=A1=E4=BB=B6=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=B9=B6=E4=BF=AE=E5=A4=8D=E6=90=9C=E7=B4=A2=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/queries/sql/getValues.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/queries/sql/getValues.ts b/src/queries/sql/getValues.ts index 8573335bf..cc5f3a489 100644 --- a/src/queries/sql/getValues.ts +++ b/src/queries/sql/getValues.ts @@ -83,10 +83,6 @@ async function clickhouseQuery(websiteId: string, column: string, filters: Query excludeDomain = `and referrer_domain != hostname and referrer_domain != ''`; } - if (search) { - searchQuery = `and positionCaseInsensitive(${column}, {search:String}) > 0`; - } - if (search) { if (decodeURIComponent(search).includes(',')) { searchQuery = `AND (${decodeURIComponent(search) From f6703d3d6d63ab9ec1d8b2560edcc399aab335bd Mon Sep 17 00:00:00 2001 From: JLUpengjiaji Date: Wed, 29 Apr 2026 15:04:08 +0800 Subject: [PATCH 05/21] =?UTF-8?q?refactor(data):=20=E9=87=8D=E6=9E=84=20JS?= =?UTF-8?q?ON=20=E6=89=81=E5=B9=B3=E5=8C=96=E5=87=BD=E6=95=B0=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 flattenJSON 重构为更简洁的递归实现 - 提取 KeyValueData 接口并导出 createKeyValue 函数 - 添加完整的单元测试覆盖所有功能 --- src/lib/__tests__/data.test.ts | 246 +++++++++++++++++++++++++++++++++ src/lib/data.ts | 55 +++----- 2 files changed, 269 insertions(+), 32 deletions(-) create mode 100644 src/lib/__tests__/data.test.ts diff --git a/src/lib/__tests__/data.test.ts b/src/lib/__tests__/data.test.ts new file mode 100644 index 000000000..61882f6c3 --- /dev/null +++ b/src/lib/__tests__/data.test.ts @@ -0,0 +1,246 @@ +import { DATA_TYPE } from '../constants'; +import { + flattenJSON, + createKeyValue, + isValidDateValue, + getDataType, + getStringValue, + objectToArray, + type KeyValueData, +} from '../data'; + +describe('isValidDateValue', () => { + test.each([ + ['2024-01-15T10:30:00Z', true], + ['2024-01-15T10:30:00.123Z', true], + ['2024-01-15T10:30:00+02:00', true], + ['not-a-date', false], + ['2024/01/15', false], + ['', false], + ])('validates datetime strings correctly (%s → %s)', (input, expected) => { + expect(isValidDateValue(input)).toBe(expected); + }); + + test('returns false for non-string values', () => { + expect(isValidDateValue(123 as any)).toBe(false); + expect(isValidDateValue(null as any)).toBe(false); + expect(isValidDateValue(undefined as any)).toBe(false); + }); +}); + +describe('getDataType', () => { + test.each([ + ['string', 'string'], + [123, 'number'], + [true, 'boolean'], + [null, 'object'], + [[], 'object'], + [{}, 'object'], + ['2024-01-15T10:30:00Z', 'date'], + ])('detects type correctly (%s → %s)', (input, expected) => { + expect(getDataType(input)).toBe(expected); + }); +}); + +describe('createKeyValue', () => { + test('handles string values', () => { + const result = createKeyValue('name', 'test'); + expect(result).toEqual({ + key: 'name', + value: 'test', + dataType: DATA_TYPE.string, + }); + }); + + test('handles number values', () => { + const result = createKeyValue('count', 42); + expect(result).toEqual({ + key: 'count', + value: 42, + dataType: DATA_TYPE.number, + }); + }); + + test('handles boolean values and converts to string', () => { + expect(createKeyValue('active', true)).toEqual({ + key: 'active', + value: 'true', + dataType: DATA_TYPE.boolean, + }); + expect(createKeyValue('active', false)).toEqual({ + key: 'active', + value: 'false', + dataType: DATA_TYPE.boolean, + }); + }); + + test('handles date strings', () => { + const dateStr = '2024-01-15T10:30:00Z'; + const result = createKeyValue('timestamp', dateStr); + expect(result).toEqual({ + key: 'timestamp', + value: dateStr, + dataType: DATA_TYPE.date, + }); + }); + + test('handles arrays and converts to JSON string', () => { + const arr = [1, 2, 3]; + const result = createKeyValue('items', arr); + expect(result).toEqual({ + key: 'items', + value: '[1,2,3]', + dataType: DATA_TYPE.array, + }); + }); + + test('handles null values', () => { + const result = createKeyValue('nullable', null); + expect(result.dataType).toBe(DATA_TYPE.array); + expect(result.value).toBe('null'); + }); +}); + +describe('getStringValue', () => { + test('formats number values with 4 decimal places', () => { + expect(getStringValue('42', DATA_TYPE.number)).toBe('42.0000'); + expect(getStringValue('3.14159', DATA_TYPE.number)).toBe('3.1416'); + }); + + test('converts date values to ISO string', () => { + const result = getStringValue('2024-01-15T10:30:00', DATA_TYPE.date); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + }); + + test('returns string values as-is for other types', () => { + expect(getStringValue('test', DATA_TYPE.string)).toBe('test'); + expect(getStringValue('true', DATA_TYPE.boolean)).toBe('true'); + }); +}); + +describe('objectToArray', () => { + test('converts object values to array', () => { + const obj = { a: 1, b: 2, c: 3 }; + const result = objectToArray(obj); + expect(result).toEqual([1, 2, 3]); + }); + + test('returns empty array for empty object', () => { + expect(objectToArray({})).toEqual([]); + }); +}); + +describe('flattenJSON', () => { + test('flattens simple object', () => { + const input = { + name: 'test', + count: 42, + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(2); + expect(result).toContainEqual(expect.objectContaining({ key: 'name', value: 'test' })); + expect(result).toContainEqual(expect.objectContaining({ key: 'count', value: 42 })); + }); + + test('flattens nested object with dot notation', () => { + const input = { + user: { + name: 'John', + age: 30, + }, + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(2); + expect(result).toContainEqual(expect.objectContaining({ key: 'user.name', value: 'John' })); + expect(result).toContainEqual(expect.objectContaining({ key: 'user.age', value: 30 })); + }); + + test('flattens deeply nested object', () => { + const input = { + level1: { + level2: { + level3: { + value: 'deep', + }, + }, + }, + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(1); + expect(result[0].key).toBe('level1.level2.level3.value'); + expect(result[0].value).toBe('deep'); + }); + + test('treats arrays as leaf values (converts to JSON string)', () => { + const input = { + tags: ['a', 'b', 'c'], + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(1); + expect(result[0].key).toBe('tags'); + expect(result[0].value).toBe('["a","b","c"]'); + expect(result[0].dataType).toBe(DATA_TYPE.array); + }); + + test('treats date strings as leaf values', () => { + const input = { + createdAt: '2024-01-15T10:30:00Z', + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(1); + expect(result[0].key).toBe('createdAt'); + expect(result[0].dataType).toBe(DATA_TYPE.date); + }); + + test('handles mixed nested and flat structure', () => { + const input = { + id: '123', + metadata: { + timestamp: '2024-01-15T10:30:00Z', + source: 'web', + details: { + referrer: 'google', + }, + }, + }; + + const result = flattenJSON(input); + const keys = result.map(r => r.key); + + expect(result).toHaveLength(4); + expect(keys).toContain('id'); + expect(keys).toContain('metadata.timestamp'); + expect(keys).toContain('metadata.source'); + expect(keys).toContain('metadata.details.referrer'); + }); + + test('returns empty array for empty object', () => { + expect(flattenJSON({})).toEqual([]); + }); + + test('converts boolean values to strings', () => { + const input = { + isActive: true, + isAdmin: false, + }; + + const result = flattenJSON(input); + + expect(result).toContainEqual( + expect.objectContaining({ key: 'isActive', value: 'true', dataType: DATA_TYPE.boolean }), + ); + expect(result).toContainEqual( + expect.objectContaining({ key: 'isAdmin', value: 'false', dataType: DATA_TYPE.boolean }), + ); + }); +}); diff --git a/src/lib/data.ts b/src/lib/data.ts index fe69edf04..07eae52a5 100644 --- a/src/lib/data.ts +++ b/src/lib/data.ts @@ -1,27 +1,26 @@ import { DATA_TYPE, DATETIME_REGEX } from './constants'; import type { DynamicDataType } from './types'; -export function flattenJSON( - eventData: Record, - keyValues: { key: string; value: any; dataType: DynamicDataType }[] = [], - parentKey = '', -): { key: string; value: any; dataType: DynamicDataType }[] { - return Object.keys(eventData).reduce( - (acc, key) => { - const value = eventData[key]; - const type = typeof eventData[key]; +export interface KeyValueData { + key: string; + value: any; + dataType: DynamicDataType; +} - // nested object - if (value && type === 'object' && !Array.isArray(value) && !isValidDateValue(value)) { - flattenJSON(value, acc.keyValues, getKeyName(key, parentKey)); - } else { - createKey(getKeyName(key, parentKey), value, acc); +export function flattenJSON(eventData: Record): KeyValueData[] { + function flatten(obj: Record, parentKey: string): KeyValueData[] { + return Object.entries(obj).flatMap(([key, value]) => { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (value && typeof value === 'object' && !Array.isArray(value) && !isValidDateValue(value)) { + return flatten(value, fullKey); } - return acc; - }, - { keyValues, parentKey }, - ).keyValues; + return [createKeyValue(fullKey, value)]; + }); + } + + return flatten(eventData, ''); } export function isValidDateValue(value: string) { @@ -50,10 +49,10 @@ export function getStringValue(value: string, dataType: number) { return value; } -function createKey(key: string, value: string, acc: { keyValues: any[]; parentKey: string }) { +export function createKeyValue(key: string, value: any): KeyValueData { const type = getDataType(value); - - let dataType = null; + let dataType: DynamicDataType; + let processedValue = value; switch (type) { case 'number': @@ -64,29 +63,21 @@ function createKey(key: string, value: string, acc: { keyValues: any[]; parentKe break; case 'boolean': dataType = DATA_TYPE.boolean; - value = value ? 'true' : 'false'; + processedValue = value ? 'true' : 'false'; break; case 'date': dataType = DATA_TYPE.date; break; case 'object': dataType = DATA_TYPE.array; - value = JSON.stringify(value); + processedValue = JSON.stringify(value); break; default: dataType = DATA_TYPE.string; break; } - acc.keyValues.push({ key, value, dataType }); -} - -function getKeyName(key: string, parentKey: string) { - if (!parentKey) { - return key; - } - - return `${parentKey}.${key}`; + return { key, value: processedValue, dataType }; } export function objectToArray(obj: object) { From 790354d3ac7baf6a3d6bb211399ff6ae016a7ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20F=C3=A9=20Santos?= Date: Thu, 30 Apr 2026 10:48:16 +0100 Subject: [PATCH 06/21] Add data-auto-pageview attribute to tracker script Allow disabling the automatic initial pageview while keeping performance tracking, click handling, and path change detection active. --- src/tracker/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index 85ad2ffea..68b7fe77b 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -37,6 +37,7 @@ const domain = config('domains') || ''; const credentials = config('fetch-credentials') || 'omit'; const perf = config('performance') === _true; + const autoPageview = config('auto-pageview') !== _false; const domains = domain.split(',').map(n => n.trim()); const host = @@ -197,7 +198,7 @@ const init = () => { if (!initialized) { initialized = true; - track(); + if (autoPageview) track(); handlePathChanges(); handleClicks(); if (perf) initPerformance(); From 8ce833c93be08367f16d51b8e1aa6f73b51c0f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20F=C3=A9=20Santos?= Date: Thu, 30 Apr 2026 11:22:30 +0100 Subject: [PATCH 07/21] fix: suppress SPA pageview tracking when auto-pageview is disabled The handlePush function was still calling track() on SPA navigations regardless of the auto-pageview setting. Gate that call behind the autoPageview flag while keeping URL state and performance flush active. --- src/tracker/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index 68b7fe77b..521a1f717 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -91,7 +91,7 @@ currentRef = currentUrl; currentUrl = normalize(new URL(url, location.href).toString()); - if (currentUrl !== currentRef) { + if (currentUrl !== currentRef && autoPageview) { setTimeout(track, delayDuration); } }; From 55f515aa4dad3e5be05586779fc0504e62ad19b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 08:42:48 +0000 Subject: [PATCH 08/21] Bump uuid from 13.0.0 to 14.0.0 Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.0 to 14.0.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0) --- updated-dependencies: - dependency-name: uuid dependency-version: 14.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 60 +++++--------------------------------------------- 2 files changed, 7 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 1d6fc31ee..8e6985cd2 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "serialize-error": "^12.0.0", "thenby": "^1.3.4", "ua-parser-js": "^2.0.9", - "uuid": "^13.0.0", + "uuid": "^14.0.0", "zod": "^4.3.6", "zustand": "^5.0.12" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 740288d7a..b1367a1e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,8 +192,8 @@ importers: specifier: ^2.0.9 version: 2.0.9 uuid: - specifier: ^13.0.0 - version: 13.0.0 + specifier: ^14.0.0 + version: 14.0.0 zod: specifier: ^4.3.6 version: 4.3.6 @@ -313,8 +313,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - dist: {} - packages: '@ampproject/remapping@2.3.0': @@ -520,28 +518,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@2.4.10': resolution: {integrity: sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.10': resolution: {integrity: sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@2.4.10': resolution: {integrity: sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@2.4.10': resolution: {integrity: sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==} @@ -1108,105 +1102,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1409,28 +1387,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.4': resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.4': resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.4': resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.4': resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} @@ -1485,42 +1459,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -1925,79 +1893,66 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -2162,28 +2117,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.15.11': resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.15.11': resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.15.11': resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.15.11': resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} @@ -6005,12 +5956,13 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -12171,7 +12123,7 @@ snapshots: util-deprecate@1.0.2: {} - uuid@13.0.0: {} + uuid@14.0.0: {} uuid@8.3.2: {} From 5fc02b6aefc08b3bae1533e1803bdc6bfa4b0d3b Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 4 May 2026 10:54:58 -0700 Subject: [PATCH 09/21] fix formatting and css for session property screen --- .../session-data/SessionPropertyChart.tsx | 16 +++++++++++++--- .../property-data/PropertyNumericChart.tsx | 17 ++++------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx b/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx index b175908df..5ae934723 100644 --- a/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx +++ b/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx @@ -74,7 +74,7 @@ export function SessionPropertyChart({ )} - + {activeTable.length === 0 ? ( @@ -97,7 +97,12 @@ export function SessionPropertyChart({ - + {row => ( @@ -118,7 +123,12 @@ export function SessionPropertyChart({ {row => formatLongNumber(row.events)} - + {row => formatShortTime(Math.abs(~~Number(row.totaltime)), ['h', 'm', 's'], ' ') } diff --git a/src/components/property-data/PropertyNumericChart.tsx b/src/components/property-data/PropertyNumericChart.tsx index ed98e23df..aad0149a7 100644 --- a/src/components/property-data/PropertyNumericChart.tsx +++ b/src/components/property-data/PropertyNumericChart.tsx @@ -48,15 +48,6 @@ export function PropertyNumericChart({ const avgRows = useMemo(() => (avgQuery.data as { t: string; y: number }[] | undefined) ?? [], [avgQuery.data]); const stats = statsQuery.data; - const formatMetricValue = useCallback( - (n: number) => - Number(n).toLocaleString(locale, { - maximumFractionDigits: 2, - minimumFractionDigits: Number.isInteger(Number(n)) ? 0 : 2, - }), - [locale], - ); - const chartData: any = useMemo(() => { if (!sumQuery.data && !avgQuery.data) return; @@ -109,10 +100,10 @@ export function PropertyNumericChart({ > - - - - + + + + Date: Mon, 4 May 2026 12:42:25 -0700 Subject: [PATCH 10/21] query optimizations on session property filters --- .../11_add_event_session_data_pivot.sql | 16 ++++++++++++++ db/clickhouse/schema.sql | 16 ++++++++++++++ src/lib/clickhouse.ts | 21 +++++++++++++++--- src/lib/prisma.ts | 22 +++++++++++++++---- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/db/clickhouse/migrations/11_add_event_session_data_pivot.sql b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql index 2d44cf0bf..388649859 100644 --- a/db/clickhouse/migrations/11_add_event_session_data_pivot.sql +++ b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql @@ -108,3 +108,19 @@ SELECT groupArrayState(data_type) FROM umami.session_data GROUP BY website_id, session_id, distinct_id; + +ALTER TABLE umami.session_data +ADD PROJECTION session_data_property_filter_projection ( + SELECT * + ORDER BY ( + website_id, + data_key, + data_type, + string_value, + number_value, + date_value, + session_id + ) +); + +ALTER TABLE umami.session_data MATERIALIZE PROJECTION session_data_property_filter_projection; diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index c5c8dce67..d4041dde9 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -90,6 +90,22 @@ ENGINE = ReplacingMergeTree ORDER BY (website_id, session_id, data_key) SETTINGS index_granularity = 8192; +ALTER TABLE umami.session_data +ADD PROJECTION session_data_property_filter_projection ( + SELECT * + ORDER BY ( + website_id, + data_key, + data_type, + string_value, + number_value, + date_value, + session_id + ) +); + +ALTER TABLE umami.session_data MATERIALIZE PROJECTION session_data_property_filter_projection; + -- stats hourly CREATE TABLE umami.website_event_stats_hourly ( diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index b6fb4f874..025e4273d 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -253,6 +253,10 @@ function getPropertyFilterQuery( const table = propertyType === 'event' ? 'event_data' : 'session_data final'; const column = propertyType === 'event' ? 'event_id' : 'session_id'; const outerColumn = propertyType === 'event' ? 'event_id' : 'website_event.session_id'; + const dateFilter = + propertyType === 'event' + ? `and created_at between {startDate:DateTime64} and {endDate:DateTime64}` + : ''; filters.forEach(({ propertyName, dataType, operator, value }, i) => { const keyParam = `pf_key_${i}`; @@ -333,15 +337,26 @@ function getPropertyFilterQuery( } } - parts.push(`and ${outerColumn} in ( - select ${column} + if (propertyType === 'session') { + parts.push(`and tuple(website_event.website_id, website_event.session_id) in ( + select website_id, session_id from ${table} where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} and data_key = {${keyParam}:String} and data_type = ${dataType} and ${condition} )`); + } else { + parts.push(`and ${outerColumn} in ( + select ${column} + from ${table} + where website_id = {websiteId:UUID} + ${dateFilter} + and data_key = {${keyParam}:String} + and data_type = ${dataType} + and ${condition} + )`); + } }); return { sql: parts.join('\n'), params }; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 1da0f81b7..95d585c1e 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -289,6 +289,8 @@ function getPropertyFilterQuery( const column = propertyType === 'event' ? 'website_event_id' : 'session_id'; const outerColumn = propertyType === 'event' ? 'website_event.event_id' : 'website_event.session_id'; + const dateFilter = + propertyType === 'event' ? `and created_at between {{startDate}} and {{endDate}}` : ''; filters.forEach(({ propertyName, dataType, operator, value }, i) => { const keyParam = `pf_key_${i}`; @@ -372,15 +374,27 @@ function getPropertyFilterQuery( } } - parts.push(`and ${outerColumn} in ( - select ${column} + if (propertyType === 'session') { + parts.push(`and exists ( + select 1 from ${table} - where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} + where website_id = website_event.website_id + and session_id = website_event.session_id and data_key = {{${keyParam}}} and data_type = ${dataType} and ${condition} )`); + } else { + parts.push(`and ${outerColumn} in ( + select ${column} + from ${table} + where website_id = {{websiteId::uuid}} + ${dateFilter} + and data_key = {{${keyParam}}} + and data_type = ${dataType} + and ${condition} + )`); + } }); return { sql: parts.join('\n'), params }; From e349a040e5256675ad2456a23031cd9d40d6e0a1 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 4 May 2026 12:48:44 -0700 Subject: [PATCH 11/21] add deduplicate_merge_projection_mode settings --- db/clickhouse/migrations/11_add_event_session_data_pivot.sql | 3 +++ db/clickhouse/schema.sql | 3 +++ 2 files changed, 6 insertions(+) diff --git a/db/clickhouse/migrations/11_add_event_session_data_pivot.sql b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql index 388649859..59a628560 100644 --- a/db/clickhouse/migrations/11_add_event_session_data_pivot.sql +++ b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql @@ -109,6 +109,9 @@ SELECT FROM umami.session_data GROUP BY website_id, session_id, distinct_id; +ALTER TABLE umami.session_data +MODIFY SETTING deduplicate_merge_projection_mode = 'drop'; + ALTER TABLE umami.session_data ADD PROJECTION session_data_property_filter_projection ( SELECT * diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index d4041dde9..f5b484716 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -90,6 +90,9 @@ ENGINE = ReplacingMergeTree ORDER BY (website_id, session_id, data_key) SETTINGS index_granularity = 8192; +ALTER TABLE umami.session_data +MODIFY SETTING deduplicate_merge_projection_mode = 'drop'; + ALTER TABLE umami.session_data ADD PROJECTION session_data_property_filter_projection ( SELECT * From 21eb6d21ecc1347ff5ee9755af07c7bf9adfea47 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 4 May 2026 14:12:08 -0700 Subject: [PATCH 12/21] remove unused session_data_pivot table --- .../11_add_event_session_data_pivot.sql | 54 ------------------- db/clickhouse/schema.sql | 35 ------------ 2 files changed, 89 deletions(-) diff --git a/db/clickhouse/migrations/11_add_event_session_data_pivot.sql b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql index 59a628560..708826786 100644 --- a/db/clickhouse/migrations/11_add_event_session_data_pivot.sql +++ b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql @@ -55,60 +55,6 @@ SELECT FROM umami.event_data GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; -CREATE TABLE IF NOT EXISTS umami.session_data_pivot -( - website_id UUID, - session_id UUID, - distinct_id String, - created_year_month UInt32, - created_at AggregateFunction(max, DateTime('UTC')), - property_keys AggregateFunction(groupArray, String), - property_values AggregateFunction(groupArray, String), - property_types AggregateFunction(groupArray, UInt32) -) -ENGINE = AggregatingMergeTree() -PARTITION BY created_year_month -ORDER BY (website_id, session_id, distinct_id) -SETTINGS index_granularity = 8192; - -CREATE MATERIALIZED VIEW IF NOT EXISTS umami.session_data_pivot_mv -TO umami.session_data_pivot -AS SELECT - website_id, - session_id, - ifNull(distinct_id, '') AS distinct_id, - toYYYYMM(max(session_data.created_at)) AS created_year_month, - maxState(session_data.created_at) AS created_at, - groupArrayState(data_key) AS property_keys, - groupArrayState(multiIf( - data_type IN (1, 3, 5), ifNull(string_value, ''), - data_type = 2, toString(ifNull(number_value, 0)), - data_type = 4, toString(ifNull(date_value, toDateTime(0))), - '' - )) AS property_values, - groupArrayState(data_type) AS property_types -FROM umami.session_data -GROUP BY website_id, session_id, distinct_id; - --- Backfill existing session data -INSERT INTO umami.session_data_pivot -SELECT - website_id, - session_id, - ifNull(distinct_id, '') AS distinct_id, - toYYYYMM(max(session_data.created_at)) AS created_year_month, - maxState(session_data.created_at) AS created_at, - groupArrayState(data_key), - groupArrayState(multiIf( - data_type IN (1, 3, 5), ifNull(string_value, ''), - data_type = 2, toString(ifNull(number_value, 0)), - data_type = 4, toString(ifNull(date_value, toDateTime(0))), - '' - )), - groupArrayState(data_type) -FROM umami.session_data -GROUP BY website_id, session_id, distinct_id; - ALTER TABLE umami.session_data MODIFY SETTING deduplicate_merge_projection_mode = 'drop'; diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index f5b484716..f37dc6e9a 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -364,38 +364,3 @@ AS SELECT FROM umami.event_data GROUP BY website_id, session_id, event_id, event_name, url_path, created_at; --- Create session_data_pivot -CREATE TABLE IF NOT EXISTS umami.session_data_pivot -( - website_id UUID, - session_id UUID, - distinct_id String, - created_year_month UInt32, - created_at AggregateFunction(max, DateTime('UTC')), - property_keys AggregateFunction(groupArray, String), - property_values AggregateFunction(groupArray, String), - property_types AggregateFunction(groupArray, UInt32) -) -ENGINE = AggregatingMergeTree() -PARTITION BY created_year_month -ORDER BY (website_id, session_id, distinct_id) -SETTINGS index_granularity = 8192; - -CREATE MATERIALIZED VIEW IF NOT EXISTS umami.session_data_pivot_mv -TO umami.session_data_pivot -AS SELECT - website_id, - session_id, - ifNull(distinct_id, '') AS distinct_id, - toYYYYMM(max(session_data.created_at)) AS created_year_month, - maxState(session_data.created_at) AS created_at, - groupArrayState(data_key) AS property_keys, - groupArrayState(multiIf( - data_type IN (1, 3, 5), ifNull(string_value, ''), - data_type = 2, toString(ifNull(number_value, 0)), - data_type = 4, toString(ifNull(date_value, toDateTime(0))), - '' - )) AS property_values, - groupArrayState(data_type) AS property_types -FROM umami.session_data -GROUP BY website_id, session_id, distinct_id; From a8c1f267756ea5f797fc9c96c58dcd7abd99aa2d Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 4 May 2026 14:26:27 -0700 Subject: [PATCH 13/21] remove duration from session property stats --- .../session-data/SessionPropertyChart.tsx | 12 +----------- src/lib/types.ts | 1 - .../sql/sessions/getSessionDataActivityStats.ts | 14 +++++--------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx b/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx index 5ae934723..048ac08d7 100644 --- a/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx +++ b/src/app/(main)/websites/[websiteId]/session-data/SessionPropertyChart.tsx @@ -3,7 +3,7 @@ import { DistributionBarChart } from '@/components/charts/DistributionBarChart'; import { Empty } from '@/components/common/Empty'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useMessages, useMobile, useSessionDataActivityStatsQuery } from '@/components/hooks'; -import { formatLongNumber, formatShortTime } from '@/lib/format'; +import { formatLongNumber } from '@/lib/format'; import type { PropertyFilter } from '@/lib/types'; import { Column, DataColumn, DataTable, Grid, Row, Text } from '@umami/react-zen'; import { useMemo } from 'react'; @@ -123,16 +123,6 @@ export function SessionPropertyChart({ {row => formatLongNumber(row.events)} - - {row => - formatShortTime(Math.abs(~~Number(row.totaltime)), ['h', 'm', 's'], ' ') - } - diff --git a/src/lib/types.ts b/src/lib/types.ts index f512c50bc..dd365aeab 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -96,7 +96,6 @@ export interface PropertyLeaderboardRow { visits: number; views: number; events: number; - totaltime: number; } export interface QueryOptions { diff --git a/src/queries/sql/sessions/getSessionDataActivityStats.ts b/src/queries/sql/sessions/getSessionDataActivityStats.ts index 3da98140b..c8bb0a277 100644 --- a/src/queries/sql/sessions/getSessionDataActivityStats.ts +++ b/src/queries/sql/sessions/getSessionDataActivityStats.ts @@ -22,7 +22,7 @@ async function relationalQuery( propertyFilters: PropertyFilter[] = [], ) { const { timezone = 'utc' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery, getTimestampDiffSQL } = prisma; + const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, @@ -65,8 +65,7 @@ async function relationalQuery( count(*) as visits, sum(activity) as activity, sum(views) as views, - sum(events) as events, - sum(${getTimestampDiffSQL('min_time', 'max_time')}) as totaltime + sum(events) as events from session_rollup group by session_id ), @@ -89,8 +88,7 @@ async function relationalQuery( count(distinct property_values.session_id) as sessions, coalesce(sum(session_stats.visits), 0) as visits, coalesce(sum(session_stats.views), 0) as views, - coalesce(sum(session_stats.events), 0) as events, - coalesce(sum(session_stats.totaltime), 0) as totaltime + coalesce(sum(session_stats.events), 0) as events from property_values join session_stats on session_stats.session_id = property_values.session_id group by property_values.value @@ -145,8 +143,7 @@ async function clickhouseQuery( count() as visits, sum(activity) as activity, sum(views) as views, - sum(events) as events, - sum(max_time - min_time) as totaltime + sum(events) as events from session_rollup group by session_id ), @@ -167,8 +164,7 @@ async function clickhouseQuery( uniq(property_values.session_id) as sessions, ifNull(sum(session_stats.visits), 0) as visits, ifNull(sum(session_stats.views), 0) as views, - ifNull(sum(session_stats.events), 0) as events, - ifNull(sum(session_stats.totaltime), 0) as totaltime + ifNull(sum(session_stats.events), 0) as events from property_values join session_stats on session_stats.session_id = property_values.session_id group by property_values.value From fbac7518fc35c608a58fe7d55efaa17bba831417 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 4 May 2026 15:43:05 -0700 Subject: [PATCH 14/21] Add MAX_PAGING_RESULTS on hooks for performance improvements. --- src/components/common/DataGrid.tsx | 3 +- src/components/common/Pager.tsx | 9 +- .../hooks/queries/useEventDataPivotQuery.ts | 2 + .../hooks/queries/useReplaysQuery.ts | 3 +- .../hooks/queries/useRevenueSessionsQuery.ts | 3 +- .../hooks/queries/useSavedReplaysQuery.ts | 1 - .../hooks/queries/useSessionDataPivotQuery.ts | 2 + .../hooks/queries/useWebsiteEventsQuery.ts | 2 + .../hooks/queries/useWebsiteSessionsQuery.ts | 3 +- src/lib/clickhouse.ts | 10 +- src/lib/constants.ts | 1 + src/lib/prisma.ts | 10 +- src/lib/request.ts | 1 + src/lib/schema.ts | 1 + src/lib/types.ts | 2 + src/queries/sql/events/getEventDataPivot.ts | 132 ++++-------------- .../sql/sessions/getSessionDataPivot.ts | 104 ++------------ 17 files changed, 74 insertions(+), 215 deletions(-) diff --git a/src/components/common/DataGrid.tsx b/src/components/common/DataGrid.tsx index 993929fd2..8d18321cf 100644 --- a/src/components/common/DataGrid.tsx +++ b/src/components/common/DataGrid.tsx @@ -41,7 +41,7 @@ export function DataGrid({ const { data, error, isLoading, isFetching } = query; const { router, updateParams, query: queryParams } = useNavigation(); const [search, setSearch] = useState(queryParams?.search || data?.search || ''); - const showPager = allowPaging && data && data.count > data.pageSize; + const showPager = allowPaging && data && (data.count > data.pageSize || data.isCapped); const { isMobile } = useMobile(); const displayMode = isMobile ? 'cards' : undefined; @@ -95,6 +95,7 @@ export function DataGrid({ page={data.page} pageSize={data.pageSize} count={data.count} + isCapped={data.isCapped} onPageChange={handlePageChange} /> diff --git a/src/components/common/Pager.tsx b/src/components/common/Pager.tsx index 128d33e24..ff22dc70e 100644 --- a/src/components/common/Pager.tsx +++ b/src/components/common/Pager.tsx @@ -6,11 +6,12 @@ export interface PagerProps { page: string | number; pageSize: string | number; count: string | number; + isCapped?: boolean; onPageChange: (nextPage: number) => void; className?: string; } -export function Pager({ page, pageSize, count, onPageChange }: PagerProps) { +export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerProps) { const { t, labels } = useMessages(); const maxPage = pageSize && count ? Math.ceil(+count / +pageSize) : 0; const lastPage = page === maxPage; @@ -28,13 +29,15 @@ export function Pager({ page, pageSize, count, onPageChange }: PagerProps) { } }; - if (maxPage === 1) { + if (maxPage === 1 && !isCapped) { return null; } + const displayCount = isCapped ? `10,000+` : (+count).toLocaleString(); + return ( - {t(labels.numberOfRecords, { x: count.toLocaleString() })} + {t(labels.numberOfRecords, { x: displayCount })} {t(labels.pageOf, { diff --git a/src/components/hooks/queries/useEventDataPivotQuery.ts b/src/components/hooks/queries/useEventDataPivotQuery.ts index e9f689ac7..a6ef902bf 100644 --- a/src/components/hooks/queries/useEventDataPivotQuery.ts +++ b/src/components/hooks/queries/useEventDataPivotQuery.ts @@ -1,3 +1,4 @@ +import { MAX_PAGING_RESULTS } from '@/lib/constants'; import { serializeEventPropertyFilters } from '@/lib/params'; import type { EventPropertyFilter, ReactQueryOptions } from '@/lib/types'; import { useApi } from '../useApi'; @@ -28,6 +29,7 @@ export function useEventDataPivotQuery( timezone, ...serializeEventPropertyFilters(eventFilters), ...params, + maxResults: MAX_PAGING_RESULTS, }), enabled: !!(websiteId && eventName), ...options, diff --git a/src/components/hooks/queries/useReplaysQuery.ts b/src/components/hooks/queries/useReplaysQuery.ts index 79c1a04bf..795512896 100644 --- a/src/components/hooks/queries/useReplaysQuery.ts +++ b/src/components/hooks/queries/useReplaysQuery.ts @@ -1,3 +1,4 @@ +import { MAX_PAGING_RESULTS } from '@/lib/constants'; import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '../useFilterParameters'; @@ -24,7 +25,7 @@ export function useReplaysQuery(websiteId: string, params?: Record { return get(`/websites/${websiteId}/replays/saved`, { ...pageParams, - pageSize: 20, }); }, }); diff --git a/src/components/hooks/queries/useSessionDataPivotQuery.ts b/src/components/hooks/queries/useSessionDataPivotQuery.ts index db5fecccf..80a6bee89 100644 --- a/src/components/hooks/queries/useSessionDataPivotQuery.ts +++ b/src/components/hooks/queries/useSessionDataPivotQuery.ts @@ -1,3 +1,4 @@ +import { MAX_PAGING_RESULTS } from '@/lib/constants'; import { serializePropertyFilters } from '@/lib/params'; import type { PropertyFilter, ReactQueryOptions } from '@/lib/types'; import { useApi } from '../useApi'; @@ -28,6 +29,7 @@ export function useSessionDataPivotQuery( propertyName, ...serializePropertyFilters(propertyFilters), ...params, + maxResults: MAX_PAGING_RESULTS, }), enabled: !!(websiteId && propertyName), ...options, diff --git a/src/components/hooks/queries/useWebsiteEventsQuery.ts b/src/components/hooks/queries/useWebsiteEventsQuery.ts index fc4dad5b1..e28e03369 100644 --- a/src/components/hooks/queries/useWebsiteEventsQuery.ts +++ b/src/components/hooks/queries/useWebsiteEventsQuery.ts @@ -1,3 +1,4 @@ +import { MAX_PAGING_RESULTS } from '@/lib/constants'; import type { ReactQueryOptions } from '@/lib/types'; import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; @@ -32,6 +33,7 @@ export function useWebsiteEventsQuery( ...filters, ...pageParams, eventType: EVENT_TYPES[params.view], + maxResults: MAX_PAGING_RESULTS, }), enabled: !!websiteId, ...options, diff --git a/src/components/hooks/queries/useWebsiteSessionsQuery.ts b/src/components/hooks/queries/useWebsiteSessionsQuery.ts index 31906be90..9de64850c 100644 --- a/src/components/hooks/queries/useWebsiteSessionsQuery.ts +++ b/src/components/hooks/queries/useWebsiteSessionsQuery.ts @@ -1,3 +1,4 @@ +import { MAX_PAGING_RESULTS } from '@/lib/constants'; import { useApi } from '../useApi'; import { useDateParameters } from '../useDateParameters'; import { useFilterParameters } from '../useFilterParameters'; @@ -27,7 +28,7 @@ export function useWebsiteSessionsQuery( ...filters, ...pageParams, ...params, - pageSize: 20, + maxResults: MAX_PAGING_RESULTS, }); }, }); diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 025e4273d..ae44c9147 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -380,13 +380,15 @@ async function pagedRawQuery( .filter(n => n) .join('\n'); - const count = await rawQuery(`select count(*) as num from (${query}) t`, queryParams).then( - res => res[0].num, - ); + const { maxResults } = filters; + const countQuery = maxResults + ? `select count(*) as num from (select 1 from (${query}) t limit ${+maxResults}) t2` + : `select count(*) as num from (${query}) t`; + const count = await rawQuery(countQuery, queryParams).then(res => res[0].num); const data = await rawQuery(`${query}${statements}`, queryParams, name); - return { data, count, page: +page, pageSize: size, orderBy, search }; + return { data, count, page: +page, pageSize: size, orderBy, search, isCapped: !!maxResults && +count >= +maxResults }; } async function rawQuery( diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f7c182b15..dc01a6d48 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -26,6 +26,7 @@ export const DEFAULT_DATE_RANGE_VALUE = '24hour'; export const DEFAULT_WEBSITE_LIMIT = 10; export const DEFAULT_RESET_DATE = '2000-01-01'; export const DEFAULT_PAGE_SIZE = 20; +export const MAX_PAGING_RESULTS = 10000; export const DEFAULT_DATE_COMPARE = 'prev'; export const DEFAULT_CURRENCY = 'USD'; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 95d585c1e..7f400d05b 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -470,13 +470,15 @@ async function pagedRawQuery( .filter(n => n) .join('\n'); - const count = await rawQuery(`select count(*) as num from (${query}) t`, queryParams).then( - res => res[0].num, - ); + const { maxResults } = filters; + const countQuery = maxResults + ? `select count(*) as num from (select 1 from (${query}) t limit ${+maxResults}) t2` + : `select count(*) as num from (${query}) t`; + const count = await rawQuery(countQuery, queryParams).then(res => Number(res[0].num)); const data = await rawQuery(`${query}${statements}`, queryParams, name); - return { data, count, page: +page, pageSize: size, orderBy }; + return { data, count, page: +page, pageSize: size, orderBy, isCapped: !!maxResults && +count >= +maxResults }; } function getSearchParameters(query: string, filters: Record[]) { diff --git a/src/lib/request.ts b/src/lib/request.ts index 9b32b6736..87c4872ad 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -174,5 +174,6 @@ export async function getQueryFilters( sortDescending: params?.sortDescending, search: params?.search, compare: params?.compare, + maxResults: params?.maxResults, }; } diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 2567c3de6..113b24cf8 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -77,6 +77,7 @@ export const searchParams = { export const pagingParams = { page: z.coerce.number().int().positive().optional(), pageSize: z.coerce.number().int().positive().optional(), + maxResults: z.coerce.number().int().positive().optional(), }; export const sortingParams = { diff --git a/src/lib/types.ts b/src/lib/types.ts index dd365aeab..6c8bb92ff 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -157,6 +157,7 @@ export interface SortParams { export interface PageParams { page?: number; pageSize?: number; + maxResults?: number; } export interface SegmentParams { @@ -172,6 +173,7 @@ export interface PageResult { orderBy?: string; sortDescending?: boolean; search?: string; + isCapped?: boolean; } export interface RealtimeData { diff --git a/src/queries/sql/events/getEventDataPivot.ts b/src/queries/sql/events/getEventDataPivot.ts index 8699d3269..210a7cdc6 100644 --- a/src/queries/sql/events/getEventDataPivot.ts +++ b/src/queries/sql/events/getEventDataPivot.ts @@ -1,5 +1,4 @@ import clickhouse from '@/lib/clickhouse'; -import { DEFAULT_PAGE_SIZE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { EventPropertyFilter, QueryFilters } from '@/lib/types'; @@ -27,10 +26,7 @@ async function relationalQuery( eventFilters: EventPropertyFilter[] = [], ) { const { timezone = 'utc' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma; - const { page = 1, pageSize } = filters; - const size = +pageSize || DEFAULT_PAGE_SIZE; - const offset = +size * (+page - 1); + const { pagedRawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, @@ -39,30 +35,10 @@ async function relationalQuery( }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); - const countResult = await rawQuery( - ` - select count(distinct website_event.event_id) as num - from website_event - join event_data on event_data.website_event_id = website_event.event_id - and event_data.website_id = {{websiteId::uuid}} - and event_data.created_at between {{startDate}} and {{endDate}} - ${cohortQuery} - ${joinSessionQuery} - where website_event.website_id = {{websiteId::uuid}} - and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_name = {{eventName}} - ${filterQuery} - ${pfSQL} - `, - { ...queryParams, eventName, ...pfParams }, - ); - - const count = countResult[0].num; - - const rows = await rawQuery( + return pagedRawQuery( ` with paged_events as ( - select website_event.event_id + select website_event.event_id, max(website_event.created_at) as sort_created_at from website_event join event_data on event_data.website_event_id = website_event.event_id and event_data.website_id = {{websiteId::uuid}} @@ -75,68 +51,43 @@ async function relationalQuery( ${filterQuery} ${pfSQL} group by website_event.event_id - order by max(website_event.created_at) desc - limit ${size} offset ${offset} ) select website_event.event_id as "eventId", website_event.session_id as "sessionId", website_event.event_name as "eventName", website_event.url_path as "urlPath", - website_event.created_at as "createdAt", - event_data.data_key as "dataKey", - coalesce( - case when event_data.data_type = 1 then event_data.string_value end, - case when event_data.data_type = 2 then cast(event_data.number_value as varchar) end, - case when event_data.data_type = 3 then event_data.string_value end, - case when event_data.data_type = 4 then ${getDateStringSQL('event_data.date_value', 'second', timezone)} end, - case when event_data.data_type = 5 then event_data.string_value end, - '' - ) as "value", - event_data.data_type as "dataType" + max(website_event.created_at) as "createdAt", + array_agg(event_data.data_key order by event_data.data_key asc) as "propertyKeys", + array_agg( + coalesce( + case when event_data.data_type = 1 then event_data.string_value end, + case when event_data.data_type = 2 then cast(event_data.number_value as varchar) end, + case when event_data.data_type = 3 then event_data.string_value end, + case when event_data.data_type = 4 then ${getDateStringSQL('event_data.date_value', 'second', timezone)} end, + case when event_data.data_type = 5 then event_data.string_value end, + '' + ) + order by event_data.data_key asc + ) as "propertyValues" from event_data join website_event on website_event.event_id = event_data.website_event_id and website_event.website_id = {{websiteId::uuid}} join paged_events on paged_events.event_id = event_data.website_event_id where event_data.website_id = {{websiteId::uuid}} and event_data.created_at between {{startDate}} and {{endDate}} - order by website_event.created_at desc + group by + website_event.event_id, + website_event.session_id, + website_event.event_name, + website_event.url_path, + paged_events.sort_created_at + order by paged_events.sort_created_at desc `, { ...queryParams, eventName, ...pfParams }, + filters, FUNCTION_NAME, ); - - // Pivot flat rows into one record per event - const eventMap = new Map< - string, - { - eventId: string; - sessionId: string; - eventName: string; - urlPath: string; - createdAt: Date; - propertyKeys: string[]; - propertyValues: string[]; - } - >(); - for (const { eventId, sessionId, eventName: name, urlPath, createdAt, dataKey, value } of rows) { - if (!eventMap.has(eventId)) { - eventMap.set(eventId, { - eventId, - sessionId, - eventName: name, - urlPath, - createdAt, - propertyKeys: [], - propertyValues: [], - }); - } - const entry = eventMap.get(eventId); - entry.propertyKeys.push(dataKey); - entry.propertyValues.push(value ?? ''); - } - - return { data: [...eventMap.values()], count, page: +page, pageSize: size }; } async function clickhouseQuery( @@ -146,10 +97,7 @@ async function clickhouseQuery( eventFilters: EventPropertyFilter[] = [], ) { const { timezone = 'UTC' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse; - const { page = 1, pageSize } = filters; - const size = +pageSize || DEFAULT_PAGE_SIZE; - const offset = +size * (+page - 1); + const { pagedRawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, @@ -158,31 +106,7 @@ async function clickhouseQuery( }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone); - const count = await rawQuery( - ` - select count() as num - from umami.event_data_pivot - any left join ( - select * - from website_event - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type = 2 - and event_name = {eventName:String}) website_event - on website_event.event_id = event_data_pivot.event_id - and website_event.session_id = event_data_pivot.session_id - and website_event.website_id = event_data_pivot.website_id - ${cohortQuery} - where event_data_pivot.website_id = {websiteId:UUID} - and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_data_pivot.event_name = {eventName:String} - ${filterQuery} - ${pfSQL} - `, - { ...queryParams, eventName, ...pfParams }, - ).then((res: any) => res[0].num); - - const data = await rawQuery( + return pagedRawQuery( ` select event_data_pivot.event_id as eventId, @@ -224,11 +148,9 @@ async function clickhouseQuery( event_data_pivot.url_path, event_data_pivot.created_at order by event_data_pivot.created_at desc - limit ${size} offset ${offset} `, { ...queryParams, eventName, ...pfParams }, + filters, FUNCTION_NAME, ); - - return { data, count, page: +page, pageSize: size }; } diff --git a/src/queries/sql/sessions/getSessionDataPivot.ts b/src/queries/sql/sessions/getSessionDataPivot.ts index ac4528123..c4e460b0e 100644 --- a/src/queries/sql/sessions/getSessionDataPivot.ts +++ b/src/queries/sql/sessions/getSessionDataPivot.ts @@ -1,5 +1,4 @@ import clickhouse from '@/lib/clickhouse'; -import { DEFAULT_PAGE_SIZE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { PageResult, PropertyFilter, QueryFilters, SessionDataPivotRow } from '@/lib/types'; @@ -25,12 +24,9 @@ async function relationalQuery( propertyName: string, filters: QueryFilters, propertyFilters: PropertyFilter[] = [], -): Promise> { +) { const { timezone = 'utc' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma; - const { page = 1, pageSize } = filters; - const size = +pageSize || DEFAULT_PAGE_SIZE; - const offset = +size * (+page - 1); + const { pagedRawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma; const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, @@ -43,47 +39,7 @@ async function relationalQuery( timezone, ); - const countResult = (await rawQuery( - ` - with filtered_sessions as ( - select distinct website_event.session_id - from website_event - ${cohortQuery} - ${joinSessionQuery} - where website_event.website_id = {{websiteId::uuid}} - and website_event.created_at between {{startDate}} and {{endDate}} - ${filterQuery} - ${pfSQL} - ), - latest_session_properties as ( - select - ranked.session_id, - ranked.data_key - from ( - select - session_data.session_id, - session_data.data_key, - row_number() over ( - partition by session_data.session_id, session_data.data_key - order by session_data.created_at desc, session_data.session_data_id desc - ) as row_num - from session_data - join filtered_sessions - on filtered_sessions.session_id = session_data.session_id - where session_data.website_id = {{websiteId::uuid}} - ) ranked - where ranked.row_num = 1 - ) - select count(*) as num - from latest_session_properties - where latest_session_properties.data_key = {{propertyName}} - `, - { ...queryParams, websiteId, propertyName, ...pfParams }, - )) as { num: number }[]; - - const count = countResult[0].num; - - const rows = (await rawQuery( + return pagedRawQuery( ` with filtered_sessions as ( select distinct website_event.session_id @@ -132,8 +88,6 @@ async function relationalQuery( latest_session_properties.created_at as sort_created_at from latest_session_properties where latest_session_properties.data_key = {{propertyName}} - order by latest_session_properties.created_at desc - limit ${size} offset ${offset} ) select latest_session_properties.session_id as "sessionId", @@ -158,10 +112,9 @@ async function relationalQuery( order by paged_sessions.sort_created_at desc `, { ...queryParams, websiteId, propertyName, ...pfParams }, + filters, FUNCTION_NAME, - )) as SessionDataPivotRow[]; - - return { data: rows, count, page: +page, pageSize: size }; + ); } async function clickhouseQuery( @@ -169,12 +122,9 @@ async function clickhouseQuery( propertyName: string, filters: QueryFilters, propertyFilters: PropertyFilter[] = [], -): Promise> { +) { const { timezone = 'UTC' } = filters; - const { rawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse; - const { page = 1, pageSize } = filters; - const size = +pageSize || DEFAULT_PAGE_SIZE; - const offset = +size * (+page - 1); + const { pagedRawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse; const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId, timezone }); const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery( @@ -183,38 +133,7 @@ async function clickhouseQuery( timezone, ); - const countResult = (await rawQuery( - ` - with filtered_sessions as ( - select distinct website_event.session_id - from website_event - ${cohortQuery} - where website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} - ${filterQuery} - ${pfSQL} - ), - latest_session_properties as ( - select - session_data.session_id as session_id, - session_data.data_key as data_key - from session_data final - join filtered_sessions - on filtered_sessions.session_id = session_data.session_id - where session_data.website_id = {websiteId:UUID} - group by - session_data.session_id, - session_data.data_key - ) - select count() as num - from latest_session_properties - where latest_session_properties.data_key = {propertyName:String} - `, - { ...queryParams, websiteId, propertyName, ...pfParams }, - )) as { num: number }[]; - const count = countResult[0].num; - - const data = (await rawQuery( + return pagedRawQuery( ` with filtered_sessions as ( select distinct website_event.session_id @@ -249,8 +168,6 @@ async function clickhouseQuery( latest_session_properties.created_at as sort_created_at from latest_session_properties where latest_session_properties.data_key = {propertyName:String} - order by latest_session_properties.created_at desc - limit ${size} offset ${offset} ) select latest_session_properties.session_id as sessionId, @@ -274,8 +191,7 @@ async function clickhouseQuery( order by paged_sessions.sort_created_at desc `, { ...queryParams, websiteId, propertyName, ...pfParams }, + filters, FUNCTION_NAME, - )) as SessionDataPivotRow[]; - - return { data, count, page: +page, pageSize: size }; + ); } From f463a4ce3f1fdf8c8ba5b4e01f41c2f9dc3e4f5a Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 5 May 2026 10:48:56 -0700 Subject: [PATCH 15/21] rename propertyName parameter --- .../[websiteId]/sessions/SessionProperties.tsx | 2 +- .../[websiteId]/session-data/properties/route.ts | 6 +++--- .../hooks/queries/useSessionDataPropertiesQuery.ts | 8 ++++---- .../sql/sessions/getSessionDataProperties.ts | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx index 56643d2d8..cfc6d4f76 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx @@ -20,7 +20,7 @@ export function SessionProperties({ websiteId }: { websiteId: string }) { const { data, isLoading, isFetching, error } = usePropertyFieldsQuery('session', websiteId); const { data: scopedData } = useSessionDataPropertiesQuery( websiteId, - propertyName ? { selectedPropertyName: propertyName, propertyFilters } : undefined, + propertyName ? { propertyName, propertyFilters } : undefined, { enabled: !!propertyName }, ); diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts index 61a5c13d9..8ac17e4b8 100644 --- a/src/app/api/websites/[websiteId]/session-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -13,7 +13,7 @@ export async function GET( const schema = z.object({ startAt: z.coerce.number().int(), endAt: z.coerce.number().int(), - selectedPropertyName: z.string().optional(), + propertyName: z.string().optional(), ...filterParams, }); @@ -29,7 +29,7 @@ export async function GET( return unauthorized(); } - const { selectedPropertyName, ...rest } = query; + const { propertyName, ...rest } = query; const filters = await getQueryFilters(rest, websiteId); const propertyFilters = parsePropertyFilters(query); @@ -37,7 +37,7 @@ export async function GET( websiteId, filters, propertyFilters, - selectedPropertyName, + propertyName, ); return json(data); diff --git a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts index fa52ae302..7bd041a0f 100644 --- a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts +++ b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts @@ -8,7 +8,7 @@ import { useFilterParameters } from '../useFilterParameters'; export function useSessionDataPropertiesQuery( websiteId: string, params?: { - selectedPropertyName?: string; + propertyName?: string; propertyFilters?: PropertyFilter[]; }, options?: ReactQueryOptions, @@ -16,14 +16,14 @@ export function useSessionDataPropertiesQuery( const { get, useQuery } = useApi(); const { startAt, endAt, unit, timezone } = useDateParameters(); const filters = useFilterParameters({ includePagination: false }); - const { selectedPropertyName, propertyFilters = [] } = params || {}; + const { propertyName, propertyFilters = [] } = params || {}; return useQuery({ queryKey: [ 'websites:session-data:properties', { websiteId, - selectedPropertyName, + propertyName, propertyFilters, startAt, endAt, @@ -38,7 +38,7 @@ export function useSessionDataPropertiesQuery( endAt, unit, timezone, - selectedPropertyName, + propertyName, ...serializePropertyFilters(propertyFilters), ...filters, }), diff --git a/src/queries/sql/sessions/getSessionDataProperties.ts b/src/queries/sql/sessions/getSessionDataProperties.ts index 446d8e465..2cae98b2c 100644 --- a/src/queries/sql/sessions/getSessionDataProperties.ts +++ b/src/queries/sql/sessions/getSessionDataProperties.ts @@ -10,7 +10,7 @@ export async function getSessionDataProperties( websiteId: string, filters: QueryFilters, propertyFilters?: PropertyFilter[], - selectedPropertyName?: string, + propertyName?: string, ] ) { return runQuery({ @@ -23,7 +23,7 @@ async function relationalQuery( websiteId: string, filters: QueryFilters, propertyFilters: PropertyFilter[] = [], - selectedPropertyName?: string, + propertyName?: string, ) { const { timezone = 'utc' } = filters; const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma; @@ -56,7 +56,7 @@ async function relationalQuery( join filtered_sessions on filtered_sessions.session_id = session_data.session_id and filtered_sessions.website_id = session_data.website_id - ${selectedPropertyName ? 'where session_data.data_key = {{selectedPropertyName}}' : ''} + ${propertyName ? 'where session_data.data_key = {{propertyName}}' : ''} ) select data_key as "propertyName", @@ -70,7 +70,7 @@ async function relationalQuery( order by 3 desc, 1 asc limit 500 `, - { ...queryParams, websiteId, selectedPropertyName, ...pfParams }, + { ...queryParams, websiteId, propertyName, ...pfParams }, FUNCTION_NAME, ); } @@ -79,7 +79,7 @@ async function clickhouseQuery( websiteId: string, filters: QueryFilters, propertyFilters: PropertyFilter[] = [], - selectedPropertyName?: string, + propertyName?: string, ): Promise<{ propertyName: string; dataType: number; total: number }[]> { const { timezone = 'UTC' } = filters; const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse; @@ -107,7 +107,7 @@ async function clickhouseQuery( join filtered_sessions on filtered_sessions.session_id = session_data.session_id and filtered_sessions.website_id = session_data.website_id - ${selectedPropertyName ? 'where session_data.data_key = {selectedPropertyName:String}' : ''} + ${propertyName ? 'where session_data.data_key = {propertyName:String}' : ''} ) select data_key as propertyName, @@ -123,7 +123,7 @@ async function clickhouseQuery( order by 3 desc, 1 asc limit 500 `, - { ...queryParams, websiteId, selectedPropertyName, ...pfParams }, + { ...queryParams, websiteId, propertyName, ...pfParams }, FUNCTION_NAME, ); } From 71893e4f89e077a72f2595472b586461b5b939ea Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 6 May 2026 09:51:28 -0700 Subject: [PATCH 16/21] fix maxheight on DialogButton --- src/components/input/DialogButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/input/DialogButton.tsx b/src/components/input/DialogButton.tsx index 6038d733b..98faf9d5d 100644 --- a/src/components/input/DialogButton.tsx +++ b/src/components/input/DialogButton.tsx @@ -42,7 +42,7 @@ export function DialogButton({ height, minWidth, minHeight, - maxHeight: 'calc(100dvh - 40px)', + maxHeight: 'min(80dvh, calc(100dvh - 40px))', overflowY: 'auto', padding: '32px', }; From a57abbe039175fe4ea2fa4366c55c1ba2d2c1a89 Mon Sep 17 00:00:00 2001 From: Stanislaw <56738034+anvme@users.noreply.github.com> Date: Thu, 7 May 2026 03:37:22 +0200 Subject: [PATCH 17/21] fix: clean up link, pixel, board rows on user/team deletion deleteUser and deleteTeam left link/pixel/board rows (and their share rows) in the database after the owner was removed. /q/ and /p/ also kept serving deleted entries because the routes did not filter deletedAt and Redis cached lookups for 24h. - deleteUser: clean up link/pixel/board + shares for the deleted user. Cloud mode: soft-delete link/pixel, hard-delete board, only userId-owned. Non-cloud: hard-delete everything matching userId or owned teamIds. - deleteTeam: same cleanup, scoped to teamId. - /q and /p route handlers: filter deletedAt: null at the call sites (not in findLink/findPixel helpers, which would null-deref the permission checks at src/permissions/link.ts and pixel.ts). - Post-transaction Redis invalidation mirrors deleteWebsite. --- src/app/(collect)/p/[slug]/route.ts | 2 ++ src/app/(collect)/q/[slug]/route.ts | 2 ++ src/queries/prisma/team.ts | 37 ++++++++++++++++++++++-- src/queries/prisma/user.ts | 44 +++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/app/(collect)/p/[slug]/route.ts b/src/app/(collect)/p/[slug]/route.ts index fd18a2834..36ba5a892 100644 --- a/src/app/(collect)/p/[slug]/route.ts +++ b/src/app/(collect)/p/[slug]/route.ts @@ -21,6 +21,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug return findPixel({ where: { slug, + deletedAt: null, }, }); }, @@ -34,6 +35,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug pixel = await findPixel({ where: { slug, + deletedAt: null, }, }); diff --git a/src/app/(collect)/q/[slug]/route.ts b/src/app/(collect)/q/[slug]/route.ts index aa9c26f3f..f585fc02e 100644 --- a/src/app/(collect)/q/[slug]/route.ts +++ b/src/app/(collect)/q/[slug]/route.ts @@ -19,6 +19,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug return findLink({ where: { slug, + deletedAt: null, }, }); }, @@ -32,6 +33,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug link = await findLink({ where: { slug, + deletedAt: null, }, }); diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts index de938df35..b24d65227 100644 --- a/src/queries/prisma/team.ts +++ b/src/queries/prisma/team.ts @@ -2,6 +2,7 @@ import { Prisma, type Team } from '@/generated/prisma/client'; import { ROLES } from '@/lib/constants'; import { uuid } from '@/lib/crypto'; import prisma from '@/lib/prisma'; +import redis from '@/lib/redis'; import type { PageResult, QueryFilters } from '@/lib/types'; import TeamFindManyArgs = Prisma.TeamFindManyArgs; @@ -144,6 +145,24 @@ export async function deleteTeam(teamId: string) { const { client, transaction } = prisma; const cloudMode = !!process.env.CLOUD_MODE; + const [links, pixels, boards] = await Promise.all([ + client.link.findMany({ where: { teamId }, select: { id: true, slug: true } }), + client.pixel.findMany({ where: { teamId }, select: { id: true, slug: true } }), + client.board.findMany({ where: { teamId }, select: { id: true } }), + ]); + const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)]; + const linkSlugs = links.map(l => l.slug); + const pixelSlugs = pixels.map(p => p.slug); + + const invalidateRedis = async () => { + if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) { + await Promise.all([ + ...linkSlugs.map(slug => redis.client.del(`link:${slug}`)), + ...pixelSlugs.map(slug => redis.client.del(`pixel:${slug}`)), + ]); + } + }; + if (cloudMode) { return transaction([ client.team.update({ @@ -154,7 +173,14 @@ export async function deleteTeam(teamId: string) { id: teamId, }, }), - ]); + client.share.deleteMany({ where: { entityId: { in: entityIds } } }), + client.link.updateMany({ data: { deletedAt: new Date() }, where: { teamId } }), + client.pixel.updateMany({ data: { deletedAt: new Date() }, where: { teamId } }), + client.board.deleteMany({ where: { teamId } }), + ]).then(async result => { + await invalidateRedis(); + return result; + }); } return transaction([ @@ -163,10 +189,17 @@ export async function deleteTeam(teamId: string) { teamId, }, }), + client.share.deleteMany({ where: { entityId: { in: entityIds } } }), + client.link.deleteMany({ where: { teamId } }), + client.pixel.deleteMany({ where: { teamId } }), + client.board.deleteMany({ where: { teamId } }), client.team.delete({ where: { id: teamId, }, }), - ]); + ]).then(async result => { + await invalidateRedis(); + return result; + }); } diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 467ea1e02..c527e6026 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -2,6 +2,7 @@ import { Prisma } from '@/generated/prisma/client'; import { ROLES } from '@/lib/constants'; import { getRandomChars } from '@/lib/generate'; import prisma from '@/lib/prisma'; +import redis from '@/lib/redis'; import type { QueryFilters, Role } from '@/lib/types'; import UserFindManyArgs = Prisma.UserFindManyArgs; @@ -126,6 +127,31 @@ export async function deleteUser(userId: string) { const teamIds = teams.map(a => a.id); + // Cloud mode keeps owned teams (and their team-owned content), so cleanup + // only covers user-direct rows. Non-cloud hard-deletes owned teams below, + // so we must also clean up team-owned content. + const ownedFilter = cloudMode + ? { userId } + : { OR: [{ userId }, { teamId: { in: teamIds } }] }; + + const [links, pixels, boards] = await Promise.all([ + client.link.findMany({ where: ownedFilter, select: { id: true, slug: true } }), + client.pixel.findMany({ where: ownedFilter, select: { id: true, slug: true } }), + client.board.findMany({ where: ownedFilter, select: { id: true } }), + ]); + const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)]; + const linkSlugs = links.map(l => l.slug); + const pixelSlugs = pixels.map(p => p.slug); + + const invalidateRedis = async () => { + if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) { + await Promise.all([ + ...linkSlugs.map(slug => redis.client.del(`link:${slug}`)), + ...pixelSlugs.map(slug => redis.client.del(`pixel:${slug}`)), + ]); + } + }; + if (cloudMode) { return transaction([ client.website.updateMany({ @@ -143,7 +169,14 @@ export async function deleteUser(userId: string) { id: userId, }, }), - ]); + client.share.deleteMany({ where: { entityId: { in: entityIds } } }), + client.link.updateMany({ data: { deletedAt: new Date() }, where: { userId } }), + client.pixel.updateMany({ data: { deletedAt: new Date() }, where: { userId } }), + client.board.deleteMany({ where: { userId } }), + ]).then(async result => { + await invalidateRedis(); + return result; + }); } return transaction([ @@ -194,6 +227,10 @@ export async function deleteUser(userId: string) { ], }, }), + client.share.deleteMany({ where: { entityId: { in: entityIds } } }), + client.link.deleteMany({ where: ownedFilter }), + client.pixel.deleteMany({ where: ownedFilter }), + client.board.deleteMany({ where: ownedFilter }), client.website.deleteMany({ where: { id: { in: websiteIds } }, }), @@ -202,5 +239,8 @@ export async function deleteUser(userId: string) { id: userId, }, }), - ]); + ]).then(async result => { + await invalidateRedis(); + return result; + }); } From 71ee000f21375ec615b468c139b431ca24433dd5 Mon Sep 17 00:00:00 2001 From: Stanislaw <56738034+anvme@users.noreply.github.com> Date: Thu, 7 May 2026 04:14:38 +0200 Subject: [PATCH 18/21] fix: avoid restamping deletedAt + skip Redis DEL for already-soft-deleted slugs Address Greptile review feedback on #4243. - Cloud-mode link.updateMany / pixel.updateMany now filter where: { ..., deletedAt: null } so a previously soft-deleted row keeps its original deletion timestamp instead of being restamped with the current time. - Pre-transaction findMany now selects deletedAt; the Redis invalidation list filters to only live slugs, avoiding harmless but wasted DEL calls for already-soft-deleted entries. Note: the share.deleteMany cleanup still uses the broad entityId list (not filtered by deletedAt) so that orphan share rows of already-soft-deleted links/pixels are still cleaned up. Filtering the prefetch itself, as Greptile's exact suggestion proposed, would skip those shares while link.deleteMany still hard-deletes the rows, leaving orphan share rows behind. Verified empirically with a 3-scenario reproduction. --- src/queries/prisma/team.ts | 26 ++++++++++++++++++++------ src/queries/prisma/user.ts | 26 ++++++++++++++++++++------ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts index b24d65227..38215335b 100644 --- a/src/queries/prisma/team.ts +++ b/src/queries/prisma/team.ts @@ -146,13 +146,20 @@ export async function deleteTeam(teamId: string) { const cloudMode = !!process.env.CLOUD_MODE; const [links, pixels, boards] = await Promise.all([ - client.link.findMany({ where: { teamId }, select: { id: true, slug: true } }), - client.pixel.findMany({ where: { teamId }, select: { id: true, slug: true } }), + client.link.findMany({ + where: { teamId }, + select: { id: true, slug: true, deletedAt: true }, + }), + client.pixel.findMany({ + where: { teamId }, + select: { id: true, slug: true, deletedAt: true }, + }), client.board.findMany({ where: { teamId }, select: { id: true } }), ]); const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)]; - const linkSlugs = links.map(l => l.slug); - const pixelSlugs = pixels.map(p => p.slug); + // Only invalidate Redis cache for slugs that are still live (not already soft-deleted). + const linkSlugs = links.filter(l => !l.deletedAt).map(l => l.slug); + const pixelSlugs = pixels.filter(p => !p.deletedAt).map(p => p.slug); const invalidateRedis = async () => { if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) { @@ -174,8 +181,15 @@ export async function deleteTeam(teamId: string) { }, }), client.share.deleteMany({ where: { entityId: { in: entityIds } } }), - client.link.updateMany({ data: { deletedAt: new Date() }, where: { teamId } }), - client.pixel.updateMany({ data: { deletedAt: new Date() }, where: { teamId } }), + // deletedAt: null avoids restamping rows that were already soft-deleted earlier. + client.link.updateMany({ + data: { deletedAt: new Date() }, + where: { teamId, deletedAt: null }, + }), + client.pixel.updateMany({ + data: { deletedAt: new Date() }, + where: { teamId, deletedAt: null }, + }), client.board.deleteMany({ where: { teamId } }), ]).then(async result => { await invalidateRedis(); diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index c527e6026..21870036c 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -135,13 +135,20 @@ export async function deleteUser(userId: string) { : { OR: [{ userId }, { teamId: { in: teamIds } }] }; const [links, pixels, boards] = await Promise.all([ - client.link.findMany({ where: ownedFilter, select: { id: true, slug: true } }), - client.pixel.findMany({ where: ownedFilter, select: { id: true, slug: true } }), + client.link.findMany({ + where: ownedFilter, + select: { id: true, slug: true, deletedAt: true }, + }), + client.pixel.findMany({ + where: ownedFilter, + select: { id: true, slug: true, deletedAt: true }, + }), client.board.findMany({ where: ownedFilter, select: { id: true } }), ]); const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)]; - const linkSlugs = links.map(l => l.slug); - const pixelSlugs = pixels.map(p => p.slug); + // Only invalidate Redis cache for slugs that are still live (not already soft-deleted). + const linkSlugs = links.filter(l => !l.deletedAt).map(l => l.slug); + const pixelSlugs = pixels.filter(p => !p.deletedAt).map(p => p.slug); const invalidateRedis = async () => { if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) { @@ -170,8 +177,15 @@ export async function deleteUser(userId: string) { }, }), client.share.deleteMany({ where: { entityId: { in: entityIds } } }), - client.link.updateMany({ data: { deletedAt: new Date() }, where: { userId } }), - client.pixel.updateMany({ data: { deletedAt: new Date() }, where: { userId } }), + // deletedAt: null avoids restamping rows that were already soft-deleted earlier. + client.link.updateMany({ + data: { deletedAt: new Date() }, + where: { userId, deletedAt: null }, + }), + client.pixel.updateMany({ + data: { deletedAt: new Date() }, + where: { userId, deletedAt: null }, + }), client.board.deleteMany({ where: { userId } }), ]).then(async result => { await invalidateRedis(); From 5ccfb6d0a52749897e8ea7ad32253d7bd0c9cd49 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 7 May 2026 10:46:03 -0700 Subject: [PATCH 19/21] implement datatable sorting on non-analytics tables. (website/board/link/pixel/team/admin) --- .../(main)/admin/teams/AdminTeamsTable.tsx | 13 ++- src/app/(main)/admin/users/UsersTable.tsx | 14 ++- .../admin/websites/AdminWebsitesTable.tsx | 11 ++- src/app/(main)/boards/BoardsTable.tsx | 16 +++- src/app/(main)/links/LinksTable.tsx | 13 ++- src/app/(main)/pixels/PixelsTable.tsx | 10 +- src/app/(main)/teams/TeamsTable.tsx | 3 +- .../teams/[teamId]/TeamWebsitesTable.tsx | 5 +- src/app/(main)/websites/WebsitesTable.tsx | 5 +- src/app/api/admin/teams/route.ts | 3 +- src/app/api/admin/users/route.ts | 3 +- src/app/api/admin/websites/route.ts | 3 +- src/app/api/boards/route.ts | 3 +- src/app/api/links/route.ts | 3 +- src/app/api/me/teams/route.ts | 3 +- src/app/api/me/websites/route.ts | 3 +- src/app/api/pixels/route.ts | 3 +- src/app/api/teams/[teamId]/boards/route.ts | 3 +- src/app/api/teams/[teamId]/links/route.ts | 3 +- src/app/api/teams/[teamId]/pixels/route.ts | 3 +- src/app/api/teams/[teamId]/websites/route.ts | 3 +- src/app/api/teams/route.ts | 3 +- src/app/api/users/[userId]/teams/route.ts | 3 +- src/app/api/users/[userId]/websites/route.ts | 3 +- src/app/api/websites/route.ts | 3 +- src/components/common/DataGrid.tsx | 2 +- src/components/common/Pager.tsx | 65 ++++++++----- src/components/common/SortableLabel.tsx | 94 +++++++++++++++++++ .../hooks/queries/useUserTeamsQuery.ts | 9 +- src/components/hooks/usePageParameters.ts | 6 +- src/components/hooks/usePagedQuery.ts | 6 +- src/lib/schema.ts | 10 ++ src/lib/sort.ts | 27 ++++++ src/queries/prisma/board.ts | 8 +- src/queries/prisma/link.ts | 8 +- src/queries/prisma/pixel.ts | 8 +- src/queries/prisma/team.ts | 8 +- src/queries/prisma/user.ts | 15 +-- src/queries/prisma/website.ts | 18 ++-- 39 files changed, 322 insertions(+), 100 deletions(-) create mode 100644 src/components/common/SortableLabel.tsx create mode 100644 src/lib/sort.ts diff --git a/src/app/(main)/admin/teams/AdminTeamsTable.tsx b/src/app/(main)/admin/teams/AdminTeamsTable.tsx index 8213b6d04..1f7cca5be 100644 --- a/src/app/(main)/admin/teams/AdminTeamsTable.tsx +++ b/src/app/(main)/admin/teams/AdminTeamsTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, Dialog, Icon, MenuItem, Modal, Row, Text } from import Link from '@/components/common/Link'; import { useState } from 'react'; import { DateDistance } from '@/components/common/DateDistance'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { Edit, Trash } from '@/components/icons'; import { MenuButton } from '@/components/input/MenuButton'; @@ -21,7 +22,11 @@ export function AdminTeamsTable({ return ( <> - + } + width="1fr" + > {(row: any) => {row.name}} @@ -41,7 +46,11 @@ export function AdminTeamsTable({ ); }} - + } + width="160px" + > {(row: any) => } {showActions && ( diff --git a/src/app/(main)/admin/users/UsersTable.tsx b/src/app/(main)/admin/users/UsersTable.tsx index fafe0e139..299cf3da3 100644 --- a/src/app/(main)/admin/users/UsersTable.tsx +++ b/src/app/(main)/admin/users/UsersTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, Icon, MenuItem, Modal, Row, Text } from '@umami/ import Link from '@/components/common/Link'; import { useState } from 'react'; import { DateDistance } from '@/components/common/DateDistance'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { Edit, Trash } from '@/components/icons'; import { MenuButton } from '@/components/input/MenuButton'; @@ -22,10 +23,14 @@ export function UsersTable({ return ( <> - + } + width="2fr" + > {(row: any) => {row.username}} - + }> {(row: any) => t(labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown) } @@ -33,7 +38,10 @@ export function UsersTable({ {(row: any) => row._count.websites} - + } + > {(row: any) => } {showActions && ( diff --git a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx index 751dc7583..edf048120 100644 --- a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx +++ b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx @@ -3,6 +3,7 @@ import Link from '@/components/common/Link'; import { useState } from 'react'; import { WebsiteDeleteForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm'; import { DateDistance } from '@/components/common/DateDistance'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { Edit, Trash, Users } from '@/components/icons'; import { MenuButton } from '@/components/input/MenuButton'; @@ -14,14 +15,14 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) { return ( <> - + }> {(row: any) => ( {row.name} )} - + }> {(row: any) => {row.domain}} @@ -45,7 +46,11 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) { ); }} - + } + width="180px" + > {(row: any) => } diff --git a/src/app/(main)/boards/BoardsTable.tsx b/src/app/(main)/boards/BoardsTable.tsx index b21cbee00..0465012cc 100644 --- a/src/app/(main)/boards/BoardsTable.tsx +++ b/src/app/(main)/boards/BoardsTable.tsx @@ -1,6 +1,7 @@ import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen'; import Link from '@/components/common/Link'; import { DateDistance } from '@/components/common/DateDistance'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation } from '@/components/hooks'; import { BoardDeleteButton } from './BoardDeleteButton'; import { BoardDesignButton } from './BoardDesignButton'; @@ -12,16 +13,23 @@ export function BoardsTable(props: DataTableProps) { return ( - + }> {({ id, name }: any) => { return {name}; }} - - + } + /> + }> {({ type }: any) => type ? type.charAt(0).toUpperCase() + type.slice(1) : ''} - + } + width="200px" + > {(row: any) => } diff --git a/src/app/(main)/links/LinksTable.tsx b/src/app/(main)/links/LinksTable.tsx index bf9b5ef7f..1758272e9 100644 --- a/src/app/(main)/links/LinksTable.tsx +++ b/src/app/(main)/links/LinksTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-ze import Link from '@/components/common/Link'; import { DateDistance } from '@/components/common/DateDistance'; import { ExternalLink } from '@/components/common/ExternalLink'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation, useSlug } from '@/components/hooks'; import { LinkDeleteButton } from './LinkDeleteButton'; import { LinkEditButton } from './LinkEditButton'; @@ -17,23 +18,27 @@ export function LinksTable({ showActions, ...props }: LinksTableProps) { return ( - + }> {({ id, name }: any) => { return {name}; }} - + }> {({ slug }: any) => { const url = getSlugUrl(slug); return {url}; }} - + }> {({ url }: any) => { return {url}; }} - + } + width="200px" + > {(row: any) => } {showActions && ( diff --git a/src/app/(main)/pixels/PixelsTable.tsx b/src/app/(main)/pixels/PixelsTable.tsx index e9bc76ad8..362bffa78 100644 --- a/src/app/(main)/pixels/PixelsTable.tsx +++ b/src/app/(main)/pixels/PixelsTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-ze import Link from '@/components/common/Link'; import { DateDistance } from '@/components/common/DateDistance'; import { ExternalLink } from '@/components/common/ExternalLink'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation, useSlug } from '@/components/hooks'; import { PixelDeleteButton } from './PixelDeleteButton'; import { PixelEditButton } from './PixelEditButton'; @@ -17,12 +18,12 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) { return ( - + }> {({ id, name }: any) => { return {name}; }} - + }> {({ slug }: any) => { const url = getSlugUrl(slug); return ( @@ -32,7 +33,10 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) { ); }} - + } + > {(row: any) => } {showActions && ( diff --git a/src/app/(main)/teams/TeamsTable.tsx b/src/app/(main)/teams/TeamsTable.tsx index 66c7355ac..77515b7b2 100644 --- a/src/app/(main)/teams/TeamsTable.tsx +++ b/src/app/(main)/teams/TeamsTable.tsx @@ -1,5 +1,6 @@ import { DataColumn, DataTable, type DataTableProps } from '@umami/react-zen'; import type { ReactNode } from 'react'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { ROLES } from '@/lib/constants'; @@ -12,7 +13,7 @@ export function TeamsTable({ renderLink, ...props }: TeamsTableProps) { return ( - + }> {renderLink} diff --git a/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx b/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx index 86d846bb2..e57a10775 100644 --- a/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx +++ b/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, Row } from '@umami/react-zen'; import Link from '@/components/common/Link'; import { TeamMemberEditButton } from '@/app/(main)/teams/[teamId]/TeamMemberEditButton'; import { TeamMemberRemoveButton } from '@/app/(main)/teams/[teamId]/TeamMemberRemoveButton'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { ROLES } from '@/lib/constants'; @@ -18,10 +19,10 @@ export function TeamWebsitesTable({ return ( - + }> {(row: any) => {row.name}} - + } /> {(row: any) => row?.createUser?.username} diff --git a/src/app/(main)/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx index 714e1c66c..bfa13e4ae 100644 --- a/src/app/(main)/websites/WebsitesTable.tsx +++ b/src/app/(main)/websites/WebsitesTable.tsx @@ -1,6 +1,7 @@ import { DataColumn, DataTable, type DataTableProps, Icon } from '@umami/react-zen'; import type { ReactNode } from 'react'; import { LinkButton } from '@/components/common/LinkButton'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation } from '@/components/hooks'; import { SquarePen } from '@/components/icons'; @@ -17,10 +18,10 @@ export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTab return ( - + }> {renderLink} - + } /> {showActions && ( {(row: any) => { diff --git a/src/app/api/admin/teams/route.ts b/src/app/api/admin/teams/route.ts index ceb16ab11..35c3f5363 100644 --- a/src/app/api/admin/teams/route.ts +++ b/src/app/api/admin/teams/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewAllTeams } from '@/permissions'; import { getTeams } from '@/queries/prisma/team'; @@ -9,6 +9,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 2e5226157..0bf94d511 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewUsers } from '@/permissions'; import { getUsers } from '@/queries/prisma/user'; @@ -9,6 +9,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/admin/websites/route.ts b/src/app/api/admin/websites/route.ts index 09b2ef98d..0150e21fd 100644 --- a/src/app/api/admin/websites/route.ts +++ b/src/app/api/admin/websites/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { ROLES } from '@/lib/constants'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewAllWebsites } from '@/permissions'; import { getWebsites } from '@/queries/prisma/website'; @@ -10,6 +10,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/boards/route.ts b/src/app/api/boards/route.ts index bb0710370..655486182 100644 --- a/src/app/api/boards/route.ts +++ b/src/app/api/boards/route.ts @@ -3,7 +3,7 @@ import { BOARD_TYPES, normalizeBoardType } from '@/lib/boards'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createBoard, getUserBoards } from '@/queries/prisma'; @@ -11,6 +11,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/links/route.ts b/src/app/api/links/route.ts index a639888bd..7c9172546 100644 --- a/src/app/api/links/route.ts +++ b/src/app/api/links/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createLink, getUserLinks } from '@/queries/prisma'; @@ -10,6 +10,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/me/teams/route.ts b/src/app/api/me/teams/route.ts index 555bf3003..12c6de43f 100644 --- a/src/app/api/me/teams/route.ts +++ b/src/app/api/me/teams/route.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json } from '@/lib/response'; -import { pagingParams } from '@/lib/schema'; +import { pagingParams, sortingParams } from '@/lib/schema'; import { getUserTeams } from '@/queries/prisma'; export async function GET(request: Request) { const schema = z.object({ ...pagingParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/me/websites/route.ts b/src/app/api/me/websites/route.ts index 0880f84e1..809c6e502 100644 --- a/src/app/api/me/websites/route.ts +++ b/src/app/api/me/websites/route.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json } from '@/lib/response'; -import { pagingParams } from '@/lib/schema'; +import { pagingParams, sortingParams } from '@/lib/schema'; import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma'; export async function GET(request: Request) { const schema = z.object({ ...pagingParams, + ...sortingParams, includeTeams: z.string().optional(), }); diff --git a/src/app/api/pixels/route.ts b/src/app/api/pixels/route.ts index 8baae4f3e..453ede247 100644 --- a/src/app/api/pixels/route.ts +++ b/src/app/api/pixels/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createPixel, getUserPixels } from '@/queries/prisma'; @@ -10,6 +10,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/[teamId]/boards/route.ts b/src/app/api/teams/[teamId]/boards/route.ts index 2e60b77e2..b4e645e49 100644 --- a/src/app/api/teams/[teamId]/boards/route.ts +++ b/src/app/api/teams/[teamId]/boards/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewTeam } from '@/permissions'; import { getTeamBoards } from '@/queries/prisma'; @@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { teamId } = await params; const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/[teamId]/links/route.ts b/src/app/api/teams/[teamId]/links/route.ts index 41e139b33..4794902a1 100644 --- a/src/app/api/teams/[teamId]/links/route.ts +++ b/src/app/api/teams/[teamId]/links/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewTeam } from '@/permissions'; import { getTeamLinks } from '@/queries/prisma'; @@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { teamId } = await params; const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/[teamId]/pixels/route.ts b/src/app/api/teams/[teamId]/pixels/route.ts index daac2040d..2ce204f7b 100644 --- a/src/app/api/teams/[teamId]/pixels/route.ts +++ b/src/app/api/teams/[teamId]/pixels/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewTeam } from '@/permissions'; import { getTeamPixels } from '@/queries/prisma'; @@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { teamId } = await params; const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/[teamId]/websites/route.ts b/src/app/api/teams/[teamId]/websites/route.ts index 05c6d8045..5d010799b 100644 --- a/src/app/api/teams/[teamId]/websites/route.ts +++ b/src/app/api/teams/[teamId]/websites/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewTeam } from '@/permissions'; import { getTeamWebsites } from '@/queries/prisma'; @@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { teamId } = await params; const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts index 2e2424a2f..6c79be7fc 100644 --- a/src/app/api/teams/route.ts +++ b/src/app/api/teams/route.ts @@ -5,13 +5,14 @@ import { fetchAccount } from '@/lib/load'; import redis from '@/lib/redis'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams } from '@/lib/schema'; +import { pagingParams, sortingParams } from '@/lib/schema'; import { canCreateTeam } from '@/permissions'; import { createTeam, getUserTeams } from '@/queries/prisma'; export async function GET(request: Request) { const schema = z.object({ ...pagingParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts index 7a834a3f1..ff8b88290 100644 --- a/src/app/api/users/[userId]/teams/route.ts +++ b/src/app/api/users/[userId]/teams/route.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams } from '@/lib/schema'; +import { pagingParams, sortingParams } from '@/lib/schema'; import { getUserTeams } from '@/queries/prisma'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ ...pagingParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts index 4587354fb..c706dae9e 100644 --- a/src/app/api/users/[userId]/websites/route.ts +++ b/src/app/api/users/[userId]/websites/route.ts @@ -1,13 +1,14 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma/website'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, includeTeams: z.string().optional(), }); diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index 288ddfe30..fd4553fb8 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto'; import { fetchAccount } from '@/lib/load'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createShare, createWebsite, getWebsiteCount } from '@/queries/prisma'; import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma/website'; @@ -15,6 +15,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, includeTeams: z.string().optional(), }); diff --git a/src/components/common/DataGrid.tsx b/src/components/common/DataGrid.tsx index 8d18321cf..9a601a129 100644 --- a/src/components/common/DataGrid.tsx +++ b/src/components/common/DataGrid.tsx @@ -41,7 +41,7 @@ export function DataGrid({ const { data, error, isLoading, isFetching } = query; const { router, updateParams, query: queryParams } = useNavigation(); const [search, setSearch] = useState(queryParams?.search || data?.search || ''); - const showPager = allowPaging && data && (data.count > data.pageSize || data.isCapped); + const showPager = allowPaging && data && data.count > 0; const { isMobile } = useMobile(); const displayMode = isMobile ? 'cards' : undefined; diff --git a/src/components/common/Pager.tsx b/src/components/common/Pager.tsx index ff22dc70e..f3d72fb47 100644 --- a/src/components/common/Pager.tsx +++ b/src/components/common/Pager.tsx @@ -11,11 +11,18 @@ export interface PagerProps { className?: string; } -export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerProps) { +export function Pager({ + page, + pageSize, + count, + isCapped, + onPageChange, +}: PagerProps) { const { t, labels } = useMessages(); const maxPage = pageSize && count ? Math.ceil(+count / +pageSize) : 0; const lastPage = page === maxPage; const firstPage = page === 1; + const showNavigation = maxPage > 1 || isCapped; if (count === 0 || !maxPage) { return null; @@ -29,34 +36,40 @@ export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerPr } }; - if (maxPage === 1 && !isCapped) { - return null; - } - const displayCount = isCapped ? `10,000+` : (+count).toLocaleString(); return ( - - {t(labels.numberOfRecords, { x: displayCount })} - - - {t(labels.pageOf, { - current: page.toLocaleString(), - total: maxPage.toLocaleString(), - })} - - - - - + + {t(labels.numberOfRecords, { x: displayCount })} + + {showNavigation && ( + <> + + {t(labels.pageOf, { + current: page.toLocaleString(), + total: maxPage.toLocaleString(), + })} + + + + + + + )} ); diff --git a/src/components/common/SortableLabel.tsx b/src/components/common/SortableLabel.tsx new file mode 100644 index 000000000..c45550c8c --- /dev/null +++ b/src/components/common/SortableLabel.tsx @@ -0,0 +1,94 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { Icon } from '@umami/react-zen'; +import { ChevronDown, ChevronUp } from '@/components/icons'; +import { useNavigation } from '@/components/hooks'; + +type SortDirection = 'asc' | 'desc'; + +export interface SortableLabelProps { + label: ReactNode; + sortKey: string; + defaultDirection?: SortDirection; +} + +export function SortableLabel({ + label, + sortKey, + defaultDirection = 'asc', +}: SortableLabelProps) { + const { router, query, updateParams } = useNavigation(); + const isActive = query.orderBy === sortKey; + const isDescending = query.sortDescending === 'true'; + const direction = isActive ? (isDescending ? 'desc' : 'asc') : undefined; + const activeColor = 'var(--text-primary)'; + + const getNextDirection = (): SortDirection => { + if (!isActive) { + return defaultDirection; + } + + return direction === 'desc' ? 'asc' : 'desc'; + }; + + const handleSort = () => { + const nextDirection = getNextDirection(); + + router.push( + updateParams({ + orderBy: sortKey, + sortDescending: nextDirection === 'desc' ? 'true' : undefined, + page: 1, + }), + ); + }; + + return ( + + ); +} diff --git a/src/components/hooks/queries/useUserTeamsQuery.ts b/src/components/hooks/queries/useUserTeamsQuery.ts index 82f65496b..825e8def3 100644 --- a/src/components/hooks/queries/useUserTeamsQuery.ts +++ b/src/components/hooks/queries/useUserTeamsQuery.ts @@ -1,14 +1,15 @@ import { useApi } from '../useApi'; import { useModified } from '../useModified'; +import { usePagedQuery } from '../usePagedQuery'; export function useUserTeamsQuery(userId: string) { - const { get, useQuery } = useApi(); + const { get } = useApi(); const { modified } = useModified(`teams`); - return useQuery({ + return usePagedQuery({ queryKey: ['teams', { userId, modified }], - queryFn: () => { - return get(`/users/${userId}/teams`); + queryFn: params => { + return get(`/users/${userId}/teams`, params); }, enabled: !!userId, }); diff --git a/src/components/hooks/usePageParameters.ts b/src/components/hooks/usePageParameters.ts index 42cf3911c..0644e6900 100644 --- a/src/components/hooks/usePageParameters.ts +++ b/src/components/hooks/usePageParameters.ts @@ -3,7 +3,7 @@ import { useNavigation } from './useNavigation'; export function usePageParameters() { const { - query: { page, pageSize, search }, + query: { page, pageSize, search, orderBy, sortDescending }, } = useNavigation(); return useMemo(() => { @@ -11,6 +11,8 @@ export function usePageParameters() { page, pageSize, search, + orderBy, + sortDescending, }; - }, [page, pageSize, search]); + }, [orderBy, page, pageSize, search, sortDescending]); } diff --git a/src/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts index c818de643..73a1ead99 100644 --- a/src/components/hooks/usePagedQuery.ts +++ b/src/components/hooks/usePagedQuery.ts @@ -15,13 +15,13 @@ export function usePagedQuery({ queryFn: (params?: object) => Promise> | PageResult; }): UseQueryResult, TError> { const { - query: { page, search }, + query: { page, search, orderBy, sortDescending }, } = useNavigation(); const { useQuery } = useApi(); return useQuery, TError>({ - queryKey: [...queryKey, page, search] as const, - queryFn: () => queryFn({ page, search }), + queryKey: [...queryKey, page, search, orderBy, sortDescending] as const, + queryFn: () => queryFn({ page, search, orderBy, sortDescending }), ...options, }); } diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 113b24cf8..71f8bb22b 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -82,6 +82,16 @@ export const pagingParams = { export const sortingParams = { orderBy: z.string().optional(), + sortDescending: z + .enum(['true', 'false']) + .optional() + .transform(value => { + if (value === undefined) { + return undefined; + } + + return value === 'true'; + }), }; export const userRoleParam = z.enum(['admin', 'user', 'view-only']); diff --git a/src/lib/sort.ts b/src/lib/sort.ts new file mode 100644 index 000000000..881c2371a --- /dev/null +++ b/src/lib/sort.ts @@ -0,0 +1,27 @@ +import type { QueryFilters } from './types'; + +export function sanitizeSortFilters( + filters: QueryFilters = {}, + allowedFields: T, + defaults: Partial> = {}, +): QueryFilters { + const { orderBy, sortDescending, ...rest } = filters; + const fallbackOrderBy = defaults.orderBy; + const fallbackSortDescending = defaults.sortDescending; + const isAllowed = orderBy ? allowedFields.includes(orderBy as T[number]) : false; + + return { + ...rest, + ...(isAllowed + ? { + orderBy, + sortDescending, + } + : { + ...(fallbackOrderBy && { orderBy: fallbackOrderBy }), + ...(fallbackSortDescending !== undefined && { + sortDescending: fallbackSortDescending, + }), + }), + }; +} diff --git a/src/queries/prisma/board.ts b/src/queries/prisma/board.ts index 862ec36dd..d433850d7 100644 --- a/src/queries/prisma/board.ts +++ b/src/queries/prisma/board.ts @@ -1,8 +1,11 @@ import type { Prisma } from '@/generated/prisma/client'; import { BOARD_TYPES } from '@/lib/boards'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters } from '@/lib/types'; +const BOARD_SORT_FIELDS = ['name', 'description', 'type', 'createdAt'] as const; + export async function findBoard(criteria: Prisma.BoardFindUniqueArgs) { return prisma.client.board.findUnique(criteria); } @@ -16,7 +19,8 @@ export async function getBoard(boardId: string) { } export async function getBoards(criteria: Prisma.BoardFindManyArgs, filters: QueryFilters = {}) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, BOARD_SORT_FIELDS); + const { search } = sortFilters; const { getSearchParameters, pagedQuery } = prisma; const where: Prisma.BoardWhereInput = { @@ -24,7 +28,7 @@ export async function getBoards(criteria: Prisma.BoardFindManyArgs, filters: Que ...getSearchParameters(search, [{ name: 'contains' }, { description: 'contains' }]), }; - return pagedQuery('board', { ...criteria, where }, filters); + return pagedQuery('board', { ...criteria, where }, sortFilters); } export async function getUserBoards(userId: string, filters?: QueryFilters) { diff --git a/src/queries/prisma/link.ts b/src/queries/prisma/link.ts index 9b971dec4..c59adc025 100644 --- a/src/queries/prisma/link.ts +++ b/src/queries/prisma/link.ts @@ -1,7 +1,10 @@ import type { Prisma } from '@/generated/prisma/client'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters } from '@/lib/types'; +const LINK_SORT_FIELDS = ['name', 'slug', 'url', 'createdAt'] as const; + export async function findLink(criteria: Prisma.LinkFindUniqueArgs) { return prisma.client.link.findUnique(criteria); } @@ -15,7 +18,8 @@ export async function getLink(linkId: string) { } export async function getLinks(criteria: Prisma.LinkFindManyArgs, filters: QueryFilters = {}) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, LINK_SORT_FIELDS); + const { search } = sortFilters; const { getSearchParameters, pagedQuery } = prisma; const where: Prisma.LinkWhereInput = { @@ -27,7 +31,7 @@ export async function getLinks(criteria: Prisma.LinkFindManyArgs, filters: Query ]), }; - return pagedQuery('link', { ...criteria, where }, filters); + return pagedQuery('link', { ...criteria, where }, sortFilters); } export async function getUserLinks(userId: string, filters?: QueryFilters) { diff --git a/src/queries/prisma/pixel.ts b/src/queries/prisma/pixel.ts index 4c9e132d5..2ea9c6f28 100644 --- a/src/queries/prisma/pixel.ts +++ b/src/queries/prisma/pixel.ts @@ -1,7 +1,10 @@ import type { Prisma } from '@/generated/prisma/client'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters } from '@/lib/types'; +const PIXEL_SORT_FIELDS = ['name', 'slug', 'createdAt'] as const; + export async function findPixel(criteria: Prisma.PixelFindUniqueArgs) { return prisma.client.pixel.findUnique(criteria); } @@ -15,14 +18,15 @@ export async function getPixel(pixelId: string) { } export async function getPixels(criteria: Prisma.PixelFindManyArgs, filters: QueryFilters = {}) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, PIXEL_SORT_FIELDS); + const { search } = sortFilters; const where: Prisma.PixelWhereInput = { ...criteria.where, ...prisma.getSearchParameters(search, [{ name: 'contains' }, { slug: 'contains' }]), }; - return prisma.pagedQuery('pixel', { ...criteria, where }, filters); + return prisma.pagedQuery('pixel', { ...criteria, where }, sortFilters); } export async function getUserPixels(userId: string, filters?: QueryFilters) { diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts index de938df35..7315921e1 100644 --- a/src/queries/prisma/team.ts +++ b/src/queries/prisma/team.ts @@ -2,10 +2,13 @@ import { Prisma, type Team } from '@/generated/prisma/client'; import { ROLES } from '@/lib/constants'; import { uuid } from '@/lib/crypto'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { PageResult, QueryFilters } from '@/lib/types'; import TeamFindManyArgs = Prisma.TeamFindManyArgs; +const TEAM_SORT_FIELDS = ['name', 'createdAt'] as const; + export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise { return prisma.client.team.findUnique(criteria); } @@ -29,7 +32,8 @@ export async function getTeams( filters: QueryFilters, ): Promise> { const { getSearchParameters } = prisma; - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, TEAM_SORT_FIELDS); + const { search } = sortFilters; const where: Prisma.TeamWhereInput = { ...criteria.where, @@ -42,7 +46,7 @@ export async function getTeams( ...criteria, where, }, - filters, + sortFilters, ); } diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 467ea1e02..0d9b1c4cf 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -2,10 +2,13 @@ import { Prisma } from '@/generated/prisma/client'; import { ROLES } from '@/lib/constants'; import { getRandomChars } from '@/lib/generate'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters, Role } from '@/lib/types'; import UserFindManyArgs = Prisma.UserFindManyArgs; +const USER_SORT_FIELDS = ['username', 'role', 'createdAt'] as const; + export interface GetUserOptions { includePassword?: boolean; showDeleted?: boolean; @@ -46,7 +49,11 @@ export async function getUserByUsername(username: string, options: GetUserOption } export async function getUsers(criteria: UserFindManyArgs, filters: QueryFilters = {}) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, USER_SORT_FIELDS, { + orderBy: 'createdAt', + sortDescending: true, + }); + const { search } = sortFilters; const where: Prisma.UserWhereInput = { ...criteria.where, @@ -60,11 +67,7 @@ export async function getUsers(criteria: UserFindManyArgs, filters: QueryFilters ...criteria, where, }, - { - orderBy: 'createdAt', - sortDescending: true, - ...filters, - }, + sortFilters, ); } diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index ae7ea6c96..254e27e15 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -2,8 +2,11 @@ import type { Prisma, Website } from '@/generated/prisma/client'; import { ROLES } from '@/lib/constants'; import prisma from '@/lib/prisma'; import redis from '@/lib/redis'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters } from '@/lib/types'; +const WEBSITE_SORT_FIELDS = ['name', 'domain', 'createdAt'] as const; + export async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs) { return prisma.client.website.findUnique(criteria); } @@ -23,7 +26,8 @@ export async function getWebsite(websiteId: string) { } export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: QueryFilters) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS); + const { search } = sortFilters; const { getSearchParameters, pagedQuery } = prisma; const where: Prisma.WebsiteWhereInput = { @@ -37,7 +41,7 @@ export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: deletedAt: null, }; - const websites = await pagedQuery('website', { ...criteria, where }, filters); + const websites = await pagedQuery('website', { ...criteria, where }, sortFilters); return attachShareIdToWebsites(websites); } @@ -62,10 +66,7 @@ export async function getAllUserWebsitesIncludingTeamAccess(userId: string, filt ], }, }, - { - orderBy: 'name', - ...filters, - }, + sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS, { orderBy: 'name' }), ); } @@ -84,10 +85,7 @@ export async function getUserWebsites(userId: string, filters?: QueryFilters) { }, }, }, - { - orderBy: 'name', - ...filters, - }, + sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS, { orderBy: 'name' }), ); } From 9b099408c2099ad5a8048e021df387aa78dd08ae Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 7 May 2026 13:24:45 -0700 Subject: [PATCH 20/21] add share page options enable filtering and enforce theme --- public/intl/messages/ar-SA.json | 1 + public/intl/messages/be-BY.json | 1 + public/intl/messages/bg-BG.json | 1 + public/intl/messages/bn-BD.json | 1 + public/intl/messages/bs-BA.json | 1 + public/intl/messages/ca-ES.json | 1 + public/intl/messages/cs-CZ.json | 1 + public/intl/messages/da-DK.json | 1 + public/intl/messages/de-CH.json | 1 + public/intl/messages/de-DE.json | 1 + public/intl/messages/el-GR.json | 1 + public/intl/messages/en-GB.json | 1 + public/intl/messages/en-US.json | 1 + public/intl/messages/es-ES.json | 1 + public/intl/messages/fa-IR.json | 1 + public/intl/messages/fi-FI.json | 1 + public/intl/messages/fo-FO.json | 1 + public/intl/messages/fr-FR.json | 1 + public/intl/messages/ga-ES.json | 1 + public/intl/messages/he-IL.json | 1 + public/intl/messages/hi-IN.json | 1 + public/intl/messages/hr-HR.json | 1 + public/intl/messages/hu-HU.json | 1 + public/intl/messages/id-ID.json | 1 + public/intl/messages/it-IT.json | 1 + public/intl/messages/ja-JP.json | 1 + public/intl/messages/km-KH.json | 1 + public/intl/messages/ko-KR.json | 1 + public/intl/messages/lt-LT.json | 1 + public/intl/messages/mn-MN.json | 1 + public/intl/messages/ms-MY.json | 1 + public/intl/messages/my-MM.json | 1 + public/intl/messages/nb-NO.json | 1 + public/intl/messages/nl-NL.json | 1 + public/intl/messages/pl-PL.json | 1 + public/intl/messages/pt-BR.json | 1 + public/intl/messages/pt-PT.json | 1 + public/intl/messages/ro-RO.json | 1 + public/intl/messages/ru-RU.json | 1 + public/intl/messages/si-LK.json | 1 + public/intl/messages/sk-SK.json | 1 + public/intl/messages/sl-SI.json | 1 + public/intl/messages/sv-SE.json | 1 + public/intl/messages/ta-IN.json | 1 + public/intl/messages/th-TH.json | 1 + public/intl/messages/tr-TR.json | 1 + public/intl/messages/uk-UA.json | 1 + public/intl/messages/ur-PK.json | 1 + public/intl/messages/uz-UZ.json | 1 + public/intl/messages/vi-VN.json | 1 + public/intl/messages/zh-CN.json | 1 + public/intl/messages/zh-TW.json | 1 + .../boards/[boardId]/BoardShareCreateForm.tsx | 68 ++++++++++++----- .../boards/[boardId]/BoardSharesTable.tsx | 4 +- .../(main)/links/[linkId]/LinkControls.tsx | 9 ++- .../(main)/pixels/[pixelId]/PixelControls.tsx | 9 ++- .../settings/preferences/ThemeSetting.tsx | 19 +---- src/app/(main)/websites/WebsitesTable.tsx | 8 ++ .../websites/[websiteId]/WebsiteControls.tsx | 11 ++- .../[websiteId]/settings/ShareEditForm.tsx | 31 +++++++- src/app/api/boards/[boardId]/shares/route.ts | 8 +- src/app/api/links/[linkId]/shares/route.ts | 8 +- src/app/api/pixels/[pixelId]/shares/route.ts | 8 +- src/app/share/ShareProvider.tsx | 4 +- src/app/share/[slug]/[[...path]]/ShareNav.tsx | 14 +++- .../share/[slug]/[[...path]]/SharePage.tsx | 15 ++-- src/components/common/DataGrid.tsx | 72 +++++++++--------- src/components/hooks/useFilterParameters.ts | 23 +++--- src/components/hooks/useFilters.ts | 47 ++++++------ src/components/input/ThemeModeSelector.tsx | 36 +++++++++ src/components/messages.ts | 1 + .../share/SimpleShareCreateForm.tsx | 75 ++++++++++++++----- src/components/share/SimpleShareEditForm.tsx | 71 ++++++++++++------ src/lib/share.ts | 20 +++++ src/lib/types.ts | 9 +++ 75 files changed, 450 insertions(+), 172 deletions(-) create mode 100644 src/components/input/ThemeModeSelector.tsx create mode 100644 src/lib/share.ts diff --git a/public/intl/messages/ar-SA.json b/public/intl/messages/ar-SA.json index dfae6b16d..3d242fa3a 100644 --- a/public/intl/messages/ar-SA.json +++ b/public/intl/messages/ar-SA.json @@ -124,6 +124,7 @@ "filter-combined": "مُجمّعة", "filter-raw": "خام", "filters": "التصفيات", + "filters-enabled": "الفلاتر مفعّلة", "first-click": "النقرة الأولى", "first-seen": "أول ظهور", "funnel": "قمع", diff --git a/public/intl/messages/be-BY.json b/public/intl/messages/be-BY.json index 4c1d35461..23ee71517 100644 --- a/public/intl/messages/be-BY.json +++ b/public/intl/messages/be-BY.json @@ -124,6 +124,7 @@ "filter-combined": "Камбініраваны", "filter-raw": "Сырыя", "filters": "Фільтры", + "filters-enabled": "Фільтры ўключаны", "first-click": "Першы клік", "first-seen": "Першы раз убачана", "funnel": "Варонка", diff --git a/public/intl/messages/bg-BG.json b/public/intl/messages/bg-BG.json index fa58a6878..171f576e4 100644 --- a/public/intl/messages/bg-BG.json +++ b/public/intl/messages/bg-BG.json @@ -124,6 +124,7 @@ "filter-combined": "Комбиниран", "filter-raw": "Суров", "filters": "Филтри", + "filters-enabled": "Филтрите са активирани", "first-click": "Първо кликване", "first-seen": "Първо видяно", "funnel": "Фуния", diff --git a/public/intl/messages/bn-BD.json b/public/intl/messages/bn-BD.json index 85c33ea21..9c083a8f5 100644 --- a/public/intl/messages/bn-BD.json +++ b/public/intl/messages/bn-BD.json @@ -124,6 +124,7 @@ "filter-combined": "সম্মিলিত", "filter-raw": "অপরিশোধিত", "filters": "ফিল্টারসমূহ", + "filters-enabled": "ফিল্টার সক্রিয়", "first-click": "প্রথম ক্লিক", "first-seen": "প্রথম দেখা", "funnel": "ফানেল", diff --git a/public/intl/messages/bs-BA.json b/public/intl/messages/bs-BA.json index a11f610bd..931956515 100644 --- a/public/intl/messages/bs-BA.json +++ b/public/intl/messages/bs-BA.json @@ -125,6 +125,7 @@ "filter-combined": "Kombinovano", "filter-raw": "Sirovo", "filters": "Filtri", + "filters-enabled": "Filteri omogućeni", "first-click": "Prvi klik", "first-seen": "Prvi put viđeno", "funnel": "Lijevak", diff --git a/public/intl/messages/ca-ES.json b/public/intl/messages/ca-ES.json index 0a804c0e1..5c7b62b47 100644 --- a/public/intl/messages/ca-ES.json +++ b/public/intl/messages/ca-ES.json @@ -125,6 +125,7 @@ "filter-combined": "Combinat", "filter-raw": "En cru", "filters": "Filtres", + "filters-enabled": "Filtres activats", "first-click": "Primer clic", "first-seen": "Vist per primer cop", "funnel": "Embut", diff --git a/public/intl/messages/cs-CZ.json b/public/intl/messages/cs-CZ.json index 764dfd2ac..e3789ff28 100644 --- a/public/intl/messages/cs-CZ.json +++ b/public/intl/messages/cs-CZ.json @@ -125,6 +125,7 @@ "filter-combined": "Kombinace", "filter-raw": "Nezpracované", "filters": "Filtry", + "filters-enabled": "Filtry povoleny", "first-click": "První kliknutí", "first-seen": "Poprvé viděno", "funnel": "Trychtýř", diff --git a/public/intl/messages/da-DK.json b/public/intl/messages/da-DK.json index 2f005c7f6..8d3ed3cda 100644 --- a/public/intl/messages/da-DK.json +++ b/public/intl/messages/da-DK.json @@ -125,6 +125,7 @@ "filter-combined": "Kombineret", "filter-raw": "Rå", "filters": "Filtre", + "filters-enabled": "Filtre aktiveret", "first-click": "Første klik", "first-seen": "Først set", "funnel": "Tragt", diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json index 50fedf16c..71cce4823 100644 --- a/public/intl/messages/de-CH.json +++ b/public/intl/messages/de-CH.json @@ -125,6 +125,7 @@ "filter-combined": "Kombiniert", "filter-raw": "Rohdate", "filters": "Filter", + "filters-enabled": "Filter aktiviert", "first-click": "Erste Klick", "first-seen": "Erstmal gse", "funnel": "Tunnel", diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json index cb422375a..907d3768c 100644 --- a/public/intl/messages/de-DE.json +++ b/public/intl/messages/de-DE.json @@ -125,6 +125,7 @@ "filter-combined": "Kombiniert", "filter-raw": "Rohdaten", "filters": "Filter", + "filters-enabled": "Filter aktiviert", "first-click": "Erster Klick", "first-seen": "Erstmalig gesehen", "funnel": "Trichter", diff --git a/public/intl/messages/el-GR.json b/public/intl/messages/el-GR.json index 199becdc4..64a004dd9 100644 --- a/public/intl/messages/el-GR.json +++ b/public/intl/messages/el-GR.json @@ -124,6 +124,7 @@ "filter-combined": "Σε συνδυασμό", "filter-raw": "Ακατέργαστο", "filters": "Φίλτρα", + "filters-enabled": "Φίλτρα ενεργοποιημένα", "first-click": "Πρώτο κλικ", "first-seen": "Πρώτη εμφάνιση", "funnel": "Χωνί", diff --git a/public/intl/messages/en-GB.json b/public/intl/messages/en-GB.json index 9de60575c..243b6e9a5 100644 --- a/public/intl/messages/en-GB.json +++ b/public/intl/messages/en-GB.json @@ -125,6 +125,7 @@ "filter-combined": "Combined", "filter-raw": "Raw", "filters": "Filters", + "filters-enabled": "Filters enabled", "first-click": "First click", "first-seen": "First seen", "funnel": "Funnel", diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 6614c6500..45328163f 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -125,6 +125,7 @@ "filter-combined": "Combined", "filter-raw": "Raw", "filters": "Filters", + "filters-enabled": "Filters enabled", "property-filter": "Property Filter", "activity-by-property": "Activity by property", "first-click": "First click", diff --git a/public/intl/messages/es-ES.json b/public/intl/messages/es-ES.json index 31861c0b9..33dbcea29 100644 --- a/public/intl/messages/es-ES.json +++ b/public/intl/messages/es-ES.json @@ -125,6 +125,7 @@ "filter-combined": "Combinado", "filter-raw": "En crudo", "filters": "Filtros", + "filters-enabled": "Filtros activados", "first-click": "Primer clic", "first-seen": "Primera vez visto", "funnel": "Embudo", diff --git a/public/intl/messages/fa-IR.json b/public/intl/messages/fa-IR.json index cd7352ac0..5ef288bfe 100644 --- a/public/intl/messages/fa-IR.json +++ b/public/intl/messages/fa-IR.json @@ -124,6 +124,7 @@ "filter-combined": "ترکیب شده", "filter-raw": "خام", "filters": "فیلترها", + "filters-enabled": "فیلترها فعال", "first-click": "اولین کلیک", "first-seen": "اولین بار دیده شده", "funnel": "فانل", diff --git a/public/intl/messages/fi-FI.json b/public/intl/messages/fi-FI.json index 049906bf3..2fa0135e8 100644 --- a/public/intl/messages/fi-FI.json +++ b/public/intl/messages/fi-FI.json @@ -125,6 +125,7 @@ "filter-combined": "Yhdistetty", "filter-raw": "Käsittelemätön", "filters": "Suodattimet", + "filters-enabled": "Suodattimet käytössä", "first-click": "Ensimmäinen klikkaus", "first-seen": "Ensimmäinen havainto", "funnel": "Suppilo", diff --git a/public/intl/messages/fo-FO.json b/public/intl/messages/fo-FO.json index e01a37717..758148dee 100644 --- a/public/intl/messages/fo-FO.json +++ b/public/intl/messages/fo-FO.json @@ -125,6 +125,7 @@ "filter-combined": "Samansett", "filter-raw": "Óviðgjørt", "filters": "Síur", + "filters-enabled": "Síur virknaðar", "first-click": "Fyrsta trýst", "first-seen": "Fyrst sæddur", "funnel": "Traktari", diff --git a/public/intl/messages/fr-FR.json b/public/intl/messages/fr-FR.json index 982370683..a02786858 100644 --- a/public/intl/messages/fr-FR.json +++ b/public/intl/messages/fr-FR.json @@ -126,6 +126,7 @@ "filter-combined": "Combiné", "filter-raw": "Brut", "filters": "Filtres", + "filters-enabled": "Filtres activés", "first-click": "Premier clic", "first-seen": "Vu pour la première fois", "funnel": "Entonnoir", diff --git a/public/intl/messages/ga-ES.json b/public/intl/messages/ga-ES.json index a9e180864..0ac9bdf82 100644 --- a/public/intl/messages/ga-ES.json +++ b/public/intl/messages/ga-ES.json @@ -125,6 +125,7 @@ "filter-combined": "Combinado", "filter-raw": "Crú", "filters": "Filtros", + "filters-enabled": "Filtros activados", "first-click": "Primeiro clic", "first-seen": "Primeira visita", "funnel": "Funil", diff --git a/public/intl/messages/he-IL.json b/public/intl/messages/he-IL.json index f5ed72c2f..dda365f0a 100644 --- a/public/intl/messages/he-IL.json +++ b/public/intl/messages/he-IL.json @@ -124,6 +124,7 @@ "filter-combined": "משותף", "filter-raw": "גולמי", "filters": "מסננים", + "filters-enabled": "מסננים מופעלים", "first-click": "קליק ראשון", "first-seen": "נראה לראשונה", "funnel": "משפך", diff --git a/public/intl/messages/hi-IN.json b/public/intl/messages/hi-IN.json index a1b7d396f..d2123d382 100644 --- a/public/intl/messages/hi-IN.json +++ b/public/intl/messages/hi-IN.json @@ -124,6 +124,7 @@ "filter-combined": "संयुक्त", "filter-raw": "रॉ", "filters": "फ़िल्टर", + "filters-enabled": "फ़िल्टर सक्षम", "first-click": "पहला क्लिक", "first-seen": "पहली बार देखा गया", "funnel": "फनल", diff --git a/public/intl/messages/hr-HR.json b/public/intl/messages/hr-HR.json index 963b86a50..a5c022162 100644 --- a/public/intl/messages/hr-HR.json +++ b/public/intl/messages/hr-HR.json @@ -125,6 +125,7 @@ "filter-combined": "Kombinirano", "filter-raw": "Neobrađeno", "filters": "Filteri", + "filters-enabled": "Filtri omogućeni", "first-click": "Prvi klik", "first-seen": "Prvi put viđeno", "funnel": "Lijevak", diff --git a/public/intl/messages/hu-HU.json b/public/intl/messages/hu-HU.json index aea98d3b2..d72587e01 100644 --- a/public/intl/messages/hu-HU.json +++ b/public/intl/messages/hu-HU.json @@ -125,6 +125,7 @@ "filter-combined": "Összevont", "filter-raw": "Nyers", "filters": "Szűrők", + "filters-enabled": "Szűrők engedélyezve", "first-click": "Első kattintás", "first-seen": "Első megtekintés", "funnel": "Tölcsér", diff --git a/public/intl/messages/id-ID.json b/public/intl/messages/id-ID.json index 1bc6b433c..64e68bbbe 100644 --- a/public/intl/messages/id-ID.json +++ b/public/intl/messages/id-ID.json @@ -125,6 +125,7 @@ "filter-combined": "Gabungan", "filter-raw": "Mentah", "filters": "Filter", + "filters-enabled": "Filter diaktifkan", "first-click": "Klik pertama", "first-seen": "Pertama kali dilihat", "funnel": "Corong", diff --git a/public/intl/messages/it-IT.json b/public/intl/messages/it-IT.json index a4589faa7..0ad631dcd 100644 --- a/public/intl/messages/it-IT.json +++ b/public/intl/messages/it-IT.json @@ -125,6 +125,7 @@ "filter-combined": "Aggregati", "filter-raw": "Grezzo", "filters": "Filtri", + "filters-enabled": "Filtri abilitati", "first-click": "Primo clic", "first-seen": "Prima visualizzazione", "funnel": "Funnel", diff --git a/public/intl/messages/ja-JP.json b/public/intl/messages/ja-JP.json index bcf304f8e..243cf1abe 100644 --- a/public/intl/messages/ja-JP.json +++ b/public/intl/messages/ja-JP.json @@ -124,6 +124,7 @@ "filter-combined": "結合", "filter-raw": "RAW", "filters": "フィルター", + "filters-enabled": "フィルター有効", "first-click": "最初のクリック", "first-seen": "初回ログイン", "funnel": "ファネル", diff --git a/public/intl/messages/km-KH.json b/public/intl/messages/km-KH.json index 9fb936623..210a411ae 100644 --- a/public/intl/messages/km-KH.json +++ b/public/intl/messages/km-KH.json @@ -124,6 +124,7 @@ "filter-combined": "រួមបញ្ចូលគ្នា", "filter-raw": "ដើម", "filters": "ចម្រោះ", + "filters-enabled": "ការច្រោះបានបើក", "first-click": "ចុចដំបូង", "first-seen": "ឃើញដំបូង", "funnel": "ផ្លូវបង្ហាញ", diff --git a/public/intl/messages/ko-KR.json b/public/intl/messages/ko-KR.json index 579b9b625..9aaffeb82 100644 --- a/public/intl/messages/ko-KR.json +++ b/public/intl/messages/ko-KR.json @@ -124,6 +124,7 @@ "filter-combined": "합쳐 보기", "filter-raw": "전체 보기", "filters": "필터", + "filters-enabled": "필터 활성화됨", "first-click": "첫 클릭", "first-seen": "첫 접속", "funnel": "퍼널", diff --git a/public/intl/messages/lt-LT.json b/public/intl/messages/lt-LT.json index c0b2aaa0c..cdb4c8191 100644 --- a/public/intl/messages/lt-LT.json +++ b/public/intl/messages/lt-LT.json @@ -125,6 +125,7 @@ "filter-combined": "Kombinuoti", "filter-raw": "Neapdoroti", "filters": "Filtrai", + "filters-enabled": "Filtrai įjungti", "first-click": "Pirmas paspaudimas", "first-seen": "Pirmą kartą matyta", "funnel": "Piltuvas", diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index a9841a278..a58e96947 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -124,6 +124,7 @@ "filter-combined": "Нэгтгэсэн", "filter-raw": "Түүхий", "filters": "Шүүлтүүр", + "filters-enabled": "Шүүлтүүр идэвхжүүлэгдсэн", "first-click": "Эхний даралт", "first-seen": "Анх харсан", "funnel": "Цутгал", diff --git a/public/intl/messages/ms-MY.json b/public/intl/messages/ms-MY.json index d34510f54..56ed4f7ac 100644 --- a/public/intl/messages/ms-MY.json +++ b/public/intl/messages/ms-MY.json @@ -125,6 +125,7 @@ "filter-combined": "Digabungkan", "filter-raw": "Mentah", "filters": "Tapis", + "filters-enabled": "Penapis diaktifkan", "first-click": "Klik pertama", "first-seen": "Pertama dilihat", "funnel": "Corong", diff --git a/public/intl/messages/my-MM.json b/public/intl/messages/my-MM.json index 74bea7fb6..b8fc9b7b7 100644 --- a/public/intl/messages/my-MM.json +++ b/public/intl/messages/my-MM.json @@ -124,6 +124,7 @@ "filter-combined": "ပေါင်းစပ်ပြီး", "filter-raw": "အရှိအတိုင်း", "filters": "Filter များ", + "filters-enabled": "စစ်ထုတ်မှုများ ဖွင့်ထားသည်", "first-click": "ပထမဆုံးနှိပ်ချက်", "first-seen": "ပထမဆုံးတွေ့ရှိချိန်", "funnel": "ဖန်နယ်", diff --git a/public/intl/messages/nb-NO.json b/public/intl/messages/nb-NO.json index 699500727..279336971 100644 --- a/public/intl/messages/nb-NO.json +++ b/public/intl/messages/nb-NO.json @@ -125,6 +125,7 @@ "filter-combined": "Kombinert", "filter-raw": "Rå", "filters": "Filter", + "filters-enabled": "Filtre aktivert", "first-click": "Første klikk", "first-seen": "Først sett", "funnel": "Trakt", diff --git a/public/intl/messages/nl-NL.json b/public/intl/messages/nl-NL.json index 0749ac70e..446f84ff4 100644 --- a/public/intl/messages/nl-NL.json +++ b/public/intl/messages/nl-NL.json @@ -125,6 +125,7 @@ "filter-combined": "Gecombineerd", "filter-raw": "Ruw", "filters": "Filters", + "filters-enabled": "Filters ingeschakeld", "first-click": "Eerste klik", "first-seen": "Eerst gezien", "funnel": "Trechter", diff --git a/public/intl/messages/pl-PL.json b/public/intl/messages/pl-PL.json index ff3051a38..aff7aa1da 100644 --- a/public/intl/messages/pl-PL.json +++ b/public/intl/messages/pl-PL.json @@ -125,6 +125,7 @@ "filter-combined": "Połączone", "filter-raw": "Surowe dane", "filters": "Filtry", + "filters-enabled": "Filtry włączone", "first-click": "Pierwsze kliknięcie", "first-seen": "Pierwsza wizyta", "funnel": "Lejek", diff --git a/public/intl/messages/pt-BR.json b/public/intl/messages/pt-BR.json index 9ee595584..698c190fd 100644 --- a/public/intl/messages/pt-BR.json +++ b/public/intl/messages/pt-BR.json @@ -125,6 +125,7 @@ "filter-combined": "Combinado", "filter-raw": "Bruto", "filters": "Filtros", + "filters-enabled": "Filtros ativados", "first-click": "Primeiro clique", "first-seen": "Visto pela primeira vez", "funnel": "Funil", diff --git a/public/intl/messages/pt-PT.json b/public/intl/messages/pt-PT.json index 625b60d01..7e24883cf 100644 --- a/public/intl/messages/pt-PT.json +++ b/public/intl/messages/pt-PT.json @@ -125,6 +125,7 @@ "filter-combined": "Combinado", "filter-raw": "Dados brutos", "filters": "Filtros", + "filters-enabled": "Filtros ativados", "first-click": "Primeiro clique", "first-seen": "Primeira visualização", "funnel": "Funil", diff --git a/public/intl/messages/ro-RO.json b/public/intl/messages/ro-RO.json index b9f010d09..84ee2bc69 100644 --- a/public/intl/messages/ro-RO.json +++ b/public/intl/messages/ro-RO.json @@ -125,6 +125,7 @@ "filter-combined": "Combinat", "filter-raw": "Brut", "filters": "Filtre", + "filters-enabled": "Filtre activate", "first-click": "Primul click", "first-seen": "Văzut pentru prima dată", "funnel": "Parcursul utilizatorului", diff --git a/public/intl/messages/ru-RU.json b/public/intl/messages/ru-RU.json index 5bf41593c..7348fc648 100644 --- a/public/intl/messages/ru-RU.json +++ b/public/intl/messages/ru-RU.json @@ -124,6 +124,7 @@ "filter-combined": "Объединенные", "filter-raw": "Сырые данные", "filters": "Фильтры", + "filters-enabled": "Фильтры включены", "first-click": "Первый клик", "first-seen": "Первый вход", "funnel": "Воронка", diff --git a/public/intl/messages/si-LK.json b/public/intl/messages/si-LK.json index 570687e4b..78a8aa3e1 100644 --- a/public/intl/messages/si-LK.json +++ b/public/intl/messages/si-LK.json @@ -124,6 +124,7 @@ "filter-combined": "ඒකාබද්ධ", "filter-raw": "අමු", "filters": "පෙරහන්", + "filters-enabled": "පෙරහන් සක්‍රීය කර ඇත", "first-click": "පළමු ක්ලික්", "first-seen": "මුලින් දුටු", "funnel": "පුනීලය", diff --git a/public/intl/messages/sk-SK.json b/public/intl/messages/sk-SK.json index d55ea416d..1f429dfa2 100644 --- a/public/intl/messages/sk-SK.json +++ b/public/intl/messages/sk-SK.json @@ -125,6 +125,7 @@ "filter-combined": "Kombinácie", "filter-raw": "Nezpracované", "filters": "Filtre", + "filters-enabled": "Filtre povolené", "first-click": "Prvé kliknutie", "first-seen": "Prvýkrát videné", "funnel": "Lievik", diff --git a/public/intl/messages/sl-SI.json b/public/intl/messages/sl-SI.json index 7e4573297..f8fc8407f 100644 --- a/public/intl/messages/sl-SI.json +++ b/public/intl/messages/sl-SI.json @@ -125,6 +125,7 @@ "filter-combined": "Skupaj", "filter-raw": "Neobdelano", "filters": "Filtri", + "filters-enabled": "Filtri omogočeni", "first-click": "Prvi klik", "first-seen": "Prvič viden", "funnel": "Prodajni lijak", diff --git a/public/intl/messages/sv-SE.json b/public/intl/messages/sv-SE.json index cc175b334..b600465ff 100644 --- a/public/intl/messages/sv-SE.json +++ b/public/intl/messages/sv-SE.json @@ -125,6 +125,7 @@ "filter-combined": "Kombinerade", "filter-raw": "Rådata", "filters": "Filter", + "filters-enabled": "Filter aktiverade", "first-click": "Första klicket", "first-seen": "Först sedd", "funnel": "Tratt", diff --git a/public/intl/messages/ta-IN.json b/public/intl/messages/ta-IN.json index 5092f810d..c605796c7 100644 --- a/public/intl/messages/ta-IN.json +++ b/public/intl/messages/ta-IN.json @@ -124,6 +124,7 @@ "filter-combined": "ஒருங்கிணைந்த", "filter-raw": "மூல", "filters": "வடிகட்டிகள்", + "filters-enabled": "வடிகட்டிகள் இயக்கப்பட்டன", "first-click": "முதல் கிளிக்", "first-seen": "முதலில் பார்த்தது", "funnel": "புனல்", diff --git a/public/intl/messages/th-TH.json b/public/intl/messages/th-TH.json index 275152e4b..ce4734ff8 100644 --- a/public/intl/messages/th-TH.json +++ b/public/intl/messages/th-TH.json @@ -124,6 +124,7 @@ "filter-combined": "ข้อมูลรวม", "filter-raw": "ข้อมูลดิบ", "filters": "ตัวกรอง", + "filters-enabled": "เปิดใช้งานตัวกรอง", "first-click": "คลิกแรก", "first-seen": "เห็นครั้งแรก", "funnel": "ช่องทางขาย", diff --git a/public/intl/messages/tr-TR.json b/public/intl/messages/tr-TR.json index 7c915c393..0725081cb 100644 --- a/public/intl/messages/tr-TR.json +++ b/public/intl/messages/tr-TR.json @@ -125,6 +125,7 @@ "filter-combined": "Birleşik filtre", "filter-raw": "Ham filtre", "filters": "Filtreler", + "filters-enabled": "Filtreler etkin", "first-click": "İlk tıklama", "first-seen": "İlk görülme", "funnel": "Huni", diff --git a/public/intl/messages/uk-UA.json b/public/intl/messages/uk-UA.json index 9d298a3f3..e91c3a8be 100644 --- a/public/intl/messages/uk-UA.json +++ b/public/intl/messages/uk-UA.json @@ -124,6 +124,7 @@ "filter-combined": "Об'єднані", "filter-raw": "Сирі дані", "filters": "Фільтри", + "filters-enabled": "Фільтри увімкнено", "first-click": "Перший клік", "first-seen": "Перший візит", "funnel": "Воронка", diff --git a/public/intl/messages/ur-PK.json b/public/intl/messages/ur-PK.json index 016af6ee0..3e59d20a9 100644 --- a/public/intl/messages/ur-PK.json +++ b/public/intl/messages/ur-PK.json @@ -124,6 +124,7 @@ "filter-combined": "مشترکہ", "filter-raw": "خام", "filters": "فلٹرز", + "filters-enabled": "فلٹرز فعال", "first-click": "پہلا کلک", "first-seen": "پہلی بار دیکھا گیا", "funnel": "فنل", diff --git a/public/intl/messages/uz-UZ.json b/public/intl/messages/uz-UZ.json index 7d61b3301..9b54f139c 100644 --- a/public/intl/messages/uz-UZ.json +++ b/public/intl/messages/uz-UZ.json @@ -125,6 +125,7 @@ "filter-combined": "Birlashtirilgan", "filter-raw": "Xom", "filters": "Filtrlar", + "filters-enabled": "Filtrlar yoqilgan", "first-click": "Birinchi bosish", "first-seen": "Birinchi koʻrilgan", "funnel": "Voronka", diff --git a/public/intl/messages/vi-VN.json b/public/intl/messages/vi-VN.json index eec7e013b..c2d649f7b 100644 --- a/public/intl/messages/vi-VN.json +++ b/public/intl/messages/vi-VN.json @@ -124,6 +124,7 @@ "filter-combined": "Kết hợp lọc", "filter-raw": "Lọc thô", "filters": "Bộ lọc", + "filters-enabled": "Bộ lọc đã bật", "first-click": "Nhấp đầu tiên", "first-seen": "Lần đầu tiên nhìn thấy", "funnel": "Phễu", diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index e552cf158..946e21ff5 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -124,6 +124,7 @@ "filter-combined": "合并", "filter-raw": "原始", "filters": "筛选", + "filters-enabled": "过滤器已启用", "first-click": "首次点击", "first-seen": "首次出现", "funnel": "分析", diff --git a/public/intl/messages/zh-TW.json b/public/intl/messages/zh-TW.json index 1f644123c..8e65e271d 100644 --- a/public/intl/messages/zh-TW.json +++ b/public/intl/messages/zh-TW.json @@ -124,6 +124,7 @@ "filter-combined": "組合", "filter-raw": "原始", "filters": "篩選條件", + "filters-enabled": "篩選器已啟用", "first-click": "首次點擊", "first-seen": "首次造訪", "funnel": "漏斗分析", diff --git a/src/app/(main)/boards/[boardId]/BoardShareCreateForm.tsx b/src/app/(main)/boards/[boardId]/BoardShareCreateForm.tsx index 1842edcc9..3598bfe2f 100644 --- a/src/app/(main)/boards/[boardId]/BoardShareCreateForm.tsx +++ b/src/app/(main)/boards/[boardId]/BoardShareCreateForm.tsx @@ -1,6 +1,16 @@ -import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen'; +import { + Button, + Checkbox, + Column, + Form, + FormField, + FormSubmitButton, + Row, + TextField, +} from '@umami/react-zen'; import { useState } from 'react'; import { useApi, useMessages, useModified } from '@/components/hooks'; +import { ThemeModeSelector } from '@/components/input/ThemeModeSelector'; export function BoardShareCreateForm({ boardId, @@ -15,15 +25,19 @@ export function BoardShareCreateForm({ const { touch } = useModified(); const { t, labels, getErrorMessage } = useMessages(); const [isPending, setIsPending] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); - const handleSubmit = async (data: { name: string }) => { + const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => { setIsPending(true); setError(null); try { await post(`/boards/${boardId}/shares`, { name: data.name, + parameters: { + allowFilter: data.allowFilter ?? true, + theme: data.theme === 'system' ? undefined : data.theme, + }, }); touch('shares'); @@ -36,22 +50,38 @@ export function BoardShareCreateForm({ }; return ( -
- - - - - - {onCancel && ( - - )} - - {t(labels.add)} - - - + + {({ watch, setValue }) => ( + + + + + + {t(labels.filters)} + + + setValue('theme', value, { shouldDirty: true })} + /> + + + {onCancel && ( + + )} + + {t(labels.add)} + + + + )}
); } diff --git a/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx b/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx index 3f4e81531..c3b30c297 100644 --- a/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx +++ b/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx @@ -3,6 +3,7 @@ import { CopyButton } from '@/components/common/CopyButton'; import { ExternalLink } from '@/components/common/ExternalLink'; import { useConfig, useMessages, useMobile } from '@/components/hooks'; import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen'; +import { SimpleShareEditButton } from '@/components/share/SimpleShareEditButton'; export function BoardSharesTable(props: DataTableProps) { const { t, labels } = useMessages(); @@ -36,9 +37,10 @@ export function BoardSharesTable(props: DataTableProps) { ); }}
- + {({ id, slug }: any) => ( + )} diff --git a/src/app/(main)/links/[linkId]/LinkControls.tsx b/src/app/(main)/links/[linkId]/LinkControls.tsx index 1d1147a82..ad09457e8 100644 --- a/src/app/(main)/links/[linkId]/LinkControls.tsx +++ b/src/app/(main)/links/[linkId]/LinkControls.tsx @@ -1,9 +1,11 @@ import { Column, Row } from '@umami/react-zen'; +import { useShare } from '@/components/hooks'; import { ExportButton } from '@/components/input/ExportButton'; import { FilterBar } from '@/components/input/FilterBar'; import { MonthFilter } from '@/components/input/MonthFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton'; +import { allowShareFilter } from '@/lib/share'; export function LinkControls({ linkId: websiteId, @@ -18,15 +20,18 @@ export function LinkControls({ allowMonthFilter?: boolean; allowDownload?: boolean; }) { + const share = useShare(); + const showFilter = allowFilter && allowShareFilter(share?.parameters); + return ( - {allowFilter ? :
} + {showFilter ? :
} {allowDateFilter && } {allowDownload && } {allowMonthFilter && } - {allowFilter && } + {showFilter && } ); } diff --git a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx index 55dcd5764..539b0adca 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx @@ -1,9 +1,11 @@ import { Column, Row } from '@umami/react-zen'; +import { useShare } from '@/components/hooks'; import { ExportButton } from '@/components/input/ExportButton'; import { FilterBar } from '@/components/input/FilterBar'; import { MonthFilter } from '@/components/input/MonthFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton'; +import { allowShareFilter } from '@/lib/share'; export function PixelControls({ pixelId: websiteId, @@ -18,15 +20,18 @@ export function PixelControls({ allowMonthFilter?: boolean; allowDownload?: boolean; }) { + const share = useShare(); + const showFilter = allowFilter && allowShareFilter(share?.parameters); + return ( - {allowFilter ? :
} + {showFilter ? :
} {allowDateFilter && } {allowDownload && } {allowMonthFilter && } - {allowFilter && } + {showFilter && } ); } diff --git a/src/app/(main)/settings/preferences/ThemeSetting.tsx b/src/app/(main)/settings/preferences/ThemeSetting.tsx index 03bd6a6e4..091a78e2e 100644 --- a/src/app/(main)/settings/preferences/ThemeSetting.tsx +++ b/src/app/(main)/settings/preferences/ThemeSetting.tsx @@ -1,21 +1,8 @@ -import { Button, Icon, Row, useTheme } from '@umami/react-zen'; -import { Moon, Sun } from '@/components/icons'; +import { useTheme } from '@umami/react-zen'; +import { ThemeModeSelector } from '@/components/input/ThemeModeSelector'; export function ThemeSetting() { const { theme, setTheme } = useTheme(); - return ( - - - - - ); + return setTheme(value as 'light' | 'dark')} />; } diff --git a/src/app/(main)/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx index bfa13e4ae..e1ed4a7d1 100644 --- a/src/app/(main)/websites/WebsitesTable.tsx +++ b/src/app/(main)/websites/WebsitesTable.tsx @@ -1,5 +1,6 @@ import { DataColumn, DataTable, type DataTableProps, Icon } from '@umami/react-zen'; import type { ReactNode } from 'react'; +import { DateDistance } from '@/components/common/DateDistance'; import { LinkButton } from '@/components/common/LinkButton'; import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation } from '@/components/hooks'; @@ -22,6 +23,13 @@ export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTab {renderLink} } /> + } + width="200px" + > + {(row: any) => } + {showActions && ( {(row: any) => { diff --git a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx index bab23bc5b..a46489a13 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx @@ -1,9 +1,11 @@ import { Column, Grid, Row } from '@umami/react-zen'; +import { useShare } from '@/components/hooks'; import { ExportButton } from '@/components/input/ExportButton'; import { FilterBar } from '@/components/input/FilterBar'; import { MonthFilter } from '@/components/input/MonthFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton'; +import { allowShareFilter } from '@/lib/share'; export function WebsiteControls({ websiteId, @@ -22,11 +24,16 @@ export function WebsiteControls({ allowDownload?: boolean; allowCompare?: boolean; }) { + const share = useShare(); + const showFilter = allowFilter && allowShareFilter(share?.parameters); + return ( - {allowFilter && } + {showFilter && ( + + )} {allowDateFilter && ( @@ -36,7 +43,7 @@ export function WebsiteControls({ {allowMonthFilter && } - {allowFilter && } + {showFilter && } ); } diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx index 161d35733..3233a8585 100644 --- a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx @@ -7,13 +7,16 @@ import { FormSubmitButton, Grid, Label, + ListSeparator, Loading, Row, + Switch, Text, TextField, } from '@umami/react-zen'; import { useEffect, useState } from 'react'; import { useApi, useConfig, useMessages, useModified } from '@/components/hooks'; +import { ThemeModeSelector } from '@/components/input/ThemeModeSelector'; import { SHARE_NAV_ITEMS } from './constants'; export function ShareEditForm({ @@ -59,12 +62,14 @@ export function ShareEditForm({ }, [shareId, modified]); const handleSubmit = async (data: any) => { - const parameters: Record = {}; + const parameters: Record = {}; SHARE_NAV_ITEMS.forEach(section => { section.items.forEach(item => { parameters[item.id] = data[item.id] ?? false; }); }); + parameters.allowFilter = data.allowFilter ?? true; + parameters.theme = data.theme === 'system' ? undefined : data.theme; setIsPending(true); setError(null); @@ -101,6 +106,8 @@ export function ShareEditForm({ // Build default values from share parameters const defaultValues: Record = { name: share?.name || '', + allowFilter: share?.parameters?.allowFilter ?? true, + theme: share?.parameters?.theme || 'system', }; SHARE_NAV_ITEMS.forEach(section => { section.items.forEach(item => { @@ -114,7 +121,7 @@ export function ShareEditForm({ return (
- {({ watch }) => { + {({ watch, setValue }) => { const values = watch(); const hasSelection = allItemIds.some(id => values[id]); @@ -129,7 +136,25 @@ export function ShareEditForm({ - + + + setValue('allowFilter', value, { shouldDirty: true })} + > + {t(labels.filtersEnabled)} + + + + setValue('theme', value, { shouldDirty: true })} + /> + + + + {SHARE_NAV_ITEMS.map(section => ( {t((labels as any)[section.section])} diff --git a/src/app/api/boards/[boardId]/shares/route.ts b/src/app/api/boards/[boardId]/shares/route.ts index f8adeff10..46e28e522 100644 --- a/src/app/api/boards/[boardId]/shares/route.ts +++ b/src/app/api/boards/[boardId]/shares/route.ts @@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto'; import { getRandomChars } from '@/lib/generate'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { filterParams, pagingParams } from '@/lib/schema'; +import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema'; import { canUpdateBoard, canViewBoard } from '@/permissions'; import { createShare, getSharesByEntityId } from '@/queries/prisma'; @@ -45,6 +45,7 @@ export async function POST( ) { const schema = z.object({ name: z.string().max(200), + parameters: anyObjectParam.optional(), }); const { auth, body, error } = await parseRequest(request, schema); @@ -54,7 +55,8 @@ export async function POST( } const { boardId } = await params; - const { name } = body; + const { name, parameters } = body; + const shareParameters = parameters ?? {}; if (!(await canUpdateBoard(auth, boardId))) { return unauthorized(); @@ -66,7 +68,7 @@ export async function POST( shareType: ENTITY_TYPE.board, name, slug: getRandomChars(16), - parameters: {}, + parameters: shareParameters, }); return json(share); diff --git a/src/app/api/links/[linkId]/shares/route.ts b/src/app/api/links/[linkId]/shares/route.ts index 0ed8516e6..f596bad85 100644 --- a/src/app/api/links/[linkId]/shares/route.ts +++ b/src/app/api/links/[linkId]/shares/route.ts @@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto'; import { getRandomChars } from '@/lib/generate'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { filterParams, pagingParams } from '@/lib/schema'; +import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema'; import { canUpdateLink, canViewLink } from '@/permissions'; import { createShare, getSharesByEntityId } from '@/queries/prisma'; @@ -45,6 +45,7 @@ export async function POST( ) { const schema = z.object({ name: z.string().max(200), + parameters: anyObjectParam.optional(), }); const { auth, body, error } = await parseRequest(request, schema); @@ -54,7 +55,8 @@ export async function POST( } const { linkId } = await params; - const { name } = body; + const { name, parameters } = body; + const shareParameters = parameters ?? {}; if (!(await canUpdateLink(auth, linkId))) { return unauthorized(); @@ -66,7 +68,7 @@ export async function POST( shareType: ENTITY_TYPE.link, name, slug: getRandomChars(16), - parameters: {}, + parameters: shareParameters, }); return json(share); diff --git a/src/app/api/pixels/[pixelId]/shares/route.ts b/src/app/api/pixels/[pixelId]/shares/route.ts index 8b688cd7c..a82b8ff7e 100644 --- a/src/app/api/pixels/[pixelId]/shares/route.ts +++ b/src/app/api/pixels/[pixelId]/shares/route.ts @@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto'; import { getRandomChars } from '@/lib/generate'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { filterParams, pagingParams } from '@/lib/schema'; +import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema'; import { canUpdatePixel, canViewPixel } from '@/permissions'; import { createShare, getSharesByEntityId } from '@/queries/prisma'; @@ -45,6 +45,7 @@ export async function POST( ) { const schema = z.object({ name: z.string().max(200), + parameters: anyObjectParam.optional(), }); const { auth, body, error } = await parseRequest(request, schema); @@ -54,7 +55,8 @@ export async function POST( } const { pixelId } = await params; - const { name } = body; + const { name, parameters } = body; + const shareParameters = parameters ?? {}; if (!(await canUpdatePixel(auth, pixelId))) { return unauthorized(); @@ -66,7 +68,7 @@ export async function POST( shareType: ENTITY_TYPE.pixel, name, slug: getRandomChars(16), - parameters: {}, + parameters: shareParameters, }); return json(share); diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx index ab01690eb..806e1347a 100644 --- a/src/app/share/ShareProvider.tsx +++ b/src/app/share/ShareProvider.tsx @@ -1,7 +1,7 @@ 'use client'; import { useShareTokenQuery } from '@/components/hooks'; import { ENTITY_TYPE } from '@/lib/constants'; -import type { WhiteLabel } from '@/lib/types'; +import type { ShareParameters, WhiteLabel } from '@/lib/types'; import { Loading } from '@umami/react-zen'; import { usePathname, useRouter } from 'next/navigation'; import { createContext, type ReactNode, useEffect } from 'react'; @@ -15,7 +15,7 @@ export interface ShareData { boardId?: string; pixelId?: string; linkId?: string; - parameters: any; + parameters: ShareParameters; token: string; whiteLabel?: WhiteLabel; } diff --git a/src/app/share/[slug]/[[...path]]/ShareNav.tsx b/src/app/share/[slug]/[[...path]]/ShareNav.tsx index 9bc238ad4..66899d256 100644 --- a/src/app/share/[slug]/[[...path]]/ShareNav.tsx +++ b/src/app/share/[slug]/[[...path]]/ShareNav.tsx @@ -3,6 +3,7 @@ import { useMessages, useNavigation, useShare } from '@/components/hooks'; import { AlignEndHorizontal, Clock, Eye, PanelLeft, Sheet, Tag, User } from '@/components/icons'; import { LanguageButton } from '@/components/input/LanguageButton'; import { PreferencesButton } from '@/components/input/PreferencesButton'; +import { allowShareFilter, excludeShareFilterParam, getShareTheme } from '@/lib/share'; import { Funnel, Gauge, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg'; import { buildPath } from '@/lib/url'; import { @@ -32,12 +33,19 @@ export function ShareNav({ const { t, labels } = useMessages(); const { pathname, query } = useNavigation(); const { slug, parameters } = share; + const allowFilter = allowShareFilter(parameters); + const shareTheme = getShareTheme(parameters); const renderPath = (path: string) => buildPath(`/share/${slug}${path}`, { - ...query, + ...Object.fromEntries( + Object.entries(query).filter(([key]) => { + return allowFilter || !excludeShareFilterParam(key); + }), + ), event: undefined, compare: undefined, + theme: undefined, view: undefined, unit: undefined, excludeBounce: undefined, @@ -202,13 +210,13 @@ export function ShareNav({ > {collapsed ? ( - + {!shareTheme && } ) : ( - + {!shareTheme && } diff --git a/src/app/share/[slug]/[[...path]]/SharePage.tsx b/src/app/share/[slug]/[[...path]]/SharePage.tsx index 122ad0a0b..d6660bd8a 100644 --- a/src/app/share/[slug]/[[...path]]/SharePage.tsx +++ b/src/app/share/[slug]/[[...path]]/SharePage.tsx @@ -22,6 +22,7 @@ import { PageBody } from '@/components/common/PageBody'; import { useShare } from '@/components/hooks'; import { MobileMenuButton } from '@/components/input/MobileMenuButton'; import { ENTITY_TYPE } from '@/lib/constants'; +import { getShareTheme } from '@/lib/share'; import { Column, Grid, Row, useTheme } from '@umami/react-zen'; import { usePathname, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -68,20 +69,20 @@ export function SharePage() { setNavCollapsed(value); }; const share = useShare(); - const { setTheme } = useTheme(); + const { initTheme } = useTheme(); const router = useRouter(); const pathname = usePathname(); const path = getSharePath(pathname); const { slug, websiteId, boardId, pixelId, linkId, parameters = {}, shareType } = share; + const shareTheme = getShareTheme(parameters); useEffect(() => { - const url = new URL(window?.location?.href); - const theme = url.searchParams.get('theme'); + initTheme(shareTheme, 'system'); - if (theme === 'light' || theme === 'dark') { - setTheme(theme); - } - }, [setTheme]); + return () => { + initTheme(undefined, 'system'); + }; + }, [shareTheme, initTheme]); // Check if the requested path is allowed const pageKey = path || ''; diff --git a/src/components/common/DataGrid.tsx b/src/components/common/DataGrid.tsx index 9a601a129..58978c35d 100644 --- a/src/components/common/DataGrid.tsx +++ b/src/components/common/DataGrid.tsx @@ -62,47 +62,47 @@ export function DataGrid({ const child = data ? (typeof children === 'function' ? children(data) : children) : null; return ( - - {allowSearch && ( - - - {renderActions?.()} - - )} - - {data && ( - <> + + + {allowSearch && ( + + + {renderActions?.()} + + )} + + {data && ( {isValidElement(child) ? cloneElement(child as ReactElement, { displayMode }) : child} - {showPager && ( - - - - )} - - )} - + )} + + + {showPager && ( + + + + )} ); } diff --git a/src/components/hooks/useFilterParameters.ts b/src/components/hooks/useFilterParameters.ts index 24211c386..b5900d465 100644 --- a/src/components/hooks/useFilterParameters.ts +++ b/src/components/hooks/useFilterParameters.ts @@ -1,27 +1,32 @@ import { useMemo } from 'react'; import { FILTER_COLUMNS } from '@/lib/constants'; +import { useShare } from './context/useShare'; import { useNavigation } from './useNavigation'; export function useFilterParameters({ includePagination = true }: { includePagination?: boolean } = {}) { const { query } = useNavigation(); + const share = useShare(); + const allowFilter = share?.parameters?.allowFilter !== false; return useMemo(() => { const filterParams: Record = {}; - for (const key of Object.keys(query)) { - const baseName = key.replace(/\d+$/, ''); - if (FILTER_COLUMNS[baseName]) { - filterParams[key] = query[key]; + if (allowFilter) { + for (const key of Object.keys(query)) { + const baseName = key.replace(/\d+$/, ''); + if (FILTER_COLUMNS[baseName]) { + filterParams[key] = query[key]; + } } } const params = { ...filterParams, search: query.search, - segment: query.segment, - cohort: query.cohort, - excludeBounce: query.excludeBounce, - match: query.match, + segment: allowFilter ? query.segment : undefined, + cohort: allowFilter ? query.cohort : undefined, + excludeBounce: allowFilter ? query.excludeBounce : undefined, + match: allowFilter ? query.match : undefined, } as Record; if (includePagination) { @@ -30,5 +35,5 @@ export function useFilterParameters({ includePagination = true }: { includePagin } return params; - }, [query, includePagination]); + }, [allowFilter, includePagination, query]); } diff --git a/src/components/hooks/useFilters.ts b/src/components/hooks/useFilters.ts index c50625deb..94e2f8936 100644 --- a/src/components/hooks/useFilters.ts +++ b/src/components/hooks/useFilters.ts @@ -1,5 +1,6 @@ import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants'; import { safeDecodeURIComponent } from '@/lib/url'; +import { useShare } from './context/useShare'; import { useFields } from './useFields'; import { useMessages } from './useMessages'; import { useNavigation } from './useNavigation'; @@ -10,6 +11,8 @@ export function useFilters() { const { query } = useNavigation(); const { fields } = useFields(); const operatorLabels = useOperatorLabels(); + const share = useShare(); + const allowFilter = share?.parameters?.allowFilter !== false; const operators = [ { name: 'eq', type: 'string', label: t(labels.is) }, @@ -56,30 +59,32 @@ export function useFilters() { uuid: [OPERATORS.equals], }; - const filters = Object.keys(query).reduce((arr, key) => { - const baseName = key.replace(/\d+$/, ''); - if (FILTER_COLUMNS[baseName]) { - let operator = 'eq'; - let value = safeDecodeURIComponent(query[key]); - const label = fields.find(({ name }) => name === baseName)?.label; + const filters = allowFilter + ? Object.keys(query).reduce((arr, key) => { + const baseName = key.replace(/\d+$/, ''); + if (FILTER_COLUMNS[baseName]) { + let operator = 'eq'; + let value = safeDecodeURIComponent(query[key]); + const label = fields.find(({ name }) => name === baseName)?.label; - const match = value.match(/^([a-z]+)\.(.*)/); + const match = value.match(/^([a-z]+)\.(.*)/); - if (match) { - operator = match[1]; - value = match[2]; - } + if (match) { + operator = match[1]; + value = match[2]; + } - return arr.concat({ - name: key, - type: baseName, - operator, - value, - label, - }); - } - return arr; - }, []); + return arr.concat({ + name: key, + type: baseName, + operator, + value, + label, + }); + } + return arr; + }, []) + : []; const getFilters = (type: string) => { return ( diff --git a/src/components/input/ThemeModeSelector.tsx b/src/components/input/ThemeModeSelector.tsx new file mode 100644 index 000000000..ad4d6a920 --- /dev/null +++ b/src/components/input/ThemeModeSelector.tsx @@ -0,0 +1,36 @@ +import type { ReactNode } from 'react'; +import { Button, Icon, Row } from '@umami/react-zen'; +import { Monitor, Moon, Sun } from '@/components/icons'; + +export type ThemeMode = 'light' | 'dark' | 'system'; + +export function ThemeModeSelector({ + value, + onChange, + includeSystem = false, +}: { + value?: ThemeMode; + onChange: (value: ThemeMode) => void; + includeSystem?: boolean; +}) { + const options: { id: ThemeMode; icon: ReactNode; label: string }[] = [ + { id: 'light', icon: , label: 'Light' }, + ...(includeSystem ? [{ id: 'system' as const, icon: , label: 'System' }] : []), + { id: 'dark', icon: , label: 'Dark' }, + ]; + + return ( + + {options.map(({ id, icon, label }) => ( + + ))} + + ); +} diff --git a/src/components/messages.ts b/src/components/messages.ts index 72ce903cf..84ce2054d 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -190,6 +190,7 @@ export const labels: Record = { type: 'label.type', filter: 'label.filter', filters: 'label.filters', + filtersEnabled: 'label.filters-enabled', propertyFilter: 'label.property-filter', activityByProperty: 'label.activity-by-property', breakdown: 'label.breakdown', diff --git a/src/components/share/SimpleShareCreateForm.tsx b/src/components/share/SimpleShareCreateForm.tsx index 9ab850bcb..28ff801cc 100644 --- a/src/components/share/SimpleShareCreateForm.tsx +++ b/src/components/share/SimpleShareCreateForm.tsx @@ -1,6 +1,16 @@ -import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen'; +import { + Button, + Column, + Form, + FormField, + FormSubmitButton, + Row, + Switch, + TextField, +} from '@umami/react-zen'; import { useState } from 'react'; import { useApi, useMessages, useModified } from '@/components/hooks'; +import { ThemeModeSelector } from '@/components/input/ThemeModeSelector'; export function SimpleShareCreateForm({ createPath, @@ -15,15 +25,19 @@ export function SimpleShareCreateForm({ const { touch } = useModified(); const { t, labels, getErrorMessage } = useMessages(); const [isPending, setIsPending] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); - const handleSubmit = async (data: { name: string }) => { + const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => { setIsPending(true); setError(null); try { await post(createPath, { name: data.name, + parameters: { + allowFilter: data.allowFilter ?? true, + theme: data.theme === 'system' ? undefined : data.theme, + }, }); touch('shares'); @@ -36,22 +50,45 @@ export function SimpleShareCreateForm({ }; return ( - - - - - - - {onCancel && ( - - )} - - {t(labels.add)} - - - + + {({ watch, setValue }) => ( + + + + + + + setValue('allowFilter', value, { shouldDirty: true })} + > + {t(labels.filtersEnabled)} + + + + setValue('theme', value, { shouldDirty: true })} + /> + + + + {onCancel && ( + + )} + + {t(labels.add)} + + + + )} ); } diff --git a/src/components/share/SimpleShareEditForm.tsx b/src/components/share/SimpleShareEditForm.tsx index 3c83fed52..6bdcf9300 100644 --- a/src/components/share/SimpleShareEditForm.tsx +++ b/src/components/share/SimpleShareEditForm.tsx @@ -7,10 +7,12 @@ import { Label, Loading, Row, + Switch, TextField, } from '@umami/react-zen'; import { useEffect, useState } from 'react'; import { useApi, useConfig, useMessages, useModified } from '@/components/hooks'; +import { ThemeModeSelector } from '@/components/input/ThemeModeSelector'; export function SimpleShareEditForm({ shareId, @@ -49,7 +51,7 @@ export function SimpleShareEditForm({ loadShare(); }, [get, modified, shareId]); - const handleSubmit = async (data: { name: string }) => { + const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => { setIsPending(true); setError(null); @@ -57,7 +59,11 @@ export function SimpleShareEditForm({ await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, - parameters: share.parameters || {}, + parameters: { + ...(share.parameters || {}), + allowFilter: data.allowFilter ?? true, + theme: data.theme === 'system' ? undefined : data.theme, + }, }); touch('shares'); @@ -78,27 +84,50 @@ export function SimpleShareEditForm({
- - - - + {({ watch, setValue }) => ( + + + + + + + + + + + setValue('allowFilter', value, { shouldDirty: true })} + > + {t(labels.filtersEnabled)} + + + + setValue('theme', value, { shouldDirty: true })} + /> + + + + {onClose && ( + + )} + + {t(labels.save)} + + - - - - - {onClose && ( - - )} - - {t(labels.save)} - - - + )} ); } diff --git a/src/lib/share.ts b/src/lib/share.ts new file mode 100644 index 000000000..4fc20366c --- /dev/null +++ b/src/lib/share.ts @@ -0,0 +1,20 @@ +import { FILTER_COLUMNS } from './constants'; +import type { ShareParameters, ShareTheme } from './types'; + +const FILTER_QUERY_PARAMS = new Set(['cohort', 'excludeBounce', 'match', 'segment']); + +export function allowShareFilter(parameters?: ShareParameters | null) { + return parameters?.allowFilter !== false; +} + +export function getShareTheme(parameters?: ShareParameters | null): ShareTheme | undefined { + return parameters?.theme === 'light' || parameters?.theme === 'dark' + ? parameters.theme + : undefined; +} + +export function excludeShareFilterParam(key: string) { + const baseName = key.replace(/\d+$/, ''); + + return baseName in FILTER_COLUMNS || FILTER_QUERY_PARAMS.has(key); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 6c8bb92ff..d2e20712b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -27,9 +27,18 @@ export interface Auth { pixelIds?: string[]; linkId?: string; linkIds?: string[]; + parameters?: ShareParameters; }; } +export type ShareTheme = 'light' | 'dark'; + +export interface ShareParameters { + allowFilter?: boolean; + theme?: ShareTheme; + [key: string]: boolean | ShareTheme | undefined; +} + export interface PropertyFilter { propertyName: string; dataType: number; From 17e7a4a63aee4a603c858d29f297ffc2df4fbbcf Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 7 May 2026 14:17:03 -0700 Subject: [PATCH 21/21] exclude performance events from non-performance metrics/lists --- src/queries/sql/events/getWebsiteEvents.ts | 3 +++ src/queries/sql/getRealtimeActivity.ts | 3 +++ src/queries/sql/reports/getRevenueSessions.ts | 3 +++ src/queries/sql/sessions/getSessionActivity.ts | 3 +++ src/queries/sql/sessions/getSessionDataActivityStats.ts | 6 +++++- src/queries/sql/sessions/getSessionDataArraySeries.ts | 4 +++- src/queries/sql/sessions/getSessionDataDateSeries.ts | 4 +++- src/queries/sql/sessions/getSessionDataNumericSeries.ts | 3 +++ src/queries/sql/sessions/getSessionDataNumericStats.ts | 3 +++ src/queries/sql/sessions/getSessionDataPivot.ts | 3 +++ src/queries/sql/sessions/getSessionDataProperties.ts | 3 +++ src/queries/sql/sessions/getSessionDataPropertySeries.ts | 4 +++- src/queries/sql/sessions/getSessionDataValues.ts | 6 +++++- src/queries/sql/sessions/getWebsiteSession.ts | 3 +++ src/queries/sql/sessions/getWebsiteSessionStats.ts | 7 +++++-- src/queries/sql/sessions/getWebsiteSessions.ts | 5 ++++- 16 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/queries/sql/events/getWebsiteEvents.ts b/src/queries/sql/events/getWebsiteEvents.ts index f11d3ff13..d7a8ecb63 100644 --- a/src/queries/sql/events/getWebsiteEvents.ts +++ b/src/queries/sql/events/getWebsiteEvents.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -55,6 +56,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { join session on session.session_id = website_event.session_id and session.website_id = website_event.website_id where website_event.website_id = {{websiteId::uuid}} + and website_event.event_type != ${EVENT_TYPE.performance} ${dateQuery} ${filterQuery} ${searchQuery} @@ -107,6 +109,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { from website_event ${cohortQuery} where website_id = {websiteId:UUID} + and event_type != ${EVENT_TYPE.performance} ${dateQuery} ${filterQuery} ${searchQuery} diff --git a/src/queries/sql/getRealtimeActivity.ts b/src/queries/sql/getRealtimeActivity.ts index c847b6f7d..f328d6488 100644 --- a/src/queries/sql/getRealtimeActivity.ts +++ b/src/queries/sql/getRealtimeActivity.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -38,6 +39,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { on session.session_id = website_event.session_id and session.website_id = website_event.website_id where website_event.website_id = {{websiteId::uuid}} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${dateQuery} order by website_event.created_at desc @@ -71,6 +73,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis from website_event ${cohortQuery} where website_id = {websiteId:UUID} + and event_type != ${EVENT_TYPE.performance} ${filterQuery} ${dateQuery} order by createdAt desc diff --git a/src/queries/sql/reports/getRevenueSessions.ts b/src/queries/sql/reports/getRevenueSessions.ts index 3f30a79ab..f7aa70dd0 100644 --- a/src/queries/sql/reports/getRevenueSessions.ts +++ b/src/queries/sql/reports/getRevenueSessions.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -64,6 +65,7 @@ async function relationalQuery(websiteId: string, currency: string, filters: Que and upper(currency) = {{currency}} ) rev on rev.session_id = website_event.session_id where website_event.website_id = {{websiteId::uuid}} + and website_event.event_type != ${EVENT_TYPE.performance} ${dateQuery} ${filterQuery} ${searchQuery} @@ -126,6 +128,7 @@ async function clickhouseQuery(websiteId: string, currency: string, filters: Que from website_event ${cohortQuery} where website_id = {websiteId:UUID} + and event_type != ${EVENT_TYPE.performance} ${dateQuery} ${filterQuery} ${searchQuery} diff --git a/src/queries/sql/sessions/getSessionActivity.ts b/src/queries/sql/sessions/getSessionActivity.ts index 1ac7e6ff8..f8cff7f39 100644 --- a/src/queries/sql/sessions/getSessionActivity.ts +++ b/src/queries/sql/sessions/getSessionActivity.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -37,6 +38,7 @@ async function relationalQuery(websiteId: string, sessionId: string, filters: Qu from website_event where website_id = {{websiteId::uuid}} and session_id = {{sessionId::uuid}} + and event_type != ${EVENT_TYPE.performance} and created_at between {{startDate}} and {{endDate}} order by created_at desc limit 500 @@ -70,6 +72,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string, filters: Qu from website_event where website_id = {websiteId:UUID} and session_id = {sessionId:UUID} + and event_type != ${EVENT_TYPE.performance} and created_at between {startDate:DateTime64} and {endDate:DateTime64} order by created_at desc limit 500 diff --git a/src/queries/sql/sessions/getSessionDataActivityStats.ts b/src/queries/sql/sessions/getSessionDataActivityStats.ts index c8bb0a277..939f437c0 100644 --- a/src/queries/sql/sessions/getSessionDataActivityStats.ts +++ b/src/queries/sql/sessions/getSessionDataActivityStats.ts @@ -1,5 +1,5 @@ import clickhouse from '@/lib/clickhouse'; -import { DATA_TYPE } from '@/lib/constants'; +import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { PropertyFilter, PropertyLeaderboardRow, QueryFilters } from '@/lib/types'; @@ -39,6 +39,7 @@ async function relationalQuery( ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ), @@ -57,6 +58,7 @@ async function relationalQuery( and filtered_sessions.website_id = website_event.website_id where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} group by website_event.session_id, website_event.visit_id ), session_stats as ( @@ -119,6 +121,7 @@ async function clickhouseQuery( ${cohortQuery} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ), @@ -135,6 +138,7 @@ async function clickhouseQuery( join filtered_sessions on filtered_sessions.session_id = website_event.session_id where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} group by website_event.session_id, website_event.visit_id ), session_stats as ( diff --git a/src/queries/sql/sessions/getSessionDataArraySeries.ts b/src/queries/sql/sessions/getSessionDataArraySeries.ts index a4b81ecb1..89e876d45 100644 --- a/src/queries/sql/sessions/getSessionDataArraySeries.ts +++ b/src/queries/sql/sessions/getSessionDataArraySeries.ts @@ -1,5 +1,5 @@ import clickhouse from '@/lib/clickhouse'; -import { DATA_TYPE } from '@/lib/constants'; +import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { EventDataSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types'; @@ -50,6 +50,7 @@ async function relationalQuery( cross join lateral jsonb_array_elements_text(coalesce(session_data.string_value, '[]')::jsonb) as array_item(value) where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {{propertyName}} and session_data.data_type = ${DATA_TYPE.array} ${filterQuery} @@ -86,6 +87,7 @@ async function clickhouseQuery( and session_data.website_id = {websiteId:UUID} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {propertyName:String} and session_data.data_type = ${DATA_TYPE.array} ${filterQuery} diff --git a/src/queries/sql/sessions/getSessionDataDateSeries.ts b/src/queries/sql/sessions/getSessionDataDateSeries.ts index d74b5e8a8..15b762959 100644 --- a/src/queries/sql/sessions/getSessionDataDateSeries.ts +++ b/src/queries/sql/sessions/getSessionDataDateSeries.ts @@ -1,5 +1,5 @@ import clickhouse from '@/lib/clickhouse'; -import { DATA_TYPE } from '@/lib/constants'; +import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { EventDataDateSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types'; @@ -48,6 +48,7 @@ async function relationalQuery( and session_data.website_id = website_event.website_id where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {{propertyName}} and session_data.data_type = ${DATA_TYPE.date} ${filterQuery} @@ -83,6 +84,7 @@ async function clickhouseQuery( and session_data.website_id = {websiteId:UUID} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {propertyName:String} and session_data.data_type = ${DATA_TYPE.date} ${filterQuery} diff --git a/src/queries/sql/sessions/getSessionDataNumericSeries.ts b/src/queries/sql/sessions/getSessionDataNumericSeries.ts index 03d660f02..e74ac8f56 100644 --- a/src/queries/sql/sessions/getSessionDataNumericSeries.ts +++ b/src/queries/sql/sessions/getSessionDataNumericSeries.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { PropertyFilter, QueryFilters } from '@/lib/types'; @@ -51,6 +52,7 @@ async function relationalQuery( ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ) @@ -98,6 +100,7 @@ async function clickhouseQuery( ${cohortQuery} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ) diff --git a/src/queries/sql/sessions/getSessionDataNumericStats.ts b/src/queries/sql/sessions/getSessionDataNumericStats.ts index a7dcd7df0..7f6ee775f 100644 --- a/src/queries/sql/sessions/getSessionDataNumericStats.ts +++ b/src/queries/sql/sessions/getSessionDataNumericStats.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { EventDataNumericStats, PropertyFilter, QueryFilters } from '@/lib/types'; @@ -43,6 +44,7 @@ async function relationalQuery( ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ) @@ -84,6 +86,7 @@ async function clickhouseQuery( ${cohortQuery} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ) diff --git a/src/queries/sql/sessions/getSessionDataPivot.ts b/src/queries/sql/sessions/getSessionDataPivot.ts index c4e460b0e..9e9445853 100644 --- a/src/queries/sql/sessions/getSessionDataPivot.ts +++ b/src/queries/sql/sessions/getSessionDataPivot.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { PageResult, PropertyFilter, QueryFilters, SessionDataPivotRow } from '@/lib/types'; @@ -48,6 +49,7 @@ async function relationalQuery( ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ), @@ -141,6 +143,7 @@ async function clickhouseQuery( ${cohortQuery} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ), diff --git a/src/queries/sql/sessions/getSessionDataProperties.ts b/src/queries/sql/sessions/getSessionDataProperties.ts index 2cae98b2c..38708b116 100644 --- a/src/queries/sql/sessions/getSessionDataProperties.ts +++ b/src/queries/sql/sessions/getSessionDataProperties.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { PropertyFilter, QueryFilters } from '@/lib/types'; @@ -47,6 +48,7 @@ async function relationalQuery( ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ), @@ -98,6 +100,7 @@ async function clickhouseQuery( ${cohortQuery} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} ${pfSQL} ), diff --git a/src/queries/sql/sessions/getSessionDataPropertySeries.ts b/src/queries/sql/sessions/getSessionDataPropertySeries.ts index fdffab423..f66620b1e 100644 --- a/src/queries/sql/sessions/getSessionDataPropertySeries.ts +++ b/src/queries/sql/sessions/getSessionDataPropertySeries.ts @@ -1,5 +1,5 @@ import clickhouse from '@/lib/clickhouse'; -import { DATA_TYPE } from '@/lib/constants'; +import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { EventDataSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types'; @@ -49,6 +49,7 @@ async function relationalQuery( and session_data.website_id = website_event.website_id where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {{propertyName}} and session_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean}) ${filterQuery} @@ -85,6 +86,7 @@ async function clickhouseQuery( and session_data.website_id = {websiteId:UUID} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {propertyName:String} and session_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean}) ${filterQuery} diff --git a/src/queries/sql/sessions/getSessionDataValues.ts b/src/queries/sql/sessions/getSessionDataValues.ts index 1c7434768..d862fa66a 100644 --- a/src/queries/sql/sessions/getSessionDataValues.ts +++ b/src/queries/sql/sessions/getSessionDataValues.ts @@ -1,5 +1,5 @@ import clickhouse from '@/lib/clickhouse'; -import { DATA_TYPE } from '@/lib/constants'; +import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -41,6 +41,7 @@ async function relationalQuery( cross join lateral jsonb_array_elements_text(coalesce(session_data.string_value, '[]')::jsonb) as array_item(value) where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {{propertyName}} and session_data.data_type = ${DATA_TYPE.array} ${filterQuery} @@ -70,6 +71,7 @@ async function relationalQuery( and session_data.website_id = website_event.website_id where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {{propertyName}} ${dataType ? `and session_data.data_type = ${dataType}` : ''} ${filterQuery} @@ -103,6 +105,7 @@ async function clickhouseQuery( and session_data.website_id = {websiteId:UUID} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {propertyName:String} and session_data.data_type = ${DATA_TYPE.array} ${filterQuery} @@ -129,6 +132,7 @@ async function clickhouseQuery( and session_data.website_id = {websiteId:UUID} where website_event.website_id = {websiteId:UUID} and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_event.event_type != ${EVENT_TYPE.performance} and session_data.data_key = {propertyName:String} ${dataType ? `and session_data.data_type = ${dataType}` : ''} ${filterQuery} diff --git a/src/queries/sql/sessions/getWebsiteSession.ts b/src/queries/sql/sessions/getWebsiteSession.ts index 3c1608713..28a85f969 100644 --- a/src/queries/sql/sessions/getWebsiteSession.ts +++ b/src/queries/sql/sessions/getWebsiteSession.ts @@ -1,4 +1,5 @@ import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; @@ -54,6 +55,7 @@ async function relationalQuery(websiteId: string, sessionId: string) { join website_event on website_event.session_id = session.session_id where session.website_id = {{websiteId::uuid}} and session.session_id = {{sessionId::uuid}} + and website_event.event_type != ${EVENT_TYPE.performance} group by session.session_id, session.distinct_id, visit_id, session.website_id, session.browser, session.os, session.device, session.screen, session.language, session.country, session.region, session.city) t group by id, distinct_id, website_id, browser, os, device, screen, language, country, region, city; `, @@ -104,6 +106,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string) { from website_event_stats_hourly where website_id = {websiteId:UUID} and session_id = {sessionId:UUID} + and event_type != ${EVENT_TYPE.performance} group by session_id, distinct_id, visit_id, website_id, browser, os, device, screen, language, country, region, city) t group by id, websiteId, distinctId, browser, os, device, screen, language, country, region, city; `, diff --git a/src/queries/sql/sessions/getWebsiteSessionStats.ts b/src/queries/sql/sessions/getWebsiteSessionStats.ts index a4615ee40..a14fbc5f3 100644 --- a/src/queries/sql/sessions/getWebsiteSessionStats.ts +++ b/src/queries/sql/sessions/getWebsiteSessionStats.ts @@ -1,5 +1,5 @@ import clickhouse from '@/lib/clickhouse'; -import { EVENT_COLUMNS } from '@/lib/constants'; +import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -36,7 +36,7 @@ async function relationalQuery( return rawQuery( ` select - count(*) as "pageviews", + sum(case when website_event.event_type = 1 then 1 else 0 end) as "pageviews", count(distinct website_event.session_id) as "visitors", count(distinct website_event.visit_id) as "visits", count(distinct session.country) as "countries", @@ -47,6 +47,7 @@ async function relationalQuery( and website_event.website_id = session.website_id where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} + and website_event.event_type != ${EVENT_TYPE.performance} ${filterQuery} `, queryParams, @@ -75,6 +76,7 @@ async function clickhouseQuery( ${cohortQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type != ${EVENT_TYPE.performance} ${filterQuery} `; } else { @@ -89,6 +91,7 @@ async function clickhouseQuery( ${cohortQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type != ${EVENT_TYPE.performance} ${filterQuery} `; } diff --git a/src/queries/sql/sessions/getWebsiteSessions.ts b/src/queries/sql/sessions/getWebsiteSessions.ts index 8efea80df..ffa3271b6 100644 --- a/src/queries/sql/sessions/getWebsiteSessions.ts +++ b/src/queries/sql/sessions/getWebsiteSessions.ts @@ -1,5 +1,5 @@ import clickhouse from '@/lib/clickhouse'; -import { EVENT_COLUMNS } from '@/lib/constants'; +import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; @@ -55,6 +55,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { join session on session.session_id = website_event.session_id and session.website_id = website_event.website_id where website_event.website_id = {{websiteId::uuid}} + and website_event.event_type != ${EVENT_TYPE.performance} ${dateQuery} ${filterQuery} ${searchQuery} @@ -118,6 +119,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { from website_event ${cohortQuery} where website_id = {websiteId:UUID} + and event_type != ${EVENT_TYPE.performance} ${dateQuery} ${filterQuery} ${searchQuery} @@ -147,6 +149,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { from website_event_stats_hourly as website_event ${cohortQuery} where website_id = {websiteId:UUID} + and event_type != ${EVENT_TYPE.performance} ${dateQuery} ${filterQuery} ${searchQuery}