@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/package.json +10 -7
  3. package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +86 -0
  4. package/src/components/ui/custom/education-todo.test.tsx +131 -0
  5. package/src/components/ui/custom/education-todo.tsx +191 -0
  6. package/src/components/ui/custom/satellite-notification-polling.test.tsx +61 -0
  7. package/src/components/ui/custom/satellite-notification-popover.tsx +7 -3
  8. package/src/components/ui/custom/workspace-picker-content.tsx +93 -30
  9. package/src/components/ui/custom/workspace-select.tsx +12 -4
  10. package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
  11. package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
  12. package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
  13. package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
  14. package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
  15. package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
  16. package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
  17. package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
  18. package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
  19. package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
  20. package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
  21. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
  22. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
  23. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
  24. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  25. package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
  26. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
  27. package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
  28. package/src/components/ui/finance/invoices/hooks.ts +3 -100
  29. package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
  30. package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
  31. package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
  32. package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
  33. package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
  34. package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
  35. package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
  36. package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +438 -0
  37. package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
  38. package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
  39. package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
  40. package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
  41. package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
  42. package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
  43. package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
  44. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
  46. package/src/components/ui/finance/invoices/types.ts +3 -3
  47. package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
  48. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
  49. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
  50. package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
  51. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
  52. package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
  53. package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
  54. package/src/components/ui/legacy/calendar/calendar-draft-recovery.ts +34 -0
  55. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
  56. package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
  57. package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
  58. package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
  59. package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1614
  60. package/src/components/ui/legacy/calendar/event-save-payload.test.ts +40 -0
  61. package/src/components/ui/legacy/calendar/event-save-payload.ts +79 -0
  62. package/src/components/ui/legacy/calendar/event-source-selection.test.ts +95 -0
  63. package/src/components/ui/legacy/calendar/event-source-selection.ts +78 -0
  64. package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
  65. package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
  66. package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
  67. package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
  68. package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
  69. package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
  70. package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
  71. package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
  72. package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
  73. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
  74. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
  75. package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
  76. package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
  77. package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
  78. package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
  79. package/src/components/ui/legacy/calendar/provider-color-picker.test.tsx +147 -0
  80. package/src/components/ui/legacy/calendar/provider-color-picker.tsx +115 -0
  81. package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
  82. package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
  83. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +35 -1
  84. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +23 -11
  85. package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +119 -0
  86. package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +141 -0
  87. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
  88. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
  89. package/src/components/ui/popover.tsx +3 -1
  90. package/src/components/ui/text-editor/__tests__/collaboration-carets.test.ts +142 -0
  91. package/src/components/ui/text-editor/__tests__/markdown-document.test.ts +50 -0
  92. package/src/components/ui/text-editor/clipboard-serialization.ts +10 -0
  93. package/src/components/ui/text-editor/collaboration-carets.ts +56 -0
  94. package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +2 -0
  95. package/src/components/ui/text-editor/editor.tsx +11 -3
  96. package/src/components/ui/text-editor/extensions.ts +8 -2
  97. package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
  98. package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
  99. package/src/components/ui/text-editor/markdown-paste-extension.ts +1 -1
  100. package/src/components/ui/text-editor/markdown.ts +11 -0
  101. package/src/components/ui/text-editor/tool-bar.tsx +45 -102
  102. package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
  103. package/src/hooks/__tests__/collaboration-awareness.test.ts +106 -0
  104. package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
  105. package/src/hooks/__tests__/supabase-provider.test.ts +110 -7
  106. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +39 -2
  107. package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
  108. package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
  109. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +31 -1
  110. package/src/hooks/__tests__/use-workspace-presence.test.tsx +3 -3
  111. package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +147 -0
  112. package/src/hooks/calendar-connection-types.ts +49 -0
  113. package/src/hooks/calendar-creation-request.test.ts +62 -0
  114. package/src/hooks/calendar-creation-request.ts +34 -0
  115. package/src/hooks/calendar-event-update-queue-types.ts +12 -0
  116. package/src/hooks/calendar-event-write-payload.ts +57 -0
  117. package/src/hooks/collaboration-document-hydration.ts +35 -0
  118. package/src/hooks/notification-subscription.ts +5 -2
  119. package/src/hooks/supabase-provider.ts +76 -112
  120. package/src/hooks/use-calendar-sync.tsx +41 -62
  121. package/src/hooks/use-calendar.tsx +44 -82
  122. package/src/hooks/use-exchange-rates.ts +4 -15
  123. package/src/hooks/use-local-storage.ts +9 -11
  124. package/src/hooks/use-native-calendar-occurrences.ts +85 -0
  125. package/src/hooks/use-workspace-presence.ts +9 -9
  126. 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.37.2",
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.16.1",
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.48.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.33.3",
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.3.5",
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.33.1",
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 : 30_000,
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 : 30_000,
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 : 30_000,
123
+ refetchInterval: realtime ? undefined : pollIntervalMs,
120
124
  enabled: Boolean(userId) && open && activeTab === 'archive',
121
125
  });
122
126