@tuturuuu/ui 0.37.2 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/package.json +10 -7
- package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +86 -0
- package/src/components/ui/custom/education-todo.test.tsx +131 -0
- package/src/components/ui/custom/education-todo.tsx +191 -0
- package/src/components/ui/custom/satellite-notification-polling.test.tsx +61 -0
- package/src/components/ui/custom/satellite-notification-popover.tsx +7 -3
- package/src/components/ui/custom/workspace-picker-content.tsx +93 -30
- package/src/components/ui/custom/workspace-select.tsx +12 -4
- package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
- package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
- package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
- package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
- package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
- package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
- package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
- package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
- package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
- package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
- package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
- package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
- package/src/components/ui/finance/invoices/hooks.ts +3 -100
- package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
- package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
- package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
- package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
- package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +438 -0
- package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
- package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
- package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
- package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
- package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
- package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
- package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
- package/src/components/ui/finance/invoices/types.ts +3 -3
- package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
- package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
- package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
- package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
- package/src/components/ui/legacy/calendar/calendar-draft-recovery.ts +34 -0
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
- package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
- package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
- package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
- package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1614
- package/src/components/ui/legacy/calendar/event-save-payload.test.ts +40 -0
- package/src/components/ui/legacy/calendar/event-save-payload.ts +79 -0
- package/src/components/ui/legacy/calendar/event-source-selection.test.ts +95 -0
- package/src/components/ui/legacy/calendar/event-source-selection.ts +78 -0
- package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
- package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
- package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
- package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
- package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
- package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
- package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
- package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
- package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
- package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
- package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
- package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
- package/src/components/ui/legacy/calendar/provider-color-picker.test.tsx +147 -0
- package/src/components/ui/legacy/calendar/provider-color-picker.tsx +115 -0
- package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
- package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +35 -1
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +23 -11
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +119 -0
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +141 -0
- package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
- package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
- package/src/components/ui/popover.tsx +3 -1
- package/src/components/ui/text-editor/__tests__/collaboration-carets.test.ts +142 -0
- package/src/components/ui/text-editor/__tests__/markdown-document.test.ts +50 -0
- package/src/components/ui/text-editor/clipboard-serialization.ts +10 -0
- package/src/components/ui/text-editor/collaboration-carets.ts +56 -0
- package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +2 -0
- package/src/components/ui/text-editor/editor.tsx +11 -3
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
- package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
- package/src/components/ui/text-editor/markdown-paste-extension.ts +1 -1
- package/src/components/ui/text-editor/markdown.ts +11 -0
- package/src/components/ui/text-editor/tool-bar.tsx +45 -102
- package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
- package/src/hooks/__tests__/collaboration-awareness.test.ts +106 -0
- package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
- package/src/hooks/__tests__/supabase-provider.test.ts +110 -7
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +39 -2
- package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
- package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +31 -1
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +3 -3
- package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +147 -0
- package/src/hooks/calendar-connection-types.ts +49 -0
- package/src/hooks/calendar-creation-request.test.ts +62 -0
- package/src/hooks/calendar-creation-request.ts +34 -0
- package/src/hooks/calendar-event-update-queue-types.ts +12 -0
- package/src/hooks/calendar-event-write-payload.ts +57 -0
- package/src/hooks/collaboration-document-hydration.ts +35 -0
- package/src/hooks/notification-subscription.ts +5 -2
- package/src/hooks/supabase-provider.ts +76 -112
- package/src/hooks/use-calendar-sync.tsx +41 -62
- package/src/hooks/use-calendar.tsx +44 -82
- package/src/hooks/use-exchange-rates.ts +4 -15
- package/src/hooks/use-local-storage.ts +9 -11
- package/src/hooks/use-native-calendar-occurrences.ts +85 -0
- package/src/hooks/use-workspace-presence.ts +9 -9
- package/src/hooks/use-yjs-collaboration.ts +68 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.39.0](https://github.com/tutur3u/platform/compare/ui-v0.38.0...ui-v0.39.0) (2026-10-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **docs:** expand platform guides, enforce SEO, and pause inactive runtimes ([#6085](https://github.com/tutur3u/platform/issues/6085)) ([df7a159](https://github.com/tutur3u/platform/commit/df7a159e9c657fee4f6e0b593c776d6887b2ae11))
|
|
9
|
+
* **meet:** simplify the in-call document toolbar ([6020a1e](https://github.com/tutur3u/platform/commit/6020a1eddfc4f8cab9179daf1feb126f9dfc0501)) ([#5918](https://github.com/tutur3u/platform/issues/5918)) ([fa358e7](https://github.com/tutur3u/platform/commit/fa358e76517e5bc9793caf72658738d9bf2b42d6))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **calendar:** stabilize civil dates and today rendering ([c1abdf0](https://github.com/tutur3u/platform/commit/c1abdf0df75c0a02cc00c37b7a0a5e55c862bf89)) ([#5950](https://github.com/tutur3u/platform/issues/5950)) ([5a00326](https://github.com/tutur3u/platform/commit/5a00326171bec65ec7210de161dd5272bfda22b2))
|
|
15
|
+
* **finance:** bound subscription schedules and block invalid inventory ([f4163bf](https://github.com/tutur3u/platform/commit/f4163bf6697316a06a8b2d3922d9664937dd6940)) ([#5990](https://github.com/tutur3u/platform/issues/5990)) ([f2096f6](https://github.com/tutur3u/platform/commit/f2096f6e20d27dd998a03d11c7b0769d7f06bb2e))
|
|
16
|
+
* **finance:** expose safe invoice history recovery ([8a53d36](https://github.com/tutur3u/platform/commit/8a53d367d6888e592a0d64f760d47234d858bd73)) ([#6034](https://github.com/tutur3u/platform/issues/6034)) ([fd65a3d](https://github.com/tutur3u/platform/commit/fd65a3db6f7472771374192ab07f5a97c6fcaee8))
|
|
17
|
+
* **finance:** fence customer search to verified actor scope ([416d710](https://github.com/tutur3u/platform/commit/416d710961a509c06637ac2a93878f24ae0a03ae)) ([#6031](https://github.com/tutur3u/platform/issues/6031)) ([1d5a023](https://github.com/tutur3u/platform/commit/1d5a023d917ab75e19f575dec51a03be841763bc))
|
|
18
|
+
* **finance:** fence invoice create completion to admitted draft ([5b1cd6f](https://github.com/tutur3u/platform/commit/5b1cd6ff9e228002ceee9d2d347e7a2f5bf61b03)) ([#6023](https://github.com/tutur3u/platform/issues/6023)) ([2e7f99f](https://github.com/tutur3u/platform/commit/2e7f99ffd26d2e877e6e8567c3873bffa6bdcf45))
|
|
19
|
+
* **finance:** fence invoice PNG export lifetimes ([7d21df4](https://github.com/tutur3u/platform/commit/7d21df4d3301a388e6fa2823f68fb25d0c71d1c4)) ([#6038](https://github.com/tutur3u/platform/issues/6038)) ([4a5365a](https://github.com/tutur3u/platform/commit/4a5365ae1a6d28ef142bdc904695491475a740d3))
|
|
20
|
+
* **finance:** isolate customer groups by actor lifetime ([d20c9d5](https://github.com/tutur3u/platform/commit/d20c9d53d23e5ce0ad6c94199d202cd30e522694)) ([#6028](https://github.com/tutur3u/platform/issues/6028)) ([c8f712c](https://github.com/tutur3u/platform/commit/c8f712cfd590b29b2d271cbf6a75895142427a52))
|
|
21
|
+
* **finance:** isolate subscription context by actor lifetime ([55f09f2](https://github.com/tutur3u/platform/commit/55f09f2a86f1956cdad5ddd28bb01a2ae92a0dad)) ([#6024](https://github.com/tutur3u/platform/issues/6024)) ([42e96b9](https://github.com/tutur3u/platform/commit/42e96b93ce1a4ebad8c7fb877bfdecd7d0451bed))
|
|
22
|
+
* **finance:** keep invoice templates usable with invalid dates ([2d4bc6d](https://github.com/tutur3u/platform/commit/2d4bc6d9f3cf0b7a1377b71a59f8efb581e3170e)) ([#6040](https://github.com/tutur3u/platform/issues/6040)) ([c84bb36](https://github.com/tutur3u/platform/commit/c84bb36369c941b67be8ca9be62fe076d34e957a))
|
|
23
|
+
* **finance:** reconcile attendance summary with current main ([fb00e6e](https://github.com/tutur3u/platform/commit/fb00e6ef4373adc1b6b9107f9bba883c101c3438))
|
|
24
|
+
* **finance:** retain history with unrenderable dates ([95b8513](https://github.com/tutur3u/platform/commit/95b851336bedc480d3e10fba6cea1e3a2c047ff1)) ([#6035](https://github.com/tutur3u/platform/issues/6035)) ([ad0a0d3](https://github.com/tutur3u/platform/commit/ad0a0d3720f560cfe0a8baec1c2b30c1e77c273d))
|
|
25
|
+
* **finance:** scope invoice history to account lifetime ([7fb93ef](https://github.com/tutur3u/platform/commit/7fb93efe2d60f17be5a936067b3141b0330fa0ab)) ([#6033](https://github.com/tutur3u/platform/issues/6033)) ([9dc373a](https://github.com/tutur3u/platform/commit/9dc373abef6838d3fe9cb2c511559d883a63be42))
|
|
26
|
+
* **finance:** stabilize attendance calendar status summaries ([0c22684](https://github.com/tutur3u/platform/commit/0c22684245e101a79e2b3c37b7304e71427eccc8)) ([#6075](https://github.com/tutur3u/platform/issues/6075)) ([0789e90](https://github.com/tutur3u/platform/commit/0789e9091d7218ac6341a2843f43b554839da454))
|
|
27
|
+
* **finance:** tolerate denied invoice preference storage ([0b17e29](https://github.com/tutur3u/platform/commit/0b17e297eae859dac01016e002397cb3a4d0d0ac)) ([#6003](https://github.com/tutur3u/platform/issues/6003)) ([56fa042](https://github.com/tutur3u/platform/commit/56fa04277bad4e3b57cc87be66952cf8ae28c44c))
|
|
28
|
+
* **mobile:** integrate Assistant and Secrets shell coverage ([141ae72](https://github.com/tutur3u/platform/commit/141ae72cab1c5cb1604dfc654fdc5ff666dc0037))
|
|
29
|
+
|
|
30
|
+
## [0.38.0](https://github.com/tutur3u/platform/compare/ui-v0.37.2...ui-v0.38.0) (2026-10-04)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
* **calendar:** support full Google color choices and scoped edits ([#5661](https://github.com/tutur3u/platform/issues/5661)) ([8cafac5](https://github.com/tutur3u/platform/commit/8cafac53438b504eafa8bcc8213d18448e4a17bc))
|
|
36
|
+
* **education:** add personal to do destinations to Teach and Learn ([9587942](https://github.com/tutur3u/platform/commit/9587942deeebcb6aca3a774f4289178771bcabd6))
|
|
37
|
+
* **education:** add personal To do in Teach and Learn ([#5785](https://github.com/tutur3u/platform/issues/5785)) ([0a5e302](https://github.com/tutur3u/platform/commit/0a5e302fa9b09c140c717751005bbda23a1dc0b1))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Bug Fixes
|
|
41
|
+
|
|
42
|
+
* **calendar:** gate uncertain creation retries on durable capability ([4a0a088](https://github.com/tutur3u/platform/commit/4a0a0883ddfff464be0880afd13758908e65203f))
|
|
43
|
+
* **calendar:** recover edited drafts and align provider color parity ([d97142a](https://github.com/tutur3u/platform/commit/d97142abe22188b6a31a4a6b0e7b3a97d35d07f5))
|
|
44
|
+
* **finance:** authenticate exchange rates with satellite sessions ([33368f1](https://github.com/tutur3u/platform/commit/33368f1c00f86f71b4a66702899af262a90122e2)) ([#5760](https://github.com/tutur3u/platform/issues/5760)) ([759b1e5](https://github.com/tutur3u/platform/commit/759b1e52f8867aced2d64288af43b861d5ebccdc))
|
|
45
|
+
* **identity:** integrate main and order unapplied creator migrations ([5512e24](https://github.com/tutur3u/platform/commit/5512e2414abaa0c78b68cbdd3186a1d4bab861e7))
|
|
46
|
+
* **notifications:** preserve Colab polling policy ([ed09b9d](https://github.com/tutur3u/platform/commit/ed09b9ddaabd9b1865ca11e0459f23b0141137df))
|
|
47
|
+
* **ui:** close workspace popover without callback ([b0693b8](https://github.com/tutur3u/platform/commit/b0693b869721b9333d81d791554864291bec769d))
|
|
48
|
+
* **ui:** default browser workspace selectors to dropdowns ([9a8da02](https://github.com/tutur3u/platform/commit/9a8da02ceb7c03ee7eb05fb6cdc729144da6192a))
|
|
49
|
+
* **ui:** refine workspace dropdown recovery and viewport bounds ([06eeee7](https://github.com/tutur3u/platform/commit/06eeee77a26c1fc632907b72a5e0218d5d3fb49d))
|
|
50
|
+
* **web:** restore anchored workspace dropdown ([2df0661](https://github.com/tutur3u/platform/commit/2df0661f911eaec319cacca5d29242804d0e76ee))
|
|
51
|
+
* **web:** restore standard workspace dropdown ([#5739](https://github.com/tutur3u/platform/issues/5739)) ([f84ae74](https://github.com/tutur3u/platform/commit/f84ae741f2abab2a21aadfacda56bd7eba623466))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
### Performance Improvements
|
|
55
|
+
|
|
56
|
+
* **notifications:** reduce idle API polling ([32ee541](https://github.com/tutur3u/platform/commit/32ee5410e51d900698a06724b440b63df363fee9)) ([#5773](https://github.com/tutur3u/platform/issues/5773)) ([55089e0](https://github.com/tutur3u/platform/commit/55089e06ed6728f4bd6543787dcae48de3cfd734))
|
|
57
|
+
|
|
3
58
|
## [0.37.2](https://github.com/tutur3u/platform/compare/ui-v0.37.1...ui-v0.37.2) (2026-10-02)
|
|
4
59
|
|
|
5
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -87,13 +87,14 @@
|
|
|
87
87
|
"@tiptap/pm": "3.31.3",
|
|
88
88
|
"@tiptap/react": "3.31.3",
|
|
89
89
|
"@tiptap/starter-kit": "3.31.3",
|
|
90
|
-
"@tuturuuu/ai": "0.
|
|
90
|
+
"@tuturuuu/ai": "0.18.0",
|
|
91
91
|
"@tuturuuu/apis": "0.13.0",
|
|
92
92
|
"@tuturuuu/hooks": "0.1.2",
|
|
93
93
|
"@tuturuuu/icons": "0.1.1",
|
|
94
|
-
"@tuturuuu/internal-api": "0.
|
|
94
|
+
"@tuturuuu/internal-api": "0.50.0",
|
|
95
|
+
"@tuturuuu/realtime": "0.10.1",
|
|
95
96
|
"@tuturuuu/supabase": "0.5.2",
|
|
96
|
-
"@tuturuuu/utils": "0.
|
|
97
|
+
"@tuturuuu/utils": "0.35.0",
|
|
97
98
|
"@types/debug": "^4.1.13",
|
|
98
99
|
"browser-image-compression": "^2.0.2",
|
|
99
100
|
"class-variance-authority": "^0.7.1",
|
|
@@ -114,7 +115,7 @@
|
|
|
114
115
|
"lodash": "4.18.1",
|
|
115
116
|
"moment": "^2.30.1",
|
|
116
117
|
"motion": "^13.2.0",
|
|
117
|
-
"next": "^16.
|
|
118
|
+
"next": "^16.4.0",
|
|
118
119
|
"next-intl": "^4.14.4",
|
|
119
120
|
"next-themes": "^0.4.6",
|
|
120
121
|
"nuqs": "^2.10.1",
|
|
@@ -154,7 +155,7 @@
|
|
|
154
155
|
"@tanstack/react-table": "^9.2.4",
|
|
155
156
|
"@testing-library/jest-dom": "^7.0.1",
|
|
156
157
|
"@testing-library/react": "^16.3.3",
|
|
157
|
-
"@tuturuuu/types": "0.
|
|
158
|
+
"@tuturuuu/types": "0.34.0",
|
|
158
159
|
"@tuturuuu/typescript-config": "0.1.2",
|
|
159
160
|
"@types/html2canvas": "^1.0.0",
|
|
160
161
|
"@types/lodash": "^4.17.25",
|
|
@@ -179,6 +180,7 @@
|
|
|
179
180
|
"@tanstack/react-table": "^9.0.0"
|
|
180
181
|
},
|
|
181
182
|
"exports": {
|
|
183
|
+
"./custom/education-todo": "./src/components/ui/custom/education-todo.tsx",
|
|
182
184
|
"./globals.css": "./src/globals.css",
|
|
183
185
|
"./marketing": "./src/components/ui/marketing/index.ts",
|
|
184
186
|
"./postcss.config": "./postcss.config.mjs",
|
|
@@ -497,7 +499,8 @@
|
|
|
497
499
|
"./custom/satellite-notification-popover": "./src/components/ui/custom/satellite-notification-popover.tsx",
|
|
498
500
|
"./custom/notification-runtime": "./src/components/ui/custom/notification-runtime.tsx",
|
|
499
501
|
"./finance/invoices/query-invalidation": "./src/components/ui/finance/invoices/query-invalidation.ts",
|
|
500
|
-
"./finance/shared/charts/use-finance-confidential-visibility": "./src/components/ui/finance/shared/charts/use-finance-confidential-visibility.ts"
|
|
502
|
+
"./finance/shared/charts/use-finance-confidential-visibility": "./src/components/ui/finance/shared/charts/use-finance-confidential-visibility.ts",
|
|
503
|
+
"./text-editor/collaboration-carets": "./src/components/ui/text-editor/collaboration-carets.ts"
|
|
501
504
|
},
|
|
502
505
|
"packageManager": "bun@1.4.1"
|
|
503
506
|
}
|
|
@@ -3,6 +3,7 @@ import type { InternalApiWorkspaceSummary } from '@tuturuuu/types';
|
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import type { useWorkspaceVisibility } from '../../../../hooks/use-workspace-visibility';
|
|
5
5
|
import { Dialog } from '../../dialog';
|
|
6
|
+
import { Popover, PopoverTrigger } from '../../popover';
|
|
6
7
|
import { WorkspacePickerContent } from '../workspace-picker-content';
|
|
7
8
|
|
|
8
9
|
vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key }));
|
|
@@ -179,3 +180,88 @@ describe('workspace sheet rendered identities', () => {
|
|
|
179
180
|
);
|
|
180
181
|
});
|
|
181
182
|
});
|
|
183
|
+
|
|
184
|
+
describe('workspace browser dropdown', () => {
|
|
185
|
+
it('keeps search and Hidden recovery in an anchored scrolling dropdown', async () => {
|
|
186
|
+
const onSelect = vi.fn();
|
|
187
|
+
const onDefault = vi.fn();
|
|
188
|
+
const onCreate = vi.fn();
|
|
189
|
+
const state = visibility({ hiddenIds: [other.id] });
|
|
190
|
+
render(
|
|
191
|
+
<Popover defaultOpen>
|
|
192
|
+
<PopoverTrigger>Choose workspace</PopoverTrigger>
|
|
193
|
+
<WorkspacePickerContent
|
|
194
|
+
presentation="dropdown"
|
|
195
|
+
workspaces={[one, other]}
|
|
196
|
+
visibility={state}
|
|
197
|
+
onSelect={onSelect}
|
|
198
|
+
onDefault={onDefault}
|
|
199
|
+
onCreate={onCreate}
|
|
200
|
+
/>
|
|
201
|
+
</Popover>
|
|
202
|
+
);
|
|
203
|
+
const dropdown = screen.getByRole('dialog', { name: 'workspaces' });
|
|
204
|
+
expect(dropdown).toHaveAttribute('data-slot', 'popover-content');
|
|
205
|
+
expect(dropdown).not.toHaveClass('inset-0');
|
|
206
|
+
expect(dropdown).not.toHaveClass('h-dvh');
|
|
207
|
+
expect(dropdown).toHaveClass(
|
|
208
|
+
'max-h-[min(32rem,var(--radix-popover-content-available-height))]'
|
|
209
|
+
);
|
|
210
|
+
expect(dropdown).toHaveClass('overflow-hidden', 'flex-col');
|
|
211
|
+
const input = screen.getByRole('textbox', { name: 'search_workspace' });
|
|
212
|
+
const row = screen.getByRole('button', { name: 'Repeated name PRO' });
|
|
213
|
+
expect(row.closest('.overflow-y-auto')).not.toBeNull();
|
|
214
|
+
expect(screen.queryByText('Another workspace')).toBeNull();
|
|
215
|
+
fireEvent.click(row);
|
|
216
|
+
expect(onSelect).toHaveBeenCalledWith(one);
|
|
217
|
+
fireEvent.click(screen.getByRole('button', { name: 'default_workspace' }));
|
|
218
|
+
expect(onDefault).toHaveBeenCalledWith(one.id);
|
|
219
|
+
const create = screen.getByRole('button', {
|
|
220
|
+
name: 'create_workspace_action',
|
|
221
|
+
});
|
|
222
|
+
const createFooter = create.parentElement;
|
|
223
|
+
expect(createFooter).not.toHaveClass('absolute');
|
|
224
|
+
fireEvent.click(create);
|
|
225
|
+
expect(onCreate).toHaveBeenCalledOnce();
|
|
226
|
+
fireEvent.change(input, { target: { value: 'missing' } });
|
|
227
|
+
expect(
|
|
228
|
+
screen.queryByRole('button', { name: 'Repeated name PRO' })
|
|
229
|
+
).toBeNull();
|
|
230
|
+
fireEvent.click(screen.getByRole('button', { name: 'hidden_workspaces' }));
|
|
231
|
+
expect(input).toHaveValue('');
|
|
232
|
+
expect(dropdown).toHaveAccessibleName('hidden_workspaces');
|
|
233
|
+
expect(createFooter).toHaveClass('hidden');
|
|
234
|
+
fireEvent.click(
|
|
235
|
+
screen.getByRole('button', {
|
|
236
|
+
name: 'restore_workspace: Another workspace',
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
expect(state.setHidden).toHaveBeenCalledWith(other.id, false);
|
|
240
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
241
|
+
fireEvent.keyDown(dropdown, { key: 'Escape' });
|
|
242
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it.each([false, true])(
|
|
247
|
+
'closes its popover with an optional callback: %s',
|
|
248
|
+
async (withCallback) => {
|
|
249
|
+
const onClose = vi.fn();
|
|
250
|
+
render(
|
|
251
|
+
<Popover defaultOpen>
|
|
252
|
+
<PopoverTrigger>Choose workspace</PopoverTrigger>
|
|
253
|
+
<WorkspacePickerContent
|
|
254
|
+
presentation="dropdown"
|
|
255
|
+
workspaces={[one]}
|
|
256
|
+
visibility={visibility()}
|
|
257
|
+
onSelect={vi.fn()}
|
|
258
|
+
onDefault={vi.fn()}
|
|
259
|
+
onClose={withCallback ? onClose : undefined}
|
|
260
|
+
/>
|
|
261
|
+
</Popover>
|
|
262
|
+
);
|
|
263
|
+
fireEvent.click(screen.getByRole('button', { name: 'close' }));
|
|
264
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
265
|
+
expect(onClose).toHaveBeenCalledTimes(withCallback ? 1 : 0);
|
|
266
|
+
}
|
|
267
|
+
);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { EducationTodo } from './education-todo';
|
|
4
|
+
|
|
5
|
+
const state = vi.hoisted(() => ({
|
|
6
|
+
query: vi.fn(),
|
|
7
|
+
refetch: vi.fn(),
|
|
8
|
+
error: false,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('@tanstack/react-query', () => ({
|
|
11
|
+
useQuery: (options: unknown) => {
|
|
12
|
+
state.query(options);
|
|
13
|
+
return {
|
|
14
|
+
isError: state.error,
|
|
15
|
+
isPending: false,
|
|
16
|
+
refetch: state.refetch,
|
|
17
|
+
data: {
|
|
18
|
+
count: 30,
|
|
19
|
+
totalPages: 2,
|
|
20
|
+
data: [
|
|
21
|
+
{
|
|
22
|
+
id: 'done',
|
|
23
|
+
title: 'Completed synthetic work',
|
|
24
|
+
courseId: 'course',
|
|
25
|
+
completed: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'open',
|
|
29
|
+
title: 'Open synthetic work',
|
|
30
|
+
courseId: 'course',
|
|
31
|
+
completed: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
vi.mock('@tuturuuu/internal-api/teach', () => ({ listEducationTodo: vi.fn() }));
|
|
39
|
+
const translate = (key: string) => key;
|
|
40
|
+
describe('personal education work controls', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.clearAllMocks();
|
|
43
|
+
state.error = false;
|
|
44
|
+
});
|
|
45
|
+
it('separates actor caches, paginates and resets page on category switch', () => {
|
|
46
|
+
const { rerender } = render(
|
|
47
|
+
<EducationTodo
|
|
48
|
+
app="learn"
|
|
49
|
+
wsId="workspace"
|
|
50
|
+
actorId="actor-one"
|
|
51
|
+
t={translate}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
expect(state.query).toHaveBeenLastCalledWith(
|
|
55
|
+
expect.objectContaining({
|
|
56
|
+
queryKey: [
|
|
57
|
+
'education-todo',
|
|
58
|
+
'learn',
|
|
59
|
+
'workspace',
|
|
60
|
+
'actor-one',
|
|
61
|
+
'tutoring',
|
|
62
|
+
1,
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
fireEvent.click(screen.getByRole('button', { name: 'next' }));
|
|
67
|
+
expect(state.query).toHaveBeenLastCalledWith(
|
|
68
|
+
expect.objectContaining({
|
|
69
|
+
queryKey: [
|
|
70
|
+
'education-todo',
|
|
71
|
+
'learn',
|
|
72
|
+
'workspace',
|
|
73
|
+
'actor-one',
|
|
74
|
+
'tutoring',
|
|
75
|
+
2,
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
fireEvent.click(screen.getByRole('button', { name: 'assignments' }));
|
|
80
|
+
expect(screen.queryByText('Completed synthetic work')).toBeNull();
|
|
81
|
+
expect(screen.getByText('Open synthetic work')).toBeTruthy();
|
|
82
|
+
expect(state.query).toHaveBeenLastCalledWith(
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
queryKey: [
|
|
85
|
+
'education-todo',
|
|
86
|
+
'learn',
|
|
87
|
+
'workspace',
|
|
88
|
+
'actor-one',
|
|
89
|
+
'assignments',
|
|
90
|
+
1,
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
fireEvent.click(screen.getByRole('checkbox'));
|
|
95
|
+
expect(screen.getByText('Completed synthetic work')).toBeTruthy();
|
|
96
|
+
rerender(
|
|
97
|
+
<EducationTodo
|
|
98
|
+
app="learn"
|
|
99
|
+
wsId="workspace"
|
|
100
|
+
actorId="actor-two"
|
|
101
|
+
t={translate}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
expect(state.query).toHaveBeenLastCalledWith(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
queryKey: [
|
|
107
|
+
'education-todo',
|
|
108
|
+
'learn',
|
|
109
|
+
'workspace',
|
|
110
|
+
'actor-two',
|
|
111
|
+
'assignments',
|
|
112
|
+
1,
|
|
113
|
+
],
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
it('offers retry on errors without rendering stale assigned rows', () => {
|
|
118
|
+
state.error = true;
|
|
119
|
+
render(
|
|
120
|
+
<EducationTodo
|
|
121
|
+
app="teach"
|
|
122
|
+
wsId="workspace"
|
|
123
|
+
actorId="actor"
|
|
124
|
+
t={translate}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
expect(screen.queryByText('Open synthetic work')).toBeNull();
|
|
128
|
+
fireEvent.click(screen.getByRole('button', { name: 'retry' }));
|
|
129
|
+
expect(state.refetch).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
4
|
+
import {
|
|
5
|
+
BookOpen,
|
|
6
|
+
CalendarCheck,
|
|
7
|
+
CheckCheck,
|
|
8
|
+
ClipboardList,
|
|
9
|
+
} from '@tuturuuu/icons';
|
|
10
|
+
import {
|
|
11
|
+
type EducationTodoItem,
|
|
12
|
+
type EducationTodoKind,
|
|
13
|
+
listEducationTodo,
|
|
14
|
+
} from '@tuturuuu/internal-api/teach';
|
|
15
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
16
|
+
import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
17
|
+
import Link from 'next/link';
|
|
18
|
+
import { useState } from 'react';
|
|
19
|
+
|
|
20
|
+
const kinds = ['tutoring', 'assignments', 'lessons', 'tests'] as const;
|
|
21
|
+
const icons = {
|
|
22
|
+
tutoring: CalendarCheck,
|
|
23
|
+
assignments: ClipboardList,
|
|
24
|
+
lessons: BookOpen,
|
|
25
|
+
tests: ClipboardList,
|
|
26
|
+
};
|
|
27
|
+
function itemHref(
|
|
28
|
+
app: 'learn' | 'teach',
|
|
29
|
+
wsId: string,
|
|
30
|
+
kind: EducationTodoKind,
|
|
31
|
+
row: EducationTodoItem
|
|
32
|
+
) {
|
|
33
|
+
if (kind === 'tutoring' || !row.courseId) return null;
|
|
34
|
+
if (kind === 'tests')
|
|
35
|
+
return `/${wsId}/courses/${row.courseId}${app === 'learn' ? `/tests/${row.id}` : ''}`;
|
|
36
|
+
if (kind === 'assignments')
|
|
37
|
+
return `/${wsId}/assignments${app === 'teach' ? `?course=${row.courseId}` : ''}`;
|
|
38
|
+
return `/${wsId}/courses/${row.courseId}${app === 'teach' ? `/${row.id}` : ''}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function EducationTodo({
|
|
42
|
+
app,
|
|
43
|
+
wsId,
|
|
44
|
+
actorId,
|
|
45
|
+
t,
|
|
46
|
+
}: {
|
|
47
|
+
app: 'learn' | 'teach';
|
|
48
|
+
wsId: string;
|
|
49
|
+
actorId: string;
|
|
50
|
+
t: (key: string, values?: Record<string, string | number>) => string;
|
|
51
|
+
}) {
|
|
52
|
+
const [kind, setKind] = useState<EducationTodoKind>('tutoring');
|
|
53
|
+
const [page, setPage] = useState(1);
|
|
54
|
+
const [showCompleted, setShowCompleted] = useState(false);
|
|
55
|
+
const query = useQuery({
|
|
56
|
+
queryKey: ['education-todo', app, wsId, actorId, kind, page],
|
|
57
|
+
queryFn: () => listEducationTodo(app, wsId, { kind, page }),
|
|
58
|
+
});
|
|
59
|
+
const rows = (query.data?.data ?? []).filter(
|
|
60
|
+
(row) => showCompleted || !row.completed
|
|
61
|
+
);
|
|
62
|
+
return (
|
|
63
|
+
<main className="mx-auto max-w-5xl space-y-5 p-5 md:p-8">
|
|
64
|
+
<header className="space-y-2">
|
|
65
|
+
<h1 className="font-semibold text-3xl">{t('title')}</h1>
|
|
66
|
+
<p className="max-w-2xl text-muted-foreground">
|
|
67
|
+
{t(app === 'learn' ? 'learnDescription' : 'teachDescription')}
|
|
68
|
+
</p>
|
|
69
|
+
</header>
|
|
70
|
+
<nav aria-label={t('categories')} className="flex flex-wrap gap-2">
|
|
71
|
+
{kinds.map((value) => {
|
|
72
|
+
const Icon = icons[value];
|
|
73
|
+
return (
|
|
74
|
+
<Button
|
|
75
|
+
key={value}
|
|
76
|
+
variant={kind === value ? 'default' : 'outline'}
|
|
77
|
+
aria-pressed={kind === value}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
setKind(value);
|
|
80
|
+
setPage(1);
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Icon className="size-4" />
|
|
84
|
+
{t(value)}
|
|
85
|
+
</Button>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</nav>
|
|
89
|
+
{kind !== 'tutoring' && app === 'learn' ? (
|
|
90
|
+
<label className="flex items-center gap-2 text-sm">
|
|
91
|
+
<Checkbox
|
|
92
|
+
checked={showCompleted}
|
|
93
|
+
onCheckedChange={(value) => setShowCompleted(value === true)}
|
|
94
|
+
/>
|
|
95
|
+
{t('showCompleted')}
|
|
96
|
+
</label>
|
|
97
|
+
) : null}
|
|
98
|
+
{query.isError ? (
|
|
99
|
+
<div role="alert" className="space-y-3 rounded-xl border p-5">
|
|
100
|
+
<p>{t('error')}</p>
|
|
101
|
+
<Button onClick={() => void query.refetch()} variant="outline">
|
|
102
|
+
{t('retry')}
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
) : query.isPending ? (
|
|
106
|
+
<p role="status">{t('loading')}</p>
|
|
107
|
+
) : (
|
|
108
|
+
<>
|
|
109
|
+
<p className="text-muted-foreground text-sm">
|
|
110
|
+
{t(app === 'learn' ? 'assignedCount' : 'pendingCount', {
|
|
111
|
+
count: query.data?.count ?? 0,
|
|
112
|
+
})}
|
|
113
|
+
</p>
|
|
114
|
+
<ul className="space-y-3">
|
|
115
|
+
{rows.map((row) => {
|
|
116
|
+
const href = itemHref(app, wsId, kind, row);
|
|
117
|
+
return (
|
|
118
|
+
<li
|
|
119
|
+
key={row.id}
|
|
120
|
+
className="flex flex-wrap items-center justify-between gap-4 rounded-xl border bg-card p-4"
|
|
121
|
+
>
|
|
122
|
+
<div className="min-w-0 space-y-1">
|
|
123
|
+
<h2 className="line-clamp-3 break-words font-medium">
|
|
124
|
+
{row.title ||
|
|
125
|
+
t(kind === 'tutoring' ? 'tutoring' : 'untitled')}
|
|
126
|
+
</h2>
|
|
127
|
+
{row.participantName ? (
|
|
128
|
+
<p className="text-muted-foreground text-sm">
|
|
129
|
+
{t(app === 'teach' ? 'student' : 'teacher')}:{' '}
|
|
130
|
+
{row.participantName}
|
|
131
|
+
</p>
|
|
132
|
+
) : null}
|
|
133
|
+
{row.date ? (
|
|
134
|
+
<p className="text-muted-foreground text-sm">
|
|
135
|
+
{row.date}
|
|
136
|
+
{row.startTime ? ` · ${row.startTime.slice(0, 5)}` : ''}
|
|
137
|
+
{row.durationMinutes
|
|
138
|
+
? ` · ${t('minutes', { count: row.durationMinutes })}`
|
|
139
|
+
: ''}
|
|
140
|
+
</p>
|
|
141
|
+
) : null}
|
|
142
|
+
{row.completed ? (
|
|
143
|
+
<p className="flex items-center gap-1 text-muted-foreground text-sm">
|
|
144
|
+
<CheckCheck className="size-4" />
|
|
145
|
+
{t('completed')}
|
|
146
|
+
</p>
|
|
147
|
+
) : null}
|
|
148
|
+
</div>
|
|
149
|
+
{href ? (
|
|
150
|
+
<Button asChild size="sm" variant="outline">
|
|
151
|
+
<Link href={href}>{t('open')}</Link>
|
|
152
|
+
</Button>
|
|
153
|
+
) : null}
|
|
154
|
+
</li>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
</ul>
|
|
158
|
+
{!rows.length ? (
|
|
159
|
+
<p className="rounded-xl border border-dashed p-6 text-muted-foreground">
|
|
160
|
+
{t((query.data?.count ?? 0) ? 'emptyPage' : 'empty')}
|
|
161
|
+
</p>
|
|
162
|
+
) : null}
|
|
163
|
+
{(query.data?.totalPages ?? 0) > 1 ? (
|
|
164
|
+
<nav
|
|
165
|
+
aria-label={t('pages')}
|
|
166
|
+
className="flex items-center justify-between gap-3"
|
|
167
|
+
>
|
|
168
|
+
<Button
|
|
169
|
+
disabled={page === 1}
|
|
170
|
+
onClick={() => setPage((value) => value - 1)}
|
|
171
|
+
variant="outline"
|
|
172
|
+
>
|
|
173
|
+
{t('previous')}
|
|
174
|
+
</Button>
|
|
175
|
+
<span className="text-sm">
|
|
176
|
+
{t('page', { page, total: query.data?.totalPages ?? 0 })}
|
|
177
|
+
</span>
|
|
178
|
+
<Button
|
|
179
|
+
disabled={page >= (query.data?.totalPages ?? 0)}
|
|
180
|
+
onClick={() => setPage((value) => value + 1)}
|
|
181
|
+
variant="outline"
|
|
182
|
+
>
|
|
183
|
+
{t('next')}
|
|
184
|
+
</Button>
|
|
185
|
+
</nav>
|
|
186
|
+
) : null}
|
|
187
|
+
</>
|
|
188
|
+
)}
|
|
189
|
+
</main>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { cleanup, render } from '@testing-library/react';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { NotificationRuntime } from './notification-runtime';
|
|
5
|
+
import SatelliteNotificationPopover from './satellite-notification-popover';
|
|
6
|
+
|
|
7
|
+
const { unread, list } = vi.hoisted(() => ({ unread: vi.fn(), list: vi.fn() }));
|
|
8
|
+
vi.mock('@tuturuuu/ui/hooks/use-notifications', () => ({
|
|
9
|
+
useUnreadCount: unread,
|
|
10
|
+
useInfiniteNotifications: list,
|
|
11
|
+
useNotificationSubscription: vi.fn(),
|
|
12
|
+
useMarkAllAsRead: () => ({ mutate: vi.fn(), isPending: false }),
|
|
13
|
+
useUpdateNotification: () => ({ mutate: vi.fn(), isPending: false }),
|
|
14
|
+
dedupeNotifications: () => [],
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('./notification-list', () => ({ NotificationList: () => null }));
|
|
17
|
+
describe('satellite freshness policy', () => {
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
cleanup();
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
it.each([
|
|
23
|
+
[undefined, 120_000],
|
|
24
|
+
[30_000, 30_000],
|
|
25
|
+
])(
|
|
26
|
+
'uses the default or an explicit app polling interval',
|
|
27
|
+
(pollIntervalMs, expected) => {
|
|
28
|
+
unread.mockReturnValue({ data: 0 });
|
|
29
|
+
list.mockReturnValue({ data: { pages: [] }, isLoading: false });
|
|
30
|
+
render(
|
|
31
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
32
|
+
<NotificationRuntime
|
|
33
|
+
value={{
|
|
34
|
+
params: { wsId: 'personal' },
|
|
35
|
+
pathname: '/',
|
|
36
|
+
router: { push: vi.fn(), refresh: vi.fn() },
|
|
37
|
+
Link: ({ children }) => <span>{children}</span>,
|
|
38
|
+
realtime: false,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<SatelliteNotificationPopover
|
|
42
|
+
userId="actor"
|
|
43
|
+
pollIntervalMs={pollIntervalMs}
|
|
44
|
+
noNotificationsText="Empty"
|
|
45
|
+
notificationsText="Notifications"
|
|
46
|
+
viewAllText="View all"
|
|
47
|
+
markAsReadText="Read"
|
|
48
|
+
markAsUnreadText="Unread"
|
|
49
|
+
/>
|
|
50
|
+
</NotificationRuntime>
|
|
51
|
+
</QueryClientProvider>
|
|
52
|
+
);
|
|
53
|
+
expect(unread.mock.calls[0]?.[1].refetchInterval).toBe(expected);
|
|
54
|
+
expect(
|
|
55
|
+
list.mock.calls.every(
|
|
56
|
+
([options]) => options.refetchInterval === expected
|
|
57
|
+
)
|
|
58
|
+
).toBe(true);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
@@ -22,10 +22,13 @@ import { NotificationPopoverTriggerButton } from './notification-popover-trigger
|
|
|
22
22
|
import { useNotificationRuntime } from './notification-runtime';
|
|
23
23
|
|
|
24
24
|
dayjs.extend(relativeTime);
|
|
25
|
+
const NOTIFICATION_POLL_INTERVAL_MS = 2 * 60_000;
|
|
25
26
|
type TabType = 'inbox' | 'archive';
|
|
26
27
|
|
|
27
28
|
export interface NotificationPopoverClientProps {
|
|
28
29
|
userId?: string;
|
|
30
|
+
/** Optional app-specific freshness policy for non-subscription consumers. */
|
|
31
|
+
pollIntervalMs?: number;
|
|
29
32
|
noNotificationsText: string;
|
|
30
33
|
notificationsText: string;
|
|
31
34
|
viewAllText: string;
|
|
@@ -63,6 +66,7 @@ function isValidWorkspaceFilterId(value: unknown): value is string {
|
|
|
63
66
|
|
|
64
67
|
export default function SatelliteNotificationPopover({
|
|
65
68
|
userId,
|
|
69
|
+
pollIntervalMs = NOTIFICATION_POLL_INTERVAL_MS,
|
|
66
70
|
noNotificationsText,
|
|
67
71
|
notificationsText,
|
|
68
72
|
viewAllText,
|
|
@@ -98,7 +102,7 @@ export default function SatelliteNotificationPopover({
|
|
|
98
102
|
const { data: unreadCount = 0 } = useUnreadCount(wsIdForFiltering, {
|
|
99
103
|
cacheScope: userId,
|
|
100
104
|
enabled: Boolean(userId),
|
|
101
|
-
refetchInterval: realtime ? undefined :
|
|
105
|
+
refetchInterval: realtime ? undefined : pollIntervalMs,
|
|
102
106
|
});
|
|
103
107
|
|
|
104
108
|
// Infinite scroll for inbox (unread) and archive (read)
|
|
@@ -107,7 +111,7 @@ export default function SatelliteNotificationPopover({
|
|
|
107
111
|
wsId: wsIdForFiltering,
|
|
108
112
|
unreadOnly: true,
|
|
109
113
|
pageSize: 15,
|
|
110
|
-
refetchInterval: realtime ? undefined :
|
|
114
|
+
refetchInterval: realtime ? undefined : pollIntervalMs,
|
|
111
115
|
enabled: Boolean(userId) && open && activeTab === 'inbox',
|
|
112
116
|
});
|
|
113
117
|
|
|
@@ -116,7 +120,7 @@ export default function SatelliteNotificationPopover({
|
|
|
116
120
|
wsId: wsIdForFiltering,
|
|
117
121
|
readOnly: true,
|
|
118
122
|
pageSize: 15,
|
|
119
|
-
refetchInterval: realtime ? undefined :
|
|
123
|
+
refetchInterval: realtime ? undefined : pollIntervalMs,
|
|
120
124
|
enabled: Boolean(userId) && open && activeTab === 'archive',
|
|
121
125
|
});
|
|
122
126
|
|