From 2446629d3cfdddb8533865ae3349d15054685cbf Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Mon, 26 Jan 2026 22:19:54 +0700 Subject: [PATCH 01/67] Update changelog (#10439) Signed-off-by: Artem Savchenko --- changelog.md | 1055 +++++++++++--------------------------------------- 1 file changed, 230 insertions(+), 825 deletions(-) diff --git a/changelog.md b/changelog.md index fbcf7e6641..8c1ffaf166 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,21 @@ Changelog. +## [0.7.353] - 2026-01-24 + +* 🚀 FEATURES: · Link preview service ([#10424](https://github.com/hcengineering/platform/issues/10424)) · Add confirmation modal for table refresh ([#10428](https://github.com/hcengineering/platform/issues/10428)) · Guest update profile (avatar, name etc) ([#10429](https://github.com/hcengineering/platform/issues/10429)) +* 🐛 BUG FIXES: · Size predicate null handle ([#10417](https://github.com/hcengineering/platform/issues/10417)) · Long title display ([#10418](https://github.com/hcengineering/platform/issues/10418)) · Sort qms templates in wizard ([#10419](https://github.com/hcengineering/platform/issues/10419)) · Original table layout ([#10427](https://github.com/hcengineering/platform/issues/10427)) · Missing collaborative content in export ([#10432](https://github.com/hcengineering/platform/issues/10432)) · Login if current location equal to target ([#10436](https://github.com/hcengineering/platform/issues/10436)) · Add more mail ervice traces ([#10438](https://github.com/hcengineering/platform/issues/10438)) · Source table columns ([#10437](https://github.com/hcengineering/platform/issues/10437)) + +## [0.7.350] - 2026-01-18 + +* 🐛 BUG FIXES: · Association attribute in markdown table ([#10398](https://github.com/hcengineering/platform/issues/10398)) · Card export ([#10403](https://github.com/hcengineering/platform/issues/10403)) · Card view settings ([#10404](https://github.com/hcengineering/platform/issues/10404)) · Customize support links ([#10405](https://github.com/hcengineering/platform/issues/10405)) · Controlled doc sequence conflicts ([#10406](https://github.com/hcengineering/platform/issues/10406)) · Default null value ([#10409](https://github.com/hcengineering/platform/issues/10409)) · View setting freeze ([#10410](https://github.com/hcengineering/platform/issues/10410)) · Show custom icons for cards breadcrumbs ([#10412](https://github.com/hcengineering/platform/issues/10412)) · Filter relations by workspaceId ([#10414](https://github.com/hcengineering/platform/issues/10414)) +* ⚙️ MISCELLANEOUS TASKS: · Core and server ([#10400](https://github.com/hcengineering/platform/issues/10400)) · Beta marks for cards and processes ([#10408](https://github.com/hcengineering/platform/issues/10408)) · Actions/upload-artifact from 4 to 6 ([#10300](https://github.com/hcengineering/platform/issues/10300)) + +## [0.7.344] - 2026-01-12 + +* 🚀 FEATURES: · Add type/tag permissions ([#10384](https://github.com/hcengineering/platform/issues/10384)) +* 🐛 BUG FIXES: · Sort controlled documents by rank and title ([#10383](https://github.com/hcengineering/platform/issues/10383)) · Card type export ([#10385](https://github.com/hcengineering/platform/issues/10385)) · Old card UI ([#10386](https://github.com/hcengineering/platform/issues/10386)) · Exporter ancestor order ([#10388](https://github.com/hcengineering/platform/issues/10388)) · Card activity ([#10393](https://github.com/hcengineering/platform/issues/10393)) · Missing events in planner ([#10395](https://github.com/hcengineering/platform/issues/10395)) +* ⚙️ MISCELLANEOUS TASKS: · Changelog · Changelog · Changelog ([#10382](https://github.com/hcengineering/platform/issues/10382)) · Postgres package version ([#10394](https://github.com/hcengineering/platform/issues/10394)) ## [0.7.342] - 2026-01-10 @@ -70,15 +85,10 @@ Changelog. * EQMS-1402: · Limit password login max attempts ([#10184](https://github.com/hcengineering/platform/issues/10184)) * EQMS-1650: · External approvers ([#9987](https://github.com/hcengineering/platform/issues/9987)) · External approvers fixes ([#10181](https://github.com/hcengineering/platform/issues/10181)) * FIX: · Reduced theme blinking on desktop app start (windows). ([#10119](https://github.com/hcengineering/platform/issues/10119)) - -## [s0.7.287] - 2025-10-20 - -* 🚀 FEATURES: · Allow to customize typing ([#10111](https://github.com/hcengineering/platform/issues/10111)) -* 🐛 BUG FIXES: · Editor table performance improvements ([#10106](https://github.com/hcengineering/platform/issues/10106)) -* QFIX: · Show billing in settings ([#10116](https://github.com/hcengineering/platform/issues/10116)) - -## [s0.7.286] - 2025-10-16 - +* OFFICE: · Use pulse for invites and join requests ([#9941](https://github.com/hcengineering/platform/issues/9941)) · Forbid anonymous invites & join requests ([#10065](https://github.com/hcengineering/platform/issues/10065)) +* QFIX: · Account client type ([#9988](https://github.com/hcengineering/platform/issues/9988)) · Use css var for calendar zone bg color ([#10013](https://github.com/hcengineering/platform/issues/10013)) · Billing livekit errors ([#10015](https://github.com/hcengineering/platform/issues/10015)) · Move user profile to default card space ([#10029](https://github.com/hcengineering/platform/issues/10029)) · Ai assistant desktop ([#10031](https://github.com/hcengineering/platform/issues/10031)) · Delete milestone ([#10043](https://github.com/hcengineering/platform/issues/10043)) · Add fulltext dlq ([#10046](https://github.com/hcengineering/platform/issues/10046)) · Add ai-assitant to list of allowed integration services ([#10052](https://github.com/hcengineering/platform/issues/10052)) · Double approval of review/approve request ([#10199](https://github.com/hcengineering/platform/issues/10199)) +* UBERF-13844,13845: · Edit card type UI improvements ([#10009](https://github.com/hcengineering/platform/issues/10009)) +* UBERF-13864: · Fix social id presenter ([#10035](https://github.com/hcengineering/platform/issues/10035)) * UBERF-13891: · Global user profile ([#10102](https://github.com/hcengineering/platform/issues/10102)) * QFIX: · Add user/socialId/workspace into rpc call traces. ([#9983](https://github.com/hcengineering/platform/issues/9983)) · Use measurements from published packages ([#9984](https://github.com/hcengineering/platform/issues/9984)) · Use generateId instead of generateUuid ([#9995](https://github.com/hcengineering/platform/issues/9995)) · Use natural mermaid diagram width and scroll ([#10006](https://github.com/hcengineering/platform/issues/10006)) · Do not allow to disable contacts plugin ([#10025](https://github.com/hcengineering/platform/issues/10025)) · Log window reload reason ([#10033](https://github.com/hcengineering/platform/issues/10033)) · Fix card open to more native ([#10047](https://github.com/hcengineering/platform/issues/10047)) · Remove token logging ([#10049](https://github.com/hcengineering/platform/issues/10049)) · Validate workspace uuid in datalake ([#10048](https://github.com/hcengineering/platform/issues/10048)) · Remove indexeddb provider ([#10073](https://github.com/hcengineering/platform/issues/10073)) · Show billing in settings ([#10116](https://github.com/hcengineering/platform/issues/10116)) * QIFX: · Address sentry issues ([#10027](https://github.com/hcengineering/platform/issues/10027)) @@ -91,15 +101,11 @@ Changelog. * ⚙️ MISCELLANEOUS TASKS: · Migration ([#9923](https://github.com/hcengineering/platform/issues/9923)) · Communication ([#9939](https://github.com/hcengineering/platform/issues/9939)) * OFFICE: · Refactor meeting creation ([#9905](https://github.com/hcengineering/platform/issues/9905)) · Meeting control bar with large buttons ([#9946](https://github.com/hcengineering/platform/issues/9946)) * QFIX: · Region when restoring from v6 ([#9920](https://github.com/hcengineering/platform/issues/9920)) · Restore draft condition ([#9921](https://github.com/hcengineering/platform/issues/9921)) - -## [s0.7.255] - 2025-09-22 - -* 🐛 BUG FIXES: · Handle disabled contacts plugin in mentions popup ([#9904](https://github.com/hcengineering/platform/issues/9904)) · Refactor presence and typing to use svelte actions ([#9908](https://github.com/hcengineering/platform/issues/9908)) * UBERF-13692: · Adjut merge contacts to global accounts ([#9911](https://github.com/hcengineering/platform/issues/9911)) * EQMS-1618: · Fix server branding ([#9907](https://github.com/hcengineering/platform/issues/9907)) -* QFIX: · Hide large values from activity ([#9894](https://github.com/hcengineering/platform/issues/9894)) +* QFIX: · Hide large values from activity ([#9894](https://github.com/hcengineering/platform/issues/9894)) · Desktop should recover on network lose ([#9967](https://github.com/hcengineering/platform/issues/9967)) · Handle outside clicks only when popup is top level ([#9972](https://github.com/hcengineering/platform/issues/9972)) -## [s0.7.253] - 2025-09-22 +## [0.7.252] - 2025-09-21 * 🐛 BUG FIXES: · Build rekoni and print images ([#9860](https://github.com/hcengineering/platform/issues/9860)) · Default space for cards ([#9868](https://github.com/hcengineering/platform/issues/9868)) · Scroll in chat navigator ([#9872](https://github.com/hcengineering/platform/issues/9872)) · Use latest base image ([#9874](https://github.com/hcengineering/platform/issues/9874)) · Properly update pulse typing sub ([#9875](https://github.com/hcengineering/platform/issues/9875)) · Processes mixin work ([#9876](https://github.com/hcengineering/platform/issues/9876)) · Subprocess trigger ([#9884](https://github.com/hcengineering/platform/issues/9884)) · Reset typing state on resubscribe ([#9881](https://github.com/hcengineering/platform/issues/9881)) · Directs order ([#9885](https://github.com/hcengineering/platform/issues/9885)) · Processes rollback ([#9886](https://github.com/hcengineering/platform/issues/9886)) · Function context presenter ([#9887](https://github.com/hcengineering/platform/issues/9887)) * ⚙️ MISCELLANEOUS TASKS: · Email mask ([#9862](https://github.com/hcengineering/platform/issues/9862)) · Electron app to 38 ([#9878](https://github.com/hcengineering/platform/issues/9878)) @@ -110,21 +116,6 @@ Changelog. * SELFH-199: · V7 restore providers social ids ([#9892](https://github.com/hcengineering/platform/issues/9892)) * UBERF-13587: · Fix integrating ai-assistant in workspaces other than the first one ([#9882](https://github.com/hcengineering/platform/issues/9882)) -## [s0.7.250] - 2025-09-18 - -* 🐛 BUG FIXES: · Reset typing state on resubscribe ([#9881](https://github.com/hcengineering/platform/issues/9881)) -* PROCESS: · Use previous step context in the same transition ([#9877](https://github.com/hcengineering/platform/issues/9877)) -* QFIX: · Import tool owner permissions ([#9883](https://github.com/hcengineering/platform/issues/9883)) -* UBERF-13587: · Huly secretary integration ([#9861](https://github.com/hcengineering/platform/issues/9861)) - -## [s0.7.249] - 2025-09-17 - -* 🐛 BUG FIXES: · Use latest base image ([#9874](https://github.com/hcengineering/platform/issues/9874)) · Properly update pulse typing sub ([#9875](https://github.com/hcengineering/platform/issues/9875)) - -## [s0.7.246] - 2025-09-16 - -* 🐛 BUG FIXES: · Build rekoni and print images ([#9860](https://github.com/hcengineering/platform/issues/9860)) - ## [0.7.245] - 2025-09-15 * 🚀 FEATURES: · Add search for card types dropdown ([#9836](https://github.com/hcengineering/platform/issues/9836)) @@ -132,319 +123,37 @@ Changelog. * UBERF-13511: · Support guest tokens with open account ([#9834](https://github.com/hcengineering/platform/issues/9834)) * EQMS-1644: · Fix mixins in server pipeline of workspace service ([#9839](https://github.com/hcengineering/platform/issues/9839)) -## [s0.7.242] - 2025-09-11 +## [0.7.242] - 2025-09-12 * 🚀 FEATURES: · Loop transition ([#9783](https://github.com/hcengineering/platform/issues/9783)) · Add content to new card form in Home ([#9803](https://github.com/hcengineering/platform/issues/9803)) · Add Home application ([#9811](https://github.com/hcengineering/platform/issues/9811)) · Add logging for event processing and transition handling in messageHandler ([#9823](https://github.com/hcengineering/platform/issues/9823)) * 🐛 BUG FIXES: · Show card icon in card title ([#9792](https://github.com/hcengineering/platform/issues/9792)) · Do not fail collab from json when wrong content type ([#9791](https://github.com/hcengineering/platform/issues/9791)) · Autofocus to qms comment popup ([#9800](https://github.com/hcengineering/platform/issues/9800)) · Office person assignee ([#9798](https://github.com/hcengineering/platform/issues/9798)) · Subprocess params ([#9805](https://github.com/hcengineering/platform/issues/9805)) · Add missing translations ([#9806](https://github.com/hcengineering/platform/issues/9806)) · Context class set ([#9830](https://github.com/hcengineering/platform/issues/9830)) · Home in desktop ([#9832](https://github.com/hcengineering/platform/issues/9832)) · Old inbox notifications ([#9835](https://github.com/hcengineering/platform/issues/9835)) * ⚙️ MISCELLANEOUS TASKS: · All icon in Home ([#9821](https://github.com/hcengineering/platform/issues/9821)) * UBERF-13510: · Card space public link ([#9804](https://github.com/hcengineering/platform/issues/9804)) * UBERF-13564: · Fix logout caused by connectivity issues ([#9809](https://github.com/hcengineering/platform/issues/9809)) -* QFIX: · Add separate gauges for sessions ([#9810](https://github.com/hcengineering/platform/issues/9810)) - -## [s0.7.237] - 2025-09-09 - -* 🐛 BUG FIXES: · Autofocus to qms comment popup ([#9800](https://github.com/hcengineering/platform/issues/9800)) * HULYLAKE: · Initial integration (build + dev docker compose) ([#9802](https://github.com/hcengineering/platform/issues/9802)) - -## [s0.7.236] - 2025-09-08 - -* 🐛 BUG FIXES: · Show card icon in card title ([#9792](https://github.com/hcengineering/platform/issues/9792)) · Do not fail collab from json when wrong content type ([#9791](https://github.com/hcengineering/platform/issues/9791)) +* QFIX: · Add separate gauges for sessions ([#9810](https://github.com/hcengineering/platform/issues/9810)) · Fix github measurements ([#9816](https://github.com/hcengineering/platform/issues/9816)) · Count anonymous users ([#9833](https://github.com/hcengineering/platform/issues/9833)) * UBERF-13507: · Support space membership via token claim ([#9786](https://github.com/hcengineering/platform/issues/9786)) -## [s0.7.235] - 2025-09-05 +## [0.7.235] - 2025-09-06 * 🚀 FEATURES: · Add exports for some packages used as external deps ([#8116](https://github.com/hcengineering/platform/issues/8116)) · Refactor markup to markdown utils ([#8134](https://github.com/hcengineering/platform/issues/8134)) · Rate limit on sendInvite ([#8150](https://github.com/hcengineering/platform/issues/8150)) · Add public schedule ([#8155](https://github.com/hcengineering/platform/issues/8155)) · Add datalake service ([#8184](https://github.com/hcengineering/platform/issues/8184)) · WS tests stability ([#8196](https://github.com/hcengineering/platform/issues/8196)) · Add a token for stream requests ([#8288](https://github.com/hcengineering/platform/issues/8288)) · Add lazy for emojis groups ([#8292](https://github.com/hcengineering/platform/issues/8292)) · Change type ([#8337](https://github.com/hcengineering/platform/issues/8337)) · Add search for meetings ([#8341](https://github.com/hcengineering/platform/issues/8341)) · Export for organizations ([#8346](https://github.com/hcengineering/platform/issues/8346)) · Add exception to rules for ai bot in identity ([#8397](https://github.com/hcengineering/platform/issues/8397)) · Add confidence number to transcription ([#8429](https://github.com/hcengineering/platform/issues/8429)) · Add createInviteLink to account rest client ([#8456](https://github.com/hcengineering/platform/issues/8456)) · Add auto-start functionality and related triggers for processes ([#8478](https://github.com/hcengineering/platform/issues/8478)) · UpdateCard editor ([#8479](https://github.com/hcengineering/platform/issues/8479)) · Add parallel execution restriction feature to processes ([#8477](https://github.com/hcengineering/platform/issues/8477)) · Add user to card collaborators when card created in personal space ([#8487](https://github.com/hcengineering/platform/issues/8487)) · Add limit for types in navigator and add list view for types ([#8489](https://github.com/hcengineering/platform/issues/8489)) · Add voice activity ui ([#8460](https://github.com/hcengineering/platform/issues/8460)) · Add browser notifications sound ([#8515](https://github.com/hcengineering/platform/issues/8515)) · Auto topic creation in consumer ([#8529](https://github.com/hcengineering/platform/issues/8529)) · Add personal labels for cards ([#8534](https://github.com/hcengineering/platform/issues/8534)) · Media state plugin ([#8674](https://github.com/hcengineering/platform/issues/8674)) · Add new chat fixes ([#8687](https://github.com/hcengineering/platform/issues/8687)) · Add connectRest helper to api-client ([#8756](https://github.com/hcengineering/platform/issues/8756)) · Add sample rate to config ([#8797](https://github.com/hcengineering/platform/issues/8797)) · Add Milestones to issue cards in kanban board view ([#8822](https://github.com/hcengineering/platform/issues/8822)) · Add more logs for tg bot ([#8829](https://github.com/hcengineering/platform/issues/8829)) · Add StorageClient for api-client ([#8850](https://github.com/hcengineering/platform/issues/8850)) · Add config for new activity/chat/inbox ([#8951](https://github.com/hcengineering/platform/issues/8951)) · Add fixes for new chat/inbox ([#8984](https://github.com/hcengineering/platform/issues/8984)) · Add guests in contacts ([#8998](https://github.com/hcengineering/platform/issues/8998)) · Add favorite/starred cards ([#9006](https://github.com/hcengineering/platform/issues/9006)) · Add chat fixes and drafts, message remove action ([#9069](https://github.com/hcengineering/platform/issues/9069)) · Add link preview in new chat ([#9090](https://github.com/hcengineering/platform/issues/9090)) · Add {PROVIDER}_DISPLAY_NAME env-var ([#8967](https://github.com/hcengineering/platform/issues/8967)) · Add beta label to processes module ([#9106](https://github.com/hcengineering/platform/issues/9106)) · Cpu profiling for ui tests ([#9149](https://github.com/hcengineering/platform/issues/9149)) · Add chat q-fixes ([#9173](https://github.com/hcengineering/platform/issues/9173)) · Workspace owners to edit spaces ([#9194](https://github.com/hcengineering/platform/issues/9194)) · Use queue for transcoding tasks ([#9216](https://github.com/hcengineering/platform/issues/9216)) · Add blockTime property to calendar events ([#9275](https://github.com/hcengineering/platform/issues/9275)) · Add card ui fixes ([#9301](https://github.com/hcengineering/platform/issues/9301)) · Add extra handlers to room controls ([#9221](https://github.com/hcengineering/platform/issues/9221)) · Space in sql filter ([#9330](https://github.com/hcengineering/platform/issues/9330)) · To disable surveys ([#9349](https://github.com/hcengineering/platform/issues/9349)) · Add text transformations ([#9351](https://github.com/hcengineering/platform/issues/9351)) · Add mathematical transformation functions ([#9357](https://github.com/hcengineering/platform/issues/9357)) · Add more chat/cards ui fixes ([#9358](https://github.com/hcengineering/platform/issues/9358)) · Add calendar to allowed ([#9373](https://github.com/hcengineering/platform/issues/9373)) · Add card icon ([#9379](https://github.com/hcengineering/platform/issues/9379)) · Add error messages to the middleware ([#9387](https://github.com/hcengineering/platform/issues/9387)) · Add red dot on desktop ([#9393](https://github.com/hcengineering/platform/issues/9393)) · Calendar sync ([#9400](https://github.com/hcengineering/platform/issues/9400)) · Add desktop pushes for new inbox ([#9414](https://github.com/hcengineering/platform/issues/9414)) · Backup logging ([#9434](https://github.com/hcengineering/platform/issues/9434)) · Add link to email notifications ([#9438](https://github.com/hcengineering/platform/issues/9438)) · To use realted object as value in processes ([#9439](https://github.com/hcengineering/platform/issues/9439)) · Newest first for activity by default ([#9454](https://github.com/hcengineering/platform/issues/9454)) · Add proccess execution log ([#9474](https://github.com/hcengineering/platform/issues/9474)) · Add jest global timeout ([#9498](https://github.com/hcengineering/platform/issues/9498)) · Add card readonly ([#9527](https://github.com/hcengineering/platform/issues/9527)) · Processes context ([#9537](https://github.com/hcengineering/platform/issues/9537)) · Add a separate setting to control if guests can join WS ([#9540](https://github.com/hcengineering/platform/issues/9540)) · Add array manipulation functions and editors for process ([#9594](https://github.com/hcengineering/platform/issues/9594)) · Notifications count ([#9598](https://github.com/hcengineering/platform/issues/9598)) · To edit description of Channel ([#9607](https://github.com/hcengineering/platform/issues/9607)) · To get blob metadata without authorization ([#9615](https://github.com/hcengineering/platform/issues/9615)) · Minimized screen recorder ([#9619](https://github.com/hcengineering/platform/issues/9619)) · Pass tracing info with websocket ([#9627](https://github.com/hcengineering/platform/issues/9627)) · Add IP geolocation support to analytics collector ([#9632](https://github.com/hcengineering/platform/issues/9632)) · Process ui ([#9645](https://github.com/hcengineering/platform/issues/9645)) · Add account method to get full social ids by person ids ([#9654](https://github.com/hcengineering/platform/issues/9654)) · Add script to restore Github integrations ([#9700](https://github.com/hcengineering/platform/issues/9700)) · Add process temporal ([#9744](https://github.com/hcengineering/platform/issues/9744)) · Add search to Home page ([#9772](https://github.com/hcengineering/platform/issues/9772)) · Add filter to Home ([#9774](https://github.com/hcengineering/platform/issues/9774)) * 🐛 BUG FIXES: · Build · Ui tests · Failed tests · Check guest access with cookie token ([#8122](https://github.com/hcengineering/platform/issues/8122)) · Svelte-check errors ([#8127](https://github.com/hcengineering/platform/issues/8127)) · Creating meeting when create event ([#8131](https://github.com/hcengineering/platform/issues/8131)) · Missing screen share thumbnails in desktop ([#8135](https://github.com/hcengineering/platform/issues/8135)) · Participant name format ([#8141](https://github.com/hcengineering/platform/issues/8141)) · Create of elastic index · Multiple single queries, add lazy, load with limit ([#8142](https://github.com/hcengineering/platform/issues/8142)) · Backup all and disable full check for migration · Typo in threads ([#8158](https://github.com/hcengineering/platform/issues/8158)) · Enums ([#8168](https://github.com/hcengineering/platform/issues/8168)) · Envs for huly-stream app for local dev ([#8171](https://github.com/hcengineering/platform/issues/8171)) · Encode content disposition file name ([#8190](https://github.com/hcengineering/platform/issues/8190)) · Enhance datalake performance logging ([#8197](https://github.com/hcengineering/platform/issues/8197)) · Keep alive consistency for account + front ([#8198](https://github.com/hcengineering/platform/issues/8198)) · Merge · Image preview not displayed ([#8207](https://github.com/hcengineering/platform/issues/8207)) · Schedule link text ([#8216](https://github.com/hcengineering/platform/issues/8216)) · Use readonly connections for guests ([#8221](https://github.com/hcengineering/platform/issues/8221)) · Attachment spaces ([#8200](https://github.com/hcengineering/platform/issues/8200)) · Use huly.local in webpack proxy config ([#8233](https://github.com/hcengineering/platform/issues/8233)) · Reindex ([#8236](https://github.com/hcengineering/platform/issues/8236)) · Account migration mongo to cr ([#8237](https://github.com/hcengineering/platform/issues/8237)) · Master tag remove ([#8239](https://github.com/hcengineering/platform/issues/8239)) · Model order ([#8250](https://github.com/hcengineering/platform/issues/8250)) · Datalake fixes ([#8251](https://github.com/hcengineering/platform/issues/8251)) · Backup-all script ([#8255](https://github.com/hcengineering/platform/issues/8255)) · Use host + port in datalake address ([#8276](https://github.com/hcengineering/platform/issues/8276)) · Override sidebar with thread ([#8277](https://github.com/hcengineering/platform/issues/8277)) · DocGuest set status cause tx and error record ([#8279](https://github.com/hcengineering/platform/issues/8279)) ([#8282](https://github.com/hcengineering/platform/issues/8282)) · Customize view freezes ([#8281](https://github.com/hcengineering/platform/issues/8281)) · Pg object query ([#8284](https://github.com/hcengineering/platform/issues/8284)) · Preload video by default ([#8286](https://github.com/hcengineering/platform/issues/8286)) · Code block styling fixes ([#8289](https://github.com/hcengineering/platform/issues/8289)) · Card available tags ([#8297](https://github.com/hcengineering/platform/issues/8297)) · Link preview size ([#8294](https://github.com/hcengineering/platform/issues/8294)) · Chat unscroll after chat freeze ([#8299](https://github.com/hcengineering/platform/issues/8299)) · Modify blob url if it has scheme ([#8301](https://github.com/hcengineering/platform/issues/8301)) · Mentions and threads ([#8309](https://github.com/hcengineering/platform/issues/8309)) · Multipart upload in datalake service ([#8307](https://github.com/hcengineering/platform/issues/8307)) · Object-fit mistake ([#8314](https://github.com/hcengineering/platform/issues/8314)) · Sql batch update ([#8296](https://github.com/hcengineering/platform/issues/8296)) · Contact array presenter ([#8332](https://github.com/hcengineering/platform/issues/8332)) · Ai reply loop ([#8333](https://github.com/hcengineering/platform/issues/8333)) · Handle token errors in front service ([#8336](https://github.com/hcengineering/platform/issues/8336)) · Thread open ([#8344](https://github.com/hcengineering/platform/issues/8344)) · Exclude first segment from account cookie domain ([#8348](https://github.com/hcengineering/platform/issues/8348)) · Remove quotes from print blob id ([#8356](https://github.com/hcengineering/platform/issues/8356)) · Handle double quotes in etag ([#8362](https://github.com/hcengineering/platform/issues/8362)) · Meeting window width ([#8365](https://github.com/hcengineering/platform/issues/8365)) · Change type ([#8388](https://github.com/hcengineering/platform/issues/8388)) · Mixins for calendar event ([#8393](https://github.com/hcengineering/platform/issues/8393)) · Meeting window width ([#8382](https://github.com/hcengineering/platform/issues/8382)) · Card migration ([#8402](https://github.com/hcengineering/platform/issues/8402)) · Update settings context on changes & update system theme ([#8405](https://github.com/hcengineering/platform/issues/8405)) · Freeze app for wrong class in mentions ([#8411](https://github.com/hcengineering/platform/issues/8411)) · Reduce amount of text typed ([#8414](https://github.com/hcengineering/platform/issues/8414)) · Stt ([#8415](https://github.com/hcengineering/platform/issues/8415)) · Fix list applications display ([#8417](https://github.com/hcengineering/platform/issues/8417)) · Build msg2file container ([#8424](https://github.com/hcengineering/platform/issues/8424)) · Transcript score ([#8434](https://github.com/hcengineering/platform/issues/8434)) · Cards view settings ([#8437](https://github.com/hcengineering/platform/issues/8437)) · Open doc on mention click in collaborative content ([#8442](https://github.com/hcengineering/platform/issues/8442)) · Admin pending ops ([#8449](https://github.com/hcengineering/platform/issues/8449)) · Duplication of views ([#8450](https://github.com/hcengineering/platform/issues/8450)) · Lookups from array ([#8453](https://github.com/hcengineering/platform/issues/8453)) · Inbound mail attachment upload ([#8472](https://github.com/hcengineering/platform/issues/8472)) · Nested tags ([#8474](https://github.com/hcengineering/platform/issues/8474)) · Office load waiting before joining meeting ([#8484](https://github.com/hcengineering/platform/issues/8484)) · Fix huge statistics send ([#8483](https://github.com/hcengineering/platform/issues/8483)) · Handle token error in collaborator service ([#8493](https://github.com/hcengineering/platform/issues/8493)) · Lookup workspace filter ([#8513](https://github.com/hcengineering/platform/issues/8513)) · Calendar visibility dev ([#8514](https://github.com/hcengineering/platform/issues/8514)) · Card spaces ([#8517](https://github.com/hcengineering/platform/issues/8517)) · Adjust text editor quote and hr styles ([#8524](https://github.com/hcengineering/platform/issues/8524)) · Mentions font-size ([#8526](https://github.com/hcengineering/platform/issues/8526)) · Override script · Calendar migration ([#8531](https://github.com/hcengineering/platform/issues/8531)) · Add error reporting in datalake ([#8535](https://github.com/hcengineering/platform/issues/8535)) · Custom field grouping ([#8538](https://github.com/hcengineering/platform/issues/8538)) · Removed types ([#8544](https://github.com/hcengineering/platform/issues/8544)) · Build for external PRs. ([#8579](https://github.com/hcengineering/platform/issues/8579)) · Handle pong message in presence client ([#8597](https://github.com/hcengineering/platform/issues/8597)) · Append port to s3 endpoint ([#8601](https://github.com/hcengineering/platform/issues/8601)) · TypedSpaces empty owners ([#8574](https://github.com/hcengineering/platform/issues/8574)) · Find for direct persons ([#8632](https://github.com/hcengineering/platform/issues/8632)) · Bot access to send messages under other persons ([#8633](https://github.com/hcengineering/platform/issues/8633)) · Otp admin login ([#8639](https://github.com/hcengineering/platform/issues/8639)) · Show warning of not visited workspace only once ([#8641](https://github.com/hcengineering/platform/issues/8641)) · Region filter in admin panel ([#8646](https://github.com/hcengineering/platform/issues/8646)) · Notifications query ([#8644](https://github.com/hcengineering/platform/issues/8644)) · Threads in new chat ([#8653](https://github.com/hcengineering/platform/issues/8653)) · Related issues query ([#8684](https://github.com/hcengineering/platform/issues/8684)) · Removed cards ([#8694](https://github.com/hcengineering/platform/issues/8694)) · The description of a user creation in dev mode in README ([#8492](https://github.com/hcengineering/platform/issues/8492)) · Increase beforeAll timeout on API tests ([#8707](https://github.com/hcengineering/platform/issues/8707)) · Remove file upload console log ([#8722](https://github.com/hcengineering/platform/issues/8722)) · Add more logs do collaborator doc saving ([#8721](https://github.com/hcengineering/platform/issues/8721)) · Incompatibilities of front files server and datalake ([#8730](https://github.com/hcengineering/platform/issues/8730)) · Adjust media popup styles ([#8724](https://github.com/hcengineering/platform/issues/8724)) · Admin panel show inactive workspaces ([#8716](https://github.com/hcengineering/platform/issues/8716)) · Child card spaces ([#8742](https://github.com/hcengineering/platform/issues/8742)) · Removed types ([#8749](https://github.com/hcengineering/platform/issues/8749)) · Checking for guest in meetings ([#8750](https://github.com/hcengineering/platform/issues/8750)) · Activity for removed tags ([#8761](https://github.com/hcengineering/platform/issues/8761)) · Use front base for datalake ([#8772](https://github.com/hcengineering/platform/issues/8772)) · Datalake pod · Improve backup find tool ([#8783](https://github.com/hcengineering/platform/issues/8783)) · Issue status icon presentation ([#8795](https://github.com/hcengineering/platform/issues/8795)) · Datalake url in dev env ([#8833](https://github.com/hcengineering/platform/issues/8833)) · Messages loading ([#8865](https://github.com/hcengineering/platform/issues/8865)) · Close media popup on meeting leave ([#8877](https://github.com/hcengineering/platform/issues/8877)) · Init workspace in dev ([#8799](https://github.com/hcengineering/platform/issues/8799)) · Undefined error in new chat ([#8892](https://github.com/hcengineering/platform/issues/8892)) · Storage adapter throw error on stat ([#8893](https://github.com/hcengineering/platform/issues/8893)) · Keep snapshots ([#8904](https://github.com/hcengineering/platform/issues/8904)) · Close room popup on stop share ([#8914](https://github.com/hcengineering/platform/issues/8914)) · Custom emojis in reactions ([#8921](https://github.com/hcengineering/platform/issues/8921)) · Adjust embedded video player style ([#8947](https://github.com/hcengineering/platform/issues/8947)) · Communication api config ([#8955](https://github.com/hcengineering/platform/issues/8955)) · Remove datalake extra retries on 404 ([#8962](https://github.com/hcengineering/platform/issues/8962)) · Scheduled events inbox notifications ([#8964](https://github.com/hcengineering/platform/issues/8964)) · Email notifications ([#8978](https://github.com/hcengineering/platform/issues/8978)) · Calendar service ([#8981](https://github.com/hcengineering/platform/issues/8981)) · Processes ([#9014](https://github.com/hcengineering/platform/issues/9014)) · Favorites ([#9020](https://github.com/hcengineering/platform/issues/9020)) · Retry for some kind of errors in Github ([#9055](https://github.com/hcengineering/platform/issues/9055)) · Ts-config to fix ts-server ([#9019](https://github.com/hcengineering/platform/issues/9019)) · Remove analytics from SplitLogger ([#9086](https://github.com/hcengineering/platform/issues/9086)) · Remove duplicate handler errors ([#9084](https://github.com/hcengineering/platform/issues/9084)) · Do not report some datalake errors to analytics ([#9085](https://github.com/hcengineering/platform/issues/9085)) · Space icons ([#9089](https://github.com/hcengineering/platform/issues/9089)) · Backlinks header ([#9107](https://github.com/hcengineering/platform/issues/9107)) · Notification when Request.requested changed ([#9105](https://github.com/hcengineering/platform/issues/9105)) · Everyone/here mentions ([#9109](https://github.com/hcengineering/platform/issues/9109)) · Add recorder assets to desktop ([#9122](https://github.com/hcengineering/platform/issues/9122)) · Prevent closing recording popup ([#9125](https://github.com/hcengineering/platform/issues/9125)) · Add stream to ext services ([#9126](https://github.com/hcengineering/platform/issues/9126)) · Invalid sql query from findAll with empty sort ([#9135](https://github.com/hcengineering/platform/issues/9135)) · Correct filename in content-disposition header ([#9139](https://github.com/hcengineering/platform/issues/9139)) · Do not find for a Card for rank ([#9145](https://github.com/hcengineering/platform/issues/9145)) · Build fix for browser list · Get rid of node-fetch ([#9150](https://github.com/hcengineering/platform/issues/9150)) · Connection reservation logic ([#9153](https://github.com/hcengineering/platform/issues/9153)) · Card parent loop ([#9159](https://github.com/hcengineering/platform/issues/9159)) · Rpc response body size ([#9160](https://github.com/hcengineering/platform/issues/9160)) · Datalake memory leak fixes ([#9161](https://github.com/hcengineering/platform/issues/9161)) · Handle links with spaces in markdown ([#9164](https://github.com/hcengineering/platform/issues/9164)) · Card migration loop ([#9176](https://github.com/hcengineering/platform/issues/9176)) · Rendering of custom attributes without icons ([#9171](https://github.com/hcengineering/platform/issues/9171)) · Services rate limit ([#9193](https://github.com/hcengineering/platform/issues/9193)) · Properly configure keep-alive in datalake HTTP server ([#9198](https://github.com/hcengineering/platform/issues/9198)) · False positive emoji detection ([#9211](https://github.com/hcengineering/platform/issues/9211)) · Image paste in new chat ([#9203](https://github.com/hcengineering/platform/issues/9203)) · Table inplace edit ([#9220](https://github.com/hcengineering/platform/issues/9220)) · Communication migration ([#9228](https://github.com/hcengineering/platform/issues/9228)) · Type properties editor ([#9227](https://github.com/hcengineering/platform/issues/9227)) · Get workspaces mode is missing ([#9237](https://github.com/hcengineering/platform/issues/9237)) · Create transcode topics on start ([#9225](https://github.com/hcengineering/platform/issues/9225)) · Rate limits ([#9242](https://github.com/hcengineering/platform/issues/9242)) · Old token ([#9238](https://github.com/hcengineering/platform/issues/9238)) · Restore in workspace service for old workspaces ([#9241](https://github.com/hcengineering/platform/issues/9241)) · Github migration + few checks ([#9244](https://github.com/hcengineering/platform/issues/9244)) · Ensure workspace and account uuid when generating token ([#9246](https://github.com/hcengineering/platform/issues/9246)) · Github service startup ([#9251](https://github.com/hcengineering/platform/issues/9251)) · Empty workspace uuid when generating token ([#9252](https://github.com/hcengineering/platform/issues/9252)) · Empty comments in github ([#9258](https://github.com/hcengineering/platform/issues/9258)) · Show collaborator connection errors ([#9259](https://github.com/hcengineering/platform/issues/9259)) · Handle processing errors in indexer ([#9262](https://github.com/hcengineering/platform/issues/9262)) · Backup fixes ([#9256](https://github.com/hcengineering/platform/issues/9256)) · Delete markup description from update ([#9269](https://github.com/hcengineering/platform/issues/9269)) · Calendar sync ([#9271](https://github.com/hcengineering/platform/issues/9271)) · Initialize server secret before storage config ([#9274](https://github.com/hcengineering/platform/issues/9274)) · Calendar migration tool ([#9279](https://github.com/hcengineering/platform/issues/9279)) · Emoji input rule ([#9280](https://github.com/hcengineering/platform/issues/9280)) · Error on removing default task states ([#9281](https://github.com/hcengineering/platform/issues/9281)) · Calendar freeze ([#9286](https://github.com/hcengineering/platform/issues/9286)) · Mixin projection ([#9287](https://github.com/hcengineering/platform/issues/9287)) · Restore markup refs corrupted by github ([#9284](https://github.com/hcengineering/platform/issues/9284)) · Integration template values ([#9294](https://github.com/hcengineering/platform/issues/9294)) · Suppress unused regex escaping ([#9299](https://github.com/hcengineering/platform/issues/9299)) · Restore api client functionality ([#9292](https://github.com/hcengineering/platform/issues/9292)) · Team ([#9307](https://github.com/hcengineering/platform/issues/9307)) · Optimize getWorkspaceInfo use ([#9302](https://github.com/hcengineering/platform/issues/9302)) · Check last visit in gmail service ([#9300](https://github.com/hcengineering/platform/issues/9300)) · Notifications read ([#9316](https://github.com/hcengineering/platform/issues/9316)) · Messages to file ([#9319](https://github.com/hcengineering/platform/issues/9319)) · Hide deleted workspaces from select workspace list ([#9337](https://github.com/hcengineering/platform/issues/9337)) · Collaborators tests ([#9331](https://github.com/hcengineering/platform/issues/9331)) · Transcode livekit videos uploaded to s3 ([#9342](https://github.com/hcengineering/platform/issues/9342)) · Adjust video player controls ([#9343](https://github.com/hcengineering/platform/issues/9343)) · Skip communication apply-templates ([#9341](https://github.com/hcengineering/platform/issues/9341)) · Backup service ([#9347](https://github.com/hcengineering/platform/issues/9347)) · Meeting minutes security ([#9348](https://github.com/hcengineering/platform/issues/9348)) · Office empty meeting ([#9353](https://github.com/hcengineering/platform/issues/9353)) · Ignore transcoding for some videos ([#9354](https://github.com/hcengineering/platform/issues/9354)) · Back /api/v1/event/:workspaceId ([#9367](https://github.com/hcengineering/platform/issues/9367)) · Reindex inplace ([#9365](https://github.com/hcengineering/platform/issues/9365)) · New collab presenter ([#9368](https://github.com/hcengineering/platform/issues/9368)) · Collab security ([#9370](https://github.com/hcengineering/platform/issues/9370)) · Secret in billing pod ([#9377](https://github.com/hcengineering/platform/issues/9377)) · Calendar freeze ([#9378](https://github.com/hcengineering/platform/issues/9378)) · Calendar sync ([#9392](https://github.com/hcengineering/platform/issues/9392)) · Adjust live recording ([#9394](https://github.com/hcengineering/platform/issues/9394)) · Meeting minutes activity ([#9395](https://github.com/hcengineering/platform/issues/9395)) · Office preferences ([#9399](https://github.com/hcengineering/platform/issues/9399)) · Calendar flood ([#9405](https://github.com/hcengineering/platform/issues/9405)) · Request camera and microphone once ([#9410](https://github.com/hcengineering/platform/issues/9410)) · Use worker for recording timing ([#9420](https://github.com/hcengineering/platform/issues/9420)) · Miss broadcast for http requests ([#9422](https://github.com/hcengineering/platform/issues/9422)) · Sort model on client ([#9421](https://github.com/hcengineering/platform/issues/9421)) · Properly parse and serialize html link ([#9427](https://github.com/hcengineering/platform/issues/9427)) · Pass cache control from request to created blob ([#9430](https://github.com/hcengineering/platform/issues/9430)) · Use object title as print file name ([#9437](https://github.com/hcengineering/platform/issues/9437)) · Desktop notifications ([#9447](https://github.com/hcengineering/platform/issues/9447)) · Use plyr player for non-hls video ([#9446](https://github.com/hcengineering/platform/issues/9446)) · Card scroll ([#9461](https://github.com/hcengineering/platform/issues/9461)) · Emoji data import for missing locales ([#9442](https://github.com/hcengineering/platform/issues/9442)) · Fix reducedCalls to stop in case of error ([#9482](https://github.com/hcengineering/platform/issues/9482)) · Telegram-bot connect ([#9488](https://github.com/hcengineering/platform/issues/9488)) · Migration ([#9497](https://github.com/hcengineering/platform/issues/9497)) · Configure love recording quality ([#9509](https://github.com/hcengineering/platform/issues/9509)) · Re-enable references in the new chat input ([#9507](https://github.com/hcengineering/platform/issues/9507)) · Communication ([#9520](https://github.com/hcengineering/platform/issues/9520)) · Config ([#9544](https://github.com/hcengineering/platform/issues/9544)) · Calendar serviced and kvs client ([#9551](https://github.com/hcengineering/platform/issues/9551)) · Red dot on new inbox ([#9555](https://github.com/hcengineering/platform/issues/9555)) · Miss config ([#9556](https://github.com/hcengineering/platform/issues/9556)) · Communication refresh ([#9559](https://github.com/hcengineering/platform/issues/9559)) · Navigation buttons visibility on mobile devices ([#9569](https://github.com/hcengineering/platform/issues/9569)) · Grouping error in Execution viewlet configuration ([#9568](https://github.com/hcengineering/platform/issues/9568)) · Fix edit request type ([#9570](https://github.com/hcengineering/platform/issues/9570)) · Show members edit table for department ([#9571](https://github.com/hcengineering/platform/issues/9571)) · Use the same hr style in editor and preview ([#9574](https://github.com/hcengineering/platform/issues/9574)) · Dev elastic healthcheck ([#9576](https://github.com/hcengineering/platform/issues/9576)) · Gmail sync retry ([#9575](https://github.com/hcengineering/platform/issues/9575)) · Messages sync for outdated gmail history id ([#9583](https://github.com/hcengineering/platform/issues/9583)) · Gmail config reset on reboot ([#9587](https://github.com/hcengineering/platform/issues/9587)) · Duplicated gmail sync on startup ([#9590](https://github.com/hcengineering/platform/issues/9590)) · Use system token when saving collaborative doc ([#9592](https://github.com/hcengineering/platform/issues/9592)) · Processes ui ([#9601](https://github.com/hcengineering/platform/issues/9601)) · Cannot find social id error for system ([#9603](https://github.com/hcengineering/platform/issues/9603)) · PayloadTooLargeError in analytics collector ([#9604](https://github.com/hcengineering/platform/issues/9604)) · Calendar ([#9620](https://github.com/hcengineering/platform/issues/9620)) · Model txes order ([#9626](https://github.com/hcengineering/platform/issues/9626)) · Log more stats in datalake ([#9624](https://github.com/hcengineering/platform/issues/9624)) · Broken ui test ([#9629](https://github.com/hcengineering/platform/issues/9629)) · Generate separate bundle for recorder worker ([#9633](https://github.com/hcengineering/platform/issues/9633)) · User mention icon and link previews ([#9635](https://github.com/hcengineering/platform/issues/9635)) · Tracker todo close time tracking ([#9636](https://github.com/hcengineering/platform/issues/9636)) · Notification navigation ([#9642](https://github.com/hcengineering/platform/issues/9642)) · Default gmail port for local setup ([#9650](https://github.com/hcengineering/platform/issues/9650)) · Gmail disconnect error ([#9649](https://github.com/hcengineering/platform/issues/9649)) · Livekit retry counter ([#9651](https://github.com/hcengineering/platform/issues/9651)) · Result editor ([#9653](https://github.com/hcengineering/platform/issues/9653)) · EQMS-1630: Separator resize logic ([#9662](https://github.com/hcengineering/platform/issues/9662)) · Ignore existing blobs in datalake migration ([#9664](https://github.com/hcengineering/platform/issues/9664)) · Notification counter in navigatoir ([#9665](https://github.com/hcengineering/platform/issues/9665)) · Increment join index even for empty result ([#9667](https://github.com/hcengineering/platform/issues/9667)) · Notify about new messages in initial Gmail sync ([#9673](https://github.com/hcengineering/platform/issues/9673)) · Use native bcrypt module ([#9675](https://github.com/hcengineering/platform/issues/9675)) · Huly mail headers format ([#9676](https://github.com/hcengineering/platform/issues/9676)) · Datalake upload fixes ([#9680](https://github.com/hcengineering/platform/issues/9680)) · Allow to edit channels from candidate card ([#9683](https://github.com/hcengineering/platform/issues/9683)) · Inbox ui ([#9688](https://github.com/hcengineering/platform/issues/9688)) · Allow undefined space and fix layout ([#9689](https://github.com/hcengineering/platform/issues/9689)) · Some issues with livekit connection ([#9690](https://github.com/hcengineering/platform/issues/9690)) · Telegram bot ([#9693](https://github.com/hcengineering/platform/issues/9693)) · ParticipantView overflow ([#9694](https://github.com/hcengineering/platform/issues/9694)) · Datalake cache memory leaks ([#9699](https://github.com/hcengineering/platform/issues/9699)) · Properly close iterators ([#9705](https://github.com/hcengineering/platform/issues/9705)) · Limit clone depth ([#9716](https://github.com/hcengineering/platform/issues/9716)) · Direct title ([#9721](https://github.com/hcengineering/platform/issues/9721)) · Set account cookie to origin and target domains ([#9719](https://github.com/hcengineering/platform/issues/9719)) · Notifications ([#9732](https://github.com/hcengineering/platform/issues/9732)) · Reaction notifications ([#9734](https://github.com/hcengineering/platform/issues/9734)) · Navigator ([#9737](https://github.com/hcengineering/platform/issues/9737)) · Load more in Home ([#9739](https://github.com/hcengineering/platform/issues/9739)) · Scroll in the meeting widget ([#9749](https://github.com/hcengineering/platform/issues/9749)) · Calendar rec events ([#9755](https://github.com/hcengineering/platform/issues/9755)) · Push base preview image ([#9756](https://github.com/hcengineering/platform/issues/9756)) · Broadcasting txes with collab security ([#9740](https://github.com/hcengineering/platform/issues/9740)) · Worker docker ([#9760](https://github.com/hcengineering/platform/issues/9760)) · Avatar for system user in Home ([#9762](https://github.com/hcengineering/platform/issues/9762)) · Race condition during lk connection ([#9764](https://github.com/hcengineering/platform/issues/9764)) · Use image/png content type for bot avatar ([#9765](https://github.com/hcengineering/platform/issues/9765)) · Image preview in preview service ([#9767](https://github.com/hcengineering/platform/issues/9767)) · Tags layout in Home ([#9768](https://github.com/hcengineering/platform/issues/9768)) · Thread name display ([#9771](https://github.com/hcengineering/platform/issues/9771)) · Preview text files ([#9773](https://github.com/hcengineering/platform/issues/9773)) · Support transcoding for attachment transactions ([#9775](https://github.com/hcengineering/platform/issues/9775)) * ⚙️ MISCELLANEOUS TASKS: · Model · Version · Model ([#8300](https://github.com/hcengineering/platform/issues/8300)) · Communication ([#8422](https://github.com/hcengineering/platform/issues/8422)) · Livekit client ([#8427](https://github.com/hcengineering/platform/issues/8427)) · Update tiptap & hocuspocus ([#8428](https://github.com/hcengineering/platform/issues/8428)) · Communication ([#8436](https://github.com/hcengineering/platform/issues/8436)) · Readme ([#8454](https://github.com/hcengineering/platform/issues/8454)) · Unused green service anymore ([#8480](https://github.com/hcengineering/platform/issues/8480)) · Livekit agent ([#8518](https://github.com/hcengineering/platform/issues/8518)) · Model ([#8522](https://github.com/hcengineering/platform/issues/8522)) · Communication ([#8578](https://github.com/hcengineering/platform/issues/8578)) · Version · API client documentation ([#8832](https://github.com/hcengineering/platform/issues/8832)) · Deprecated types packages from dependencies ([#8856](https://github.com/hcengineering/platform/issues/8856)) · Unstable/unused Github project support ([#8891](https://github.com/hcengineering/platform/issues/8891)) · Github integrations · Model version ([#8963](https://github.com/hcengineering/platform/issues/8963)) · Version ([#9057](https://github.com/hcengineering/platform/issues/9057)) · Node to v22 ([#9151](https://github.com/hcengineering/platform/issues/9151)) · Extra logs ([#9250](https://github.com/hcengineering/platform/issues/9250)) · Communication ([#9340](https://github.com/hcengineering/platform/issues/9340)) · Mongodb from dev setup ([#9336](https://github.com/hcengineering/platform/issues/9336)) · Extra in for contact.mixin.Employee ([#9356](https://github.com/hcengineering/platform/issues/9356)) · Favorite on card removed ([#9364](https://github.com/hcengineering/platform/issues/9364)) · Emoji img margin ([#9471](https://github.com/hcengineering/platform/issues/9471)) · Communication ([#9487](https://github.com/hcengineering/platform/issues/9487)) · Fulltext indexing from non-text fields, index calendar events/todos ([#9493](https://github.com/hcengineering/platform/issues/9493)) · Communication ([#9548](https://github.com/hcengineering/platform/issues/9548)) · Model version · Card trigger ([#9579](https://github.com/hcengineering/platform/issues/9579)) · Extra ([#9582](https://github.com/hcengineering/platform/issues/9582)) · Rush to 158.1 ([#9752](https://github.com/hcengineering/platform/issues/9752)) · Livekit packages ([#9763](https://github.com/hcengineering/platform/issues/9763)) · Card space icon ([#9779](https://github.com/hcengineering/platform/issues/9779)) · Model version · Readme.md * ANALYTICS: · Add OAuth authentication and guest access events ([#9541](https://github.com/hcengineering/platform/issues/9541)) - -## [s0.7.183] - 2025-07-18 - -* QFIX: · Org space auto join in import ([#9562](https://github.com/hcengineering/platform/issues/9562)) -* UBERF-12633: · Fix GH local identities ([#9566](https://github.com/hcengineering/platform/issues/9566)) - -## [s0.7.182] - 2025-07-17 - -* QFIX: · Qms tests ([#9557](https://github.com/hcengineering/platform/issues/9557)) -* QFIX: · Allow backup api to be used by admin ([#9560](https://github.com/hcengineering/platform/issues/9560)) · Fix export service ([#9558](https://github.com/hcengineering/platform/issues/9558)) - -## [s0.7.181] - 2025-07-15 - -* UBER-1290: · Fix refresh token in subsequent requests ([#9550](https://github.com/hcengineering/platform/issues/9550)) -* UBERF-12325: · Add mongo warning for v7 ([#9543](https://github.com/hcengineering/platform/issues/9543)) -* QFIX: · Remap github installation to another workspace ([#9553](https://github.com/hcengineering/platform/issues/9553)) - -## [s0.7.180] - 2025-07-15 - -* QFIX: · Editor toolbar z-index ([#9549](https://github.com/hcengineering/platform/issues/9549)) - -## [s0.7.179] - 2025-07-14 - -* UBERF-12227: · Stabilize UI tests ([#9521](https://github.com/hcengineering/platform/issues/9521)) -* UBERF-12445: · Fix adding second Github integration for same user ([#9531](https://github.com/hcengineering/platform/issues/9531)) - -## [s0.7.178] - 2025-07-11 - -* Q-FIX: · Update communication ([#9524](https://github.com/hcengineering/platform/issues/9524)) - -## [s0.7.175] - 2025-07-10 - -* 🐛 BUG FIXES: · Configure love recording quality ([#9509](https://github.com/hcengineering/platform/issues/9509)) -* QFIX: · Re-enable references in the new chat input ([#9507](https://github.com/hcengineering/platform/issues/9507)) -* UBERF-12214: · Fix GitHub assignee update ([#9515](https://github.com/hcengineering/platform/issues/9515)) -* UBERF-12299: · Fix gmail integration selection ([#9505](https://github.com/hcengineering/platform/issues/9505)) -* UBERF-12313: · Pass editor-kit options in StyledTextBox ([#9512](https://github.com/hcengineering/platform/issues/9512)) - -## [s0.7.174] - 2025-07-09 - -* UBERF-12146: · Fix queries with lookup conditions ([#9495](https://github.com/hcengineering/platform/issues/9495)) -* QFIX: · Backup blob info ([#9496](https://github.com/hcengineering/platform/issues/9496)) · Connection mgr close ([#9502](https://github.com/hcengineering/platform/issues/9502)) -* UBERF-9488: · Account operations unit tests ([#9503](https://github.com/hcengineering/platform/issues/9503)) - -## [s0.7.173] - 2025-07-08 - -* QFIX: · Fix reducedCalls to stop in case of error ([#9482](https://github.com/hcengineering/platform/issues/9482)) -* UBERF-12229: · Fix default gmail integration selection ([#9486](https://github.com/hcengineering/platform/issues/9486)) -* UBERF-9485: · Fix state description save ([#9483](https://github.com/hcengineering/platform/issues/9483)) - -## [s0.7.172] - 2025-07-07 - -* UBERF-12170: · Support merging person in addSocialIdToPerson ([#9470](https://github.com/hcengineering/platform/issues/9470)) - -## [s0.7.171] - 2025-07-04 - -* QFIX: · Adding/deleting social ids ([#9466](https://github.com/hcengineering/platform/issues/9466)) - -## [s0.7.170] - 2025-07-04 - -* 🐛 BUG FIXES: · Use plyr player for non-hls video ([#9446](https://github.com/hcengineering/platform/issues/9446)) -* UBERF-11998: · Support account deletion from admin page ([#9441](https://github.com/hcengineering/platform/issues/9441)) -* UBERF-12149: · Fix email send with irrelevant social id ([#9452](https://github.com/hcengineering/platform/issues/9452)) - -## [s0.7.169] - 2025-07-03 - -* 🐛 BUG FIXES: · Pass cache control from request to created blob ([#9430](https://github.com/hcengineering/platform/issues/9430)) · Use object title as print file name ([#9437](https://github.com/hcengineering/platform/issues/9437)) -* QFIX: · Clean up gmail logs ([#9433](https://github.com/hcengineering/platform/issues/9433)) -* UBERF-10254: · Manage own social ids ([#9398](https://github.com/hcengineering/platform/issues/9398)) - -## [s0.7.168] - 2025-07-02 - -* 🐛 BUG FIXES: · Use worker for recording timing ([#9420](https://github.com/hcengineering/platform/issues/9420)) · Properly parse and serialize html link ([#9427](https://github.com/hcengineering/platform/issues/9427)) -* QFIX: · Fix mail messages order ([#9419](https://github.com/hcengineering/platform/issues/9419)) · Markdown parsing & serialization fixes for images & tables ([#9429](https://github.com/hcengineering/platform/issues/9429)) · Use new hulygun API for mail ([#9426](https://github.com/hcengineering/platform/issues/9426)) -* QFIX: · Backups using pipeline ([#9396](https://github.com/hcengineering/platform/issues/9396)) · Refactor love plugin to not import . ([#9413](https://github.com/hcengineering/platform/issues/9413)) - -## [s0.7.167] - 2025-07-01 - -* 🐛 BUG FIXES: · Request camera and microphone once ([#9410](https://github.com/hcengineering/platform/issues/9410)) -* QFIX: · Account unit test ([#9409](https://github.com/hcengineering/platform/issues/9409)) · Allow tool to create workspace with dataid for testing ([#9404](https://github.com/hcengineering/platform/issues/9404)) - -## [s0.7.166] - 2025-06-30 - -* 🐛 BUG FIXES: · Adjust live recording ([#9394](https://github.com/hcengineering/platform/issues/9394)) -* QFIX: · Update last visit · Update last visit ([#9401](https://github.com/hcengineering/platform/issues/9401)) · Get pending data id ([#9402](https://github.com/hcengineering/platform/issues/9402)) · Increment attempts on restore retry ([#9403](https://github.com/hcengineering/platform/issues/9403)) - -## [s0.7.165] - 2025-06-27 - -* UBERF-11657: · Better handling of disabled employees ([#9389](https://github.com/hcengineering/platform/issues/9389)) - -## [s0.7.164] - 2025-06-26 - -* QFIX: · Tune backup limits ([#9381](https://github.com/hcengineering/platform/issues/9381)) · Use a fulltext queue for blocked ops ([#9388](https://github.com/hcengineering/platform/issues/9388)) - -## [s0.7.162] - 2025-06-25 - -* EQMS-1406: · Added html doc link presenter for qms docs ([#9382](https://github.com/hcengineering/platform/issues/9382)) -* EQMS-1587: · Fixed impacted doc list selector in qms ([#9374](https://github.com/hcengineering/platform/issues/9374)) -* QFIX: · Statistics contexts ([#9380](https://github.com/hcengineering/platform/issues/9380)) -* QFIX: · Fast migration cmd of created/modified by ([#9375](https://github.com/hcengineering/platform/issues/9375)) - -## [s0.7.160] - 2025-06-25 - -* QFIX: · Reindex inplace ([#9365](https://github.com/hcengineering/platform/issues/9365)) -* QFIX: · Fixed parsing of trailing parentless text and empty markers in the markdown -> markup converter ([#9360](https://github.com/hcengineering/platform/issues/9360)) · Fix attachments in mail threads ([#9371](https://github.com/hcengineering/platform/issues/9371)) -* UBERF-11712: · Rework communication integration ([#9335](https://github.com/hcengineering/platform/issues/9335)) -* UBERF-11786: · Do not notify about old emails ([#9352](https://github.com/hcengineering/platform/issues/9352)) -* UBERF-11798: · Win arm build ([#9366](https://github.com/hcengineering/platform/issues/9366)) - -## [s0.7.158] - 2025-06-24 - -* 🐛 BUG FIXES: · Ignore transcoding for some videos ([#9354](https://github.com/hcengineering/platform/issues/9354)) - -## [s0.7.157] - 2025-06-24 - -* QFIX: · Backup service ([#9347](https://github.com/hcengineering/platform/issues/9347)) -* UBERF-11769: · Fix messages order in mail thread ([#9350](https://github.com/hcengineering/platform/issues/9350)) - -## [s0.7.156] - 2025-06-24 - -* 🐛 BUG FIXES: · Transcode livekit videos uploaded to s3 ([#9342](https://github.com/hcengineering/platform/issues/9342)) · Adjust video player controls ([#9343](https://github.com/hcengineering/platform/issues/9343)) -* QFIX: · Skip communication apply-templates ([#9341](https://github.com/hcengineering/platform/issues/9341)) -* QFIX: · Remove unactual tests and mongo-memory-server ([#9345](https://github.com/hcengineering/platform/issues/9345)) -* UBERF-11769: · Adjust gmail for communication updates ([#9346](https://github.com/hcengineering/platform/issues/9346)) - -## [s0.7.155] - 2025-06-23 - -* QFIX: · Hide deleted workspaces from select workspace list ([#9337](https://github.com/hcengineering/platform/issues/9337)) - -## [s0.7.153] - 2025-06-20 - -* QFIX: · Check last visit in gmail service ([#9300](https://github.com/hcengineering/platform/issues/9300)) -* QFIX: · Backup download skip support ([#9312](https://github.com/hcengineering/platform/issues/9312)) - -## [s0.7.151] - 2025-06-19 - -* QFIX: · Fix errors when disabling gmail synchronization ([#9310](https://github.com/hcengineering/platform/issues/9310)) -* QFIX: · Backup info cors ([#9308](https://github.com/hcengineering/platform/issues/9308)) · Property pass context with OnThreadMessageCreated ([#9311](https://github.com/hcengineering/platform/issues/9311)) - -## [s0.7.150] - 2025-06-19 - -* QFIX: · Optimize getWorkspaceInfo use ([#9302](https://github.com/hcengineering/platform/issues/9302)) -* QFIX: · Tg-bot fix typo in token generation and remove mongo ([#9306](https://github.com/hcengineering/platform/issues/9306)) - -## [s0.7.149] - 2025-06-19 - -* 🐛 BUG FIXES: · Restore markup refs corrupted by github ([#9284](https://github.com/hcengineering/platform/issues/9284)) · Integration template values ([#9294](https://github.com/hcengineering/platform/issues/9294)) · Suppress unused regex escaping ([#9299](https://github.com/hcengineering/platform/issues/9299)) · Restore api client functionality ([#9292](https://github.com/hcengineering/platform/issues/9292)) -* EQMS-1586: · Fixed training related editable state in qms doc's team tab ([#9282](https://github.com/hcengineering/platform/issues/9282)) -* QFIX: · Find options ([#9290](https://github.com/hcengineering/platform/issues/9290)) · Fix parseMail test ([#9303](https://github.com/hcengineering/platform/issues/9303)) -* QFIX: · Ws filter in fixed tool ([#9291](https://github.com/hcengineering/platform/issues/9291)) -* QFIX: · Backup recheck ([#9288](https://github.com/hcengineering/platform/issues/9288)) -* UBERF-11651: · Fix huly id confirmation for dev setup ([#9296](https://github.com/hcengineering/platform/issues/9296)) - -## [s0.7.146] - 2025-06-18 - -* UBERF-8425: · Improve account methods params checks ([#9278](https://github.com/hcengineering/platform/issues/9278)) · Fix created-modified owners tool ([#9283](https://github.com/hcengineering/platform/issues/9283)) - -## [s0.7.145] - 2025-06-17 - -* 🐛 BUG FIXES: · Delete markup description from update ([#9269](https://github.com/hcengineering/platform/issues/9269)) · Initialize server secret before storage config ([#9274](https://github.com/hcengineering/platform/issues/9274)) -* QFIX: · Backup fixes ([#9256](https://github.com/hcengineering/platform/issues/9256)) -* QFIX: · Public links ([#9264](https://github.com/hcengineering/platform/issues/9264)) · Hide achievements until achievement service implementation ([#9266](https://github.com/hcengineering/platform/issues/9266)) · Fix account use in github service ([#9276](https://github.com/hcengineering/platform/issues/9276)) -* UBERF-11586: · Fix gmail migration ([#9277](https://github.com/hcengineering/platform/issues/9277)) -* QFIX: · Ignore ancestor error for txremovedoc ([#9267](https://github.com/hcengineering/platform/issues/9267)) - -## [s0.7.144] - 2025-06-17 - -* 🐛 BUG FIXES: · Show collaborator connection errors ([#9259](https://github.com/hcengineering/platform/issues/9259)) · Handle processing errors in indexer ([#9262](https://github.com/hcengineering/platform/issues/9262)) -* QFIX: · Empty comments in github ([#9258](https://github.com/hcengineering/platform/issues/9258)) -* QFIX: · Restore old workspaces ([#9260](https://github.com/hcengineering/platform/issues/9260)) -* UBERF-11529: · Fix parseMail test ([#9255](https://github.com/hcengineering/platform/issues/9255)) -* UBERF-11533: · Speed up gmail migration ([#9257](https://github.com/hcengineering/platform/issues/9257)) - -## [s0.7.143] - 2025-06-16 - -* 🐛 BUG FIXES: · Empty workspace uuid when generating token ([#9252](https://github.com/hcengineering/platform/issues/9252)) -* QFIX: · Github service startup ([#9251](https://github.com/hcengineering/platform/issues/9251)) - -## [s0.7.142] - 2025-06-16 - -* TOOL: · Reindex all workspaces ([#9249](https://github.com/hcengineering/platform/issues/9249)) - -## [s0.7.141] - 2025-06-15 - -* 🐛 BUG FIXES: · Create transcode topics on start ([#9225](https://github.com/hcengineering/platform/issues/9225)) · Ensure workspace and account uuid when generating token ([#9246](https://github.com/hcengineering/platform/issues/9246)) -* QFIX: · Get workspaces mode is missing ([#9237](https://github.com/hcengineering/platform/issues/9237)) · Rate limits ([#9242](https://github.com/hcengineering/platform/issues/9242)) · Old token ([#9238](https://github.com/hcengineering/platform/issues/9238)) · Restore in workspace service for old workspaces ([#9241](https://github.com/hcengineering/platform/issues/9241)) · Github migration + few checks ([#9244](https://github.com/hcengineering/platform/issues/9244)) -* QFIX: · Fix hang in our rate limitter ([#9243](https://github.com/hcengineering/platform/issues/9243)) · Rate limit hello ([#9245](https://github.com/hcengineering/platform/issues/9245)) -* QFIX: · Make github login/signup case insensitive ([#9247](https://github.com/hcengineering/platform/issues/9247)) - -## [s0.7.140] - 2025-06-13 - -* EQMS-1569: · Fixed ui crash when editing product members ([#9229](https://github.com/hcengineering/platform/issues/9229)) -* EQMS-1576: · Hide region in ws selector for a regular user ([#9231](https://github.com/hcengineering/platform/issues/9231)) -* EQMS-1582: · Fix roles migration ([#9230](https://github.com/hcengineering/platform/issues/9230)) - -## [s0.7.139] - 2025-06-13 - -* 🚀 FEATURES: · Use queue for transcoding tasks ([#9216](https://github.com/hcengineering/platform/issues/9216)) -* 🐛 BUG FIXES: · Properly configure keep-alive in datalake HTTP server ([#9198](https://github.com/hcengineering/platform/issues/9198)) -* UBERF-11451: · Replace ses service with notification service ([#9200](https://github.com/hcengineering/platform/issues/9200)) -* QFIX: · ListView, Table ([#9213](https://github.com/hcengineering/platform/issues/9213)) -* UBERF-11415: · Person cache unit tests ([#9202](https://github.com/hcengineering/platform/issues/9202)) - -## [s0.7.138] - 2025-06-10 - -* QFIX: · Services rate limit ([#9193](https://github.com/hcengineering/platform/issues/9193)) -* UBERF-11415: · Optimise contact UI stores ([#9185](https://github.com/hcengineering/platform/issues/9185)) -* UBERF-9797: · Idp auth state ([#9196](https://github.com/hcengineering/platform/issues/9196)) - -## [s0.7.135] - 2025-06-05 - -* UBERF-11423: · Fix attachments in emails ([#9166](https://github.com/hcengineering/platform/issues/9166)) - -## [s0.7.134] - 2025-06-04 - -* 🐛 BUG FIXES: · Handle links with spaces in markdown ([#9164](https://github.com/hcengineering/platform/issues/9164)) - -## [s0.7.133] - 2025-06-03 - -* 🐛 BUG FIXES: · Datalake memory leak fixes ([#9161](https://github.com/hcengineering/platform/issues/9161)) -* QFIX: · Rpc response body size ([#9160](https://github.com/hcengineering/platform/issues/9160)) -* UBERF-11156: · Decode encoded mail content and subject ([#9157](https://github.com/hcengineering/platform/issues/9157)) -* UBERF-11411: · Add communication threads for emails ([#9156](https://github.com/hcengineering/platform/issues/9156)) -* UBERF-8425: · Fix account upgrade deadlocks ([#9163](https://github.com/hcengineering/platform/issues/9163)) - -## [s0.7.132] - 2025-06-03 - -* 🐛 BUG FIXES: · Get rid of node-fetch ([#9150](https://github.com/hcengineering/platform/issues/9150)) -* UBERF-8425: · Improved pg/acc/ws error handling ([#9144](https://github.com/hcengineering/platform/issues/9144)) - -## [s0.7.131] - 2025-06-02 - -* QFIX: · Build fix for browser list - -## [s0.7.130] - 2025-06-02 - -* 🐛 BUG FIXES: · Correct filename in content-disposition header ([#9139](https://github.com/hcengineering/platform/issues/9139)) -* QFIX: · Do not find for a Card for rank ([#9145](https://github.com/hcengineering/platform/issues/9145)) -* UBERF-11398: · Fixing rate limits ([#9143](https://github.com/hcengineering/platform/issues/9143)) - -## [s0.7.129] - 2025-05-30 - -* UBERF-11392: · Fixes to statistics ([#9138](https://github.com/hcengineering/platform/issues/9138)) -* UBERF-8425: · Retry tx account ([#9133](https://github.com/hcengineering/platform/issues/9133)) - -## [s0.7.128] - 2025-05-29 - -* 🐛 BUG FIXES: · Add stream to ext services ([#9126](https://github.com/hcengineering/platform/issues/9126)) -* UBERF-11383: · Fix gmail push processing ([#9127](https://github.com/hcengineering/platform/issues/9127)) -* UBERF-8425: · Improve parallel ws upgrade logging within one ws service ([#9118](https://github.com/hcengineering/platform/issues/9118)) - -## [s0.7.127] - 2025-05-28 - -* 🐛 BUG FIXES: · Add recorder assets to desktop ([#9122](https://github.com/hcengineering/platform/issues/9122)) · Prevent closing recording popup ([#9125](https://github.com/hcengineering/platform/issues/9125)) -* UBERF-11233: · Fix non-confirmed sign-up/login flow ([#9108](https://github.com/hcengineering/platform/issues/9108)) -* UBERF-11347: · Fix gmail recipients ([#9115](https://github.com/hcengineering/platform/issues/9115)) - -## [s0.7.126] - 2025-05-27 - -* UBERF-11206: · Few more fixes related to Github ([#9117](https://github.com/hcengineering/platform/issues/9117)) - -## [s0.7.125] - 2025-05-27 - -* UBERF-11111: · Add retry package ([#9081](https://github.com/hcengineering/platform/issues/9081)) -* UBERF-11239: · Fix multipart content in mta-hook ([#9110](https://github.com/hcengineering/platform/issues/9110)) -* UBERF-11342: · Fix race conditions handling in mail sync mutex ([#9111](https://github.com/hcengineering/platform/issues/9111)) - -## [s0.7.124] - 2025-05-27 - * BUMP: · Bump few deps with fixes ([#9093](https://github.com/hcengineering/platform/issues/9093)) -* UBERF-11206: · Few fixes related to Github sync ([#9102](https://github.com/hcengineering/platform/issues/9102)) - -## [s0.7.123] - 2025-05-27 - -* 🐛 BUG FIXES: · Remove analytics from SplitLogger ([#9086](https://github.com/hcengineering/platform/issues/9086)) · Do not report some datalake errors to analytics ([#9085](https://github.com/hcengineering/platform/issues/9085)) -* QFIX: · Remove duplicate handler errors ([#9084](https://github.com/hcengineering/platform/issues/9084)) -* UBERF-11175: · Fix new person names in mail ([#9094](https://github.com/hcengineering/platform/issues/9094)) -* UBERF-11203: · Display address in gmail integration ([#9095](https://github.com/hcengineering/platform/issues/9095)) -* UBERF-8425: · More adjustments for migration scripts and tools ([#9099](https://github.com/hcengineering/platform/issues/9099)) - -## [s0.7.122] - 2025-05-23 - -* UBERF-10925: · Save gmail messages only for integration owner ([#9061](https://github.com/hcengineering/platform/issues/9061)) -* UBERF-11004: · Fix mta-hook email content parsing ([#9066](https://github.com/hcengineering/platform/issues/9066)) -* UBERF-11067: · Fix html to md conversion for complex links ([#9070](https://github.com/hcengineering/platform/issues/9070)) - -## [s0.7.121] - 2025-05-23 - +* EQMS-1302: · Fixed RBAC bypass for space / team related wizards and popups (develop port) ([#8979](https://github.com/hcengineering/platform/issues/8979)) +* EQMS-1406: · Added html doc link presenter for qms docs ([#9382](https://github.com/hcengineering/platform/issues/9382)) +* EQMS-1411: · Fixed the approval-to-effective status transition in QMS documents ([#8598](https://github.com/hcengineering/platform/issues/8598)) +* EQMS-1441: · Editable QMS doc reviewers and approvers during requests. ([#8699](https://github.com/hcengineering/platform/issues/8699)) +* EQMS-1443: · Fixed qms-comments position / decoration mapping (develop branch) ([#8119](https://github.com/hcengineering/platform/issues/8119)) · Fix qms-comments position / decoration mapping ([#8118](https://github.com/hcengineering/platform/issues/8118)) +* EQMS-1471: · Fixed authorship and ownership semantics & labels in qms to prevent confusion ([#8629](https://github.com/hcengineering/platform/issues/8629)) +* EQMS-1475: · Space browser for qms documents ([#8668](https://github.com/hcengineering/platform/issues/8668)) +* EQMS-1484: · Fixed permission checks when sending a QMS document for approval from the Workflow Validation tab ([#8628](https://github.com/hcengineering/platform/issues/8628)) +* EQMS-1510: · QMS documents now start at version 1.0 instead of 0.1 ([#8669](https://github.com/hcengineering/platform/issues/8669)) +* EQMS-1524: · Fixed c-state cleanup after state transition in qms docs ([#8697](https://github.com/hcengineering/platform/issues/8697)) +* EQMS-1537: · Cleanup all review/approval requests on qms doc deletion ([#8790](https://github.com/hcengineering/platform/issues/8790)) +* EQMS-1541: · Fixes TeamStep in qms document wizard ([#8840](https://github.com/hcengineering/platform/issues/8840)) +* EQMS-1548: · TraceX desktop app ([#9666](https://github.com/hcengineering/platform/issues/9666)) * EQMS-1560: · Fix & migrate duplicate active review/approval requests in qms ([#9062](https://github.com/hcengineering/platform/issues/9062)) * EQMS-1561: · Fixed approval/review requests query when switching between documents ([#9059](https://github.com/hcengineering/platform/issues/9059)) * EQMS-1569: · Fixed ui crash when editing product members ([#9229](https://github.com/hcengineering/platform/issues/9229)) @@ -459,450 +168,195 @@ Changelog. * QFIX: · The numbers look like emojis ([#8266](https://github.com/hcengineering/platform/issues/8266)) · ListView header ([#8570](https://github.com/hcengineering/platform/issues/8570)) · Remove dev migrations ([#8651](https://github.com/hcengineering/platform/issues/8651)) · Emoji input detection ([#8927](https://github.com/hcengineering/platform/issues/8927)) · Make github login/signup case insensitive ([#9247](https://github.com/hcengineering/platform/issues/9247)) · Ws filter in fixed tool ([#9291](https://github.com/hcengineering/platform/issues/9291)) · Tg-bot fix typo in token generation and remove mongo ([#9306](https://github.com/hcengineering/platform/issues/9306)) · Fast migration cmd of created/modified by ([#9375](https://github.com/hcengineering/platform/issues/9375)) · Adding/deleting social ids ([#9466](https://github.com/hcengineering/platform/issues/9466)) · Qms tests ([#9557](https://github.com/hcengineering/platform/issues/9557)) · Org space auto join in import ([#9562](https://github.com/hcengineering/platform/issues/9562)) · Calendar user migration ([#9707](https://github.com/hcengineering/platform/issues/9707)) · Do not cache desktop packaging results ([#9708](https://github.com/hcengineering/platform/issues/9708)) · Migrate deleted social ids in calendar events user ([#9709](https://github.com/hcengineering/platform/issues/9709)) * UBER-1290: · Fix refresh token in subsequent requests ([#9550](https://github.com/hcengineering/platform/issues/9550)) * UBERF-10222: · Fix Github reviews field update ([#9013](https://github.com/hcengineering/platform/issues/9013)) -* UBERF-10672: · Fix person duplicates ([#9004](https://github.com/hcengineering/platform/issues/9004)) - -## [s0.7.116] - 2025-05-21 - +* UBERF-10224: · Always include a link target in the markup when rendering (QFix) ([#8566](https://github.com/hcengineering/platform/issues/8566)) +* UBERF-10248: · Fix timezone loading ([#8586](https://github.com/hcengineering/platform/issues/8586)) · Fix avatar status in compact mode ([#8583](https://github.com/hcengineering/platform/issues/8583)) · Fix local time ([#8623](https://github.com/hcengineering/platform/issues/8623)) · Reduce profile preview size ([#8723](https://github.com/hcengineering/platform/issues/8723)) +* UBERF-10254: · Manage own social ids ([#9398](https://github.com/hcengineering/platform/issues/9398)) +* UBERF-10272: · Allow workspace owners to enable/disable modules ([#8638](https://github.com/hcengineering/platform/issues/8638)) +* UBERF-10303: · Always sign up with OTP ([#8665](https://github.com/hcengineering/platform/issues/8665)) +* UBERF-10346: · Combined ensure person ([#8701](https://github.com/hcengineering/platform/issues/8701)) +* UBERF-10368: · Fix direct create ([#8760](https://github.com/hcengineering/platform/issues/8760)) +* UBERF-10375: · Fix full email messages sync ([#8758](https://github.com/hcengineering/platform/issues/8758)) +* UBERF-10376: · Allow todos filtering ([#8729](https://github.com/hcengineering/platform/issues/8729)) +* UBERF-10386: · Fix SES client ([#8737](https://github.com/hcengineering/platform/issues/8737)) +* UBERF-10407: · Fix Team display ([#8762](https://github.com/hcengineering/platform/issues/8762)) +* UBERF-10408: · New gmail integration ([#8869](https://github.com/hcengineering/platform/issues/8869)) +* UBERF-10412: · Make rate limit less aggressive ([#8765](https://github.com/hcengineering/platform/issues/8765)) +* UBERF-10413: · Fix update of %hash% and update migration ([#8771](https://github.com/hcengineering/platform/issues/8771)) +* UBERF-10417: · Corrected red and green colors, icons ([#8808](https://github.com/hcengineering/platform/issues/8808)) +* UBERF-10418: · Removed empty popup in the user's personal room ([#8775](https://github.com/hcengineering/platform/issues/8775)) +* UBERF-10441: · Fix configure and board item displayed ([#8789](https://github.com/hcengineering/platform/issues/8789)) +* UBERF-10471: · Fix Github miss status updates and allow to re-integrate existing repos ([#8842](https://github.com/hcengineering/platform/issues/8842)) +* UBERF-10491: · Fix gmail client duplicates ([#8837](https://github.com/hcengineering/platform/issues/8837)) +* UBERF-10499: · Fix team planner ([#8847](https://github.com/hcengineering/platform/issues/8847)) +* UBERF-10523: · Fixes for backup/compact ([#8888](https://github.com/hcengineering/platform/issues/8888)) +* UBERF-10525: · Update MTA-hook integration ([#8925](https://github.com/hcengineering/platform/issues/8925)) +* UBERF-10550: · Support internal endpoint in getLoginInfoByToken ([#8902](https://github.com/hcengineering/platform/issues/8902)) +* UBERF-10555: · Fix gmail migration ([#8900](https://github.com/hcengineering/platform/issues/8900)) * UBERF-10557: · Use communication queue ([#8993](https://github.com/hcengineering/platform/issues/8993)) - -## [s0.7.115] - 2025-05-21 - -* UBERF-10669: · Fix email channel duplicates ([#8996](https://github.com/hcengineering/platform/issues/8996)) -* UBERF-10649: · Fix last visit for stale workspaces ([#8999](https://github.com/hcengineering/platform/issues/8999)) - -## [s0.7.114] - 2025-05-20 - -* UBERF-10653: · Handle gmail integration errors ([#8985](https://github.com/hcengineering/platform/issues/8985)) -* UBERF-10664: · Fix person preview ([#8995](https://github.com/hcengineering/platform/issues/8995)) -* UBERF-8425: · Speed up accounts migration ([#8994](https://github.com/hcengineering/platform/issues/8994)) - -## [s0.7.112] - 2025-05-19 - -* EQMS-1302: · Fixed RBAC bypass for space / team related wizards and popups (develop port) ([#8979](https://github.com/hcengineering/platform/issues/8979)) -* UBERF-10632: · Fix email thread creation date ([#8968](https://github.com/hcengineering/platform/issues/8968)) - -## [s0.7.111] - 2025-05-16 - -* UBERF-10631: · Fix attachments in old gmail integration ([#8971](https://github.com/hcengineering/platform/issues/8971)) - -## [s0.7.110] - 2025-05-16 - -* 🐛 BUG FIXES: · Remove datalake extra retries on 404 ([#8962](https://github.com/hcengineering/platform/issues/8962)) -* UBERF-10637: · Fix duplication on github with few integerations added ([#8970](https://github.com/hcengineering/platform/issues/8970)) -* UBERF-10626: · Fix social identity duplicate key exception ([#8969](https://github.com/hcengineering/platform/issues/8969)) - -## [s0.7.109] - 2025-05-16 - -* QFIX: · Fix MTA-hook token ([#8954](https://github.com/hcengineering/platform/issues/8954)) - -## [s0.7.108] - 2025-05-15 - -* 🐛 BUG FIXES: · Adjust embedded video player style ([#8947](https://github.com/hcengineering/platform/issues/8947)) * UBERF-10590: · Suport disabled integrations ([#8937](https://github.com/hcengineering/platform/issues/8937)) * UBERF-10593: · Fix MTA hook and reuse clients ([#8938](https://github.com/hcengineering/platform/issues/8938)) * UBERF-10599: · Fix ws not found in gmail ([#8943](https://github.com/hcengineering/platform/issues/8943)) -* UBERF-10308: · Adjust onboarding ([#8949](https://github.com/hcengineering/platform/issues/8949)) -* UBERF-10455: · Merge accounts for merged persons ([#8942](https://github.com/hcengineering/platform/issues/8942)) - -## [s0.7.107] - 2025-05-14 - -* QFIX: · Updated color for disabled mentions ([#8918](https://github.com/hcengineering/platform/issues/8918)) · Add ws dependency to gmail ([#8922](https://github.com/hcengineering/platform/issues/8922)) · Continue processing other workspaces in case of error ([#8923](https://github.com/hcengineering/platform/issues/8923)) -* QFIX: · Emoji input detection ([#8927](https://github.com/hcengineering/platform/issues/8927)) -* UBERF-10525: · Update MTA-hook integration ([#8925](https://github.com/hcengineering/platform/issues/8925)) - -## [s0.7.106] - 2025-05-14 - -* 🐛 BUG FIXES: · Close room popup on stop share ([#8914](https://github.com/hcengineering/platform/issues/8914)) -* QFIX: · Add external ws for gmail ([#8910](https://github.com/hcengineering/platform/issues/8910)) - -## [s0.7.105] - 2025-05-13 - -* QFIX: · Keep snapshots ([#8904](https://github.com/hcengineering/platform/issues/8904)) -* UBERF-10550: · Support internal endpoint in getLoginInfoByToken ([#8902](https://github.com/hcengineering/platform/issues/8902)) -* UBERF-10555: · Fix gmail migration ([#8900](https://github.com/hcengineering/platform/issues/8900)) - -## [s0.7.104] - 2025-05-12 - -* 🐛 BUG FIXES: · Storage adapter throw error on stat ([#8893](https://github.com/hcengineering/platform/issues/8893)) - -## [s0.7.103] - 2025-05-12 - -* UBERF-10408: · New gmail integration ([#8869](https://github.com/hcengineering/platform/issues/8869)) -* UBERF-10523: · Fixes for backup/compact ([#8888](https://github.com/hcengineering/platform/issues/8888)) - -## [s0.7.102] - 2025-05-08 - -* 🐛 BUG FIXES: · Close media popup on meeting leave ([#8877](https://github.com/hcengineering/platform/issues/8877)) - -## [s0.7.101] - 2025-05-07 - -* UBERF-10471: · Fix Github miss status updates and allow to re-integrate existing repos ([#8842](https://github.com/hcengineering/platform/issues/8842)) -* UBERF-10488: · Allow ws limit per account ([#8864](https://github.com/hcengineering/platform/issues/8864)) - -## [s0.7.100] - 2025-05-06 - -* EQMS-1541: · Fixes TeamStep in qms document wizard ([#8840](https://github.com/hcengineering/platform/issues/8840)) -* UBERF-10491: · Fix gmail client duplicates ([#8837](https://github.com/hcengineering/platform/issues/8837)) -* UBERF-10499: · Fix team planner ([#8847](https://github.com/hcengineering/platform/issues/8847)) -* UBERF-9559: · Make CR accounts migrations concurrency safe ([#8821](https://github.com/hcengineering/platform/issues/8821)) -* UBERF-10255: · Migrate accounts in saved filters ([#8846](https://github.com/hcengineering/platform/issues/8846)) - -## [s0.7.99] - 2025-05-02 - -* LOVE: · Updated ParticipantView ([#8811](https://github.com/hcengineering/platform/issues/8811)) -* UBERF-10417: · Corrected red and green colors, icons ([#8808](https://github.com/hcengineering/platform/issues/8808)) -* UBERF-10454: · Support ensure person by system user ([#8807](https://github.com/hcengineering/platform/issues/8807)) - -## [s0.7.98] - 2025-05-01 - -* QFIX: · Added appropriate styling for unavailable mentions ([#8803](https://github.com/hcengineering/platform/issues/8803)) - -## [s0.7.95] - 2025-04-30 - -* EQMS-1537: · Cleanup all review/approval requests on qms doc deletion ([#8790](https://github.com/hcengineering/platform/issues/8790)) -* QFIX: · Improve backup find tool ([#8783](https://github.com/hcengineering/platform/issues/8783)) -* UBERF-10441: · Fix configure and board item displayed ([#8789](https://github.com/hcengineering/platform/issues/8789)) - -## [s0.7.94] - 2025-04-30 - -* QFIX: · Fix gmail history migration ([#8778](https://github.com/hcengineering/platform/issues/8778)) -* UBERF-10418: · Removed empty popup in the user's personal room ([#8775](https://github.com/hcengineering/platform/issues/8775)) -* QFIX: · Disable mixins migration - -## [s0.7.93] - 2025-04-30 - -* QFIX: · Use front base for datalake ([#8772](https://github.com/hcengineering/platform/issues/8772)) - -## [s0.7.92] - 2025-04-29 - -* EQMS-1441: · Editable QMS doc reviewers and approvers during requests. ([#8699](https://github.com/hcengineering/platform/issues/8699)) -* UBERF-10368: · Fix direct create ([#8760](https://github.com/hcengineering/platform/issues/8760)) -* UBERF-10375: · Fix full email messages sync ([#8758](https://github.com/hcengineering/platform/issues/8758)) -* UBERF-10407: · Fix Team display ([#8762](https://github.com/hcengineering/platform/issues/8762)) -* UBERF-10412: · Make rate limit less aggressive ([#8765](https://github.com/hcengineering/platform/issues/8765)) -* UBERF-10413: · Fix update of %hash% and update migration ([#8771](https://github.com/hcengineering/platform/issues/8771)) -* EQMS-1533: · Fix template versions query ([#8753](https://github.com/hcengineering/platform/issues/8753)) ([#8766](https://github.com/hcengineering/platform/issues/8766)) - -## [s0.7.91] - 2025-04-28 - -* 🐛 BUG FIXES: · Remove file upload console log ([#8722](https://github.com/hcengineering/platform/issues/8722)) · Add more logs do collaborator doc saving ([#8721](https://github.com/hcengineering/platform/issues/8721)) · Adjust media popup styles ([#8724](https://github.com/hcengineering/platform/issues/8724)) -* QFIX: · Admin panel show inactive workspaces ([#8716](https://github.com/hcengineering/platform/issues/8716)) -* UBERF-10248: · Reduce profile preview size ([#8723](https://github.com/hcengineering/platform/issues/8723)) -* UBERF-10376: · Allow todos filtering ([#8729](https://github.com/hcengineering/platform/issues/8729)) -* UBERF-10386: · Fix SES client ([#8737](https://github.com/hcengineering/platform/issues/8737)) -* UBERF-9764: · Adjust gmail for new accounts ([#8681](https://github.com/hcengineering/platform/issues/8681)) - -## [s0.7.90] - 2025-04-25 - -* 🐛 BUG FIXES: · Increase beforeAll timeout on API tests ([#8707](https://github.com/hcengineering/platform/issues/8707)) -* UBERF-10222: · Add logging ([#8709](https://github.com/hcengineering/platform/issues/8709)) -* UBERF-10342: · Fix init script executor ([#8702](https://github.com/hcengineering/platform/issues/8702)) - -## [s0.7.89] - 2025-04-25 - -* EQMS-1510: · QMS documents now start at version 1.0 instead of 0.1 ([#8669](https://github.com/hcengineering/platform/issues/8669)) -* EQMS-1524: · Fixed c-state cleanup after state transition in qms docs ([#8697](https://github.com/hcengineering/platform/issues/8697)) -* UBERF-10346: · Combined ensure person ([#8701](https://github.com/hcengineering/platform/issues/8701)) - -## [s0.7.87] - 2025-04-23 - -* 🚀 FEATURES: · Media state plugin ([#8674](https://github.com/hcengineering/platform/issues/8674)) -* EQMS-1475: · Space browser for qms documents ([#8668](https://github.com/hcengineering/platform/issues/8668)) -* UBERF-10303: · Always sign up with OTP ([#8665](https://github.com/hcengineering/platform/issues/8665)) -* UBERF-10318: · Fix push subscriptions ([#8666](https://github.com/hcengineering/platform/issues/8666)) - -## [s0.7.86] - 2025-04-22 - -* QFIX: · Remove dev migrations ([#8651](https://github.com/hcengineering/platform/issues/8651)) - -## [s0.7.85] - 2025-04-21 - -* EQMS-1471: · Fixed authorship and ownership semantics & labels in qms to prevent confusion ([#8629](https://github.com/hcengineering/platform/issues/8629)) -* EQMS-1484: · Fixed permission checks when sending a QMS document for approval from the Workflow Validation tab ([#8628](https://github.com/hcengineering/platform/issues/8628)) -* QFIX: · Show warning of not visited workspace only once ([#8641](https://github.com/hcengineering/platform/issues/8641)) · Region filter in admin panel ([#8646](https://github.com/hcengineering/platform/issues/8646)) -* UBERF-10272: · Allow workspace owners to enable/disable modules ([#8638](https://github.com/hcengineering/platform/issues/8638)) - -## [s0.7.83] - 2025-04-18 - -* 🐛 BUG FIXES: · Append port to s3 endpoint ([#8601](https://github.com/hcengineering/platform/issues/8601)) -* UBERF-10248: · Fix local time ([#8623](https://github.com/hcengineering/platform/issues/8623)) - -## [s0.7.81] - 2025-04-17 - -* 🐛 BUG FIXES: · Handle pong message in presence client ([#8597](https://github.com/hcengineering/platform/issues/8597)) -* EQMS-1411: · Fixed the approval-to-effective status transition in QMS documents ([#8598](https://github.com/hcengineering/platform/issues/8598)) -* LOVE: · Correcting the layout ([#8599](https://github.com/hcengineering/platform/issues/8599)) -* QFIX: · Build for external PRs. ([#8579](https://github.com/hcengineering/platform/issues/8579)) -* UBERF-10248: · Fix timezone loading ([#8586](https://github.com/hcengineering/platform/issues/8586)) · Fix avatar status in compact mode ([#8583](https://github.com/hcengineering/platform/issues/8583)) -* UBERF-9521: · Refactor session manager ([#8560](https://github.com/hcengineering/platform/issues/8560)) -* UBERF-9578: · The correct display of the user's personal avatar in Direct messages. ([#8595](https://github.com/hcengineering/platform/issues/8595)) -* UBERF-9756: · Speed up CR account migrations ([#8573](https://github.com/hcengineering/platform/issues/8573)) -* UBERF-10227: · Fix createdOn type in getUserWorkspaces ([#8584](https://github.com/hcengineering/platform/issues/8584)) -* UBERF-10252: · Fix collaborators activity presenter ([#8594](https://github.com/hcengineering/platform/issues/8594)) - -## [s0.7.80] - 2025-04-15 - -* QFIX: · ListView header ([#8570](https://github.com/hcengineering/platform/issues/8570)) -* UBERF-10224: · Always include a link target in the markup when rendering (QFix) ([#8566](https://github.com/hcengineering/platform/issues/8566)) -* UBERF-10228: · Expose release social id to services ([#8562](https://github.com/hcengineering/platform/issues/8562)) - -## [s0.7.79] - 2025-04-15 - -* QFIX: · Fix default timezone ([#8547](https://github.com/hcengineering/platform/issues/8547)) -* UBERF-9724: · Fix github functionality on 0.7 ([#8554](https://github.com/hcengineering/platform/issues/8554)) - -## [s0.7.78] - 2025-04-15 - -* UBERF-9716: · New profile preview and initial achievements ([#8504](https://github.com/hcengineering/platform/issues/8504)) - -## [s0.7.77] - 2025-04-15 - -* UBERF-9604: · Add edit permission check per row in Table ([#8528](https://github.com/hcengineering/platform/issues/8528)) - -## [s0.7.76] - 2025-04-14 - -* UBERF-9724: · Use updated accounts ([#8452](https://github.com/hcengineering/platform/issues/8452)) - -## [s0.7.75] - 2025-04-11 - -* 🐛 BUG FIXES: · Add error reporting in datalake ([#8535](https://github.com/hcengineering/platform/issues/8535)) - -## [s0.7.73] - 2025-04-10 - -* 🐛 BUG FIXES: · Adjust text editor quote and hr styles ([#8524](https://github.com/hcengineering/platform/issues/8524)) - -## [s0.7.72] - 2025-04-10 - -* UBERF-9754: · Fix account timestamp ([#8520](https://github.com/hcengineering/platform/issues/8520)) - -## [s0.7.70] - 2025-04-10 - -* UBERF-9530: · Support old guest link ([#8506](https://github.com/hcengineering/platform/issues/8506)) - -## [s0.7.68] - 2025-04-09 - -* QFIX: · Keep alive connections in account client ([#8503](https://github.com/hcengineering/platform/issues/8503)) -* UBERF-9732: · Use huly id as primary social id ([#8499](https://github.com/hcengineering/platform/issues/8499)) -* UBERF-9752: · Properly handle streams to avoid datalake memory leak ([#8502](https://github.com/hcengineering/platform/issues/8502)) - -## [s0.7.67] - 2025-04-08 - -* 🐛 BUG FIXES: · Handle token error in collaborator service ([#8493](https://github.com/hcengineering/platform/issues/8493)) -* UBERF-9726: · Fix integrations in accounts for CR 24.1 ([#8490](https://github.com/hcengineering/platform/issues/8490)) -* UBERF-9739: · Try to fix backup hang ([#8496](https://github.com/hcengineering/platform/issues/8496)) -* UBERF-9748: · Refactor server-ws ([#8495](https://github.com/hcengineering/platform/issues/8495)) - -## [s0.7.66] - 2025-04-07 - -* QFIX: · Fix huge statistics send ([#8483](https://github.com/hcengineering/platform/issues/8483)) - -## [s0.7.65] - 2025-04-07 - -* UBERF-9734: · Set default account timezone ([#8469](https://github.com/hcengineering/platform/issues/8469)) -* UBERF-9740: · Send mail errors to Sentry ([#8481](https://github.com/hcengineering/platform/issues/8481)) -* UBERF-9726: · Manage integrations in accounts ([#8475](https://github.com/hcengineering/platform/issues/8475)) - -## [s0.7.64] - 2025-04-04 - -* UBERF-9736: · Fix backup hang ([#8468](https://github.com/hcengineering/platform/issues/8468)) - -## [s0.7.62] - 2025-04-03 - -* LOVE: · Fixed the size for avatars ([#8443](https://github.com/hcengineering/platform/issues/8443)) - -## [s0.7.61] - 2025-04-02 - -* QFIX: · Show proper current employee ([#8435](https://github.com/hcengineering/platform/issues/8435)) -* UBERF-9703: · Profile cards for persons ([#8410](https://github.com/hcengineering/platform/issues/8410)) -* UBERF-9727: · Allow adding social id to existing person ([#8439](https://github.com/hcengineering/platform/issues/8439)) - -## [s0.7.60] - 2025-04-02 - -* ⚙️ MISCELLANEOUS TASKS: · Update tiptap & hocuspocus ([#8428](https://github.com/hcengineering/platform/issues/8428)) -* LOVE: · Updated ParticipantView layout ([#8426](https://github.com/hcengineering/platform/issues/8426)) -* UBERF-9725: · Fix accounts mismatch in plugins config ([#8430](https://github.com/hcengineering/platform/issues/8430)) - -## [s0.7.59] - 2025-04-02 - -* 🐛 BUG FIXES: · Reduce amount of text typed ([#8414](https://github.com/hcengineering/platform/issues/8414)) · Build msg2file container ([#8424](https://github.com/hcengineering/platform/issues/8424)) -* QFIX: · Fix list applications display ([#8417](https://github.com/hcengineering/platform/issues/8417)) -* QFIX: · Fix duplicated secret env ([#8416](https://github.com/hcengineering/platform/issues/8416)) -* UBERF-9639: · Master-detail view for cards ([#8413](https://github.com/hcengineering/platform/issues/8413)) -* UBERF-9694: · Queue processing improvements ([#8418](https://github.com/hcengineering/platform/issues/8418)) -* UBERF-9714: · Support subsecutive meeting joins ([#8421](https://github.com/hcengineering/platform/issues/8421)) - -## [s0.7.58] - 2025-04-01 - -* 🐛 BUG FIXES: · Update settings context on changes & update system theme ([#8405](https://github.com/hcengineering/platform/issues/8405)) - -## [s0.7.57] - 2025-03-31 - -* UBERF-9712: · Improve mail TLS settings and logs for self hosters ([#8399](https://github.com/hcengineering/platform/issues/8399)) ([#8400](https://github.com/hcengineering/platform/issues/8400)) -* UBERF-9713: · Fix auto join condition ([#8404](https://github.com/hcengineering/platform/issues/8404)) - -## [s0.7.55] - 2025-03-31 - -* UBERF-9711: · Add by region groupping for admin console ([#8396](https://github.com/hcengineering/platform/issues/8396)) - -## [s0.7.54] - 2025-03-30 - -* TXU-105: · Fix model lookups ([#8386](https://github.com/hcengineering/platform/issues/8386)) - -## [s0.7.53] - 2025-03-29 - -* UBERF-9636: · Meeting links - more cases ([#8369](https://github.com/hcengineering/platform/issues/8369)) -* UBERF-9705: · Fix issues labels add remove ([#8373](https://github.com/hcengineering/platform/issues/8373)) -* UBERF-9710: · Fix always on status on front ([#8391](https://github.com/hcengineering/platform/issues/8391)) -* UBERF-9603: · Fix account rename ([#8371](https://github.com/hcengineering/platform/issues/8371)) - -## [s0.7.52] - 2025-03-27 - -* 🐛 BUG FIXES: · Handle double quotes in etag ([#8362](https://github.com/hcengineering/platform/issues/8362)) -* UBERF-9698: · Fix identity swap issue ([#8360](https://github.com/hcengineering/platform/issues/8360)) - -## [s0.7.51] - 2025-03-26 - -* 🐛 BUG FIXES: · Exclude first segment from account cookie domain ([#8348](https://github.com/hcengineering/platform/issues/8348)) · Remove quotes from print blob id ([#8356](https://github.com/hcengineering/platform/issues/8356)) -* PLATF-8339: · Allow test project editing(dev) ([#8354](https://github.com/hcengineering/platform/issues/8354)) - -## [s0.7.49] - 2025-03-25 - -* UBERF-9693: · Allow to reindex from migration ([#8345](https://github.com/hcengineering/platform/issues/8345)) - -## [s0.7.48] - 2025-03-25 - -* 🐛 BUG FIXES: · Handle token errors in front service ([#8336](https://github.com/hcengineering/platform/issues/8336)) -* UBERF-9636: · Meeting links ([#8334](https://github.com/hcengineering/platform/issues/8334)) -* UBERF-9691: · Expose full social ids in own account ([#8340](https://github.com/hcengineering/platform/issues/8340)) - -## [s0.7.45] - 2025-03-21 - -* UBERF-9624: · Add card viewlet settings ([#8258](https://github.com/hcengineering/platform/issues/8258)) - -## [s0.7.44] - 2025-03-21 - -* 🐛 BUG FIXES: · Multipart upload in datalake service ([#8307](https://github.com/hcengineering/platform/issues/8307)) -* QFIX: · Add support for recording videos from desktop ([#8306](https://github.com/hcengineering/platform/issues/8306)) -* UBERF-9671: · Fix gh accounts migration ([#8308](https://github.com/hcengineering/platform/issues/8308)) - -## [s0.7.43] - 2025-03-21 - -* UBERF-9670: · Fix reply avatars ([#8302](https://github.com/hcengineering/platform/issues/8302)) - -## [s0.7.42] - 2025-03-20 - -* LOVE: · Updated ParticipantView layout ([#8287](https://github.com/hcengineering/platform/issues/8287)) -* QFIX: · Code block styling fixes ([#8289](https://github.com/hcengineering/platform/issues/8289)) -* UBERF-9503: · Generated social ids ([#8208](https://github.com/hcengineering/platform/issues/8208)) - -## [s0.7.41] - 2025-03-19 - -* 🐛 BUG FIXES: · Use host + port in datalake address ([#8276](https://github.com/hcengineering/platform/issues/8276)) -* QFIX: · DocGuest set status cause tx and error record ([#8279](https://github.com/hcengineering/platform/issues/8279)) ([#8282](https://github.com/hcengineering/platform/issues/8282)) · Pg object query ([#8284](https://github.com/hcengineering/platform/issues/8284)) -* UBERF-9661: · Use MAIL_URL env for mail integration ([#8272](https://github.com/hcengineering/platform/issues/8272)) -* UBERF-9663: · Improve mail logging ([#8275](https://github.com/hcengineering/platform/issues/8275)) - -## [s0.7.40] - 2025-03-19 - -* LOVE: · Updated layout of floors and ParticipantView ([#8270](https://github.com/hcengineering/platform/issues/8270)) -* QFIX: · The numbers look like emojis ([#8266](https://github.com/hcengineering/platform/issues/8266)) -* QFIX: · Fix issues with hls player ([#8268](https://github.com/hcengineering/platform/issues/8268)) · Upload a folder produces duplicates ([#8269](https://github.com/hcengineering/platform/issues/8269)) - -## [s0.7.38] - 2025-03-17 - -* 🐛 BUG FIXES: · Datalake fixes ([#8251](https://github.com/hcengineering/platform/issues/8251)) - -## [s0.7.37] - 2025-03-17 - -* UBERF-9633: · Reduce migration calls during workspace creation ([#8242](https://github.com/hcengineering/platform/issues/8242)) ([#8244](https://github.com/hcengineering/platform/issues/8244)) · More proper fix ([#8249](https://github.com/hcengineering/platform/issues/8249)) - -## [s0.7.36] - 2025-03-17 - -* 🐛 BUG FIXES: · Use huly.local in webpack proxy config ([#8233](https://github.com/hcengineering/platform/issues/8233)) -* QFIX: · Account migration mongo to cr ([#8237](https://github.com/hcengineering/platform/issues/8237)) - -## [s0.7.34] - 2025-03-14 - -* 🐛 BUG FIXES: · Use readonly connections for guests ([#8221](https://github.com/hcengineering/platform/issues/8221)) -* QFIX: · (drive) add title providers for files and folders ([#8224](https://github.com/hcengineering/platform/issues/8224)) -* UBERF-9500: · Fix indexing on staging ([#8231](https://github.com/hcengineering/platform/issues/8231)) -* QFIX: · Remove cf workers to fix ci/build on develop branch ([#8226](https://github.com/hcengineering/platform/issues/8226)) - -## [s0.7.30] - 2025-03-12 - -* 🐛 BUG FIXES: · Image preview not displayed ([#8207](https://github.com/hcengineering/platform/issues/8207)) - -## [s0.7.29] - 2025-03-11 - -* 🐛 BUG FIXES: · Encode content disposition file name ([#8190](https://github.com/hcengineering/platform/issues/8190)) · Enhance datalake performance logging ([#8197](https://github.com/hcengineering/platform/issues/8197)) -* UBERF-9605: · Test MTA hook integration ([#8189](https://github.com/hcengineering/platform/issues/8189)) -* UBERF-9606: · Limit a number of workspaces per user ([#8192](https://github.com/hcengineering/platform/issues/8192)) ([#8199](https://github.com/hcengineering/platform/issues/8199)) - -## [s0.7.28] - 2025-03-10 - -* 🚀 FEATURES: · Add datalake service ([#8184](https://github.com/hcengineering/platform/issues/8184)) +* UBERF-10631: · Fix attachments in old gmail integration ([#8971](https://github.com/hcengineering/platform/issues/8971)) +* UBERF-10632: · Fix email thread creation date ([#8968](https://github.com/hcengineering/platform/issues/8968)) +* UBERF-10637: · Fix duplication on github with few integerations added ([#8970](https://github.com/hcengineering/platform/issues/8970)) +* UBERF-10653: · Handle gmail integration errors ([#8985](https://github.com/hcengineering/platform/issues/8985)) +* UBERF-10664: · Fix person preview ([#8995](https://github.com/hcengineering/platform/issues/8995)) +* UBERF-10669: · Fix email channel duplicates ([#8996](https://github.com/hcengineering/platform/issues/8996)) +* UBERF-10672: · Fix person duplicates ([#9004](https://github.com/hcengineering/platform/issues/9004)) +* UBERF-10691: · Fix user selection component to not miss selection ([#9017](https://github.com/hcengineering/platform/issues/9017)) +* UBERF-10741: · The application name has been corrected and the Customize label has been added ([#9056](https://github.com/hcengineering/platform/issues/9056)) +* UBERF-10925: · Save gmail messages only for integration owner ([#9061](https://github.com/hcengineering/platform/issues/9061)) +* UBERF-11004: · Fix mta-hook email content parsing ([#9066](https://github.com/hcengineering/platform/issues/9066)) +* UBERF-11067: · Fix html to md conversion for complex links ([#9070](https://github.com/hcengineering/platform/issues/9070)) +* UBERF-11111: · Add retry package ([#9081](https://github.com/hcengineering/platform/issues/9081)) +* UBERF-11156: · Decode encoded mail content and subject ([#9157](https://github.com/hcengineering/platform/issues/9157)) +* UBERF-11175: · Fix new person names in mail ([#9094](https://github.com/hcengineering/platform/issues/9094)) +* UBERF-11203: · Display address in gmail integration ([#9095](https://github.com/hcengineering/platform/issues/9095)) +* UBERF-11206: · Few fixes related to Github sync ([#9102](https://github.com/hcengineering/platform/issues/9102)) · Few more fixes related to Github ([#9117](https://github.com/hcengineering/platform/issues/9117)) +* UBERF-11233: · Fix non-confirmed sign-up/login flow ([#9108](https://github.com/hcengineering/platform/issues/9108)) +* UBERF-11239: · Fix multipart content in mta-hook ([#9110](https://github.com/hcengineering/platform/issues/9110)) +* UBERF-11342: · Fix race conditions handling in mail sync mutex ([#9111](https://github.com/hcengineering/platform/issues/9111)) +* UBERF-11347: · Fix gmail recipients ([#9115](https://github.com/hcengineering/platform/issues/9115)) +* UBERF-11383: · Fix gmail push processing ([#9127](https://github.com/hcengineering/platform/issues/9127)) +* UBERF-11392: · Fixes to statistics ([#9138](https://github.com/hcengineering/platform/issues/9138)) +* UBERF-11398: · Fixing rate limits ([#9143](https://github.com/hcengineering/platform/issues/9143)) +* UBERF-11411: · Add communication threads for emails ([#9156](https://github.com/hcengineering/platform/issues/9156)) +* UBERF-11414: · Integrations ([#9610](https://github.com/hcengineering/platform/issues/9610)) +* UBERF-11415: · Optimise contact UI stores ([#9185](https://github.com/hcengineering/platform/issues/9185)) +* UBERF-11423: · Fix attachments in emails ([#9166](https://github.com/hcengineering/platform/issues/9166)) +* UBERF-11451: · Replace ses service with notification service ([#9200](https://github.com/hcengineering/platform/issues/9200)) +* UBERF-11529: · Fix parseMail test ([#9255](https://github.com/hcengineering/platform/issues/9255)) +* UBERF-11533: · Speed up gmail migration ([#9257](https://github.com/hcengineering/platform/issues/9257)) +* UBERF-11586: · Fix gmail migration ([#9277](https://github.com/hcengineering/platform/issues/9277)) +* UBERF-11657: · Better handling of disabled employees ([#9389](https://github.com/hcengineering/platform/issues/9389)) +* UBERF-11712: · Rework communication integration ([#9335](https://github.com/hcengineering/platform/issues/9335)) +* UBERF-11769: · Adjust gmail for communication updates ([#9346](https://github.com/hcengineering/platform/issues/9346)) · Fix messages order in mail thread ([#9350](https://github.com/hcengineering/platform/issues/9350)) +* UBERF-11786: · Do not notify about old emails ([#9352](https://github.com/hcengineering/platform/issues/9352)) +* UBERF-11998: · Support account deletion from admin page ([#9441](https://github.com/hcengineering/platform/issues/9441)) +* UBERF-12146: · Fix queries with lookup conditions ([#9495](https://github.com/hcengineering/platform/issues/9495)) +* UBERF-12149: · Fix email send with irrelevant social id ([#9452](https://github.com/hcengineering/platform/issues/9452)) +* UBERF-12214: · Fix GitHub assignee update ([#9515](https://github.com/hcengineering/platform/issues/9515)) +* UBERF-12227: · Stabilize UI tests ([#9521](https://github.com/hcengineering/platform/issues/9521)) +* UBERF-12229: · Fix default gmail integration selection ([#9486](https://github.com/hcengineering/platform/issues/9486)) +* UBERF-12299: · Fix gmail integration selection ([#9505](https://github.com/hcengineering/platform/issues/9505)) +* UBERF-12313: · Pass editor-kit options in StyledTextBox ([#9512](https://github.com/hcengineering/platform/issues/9512)) +* UBERF-12323: · Include accounts info into backup ([#9659](https://github.com/hcengineering/platform/issues/9659)) +* UBERF-12325: · Add mongo warning for v7 ([#9543](https://github.com/hcengineering/platform/issues/9543)) +* UBERF-12445: · Fix adding second Github integration for same user ([#9531](https://github.com/hcengineering/platform/issues/9531)) +* UBERF-12509: · Trusted accounts migration tool ([#9652](https://github.com/hcengineering/platform/issues/9652)) +* UBERF-12633: · Fix GH local identities ([#9566](https://github.com/hcengineering/platform/issues/9566)) +* UBERF-12966: · Send messages from Gmail threads ([#9657](https://github.com/hcengineering/platform/issues/9657)) +* UBERF-12970: · Migrate integrations data ([#9640](https://github.com/hcengineering/platform/issues/9640)) +* UBERF-12988: · Add integration status and redesign integration state ([#9643](https://github.com/hcengineering/platform/issues/9643)) +* UBERF-13118: · Send emails from Huly mail threads ([#9669](https://github.com/hcengineering/platform/issues/9669)) +* UBERF-13120: · Support SMTP mail for different accounts ([#9674](https://github.com/hcengineering/platform/issues/9674)) +* UBERF-13123: · Fix mail message duplicates ([#9684](https://github.com/hcengineering/platform/issues/9684)) +* UBERF-13124: · Format hulygram phone number ([#9686](https://github.com/hcengineering/platform/issues/9686)) +* UBERF-13241: · Support gmail space config ([#9710](https://github.com/hcengineering/platform/issues/9710)) +* UBERF-13271: · Add Huly mail integration ([#9722](https://github.com/hcengineering/platform/issues/9722)) +* UBERF-13383: · Guest access token ([#9747](https://github.com/hcengineering/platform/issues/9747)) +* UBERF-13430: · Display date and space for cards in Home ([#9759](https://github.com/hcengineering/platform/issues/9759)) +* UBERF-13433: · Migrate channels to threads ([#9761](https://github.com/hcengineering/platform/issues/9761)) +* UBERF-13485: · Restore v6 from storage tool ([#9777](https://github.com/hcengineering/platform/issues/9777)) +* UBERF-8425: · Speed up accounts migration ([#8994](https://github.com/hcengineering/platform/issues/8994)) · More adjustments for migration scripts and tools ([#9099](https://github.com/hcengineering/platform/issues/9099)) · Improve parallel ws upgrade logging within one ws service ([#9118](https://github.com/hcengineering/platform/issues/9118)) · Improved pg/acc/ws error handling ([#9144](https://github.com/hcengineering/platform/issues/9144)) · Fix account upgrade deadlocks ([#9163](https://github.com/hcengineering/platform/issues/9163)) * UBERF-8522: · Allow to use any assignee for github projects ([#8179](https://github.com/hcengineering/platform/issues/8179)) -* UBERF-9568: · Fix person space filter ([#8183](https://github.com/hcengineering/platform/issues/8183)) - -## [s0.7.27] - 2025-03-08 - -* QFIX: · Fix url for recording videos ([#8174](https://github.com/hcengineering/platform/issues/8174)) - -## [s0.7.26] - 2025-03-07 - -* QFIX: · Add missed recorderId for desktop ([#8172](https://github.com/hcengineering/platform/issues/8172)) - -## [s0.7.24] - 2025-03-07 - -* UBERF-9560: · Filter query fixes -* UBERF-9575: · Fix filter disappear problem ([#8159](https://github.com/hcengineering/platform/issues/8159)) - -## [s0.7.23] - 2025-03-07 - * UBERF-9126: · Drive plugins + init version of screen recorder ([#8126](https://github.com/hcengineering/platform/issues/8126)) -* UBERF-9569: · Fix hanging transactor connections ([#8152](https://github.com/hcengineering/platform/issues/8152)) -* UBERF-9577: · Fix using default from address in emails ([#8163](https://github.com/hcengineering/platform/issues/8163)) -* UBERF-9571: · Fix empty threads ([#8153](https://github.com/hcengineering/platform/issues/8153)) - -## [s0.7.22] - 2025-03-06 - -* LOVE: · Scaling the floor ([#8145](https://github.com/hcengineering/platform/issues/8145)) -* QFIX: · Add mail domain ([#8147](https://github.com/hcengineering/platform/issues/8147)) -* UBERF-9543: · Restore-all tool ([#8132](https://github.com/hcengineering/platform/issues/8132)) -* UBERF-9550: · Add backup with verify ([#8137](https://github.com/hcengineering/platform/issues/8137)) · Fix backup verification memory usage ([#8138](https://github.com/hcengineering/platform/issues/8138)) - -## [s0.7.21] - 2025-03-05 - -* UBERF-9504: · Add role to employee mixin ([#8072](https://github.com/hcengineering/platform/issues/8072)) - -## [s0.7.19] - 2025-03-05 - -* 🚀 FEATURES: · Refactor markup to markdown utils ([#8134](https://github.com/hcengineering/platform/issues/8134)) -* QFIX: · Allow mail service to ensure person ([#8140](https://github.com/hcengineering/platform/issues/8140)) +* UBERF-9500: · Fix indexing on staging ([#8231](https://github.com/hcengineering/platform/issues/8231)) * UBERF-9502: · Account uuids in models ([#8125](https://github.com/hcengineering/platform/issues/8125)) -* UBERF-9557: · Support attachments in mail service ([#8139](https://github.com/hcengineering/platform/issues/8139)) - -## [s0.7.18] - 2025-03-05 - -* 🐛 BUG FIXES: · Missing screen share thumbnails in desktop ([#8135](https://github.com/hcengineering/platform/issues/8135)) -* UBERF-9542: · Add mail service with SMTP and SES support ([#8130](https://github.com/hcengineering/platform/issues/8130)) -* UBERF-9551: · Add web push URL ([#8133](https://github.com/hcengineering/platform/issues/8133)) - -## [s0.7.17] - 2025-03-03 - -* 🐛 BUG FIXES: · Check guest access with cookie token ([#8122](https://github.com/hcengineering/platform/issues/8122)) - -## [s0.7.16] - 2025-03-03 - -* EQMS-1443: · Fixed qms-comments position / decoration mapping (develop branch) ([#8119](https://github.com/hcengineering/platform/issues/8119)) · Fix qms-comments position / decoration mapping ([#8118](https://github.com/hcengineering/platform/issues/8118)) +* UBERF-9503: · Generated social ids ([#8208](https://github.com/hcengineering/platform/issues/8208)) +* UBERF-9504: · Add role to employee mixin ([#8072](https://github.com/hcengineering/platform/issues/8072)) * UBERF-9516: · Disable my space and use standard presenters ([#8114](https://github.com/hcengineering/platform/issues/8114)) +* UBERF-9521: · Refactor session manager ([#8560](https://github.com/hcengineering/platform/issues/8560)) * UBERF-9537: · Fix Invalid navigate to guest not authorised ([#8121](https://github.com/hcengineering/platform/issues/8121)) * UBERF-9540: · Fix invite message and add rate limit ([#8123](https://github.com/hcengineering/platform/issues/8123)) +* UBERF-9542: · Add mail service with SMTP and SES support ([#8130](https://github.com/hcengineering/platform/issues/8130)) +* UBERF-9543: · Restore-all tool ([#8132](https://github.com/hcengineering/platform/issues/8132)) +* UBERF-9550: · Add backup with verify ([#8137](https://github.com/hcengineering/platform/issues/8137)) · Fix backup verification memory usage ([#8138](https://github.com/hcengineering/platform/issues/8138)) +* UBERF-9551: · Add web push URL ([#8133](https://github.com/hcengineering/platform/issues/8133)) +* UBERF-9557: · Support attachments in mail service ([#8139](https://github.com/hcengineering/platform/issues/8139)) +* UBERF-9559: · Make CR accounts migrations concurrency safe ([#8821](https://github.com/hcengineering/platform/issues/8821)) +* UBERF-9560: · Filter query fixes +* UBERF-9568: · Fix person space filter ([#8183](https://github.com/hcengineering/platform/issues/8183)) +* UBERF-9569: · Fix hanging transactor connections ([#8152](https://github.com/hcengineering/platform/issues/8152)) +* UBERF-9575: · Fix filter disappear problem ([#8159](https://github.com/hcengineering/platform/issues/8159)) +* UBERF-9577: · Fix using default from address in emails ([#8163](https://github.com/hcengineering/platform/issues/8163)) +* UBERF-9578: · The correct display of the user's personal avatar in Direct messages. ([#8595](https://github.com/hcengineering/platform/issues/8595)) +* UBERF-9604: · Add edit permission check per row in Table ([#8528](https://github.com/hcengineering/platform/issues/8528)) +* UBERF-9605: · Test MTA hook integration ([#8189](https://github.com/hcengineering/platform/issues/8189)) +* UBERF-9606: · Limit a number of workspaces per user ([#8192](https://github.com/hcengineering/platform/issues/8192)) ([#8199](https://github.com/hcengineering/platform/issues/8199)) +* UBERF-9624: · Add card viewlet settings ([#8258](https://github.com/hcengineering/platform/issues/8258)) +* UBERF-9633: · Reduce migration calls during workspace creation ([#8242](https://github.com/hcengineering/platform/issues/8242)) ([#8244](https://github.com/hcengineering/platform/issues/8244)) · More proper fix ([#8249](https://github.com/hcengineering/platform/issues/8249)) +* UBERF-9636: · Meeting links ([#8334](https://github.com/hcengineering/platform/issues/8334)) · Meeting links - more cases ([#8369](https://github.com/hcengineering/platform/issues/8369)) +* UBERF-9639: · Master-detail view for cards ([#8413](https://github.com/hcengineering/platform/issues/8413)) +* UBERF-9661: · Use MAIL_URL env for mail integration ([#8272](https://github.com/hcengineering/platform/issues/8272)) +* UBERF-9693: · Allow to reindex from migration ([#8345](https://github.com/hcengineering/platform/issues/8345)) +* UBERF-9694: · Queue processing improvements ([#8418](https://github.com/hcengineering/platform/issues/8418)) +* UBERF-9698: · Fix identity swap issue ([#8360](https://github.com/hcengineering/platform/issues/8360)) +* UBERF-9703: · Profile cards for persons ([#8410](https://github.com/hcengineering/platform/issues/8410)) +* UBERF-9705: · Fix issues labels add remove ([#8373](https://github.com/hcengineering/platform/issues/8373)) +* UBERF-9710: · Fix always on status on front ([#8391](https://github.com/hcengineering/platform/issues/8391)) +* UBERF-9711: · Add by region groupping for admin console ([#8396](https://github.com/hcengineering/platform/issues/8396)) +* UBERF-9712: · Improve mail TLS settings and logs for self hosters ([#8399](https://github.com/hcengineering/platform/issues/8399)) ([#8400](https://github.com/hcengineering/platform/issues/8400)) +* UBERF-9714: · Support subsecutive meeting joins ([#8421](https://github.com/hcengineering/platform/issues/8421)) +* UBERF-9716: · New profile preview and initial achievements ([#8504](https://github.com/hcengineering/platform/issues/8504)) +* UBERF-9724: · Use updated accounts ([#8452](https://github.com/hcengineering/platform/issues/8452)) · Fix github functionality on 0.7 ([#8554](https://github.com/hcengineering/platform/issues/8554)) +* UBERF-9726: · Fix integrations in accounts for CR 24.1 ([#8490](https://github.com/hcengineering/platform/issues/8490)) +* UBERF-9732: · Use huly id as primary social id ([#8499](https://github.com/hcengineering/platform/issues/8499)) +* UBERF-9734: · Set default account timezone ([#8469](https://github.com/hcengineering/platform/issues/8469)) +* UBERF-9736: · Fix backup hang ([#8468](https://github.com/hcengineering/platform/issues/8468)) +* UBERF-9739: · Try to fix backup hang ([#8496](https://github.com/hcengineering/platform/issues/8496)) +* UBERF-9740: · Send mail errors to Sentry ([#8481](https://github.com/hcengineering/platform/issues/8481)) +* UBERF-9748: · Refactor server-ws ([#8495](https://github.com/hcengineering/platform/issues/8495)) +* UBERF-9752: · Properly handle streams to avoid datalake memory leak ([#8502](https://github.com/hcengineering/platform/issues/8502)) +* UBERF-9756: · Speed up CR account migrations ([#8573](https://github.com/hcengineering/platform/issues/8573)) +* UBERF-9764: · Adjust gmail for new accounts ([#8681](https://github.com/hcengineering/platform/issues/8681)) +* UBERF-13261: · Improve office UX ([#9733](https://github.com/hcengineering/platform/issues/9733)) +* UBERF-9663: · Improve mail logging ([#8275](https://github.com/hcengineering/platform/issues/8275)) +* ANALYTICS: · Add IP headers collection for geo debugging and session tracking ([#9648](https://github.com/hcengineering/platform/issues/9648)) +* EQMS-1533: · Fix template versions query ([#8753](https://github.com/hcengineering/platform/issues/8753)) ([#8766](https://github.com/hcengineering/platform/issues/8766)) +* EQMS-1548: · Fix linux dist for desktop ([#9670](https://github.com/hcengineering/platform/issues/9670)) +* EQMS-1576: · Hide region in ws selector for a regular user ([#9231](https://github.com/hcengineering/platform/issues/9231)) +* EQMS-1582: · Fix roles migration ([#9230](https://github.com/hcengineering/platform/issues/9230)) +* QFIX: · ListView, Table ([#9213](https://github.com/hcengineering/platform/issues/9213)) +* QFIX: · Add missed recorderId for desktop ([#8172](https://github.com/hcengineering/platform/issues/8172)) · Fix url for recording videos ([#8174](https://github.com/hcengineering/platform/issues/8174)) · Remove cf workers to fix ci/build on develop branch ([#8226](https://github.com/hcengineering/platform/issues/8226)) · Fix issues with hls player ([#8268](https://github.com/hcengineering/platform/issues/8268)) · Upload a folder produces duplicates ([#8269](https://github.com/hcengineering/platform/issues/8269)) · Add support for recording videos from desktop ([#8306](https://github.com/hcengineering/platform/issues/8306)) · Disable mixins migration · Ignore ancestor error for txremovedoc ([#9267](https://github.com/hcengineering/platform/issues/9267)) · Backup recheck ([#9288](https://github.com/hcengineering/platform/issues/9288)) · Backup info cors ([#9308](https://github.com/hcengineering/platform/issues/9308)) · Property pass context with OnThreadMessageCreated ([#9311](https://github.com/hcengineering/platform/issues/9311)) · Backup download skip support ([#9312](https://github.com/hcengineering/platform/issues/9312)) · Tune backup limits ([#9381](https://github.com/hcengineering/platform/issues/9381)) · Use a fulltext queue for blocked ops ([#9388](https://github.com/hcengineering/platform/issues/9388)) · Update last visit · Update last visit ([#9401](https://github.com/hcengineering/platform/issues/9401)) · Get pending data id ([#9402](https://github.com/hcengineering/platform/issues/9402)) · Increment attempts on restore retry ([#9403](https://github.com/hcengineering/platform/issues/9403)) · Account unit test ([#9409](https://github.com/hcengineering/platform/issues/9409)) · Allow tool to create workspace with dataid for testing ([#9404](https://github.com/hcengineering/platform/issues/9404)) · Backups using pipeline ([#9396](https://github.com/hcengineering/platform/issues/9396)) · Refactor love plugin to not import . ([#9413](https://github.com/hcengineering/platform/issues/9413)) · Backup blob info ([#9496](https://github.com/hcengineering/platform/issues/9496)) · Connection mgr close ([#9502](https://github.com/hcengineering/platform/issues/9502)) · Remap github installation to another workspace ([#9553](https://github.com/hcengineering/platform/issues/9553)) · Allow backup api to be used by admin ([#9560](https://github.com/hcengineering/platform/issues/9560)) · Fix export service ([#9558](https://github.com/hcengineering/platform/issues/9558)) · Add Logs/metrics to telemetry ([#9595](https://github.com/hcengineering/platform/issues/9595)) · Remove sentry on server ([#9597](https://github.com/hcengineering/platform/issues/9597)) · A better fulltext service logs ([#9606](https://github.com/hcengineering/platform/issues/9606)) · Rework span creation ([#9613](https://github.com/hcengineering/platform/issues/9613)) · Connect timeout + service ws info cache ([#9622](https://github.com/hcengineering/platform/issues/9622)) · Backward compatible multiple desktop updates channels ([#9687](https://github.com/hcengineering/platform/issues/9687)) · Show space for document's ([#9701](https://github.com/hcengineering/platform/issues/9701)) · Filter content for admin in case person is present in workspace ([#9720](https://github.com/hcengineering/platform/issues/9720)) · Update traces send to opentelemetry ([#9728](https://github.com/hcengineering/platform/issues/9728)) · Docker config mem usage ([#9745](https://github.com/hcengineering/platform/issues/9745)) +* SELFH-182: · Add v7 tools for migration from v6 ([#9678](https://github.com/hcengineering/platform/issues/9678)) +* SELFH-195: · Generate uuid for workspaces where missing ([#9751](https://github.com/hcengineering/platform/issues/9751)) +* TOOL: · Reindex all workspaces ([#9249](https://github.com/hcengineering/platform/issues/9249)) +* TXU-105: · Fix model lookups ([#8386](https://github.com/hcengineering/platform/issues/8386)) +* UBERF-10222: · Add logging ([#8709](https://github.com/hcengineering/platform/issues/8709)) +* UBERF-10227: · Fix createdOn type in getUserWorkspaces ([#8584](https://github.com/hcengineering/platform/issues/8584)) +* UBERF-10228: · Expose release social id to services ([#8562](https://github.com/hcengineering/platform/issues/8562)) +* UBERF-10252: · Fix collaborators activity presenter ([#8594](https://github.com/hcengineering/platform/issues/8594)) +* UBERF-10255: · Migrate accounts in saved filters ([#8846](https://github.com/hcengineering/platform/issues/8846)) +* UBERF-10308: · Adjust onboarding ([#8949](https://github.com/hcengineering/platform/issues/8949)) +* UBERF-10318: · Fix push subscriptions ([#8666](https://github.com/hcengineering/platform/issues/8666)) +* UBERF-10342: · Fix init script executor ([#8702](https://github.com/hcengineering/platform/issues/8702)) +* UBERF-10454: · Support ensure person by system user ([#8807](https://github.com/hcengineering/platform/issues/8807)) +* UBERF-10455: · Merge accounts for merged persons ([#8942](https://github.com/hcengineering/platform/issues/8942)) +* UBERF-10488: · Allow ws limit per account ([#8864](https://github.com/hcengineering/platform/issues/8864)) +* UBERF-10626: · Fix social identity duplicate key exception ([#8969](https://github.com/hcengineering/platform/issues/8969)) +* UBERF-10649: · Fix last visit for stale workspaces ([#8999](https://github.com/hcengineering/platform/issues/8999)) +* UBERF-11415: · Person cache unit tests ([#9202](https://github.com/hcengineering/platform/issues/9202)) +* UBERF-11651: · Fix huly id confirmation for dev setup ([#9296](https://github.com/hcengineering/platform/issues/9296)) +* UBERF-11798: · Win arm build ([#9366](https://github.com/hcengineering/platform/issues/9366)) +* UBERF-12170: · Support merging person in addSocialIdToPerson ([#9470](https://github.com/hcengineering/platform/issues/9470)) +* UBERF-12632: · Fix profile name change ([#9691](https://github.com/hcengineering/platform/issues/9691)) +* UBERF-8425: · Retry tx account ([#9133](https://github.com/hcengineering/platform/issues/9133)) · Improve account methods params checks ([#9278](https://github.com/hcengineering/platform/issues/9278)) · Fix created-modified owners tool ([#9283](https://github.com/hcengineering/platform/issues/9283)) +* UBERF-9485: · Fix state description save ([#9483](https://github.com/hcengineering/platform/issues/9483)) +* UBERF-9488: · Account operations unit tests ([#9503](https://github.com/hcengineering/platform/issues/9503)) +* UBERF-9530: · Support old guest link ([#8506](https://github.com/hcengineering/platform/issues/8506)) * UBERF-9534: · Ensure person ([#8117](https://github.com/hcengineering/platform/issues/8117)) - -## [s0.7.15] - 2025-02-27 - -* QFIX: · Build +* UBERF-9571: · Fix empty threads ([#8153](https://github.com/hcengineering/platform/issues/8153)) +* UBERF-9603: · Fix account rename ([#8371](https://github.com/hcengineering/platform/issues/8371)) +* UBERF-9670: · Fix reply avatars ([#8302](https://github.com/hcengineering/platform/issues/8302)) +* UBERF-9671: · Fix gh accounts migration ([#8308](https://github.com/hcengineering/platform/issues/8308)) +* UBERF-9691: · Expose full social ids in own account ([#8340](https://github.com/hcengineering/platform/issues/8340)) +* UBERF-9713: · Fix auto join condition ([#8404](https://github.com/hcengineering/platform/issues/8404)) +* UBERF-9725: · Fix accounts mismatch in plugins config ([#8430](https://github.com/hcengineering/platform/issues/8430)) +* UBERF-9726: · Manage integrations in accounts ([#8475](https://github.com/hcengineering/platform/issues/8475)) +* UBERF-9727: · Allow adding social id to existing person ([#8439](https://github.com/hcengineering/platform/issues/8439)) +* UBERF-9754: · Fix account timestamp ([#8520](https://github.com/hcengineering/platform/issues/8520)) +* UBERF-9797: · Idp auth state ([#9196](https://github.com/hcengineering/platform/issues/9196)) ## [0.6.458] - 2025-02-27 @@ -940,19 +394,6 @@ Changelog. * UBERF-9501: · Fix use of Date.now() ([#8069](https://github.com/hcengineering/platform/issues/8069)) * UBERF-9516: · Use cards for mail threads ([#8088](https://github.com/hcengineering/platform/issues/8088)) -## [s0.7.13] - 2025-02-25 - -* 🐛 BUG FIXES: · Extract video player to separate component ([#8086](https://github.com/hcengineering/platform/issues/8086)) - -## [s0.7.12] - 2025-02-24 - -* 🚀 FEATURES: · Cookie token ([#8057](https://github.com/hcengineering/platform/issues/8057)) -* 🐛 BUG FIXES: · Filter props on component update ([#8080](https://github.com/hcengineering/platform/issues/8080)) - -## [s0.7.11] - 2025-02-22 - -* UBERF-9501: · Fix use of Date.now() ([#8069](https://github.com/hcengineering/platform/issues/8069)) - ## [0.6.450] - 2025-02-20 * 🚀 FEATURES: · TRANSACTOR_OVERRIDE in front service @@ -960,7 +401,11 @@ Changelog. * EQMS-1437: · Use different label to display trainees' results "owner". ([#8046](https://github.com/hcengineering/platform/issues/8046)) * EQMS-1440: · Disable delayed qms doc effectiveness and review interval ([#8049](https://github.com/hcengineering/platform/issues/8049)) * UBERF-8545: · Fix links in readonly documents ([#8050](https://github.com/hcengineering/platform/issues/8050)) +* UBERF-9144: · Stay in same view after delete sub-issue ([#8051](https://github.com/hcengineering/platform/issues/8051)) * UBERF-9334: · Fixed ActionContext managment ([#8047](https://github.com/hcengineering/platform/issues/8047)) +* UBERF-9488: · More account unit tests ([#8058](https://github.com/hcengineering/platform/issues/8058)) +* UBERF-9488: · Part of account unit tests ([#8054](https://github.com/hcengineering/platform/issues/8054)) +* UBERF-9492: · Allow restricting hostnames for print service ([#8059](https://github.com/hcengineering/platform/issues/8059)) ## [0.6.449] - 2025-02-18 @@ -969,15 +414,10 @@ Changelog. * EQMS-1435: · Enable watermark in obsolete documents ([#8032](https://github.com/hcengineering/platform/issues/8032)) * EQMS-1445: · Fixed qms doc commments theme styling ([#8031](https://github.com/hcengineering/platform/issues/8031)) * UBERF-9297: · Fix space selection for emails ([#8035](https://github.com/hcengineering/platform/issues/8035)) · Store smtp mail id ([#8044](https://github.com/hcengineering/platform/issues/8044)) +* UBERF-9400: · Show name on login screen ([#8013](https://github.com/hcengineering/platform/issues/8013)) * UBERF-9458: · OTP sign up ([#8043](https://github.com/hcengineering/platform/issues/8043)) * UBERF-9489: · Fixes and cleanup ([#8048](https://github.com/hcengineering/platform/issues/8048)) * UBERF-9394: · Adjust readme to account changes ([#8030](https://github.com/hcengineering/platform/issues/8030)) - -## [s0.7.7] - 2025-02-17 - -* 🐛 BUG FIXES: · Use workspace uuid in front service ([#8024](https://github.com/hcengineering/platform/issues/8024)) -* QFIX: · Allow to copy workspace uuid from admin ([#8025](https://github.com/hcengineering/platform/issues/8025)) · Fix missing SES_AUTH_TOKEN ([#8026](https://github.com/hcengineering/platform/issues/8026)) -* UBERF-9400: · Show name on login screen ([#8013](https://github.com/hcengineering/platform/issues/8013)) * UBERF-9428: · Migrate accounts with multiple active services ([#8027](https://github.com/hcengineering/platform/issues/8027)) * UBERF-9451: · Drop old tokens from local storage ([#8028](https://github.com/hcengineering/platform/issues/8028)) @@ -988,13 +428,11 @@ Changelog. * UBERF-8425: · Fix get pending workspace on CR ([#8009](https://github.com/hcengineering/platform/issues/8009)) * UBERF-9429: · Provide workspace ids to storage adapters ([#7956](https://github.com/hcengineering/platform/issues/7956)) * UBERF-9479: · Fix adapter security selection ([#8007](https://github.com/hcengineering/platform/issues/8007)) +* UBERF-9484: · Fix findAll for PG driver ([#8022](https://github.com/hcengineering/platform/issues/8022)) +* UBERF-9383: · Fix ws init and import ([#8005](https://github.com/hcengineering/platform/issues/8005)) * UBERF-9430: · Fix provider auth case ([#8011](https://github.com/hcengineering/platform/issues/8011)) * UBERF-9476: · Optimize person store ([#8012](https://github.com/hcengineering/platform/issues/8012)) -## [s0.7.2] - 2025-02-14 - -* UBERF-9383: · Fix ws init and import ([#8005](https://github.com/hcengineering/platform/issues/8005)) - ## [0.6.447] - 2025-02-13 * 🐛 BUG FIXES: · 'Code' style ([#7999](https://github.com/hcengineering/platform/issues/7999)) · Minor changes to cloud transactor ([#7998](https://github.com/hcengineering/platform/issues/7998)) · Versions · Add promise catches ([#8002](https://github.com/hcengineering/platform/issues/8002)) @@ -1009,18 +447,10 @@ Changelog. * UBERF-9457: · Region move fixes + tests ([#7986](https://github.com/hcengineering/platform/issues/7986)) * UBERF-8425: · Fix getPendingWorkspace in CR ([#7996](https://github.com/hcengineering/platform/issues/7996)) -## [s0.6.444] - 2025-02-12 - -* QFIX: · Mongo status collection ([#7990](https://github.com/hcengineering/platform/issues/7990)) -* UBERF-8425: · Account DB unit tests ([#7994](https://github.com/hcengineering/platform/issues/7994)) - ## [0.6.441] - 2025-02-11 -* UBERF-9447: · Move accounts to pg tool ([#7976](https://github.com/hcengineering/platform/issues/7976)) - -## [s0.6.440] - 2025-02-11 - * UBERF-9465: · Fix ping/pong in Blob format ([#7981](https://github.com/hcengineering/platform/issues/7981)) +* UBERF-9447: · Move accounts to pg tool ([#7976](https://github.com/hcengineering/platform/issues/7976)) ## [0.6.438] - 2025-02-11 @@ -1045,10 +475,6 @@ Changelog. * QFIX: · Remove types cmd ([#7962](https://github.com/hcengineering/platform/issues/7962)) * QFIX: · Correctly display long strings ([#7957](https://github.com/hcengineering/platform/issues/7957)) -## [s0.6.434] - 2025-02-07 - -* QFIX: · Admin panel ([#7953](https://github.com/hcengineering/platform/issues/7953)) - ## [0.6.432] - 2025-02-06 * 🐛 BUG FIXES: · Wrong workspace id passed to collaborator client ([#7950](https://github.com/hcengineering/platform/issues/7950)) @@ -1541,15 +967,11 @@ Changelog. ## [0.6.339] - 2024-10-25 -* 🐛 BUG FIXES: · Use proper blob size in backup ([#7032](https://github.com/hcengineering/platform/issues/7032)) +* 🐛 BUG FIXES: · More datalake logs ([#7019](https://github.com/hcengineering/platform/issues/7019)) · Use proper blob size in backup ([#7032](https://github.com/hcengineering/platform/issues/7032)) * ⚙️ MISCELLANEOUS TASKS: · Update datalake configs ([#7033](https://github.com/hcengineering/platform/issues/7033)) * QFIX: · Fix setting ui metadata from config ([#7025](https://github.com/hcengineering/platform/issues/7025)) * UBERF-8528: · Share VS code formatter and plugin settings ([#7024](https://github.com/hcengineering/platform/issues/7024)) -## [s0.6.338] - 2024-10-23 - -* 🐛 BUG FIXES: · More datalake logs ([#7019](https://github.com/hcengineering/platform/issues/7019)) - ## [0.6.336] - 2024-10-23 * 🚀 FEATURES: · Convert guest to user via invite link ([#7014](https://github.com/hcengineering/platform/issues/7014)) @@ -1573,15 +995,11 @@ Changelog. * 🐛 BUG FIXES: · Reverse scroll bar scrolling with bar pull ([#6968](https://github.com/hcengineering/platform/issues/6968)) · Use ordered list start attribute ([#6974](https://github.com/hcengineering/platform/issues/6974)) * UBERF-8427: · Fix desktop oauth flow ([#6975](https://github.com/hcengineering/platform/issues/6975)) * UBERF-8500: · Improve OIDC init logging ([#6981](https://github.com/hcengineering/platform/issues/6981)) -* QFIX: · Pass file uuid to uppy metadata ([#6985](https://github.com/hcengineering/platform/issues/6985)) -* UBERF-8511: · Configurable account db ns ([#6978](https://github.com/hcengineering/platform/issues/6978)) - -## [s0.6.331] - 2024-10-18 - -* 🚀 FEATURES: · Datalake worker initial version ([#6952](https://github.com/hcengineering/platform/issues/6952)) -* 🐛 BUG FIXES: · Use ordered list start attribute ([#6974](https://github.com/hcengineering/platform/issues/6974)) * UBERF-8510: · Fix OOM in backup service ([#6973](https://github.com/hcengineering/platform/issues/6973)) +* UBERF-8516: · Stable mentions popup ([#6993](https://github.com/hcengineering/platform/issues/6993)) +* QFIX: · Pass file uuid to uppy metadata ([#6985](https://github.com/hcengineering/platform/issues/6985)) * UBERF-8509: · Fix docx to html conversion ([#6970](https://github.com/hcengineering/platform/issues/6970)) +* UBERF-8511: · Configurable account db ns ([#6978](https://github.com/hcengineering/platform/issues/6978)) ## [0.6.331] - 2024-10-16 @@ -1854,31 +1272,18 @@ Changelog. * UBERF-7684: · Workspace service ([#6460](https://github.com/hcengineering/platform/issues/6460)) * UBERF-7915: · Support tg bot attachments ([#6471](https://github.com/hcengineering/platform/issues/6471)) * UBERF-7922: · Split Server Storage to middlewares ([#6464](https://github.com/hcengineering/platform/issues/6464)) +* UBERF-7985: · Fix private targets ([#6439](https://github.com/hcengineering/platform/issues/6439)) * UBERF-8005: · Add tests to tracker projects, fix failed tests ([#6454](https://github.com/hcengineering/platform/issues/6454)) * UBERF-8017: · Support updating workspace name and deleting workspace ([#6476](https://github.com/hcengineering/platform/issues/6476)) * UBERF-8044: · Staging model version ([#6492](https://github.com/hcengineering/platform/issues/6492)) * UBERF-8047: · Add tests to channels and chats ([#6496](https://github.com/hcengineering/platform/issues/6496)) * UBERF-8052: · Allow easy profiling of transactor ([#6502](https://github.com/hcengineering/platform/issues/6502)) -* UBERF-8053: · Disable re-check for milestones +* UBERF-8053: · Disable re-check for milestones · Disable re-check for milestones * EZQMS-1149: · Allow archiving effective doc ([#6489](https://github.com/hcengineering/platform/issues/6489)) * EZQMS-1171: · Drop h4-h6 during import of controlled doc ([#6487](https://github.com/hcengineering/platform/issues/6487)) * EZQMS-1185: · Fix delete document availability ([#6485](https://github.com/hcengineering/platform/issues/6485)) -* UBERF-7684: · Add workspace pod to docker build ([#6465](https://github.com/hcengineering/platform/issues/6465)) - -## [s0.6.288b] - 2024-08-30 - -* 🐛 BUG FIXES: · Copy template content when creating controlled document ([#6441](https://github.com/hcengineering/platform/issues/6441)) · Use workspace id in collaborator ([#6447](https://github.com/hcengineering/platform/issues/6447)) · URI encode datalake blob id and more workspace fixes ([#6449](https://github.com/hcengineering/platform/issues/6449)) - -## [s0.6.288a] - 2024-08-29 - -* 🐛 BUG FIXES: · Build and push docker containers for s-prefixed tags ([#6442](https://github.com/hcengineering/platform/issues/6442)) - -## [s0.6.278] - 2024-08-29 - -* 🐛 BUG FIXES: · Improve codeblock wrapping ([#6440](https://github.com/hcengineering/platform/issues/6440)) -* UBERF-7985: · Fix private targets ([#6439](https://github.com/hcengineering/platform/issues/6439)) -* UBERF-8053: · Disable re-check for milestones * QFIX: · Add fire and rocket smiley ([#6438](https://github.com/hcengineering/platform/issues/6438)) +* UBERF-7684: · Add workspace pod to docker build ([#6465](https://github.com/hcengineering/platform/issues/6465)) ## [0.6.288] - 2024-09-03 From 34128dfc604e4341499ca6ac7f959fb24264334e Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Tue, 27 Jan 2026 13:10:12 +0700 Subject: [PATCH 02/67] Fix okit.graphql is not a function (#10442) Signed-off-by: Artem Savchenko --- .../github/pod-github/src/sync/comments.ts | 7 ++++-- .../github/pod-github/src/sync/issueBase.ts | 7 ++++-- services/github/pod-github/src/sync/issues.ts | 22 +++++++++++++------ .../pod-github/src/sync/pullrequests.ts | 7 ++++-- .../pod-github/src/sync/reviewComments.ts | 17 ++++++++++---- .../pod-github/src/sync/reviewThreads.ts | 20 ++++++++++++++--- .../github/pod-github/src/sync/reviews.ts | 12 +++++++--- services/github/pod-github/src/sync/utils.ts | 17 +++++++++++++- 8 files changed, 85 insertions(+), 24 deletions(-) diff --git a/services/github/pod-github/src/sync/comments.ts b/services/github/pod-github/src/sync/comments.ts index c435fa41f2..b64b290a75 100644 --- a/services/github/pod-github/src/sync/comments.ts +++ b/services/github/pod-github/src/sync/comments.ts @@ -24,7 +24,7 @@ import { githubExternalSyncVersion, githubSyncVersion } from '../types' -import { collectUpdate, deleteObjects, errorToObj, getSince, isGHWriteAllowed } from './utils' +import { collectUpdate, deleteObjects, ensureGraphQLOctokit, errorToObj, getSince, isGHWriteAllowed } from './utils' import { Analytics } from '@hcengineering/analytics' import { IssueComment, IssueCommentCreatedEvent, IssueCommentEvent } from '@octokit/webhooks-types' @@ -144,7 +144,10 @@ export class CommentSyncManager implements DocSyncManager { account: PersonId, id: string ): Promise { - const okit = (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit, + container + ) const q = `mutation deleteComment($commentID: ID!) { deleteIssueComment( diff --git a/services/github/pod-github/src/sync/issueBase.ts b/services/github/pod-github/src/sync/issueBase.ts index 1cd2872dd0..bf4ce7535f 100644 --- a/services/github/pod-github/src/sync/issueBase.ts +++ b/services/github/pod-github/src/sync/issueBase.ts @@ -38,7 +38,7 @@ import { Octokit } from 'octokit' import { ContainerFocus, IntegrationManager, githubExternalSyncVersion, githubSyncVersion } from '../types' import { IssueExternalData } from './githubTypes' import { stripGuestLink } from './guest' -import { collectUpdate, compareMarkdown, deleteObjects, errorToObj, guessStatus } from './utils' +import { collectUpdate, compareMarkdown, deleteObjects, ensureGraphQLOctokit, errorToObj, guessStatus } from './utils' /** * @public @@ -338,7 +338,10 @@ export abstract class IssueSyncManagerBase { const allAttributes = this.client.getHierarchy().getAllAttributes(existingIssue._class) const platformUpdate = collectUpdate(previousData, existingIssue, Array.from(allAttributes.keys())) - const okit = (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit, + container + ) // Remove current same values from update for (const [k, v] of Object.entries(update)) { diff --git a/services/github/pod-github/src/sync/issues.ts b/services/github/pod-github/src/sync/issues.ts index d5faa06234..200b74c445 100644 --- a/services/github/pod-github/src/sync/issues.ts +++ b/services/github/pod-github/src/sync/issues.ts @@ -48,7 +48,7 @@ import { } from '../types' import { IssueExternalData, issueDetails } from './githubTypes' import { GithubIssueData, IssueSyncManagerBase, IssueUpdate, WithMarkup } from './issueBase' -import { getSince, gqlp, guessStatus, isGHWriteAllowed, syncRunner } from './utils' +import { ensureGraphQLOctokit, getSince, gqlp, guessStatus, isGHWriteAllowed, syncRunner } from './utils' export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncManager { createPromise: Promise | undefined @@ -608,6 +608,8 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan okit: Octokit, account: PersonId ): Promise { + const graphqlOkit = ensureGraphQLOctokit(okit, container) + const { state, stateReason, body, ...issueUpdate } = await this.collectIssueUpdate( info, existing, @@ -624,7 +626,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan // We should allow modification from user. const closeIssue = async (): Promise => { - await okit.graphql( + await graphqlOkit.graphql( ` mutation closeIssue($issue: ID!) { closeIssue(input: { @@ -642,7 +644,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan } const reopenIssue = async (): Promise => { - await okit.graphql( + await graphqlOkit.graphql( ` mutation reopenIssue($issue: ID!) { reopenIssue(input: { @@ -675,7 +677,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan // We need to call re-open issue await reopenIssue() } - await okit.graphql( + await graphqlOkit.graphql( ` mutation updateIssue($issue: ID!, $body: String! ) { updateIssue(input: { @@ -713,7 +715,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan await reopenIssue() } if (hasOtherChanges) { - await okit.graphql( + await graphqlOkit.graphql( ` mutation updateIssue($issue: ID!) { updateIssue(input: { @@ -751,7 +753,10 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan ): Promise { const existingIssue = existing - const okit = (await this.provider.getOctokit(ctx, existingIssue.modifiedBy)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, existingIssue.modifiedBy)) ?? container.container.octokit, + container + ) const repoId = repository.nodeId @@ -797,7 +802,10 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan account: PersonId, id: string ): Promise { - const okit = (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit, + container + ) const q = `mutation deleteIssue($issueID: ID!) { deleteIssue( diff --git a/services/github/pod-github/src/sync/pullrequests.ts b/services/github/pod-github/src/sync/pullrequests.ts index 11bff84de4..97df74b3f1 100644 --- a/services/github/pod-github/src/sync/pullrequests.ts +++ b/services/github/pod-github/src/sync/pullrequests.ts @@ -61,6 +61,7 @@ import { } from './githubTypes' import { GithubIssueData, IssueSyncManagerBase, WithMarkup } from './issueBase' import { + ensureGraphQLOctokit, errorToObj, getSinceRaw, gqlp, @@ -975,6 +976,8 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS okit: Octokit, account: PersonId ): Promise { + const graphqlOkit = ensureGraphQLOctokit(okit, container) + let { state, stateReason, body, ...issueUpdate } = await this.collectIssueUpdate( info, existing, @@ -1006,7 +1009,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS workspace: this.provider.getWorkspaceId() }) if (isGHWriteAllowed()) { - await okit.graphql( + await graphqlOkit.graphql( ` mutation updatePullRequest($issue: ID!, $body: String!) { updatePullRequest(input: { @@ -1040,7 +1043,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS workspace: this.provider.getWorkspaceId() }) if (isGHWriteAllowed()) { - await okit.graphql( + await graphqlOkit.graphql( ` mutation updatePullRequest($issue: ID!) { updatePullRequest(input: { diff --git a/services/github/pod-github/src/sync/reviewComments.ts b/services/github/pod-github/src/sync/reviewComments.ts index c1c4a378d4..d4214d6cf6 100644 --- a/services/github/pod-github/src/sync/reviewComments.ts +++ b/services/github/pod-github/src/sync/reviewComments.ts @@ -29,7 +29,7 @@ import { githubSyncVersion } from '../types' import { ReviewComment as ReviewCommentExternalData, reviewCommentDetails } from './githubTypes' -import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed } from './utils' +import { collectUpdate, deleteObjects, ensureGraphQLOctokit, errorToObj, isGHWriteAllowed } from './utils' import { Analytics } from '@hcengineering/analytics' import { PullRequestReviewCommentCreatedEvent, PullRequestReviewCommentEvent } from '@octokit/webhooks-types' @@ -155,7 +155,10 @@ export class ReviewCommentSyncManager implements DocSyncManager { derivedClient: TxOperations, parent?: DocSyncInfo ): Promise { - const okit = (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit, + container + ) const q = `mutation deleteReviewComment($reviewID: ID!) { deletePullRequestReviewComment(input: { id: $reviewID @@ -435,7 +438,10 @@ export class ReviewCommentSyncManager implements DocSyncManager { if (Object.keys(platformUpdate).length > 0) { if (platformUpdate.body !== undefined) { const body = await this.provider.getMarkupSafe(container.container, platformUpdate.body) - const okit = (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit, + container + ) const q = `mutation updateReviewComment($commentID: ID!, $body: String!) { updatePullRequestReviewComment(input: { threadId: $threadID @@ -510,7 +516,10 @@ export class ReviewCommentSyncManager implements DocSyncManager { return {} } const existingReview = existing as GithubReviewComment - const okit = (await this.provider.getOctokit(ctx, existingReview.modifiedBy)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, existingReview.modifiedBy)) ?? container.container.octokit, + container + ) // No external version yet, create it. try { diff --git a/services/github/pod-github/src/sync/reviewThreads.ts b/services/github/pod-github/src/sync/reviewThreads.ts index fbc48a80e1..bd65b0ec32 100644 --- a/services/github/pod-github/src/sync/reviewThreads.ts +++ b/services/github/pod-github/src/sync/reviewThreads.ts @@ -35,7 +35,15 @@ import { getUpdatedAtReviewThread, reviewThreadDetails } from './githubTypes' -import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed, syncChilds, syncDerivedDocuments } from './utils' +import { + collectUpdate, + deleteObjects, + ensureGraphQLOctokit, + errorToObj, + isGHWriteAllowed, + syncChilds, + syncDerivedDocuments +} from './utils' import { Analytics } from '@hcengineering/analytics' import { PullRequestReviewThreadEvent } from '@octokit/webhooks-types' @@ -371,7 +379,10 @@ export class ReviewThreadSyncManager implements DocSyncManager { if (Object.keys(platformUpdate).length > 0) { // Check and update external if (platformUpdate.isResolved !== undefined && githubConfiguration.ResolveThreadSupported) { - const okit = (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit, + container + ) const q = `mutation updateReviewThread($threadID: ID!) { ${platformUpdate.isResolved ? 'resolveReviewThread' : 'unresolveReviewThread'} ( input: { @@ -446,7 +457,10 @@ export class ReviewThreadSyncManager implements DocSyncManager { return {} } const existingReview = existing as GithubReviewThread - const okit = (await this.provider.getOctokit(ctx, existingReview.modifiedBy)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, existingReview.modifiedBy)) ?? container.container.octokit, + container + ) // No external version yet, create it. // Will be added into pending state. diff --git a/services/github/pod-github/src/sync/reviews.ts b/services/github/pod-github/src/sync/reviews.ts index 1c5496b51d..0519d46145 100644 --- a/services/github/pod-github/src/sync/reviews.ts +++ b/services/github/pod-github/src/sync/reviews.ts @@ -29,7 +29,7 @@ import { githubSyncVersion } from '../types' import { PullRequestExternalData, Review as ReviewExternalData, reviewDetails, toReviewState } from './githubTypes' -import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed, syncChilds } from './utils' +import { collectUpdate, deleteObjects, ensureGraphQLOctokit, errorToObj, isGHWriteAllowed, syncChilds } from './utils' import { Analytics } from '@hcengineering/analytics' import { PullRequestReviewEvent, PullRequestReviewSubmittedEvent } from '@octokit/webhooks-types' @@ -150,7 +150,10 @@ export class ReviewSyncManager implements DocSyncManager { account: PersonId, id: string ): Promise { - const okit = (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, account)) ?? container.container.octokit, + container + ) const q = `mutation deleteReview($reviewID: ID!) { deletePullRequestReview(input: { pullRequestReviewId: $reviewID @@ -432,7 +435,10 @@ export class ReviewSyncManager implements DocSyncManager { return {} } const existingReview = existing as GithubReview - const okit = (await this.provider.getOctokit(ctx, existingReview.modifiedBy)) ?? container.container.octokit + const okit = ensureGraphQLOctokit( + (await this.provider.getOctokit(ctx, existingReview.modifiedBy)) ?? container.container.octokit, + container + ) // No external version yet, create it. try { diff --git a/services/github/pod-github/src/sync/utils.ts b/services/github/pod-github/src/sync/utils.ts index 342224b53f..5feafce402 100644 --- a/services/github/pod-github/src/sync/utils.ts +++ b/services/github/pod-github/src/sync/utils.ts @@ -23,7 +23,8 @@ import { PlatformError, unknownStatus } from '@hcengineering/platform' import task from '@hcengineering/task' import { IssueStatus } from '@hcengineering/tracker' import { deepEqual } from 'fast-equals' -import { githubExternalSyncVersion } from '../types' +import { Octokit } from 'octokit' +import { ContainerFocus, githubExternalSyncVersion } from '../types' /** * Return if github write operations are allowed. @@ -35,6 +36,20 @@ export function isGHWriteAllowed (): boolean { return true } +/** + * Ensures an Octokit instance has the graphql method available. + * If the provided okit doesn't have graphql, falls back to container.octokit which is guaranteed to have it. + * @param okit - The Octokit instance to check (may be undefined) + * @param container - The ContainerFocus containing the fallback octokit instance + * @returns An Octokit instance with graphql method available + */ +export function ensureGraphQLOctokit (okit: Octokit | undefined, container: ContainerFocus): Octokit { + if (okit !== undefined && typeof (okit as any).graphql === 'function') { + return okit + } + return container.container.octokit +} + /** * @public */ From 568749a13ef872a6c9627b1a926e6b73e8c21ccc Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Tue, 27 Jan 2026 13:10:24 +0700 Subject: [PATCH 03/67] Skip archived and outdated documents in export by default (#10443) Signed-off-by: Artem Savchenko --- plugins/export-assets/lang/cs.json | 3 +- plugins/export-assets/lang/de.json | 3 +- plugins/export-assets/lang/en.json | 3 +- plugins/export-assets/lang/es.json | 3 +- plugins/export-assets/lang/fr.json | 3 +- plugins/export-assets/lang/it.json | 3 +- plugins/export-assets/lang/ja.json | 3 +- plugins/export-assets/lang/pt.json | 3 +- plugins/export-assets/lang/ru.json | 3 +- plugins/export-assets/lang/tr.json | 3 +- plugins/export-assets/lang/zh.json | 3 +- .../components/ExportToWorkspaceModal.svelte | 38 ++++++++++++------- plugins/export-resources/src/export.ts | 6 ++- plugins/export-resources/src/plugin.ts | 3 +- plugins/export/src/index.ts | 1 + plugins/export/src/utils.ts | 36 ++++++++++++++++++ services/export/pod-export/src/server.ts | 7 +++- .../export/pod-export/src/workspace/types.ts | 2 + .../src/workspace/workspace-exporter.ts | 11 ++++-- 19 files changed, 105 insertions(+), 32 deletions(-) create mode 100644 plugins/export/src/utils.ts diff --git a/plugins/export-assets/lang/cs.json b/plugins/export-assets/lang/cs.json index 338a65c0ca..faddf6fd9f 100644 --- a/plugins/export-assets/lang/cs.json +++ b/plugins/export-assets/lang/cs.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Vybrat pracovní prostor pro export dokumentů z prostoru", "SelectSpace": "Vybrat prostor", "NoSelectedDocuments": "Žádné dokumenty nevybrány", - "RequestPermissionToImport": "Prosím, požádejte vlastníka cílového pracovního prostoru o povolení k importu dokumentů." + "RequestPermissionToImport": "Prosím, požádejte vlastníka cílového pracovního prostoru o povolení k importu dokumentů.", + "SkipDeletedObsolete": "Přeskočit archivované nebo zastaralé dokumenty" } } diff --git a/plugins/export-assets/lang/de.json b/plugins/export-assets/lang/de.json index 670c65d2d5..b9fcd671dd 100644 --- a/plugins/export-assets/lang/de.json +++ b/plugins/export-assets/lang/de.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Arbeitsbereich zum Exportieren von Dokumenten aus dem Bereich auswählen", "SelectSpace": "Bereich auswählen", "NoSelectedDocuments": "Keine Dokumente ausgewählt", - "RequestPermissionToImport": "Bitte fordern Sie die Berechtigung vom Eigentümer des Ziel-Arbeitsbereichs an, um Dokumente zu importieren." + "RequestPermissionToImport": "Bitte fordern Sie die Berechtigung vom Eigentümer des Ziel-Arbeitsbereichs an, um Dokumente zu importieren.", + "SkipDeletedObsolete": "Archivierte oder veraltete Dokumente überspringen" } } diff --git a/plugins/export-assets/lang/en.json b/plugins/export-assets/lang/en.json index 112ea10bab..af3fbe65fb 100644 --- a/plugins/export-assets/lang/en.json +++ b/plugins/export-assets/lang/en.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Select workspace to export documents from space", "SelectSpace": "Select space", "NoSelectedDocuments": "No documents selected", - "RequestPermissionToImport": "Please request permission from the target workspace owner to import documents." + "RequestPermissionToImport": "Please request permission from the target workspace owner to import documents.", + "SkipDeletedObsolete": "Skip archived or obsolete documents" } } diff --git a/plugins/export-assets/lang/es.json b/plugins/export-assets/lang/es.json index f891905558..f6ac4c270b 100644 --- a/plugins/export-assets/lang/es.json +++ b/plugins/export-assets/lang/es.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Seleccionar espacio de trabajo para exportar documentos del espacio", "SelectSpace": "Seleccionar espacio", "NoSelectedDocuments": "No hay documentos seleccionados", - "RequestPermissionToImport": "Por favor, solicite permiso al propietario del espacio de trabajo de destino para importar documentos." + "RequestPermissionToImport": "Por favor, solicite permiso al propietario del espacio de trabajo de destino para importar documentos.", + "SkipDeletedObsolete": "Omitir documentos archivados u obsoletos" } } diff --git a/plugins/export-assets/lang/fr.json b/plugins/export-assets/lang/fr.json index eaaa78a9dc..b451b4fe4c 100644 --- a/plugins/export-assets/lang/fr.json +++ b/plugins/export-assets/lang/fr.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Sélectionner l'espace de travail pour exporter les documents de l'espace", "SelectSpace": "Sélectionner l'espace", "NoSelectedDocuments": "Aucun document sélectionné", - "RequestPermissionToImport": "Veuillez demander l'autorisation au propriétaire de l'espace de travail cible pour importer des documents." + "RequestPermissionToImport": "Veuillez demander l'autorisation au propriétaire de l'espace de travail cible pour importer des documents.", + "SkipDeletedObsolete": "Ignorer les documents archivés ou obsolètes" } } diff --git a/plugins/export-assets/lang/it.json b/plugins/export-assets/lang/it.json index 4690c0a36e..3db1e4eaf3 100644 --- a/plugins/export-assets/lang/it.json +++ b/plugins/export-assets/lang/it.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Seleziona spazio di lavoro per esportare documenti dallo spazio", "SelectSpace": "Seleziona spazio", "NoSelectedDocuments": "Nessun documento selezionato", - "RequestPermissionToImport": "Si prega di richiedere l'autorizzazione al proprietario dello spazio di lavoro di destinazione per importare documenti." + "RequestPermissionToImport": "Si prega di richiedere l'autorizzazione al proprietario dello spazio di lavoro di destinazione per importare documenti.", + "SkipDeletedObsolete": "Salta documenti archivati o obsoleti" } } diff --git a/plugins/export-assets/lang/ja.json b/plugins/export-assets/lang/ja.json index feaf09c4f2..39a9f11645 100644 --- a/plugins/export-assets/lang/ja.json +++ b/plugins/export-assets/lang/ja.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "スペースからドキュメントをエクスポートするワークスペースを選択", "SelectSpace": "スペースを選択", "NoSelectedDocuments": "ドキュメントが選択されていません", - "RequestPermissionToImport": "ドキュメントをインポートするには、対象ワークスペースの所有者に許可をリクエストしてください。" + "RequestPermissionToImport": "ドキュメントをインポートするには、対象ワークスペースの所有者に許可をリクエストしてください。", + "SkipDeletedObsolete": "アーカイブ済みまたは廃止されたドキュメントをスキップ" } } diff --git a/plugins/export-assets/lang/pt.json b/plugins/export-assets/lang/pt.json index 14343f2077..c863d578b8 100644 --- a/plugins/export-assets/lang/pt.json +++ b/plugins/export-assets/lang/pt.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Selecionar espaço de trabalho para exportar documentos do espaço", "SelectSpace": "Selecionar espaço", "NoSelectedDocuments": "Nenhum documento selecionado", - "RequestPermissionToImport": "Por favor, solicite permissão ao proprietário do espaço de trabalho de destino para importar documentos." + "RequestPermissionToImport": "Por favor, solicite permissão ao proprietário do espaço de trabalho de destino para importar documentos.", + "SkipDeletedObsolete": "Pular documentos arquivados ou obsoletos" } } diff --git a/plugins/export-assets/lang/ru.json b/plugins/export-assets/lang/ru.json index 20634e04be..d24a9932e5 100644 --- a/plugins/export-assets/lang/ru.json +++ b/plugins/export-assets/lang/ru.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Выбрать рабочее пространство для экспорта документов из пространства", "SelectSpace": "Выбрать пространство", "NoSelectedDocuments": "Документы не выбраны", - "RequestPermissionToImport": "Пожалуйста, запросите разрешение у владельца рабочего пространства, куда вы хотите экспортировать документы." + "RequestPermissionToImport": "Пожалуйста, запросите разрешение у владельца рабочего пространства, куда вы хотите экспортировать документы.", + "SkipDeletedObsolete": "Пропускать архивированные или устаревшие документы" } } diff --git a/plugins/export-assets/lang/tr.json b/plugins/export-assets/lang/tr.json index 32c2f7e7f8..0482f4c219 100644 --- a/plugins/export-assets/lang/tr.json +++ b/plugins/export-assets/lang/tr.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "Alanın belgelerini dışa aktarmak için çalışma alanı seç", "SelectSpace": "Alan seç", "NoSelectedDocuments": "Seçili belge yok", - "RequestPermissionToImport": "Belgeleri içe aktarmak için lütfen hedef çalışma alanı sahibinden izin isteyin." + "RequestPermissionToImport": "Belgeleri içe aktarmak için lütfen hedef çalışma alanı sahibinden izin isteyin.", + "SkipDeletedObsolete": "Arşivlenmiş veya kullanımdan kaldırılmış belgeleri atla" } } diff --git a/plugins/export-assets/lang/zh.json b/plugins/export-assets/lang/zh.json index 5c09719486..b332bdda97 100644 --- a/plugins/export-assets/lang/zh.json +++ b/plugins/export-assets/lang/zh.json @@ -37,6 +37,7 @@ "SelectWorkspaceToExportSpace": "选择工作区以从空间导出文档", "SelectSpace": "选择空间", "NoSelectedDocuments": "未选择文档", - "RequestPermissionToImport": "请向目标工作区所有者请求导入文档的权限。" + "RequestPermissionToImport": "请向目标工作区所有者请求导入文档的权限。", + "SkipDeletedObsolete": "跳过已归档或已过时的文档" } } diff --git a/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte b/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte index 8f60045131..8cba809214 100644 --- a/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte +++ b/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte @@ -25,10 +25,10 @@ type Class } from '@hcengineering/core' import { Card, getCurrentWorkspaceUuid } from '@hcengineering/presentation' - import { DropdownLabels, Label } from '@hcengineering/ui' + import { CheckBox, DropdownLabels, Label } from '@hcengineering/ui' import { getResource } from '@hcengineering/platform' import login from '@hcengineering/login' - import { type RelationDefinition } from '@hcengineering/export' + import { type RelationDefinition, shouldSkipDocument } from '@hcengineering/export' import { createEventDispatcher } from 'svelte' @@ -41,12 +41,6 @@ export let docClass: Ref> | undefined = undefined export let spaceExport: boolean | undefined = false - $: selectedDocs = spaceExport !== true ? (Array.isArray(value) ? value : value != null ? [value] : []) : [] - $: _class = docClass ?? (selectedDocs.length > 0 ? selectedDocs[0]._class : undefined) - - // Build query with space filter when exporting from space - $: exportQuery = spaceExport === true ? { ...(query ?? {}), space: (value as Space)._id } : query - const dispatch = createEventDispatcher() let targetWorkspace: string | undefined = undefined @@ -54,6 +48,18 @@ let workspacesWithPermission = new Set() let loading = false let workspaceLoading = false + let skipDeletedObsolete = true + + $: selectedDocs = spaceExport !== true ? (Array.isArray(value) ? value : value != null ? [value] : []) : [] + $: _class = docClass ?? (selectedDocs.length > 0 ? selectedDocs[0]._class : undefined) + + $: filteredSelectedDocs = + skipDeletedObsolete && selectedDocs.length > 0 + ? selectedDocs.filter((doc) => !shouldSkipDocument(doc)) + : selectedDocs + + // Build query with space filter when exporting from space + $: exportQuery = spaceExport === true ? { ...(query ?? {}), space: (value as Space)._id } : query async function loadWorkspaces (): Promise { try { @@ -97,13 +103,13 @@ }) ) - $: canSave = targetWorkspace !== undefined && _class != null + $: canSave = targetWorkspace !== undefined && _class != null && filteredSelectedDocs.length > 0 async function handleExport (): Promise { if (!canSave || _class == null) return loading = true - void exportToWorkspace(_class, exportQuery, selectedDocs, targetWorkspace, relations) + void exportToWorkspace(_class, exportQuery, filteredSelectedDocs, targetWorkspace, relations, skipDeletedObsolete) loading = false dispatch('close', true) } @@ -116,18 +122,18 @@ okAction={handleExport} okLabel={plugin.string.Export} canSave={canSave && !loading && !workspaceLoading} - width="x-small" + width="small" on:close={() => dispatch('close')} on:changeContent >
- + {#if !workspaceLoading && workspaces.length === 0} +
+ +
+
+
diff --git a/plugins/export-resources/src/export.ts b/plugins/export-resources/src/export.ts index b47473c25e..a60a98c37e 100644 --- a/plugins/export-resources/src/export.ts +++ b/plugins/export-resources/src/export.ts @@ -24,7 +24,8 @@ export async function exportToWorkspace ( query: DocumentQuery | undefined, selectedDocs: Doc[], targetWorkspace: string | undefined, - relations: RelationDefinition[] | undefined + relations: RelationDefinition[] | undefined, + skipDeletedObsolete?: boolean ): Promise { const lang = getCurrentLanguage() @@ -65,7 +66,8 @@ export async function exportToWorkspace ( targetWorkspace, _class, relations, - fieldMappers + fieldMappers, + skipDeletedObsolete } body.query = diff --git a/plugins/export-resources/src/plugin.ts b/plugins/export-resources/src/plugin.ts index 457a4c243a..13d7e892b3 100644 --- a/plugins/export-resources/src/plugin.ts +++ b/plugins/export-resources/src/plugin.ts @@ -50,6 +50,7 @@ export default mergeIds(exportId, exportPlugin, { SelectWorkspaceToExportSpace: '' as IntlString, SelectSpace: '' as IntlString, NoSelectedDocuments: '' as IntlString, - RequestPermissionToImport: '' as IntlString + RequestPermissionToImport: '' as IntlString, + SkipDeletedObsolete: '' as IntlString } }) diff --git a/plugins/export/src/index.ts b/plugins/export/src/index.ts index 3940d36011..1746b47579 100644 --- a/plugins/export/src/index.ts +++ b/plugins/export/src/index.ts @@ -16,6 +16,7 @@ import { exportPlugin, exportId } from './plugin' export * from './types' +export * from './utils' export { exportId } diff --git a/plugins/export/src/utils.ts b/plugins/export/src/utils.ts new file mode 100644 index 0000000000..af27964436 --- /dev/null +++ b/plugins/export/src/utils.ts @@ -0,0 +1,36 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import type { Doc } from '@hcengineering/core' + +/** + * Checks if a document should be skipped based on its state. + * Returns true if the document should be skipped (archived, deleted, or obsolete). + * @param doc - The document to check + * @returns true if the document should be skipped, false otherwise + */ +export function shouldSkipDocument (doc: Doc): boolean { + // Check if document has a state field + if ('state' in doc) { + const docWithState = doc as Doc & { state?: string } + if (typeof docWithState.state === 'string') { + const state = docWithState.state + // Skip documents in archived, deleted, or obsolete state + if (state === 'deleted' || state === 'archived' || state === 'obsolete') { + return true + } + } + } + return false +} diff --git a/services/export/pod-export/src/server.ts b/services/export/pod-export/src/server.ts index 217ca64a54..993f56b270 100644 --- a/services/export/pod-export/src/server.ts +++ b/services/export/pod-export/src/server.ts @@ -404,7 +404,8 @@ export function createServer ( conflictStrategy, includeAttachments, relations: rawRelations, - fieldMappers + fieldMappers, + skipDeletedObsolete }: { targetWorkspace: WorkspaceUuid _class: Ref> @@ -415,6 +416,7 @@ export function createServer ( objectId?: Ref objectSpace?: Ref fieldMappers?: Record> + skipDeletedObsolete?: boolean } = req.body // Validate required parameters @@ -528,7 +530,8 @@ export function createServer ( conflictStrategy: conflictStrategy ?? 'duplicate', includeAttachments: includeAttachments ?? true, relations, - fieldMappers + fieldMappers, + skipDeletedObsolete: skipDeletedObsolete ?? true } const exportResult: ExportResult = await exporter.export(options) diff --git a/services/export/pod-export/src/workspace/types.ts b/services/export/pod-export/src/workspace/types.ts index 7bf3b92ffb..4c824ceee1 100644 --- a/services/export/pod-export/src/workspace/types.ts +++ b/services/export/pod-export/src/workspace/types.ts @@ -47,6 +47,8 @@ export interface ExportOptions { // Field mappers per class: { classA: { fieldA: value, fieldB: '$currentUser' }, ... } // Special value '$currentUser' will be replaced with current account's employee ID fieldMappers?: Record> + // Whether to skip documents and templates in deleted or obsolete state + skipDeletedObsolete?: boolean } export interface ExportResult { diff --git a/services/export/pod-export/src/workspace/workspace-exporter.ts b/services/export/pod-export/src/workspace/workspace-exporter.ts index de2a00196f..f3067c4e2c 100644 --- a/services/export/pod-export/src/workspace/workspace-exporter.ts +++ b/services/export/pod-export/src/workspace/workspace-exporter.ts @@ -24,6 +24,7 @@ import { } from '@hcengineering/core' import contact, { type Employee } from '@hcengineering/contact' import { type StorageAdapter } from '@hcengineering/server-core' +import { shouldSkipDocument } from '@hcengineering/export' import { AttachmentExporter } from './attachment-exporter' import { DataMapper } from './data-mapper' import { DocumentExporter } from './document-exporter' @@ -124,7 +125,8 @@ export class CrossWorkspaceExporter { includeAttachments = true, mapper, relations = [], - fieldMappers = {} + fieldMappers = {}, + skipDeletedObsolete = true } = options // Store field mappers @@ -202,10 +204,13 @@ export class CrossWorkspaceExporter { this.context.info(`Processing batch: ${processedCount + 1}-${processedCount + docs.length}`) + // Filter out archived/deleted/obsolete documents if skipDeletedObsolete is enabled + const docsToProcess = skipDeletedObsolete ? docs.filter((doc) => !shouldSkipDocument(doc)) : docs + // Check for existing documents in bulk if needed const existingDocsMap = new Map, Doc>() if (conflictStrategy === 'skip') { - const docIds = docs.map((d) => d._id) + const docIds = docsToProcess.map((d) => d._id) const existing = await this.targetClient.findAll(_class, { _id: { $in: docIds } }) for (const doc of existing) { existingDocsMap.set(doc._id, doc) @@ -213,7 +218,7 @@ export class CrossWorkspaceExporter { } // Export documents in this batch - for (const doc of docs) { + for (const doc of docsToProcess) { try { // Apply mapper if provided const mappedDoc = mapper !== undefined ? await mapper(doc) : doc From 22cd2bf25700a875aafcf87a945c663e0471439d Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Tue, 27 Jan 2026 13:31:57 +0700 Subject: [PATCH 04/67] Reuse one metadata interface (#10444) * Reuse one metadata interface Signed-off-by: Artem Savchenko * Fix imports Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- .../extension/shortcuts/tablePaste.ts | 14 +------------- .../actions/OriginalTableDataViewer.svelte | 3 +-- .../table/actions/TableDiffViewer.svelte | 2 +- .../actions/TableRefreshConfirmation.svelte | 2 +- .../table/actions/TableSourceInfo.svelte | 3 +-- .../extension/table/actions/tableUtils.ts | 2 +- .../extension/table/refreshTable.ts | 3 +-- .../extension/table/tableMetadata.ts | 11 +---------- .../view-resources/src/copyAsMarkdownTable.ts | 19 ++----------------- plugins/view-resources/src/index.ts | 1 - plugins/view/src/types.ts | 12 ++++++++++++ 11 files changed, 22 insertions(+), 50 deletions(-) diff --git a/plugins/text-editor-resources/src/components/extension/shortcuts/tablePaste.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/tablePaste.ts index e824bc598f..d0c11e261b 100644 --- a/plugins/text-editor-resources/src/components/extension/shortcuts/tablePaste.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/tablePaste.ts @@ -16,19 +16,7 @@ import { markdownToMarkup } from '@hcengineering/text-markdown' import { Extension } from '@tiptap/core' import { Fragment, Node } from '@tiptap/pm/model' import { Plugin } from '@tiptap/pm/state' - -// TableMetadata type - matches the definition in @hcengineering/view-resources -// Defined here to avoid circular dependency (view-resources depends on text-editor-resources) -interface TableMetadata { - version: string - cardClass: string - viewletId?: string - config?: Array> - query?: Record - documentIds: string[] - timestamp: number - workspace?: string -} +import type { TableMetadata } from '@hcengineering/view' export const TableMetadataPasteExtension = Extension.create({ name: 'tableMetadataPaste', diff --git a/plugins/text-editor-resources/src/components/extension/table/actions/OriginalTableDataViewer.svelte b/plugins/text-editor-resources/src/components/extension/table/actions/OriginalTableDataViewer.svelte index ceb5408d2a..972110d318 100644 --- a/plugins/text-editor-resources/src/components/extension/table/actions/OriginalTableDataViewer.svelte +++ b/plugins/text-editor-resources/src/components/extension/table/actions/OriginalTableDataViewer.svelte @@ -18,9 +18,8 @@ import presentation, { Card } from '@hcengineering/presentation' import textEditor from '@hcengineering/text-editor' import { Component, Loading } from '@hcengineering/ui' - import type { BuildModelKey, Viewlet } from '@hcengineering/view' + import type { BuildModelKey, TableMetadata, Viewlet } from '@hcengineering/view' import { createEventDispatcher } from 'svelte' - import type { TableMetadata } from '../tableMetadata' import TableSourceInfo from './TableSourceInfo.svelte' diff --git a/plugins/text-editor-resources/src/components/extension/table/actions/TableDiffViewer.svelte b/plugins/text-editor-resources/src/components/extension/table/actions/TableDiffViewer.svelte index 6bc051a5e4..c8d0b2515b 100644 --- a/plugins/text-editor-resources/src/components/extension/table/actions/TableDiffViewer.svelte +++ b/plugins/text-editor-resources/src/components/extension/table/actions/TableDiffViewer.svelte @@ -18,7 +18,7 @@ import presentation, { Card } from '@hcengineering/presentation' import textEditor from '@hcengineering/text-editor' import { createEventDispatcher } from 'svelte' - import type { TableMetadata } from '../tableMetadata' + import type { TableMetadata } from '@hcengineering/view' import MarkupDiffViewer from '../../../MarkupDiffViewer.svelte' import TableSourceInfo from './TableSourceInfo.svelte' diff --git a/plugins/text-editor-resources/src/components/extension/table/actions/TableRefreshConfirmation.svelte b/plugins/text-editor-resources/src/components/extension/table/actions/TableRefreshConfirmation.svelte index 8becb0d687..c09804c6ff 100644 --- a/plugins/text-editor-resources/src/components/extension/table/actions/TableRefreshConfirmation.svelte +++ b/plugins/text-editor-resources/src/components/extension/table/actions/TableRefreshConfirmation.svelte @@ -19,7 +19,7 @@ import textEditor from '@hcengineering/text-editor' import { Button } from '@hcengineering/ui' import { createEventDispatcher } from 'svelte' - import type { TableMetadata } from '../tableMetadata' + import type { TableMetadata } from '@hcengineering/view' import MarkupDiffViewer from '../../../MarkupDiffViewer.svelte' import TableSourceInfo from './TableSourceInfo.svelte' diff --git a/plugins/text-editor-resources/src/components/extension/table/actions/TableSourceInfo.svelte b/plugins/text-editor-resources/src/components/extension/table/actions/TableSourceInfo.svelte index 96b8d9bb6c..ff2937d43e 100644 --- a/plugins/text-editor-resources/src/components/extension/table/actions/TableSourceInfo.svelte +++ b/plugins/text-editor-resources/src/components/extension/table/actions/TableSourceInfo.svelte @@ -17,9 +17,8 @@ import core from '@hcengineering/core' import { IconWithEmoji, getClient } from '@hcengineering/presentation' import ui, { Icon, Label } from '@hcengineering/ui' - import view from '@hcengineering/view' + import view, { type TableMetadata } from '@hcengineering/view' import textEditor from '@hcengineering/text-editor' - import type { TableMetadata } from '../tableMetadata' export let metadata: TableMetadata diff --git a/plugins/text-editor-resources/src/components/extension/table/actions/tableUtils.ts b/plugins/text-editor-resources/src/components/extension/table/actions/tableUtils.ts index ab9c9f7317..13f6b5796a 100644 --- a/plugins/text-editor-resources/src/components/extension/table/actions/tableUtils.ts +++ b/plugins/text-editor-resources/src/components/extension/table/actions/tableUtils.ts @@ -16,8 +16,8 @@ import { type Node } from '@tiptap/pm/model' import { TableMap } from '@tiptap/pm/tables' import type { Client } from '@hcengineering/core' import { getClient } from '@hcengineering/presentation' +import type { TableMetadata } from '@hcengineering/view' import { buildMarkdownTableFromDocs } from '../refreshTable' -import type { TableMetadata } from '../tableMetadata' /** * Extract markdown string from a ProseMirror table node diff --git a/plugins/text-editor-resources/src/components/extension/table/refreshTable.ts b/plugins/text-editor-resources/src/components/extension/table/refreshTable.ts index a9b2ed7ed1..06567f0718 100644 --- a/plugins/text-editor-resources/src/components/extension/table/refreshTable.ts +++ b/plugins/text-editor-resources/src/components/extension/table/refreshTable.ts @@ -14,8 +14,7 @@ import type { Client, Doc } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' -import view, { type BuildMarkdownTableMetadata } from '@hcengineering/view' -import type { TableMetadata } from './tableMetadata' +import view, { type BuildMarkdownTableMetadata, type TableMetadata } from '@hcengineering/view' /** * Build markdown table string from documents and metadata diff --git a/plugins/text-editor-resources/src/components/extension/table/tableMetadata.ts b/plugins/text-editor-resources/src/components/extension/table/tableMetadata.ts index e0f5d6f552..7640a9d087 100644 --- a/plugins/text-editor-resources/src/components/extension/table/tableMetadata.ts +++ b/plugins/text-editor-resources/src/components/extension/table/tableMetadata.ts @@ -13,16 +13,7 @@ // import type { Node } from '@tiptap/pm/model' -import type { BuildMarkdownTableMetadata } from '@hcengineering/view' - -// Extended TableMetadata for text editor storage (includes additional fields for persistence) -export interface TableMetadata extends BuildMarkdownTableMetadata { - version: string - documentIds: string[] - timestamp: number - workspace?: string - originalUrl?: string // Original URL of the page/view where the table was created -} +import type { TableMetadata } from '@hcengineering/view' /** * Extract table metadata from a ProseMirror table node diff --git a/plugins/view-resources/src/copyAsMarkdownTable.ts b/plugins/view-resources/src/copyAsMarkdownTable.ts index 8e252dd4e7..6f64933b6a 100644 --- a/plugins/view-resources/src/copyAsMarkdownTable.ts +++ b/plugins/view-resources/src/copyAsMarkdownTable.ts @@ -32,7 +32,8 @@ import viewPlugin, { type Viewlet, type AttributeModel, type BuildModelKey, - type BuildMarkdownTableMetadata + type BuildMarkdownTableMetadata, + type TableMetadata } from '@hcengineering/view' import presentation, { getClient } from '@hcengineering/presentation' import { getName, getPersonByPersonId } from '@hcengineering/contact' @@ -479,22 +480,6 @@ export interface CopyRelationshipTableAsMarkdownProps { query?: DocumentQuery // Original query used to fetch documents } -/** - * Metadata structure for table clipboard data - * Used to preserve query and configuration for refresh/diff functionality - */ -export interface TableMetadata { - version: string // For future compatibility - cardClass: Ref> - viewletId?: Ref - config?: Array - query?: DocumentQuery - documentIds: Array> - timestamp: number - workspace?: string // Optional workspace identifier - originalUrl?: string // Original URL of the page/view where the table was created -} - /** * Build metadata object from props and documents * If viewlet is not provided, tries to find a default viewlet for the class diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 71176bda35..46e29c8640 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -225,7 +225,6 @@ export { type CopyRelationshipTableAsMarkdownProps, type RelationshipCellModel, type RelationshipRowModel, - type TableMetadata, type ValueFormatter, registerValueFormatterForClass, registerValueFormatter, diff --git a/plugins/view/src/types.ts b/plugins/view/src/types.ts index 3c07a45cec..fde684d54b 100644 --- a/plugins/view/src/types.ts +++ b/plugins/view/src/types.ts @@ -906,3 +906,15 @@ export interface BuildMarkdownTableMetadata { query?: Record | DocumentQuery originalUrl?: string // Original URL of the page/view where the table was created } + +/** + * @public + * Complete table metadata including persistence fields + * Extends BuildMarkdownTableMetadata with additional fields for storage and versioning + */ +export interface TableMetadata extends BuildMarkdownTableMetadata { + version: string // For future compatibility + documentIds: Array> // Document IDs used in the table + timestamp: number // Timestamp when the table was created/updated + workspace?: string // Optional workspace identifier +} From 3fecde8f7e6b608ef9120382e51d6c87dfad6100 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Tue, 27 Jan 2026 15:08:31 +0700 Subject: [PATCH 05/67] Fix custom attributes in markdown table (#10445) * Fix custom attributes in markdown table Signed-off-by: Artem Savchenko * Clean up Signed-off-by: Artem Savchenko * Refactor Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- .../src/__tests__/copyAsMarkdownTable.test.ts | 3 +- .../view-resources/src/copyAsMarkdownTable.ts | 163 ++++----- plugins/view-resources/src/index.ts | 2 +- .../view-resources/src/markdownTableUtils.ts | 343 ++++++++++++++++++ 4 files changed, 409 insertions(+), 102 deletions(-) create mode 100644 plugins/view-resources/src/markdownTableUtils.ts diff --git a/plugins/view-resources/src/__tests__/copyAsMarkdownTable.test.ts b/plugins/view-resources/src/__tests__/copyAsMarkdownTable.test.ts index 41fe3512ab..639beef7b0 100644 --- a/plugins/view-resources/src/__tests__/copyAsMarkdownTable.test.ts +++ b/plugins/view-resources/src/__tests__/copyAsMarkdownTable.test.ts @@ -15,7 +15,8 @@ // Mock platform plugin function first (before any imports) // Import after mocks are set up -import { CopyAsMarkdownTable, isIntlString } from '../copyAsMarkdownTable' +import { CopyAsMarkdownTable } from '../copyAsMarkdownTable' +import { isIntlString } from '../markdownTableUtils' import core, { type Class, type Doc, type Ref } from '@hcengineering/core' import { type IntlString } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' diff --git a/plugins/view-resources/src/copyAsMarkdownTable.ts b/plugins/view-resources/src/copyAsMarkdownTable.ts index 6f64933b6a..2b6d71fbb5 100644 --- a/plugins/view-resources/src/copyAsMarkdownTable.ts +++ b/plugins/view-resources/src/copyAsMarkdownTable.ts @@ -37,7 +37,17 @@ import viewPlugin, { } from '@hcengineering/view' import presentation, { getClient } from '@hcengineering/presentation' import { getName, getPersonByPersonId } from '@hcengineering/contact' -import { buildModel, buildConfigLookup, getAttributeValue, getObjectLinkFragment } from './utils' +import { buildModel, buildConfigLookup, getAttributeValue } from './utils' +import { + generateHeaders, + modelToConfig, + formatArrayValue, + extractObjectTitleOrName, + formatCustomAttributeValue, + escapeMarkdownLinkText, + createMarkdownLink, + isIntlString +} from './markdownTableUtils' import view from './plugin' import SimpleNotification from './components/SimpleNotification.svelte' import { copyMarkdown } from './actionImpl' @@ -190,13 +200,6 @@ async function buildTableModel ( * Examples: card:string:Card, contact:class:UserProfile, card:types:Document * @public */ -export function isIntlString (value: string): boolean { - if (typeof value !== 'string' || value.length === 0) { - return false - } - const parts = value.split(':') - return parts.length >= 3 && parts.every((part) => part.length > 0) -} async function loadPersonName ( personId: PersonId, @@ -335,9 +338,29 @@ async function formatValue ( } } - // If this is an empty key but NOT the first column, return empty string - // (empty key should only be used for the object presenter in the first column) + // Handle custom attributes that failed to build properly in the model + // These have key: '' but the actual attribute name is in the label if (attr.key === '' && !isFirstColumn) { + const labelStr = typeof attr.label === 'string' ? attr.label : '' + const isCustomAttribute = labelStr.startsWith('custom') + + if (isCustomAttribute) { + const customValue = (card as any)[labelStr] + if (customValue === null || customValue === undefined) { + return '' + } + + const docClass = card._class + let customAttr = hierarchy.findAttribute(docClass, labelStr) + + if (customAttr === undefined) { + const allAttrs = hierarchy.getAllAttributes(docClass) + customAttr = allAttrs.get(labelStr) + } + + return await formatCustomAttributeValue(customValue, customAttr, card, hierarchy, language) + } + return '' } @@ -345,7 +368,8 @@ async function formatValue ( return '' } - const attribute = hierarchy.findAttribute(_class, attr.key) + // Use attribute from model if available, otherwise try to find it + const attribute = attr.attribute ?? hierarchy.findAttribute(_class, attr.key) const attrType = attribute?.type if (typeof value === 'number' && attrType?._class === core.class.TypeTimestamp) { @@ -366,6 +390,22 @@ async function formatValue ( } if (typeof value === 'string') { + const isRef = attrType?._class === core.class.RefTo + if (isRef) { + const cardWithLookup = card as any + const lookupData = cardWithLookup.$lookup?.[attr.key] + if (lookupData !== undefined && lookupData !== null) { + const resolvedObj = lookupData + if (typeof resolvedObj === 'object' && resolvedObj !== null && 'title' in resolvedObj) { + const title = resolvedObj[DocumentAttributeKey.Title] ?? '' + if (typeof title === 'string' && isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return String(title) + } + } + } + if (isIntlString(value)) { return await translate(value as unknown as IntlString, {}, language) } @@ -376,79 +416,18 @@ async function formatValue ( } if (Array.isArray(value)) { - const translatedValues = await Promise.all( - value.map(async (v) => { - if (typeof v === 'object' && v !== null && 'title' in v) { - const title = v[DocumentAttributeKey.Title] ?? '' - if (typeof title === 'string' && isIntlString(title)) { - return await translate(title as unknown as IntlString, {}, language) - } - return String(title) - } - if (typeof v === 'string' && isIntlString(v)) { - return await translate(v as unknown as IntlString, {}, language) - } - return typeof v === 'string' ? v : String(v) - }) - ) - return translatedValues.join(', ') + return await formatArrayValue(value, attrType, attribute, attr.key, card, language) } if (typeof value === 'object' && value !== null) { const obj = value as Record - const title = obj[DocumentAttributeKey.Title] - if (title != null && title !== undefined) { - const titleStr = String(title) - if (isIntlString(titleStr)) { - return await translate(titleStr as unknown as IntlString, {}, language) - } - return titleStr - } - const name = obj[DocumentAttributeKey.Name] - if (name != null && name !== undefined) { - const nameStr = String(name) - if (isIntlString(nameStr)) { - return await translate(nameStr as unknown as IntlString, {}, language) - } - return nameStr - } - return String(value) + const titleOrName = await extractObjectTitleOrName(obj, language) + return titleOrName !== '' ? titleOrName : String(value) } return String(value) } -function escapeMarkdownLinkText (text: string): string { - // Escape backslashes first, then brackets and pipes, and normalize newlines to spaces - return text - .replace(/\\/g, '\\\\') - .replace(/\[/g, '\\[') - .replace(/\]/g, '\\]') - .replace(/\|/g, '\\|') - .replace(/\r?\n/g, ' ') -} - -function escapeMarkdownLinkUrl (url: string): string { - // Escape backslashes and closing parentheses used to terminate the URL - return url.replace(/\\/g, '\\\\').replace(/\)/g, '\\)') -} - -async function createMarkdownLink (hierarchy: Hierarchy, card: Doc, value: string): Promise { - try { - const loc = await getObjectLinkFragment(hierarchy, card, {}, view.component.EditDoc) - const relativeUrl = locationToUrl(loc) - const frontUrl = - getMetadata(presentation.metadata.FrontUrl) ?? (typeof window !== 'undefined' ? window.location.origin : '') - const fullUrl = concatLink(frontUrl, relativeUrl) - const escapedText = escapeMarkdownLinkText(value) - const escapedUrl = escapeMarkdownLinkUrl(fullUrl) - return `[${escapedText}](${escapedUrl})` - } catch { - // If link generation fails, fall back to plain text - return escapeMarkdownLinkText(value) - } -} - export interface CopyAsMarkdownTableProps { cardClass: Ref> viewlet?: Viewlet @@ -543,7 +522,7 @@ export function buildRelationshipTableMetadata ( version: '1.0', cardClass: props.cardClass, viewletId: undefined, // Relationship tables don't use viewlets - config: props.model.map((m) => m.key), + config: modelToConfig(props.model), // Preserve custom attributes by converting model to config query: props.query, documentIds: docs.map((d) => d._id), timestamp: Date.now(), @@ -641,19 +620,11 @@ export async function buildMarkdownTableFromDocs ( const language = getCurrentLanguage() - // Cache for user ID (PersonId) -> name mappings to reduce database calls const userCache = new Map() - const headers: string[] = [] - for (const attr of displayableModel) { - let label: string - if (typeof attr.label === 'string') { - label = isIntlString(attr.label) ? await translate(attr.label as unknown as IntlString, {}, language) : attr.label - } else { - label = await translate(attr.label, {}, language) - } - headers.push(label) - } + const firstDocClass = docs.length > 0 ? docs[0]._class : props.cardClass + + const headers = await generateHeaders(displayableModel, firstDocClass, hierarchy, language) const rows: string[][] = [] for (const card of docs) { @@ -761,19 +732,11 @@ export async function CopyRelationshipTableAsMarkdown ( // Cache for user ID (PersonId) -> name mappings to reduce database calls const userCache = new Map() - // Extract headers from model - const headers: string[] = [] - for (const attr of props.model) { - let label: string - if (typeof attr.label === 'string') { - label = isIntlString(attr.label) - ? await translate(attr.label as unknown as IntlString, {}, language) - : attr.label - } else { - label = await translate(attr.label, {}, language) - } - headers.push(label) - } + // Get the first document's class for custom attribute lookup + const firstDocClass = props.objects.length > 0 ? props.objects[0]._class : props.cardClass + + // Generate headers using common function + const headers = await generateHeaders(props.model, firstDocClass, hierarchy, language) // Build a map of attribute keys to their index in the model for quick lookup const attributeKeyToIndex = new Map() diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 46e29c8640..eb64971b61 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -228,10 +228,10 @@ export { type ValueFormatter, registerValueFormatterForClass, registerValueFormatter, - isIntlString, buildMarkdownTableFromDocs, buildMarkdownTableFromMetadata } from './copyAsMarkdownTable' +export { isIntlString } from './markdownTableUtils' export type { BuildMarkdownTableMetadata } from '@hcengineering/view' export { ArrayEditor, diff --git a/plugins/view-resources/src/markdownTableUtils.ts b/plugins/view-resources/src/markdownTableUtils.ts new file mode 100644 index 0000000000..b4252bae7c --- /dev/null +++ b/plugins/view-resources/src/markdownTableUtils.ts @@ -0,0 +1,343 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + type AnyAttribute, + type Class, + type Doc, + type Hierarchy, + type Ref, + concatLink, + getDisplayTime +} from '@hcengineering/core' +import { translate, type IntlString, getMetadata } from '@hcengineering/platform' +import { locationToUrl } from '@hcengineering/ui' +import presentation from '@hcengineering/presentation' +import { type AttributeModel, type BuildModelKey } from '@hcengineering/view' +import { getObjectLinkFragment } from './utils' +import view from './plugin' + +enum DateFormatOption { + Numeric = 'numeric', + Short = 'short' +} + +/** + * Check if a string is an IntlString (format: "plugin:resource:key") + */ +export function isIntlString (value: string): boolean { + if (typeof value !== 'string' || value.length === 0) { + return false + } + const parts = value.split(':') + return parts.length >= 3 && parts.every((part) => part.length > 0) +} + +/** + * Resolve the human-readable label for a custom attribute + * @param attrLabel - The attribute label (may be an ID like "custom...") + * @param docClass - The document's class (MasterTag) where the attribute is defined + * @param hierarchy - The hierarchy instance + * @param language - Current language + * @returns The translated human-readable label, or the original label if not found + */ +export async function resolveCustomAttributeLabel ( + attrLabel: string, + docClass: Ref>, + hierarchy: Hierarchy, + language: string | undefined +): Promise { + if (!attrLabel.startsWith('custom')) { + return attrLabel + } + + let customAttr = hierarchy.findAttribute(docClass, attrLabel) + if (customAttr === undefined) { + const allAttrs = hierarchy.getAllAttributes(docClass) + customAttr = allAttrs.get(attrLabel) + } + + if (customAttr?.label !== undefined) { + return await translate(customAttr.label, {}, language) + } + + return attrLabel +} + +/** + * Generate table headers from AttributeModel array + * Handles custom attributes, IntlStrings, and regular labels + * @param model - Array of AttributeModel to generate headers from + * @param firstDocClass - The first document's class (for custom attribute lookup) + * @param hierarchy - The hierarchy instance + * @param language - Current language + * @returns Array of header strings + */ +export async function generateHeaders ( + model: AttributeModel[], + firstDocClass: Ref>, + hierarchy: Hierarchy, + language: string | undefined +): Promise { + const headers: string[] = [] + for (const attr of model) { + let label: string + if (typeof attr.label === 'string') { + if (attr.label.startsWith('custom')) { + label = await resolveCustomAttributeLabel(attr.label, firstDocClass, hierarchy, language) + } else if (isIntlString(attr.label)) { + label = await translate(attr.label as unknown as IntlString, {}, language) + } else { + label = attr.label + } + } else { + label = await translate(attr.label, {}, language) + } + headers.push(label) + } + return headers +} + +/** + * Convert AttributeModel array back to config format (Array) + * Preserves custom attributes by using label as key when key is empty + * @param model - Array of AttributeModel to convert + * @returns Config array that can be used to rebuild the table + */ +export function modelToConfig (model: AttributeModel[]): Array { + return model.map((m) => { + if (m.key === '' && typeof m.label === 'string' && m.label.startsWith('custom')) { + return { + key: m.label, // Use label (custom attribute name) as key + label: m.label, + displayProps: m.displayProps, + props: m.props, + sortingKey: m.sortingKey + } + } + if (m.key !== '') { + return m.key + } + if (m.castRequest !== undefined) { + return { + key: m.key, + label: m.label, + displayProps: m.displayProps, + props: m.props, + sortingKey: m.sortingKey + } + } + return m.key + }) +} + +/** + * Format an array of values, handling reference lookups if needed + * @param value - The array value + * @param attrType - The attribute type + * @param attribute - The attribute definition (for getting the name) + * @param attrKey - The attribute key (fallback if attribute.name is not available) + * @param card - The document + * @param language - Current language + * @returns Formatted string with comma-separated values + */ +export async function formatArrayValue ( + value: any[], + attrType: any, + attribute: AnyAttribute | undefined, + attrKey: string, + card: Doc, + language: string | undefined +): Promise { + const isRefArray = + attrType?._class === core.class.ArrOf && + (attrType as { of?: { _class?: Ref> } })?.of?._class === core.class.RefTo + + if (isRefArray && (attribute !== undefined || attrKey !== '')) { + const cardWithLookup = card as any + const lookupKey = attribute?.name ?? attrKey + const lookupData = cardWithLookup.$lookup?.[lookupKey] + + if (lookupData !== undefined && lookupData !== null) { + const resolvedArray = Array.isArray(lookupData) ? lookupData : [lookupData] + const translatedValues = await Promise.all( + resolvedArray.map(async (v) => { + if (typeof v === 'object' && v !== null && 'title' in v) { + const title = v.title ?? '' + if (typeof title === 'string' && isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return String(title) + } + return typeof v === 'string' ? v : String(v) + }) + ) + return translatedValues.join(', ') + } + } + + const translatedValues = await Promise.all( + value.map(async (v) => { + if (typeof v === 'object' && v !== null && 'title' in v) { + const title = v.title ?? '' + if (typeof title === 'string' && isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return String(title) + } + if (typeof v === 'string' && isIntlString(v)) { + return await translate(v as unknown as IntlString, {}, language) + } + return typeof v === 'string' ? v : String(v) + }) + ) + return translatedValues.join(', ') +} + +/** + * Extract title or name from an object, translating if needed + * @param obj - The object to extract from + * @param language - Current language + * @returns The title/name string, or empty string if not found + */ +export async function extractObjectTitleOrName ( + obj: Record, + language: string | undefined +): Promise { + if ('title' in obj) { + const title = String(obj.title ?? '') + if (isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return title + } + if ('name' in obj) { + const name = String(obj.name ?? '') + if (isIntlString(name)) { + return await translate(name as unknown as IntlString, {}, language) + } + return name + } + return '' +} + +/** + * Format a custom attribute value for markdown display + * Handles various types: string, number, boolean, arrays, references + */ +export async function formatCustomAttributeValue ( + value: any, + attribute: AnyAttribute | undefined, + card: Doc, + hierarchy: Hierarchy, + language: string | undefined +): Promise { + if (value === null || value === undefined) { + return '' + } + + const attrType = attribute?.type + + if (typeof value === 'number' && attrType?._class === core.class.TypeTimestamp) { + return getDisplayTime(value) + } + + if (value instanceof Date) { + const options: Intl.DateTimeFormatOptions = { + year: DateFormatOption.Numeric, + month: DateFormatOption.Short, + day: DateFormatOption.Numeric + } + return value.toLocaleDateString(language ?? 'default', options) + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value) + } + + if (typeof value === 'string') { + if (isIntlString(value)) { + return await translate(value as unknown as IntlString, {}, language) + } + + const isRef = attrType?._class === core.class.RefTo + if (isRef && attribute !== undefined) { + const cardWithLookup = card as any + const lookupData = cardWithLookup.$lookup?.[attribute.name] + if (lookupData !== undefined && lookupData !== null) { + if (typeof lookupData === 'object' && 'title' in lookupData) { + const title = lookupData.title ?? '' + if (typeof title === 'string' && isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return String(title) + } + } + } + + return value + } + + if (Array.isArray(value)) { + return await formatArrayValue(value, attrType, attribute, attribute?.name ?? '', card, language) + } + + if (typeof value === 'object' && value !== null) { + const obj = value as Record + const titleOrName = await extractObjectTitleOrName(obj, language) + return titleOrName !== '' ? titleOrName : String(value) + } + + return String(value) +} + +/** + * Escape markdown link text (brackets, pipes, backslashes, newlines) + */ +export function escapeMarkdownLinkText (text: string): string { + // Escape backslashes first, then brackets and pipes, and normalize newlines to spaces + return text + .replace(/\\/g, '\\\\') + .replace(/\[/g, '\\[') + .replace(/\]/g, '\\]') + .replace(/\|/g, '\\|') + .replace(/\r?\n/g, ' ') +} + +/** + * Escape markdown link URL (backslashes and closing parentheses) + */ +export function escapeMarkdownLinkUrl (url: string): string { + // Escape backslashes and closing parentheses used to terminate the URL + return url.replace(/\\/g, '\\\\').replace(/\)/g, '\\)') +} + +/** + * Create a markdown link for a document + */ +export async function createMarkdownLink (hierarchy: Hierarchy, card: Doc, value: string): Promise { + try { + const loc = await getObjectLinkFragment(hierarchy, card, {}, view.component.EditDoc) + const relativeUrl = locationToUrl(loc) + const frontUrl = + getMetadata(presentation.metadata.FrontUrl) ?? (typeof window !== 'undefined' ? window.location.origin : '') + const fullUrl = concatLink(frontUrl, relativeUrl) + const escapedText = escapeMarkdownLinkText(value) + const escapedUrl = escapeMarkdownLinkUrl(fullUrl) + return `[${escapedText}](${escapedUrl})` + } catch { + // If link generation fails, fall back to plain text + return escapeMarkdownLinkText(value) + } +} From de405134aa445670789d42b365c6e959eedd2feb Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Tue, 27 Jan 2026 16:22:23 +0700 Subject: [PATCH 06/67] Fix relationship table diff/refresh (#10446) Signed-off-by: Artem Savchenko --- .../view-resources/src/copyAsMarkdownTable.ts | 460 ++++++++++++------ 1 file changed, 322 insertions(+), 138 deletions(-) diff --git a/plugins/view-resources/src/copyAsMarkdownTable.ts b/plugins/view-resources/src/copyAsMarkdownTable.ts index 2b6d71fbb5..1671259374 100644 --- a/plugins/view-resources/src/copyAsMarkdownTable.ts +++ b/plugins/view-resources/src/copyAsMarkdownTable.ts @@ -37,7 +37,7 @@ import viewPlugin, { } from '@hcengineering/view' import presentation, { getClient } from '@hcengineering/presentation' import { getName, getPersonByPersonId } from '@hcengineering/contact' -import { buildModel, buildConfigLookup, getAttributeValue } from './utils' +import { buildModel, buildConfigLookup, buildConfigAssociation, getAttributeValue } from './utils' import { generateHeaders, modelToConfig, @@ -500,6 +500,14 @@ async function buildTableMetadata ( } } +/** + * Check if a table metadata represents a relationship table + * Relationship tables have viewletId: undefined + */ +export function isRelationshipTable (metadata: TableMetadata): boolean { + return metadata.viewletId === undefined +} + /** * Build metadata object for relationship tables */ @@ -530,6 +538,157 @@ export function buildRelationshipTableMetadata ( } } +/** + * Rebuild relationship table viewModel from documents and metadata + * Recreates the hierarchical structure with row spans and separate rows for each associated child + */ +async function rebuildRelationshipTableViewModel ( + docs: Doc[], + model: AttributeModel[], + cardClass: Ref>, + hierarchy: Hierarchy, + client: Client +): Promise { + const viewModel: RelationshipRowModel[] = [] + + // Build association queries from config to fetch associations + const config = model.map((m) => m.key) + const associations = buildConfigAssociation(config) + const lookup = buildConfigLookup(hierarchy, cardClass, config) + + const associationAttrs = model.filter((attr) => attr.key.startsWith('$associations')) + + // Fetch documents with associations if needed + let docsWithAssociations: Doc[] = docs + if (associations !== undefined && associations.length > 0) { + // Re-fetch documents with associations + const docIds = docs.map((d) => d._id) + const query = { _id: { $in: docIds } } + docsWithAssociations = await client.findAll(cardClass, query, { lookup, associations }) + } + + // Process each parent document and create rows with proper hierarchy + for (const parentDoc of docsWithAssociations) { + const docWithAssoc = parentDoc as any + const parentAssociations = docWithAssoc.$associations ?? {} + + // Find the maximum number of children across all association columns + let maxChildren = 0 + for (const assocAttr of associationAttrs) { + const assocKey = assocAttr.key.replace('$associations.', '') + const children = parentAssociations[assocKey] + if (Array.isArray(children)) { + maxChildren = Math.max(maxChildren, children.length) + } else if (children !== undefined && children !== null) { + maxChildren = Math.max(maxChildren, 1) + } + } + + // If no children, create a single row for the parent + if (maxChildren === 0) { + const cells: RelationshipCellModel[] = [] + for (const attr of model) { + const isAssociationKey = attr.key.startsWith('$associations') + cells.push({ + attribute: attr, + rowSpan: 1, + object: isAssociationKey ? undefined : parentDoc, + parentObject: isAssociationKey ? parentDoc : undefined + }) + } + viewModel.push({ cells }) + continue + } + + // Create rows: first row has parent with rowSpan, then one row per child + for (let childIndex = 0; childIndex < maxChildren; childIndex++) { + const cells: RelationshipCellModel[] = [] + + for (const attr of model) { + const isAssociationKey = attr.key.startsWith('$associations') + + if (attr.key === '') { + // First column: parent document with row span + cells.push({ + attribute: attr, + rowSpan: maxChildren, // Span across all child rows + object: parentDoc, + parentObject: undefined + }) + } else if (isAssociationKey) { + // Association column: show child at current index + const assocKey = attr.key.replace('$associations.', '') + const children = parentAssociations[assocKey] + let childDoc: Doc | undefined + if (Array.isArray(children) && children.length > childIndex) { + childDoc = children[childIndex] as Doc + } else if (!Array.isArray(children) && children !== undefined && children !== null && childIndex === 0) { + childDoc = children as Doc + } + + cells.push({ + attribute: attr, + rowSpan: 1, + object: childDoc, + parentObject: parentDoc + }) + } else { + // Regular attribute: show parent value only in first row + // In subsequent rows, this will be empty (handled by row span logic in markdown generation) + cells.push({ + attribute: attr, + rowSpan: 1, + object: childIndex === 0 ? parentDoc : undefined, + parentObject: undefined + }) + } + } + + viewModel.push({ cells }) + } + } + + return viewModel +} + +/** + * Build relationship table markdown from metadata + * Rebuilds the viewModel and model, then uses the common markdown generation logic + */ +async function buildRelationshipTableFromMetadata ( + docs: Doc[], + metadata: BuildMarkdownTableMetadata, + client: Client +): Promise { + const hierarchy = client.getHierarchy() + const cardClass = metadata.cardClass as Ref> + + // Rebuild model from config + const config = metadata.config ?? [] + const lookup = buildConfigLookup(hierarchy, cardClass, config) + const model = await buildModel({ + client, + _class: cardClass, + keys: config, + lookup + }) + + // Rebuild viewModel from documents + const viewModel = await rebuildRelationshipTableViewModel(docs, model, cardClass, hierarchy, client) + + // Build props and use common markdown generation + const props: CopyRelationshipTableAsMarkdownProps = { + viewModel, + model, + objects: docs, + cardClass, + query: metadata.query + } + + const language = getCurrentLanguage() + return await buildRelationshipTableMarkdown(props, hierarchy, language) +} + /** * Wrapper function for building markdown table from BuildMarkdownTableMetadata * This is used by text-editor-resources to refresh tables @@ -540,7 +699,13 @@ export async function buildMarkdownTableFromMetadata ( metadata: BuildMarkdownTableMetadata, client: Client ): Promise { - // Load viewlet if viewletId is provided + // Check if this is a relationship table (viewletId is undefined) + const tableMetadata = metadata as TableMetadata + if (isRelationshipTable(tableMetadata)) { + return await buildRelationshipTableFromMetadata(docs, metadata, client) + } + + // Regular table: Load viewlet if viewletId is provided let viewlet: Viewlet | undefined if (metadata.viewletId !== undefined) { viewlet = await client.findOne(viewPlugin.class.Viewlet, { _id: metadata.viewletId as Ref }) @@ -707,6 +872,157 @@ export async function CopyAsMarkdownTable ( } } +/** + * Build markdown table from relationship table props (viewModel, model, objects) + * This is the core logic extracted for reuse in both copy and refresh operations + */ +async function buildRelationshipTableMarkdown ( + props: CopyRelationshipTableAsMarkdownProps, + hierarchy: Hierarchy, + language: string | undefined +): Promise { + if (props.viewModel.length === 0 || props.model.length === 0) { + return '' + } + + // Cache for user ID (PersonId) -> name mappings to reduce database calls + const userCache = new Map() + + // Get the first document's class for custom attribute lookup + const firstDocClass = props.objects.length > 0 ? props.objects[0]._class : props.cardClass + + // Generate headers using common function + const headers = await generateHeaders(props.model, firstDocClass, hierarchy, language) + + // Build a map of attribute keys to their index in the model for quick lookup + const attributeKeyToIndex = new Map() + props.model.forEach((attr, index) => { + attributeKeyToIndex.set(attr.key, index) + }) + + // Track active row spans - maps attribute key to remaining span count + const activeRowSpans = new Map() + + // Process rows from viewModel + const rows: string[][] = [] + for (let rowIdx = 0; rowIdx < props.viewModel.length; rowIdx++) { + const rowModel = props.viewModel[rowIdx] + const row: string[] = new Array(headers.length).fill('') + + // First, handle cells that are continuing from previous rows (row spans) + for (const [attrKey, spanInfo] of activeRowSpans.entries()) { + if (spanInfo.remaining > 0) { + const attrIndex = attributeKeyToIndex.get(attrKey) + if (attrIndex !== undefined) { + row[attrIndex] = spanInfo.value + spanInfo.remaining-- + if (spanInfo.remaining === 0) { + activeRowSpans.delete(attrKey) + } + } + } + } + + // Then, process cells in the current row + for (const cell of rowModel.cells) { + const attrIndex = attributeKeyToIndex.get(cell.attribute.key) + if (attrIndex === undefined) continue + + // Determine if this is an association column + const isAssociationKey = cell.attribute.key.startsWith('$associations') + + let doc: Doc | undefined + if (isAssociationKey) { + doc = cell.object + } else { + doc = cell.object ?? cell.parentObject + } + + if (doc === undefined) { + // Empty cell + row[attrIndex] = '' + continue + } + + // Use the same getValue logic as RelationshipTable + // For association keys, this returns the child document object itself + const rawValue = getAttributeValue(cell.attribute, doc, hierarchy) + + // Determine which document and class to use for formatting + let docToUse = doc + let docClass = props.cardClass + let attributeToUse = cell.attribute + + if (isAssociationKey) { + // For association keys, the value IS the child document object + if (rawValue !== undefined && rawValue !== null && typeof rawValue === 'object' && '_class' in rawValue) { + docToUse = rawValue as Doc + docClass = docToUse._class + const parts = cell.attribute.key.split('$associations.') + if (parts.length > 1) { + const afterAssoc = parts[1].substring(1) // Remove leading dot + const dotIndex = afterAssoc.indexOf('.') + if (dotIndex > 0) { + const attributeName = afterAssoc.substring(dotIndex + 1) + attributeToUse = { + ...cell.attribute, + key: attributeName + } + } else { + attributeToUse = { + ...cell.attribute, + key: '' + } + } + } + } + } + + // Format the value using the same logic as regular tables + const isFirstColumn = attrIndex === 0 + const allowEmptyKey = isFirstColumn || isAssociationKey + let value = await formatValue( + attributeToUse, + docToUse, + hierarchy, + docClass, + language, + allowEmptyKey, // Pass true for association keys so empty key works + userCache, + props.valueFormatter + ) + + const isDocumentTitle = attributeToUse.key === '' && docToUse !== undefined + if (isDocumentTitle) { + value = await createMarkdownLink(hierarchy, docToUse, value) + } else { + value = escapeMarkdownLinkText(value) + } + + row[attrIndex] = value + + // If this cell has a row span > 1, track it for subsequent rows + if (cell.rowSpan > 1) { + activeRowSpans.set(cell.attribute.key, { + value, + remaining: cell.rowSpan - 1 + }) + } + } + + rows.push(row) + } + + // Build markdown table + let markdown = '| ' + headers.join(' | ') + ' |\n' + markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n' + for (const row of rows) { + markdown += '| ' + row.join(' | ') + ' |\n' + } + + return markdown +} + /** * Copy RelationshipTable as markdown table * Handles hierarchical data with row spans by duplicating cell values across spanned rows @@ -716,10 +1032,6 @@ export async function CopyRelationshipTableAsMarkdown ( props: CopyRelationshipTableAsMarkdownProps ): Promise { try { - if (props.viewModel.length === 0 || props.model.length === 0) { - return - } - const client = getClient() const hierarchy = client.getHierarchy() const cardClass = hierarchy.getClass(props.cardClass) @@ -729,139 +1041,11 @@ export async function CopyRelationshipTableAsMarkdown ( const language = getCurrentLanguage() - // Cache for user ID (PersonId) -> name mappings to reduce database calls - const userCache = new Map() + // Build markdown using extracted function + const markdown = await buildRelationshipTableMarkdown(props, hierarchy, language) - // Get the first document's class for custom attribute lookup - const firstDocClass = props.objects.length > 0 ? props.objects[0]._class : props.cardClass - - // Generate headers using common function - const headers = await generateHeaders(props.model, firstDocClass, hierarchy, language) - - // Build a map of attribute keys to their index in the model for quick lookup - const attributeKeyToIndex = new Map() - props.model.forEach((attr, index) => { - attributeKeyToIndex.set(attr.key, index) - }) - - // Track active row spans - maps attribute key to remaining span count - const activeRowSpans = new Map() - - // Process rows from viewModel - const rows: string[][] = [] - for (let rowIdx = 0; rowIdx < props.viewModel.length; rowIdx++) { - const rowModel = props.viewModel[rowIdx] - const row: string[] = new Array(headers.length).fill('') - - // First, handle cells that are continuing from previous rows (row spans) - for (const [attrKey, spanInfo] of activeRowSpans.entries()) { - if (spanInfo.remaining > 0) { - const attrIndex = attributeKeyToIndex.get(attrKey) - if (attrIndex !== undefined) { - row[attrIndex] = spanInfo.value - spanInfo.remaining-- - if (spanInfo.remaining === 0) { - activeRowSpans.delete(attrKey) - } - } - } - } - - // Then, process cells in the current row - for (const cell of rowModel.cells) { - const attrIndex = attributeKeyToIndex.get(cell.attribute.key) - if (attrIndex === undefined) continue - - // Determine if this is an association column - const isAssociationKey = cell.attribute.key.startsWith('$associations') - - let doc: Doc | undefined - if (isAssociationKey) { - doc = cell.object - } else { - doc = cell.object ?? cell.parentObject - } - - if (doc === undefined) { - // Empty cell - row[attrIndex] = '' - continue - } - - // Use the same getValue logic as RelationshipTable - // For association keys, this returns the child document object itself - const rawValue = getAttributeValue(cell.attribute, doc, hierarchy) - - // Determine which document and class to use for formatting - let docToUse = doc - let docClass = props.cardClass - let attributeToUse = cell.attribute - - if (isAssociationKey) { - // For association keys, the value IS the child document object - if (rawValue !== undefined && rawValue !== null && typeof rawValue === 'object' && '_class' in rawValue) { - docToUse = rawValue as Doc - docClass = docToUse._class - const parts = cell.attribute.key.split('$associations.') - if (parts.length > 1) { - const afterAssoc = parts[1].substring(1) // Remove leading dot - const dotIndex = afterAssoc.indexOf('.') - if (dotIndex > 0) { - const attributeName = afterAssoc.substring(dotIndex + 1) - attributeToUse = { - ...cell.attribute, - key: attributeName - } - } else { - attributeToUse = { - ...cell.attribute, - key: '' - } - } - } - } - } - - // Format the value using the same logic as regular tables - const isFirstColumn = attrIndex === 0 - const allowEmptyKey = isFirstColumn || isAssociationKey - let value = await formatValue( - attributeToUse, - docToUse, - hierarchy, - docClass, - language, - allowEmptyKey, // Pass true for association keys so empty key works - userCache, - props.valueFormatter - ) - - const isDocumentTitle = attributeToUse.key === '' && docToUse !== undefined - if (isDocumentTitle) { - value = await createMarkdownLink(hierarchy, docToUse, value) - } else { - value = escapeMarkdownLinkText(value) - } - - row[attrIndex] = value - - // If this cell has a row span > 1, track it for subsequent rows - if (cell.rowSpan > 1) { - activeRowSpans.set(cell.attribute.key, { - value, - remaining: cell.rowSpan - 1 - }) - } - } - - rows.push(row) - } - - // Build markdown table - let markdown = '| ' + headers.join(' | ') + ' |\n' - markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n' - for (const row of rows) { - markdown += '| ' + row.join(' | ') + ' |\n' + if (markdown.length === 0) { + return } // Build metadata for relationship table refresh/diff functionality From 0adaefe5f32b632911d18bdcc846eea439d43442 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Wed, 28 Jan 2026 14:56:32 +0700 Subject: [PATCH 07/67] Add activity section in controlled doc (#10457) * Add activity section in controlled doc Signed-off-by: Artem Savchenko * Remove approved state Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- .../components/document/EditDocContent.svelte | 22 ++++++++++++++++++- .../src/utils.ts | 15 +++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/plugins/controlled-documents-resources/src/components/document/EditDocContent.svelte b/plugins/controlled-documents-resources/src/components/document/EditDocContent.svelte index 59731ba758..ec99525af6 100644 --- a/plugins/controlled-documents-resources/src/components/document/EditDocContent.svelte +++ b/plugins/controlled-documents-resources/src/components/document/EditDocContent.svelte @@ -28,10 +28,11 @@ highlightUpdateCommand, selectNode } from '@hcengineering/text-editor-resources' - import { EditBox, Label, Scroller } from '@hcengineering/ui' + import { Component, EditBox, Label, Scroller } from '@hcengineering/ui' import { getCollaborationUser } from '@hcengineering/view-resources' import { merge } from 'effector' import { createEventDispatcher, onDestroy, tick } from 'svelte' + import activity from '@hcengineering/activity' import plugin from '../../plugin' import { @@ -41,11 +42,13 @@ $controlledDocument as controlledDocument, $documentCommentHighlightedLocation as documentCommentHighlightedLocation, $documentComments as documentComments, + $documentState as documentState, documentCommentsDisplayRequested, documentCommentsLocationNavigateRequested, documentCommentsAddCanceled, $isEditable as isEditable } from '../../stores/editors/document' + import { isActivityDocumentState } from '../../utils' import DocumentPrintTitlePage from '../print/DocumentPrintTitlePage.svelte' import DocumentTitle from './DocumentTitle.svelte' @@ -276,6 +279,19 @@ return await createEmbedding(file) }} /> + {#if isActivityDocumentState($documentState)} +
+ +
+ {/if}
@@ -340,6 +356,10 @@ padding-bottom: 55vh; } + .activity-container { + padding-top: 2rem; + } + .watermark-container { position: absolute; z-index: 100; diff --git a/plugins/controlled-documents-resources/src/utils.ts b/plugins/controlled-documents-resources/src/utils.ts index 830ef54e85..c58a52dc1b 100644 --- a/plugins/controlled-documents-resources/src/utils.ts +++ b/plugins/controlled-documents-resources/src/utils.ts @@ -536,6 +536,14 @@ export const controlledDocumentStatesOrder = [ ControlledDocumentState.ToReview ] +const activityDocumentStates = [ + DocumentState.Draft, + ControlledDocumentState.InReview, + ControlledDocumentState.Reviewed, + ControlledDocumentState.InApproval, + ControlledDocumentState.Rejected +] + export interface LoginInfo { email: string password: string @@ -1233,3 +1241,10 @@ export async function extractValidationWorkflow ( return result } + +export function isActivityDocumentState (state: DocumentState | ControlledDocumentState | null): boolean { + if (state == null) { + return false + } + return activityDocumentStates.includes(state) +} From 0a9c3806bc6ca8bf64eebaa020318556cce73179 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Wed, 28 Jan 2026 23:47:32 +0700 Subject: [PATCH 08/67] fix: handle youtu.be links (#10458) Signed-off-by: Alexander Onnikov --- .../src/components/extension/embed/providers/youtube.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts b/plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts index 0004c9e685..6660d9cbbd 100644 --- a/plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts +++ b/plugins/text-editor-resources/src/components/extension/embed/providers/youtube.ts @@ -135,9 +135,9 @@ export const getEmbedUrlFromYoutubeUrl = (url: string, options: YoutubeEmbedUrlO const id = url.split('/').pop() if (id !== undefined) { - return + return `${getYoutubeEmbedUrl(nocookie)}${id}` } - return `${getYoutubeEmbedUrl(nocookie)}${id}` + return } const videoIdRegex = /(?:(v|list)=|shorts\/)([-\w]+)/gm From b27b511e7a46d98cabfb9c1f02bdca62ecb9f3c9 Mon Sep 17 00:00:00 2001 From: Dominic O'Carroll <99632940+domocarroll@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:44:14 +1000 Subject: [PATCH 09/67] fix: Calendar timezone day-shift bug (#7048) (#10459) Events scheduled for today were appearing in tomorrow's column due to raw millisecond arithmetic not handling timezone transitions properly. Changes: - Replace MILLISECONDS_IN_DAY * day arithmetic with Date.setDate() - Fix mutation bug where date parameter was being modified - Use proper calendar day arithmetic that handles DST/timezone Tested in Brisbane timezone (GMT+10) - events now display in correct column. Signed-off-by: Dominic O'Carroll <99632940+domocarroll@users.noreply.github.com> Co-authored-by: Dom Carroll Co-authored-by: Claude Opus 4.5 --- .../src/components/DayCalendar.svelte | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/calendar-resources/src/components/DayCalendar.svelte b/plugins/calendar-resources/src/components/DayCalendar.svelte index eb5bc05fd2..5f821af4ae 100644 --- a/plugins/calendar-resources/src/components/DayCalendar.svelte +++ b/plugins/calendar-resources/src/components/DayCalendar.svelte @@ -106,9 +106,12 @@ ): CalendarItem[] => { const result: CalendarItem[] = [] for (let day = 0; day < days; day++) { - const startDay = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(0, 0, 0, 0) - const startDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(startHour, 0, 0, 0) - const lastDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(endHour, 0, 0, 0) + // Use setDate() for proper day arithmetic that handles timezone transitions + const targetDay = new Date(date) + targetDay.setDate(targetDay.getDate() + day) + const startDay = new Date(targetDay).setHours(0, 0, 0, 0) + const startDate = new Date(targetDay).setHours(startHour, 0, 0, 0) + const lastDate = new Date(targetDay).setHours(endHour, 0, 0, 0) events.forEach((event) => { const eventStart = event.allDay ? event.date + offsetTZ : event.date const eventEnd = event.allDay ? event.dueDate + offsetTZ : event.dueDate @@ -125,8 +128,11 @@ } }) } - const sd = date.setHours(0, 0, 0, 0) - const ld = new Date(MILLISECONDS_IN_DAY * (days - 1) + date.getTime()).setHours(23, 59, 59, 999) + // Fix: Don't mutate the date parameter, use setDate() for proper day arithmetic + const sd = new Date(date).setHours(0, 0, 0, 0) + const lastDayDate = new Date(date) + lastDayDate.setDate(lastDayDate.getDate() + days - 1) + const ld = new Date(lastDayDate).setHours(23, 59, 59, 999) events .filter((ev) => ev.allDay) .sort((a, b) => b.dueDate - b.date - (a.dueDate - a.date)) From 111f9e14d79dd33453225a5d26eeca43e63ee695 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Thu, 29 Jan 2026 14:44:13 +0700 Subject: [PATCH 10/67] Add data converter plugin (#10460) * Add data converter plugin Signed-off-by: Artem Savchenko * Clean up Signed-off-by: Artem Savchenko * Apply templates Signed-off-by: Artem Savchenko * Fix test Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- common/config/rush/pnpm-lock.yaml | 234 ++++ desktop/package.json | 2 + desktop/src/ui/platform.ts | 2 + dev/prod/package.json | 2 + dev/prod/src/platform.ts | 2 + models/all/package.json | 1 + models/all/src/index.ts | 2 + models/card/package.json | 1 + models/card/src/index.ts | 5 + models/controlled-documents/package.json | 1 + models/controlled-documents/src/index.ts | 5 + models/converter/.eslintrc.js | 7 + models/converter/.npmignore | 4 + models/converter/config/rig.json | 5 + models/converter/jest.config.js | 7 + models/converter/package.json | 43 + models/converter/src/index.ts | 30 + models/converter/tsconfig.json | 12 + models/tracker/package.json | 1 + models/tracker/src/index.ts | 5 + plugins/card-resources/package.json | 2 + .../card-resources/src/cardTableFormatter.ts | 15 +- plugins/card-resources/src/index.ts | 6 +- plugins/card-resources/src/plugin.ts | 4 +- .../package.json | 2 + .../src/docTableFormatter.ts | 7 +- .../src/index.ts | 3 +- plugins/controlled-documents/package.json | 1 + plugins/controlled-documents/src/plugin.ts | 3 +- plugins/converter-resources/.eslintrc.js | 7 + plugins/converter-resources/config/rig.json | 4 + plugins/converter-resources/jest.config.js | 7 + plugins/converter-resources/package.json | 50 + .../__tests__/MarkdownTableConverter.test.ts | 67 ++ .../src/__tests__/copyAsMarkdownTable.test.ts | 220 ++++ .../src/__tests__/formatter.utils.test.ts | 78 ++ .../src/__tests__/markdown.escape.test.ts | 55 + .../src/__tests__/model.tableModel.test.ts | 96 ++ plugins/converter-resources/src/actionImpl.ts | 26 + plugins/converter-resources/src/data/index.ts | 18 + .../src/data/metadataBuilder.ts | 99 ++ .../src/data/personLoader.ts | 50 + .../src/data/relationshipBuilder.ts | 121 ++ .../src/formatter/index.ts | 24 + .../src/formatter/registry.ts | 71 ++ .../src/formatter/utils.ts | 123 ++ .../src/formatter/valueFormatter.ts | 258 ++++ plugins/converter-resources/src/index.ts | 54 + .../src/markdown/copyActions.ts | 128 ++ .../src/markdown/escape.ts | 33 + .../converter-resources/src/markdown/index.ts | 23 + .../converter-resources/src/markdown/link.ts | 42 + .../src/markdown/tableBuilder.ts | 311 +++++ .../src/model/headerGenerator.ts | 74 ++ .../converter-resources/src/model/index.ts | 18 + .../src/model/tableModel.ts | 112 ++ .../src/model/viewletLoader.ts | 62 + plugins/converter-resources/src/plugin.ts | 19 + plugins/converter-resources/src/types.ts | 138 +++ plugins/converter-resources/tsconfig.json | 12 + plugins/converter/.eslintrc.js | 7 + plugins/converter/config/rig.json | 4 + plugins/converter/jest.config.js | 7 + plugins/converter/package.json | 44 + plugins/converter/src/index.ts | 22 + plugins/converter/src/plugin.ts | 50 + plugins/converter/src/types.ts | 95 ++ plugins/converter/tsconfig.json | 12 + plugins/text-editor-resources/package.json | 1 + .../extension/table/refreshTable.ts | 11 +- plugins/tracker-resources/package.json | 2 + plugins/tracker-resources/src/index.ts | 5 +- .../src/issueTableFormatter.ts | 6 +- plugins/tracker-resources/src/plugin.ts | 4 +- plugins/view-resources/package.json | 1 + .../src/__tests__/copyAsMarkdownTable.test.ts | 446 ------- plugins/view-resources/src/actionImpl.ts | 5 +- .../src/components/RelationshipTable.svelte | 5 +- .../view-resources/src/copyAsMarkdownTable.ts | 1072 ----------------- plugins/view-resources/src/index.ts | 20 +- .../view-resources/src/markdownTableUtils.ts | 343 ------ plugins/view/src/index.ts | 8 +- rush.json | 15 + 83 files changed, 3078 insertions(+), 1921 deletions(-) create mode 100644 models/converter/.eslintrc.js create mode 100644 models/converter/.npmignore create mode 100644 models/converter/config/rig.json create mode 100644 models/converter/jest.config.js create mode 100644 models/converter/package.json create mode 100644 models/converter/src/index.ts create mode 100644 models/converter/tsconfig.json create mode 100644 plugins/converter-resources/.eslintrc.js create mode 100644 plugins/converter-resources/config/rig.json create mode 100644 plugins/converter-resources/jest.config.js create mode 100644 plugins/converter-resources/package.json create mode 100644 plugins/converter-resources/src/__tests__/MarkdownTableConverter.test.ts create mode 100644 plugins/converter-resources/src/__tests__/copyAsMarkdownTable.test.ts create mode 100644 plugins/converter-resources/src/__tests__/formatter.utils.test.ts create mode 100644 plugins/converter-resources/src/__tests__/markdown.escape.test.ts create mode 100644 plugins/converter-resources/src/__tests__/model.tableModel.test.ts create mode 100644 plugins/converter-resources/src/actionImpl.ts create mode 100644 plugins/converter-resources/src/data/index.ts create mode 100644 plugins/converter-resources/src/data/metadataBuilder.ts create mode 100644 plugins/converter-resources/src/data/personLoader.ts create mode 100644 plugins/converter-resources/src/data/relationshipBuilder.ts create mode 100644 plugins/converter-resources/src/formatter/index.ts create mode 100644 plugins/converter-resources/src/formatter/registry.ts create mode 100644 plugins/converter-resources/src/formatter/utils.ts create mode 100644 plugins/converter-resources/src/formatter/valueFormatter.ts create mode 100644 plugins/converter-resources/src/index.ts create mode 100644 plugins/converter-resources/src/markdown/copyActions.ts create mode 100644 plugins/converter-resources/src/markdown/escape.ts create mode 100644 plugins/converter-resources/src/markdown/index.ts create mode 100644 plugins/converter-resources/src/markdown/link.ts create mode 100644 plugins/converter-resources/src/markdown/tableBuilder.ts create mode 100644 plugins/converter-resources/src/model/headerGenerator.ts create mode 100644 plugins/converter-resources/src/model/index.ts create mode 100644 plugins/converter-resources/src/model/tableModel.ts create mode 100644 plugins/converter-resources/src/model/viewletLoader.ts create mode 100644 plugins/converter-resources/src/plugin.ts create mode 100644 plugins/converter-resources/src/types.ts create mode 100644 plugins/converter-resources/tsconfig.json create mode 100644 plugins/converter/.eslintrc.js create mode 100644 plugins/converter/config/rig.json create mode 100644 plugins/converter/jest.config.js create mode 100644 plugins/converter/package.json create mode 100644 plugins/converter/src/index.ts create mode 100644 plugins/converter/src/plugin.ts create mode 100644 plugins/converter/src/types.ts create mode 100644 plugins/converter/tsconfig.json delete mode 100644 plugins/view-resources/src/__tests__/copyAsMarkdownTable.test.ts delete mode 100644 plugins/view-resources/src/copyAsMarkdownTable.ts delete mode 100644 plugins/view-resources/src/markdownTableUtils.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 52193c56b7..bfa42de239 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -165,6 +165,12 @@ importers: '@hcengineering/controlled-documents-resources': specifier: workspace:^0.7.0 version: link:../plugins/controlled-documents-resources + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../plugins/converter + '@hcengineering/converter-resources': + specifier: workspace:^0.7.0 + version: link:../plugins/converter-resources '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../foundations/core/packages/core @@ -1216,6 +1222,12 @@ importers: '@hcengineering/controlled-documents-resources': specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents-resources + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../../plugins/converter + '@hcengineering/converter-resources': + specifier: workspace:^0.7.0 + version: link:../../plugins/converter-resources '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -5962,6 +5974,9 @@ importers: '@hcengineering/model-controlled-documents': specifier: workspace:^0.7.0 version: link:../controlled-documents + '@hcengineering/model-converter': + specifier: workspace:^0.7.0 + version: link:../converter '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core @@ -6824,6 +6839,9 @@ importers: '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../../plugins/converter '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -7345,6 +7363,9 @@ importers: '@hcengineering/controlled-documents-resources': specifier: workspace:^0.7.0 version: link:../../plugins/controlled-documents-resources + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../../plugins/converter '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -7467,6 +7488,67 @@ importers: specifier: ^5.9.3 version: 5.9.3 + ../../models/converter: + dependencies: + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../../plugins/converter + '@hcengineering/core': + specifier: workspace:^0.7.24 + version: link:../../foundations/core/packages/core + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/platform + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + ../../models/core: dependencies: '@hcengineering/collaboration': @@ -13252,6 +13334,9 @@ importers: '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../../plugins/contact + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../../plugins/converter '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -17359,6 +17444,12 @@ importers: '@hcengineering/contact-resources': specifier: workspace:^0.7.0 version: link:../contact-resources + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../converter + '@hcengineering/converter-resources': + specifier: workspace:^0.7.0 + version: link:../converter-resources '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -18581,6 +18672,9 @@ importers: '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../converter '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -18739,6 +18833,12 @@ importers: '@hcengineering/controlled-documents': specifier: workspace:^0.7.0 version: link:../controlled-documents + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../converter + '@hcengineering/converter-resources': + specifier: workspace:^0.7.0 + version: link:../converter-resources '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -18888,6 +18988,128 @@ importers: specifier: ^5.9.3 version: 5.9.3 + ../../plugins/converter: + dependencies: + '@hcengineering/core': + specifier: workspace:^0.7.24 + version: link:../../foundations/core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/platform + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../view + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../plugins/converter-resources: + dependencies: + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../contact + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../converter + '@hcengineering/core': + specifier: workspace:^0.7.24 + version: link:../../foundations/core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.19 + version: link:../../foundations/core/packages/platform + '@hcengineering/presentation': + specifier: workspace:^0.7.0 + version: link:../../packages/presentation + '@hcengineering/theme': + specifier: workspace:^0.7.0 + version: link:../../packages/theme + '@hcengineering/ui': + specifier: workspace:^0.7.0 + version: link:../../packages/ui + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../view + '@hcengineering/view-resources': + specifier: workspace:^0.7.0 + version: link:../view-resources + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.19 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + ../../plugins/desktop-downloads: dependencies: '@hcengineering/core': @@ -27991,6 +28213,9 @@ importers: '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../converter '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -28666,6 +28891,12 @@ importers: '@hcengineering/contact-resources': specifier: workspace:^0.7.0 version: link:../contact-resources + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../converter + '@hcengineering/converter-resources': + specifier: workspace:^0.7.0 + version: link:../converter-resources '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core @@ -29377,6 +29608,9 @@ importers: '@hcengineering/contact': specifier: workspace:^0.7.0 version: link:../contact + '@hcengineering/converter': + specifier: workspace:^0.7.0 + version: link:../converter '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core diff --git a/desktop/package.json b/desktop/package.json index 2a89a1e6ea..7fe030affb 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -88,6 +88,8 @@ "@hcengineering/view": "workspace:^0.7.0", "@hcengineering/view-assets": "workspace:^0.7.0", "@hcengineering/view-resources": "workspace:^0.7.0", + "@hcengineering/converter": "workspace:^0.7.0", + "@hcengineering/converter-resources": "workspace:^0.7.0", "@hcengineering/contact": "workspace:^0.7.0", "@hcengineering/contact-resources": "workspace:^0.7.0", "@hcengineering/task": "workspace:^0.7.0", diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index 62c74178cb..d46c8f5b12 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -33,6 +33,7 @@ import { cardId } from '@hcengineering/card' import { chunterId } from '@hcengineering/chunter' import client, { clientId } from '@hcengineering/client' import contactPlugin, { contactId } from '@hcengineering/contact' +import { converterId } from '@hcengineering/converter' import { documentsId } from '@hcengineering/controlled-documents' import { desktopPreferencesId } from '@hcengineering/desktop-preferences' import { desktopDownloadsId } from '@hcengineering/desktop-downloads' @@ -435,6 +436,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise await import('@hcengineering/onboard-resources')) addLocation(workbenchId, async () => await import('@hcengineering/workbench-resources')) addLocation(viewId, async () => await import('@hcengineering/view-resources')) + addLocation(converterId, async () => await import('@hcengineering/converter-resources')) addLocation(taskId, async () => await import('@hcengineering/task-resources')) addLocation(contactId, async () => await import('@hcengineering/contact-resources')) addLocation(chunterId, async () => await import('@hcengineering/chunter-resources')) diff --git a/dev/prod/package.json b/dev/prod/package.json index 5e83bdde1b..be3d41825c 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -265,6 +265,8 @@ "@hcengineering/view": "workspace:^0.7.0", "@hcengineering/view-assets": "workspace:^0.7.0", "@hcengineering/view-resources": "workspace:^0.7.0", + "@hcengineering/converter": "workspace:^0.7.0", + "@hcengineering/converter-resources": "workspace:^0.7.0", "@hcengineering/workbench": "workspace:^0.7.0", "@hcengineering/workbench-assets": "workspace:^0.7.0", "@hcengineering/workbench-resources": "workspace:^0.7.0", diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index a1c6f7f470..7446fc8c3b 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -25,6 +25,7 @@ import { cardId } from '@hcengineering/card' import { chunterId } from '@hcengineering/chunter' import client, { clientId } from '@hcengineering/client' import contactPlugin, { contactId } from '@hcengineering/contact' +import { converterId } from '@hcengineering/converter' import { documentsId } from '@hcengineering/controlled-documents' import { desktopPreferencesId } from '@hcengineering/desktop-preferences' import { diffviewId } from '@hcengineering/diffview' @@ -570,6 +571,7 @@ export async function configurePlatform() { async () => await import(/* webpackChunkName: "workbench" */ '@hcengineering/workbench-resources') ) addLocation(viewId, async () => await import(/* webpackChunkName: "view" */ '@hcengineering/view-resources')) + addLocation(converterId, async () => await import(/* webpackChunkName: "converter" */ '@hcengineering/converter-resources')) addLocation(taskId, async () => await import(/* webpackChunkName: "task" */ '@hcengineering/task-resources')) addLocation(contactId, async () => await import(/* webpackChunkName: "contact" */ '@hcengineering/contact-resources')) addLocation(chunterId, async () => await import(/* webpackChunkName: "chunter" */ '@hcengineering/chunter-resources')) diff --git a/models/all/package.json b/models/all/package.json index 5fd9d1b953..e5c92c789a 100644 --- a/models/all/package.json +++ b/models/all/package.json @@ -42,6 +42,7 @@ "@hcengineering/model": "workspace:^0.7.17", "@hcengineering/model-core": "workspace:^0.7.0", "@hcengineering/model-view": "workspace:^0.7.0", + "@hcengineering/model-converter": "workspace:^0.7.0", "@hcengineering/model-workbench": "workspace:^0.7.0", "@hcengineering/model-contact": "workspace:^0.7.0", "@hcengineering/model-task": "workspace:^0.7.0", diff --git a/models/all/src/index.ts b/models/all/src/index.ts index 7b8a005635..47cb2ed9fa 100644 --- a/models/all/src/index.ts +++ b/models/all/src/index.ts @@ -82,6 +82,7 @@ import tracker, { trackerId, createModel as trackerModel } from '@hcengineering/ import { uploaderId, createModel as uploaderModel } from '@hcengineering/model-uploader' import view, { viewId, createModel as viewModel } from '@hcengineering/model-view' import workbench, { workbenchId, createModel as workbenchModel } from '@hcengineering/model-workbench' +import { converterId, createModel as converterModel } from '@hcengineering/model-converter' import document, { documentId, createModel as documentModel } from '@hcengineering/model-document' import { serverDocumentId, createModel as serverDocumentModel } from '@hcengineering/model-server-document' @@ -524,6 +525,7 @@ export default function buildModel (): Builder { classFilter: defaultFilter } ], + [converterModel, converterId], [serverCoreModel, serverCoreId], [serverAttachmentModel, serverAttachmentId], diff --git a/models/card/package.json b/models/card/package.json index e706197bd4..833ff66d01 100644 --- a/models/card/package.json +++ b/models/card/package.json @@ -37,6 +37,7 @@ "@hcengineering/activity": "workspace:^0.7.0", "@hcengineering/contact": "workspace:^0.7.0", "@hcengineering/communication": "workspace:^0.7.0", + "@hcengineering/converter": "workspace:^0.7.0", "@hcengineering/core": "workspace:^0.7.24", "@hcengineering/model": "workspace:^0.7.17", "@hcengineering/chunter": "workspace:^0.7.0", diff --git a/models/card/src/index.ts b/models/card/src/index.ts index 42241d89f6..e156dbeb84 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -75,6 +75,7 @@ import presentation from '@hcengineering/model-presentation' import setting from '@hcengineering/model-setting' import view, { type Viewlet } from '@hcengineering/model-view' import workbench, { WidgetType } from '@hcengineering/model-workbench' +import converter from '@hcengineering/converter' import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform' import time, { type ToDo } from '@hcengineering/time' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' @@ -327,6 +328,10 @@ export function createSystemType ( } }) + builder.mixin(card.class.Card, core.class.Class, converter.mixin.MarkdownValueFormatter, { + formatter: card.function.FormatCardMarkdownValue + }) + builder.createDoc(view.class.Viewlet, core.space.Model, { attachTo: type, descriptor: view.viewlet.Table, diff --git a/models/controlled-documents/package.json b/models/controlled-documents/package.json index 2c4e04f9f9..58dfbfa824 100644 --- a/models/controlled-documents/package.json +++ b/models/controlled-documents/package.json @@ -49,6 +49,7 @@ "@hcengineering/model": "workspace:^0.7.17", "@hcengineering/setting": "workspace:^0.7.0", "@hcengineering/core": "workspace:^0.7.24", + "@hcengineering/converter": "workspace:^0.7.0", "@hcengineering/ui": "workspace:^0.7.0", "@hcengineering/platform": "workspace:^0.7.19", "@hcengineering/view": "workspace:^0.7.0", diff --git a/models/controlled-documents/src/index.ts b/models/controlled-documents/src/index.ts index 685064f13c..d57b95d36a 100644 --- a/models/controlled-documents/src/index.ts +++ b/models/controlled-documents/src/index.ts @@ -36,6 +36,7 @@ import request from '@hcengineering/model-request' import tracker from '@hcengineering/model-tracker' import view, { classPresenter, createAction } from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' +import converter from '@hcengineering/converter' import notification from '@hcengineering/notification' import contacts from '@hcengineering/model-contact' import setting from '@hcengineering/setting' @@ -111,6 +112,10 @@ export function createModel (builder: Builder): void { titleProvider: documents.function.ControlledDocumentTitleProvider }) + builder.mixin(documents.class.Document, core.class.Class, converter.mixin.MarkdownValueFormatter, { + formatter: documents.function.FormatDocumentMarkdownValue + }) + builder.mixin(documents.class.DocumentApprovalRequest, core.class.Class, view.mixin.ObjectPresenter, { presenter: documents.component.DocumentApprovalRequestPresenter }) diff --git a/models/converter/.eslintrc.js b/models/converter/.eslintrc.js new file mode 100644 index 0000000000..c1cf82cba0 --- /dev/null +++ b/models/converter/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/models/converter/.npmignore b/models/converter/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/models/converter/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/models/converter/config/rig.json b/models/converter/config/rig.json new file mode 100644 index 0000000000..2f6be36605 --- /dev/null +++ b/models/converter/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "model" +} diff --git a/models/converter/jest.config.js b/models/converter/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/models/converter/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/models/converter/package.json b/models/converter/package.json new file mode 100644 index 0000000000..9824ea82e7 --- /dev/null +++ b/models/converter/package.json @@ -0,0 +1,43 @@ +{ + "name": "@hcengineering/model-converter", + "version": "0.7.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Copyright © Hardcore Engineering Inc.", + "template": "@hcengineering/model-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:format": "format src", + "_phase:validate": "compile validate", + "_phase:test": "jest --passWithNoTests --silent --forceExit", + "test": "jest --passWithNoTests --silent --forceExit" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "@types/node": "^22.18.1", + "jest": "^29.7.0", + "@types/jest": "^29.5.5", + "ts-jest": "^29.1.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.24", + "@hcengineering/model-core": "workspace:^0.7.0", + "@hcengineering/model": "workspace:^0.7.17", + "@hcengineering/platform": "workspace:^0.7.19", + "@hcengineering/converter": "workspace:^0.7.0" + } +} diff --git a/models/converter/src/index.ts b/models/converter/src/index.ts new file mode 100644 index 0000000000..59eaa1ad4b --- /dev/null +++ b/models/converter/src/index.ts @@ -0,0 +1,30 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Builder, Mixin } from '@hcengineering/model' +import core, { TClass } from '@hcengineering/model-core' +import converter, { type MarkdownValueFormatter, type ValueFormatter } from '@hcengineering/converter' +import type { Resource } from '@hcengineering/platform' + +export { converterId } from '@hcengineering/converter' + +@Mixin(converter.mixin.MarkdownValueFormatter, core.class.Class) +export class TMarkdownValueFormatter extends TClass implements MarkdownValueFormatter { + formatter!: Resource +} + +export function createModel (builder: Builder): void { + builder.createModel(TMarkdownValueFormatter) +} diff --git a/models/converter/tsconfig.json b/models/converter/tsconfig.json new file mode 100644 index 0000000000..367a8578c9 --- /dev/null +++ b/models/converter/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/models/tracker/package.json b/models/tracker/package.json index a7cf3083ac..0bdad7a260 100644 --- a/models/tracker/package.json +++ b/models/tracker/package.json @@ -37,6 +37,7 @@ "@hcengineering/activity": "workspace:^0.7.0", "@hcengineering/chunter": "workspace:^0.7.0", "@hcengineering/contact": "workspace:^0.7.0", + "@hcengineering/converter": "workspace:^0.7.0", "@hcengineering/core": "workspace:^0.7.24", "@hcengineering/model": "workspace:^0.7.17", "@hcengineering/model-attachment": "workspace:^0.7.0", diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 04395afc0e..26558b5c57 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -23,6 +23,7 @@ import presentation from '@hcengineering/model-presentation' import task from '@hcengineering/model-task' import view from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' +import converter from '@hcengineering/converter' import notification from '@hcengineering/notification' import setting from '@hcengineering/setting' import pluginState, { type Issue, trackerId } from '@hcengineering/tracker' @@ -82,6 +83,10 @@ export const classicIssueTaskStatuses: TaskStatusFactory[] = [ ] function defineSortAndGrouping (builder: Builder): void { + builder.mixin(tracker.class.Issue, core.class.Class, converter.mixin.MarkdownValueFormatter, { + formatter: tracker.function.FormatIssueMarkdownValue + }) + builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.SortFuncs, { func: tracker.function.IssueStatusSort }) diff --git a/plugins/card-resources/package.json b/plugins/card-resources/package.json index 5076796d7b..1edaa94b19 100644 --- a/plugins/card-resources/package.json +++ b/plugins/card-resources/package.json @@ -65,6 +65,8 @@ "@hcengineering/text-markdown": "workspace:^0.7.20", "@hcengineering/workbench-resources": "workspace:^0.7.0", "@hcengineering/view-resources": "workspace:^0.7.0", + "@hcengineering/converter": "workspace:^0.7.0", + "@hcengineering/converter-resources": "workspace:^0.7.0", "@hcengineering/notification": "workspace:^0.7.0", "@hcengineering/contact": "workspace:^0.7.0", "@hcengineering/workbench": "workspace:^0.7.0", diff --git a/plugins/card-resources/src/cardTableFormatter.ts b/plugins/card-resources/src/cardTableFormatter.ts index 7145afad46..ac999dcdbc 100644 --- a/plugins/card-resources/src/cardTableFormatter.ts +++ b/plugins/card-resources/src/cardTableFormatter.ts @@ -18,7 +18,7 @@ import { translate, type IntlString } from '@hcengineering/platform' import cardPlugin, { type CardSpace } from '@hcengineering/card' import { type AttributeModel } from '@hcengineering/view' import { getClient } from '@hcengineering/presentation' -import { registerValueFormatterForClass, isIntlString } from '@hcengineering/view-resources' +import { isIntlString } from '@hcengineering/converter-resources' /** * Cache for MasterTag ID -> label mappings to reduce database calls @@ -95,7 +95,7 @@ async function loadCardSpaceName (spaceRef: Ref): Promise { * Value formatter for card fields * Handles special cases for type (MasterTag) and space (CardSpace) fields */ -async function formatCardValue ( +export async function formatCardValue ( attr: AttributeModel, card: Doc, hierarchy: Hierarchy, @@ -120,13 +120,11 @@ async function formatCardValue ( if (lookupClass !== undefined && lookupClass !== null) { const classObj = lookupClass as Record const label: unknown = classObj.label - if (label !== undefined) { - if (typeof label === 'string' && isIntlString(label)) { + if (typeof label === 'string') { + if (isIntlString(label)) { return await translate(label as unknown as IntlString, {}, language) } - if (typeof label === 'string') { - return label - } + return label } } // If not in lookup, get from hierarchy @@ -161,6 +159,3 @@ async function formatCardValue ( return undefined } - -// Register the formatter for Card class -registerValueFormatterForClass(cardPlugin.class.Card, formatCardValue) diff --git a/plugins/card-resources/src/index.ts b/plugins/card-resources/src/index.ts index 30e30f9163..d2c77a033f 100644 --- a/plugins/card-resources/src/index.ts +++ b/plugins/card-resources/src/index.ts @@ -37,6 +37,7 @@ import { cardFactory, duplicateCard } from './utils' +import { formatCardValue } from './cardTableFormatter' import ManageMasterTagsContent from './components/settings/ManageMasterTagsContent.svelte' import ManageMasterTagsTools from './components/settings/ManageMasterTagsTools.svelte' import ManageMasterTags from './components/settings/ManageMasterTags.svelte' @@ -80,8 +81,6 @@ import CardWidgetTab from './components/CardWidgetTab.svelte' import CardIcon from './components/CardIcon.svelte' import CardFeedView from './components/CardFeedView.svelte' -import './cardTableFormatter' - export { default as CardSelector } from './components/CardSelector.svelte' export { default as CardIcon } from './components/CardIcon.svelte' export { default as Navigator } from './components/navigator-next/Navigator.svelte' @@ -169,6 +168,7 @@ export default async (): Promise => ({ CheckCommunicationMessagesSectionVisibility: checkCommunicationMessagesSectionVisibility, GetSpaceAccessPublicLink: getSpaceAccessPublicLink, CanGetSpaceAccessPublicLink: canGetSpaceAccessPublicLink, - CardFactory: cardFactory + CardFactory: cardFactory, + FormatCardMarkdownValue: formatCardValue } }) diff --git a/plugins/card-resources/src/plugin.ts b/plugins/card-resources/src/plugin.ts index 39e2ebb2e5..bcc7d4c606 100644 --- a/plugins/card-resources/src/plugin.ts +++ b/plugins/card-resources/src/plugin.ts @@ -19,6 +19,7 @@ import { type IntlString, mergeIds, type Resource } from '@hcengineering/platfor import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/presentation' import { type AnyComponent } from '@hcengineering/ui/src/types' import type { ViewletDescriptor, Viewlet } from '@hcengineering/view' +import type { ValueFormatter } from '@hcengineering/converter' export default mergeIds(cardId, card, { component: { @@ -57,7 +58,8 @@ export default mergeIds(cardId, card, { CreateRolePopup: '' as AnyComponent }, function: { - CardFactory: '' as Resource<(props?: Record) => Promise | undefined>> + CardFactory: '' as Resource<(props?: Record) => Promise | undefined>>, + FormatCardMarkdownValue: '' as Resource }, permission: { CreateCard: '' as Ref, diff --git a/plugins/controlled-documents-resources/package.json b/plugins/controlled-documents-resources/package.json index 69f5795c5d..9f1a5ce4e9 100644 --- a/plugins/controlled-documents-resources/package.json +++ b/plugins/controlled-documents-resources/package.json @@ -48,6 +48,8 @@ "@hcengineering/notification-resources": "workspace:^0.7.0", "@hcengineering/panel": "workspace:^0.7.0", "@hcengineering/view-resources": "workspace:^0.7.0", + "@hcengineering/converter": "workspace:^0.7.0", + "@hcengineering/converter-resources": "workspace:^0.7.0", "@hcengineering/attachment": "workspace:^0.7.0", "@hcengineering/notification": "workspace:^0.7.0", "@hcengineering/account-client": "workspace:^0.7.21", diff --git a/plugins/controlled-documents-resources/src/docTableFormatter.ts b/plugins/controlled-documents-resources/src/docTableFormatter.ts index b66660af82..19bc1feefe 100644 --- a/plugins/controlled-documents-resources/src/docTableFormatter.ts +++ b/plugins/controlled-documents-resources/src/docTableFormatter.ts @@ -18,7 +18,7 @@ import { translate, type IntlString } from '@hcengineering/platform' import documentsPlugin from '@hcengineering/controlled-documents' import { type AttributeModel } from '@hcengineering/view' import { getClient } from '@hcengineering/presentation' -import { registerValueFormatterForClass, isIntlString } from '@hcengineering/view-resources' +import { isIntlString } from '@hcengineering/converter-resources' /** * Format version number from major and minor @@ -78,7 +78,7 @@ async function loadSpaceName (spaceRef: Ref): Promise { * Value formatter for controlled document fields * Handles special cases where empty keys use custom presenters */ -async function formatControlledDocumentValue ( +export async function formatControlledDocumentValue ( attr: AttributeModel, card: Doc, hierarchy: Hierarchy, @@ -255,6 +255,3 @@ async function formatControlledDocumentValue ( return undefined } - -// Register the formatter for Document class and all derived classes -registerValueFormatterForClass(documentsPlugin.class.Document, formatControlledDocumentValue) diff --git a/plugins/controlled-documents-resources/src/index.ts b/plugins/controlled-documents-resources/src/index.ts index 97a456c5a9..b1953e3ac5 100644 --- a/plugins/controlled-documents-resources/src/index.ts +++ b/plugins/controlled-documents-resources/src/index.ts @@ -117,7 +117,7 @@ import { getDocumentMetaTitle } from './utils' -import './docTableFormatter' +import { formatControlledDocumentValue } from './docTableFormatter' export { DocumentStatusTag, DocumentTitle, DocumentVersionPresenter, StatePresenter } @@ -444,6 +444,7 @@ export default async (): Promise => ({ ProjectDocumentReferenceObjectProvider: projectDocumentReferenceObjectProvider, ControlledDocumentTitleProvider: getControlledDocumentTitle, DocumentMetaTitleProvider: getDocumentMetaTitle, + FormatDocumentMarkdownValue: formatControlledDocumentValue, Comment: comment, IsCommentVisible: isCommentVisible }, diff --git a/plugins/controlled-documents/package.json b/plugins/controlled-documents/package.json index 7a93b379d4..274361f301 100644 --- a/plugins/controlled-documents/package.json +++ b/plugins/controlled-documents/package.json @@ -40,6 +40,7 @@ "@hcengineering/platform": "workspace:^0.7.19", "@hcengineering/ui": "workspace:^0.7.0", "@hcengineering/core": "workspace:^0.7.24", + "@hcengineering/converter": "workspace:^0.7.0", "@hcengineering/view": "workspace:^0.7.0", "@hcengineering/contact": "workspace:^0.7.0", "@hcengineering/notification": "workspace:^0.7.0", diff --git a/plugins/controlled-documents/src/plugin.ts b/plugins/controlled-documents/src/plugin.ts index a542f656ce..1ecf43d074 100644 --- a/plugins/controlled-documents/src/plugin.ts +++ b/plugins/controlled-documents/src/plugin.ts @@ -134,7 +134,8 @@ export const documentsPlugin = plugin(documentsId, { }, function: { CanChangeDocumentOwner: '' as Resource<(doc?: Doc | Doc[]) => Promise>, - CanDeleteDocumentCategory: '' as Resource<(doc?: Doc | Doc[]) => Promise> + CanDeleteDocumentCategory: '' as Resource<(doc?: Doc | Doc[]) => Promise>, + FormatDocumentMarkdownValue: '' as Resource }, icon: { Approvals: '' as Asset, diff --git a/plugins/converter-resources/.eslintrc.js b/plugins/converter-resources/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/plugins/converter-resources/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/converter-resources/config/rig.json b/plugins/converter-resources/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/plugins/converter-resources/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/plugins/converter-resources/jest.config.js b/plugins/converter-resources/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/converter-resources/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/converter-resources/package.json b/plugins/converter-resources/package.json new file mode 100644 index 0000000000..73b221692f --- /dev/null +++ b/plugins/converter-resources/package.json @@ -0,0 +1,50 @@ +{ + "name": "@hcengineering/converter-resources", + "version": "0.7.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Copyright © Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "format": "format src", + "build:watch": "compile", + "_phase:build": "compile transpile src", + "_phase:format": "format src", + "_phase:validate": "compile validate", + "_phase:test": "jest --passWithNoTests --silent", + "test": "jest --passWithNoTests --silent" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "eslint": "^8.54.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.19", + "@hcengineering/core": "workspace:^0.7.24", + "@hcengineering/view": "workspace:^0.7.0", + "@hcengineering/converter": "workspace:^0.7.0", + "@hcengineering/presentation": "workspace:^0.7.0", + "@hcengineering/ui": "workspace:^0.7.0", + "@hcengineering/theme": "workspace:^0.7.0", + "@hcengineering/contact": "workspace:^0.7.0", + "@hcengineering/view-resources": "workspace:^0.7.0" + } +} diff --git a/plugins/converter-resources/src/__tests__/MarkdownTableConverter.test.ts b/plugins/converter-resources/src/__tests__/MarkdownTableConverter.test.ts new file mode 100644 index 0000000000..dffc6c2a6c --- /dev/null +++ b/plugins/converter-resources/src/__tests__/MarkdownTableConverter.test.ts @@ -0,0 +1,67 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MarkdownTableConverter } from '../types' + +describe('MarkdownTableConverter', () => { + const converter = new MarkdownTableConverter() + + it('has format "markdown"', () => { + expect(converter.format).toBe('markdown') + }) + + describe('buildTable', () => { + it('builds markdown table from headers and rows', () => { + const headers = ['Name', 'Status'] + const rows = [ + ['Task A', 'Done'], + ['Task B', 'In Progress'] + ] + const result = converter.buildTable(headers, rows) + expect(result).toBe( + '| Name | Status |\n' + '| --- | --- |\n' + '| Task A | Done |\n' + '| Task B | In Progress |\n' + ) + }) + + it('handles empty rows', () => { + const headers = ['Col1'] + const result = converter.buildTable(headers, []) + expect(result).toBe('| Col1 |\n| --- |\n') + }) + }) + + describe('escapeValue', () => { + it('escapes pipe in text', () => { + expect(converter.escapeValue('a|b')).toBe('a\\|b') + }) + + it('escapes brackets in text', () => { + expect(converter.escapeValue('[link]')).toBe('\\[link\\]') + }) + }) + + describe('createLink', () => { + it('produces markdown link syntax', () => { + const result = converter.createLink('https://example.com', 'Example') + expect(result).toBe('[Example](https://example.com)') + }) + + it('escapes link text and url', () => { + const result = converter.createLink('https://example.com/path)', 'Text|here') + expect(result).toContain('\\|') + expect(result).toContain('\\)') + }) + }) +}) diff --git a/plugins/converter-resources/src/__tests__/copyAsMarkdownTable.test.ts b/plugins/converter-resources/src/__tests__/copyAsMarkdownTable.test.ts new file mode 100644 index 0000000000..d962978a5d --- /dev/null +++ b/plugins/converter-resources/src/__tests__/copyAsMarkdownTable.test.ts @@ -0,0 +1,220 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { buildMarkdownTableFromDocs, copyAsMarkdownTable, isIntlString } from '../index' +import type { Doc, Class, Ref } from '@hcengineering/core' +import type { CopyAsMarkdownTableProps } from '../types' +import { getClient } from '@hcengineering/presentation' +import { getCurrentLanguage } from '@hcengineering/theme' +import { copyMarkdown, buildModel, getObjectLinkFragment } from '@hcengineering/view-resources' +import core from '@hcengineering/core' +import { type IntlString } from '@hcengineering/platform' + +jest.mock('@hcengineering/platform', () => { + const actual = jest.requireActual('@hcengineering/platform') + return { + ...actual, + translate: jest.fn(async (str: unknown) => `translated:${String(str)}`), + getMetadata: jest.fn((key: unknown) => { + if (key != null && String(key).includes('FrontUrl')) { + return 'http://test.local:8080' + } + return undefined + }) + } +}) + +jest.mock('@hcengineering/presentation', () => ({ + getClient: jest.fn() +})) + +jest.mock('@hcengineering/theme', () => ({ + getCurrentLanguage: jest.fn(() => 'en') +})) + +jest.mock('@hcengineering/view-resources', () => ({ + copyMarkdown: jest.fn(), + buildModel: jest.fn(), + buildConfigLookup: jest.fn(() => ({})), + getObjectLinkFragment: jest.fn() +})) + +jest.mock('@hcengineering/ui', () => ({ + addNotification: jest.fn(), + NotificationSeverity: { Success: 'success', Error: 'error' }, + locationToUrl: jest.fn((loc: unknown) => { + if (loc != null && typeof loc === 'object' && 'path' in loc && Array.isArray((loc as { path: string[] }).path)) { + return (loc as { path: string[] }).path.join('/') + } + return 'workbench/w3/card/test-id' + }), + getCurrentResolvedLocation: jest.fn(() => ({ path: ['workbench', 'w3', 'card', 'test-id'] })) +})) + +jest.mock('@hcengineering/view', () => { + const viewMock = { + string: { + Copied: 'view:string:Copied', + TableCopiedToClipboard: 'view:string:TableCopiedToClipboard', + TableCopyFailed: 'view:string:TableCopyFailed' + }, + class: { Viewlet: 'view:class:Viewlet', ViewletPreference: 'view:class:ViewletPreference' }, + viewlet: { Table: 'view:viewlet:Table' } + } + return { __esModule: true, default: viewMock } +}) + +const mockGetObjectLinkFragment = getObjectLinkFragment as jest.Mock + +describe('converter-resources', () => { + describe('isIntlString', () => { + it('returns true for plugin:resource:key', () => { + expect(isIntlString('card:string:Card')).toBe(true) + }) + + it('returns false for plain string', () => { + expect(isIntlString('Hello')).toBe(false) + }) + }) + + describe('buildMarkdownTableFromDocs', () => { + let mockClient: any + let mockHierarchy: any + let mockDoc: Doc + + beforeEach(() => { + jest.clearAllMocks() + mockGetObjectLinkFragment.mockResolvedValue({ path: ['workbench', 'w3', 'card', 'doc1'] }) + + mockHierarchy = { + getClass: jest.fn((ref: Ref>) => (ref === 'card:class:Card' ? { _id: ref } : null)), + findAttribute: jest.fn(() => ({ type: { _class: core.class.TypeString } })), + as: jest.fn((doc: Doc) => doc), + classHierarchyMixin: jest.fn(() => undefined) + } + + mockClient = { + getHierarchy: () => mockHierarchy, + findAll: jest.fn(async () => []), + findOne: jest.fn(async () => null) + } + + mockDoc = { + _id: 'doc1', + _class: 'card:class:Card', + title: 'Test Card', + createdOn: 1732178763949, + modifiedOn: 1732178770252 + } as unknown as Doc + ;(getClient as jest.Mock).mockReturnValue(mockClient) + ;(getCurrentLanguage as jest.Mock).mockReturnValue('en') + ;(buildModel as jest.Mock).mockResolvedValue([ + { key: '', label: 'card:string:Card' as IntlString, displayProps: {}, attribute: undefined }, + { key: 'createdOn', label: 'Created', displayProps: {}, attribute: undefined } + ]) + }) + + it('returns empty string for empty docs', async () => { + const result = await buildMarkdownTableFromDocs( + [], + { cardClass: 'card:class:Card' as Ref> }, + mockClient + ) + expect(result).toBe('') + }) + + it('builds markdown table from docs with viewlet config', async () => { + mockClient.findAll.mockResolvedValueOnce([]).mockResolvedValueOnce([{ config: ['', 'createdOn'] }]) + + const result = await buildMarkdownTableFromDocs( + [mockDoc], + { cardClass: 'card:class:Card' as Ref> }, + mockClient + ) + + expect(result).toContain('| ') + expect(result).toContain(' |\n') + expect(result).toContain('---') + expect(result).toContain('Test Card') + }) + + it('uses escaped text for non-title columns', async () => { + mockClient.findAll.mockResolvedValueOnce([]).mockResolvedValueOnce([{ config: ['', 'createdOn'] }]) + + const result = await buildMarkdownTableFromDocs( + [mockDoc], + { cardClass: 'card:class:Card' as Ref> }, + mockClient + ) + + expect(result.split('\n').length).toBeGreaterThanOrEqual(2) + expect(result).toMatch(/\|.*\|/) + }) + }) + + describe('copyAsMarkdownTable', () => { + let mockClient: any + let mockHierarchy: any + let mockDoc: Doc + + beforeEach(() => { + jest.clearAllMocks() + mockGetObjectLinkFragment.mockResolvedValue({ path: ['workbench', 'w3', 'card', 'doc1'] }) + + mockHierarchy = { + getClass: jest.fn(() => ({ _id: 'card:class:Card' })), + findAttribute: jest.fn(() => ({ type: { _class: core.class.TypeString } })), + as: jest.fn((doc: Doc) => doc), + classHierarchyMixin: jest.fn(() => undefined) + } + + mockClient = { + getHierarchy: () => mockHierarchy, + findAll: jest.fn(async () => []), + findOne: jest.fn(async () => null) + } + + mockDoc = { + _id: 'doc1', + _class: 'card:class:Card', + title: 'Test Card', + createdOn: 1732178763949, + modifiedOn: 1732178770252 + } as unknown as Doc + ;(getClient as jest.Mock).mockReturnValue(mockClient) + ;(getCurrentLanguage as jest.Mock).mockReturnValue('en') + ;(buildModel as jest.Mock).mockResolvedValue([ + { key: '', label: 'card:string:Card', displayProps: {}, attribute: undefined }, + { key: 'createdOn', label: 'Created', displayProps: {}, attribute: undefined } + ]) + }) + + it('does nothing when docs is empty', async () => { + const evt: Event = {} as any + const props: CopyAsMarkdownTableProps = { cardClass: 'card:class:Card' as Ref> } + await copyAsMarkdownTable([], evt, props) + expect(copyMarkdown).not.toHaveBeenCalled() + }) + + it('calls copyMarkdown and addNotification on success', async () => { + const evt: Event = {} as any + const props: CopyAsMarkdownTableProps = { cardClass: 'card:class:Card' as Ref> } + await copyAsMarkdownTable([mockDoc], evt, props) + expect(copyMarkdown).toHaveBeenCalled() + const { addNotification } = await import('@hcengineering/ui') + expect(addNotification).toHaveBeenCalled() + }) + }) +}) diff --git a/plugins/converter-resources/src/__tests__/formatter.utils.test.ts b/plugins/converter-resources/src/__tests__/formatter.utils.test.ts new file mode 100644 index 0000000000..a8729ef016 --- /dev/null +++ b/plugins/converter-resources/src/__tests__/formatter.utils.test.ts @@ -0,0 +1,78 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { isIntlString, extractObjectTitleOrName } from '../formatter/utils' + +jest.mock('@hcengineering/platform', () => { + const actual = jest.requireActual('@hcengineering/platform') + return { + ...actual, + plugin: jest.fn((_id: string, def: unknown) => def), + translate: jest.fn(async (str: unknown) => `translated:${String(str)}`) + } +}) + +describe('formatter/utils', () => { + describe('isIntlString', () => { + it('returns true for plugin:resource:key format', () => { + expect(isIntlString('card:string:Card')).toBe(true) + expect(isIntlString('contact:class:UserProfile')).toBe(true) + expect(isIntlString('a:b:c')).toBe(true) + }) + + it('returns false for empty string', () => { + expect(isIntlString('')).toBe(false) + }) + + it('returns false for string with fewer than 3 parts', () => { + expect(isIntlString('plugin:key')).toBe(false) + expect(isIntlString('single')).toBe(false) + }) + + it('returns false for string with empty segment', () => { + expect(isIntlString('plugin::key')).toBe(false) + expect(isIntlString(':resource:key')).toBe(false) + expect(isIntlString('plugin:resource:')).toBe(false) + }) + + it('returns false for non-string', () => { + expect(isIntlString(null as any)).toBe(false) + expect(isIntlString(undefined as any)).toBe(false) + }) + }) + + describe('extractObjectTitleOrName', () => { + it('returns title when present and not IntlString', async () => { + expect(await extractObjectTitleOrName({ title: 'My Title' }, undefined)).toBe('My Title') + }) + + it('returns name when present and title absent', async () => { + expect(await extractObjectTitleOrName({ name: 'My Name' }, undefined)).toBe('My Name') + }) + + it('prefers title over name', async () => { + expect(await extractObjectTitleOrName({ title: 'Title', name: 'Name' }, undefined)).toBe('Title') + }) + + it('returns translated value for IntlString title', async () => { + expect(await extractObjectTitleOrName({ title: 'card:string:Card' }, 'en')).toBe('translated:card:string:Card') + }) + + it('returns empty string when neither title nor name', async () => { + expect(await extractObjectTitleOrName({}, undefined)).toBe('') + expect(await extractObjectTitleOrName({ other: 'x' }, undefined)).toBe('') + }) + }) +}) diff --git a/plugins/converter-resources/src/__tests__/markdown.escape.test.ts b/plugins/converter-resources/src/__tests__/markdown.escape.test.ts new file mode 100644 index 0000000000..ee83407268 --- /dev/null +++ b/plugins/converter-resources/src/__tests__/markdown.escape.test.ts @@ -0,0 +1,55 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { escapeMarkdownLinkText, escapeMarkdownLinkUrl } from '../markdown/escape' + +describe('markdown/escape', () => { + describe('escapeMarkdownLinkText', () => { + it('escapes backslashes', () => { + expect(escapeMarkdownLinkText('a\\b')).toBe('a\\\\b') + }) + + it('escapes square brackets', () => { + expect(escapeMarkdownLinkText('[text]')).toBe('\\[text\\]') + }) + + it('escapes pipe', () => { + expect(escapeMarkdownLinkText('a|b')).toBe('a\\|b') + }) + + it('replaces newlines with space', () => { + expect(escapeMarkdownLinkText('a\nb')).toBe('a b') + expect(escapeMarkdownLinkText('a\r\nb')).toBe('a b') + }) + + it('returns plain text unchanged when no special chars', () => { + expect(escapeMarkdownLinkText('Hello World')).toBe('Hello World') + }) + }) + + describe('escapeMarkdownLinkUrl', () => { + it('escapes backslashes', () => { + expect(escapeMarkdownLinkUrl('path\\to')).toBe('path\\\\to') + }) + + it('escapes closing parenthesis', () => { + expect(escapeMarkdownLinkUrl('https://example.com/path)')).toBe('https://example.com/path\\)') + }) + + it('returns plain URL unchanged when no special chars', () => { + expect(escapeMarkdownLinkUrl('https://example.com')).toBe('https://example.com') + }) + }) +}) diff --git a/plugins/converter-resources/src/__tests__/model.tableModel.test.ts b/plugins/converter-resources/src/__tests__/model.tableModel.test.ts new file mode 100644 index 0000000000..531cb4e970 --- /dev/null +++ b/plugins/converter-resources/src/__tests__/model.tableModel.test.ts @@ -0,0 +1,96 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { AttributeModel } from '@hcengineering/view' +import { modelToConfig } from '../model/tableModel' + +jest.mock('@hcengineering/view-resources', () => ({ + buildModel: jest.fn(), + buildConfigLookup: jest.fn() +})) + +jest.mock('@hcengineering/view', () => ({ + default: { + class: { Viewlet: 'view:class:Viewlet', ViewletPreference: 'view:class:ViewletPreference' }, + viewlet: { Table: 'view:viewlet:Table' } + } +})) + +describe('model/tableModel', () => { + describe('modelToConfig', () => { + it('returns key for model with non-empty key', () => { + const model: AttributeModel[] = [{ key: 'title', label: 'Title', displayProps: {}, attribute: undefined } as any] + expect(modelToConfig(model)).toEqual(['title']) + }) + + it('preserves custom attribute as object when key is empty and label starts with custom', () => { + const model: AttributeModel[] = [ + { + key: '', + label: 'customAttr1', + displayProps: {}, + props: {}, + sortingKey: undefined, + attribute: undefined + } as any + ] + expect(modelToConfig(model)).toEqual([ + { + key: 'customAttr1', + label: 'customAttr1', + displayProps: {}, + props: {}, + sortingKey: undefined + } + ]) + }) + + it('returns key string for model with castRequest when key is empty', () => { + const model: AttributeModel[] = [ + { + key: '', + label: 'Label', + displayProps: {}, + props: {}, + sortingKey: undefined, + castRequest: 'ref', + attribute: undefined + } as any + ] + expect(modelToConfig(model)).toEqual([ + { + key: '', + label: 'Label', + displayProps: {}, + props: {}, + sortingKey: undefined + } + ]) + }) + + it('returns empty string for simple empty key without custom/castRequest', () => { + const model: AttributeModel[] = [{ key: '', label: 'Title', displayProps: {}, attribute: undefined } as any] + expect(modelToConfig(model)).toEqual(['']) + }) + + it('handles mixed model', () => { + const model: AttributeModel[] = [ + { key: '', label: 'card:string:Card', displayProps: {}, attribute: undefined } as any, + { key: 'createdOn', label: 'Created', displayProps: {}, attribute: undefined } as any + ] + expect(modelToConfig(model)).toEqual(['', 'createdOn']) + }) + }) +}) diff --git a/plugins/converter-resources/src/actionImpl.ts b/plugins/converter-resources/src/actionImpl.ts new file mode 100644 index 0000000000..df8706e8e6 --- /dev/null +++ b/plugins/converter-resources/src/actionImpl.ts @@ -0,0 +1,26 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Doc } from '@hcengineering/core' +import type { CopyAsMarkdownTableProps } from '@hcengineering/converter' +import { copyAsMarkdownTable } from './markdown' + +export async function copyAsMarkdownTableAction ( + doc: Doc | Doc[], + evt: Event, + props: CopyAsMarkdownTableProps +): Promise { + await copyAsMarkdownTable(doc, evt, props) +} diff --git a/plugins/converter-resources/src/data/index.ts b/plugins/converter-resources/src/data/index.ts new file mode 100644 index 0000000000..970e1aa51a --- /dev/null +++ b/plugins/converter-resources/src/data/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { loadPersonName } from './personLoader' +export { buildTableMetadata, buildRelationshipTableMetadata, isRelationshipTable } from './metadataBuilder' +export { rebuildRelationshipTableViewModel } from './relationshipBuilder' diff --git a/plugins/converter-resources/src/data/metadataBuilder.ts b/plugins/converter-resources/src/data/metadataBuilder.ts new file mode 100644 index 0000000000..c48b62266a --- /dev/null +++ b/plugins/converter-resources/src/data/metadataBuilder.ts @@ -0,0 +1,99 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { concatLink, type Client, type Doc, type Ref } from '@hcengineering/core' +import { getMetadata } from '@hcengineering/platform' +import { getCurrentResolvedLocation, locationToUrl } from '@hcengineering/ui' +import presentation from '@hcengineering/presentation' +import type { TableMetadata, Viewlet } from '@hcengineering/view' +import type { CopyAsMarkdownTableProps, CopyRelationshipTableAsMarkdownProps } from '../types' +import { loadViewletConfig } from '../model/viewletLoader' +import { modelToConfig } from '../model/tableModel' + +/** + * Build metadata object from props and documents + * If viewlet is not provided, tries to find a default viewlet for the class + */ +export async function buildTableMetadata ( + props: CopyAsMarkdownTableProps, + docs: Doc[], + client?: Client +): Promise { + let viewletId: Ref | undefined = props.viewlet?._id + if (viewletId === undefined && client !== undefined) { + const { viewlet } = await loadViewletConfig(client, client.getHierarchy(), props.cardClass, undefined, props.config) + viewletId = viewlet?._id + } + + let originalUrl: string | undefined + try { + const currentLocation = getCurrentResolvedLocation() + const relativeUrl = locationToUrl(currentLocation) + const frontUrl = + getMetadata(presentation.metadata.FrontUrl) ?? (typeof window !== 'undefined' ? window.location.origin : '') + originalUrl = concatLink(frontUrl, relativeUrl) + } catch (error) { + console.warn('Failed to capture original URL for table metadata:', error) + } + + return { + version: '1.0', + cardClass: props.cardClass, + viewletId, + config: props.config, + query: props.query, + documentIds: docs.map((d) => d._id), + timestamp: Date.now(), + originalUrl + } +} + +/** + * Check if a table metadata represents a relationship table + * Relationship tables have viewletId: undefined + */ +export function isRelationshipTable (metadata: TableMetadata): boolean { + return metadata.viewletId === undefined +} + +/** + * Build metadata object for relationship tables + */ +export function buildRelationshipTableMetadata ( + props: CopyRelationshipTableAsMarkdownProps, + docs: Doc[] +): TableMetadata { + let originalUrl: string | undefined + try { + const currentLocation = getCurrentResolvedLocation() + const relativeUrl = locationToUrl(currentLocation) + const frontUrl = + getMetadata(presentation.metadata.FrontUrl) ?? (typeof window !== 'undefined' ? window.location.origin : '') + originalUrl = concatLink(frontUrl, relativeUrl) + } catch (error) { + console.warn('Failed to capture original URL for relationship table metadata:', error) + } + + return { + version: '1.0', + cardClass: props.cardClass, + viewletId: undefined, + config: modelToConfig(props.model), + query: props.query, + documentIds: docs.map((d) => d._id), + timestamp: Date.now(), + originalUrl + } +} diff --git a/plugins/converter-resources/src/data/personLoader.ts b/plugins/converter-resources/src/data/personLoader.ts new file mode 100644 index 0000000000..c225f610c8 --- /dev/null +++ b/plugins/converter-resources/src/data/personLoader.ts @@ -0,0 +1,50 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Hierarchy, PersonId } from '@hcengineering/core' +import { getClient } from '@hcengineering/presentation' +import { getName, getPersonByPersonId } from '@hcengineering/contact' + +/** + * Load person display name by PersonId with optional caching + */ +export async function loadPersonName ( + personId: PersonId, + hierarchy: Hierarchy, + userCache?: Map +): Promise { + if (userCache !== undefined) { + const cachedName = userCache.get(personId) + if (cachedName !== undefined) { + return cachedName + } + } + + try { + const client = getClient() + const person = await getPersonByPersonId(client, personId) + if (person !== null) { + const name = getName(hierarchy, person) + if (userCache !== undefined) { + userCache.set(personId, name) + } + return name + } + } catch (error) { + console.warn('Failed to lookup user name for PersonId:', personId, error) + } + + return personId +} diff --git a/plugins/converter-resources/src/data/relationshipBuilder.ts b/plugins/converter-resources/src/data/relationshipBuilder.ts new file mode 100644 index 0000000000..54d4d55d9a --- /dev/null +++ b/plugins/converter-resources/src/data/relationshipBuilder.ts @@ -0,0 +1,121 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Class, Client, Doc, Hierarchy, Ref } from '@hcengineering/core' +import type { AttributeModel } from '@hcengineering/view' +import { buildConfigAssociation, buildConfigLookup } from '@hcengineering/view-resources' +import type { RelationshipCellModel, RelationshipRowModel } from '../types' + +/** + * Rebuild relationship table viewModel from documents and metadata + * Recreates the hierarchical structure with row spans and separate rows for each associated child + */ +export async function rebuildRelationshipTableViewModel ( + docs: Doc[], + model: AttributeModel[], + cardClass: Ref>, + hierarchy: Hierarchy, + client: Client +): Promise { + const viewModel: RelationshipRowModel[] = [] + + const config = model.map((m) => m.key) + const associations = buildConfigAssociation(config) + const lookup = buildConfigLookup(hierarchy, cardClass, config) + + const associationAttrs = model.filter((attr) => attr.key.startsWith('$associations')) + + let docsWithAssociations: Doc[] = docs + if (associations !== undefined && associations.length > 0) { + const docIds = docs.map((d) => d._id) + const query = { _id: { $in: docIds } } + docsWithAssociations = await client.findAll(cardClass, query, { lookup, associations }) + } + + for (const parentDoc of docsWithAssociations) { + const docWithAssoc = parentDoc as any + const parentAssociations = docWithAssoc.$associations ?? {} + + let maxChildren = 0 + for (const assocAttr of associationAttrs) { + const assocKey = assocAttr.key.replace('$associations.', '') + const children = parentAssociations[assocKey] + if (Array.isArray(children)) { + maxChildren = Math.max(maxChildren, children.length) + } else if (children !== undefined && children !== null) { + maxChildren = Math.max(maxChildren, 1) + } + } + + if (maxChildren === 0) { + const cells: RelationshipCellModel[] = [] + for (const attr of model) { + const isAssociationKey = attr.key.startsWith('$associations') + cells.push({ + attribute: attr, + rowSpan: 1, + object: isAssociationKey ? undefined : parentDoc, + parentObject: isAssociationKey ? parentDoc : undefined + }) + } + viewModel.push({ cells }) + continue + } + + for (let childIndex = 0; childIndex < maxChildren; childIndex++) { + const cells: RelationshipCellModel[] = [] + + for (const attr of model) { + const isAssociationKey = attr.key.startsWith('$associations') + + if (attr.key === '') { + cells.push({ + attribute: attr, + rowSpan: maxChildren, + object: parentDoc, + parentObject: undefined + }) + } else if (isAssociationKey) { + const assocKey = attr.key.replace('$associations.', '') + const children = parentAssociations[assocKey] + let childDoc: Doc | undefined + if (Array.isArray(children) && children.length > childIndex) { + childDoc = children[childIndex] as Doc + } else if (!Array.isArray(children) && children !== undefined && children !== null && childIndex === 0) { + childDoc = children as Doc + } + + cells.push({ + attribute: attr, + rowSpan: 1, + object: childDoc, + parentObject: parentDoc + }) + } else { + cells.push({ + attribute: attr, + rowSpan: 1, + object: childIndex === 0 ? parentDoc : undefined, + parentObject: undefined + }) + } + } + + viewModel.push({ cells }) + } + } + + return viewModel +} diff --git a/plugins/converter-resources/src/formatter/index.ts b/plugins/converter-resources/src/formatter/index.ts new file mode 100644 index 0000000000..c632f2236a --- /dev/null +++ b/plugins/converter-resources/src/formatter/index.ts @@ -0,0 +1,24 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { registerValueFormatterForClass, registerValueFormatter, getFormattersForClass } from './registry' +export { formatValue, formatCustomAttributeValue } from './valueFormatter' +export { + isIntlString, + formatArrayValue, + extractObjectTitleOrName, + DocumentAttributeKey, + DateFormatOption +} from './utils' diff --git a/plugins/converter-resources/src/formatter/registry.ts b/plugins/converter-resources/src/formatter/registry.ts new file mode 100644 index 0000000000..2ccadaa17f --- /dev/null +++ b/plugins/converter-resources/src/formatter/registry.ts @@ -0,0 +1,71 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Doc, Class, Ref, Hierarchy } from '@hcengineering/core' +import type { ValueFormatter } from '../types' + +/** + * Registry for value formatters by document class + * Plugins can register custom formatters for specific document classes + */ +const valueFormattersByClass = new Map>, ValueFormatter[]>() + +/** + * Global formatters (checked for all classes) + */ +const globalValueFormatters: ValueFormatter[] = [] + +/** + * Register a value formatter for a specific document class + * @param _class - The document class this formatter applies to + * @param formatter - The formatter function to register + */ +export function registerValueFormatterForClass (_class: Ref>, formatter: ValueFormatter): void { + const formatters = valueFormattersByClass.get(_class) ?? [] + formatters.push(formatter) + valueFormattersByClass.set(_class, formatters) +} + +/** + * Register a global value formatter (applies to all classes) + * @param formatter - The formatter function to register + * @deprecated Use registerValueFormatterForClass for better performance and explicit class association + */ +export function registerValueFormatter (formatter: ValueFormatter): void { + globalValueFormatters.push(formatter) +} + +/** + * Get formatters for a specific class (including parent classes) + */ +export function getFormattersForClass (hierarchy: Hierarchy, _class: Ref>): ValueFormatter[] { + const formatters: ValueFormatter[] = [] + + // Get formatters for this class and all parent classes + let currentClass: Ref> | undefined = _class + while (currentClass !== undefined) { + const classFormatters = valueFormattersByClass.get(currentClass) + if (classFormatters !== undefined) { + formatters.push(...classFormatters) + } + const classDef: Class | undefined = hierarchy.getClass(currentClass) + currentClass = classDef?.extends + } + + // Add global formatters + formatters.push(...globalValueFormatters) + + return formatters +} diff --git a/plugins/converter-resources/src/formatter/utils.ts b/plugins/converter-resources/src/formatter/utils.ts new file mode 100644 index 0000000000..b0499f6fee --- /dev/null +++ b/plugins/converter-resources/src/formatter/utils.ts @@ -0,0 +1,123 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { type AnyAttribute, type Class, type Doc, type Ref } from '@hcengineering/core' +import { translate, type IntlString } from '@hcengineering/platform' + +export enum DocumentAttributeKey { + CreatedBy = 'createdBy', + CreatedOn = 'createdOn', + ModifiedBy = 'modifiedBy', + ModifiedOn = 'modifiedOn', + Title = 'title', + Name = 'name' +} + +export enum DateFormatOption { + Numeric = 'numeric', + Short = 'short' +} + +/** + * Check if a value is an IntlString (format: "plugin:resource:key"). + * Type guard: narrows unknown to string when true. + */ +export function isIntlString (value: unknown): value is string { + if (typeof value !== 'string' || value.length === 0) { + return false + } + const parts = value.split(':') + return parts.length >= 3 && parts.every((part) => part.length > 0) +} + +/** + * Format an array of values, handling reference lookups if needed + */ +export async function formatArrayValue ( + value: any[], + attrType: any, + attribute: AnyAttribute | undefined, + attrKey: string, + card: Doc, + language: string | undefined +): Promise { + const isRefArray = + attrType?._class === core.class.ArrOf && + (attrType as { of?: { _class?: Ref> } })?.of?._class === core.class.RefTo + + if (isRefArray && (attribute !== undefined || attrKey !== '')) { + const cardWithLookup = card as any + const lookupKey = attribute?.name ?? attrKey + const lookupData = cardWithLookup.$lookup?.[lookupKey] + + if (lookupData !== undefined && lookupData !== null) { + const resolvedArray = Array.isArray(lookupData) ? lookupData : [lookupData] + const translatedValues = await Promise.all( + resolvedArray.map(async (v) => { + if (typeof v === 'object' && v !== null && 'title' in v) { + const title = v.title ?? '' + if (typeof title === 'string' && isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return String(title) + } + return typeof v === 'string' ? v : String(v) + }) + ) + return translatedValues.join(', ') + } + } + + const translatedValues = await Promise.all( + value.map(async (v) => { + if (typeof v === 'object' && v !== null && 'title' in v) { + const title = v.title ?? '' + if (typeof title === 'string' && isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return String(title) + } + if (typeof v === 'string' && isIntlString(v)) { + return await translate(v as unknown as IntlString, {}, language) + } + return typeof v === 'string' ? v : String(v) + }) + ) + return translatedValues.join(', ') +} + +/** + * Extract title or name from an object, translating if needed + */ +export async function extractObjectTitleOrName ( + obj: Record, + language: string | undefined +): Promise { + if ('title' in obj) { + const title = String(obj.title ?? '') + if (isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return title + } + if ('name' in obj) { + const name = String(obj.name ?? '') + if (isIntlString(name)) { + return await translate(name as unknown as IntlString, {}, language) + } + return name + } + return '' +} diff --git a/plugins/converter-resources/src/formatter/valueFormatter.ts b/plugins/converter-resources/src/formatter/valueFormatter.ts new file mode 100644 index 0000000000..ff5ee7d1d3 --- /dev/null +++ b/plugins/converter-resources/src/formatter/valueFormatter.ts @@ -0,0 +1,258 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + type AnyAttribute, + type Class, + type Doc, + type Hierarchy, + type Ref, + type PersonId, + getDisplayTime, + getObjectValue +} from '@hcengineering/core' +import { translate, type IntlString, getResource } from '@hcengineering/platform' +import type { AttributeModel } from '@hcengineering/view' +import converter from '@hcengineering/converter' +import { getFormattersForClass } from './registry' +import { + formatArrayValue, + extractObjectTitleOrName, + isIntlString, + DocumentAttributeKey, + DateFormatOption +} from './utils' +import { loadPersonName } from '../data/personLoader' +import type { ValueFormatter } from '../types' + +/** + * Format a custom attribute value for markdown display + * Handles various types: string, number, boolean, arrays, references + */ +export async function formatCustomAttributeValue ( + value: any, + attribute: AnyAttribute | undefined, + card: Doc, + hierarchy: Hierarchy, + language: string | undefined +): Promise { + if (value === null || value === undefined) { + return '' + } + + const attrType = attribute?.type + + if (typeof value === 'number' && attrType?._class === core.class.TypeTimestamp) { + return getDisplayTime(value) + } + + if (value instanceof Date) { + const options: Intl.DateTimeFormatOptions = { + year: DateFormatOption.Numeric, + month: DateFormatOption.Short, + day: DateFormatOption.Numeric + } + return value.toLocaleDateString(language ?? 'default', options) + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value) + } + + if (typeof value === 'string') { + if (isIntlString(value)) { + return await translate(value as unknown as IntlString, {}, language) + } + + const isRef = attrType?._class === core.class.RefTo + if (isRef && attribute !== undefined) { + const cardWithLookup = card as any + const lookupData = cardWithLookup.$lookup?.[attribute.name] + if (lookupData !== undefined && lookupData !== null) { + if (typeof lookupData === 'object' && 'title' in lookupData) { + const title = lookupData.title ?? '' + if (typeof title === 'string' && isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return String(title) + } + } + } + + return value + } + + if (Array.isArray(value)) { + return await formatArrayValue(value, attrType, attribute, attribute?.name ?? '', card, language) + } + + if (typeof value === 'object' && value !== null) { + const obj = value as Record + const titleOrName = await extractObjectTitleOrName(obj, language) + return titleOrName !== '' ? titleOrName : String(value) + } + + return String(value) +} + +/** + * Format a single attribute value for display (used by table builders) + */ +export async function formatValue ( + attr: AttributeModel, + card: Doc, + hierarchy: Hierarchy, + _class: Ref>, + language: string | undefined, + isFirstColumn: boolean = false, + userCache?: Map, + customFormatter?: ValueFormatter +): Promise { + // Try custom formatter first (from actionProps) + if (customFormatter !== undefined) { + const formattedValue = await customFormatter(attr, card, hierarchy, _class, language) + if (formattedValue !== undefined) { + return formattedValue + } + } + + // Try mixin-based formatter (MarkdownValueFormatter on the class) + const formatterMixin = hierarchy.classHierarchyMixin(_class, converter.mixin.MarkdownValueFormatter) + if (formatterMixin?.formatter !== undefined) { + const formatter = await getResource(formatterMixin.formatter) + const result = await formatter(attr, card, hierarchy, _class, language) + if (result !== undefined) { + return result + } + } + + // Fall back to registered value formatters + const formatters = getFormattersForClass(hierarchy, _class) + for (const formatter of formatters) { + const formattedValue = await formatter(attr, card, hierarchy, _class, language) + if (formattedValue !== undefined) { + return formattedValue + } + } + + let value: any + if (attr.castRequest != null) { + value = getObjectValue(attr.key.substring(attr.castRequest.length + 1), hierarchy.as(card, attr.castRequest)) + } else { + if (attr.key.startsWith('$lookup.')) { + const lookupKey = attr.key.replace('$lookup.', '') + const lookupParts = lookupKey.split('.') + const cardWithLookup = card as any + const lookupObj = cardWithLookup.$lookup?.[lookupParts[0]] + if (lookupObj !== undefined && lookupObj !== null) { + if (lookupParts.length > 1) { + value = getObjectValue(lookupParts.slice(1).join('.'), lookupObj) + } else { + value = lookupObj + } + } else { + value = undefined + } + } else { + value = getObjectValue(attr.key, card) + } + } + + if (attr.key === '' && !isFirstColumn) { + const labelStr = typeof attr.label === 'string' ? attr.label : '' + const isCustomAttribute = labelStr.startsWith('custom') + + if (isCustomAttribute) { + const customValue = (card as any)[labelStr] + if (customValue === null || customValue === undefined) { + return '' + } + + const docClass = card._class + let customAttr = hierarchy.findAttribute(docClass, labelStr) + + if (customAttr === undefined) { + const allAttrs = hierarchy.getAllAttributes(docClass) + customAttr = allAttrs.get(labelStr) + } + + return await formatCustomAttributeValue(customValue, customAttr, card, hierarchy, language) + } + + return '' + } + + if (value === null || value === undefined) { + return '' + } + + const attribute = attr.attribute ?? hierarchy.findAttribute(_class, attr.key) + const attrType = attribute?.type + + if (typeof value === 'number' && attrType?._class === core.class.TypeTimestamp) { + return getDisplayTime(value) + } + + if (value instanceof Date) { + const options: Intl.DateTimeFormatOptions = { + year: DateFormatOption.Numeric, + month: DateFormatOption.Short, + day: DateFormatOption.Numeric + } + return value.toLocaleDateString(language ?? 'default', options) + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value) + } + + if (typeof value === 'string') { + const isRef = attrType?._class === core.class.RefTo + if (isRef) { + const cardWithLookup = card as any + const lookupData = cardWithLookup.$lookup?.[attr.key] + if (lookupData !== undefined && lookupData !== null) { + const resolvedObj = lookupData + if (typeof resolvedObj === 'object' && resolvedObj !== null && 'title' in resolvedObj) { + const title = resolvedObj[DocumentAttributeKey.Title] ?? '' + if (typeof title === 'string' && isIntlString(title)) { + return await translate(title as unknown as IntlString, {}, language) + } + return String(title) + } + } + } + + if (isIntlString(value)) { + return await translate(value as unknown as IntlString, {}, language) + } + if (attr.key === DocumentAttributeKey.CreatedBy || attr.key === DocumentAttributeKey.ModifiedBy) { + return await loadPersonName(value as PersonId, hierarchy, userCache) + } + return value + } + + if (Array.isArray(value)) { + return await formatArrayValue(value, attrType, attribute, attr.key, card, language) + } + + if (typeof value === 'object' && value !== null) { + const obj = value as Record + const titleOrName = await extractObjectTitleOrName(obj, language) + return titleOrName !== '' ? titleOrName : String(value) + } + + return String(value) +} diff --git a/plugins/converter-resources/src/index.ts b/plugins/converter-resources/src/index.ts new file mode 100644 index 0000000000..3e79425673 --- /dev/null +++ b/plugins/converter-resources/src/index.ts @@ -0,0 +1,54 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Resources } from '@hcengineering/platform' +import { + copyAsMarkdownTableFromResource, + copyRelationshipTableAsMarkdown, + buildMarkdownTableFromMetadata as buildMarkdownTableFromMetadataImpl, + buildMarkdownTableFromDocs as buildMarkdownTableFromDocsImpl +} from './markdown' +import { copyAsMarkdownTableAction } from './actionImpl' + +export * from './formatter' +export * from './model' +export * from './data' + +export { + copyAsMarkdownTable, + copyAsMarkdownTableFromResource, + copyRelationshipTableAsMarkdown, + buildMarkdownTableFromDocs, + buildMarkdownTableFromMetadata +} from './markdown' + +export { isRelationshipTable, buildRelationshipTableMetadata } from './data' + +export { isIntlString } from './formatter' +export { registerValueFormatterForClass, registerValueFormatter } from './formatter' + +export { type TableConverter, MarkdownTableConverter } from './types' + +export default async (): Promise => ({ + function: { + CopyAsMarkdownTable: copyAsMarkdownTableFromResource, + CopyRelationshipAsMarkdown: copyRelationshipTableAsMarkdown, + BuildMarkdownTableFromMetadata: buildMarkdownTableFromMetadataImpl, + BuildMarkdownTableFromDocs: buildMarkdownTableFromDocsImpl + }, + actionImpl: { + CopyAsMarkdownTable: copyAsMarkdownTableAction + } +}) diff --git a/plugins/converter-resources/src/markdown/copyActions.ts b/plugins/converter-resources/src/markdown/copyActions.ts new file mode 100644 index 0000000000..98d87f6543 --- /dev/null +++ b/plugins/converter-resources/src/markdown/copyActions.ts @@ -0,0 +1,128 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Doc } from '@hcengineering/core' +import { translate } from '@hcengineering/platform' +import { addNotification, NotificationSeverity } from '@hcengineering/ui' +import { getCurrentLanguage } from '@hcengineering/theme' +import view from '@hcengineering/view' +import { getClient } from '@hcengineering/presentation' +import { copyMarkdown, SimpleNotification } from '@hcengineering/view-resources' + +import type { CopyAsMarkdownTableProps, CopyRelationshipTableAsMarkdownProps } from '../types' +import { buildTableMetadata, buildRelationshipTableMetadata } from '../data' +import { buildMarkdownTableFromDocs, buildRelationshipTableMarkdown } from './tableBuilder' + +/** + * Copy documents as markdown table to clipboard + */ +export async function copyAsMarkdownTable ( + doc: Doc | Doc[], + evt: Event, + props: CopyAsMarkdownTableProps +): Promise { + try { + const docs = Array.isArray(doc) ? doc : doc !== undefined ? [doc] : [] + if (docs.length === 0) { + return + } + const client = getClient() + + const markdown = await buildMarkdownTableFromDocs(docs, props, client) + + if (markdown.length === 0) { + return + } + + const metadata = await buildTableMetadata(props, docs, client) + await copyMarkdown(markdown, metadata) + + const language = getCurrentLanguage() + addNotification( + await translate(view.string.Copied, {}, language), + await translate(view.string.TableCopiedToClipboard, {}, language), + SimpleNotification, + undefined, + NotificationSeverity.Success + ) + } catch (error) { + console.error('Error copying markdown table', error) + const language = getCurrentLanguage() + addNotification( + await translate(view.string.Copied, {}, language), + await translate(view.string.TableCopyFailed, {}, language), + SimpleNotification, + undefined, + NotificationSeverity.Error + ) + } +} + +/** + * Wrapper for the function resource (evt, props). + * Callers must pass docs in props when using getResource(converter.function.CopyAsMarkdownTable). + */ +export async function copyAsMarkdownTableFromResource ( + evt: Event, + props: CopyAsMarkdownTableProps & { docs?: Doc[] } +): Promise { + const docs = props.docs ?? [] + await copyAsMarkdownTable(docs, evt, props) +} + +/** + * Copy RelationshipTable as markdown table + */ +export async function copyRelationshipTableAsMarkdown ( + evt: Event, + props: CopyRelationshipTableAsMarkdownProps +): Promise { + try { + const client = getClient() + const hierarchy = client.getHierarchy() + const cardClass = hierarchy.getClass(props.cardClass) + if (cardClass == null) { + return + } + + const language = getCurrentLanguage() + const markdown = await buildRelationshipTableMarkdown(props, hierarchy, language) + + if (markdown.length === 0) { + return + } + + const metadata = buildRelationshipTableMetadata(props, props.objects) + await copyMarkdown(markdown, metadata) + + addNotification( + await translate(view.string.Copied, {}, language), + await translate(view.string.TableCopiedToClipboard, {}, language), + SimpleNotification, + undefined, + NotificationSeverity.Success + ) + } catch (error) { + console.error('Error copying relationship table', error) + const language = getCurrentLanguage() + addNotification( + await translate(view.string.Copied, {}, language), + await translate(view.string.TableCopyFailed, {}, language), + SimpleNotification, + undefined, + NotificationSeverity.Error + ) + } +} diff --git a/plugins/converter-resources/src/markdown/escape.ts b/plugins/converter-resources/src/markdown/escape.ts new file mode 100644 index 0000000000..00ea4f81d8 --- /dev/null +++ b/plugins/converter-resources/src/markdown/escape.ts @@ -0,0 +1,33 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + * Escape markdown link text (brackets, pipes, backslashes, newlines) + */ +export function escapeMarkdownLinkText (text: string): string { + return text + .replace(/\\/g, '\\\\') + .replace(/\[/g, '\\[') + .replace(/\]/g, '\\]') + .replace(/\|/g, '\\|') + .replace(/\r?\n/g, ' ') +} + +/** + * Escape markdown link URL (backslashes and closing parentheses) + */ +export function escapeMarkdownLinkUrl (url: string): string { + return url.replace(/\\/g, '\\\\').replace(/\)/g, '\\)') +} diff --git a/plugins/converter-resources/src/markdown/index.ts b/plugins/converter-resources/src/markdown/index.ts new file mode 100644 index 0000000000..84726d0770 --- /dev/null +++ b/plugins/converter-resources/src/markdown/index.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { escapeMarkdownLinkText, escapeMarkdownLinkUrl } from './escape' +export { createMarkdownLink } from './link' +export { + buildMarkdownTableFromDocs, + buildMarkdownTableFromMetadata, + buildRelationshipTableMarkdown +} from './tableBuilder' +export { copyAsMarkdownTable, copyAsMarkdownTableFromResource, copyRelationshipTableAsMarkdown } from './copyActions' diff --git a/plugins/converter-resources/src/markdown/link.ts b/plugins/converter-resources/src/markdown/link.ts new file mode 100644 index 0000000000..695f18c55d --- /dev/null +++ b/plugins/converter-resources/src/markdown/link.ts @@ -0,0 +1,42 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Doc, Hierarchy } from '@hcengineering/core' +import { concatLink } from '@hcengineering/core' +import { getMetadata } from '@hcengineering/platform' +import presentation from '@hcengineering/presentation' +import { getObjectLinkFragment } from '@hcengineering/view-resources' +import { locationToUrl } from '@hcengineering/ui' +import view from '@hcengineering/view' +import { escapeMarkdownLinkText, escapeMarkdownLinkUrl } from './escape' + +/** + * Create a markdown link for a document + */ +export async function createMarkdownLink (hierarchy: Hierarchy, card: Doc, value: string): Promise { + try { + const loc = await getObjectLinkFragment(hierarchy, card, {}, view.component.EditDoc) + const relativeUrl = locationToUrl(loc) + const frontUrl = + getMetadata(presentation.metadata.FrontUrl) ?? (typeof window !== 'undefined' ? window.location.origin : '') + const fullUrl = concatLink(frontUrl, relativeUrl) + const escapedText = escapeMarkdownLinkText(value) + const escapedUrl = escapeMarkdownLinkUrl(fullUrl) + return `[${escapedText}](${escapedUrl})` + } catch (error) { + console.warn('Error creating markdown link', error) + return escapeMarkdownLinkText(value) + } +} diff --git a/plugins/converter-resources/src/markdown/tableBuilder.ts b/plugins/converter-resources/src/markdown/tableBuilder.ts new file mode 100644 index 0000000000..7a74730cca --- /dev/null +++ b/plugins/converter-resources/src/markdown/tableBuilder.ts @@ -0,0 +1,311 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Class, Client, Doc, Hierarchy, Ref, PersonId } from '@hcengineering/core' +import { getCurrentLanguage } from '@hcengineering/theme' +import type { AttributeModel, BuildMarkdownTableMetadata, TableMetadata, Viewlet } from '@hcengineering/view' +import viewPlugin from '@hcengineering/view' +import { buildConfigLookup, buildModel, getAttributeValue } from '@hcengineering/view-resources' +import type { CopyAsMarkdownTableProps, CopyRelationshipTableAsMarkdownProps } from '../types' +import { formatValue } from '../formatter' +import { generateHeaders, loadViewletConfig, buildTableModel } from '../model' +import { rebuildRelationshipTableViewModel, isRelationshipTable } from '../data' +import { escapeMarkdownLinkText } from './escape' +import { createMarkdownLink } from './link' + +async function buildRelationshipTableFromMetadata ( + docs: Doc[], + metadata: BuildMarkdownTableMetadata, + client: Client +): Promise { + const hierarchy = client.getHierarchy() + const cardClass = metadata.cardClass as Ref> + + const config = metadata.config ?? [] + const lookup = buildConfigLookup(hierarchy, cardClass, config) + const model = await buildModel({ + client, + _class: cardClass, + keys: config, + lookup + }) + + const viewModel = await rebuildRelationshipTableViewModel(docs, model, cardClass, hierarchy, client) + + const props: CopyRelationshipTableAsMarkdownProps = { + viewModel, + model, + objects: docs, + cardClass, + query: metadata.query + } + + const language = getCurrentLanguage() + return await buildRelationshipTableMarkdown(props, hierarchy, language) +} + +/** + * Wrapper function for building markdown table from BuildMarkdownTableMetadata + * This is used by text-editor-resources to refresh tables + */ +export async function buildMarkdownTableFromMetadata ( + docs: Doc[], + metadata: BuildMarkdownTableMetadata, + client: Client +): Promise { + const tableMetadata = metadata as TableMetadata + if (isRelationshipTable(tableMetadata)) { + return await buildRelationshipTableFromMetadata(docs, metadata, client) + } + + let viewlet: Viewlet | undefined + if (metadata.viewletId !== undefined) { + viewlet = await client.findOne(viewPlugin.class.Viewlet, { _id: metadata.viewletId as Ref }) + } + + const props: CopyAsMarkdownTableProps = { + cardClass: metadata.cardClass as Ref>, + viewlet, + config: metadata.config, + query: metadata.query + } + + return await buildMarkdownTableFromDocs(docs, props, client) +} + +/** + * Build markdown table string from documents and props + */ +export async function buildMarkdownTableFromDocs ( + docs: Doc[], + props: CopyAsMarkdownTableProps, + client: Client +): Promise { + if (docs.length === 0) { + return '' + } + + const hierarchy = client.getHierarchy() + const cardClass = hierarchy.getClass(props.cardClass) + if (cardClass == null) { + return '' + } + + const { viewlet, config: actualConfig } = await loadViewletConfig( + client, + hierarchy, + props.cardClass, + props.viewlet, + props.config + ) + + let displayableModel: AttributeModel[] + if (actualConfig !== undefined && actualConfig.length > 0) { + const lookup = + viewlet !== undefined + ? buildConfigLookup(hierarchy, props.cardClass, actualConfig, viewlet.options?.lookup) + : undefined + const hiddenKeys = viewlet?.configOptions?.hiddenKeys ?? [] + const model = await buildModel({ + client, + _class: props.cardClass, + keys: actualConfig.filter((key: string | import('@hcengineering/view').BuildModelKey) => { + if (typeof key === 'string') { + return !hiddenKeys.includes(key) + } + return !hiddenKeys.includes(key.key) && key.displayProps?.grow !== true + }), + lookup + }) + displayableModel = model.filter((attr) => attr.displayProps?.grow !== true) + } else { + displayableModel = await buildTableModel(client, hierarchy, props.cardClass, viewlet) + } + + if (displayableModel.length === 0) { + return '' + } + + const language = getCurrentLanguage() + const userCache = new Map() + const firstDocClass = docs.length > 0 ? docs[0]._class : props.cardClass + const headers = await generateHeaders(displayableModel, firstDocClass, hierarchy, language) + + const rows: string[][] = [] + for (const card of docs) { + const row: string[] = [] + for (let i = 0; i < displayableModel.length; i++) { + const attr = displayableModel[i] + const isFirstColumn = i === 0 + const value = await formatValue( + attr, + card, + hierarchy, + props.cardClass, + language, + isFirstColumn, + userCache, + props.valueFormatter + ) + + if (isFirstColumn && attr.key === '') { + const linkValue = await createMarkdownLink(hierarchy, card, value) + row.push(linkValue) + } else { + row.push(escapeMarkdownLinkText(value)) + } + } + rows.push(row) + } + + let markdown = '| ' + headers.join(' | ') + ' |\n' + markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n' + for (const row of rows) { + markdown += '| ' + row.join(' | ') + ' |\n' + } + + return markdown +} + +/** + * Build markdown table from relationship table props (viewModel, model, objects) + */ +export async function buildRelationshipTableMarkdown ( + props: CopyRelationshipTableAsMarkdownProps, + hierarchy: Hierarchy, + language: string | undefined +): Promise { + if (props.viewModel.length === 0 || props.model.length === 0) { + return '' + } + + const userCache = new Map() + const firstDocClass = props.objects.length > 0 ? props.objects[0]._class : props.cardClass + const headers = await generateHeaders(props.model, firstDocClass, hierarchy, language) + + const attributeKeyToIndex = new Map() + props.model.forEach((attr, index) => { + attributeKeyToIndex.set(attr.key, index) + }) + + const activeRowSpans = new Map() + const rows: string[][] = [] + + for (let rowIdx = 0; rowIdx < props.viewModel.length; rowIdx++) { + const rowModel = props.viewModel[rowIdx] + const row: string[] = new Array(headers.length).fill('') + + for (const [attrKey, spanInfo] of activeRowSpans.entries()) { + if (spanInfo.remaining > 0) { + const attrIndex = attributeKeyToIndex.get(attrKey) + if (attrIndex !== undefined) { + row[attrIndex] = spanInfo.value + spanInfo.remaining-- + if (spanInfo.remaining === 0) { + activeRowSpans.delete(attrKey) + } + } + } + } + + for (const cell of rowModel.cells) { + const attrIndex = attributeKeyToIndex.get(cell.attribute.key) + if (attrIndex === undefined) continue + + const isAssociationKey = cell.attribute.key.startsWith('$associations') + + let doc: Doc | undefined + if (isAssociationKey) { + doc = cell.object + } else { + doc = cell.object ?? cell.parentObject + } + + if (doc === undefined) { + row[attrIndex] = '' + continue + } + + const rawValue = getAttributeValue(cell.attribute, doc, hierarchy) + + let docToUse = doc + let docClass = props.cardClass + let attributeToUse = cell.attribute + + if (isAssociationKey) { + if (rawValue !== undefined && rawValue !== null && typeof rawValue === 'object' && '_class' in rawValue) { + docToUse = rawValue as Doc + docClass = docToUse._class + const parts = cell.attribute.key.split('$associations.') + if (parts.length > 1) { + const afterAssoc = parts[1].substring(1) + const dotIndex = afterAssoc.indexOf('.') + if (dotIndex > 0) { + const attributeName = afterAssoc.substring(dotIndex + 1) + attributeToUse = { + ...cell.attribute, + key: attributeName + } + } else { + attributeToUse = { + ...cell.attribute, + key: '' + } + } + } + } + } + + const isFirstColumn = attrIndex === 0 + const allowEmptyKey = isFirstColumn || isAssociationKey + let value = await formatValue( + attributeToUse, + docToUse, + hierarchy, + docClass, + language, + allowEmptyKey, + userCache, + props.valueFormatter + ) + + const isDocumentTitle = attributeToUse.key === '' && docToUse !== undefined + if (isDocumentTitle) { + value = await createMarkdownLink(hierarchy, docToUse, value) + } else { + value = escapeMarkdownLinkText(value) + } + + row[attrIndex] = value + + if (cell.rowSpan > 1) { + activeRowSpans.set(cell.attribute.key, { + value, + remaining: cell.rowSpan - 1 + }) + } + } + + rows.push(row) + } + + let markdown = '| ' + headers.join(' | ') + ' |\n' + markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n' + for (const row of rows) { + markdown += '| ' + row.join(' | ') + ' |\n' + } + + return markdown +} diff --git a/plugins/converter-resources/src/model/headerGenerator.ts b/plugins/converter-resources/src/model/headerGenerator.ts new file mode 100644 index 0000000000..78eb02c614 --- /dev/null +++ b/plugins/converter-resources/src/model/headerGenerator.ts @@ -0,0 +1,74 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Class, Doc, Hierarchy, Ref } from '@hcengineering/core' +import { translate, type IntlString } from '@hcengineering/platform' +import type { AttributeModel } from '@hcengineering/view' +import { isIntlString } from '../formatter/utils' + +/** + * Resolve the human-readable label for a custom attribute + */ +export async function resolveCustomAttributeLabel ( + attrLabel: string, + docClass: Ref>, + hierarchy: Hierarchy, + language: string | undefined +): Promise { + if (!attrLabel.startsWith('custom')) { + return attrLabel + } + + let customAttr = hierarchy.findAttribute(docClass, attrLabel) + if (customAttr === undefined) { + const allAttrs = hierarchy.getAllAttributes(docClass) + customAttr = allAttrs.get(attrLabel) + } + + if (customAttr?.label !== undefined) { + return await translate(customAttr.label, {}, language) + } + + return attrLabel +} + +/** + * Generate table headers from AttributeModel array + * Handles custom attributes, IntlStrings, and regular labels + */ +export async function generateHeaders ( + model: AttributeModel[], + firstDocClass: Ref>, + hierarchy: Hierarchy, + language: string | undefined +): Promise { + const headers: string[] = [] + for (const attr of model) { + let label: string + if (typeof attr.label === 'string') { + if (attr.label.startsWith('custom')) { + label = await resolveCustomAttributeLabel(attr.label, firstDocClass, hierarchy, language) + } else if (isIntlString(attr.label)) { + label = await translate(attr.label as unknown as IntlString, {}, language) + } else { + label = attr.label + } + } else { + label = await translate(attr.label, {}, language) + } + headers.push(label) + } + return headers +} diff --git a/plugins/converter-resources/src/model/index.ts b/plugins/converter-resources/src/model/index.ts new file mode 100644 index 0000000000..ce8db051ef --- /dev/null +++ b/plugins/converter-resources/src/model/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export { buildTableModel, modelToConfig } from './tableModel' +export { loadViewletConfig } from './viewletLoader' +export { generateHeaders, resolveCustomAttributeLabel } from './headerGenerator' diff --git a/plugins/converter-resources/src/model/tableModel.ts b/plugins/converter-resources/src/model/tableModel.ts new file mode 100644 index 0000000000..a9a63c2c13 --- /dev/null +++ b/plugins/converter-resources/src/model/tableModel.ts @@ -0,0 +1,112 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { type Class, type Client, type Doc, type Hierarchy, type Ref } from '@hcengineering/core' +import type { AttributeModel, BuildModelKey, Viewlet } from '@hcengineering/view' +import viewPlugin from '@hcengineering/view' +import { buildModel, buildConfigLookup } from '@hcengineering/view-resources' +import { DocumentAttributeKey } from '../formatter/utils' + +/** + * Convert AttributeModel array back to config format (Array) + * Preserves custom attributes by using label as key when key is empty + */ +export function modelToConfig (model: AttributeModel[]): Array { + return model.map((m) => { + if (m.key === '' && typeof m.label === 'string' && m.label.startsWith('custom')) { + return { + key: m.label, + label: m.label, + displayProps: m.displayProps, + props: m.props, + sortingKey: m.sortingKey + } + } + if (m.key !== '') { + return m.key + } + if (m.castRequest !== undefined) { + return { + key: m.key, + label: m.label, + displayProps: m.displayProps, + props: m.props, + sortingKey: m.sortingKey + } + } + return m.key + }) +} + +/** + * Build AttributeModel from viewlet config (or default config) + */ +export async function buildTableModel ( + client: Client, + hierarchy: Hierarchy, + _class: Ref>, + viewlet: Viewlet | undefined +): Promise { + if (viewlet !== undefined) { + const preferences = await client.findAll(viewPlugin.class.ViewletPreference, { + space: core.space.Workspace, + attachedTo: viewlet._id + }) + const config = preferences.length > 0 && preferences[0].config.length > 0 ? preferences[0].config : viewlet.config + + const lookup = buildConfigLookup(hierarchy, _class, config, viewlet.options?.lookup) + const hiddenKeys = viewlet.configOptions?.hiddenKeys ?? [] + const model = await buildModel({ + client, + _class, + keys: config.filter((key: string | BuildModelKey) => { + if (typeof key === 'string') { + return !hiddenKeys.includes(key) + } + return !hiddenKeys.includes(key.key) && key.displayProps?.grow !== true + }), + lookup + }) + + return model.filter((attr) => attr.displayProps?.grow !== true) + } + + const defaultConfig: Array = [ + '', // Object presenter (title) + DocumentAttributeKey.CreatedBy, + DocumentAttributeKey.CreatedOn, + DocumentAttributeKey.ModifiedBy, + DocumentAttributeKey.ModifiedOn + ] + + const model = await buildModel({ + client, + _class, + keys: defaultConfig, + lookup: undefined + }) + + return model.filter((attr) => { + if ( + attr.key === DocumentAttributeKey.CreatedBy || + attr.key === DocumentAttributeKey.CreatedOn || + attr.key === DocumentAttributeKey.ModifiedBy || + attr.key === DocumentAttributeKey.ModifiedOn + ) { + return hierarchy.findAttribute(_class, attr.key) !== undefined + } + return true + }) +} diff --git a/plugins/converter-resources/src/model/viewletLoader.ts b/plugins/converter-resources/src/model/viewletLoader.ts new file mode 100644 index 0000000000..f0fd7e8822 --- /dev/null +++ b/plugins/converter-resources/src/model/viewletLoader.ts @@ -0,0 +1,62 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { type Class, type Client, type Doc, type Hierarchy, type Ref } from '@hcengineering/core' +import type { BuildModelKey, Viewlet } from '@hcengineering/view' +import viewPlugin from '@hcengineering/view' + +/** + * Loads the actual viewlet configuration, including user preferences + */ +export async function loadViewletConfig ( + client: Client, + hierarchy: Hierarchy, + cardClass: Ref>, + propsViewlet?: Viewlet, + propsConfig?: Array +): Promise<{ viewlet: Viewlet | undefined, config: Array | undefined }> { + if (propsConfig !== undefined && propsConfig.length > 0) { + return { viewlet: propsViewlet, config: propsConfig } + } + + let viewlet: Viewlet | undefined = propsViewlet + if (viewlet === undefined) { + const allClasses = [cardClass] + let currentClass = hierarchy.getClass(cardClass) + while (currentClass?.extends !== undefined) { + allClasses.push(currentClass.extends) + currentClass = hierarchy.getClass(currentClass.extends) + } + const viewlets = await client.findAll(viewPlugin.class.Viewlet, { + attachTo: { $in: allClasses }, + descriptor: viewPlugin.viewlet.Table + }) + viewlet = + viewlets.find((v) => v.attachTo === cardClass) ?? + viewlets.find((v) => allClasses.includes(v.attachTo)) ?? + viewlets[0] + } + + let actualConfig: Array | undefined + if (viewlet !== undefined) { + const preferences = await client.findAll(viewPlugin.class.ViewletPreference, { + space: core.space.Workspace, + attachedTo: viewlet._id + }) + actualConfig = preferences.length > 0 && preferences[0].config.length > 0 ? preferences[0].config : viewlet.config + } + + return { viewlet, config: actualConfig } +} diff --git a/plugins/converter-resources/src/plugin.ts b/plugins/converter-resources/src/plugin.ts new file mode 100644 index 0000000000..5992d78958 --- /dev/null +++ b/plugins/converter-resources/src/plugin.ts @@ -0,0 +1,19 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { mergeIds } from '@hcengineering/platform' +import converter, { converterId } from '@hcengineering/converter' + +export default mergeIds(converterId, converter, {}) diff --git a/plugins/converter-resources/src/types.ts b/plugins/converter-resources/src/types.ts new file mode 100644 index 0000000000..bee6456b54 --- /dev/null +++ b/plugins/converter-resources/src/types.ts @@ -0,0 +1,138 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Doc, Class, Ref, Hierarchy, DocumentQuery } from '@hcengineering/core' +import type { AttributeModel, BuildModelKey, Viewlet } from '@hcengineering/view' +import type { Resource } from '@hcengineering/platform' + +import { escapeMarkdownLinkText, escapeMarkdownLinkUrl } from './markdown/escape' + +/** + * Value formatter function for custom field extraction during markdown/export + * @param attr - The attribute model + * @param doc - The document object + * @param hierarchy - The hierarchy instance + * @param _class - The document class + * @param language - Current language + * @returns The formatted value, or undefined if this formatter doesn't apply + */ +export type ValueFormatter = ( + attr: AttributeModel, + doc: Doc, + hierarchy: Hierarchy, + _class: Ref>, + language: string | undefined +) => Promise + +/** + * Mixin for classes that provide a custom value formatter for markdown/export + */ +export interface MarkdownValueFormatter extends Class { + formatter: Resource +} + +/** + * Props for CopyAsMarkdownTable function + */ +export interface CopyAsMarkdownTableProps { + cardClass: Ref> + viewlet?: Viewlet + config?: Array + valueFormatter?: ValueFormatter + query?: DocumentQuery +} + +/** + * Interface for RelationshipTable's cell model + */ +export interface RelationshipCellModel { + attribute: AttributeModel + rowSpan: number + object: Doc | undefined + parentObject: Doc | undefined +} + +/** + * Interface for RelationshipTable's row model + */ +export interface RelationshipRowModel { + cells: RelationshipCellModel[] +} + +/** + * Props for CopyRelationshipTableAsMarkdown function + */ +export interface CopyRelationshipTableAsMarkdownProps { + viewModel: RelationshipRowModel[] + model: AttributeModel[] + objects: Doc[] + cardClass: Ref> + valueFormatter?: ValueFormatter + query?: DocumentQuery +} + +/** + * Function type for CopyAsMarkdownTable (used in action context) + */ +export type CopyAsMarkdownTableFunction = (evt: Event, props: CopyAsMarkdownTableProps) => Promise + +/** + * Function type for CopyRelationshipTableAsMarkdown + */ +export type CopyRelationshipTableAsMarkdownFunction = ( + evt: Event, + props: CopyRelationshipTableAsMarkdownProps +) => Promise + +/** + * Interface for a generic table converter to different formats. + */ +export interface TableConverter { + // Build table output from formatted rows + buildTable: (headers: string[], rows: string[][]) => string + + // Escape a cell value for this format + escapeValue: (value: string) => string + + // Create a link in this format + createLink: (url: string, text: string) => string + + // Format identifier (e.g., 'markdown', 'csv', 'html') + readonly format: string +} + +/** + * Markdown implementation of TableConverter. + */ +export class MarkdownTableConverter implements TableConverter { + readonly format = 'markdown' + + buildTable (headers: string[], rows: string[][]): string { + let markdown = '| ' + headers.join(' | ') + ' |\n' + markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n' + for (const row of rows) { + markdown += '| ' + row.join(' | ') + ' |\n' + } + return markdown + } + + escapeValue (value: string): string { + return escapeMarkdownLinkText(value) + } + + createLink (url: string, text: string): string { + return `[${escapeMarkdownLinkText(text)}](${escapeMarkdownLinkUrl(url)})` + } +} diff --git a/plugins/converter-resources/tsconfig.json b/plugins/converter-resources/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/plugins/converter-resources/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/plugins/converter/.eslintrc.js b/plugins/converter/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/plugins/converter/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/converter/config/rig.json b/plugins/converter/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/plugins/converter/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/plugins/converter/jest.config.js b/plugins/converter/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/converter/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/converter/package.json b/plugins/converter/package.json new file mode 100644 index 0000000000..9c4c5c9517 --- /dev/null +++ b/plugins/converter/package.json @@ -0,0 +1,44 @@ +{ + "name": "@hcengineering/converter", + "version": "0.7.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Copyright © Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.19", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/platform": "workspace:^0.7.19", + "@hcengineering/core": "workspace:^0.7.24", + "@hcengineering/view": "workspace:^0.7.0" + } +} diff --git a/plugins/converter/src/index.ts b/plugins/converter/src/index.ts new file mode 100644 index 0000000000..2e477cc74b --- /dev/null +++ b/plugins/converter/src/index.ts @@ -0,0 +1,22 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { converterPlugin, converterId } from './plugin' + +export * from './types' + +export { converterId } + +export default converterPlugin diff --git a/plugins/converter/src/plugin.ts b/plugins/converter/src/plugin.ts new file mode 100644 index 0000000000..dbcf9b3f16 --- /dev/null +++ b/plugins/converter/src/plugin.ts @@ -0,0 +1,50 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Client, Doc, Mixin, Ref } from '@hcengineering/core' +import { type Plugin, type Resource, plugin } from '@hcengineering/platform' +import type { Action, BuildMarkdownTableMetadata, ViewAction } from '@hcengineering/view' +import type { + CopyAsMarkdownTableFunction, + CopyRelationshipTableAsMarkdownFunction, + MarkdownValueFormatter, + CopyAsMarkdownTableProps +} from './types' + +export const converterId = 'converter' as Plugin + +export const converterPlugin = plugin(converterId, { + mixin: { + MarkdownValueFormatter: '' as Ref> + }, + function: { + CopyAsMarkdownTable: '' as Resource, + CopyRelationshipAsMarkdown: '' as Resource, + BuildMarkdownTableFromMetadata: '' as Resource< + (docs: Doc[], metadata: BuildMarkdownTableMetadata, client: Client) => Promise + >, + BuildMarkdownTableFromDocs: '' as Resource< + (docs: Doc[], props: CopyAsMarkdownTableProps, client: Client) => Promise + > + }, + action: { + CopyAsMarkdownTable: '' as Ref>> + }, + actionImpl: { + CopyAsMarkdownTable: '' as ViewAction + } +}) + +export default converterPlugin diff --git a/plugins/converter/src/types.ts b/plugins/converter/src/types.ts new file mode 100644 index 0000000000..50a3f1d742 --- /dev/null +++ b/plugins/converter/src/types.ts @@ -0,0 +1,95 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Class, Doc, DocumentQuery, Hierarchy, Ref } from '@hcengineering/core' +import type { Resource } from '@hcengineering/platform' +import type { AttributeModel, BuildModelKey, Viewlet } from '@hcengineering/view' + +/** + * Value formatter function for custom field extraction during markdown/export + * @param attr - The attribute model + * @param doc - The document object + * @param hierarchy - The hierarchy instance + * @param _class - The document class + * @param language - Current language + * @returns The formatted value, or undefined if this formatter doesn't apply + */ +export type ValueFormatter = ( + attr: AttributeModel, + doc: Doc, + hierarchy: Hierarchy, + _class: Ref>, + language: string | undefined +) => Promise + +/** + * Mixin for classes that provide a custom value formatter for markdown/export + */ +export interface MarkdownValueFormatter extends Class { + formatter: Resource +} + +/** + * Props for CopyAsMarkdownTable function + */ +export interface CopyAsMarkdownTableProps { + cardClass: Ref> + viewlet?: Viewlet + config?: Array + valueFormatter?: ValueFormatter + query?: DocumentQuery +} + +/** + * Interface for RelationshipTable's cell model + */ +export interface RelationshipCellModel { + attribute: AttributeModel + rowSpan: number + object: Doc | undefined + parentObject: Doc | undefined +} + +/** + * Interface for RelationshipTable's row model + */ +export interface RelationshipRowModel { + cells: RelationshipCellModel[] +} + +/** + * Props for CopyRelationshipTableAsMarkdown function + */ +export interface CopyRelationshipTableAsMarkdownProps { + viewModel: RelationshipRowModel[] + model: AttributeModel[] + objects: Doc[] + cardClass: Ref> + valueFormatter?: ValueFormatter + query?: DocumentQuery +} + +/** + * Function type for CopyAsMarkdownTable (used in action context) + */ +export type CopyAsMarkdownTableFunction = (evt: Event, props: CopyAsMarkdownTableProps) => Promise + +/** + * Function type for CopyRelationshipTableAsMarkdown + */ +export type CopyRelationshipTableAsMarkdownFunction = ( + evt: Event, + props: CopyRelationshipTableAsMarkdownProps +) => Promise diff --git a/plugins/converter/tsconfig.json b/plugins/converter/tsconfig.json new file mode 100644 index 0000000000..b5ae22f6e4 --- /dev/null +++ b/plugins/converter/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/plugins/text-editor-resources/package.json b/plugins/text-editor-resources/package.json index d2b072b0bf..aa438e696a 100644 --- a/plugins/text-editor-resources/package.json +++ b/plugins/text-editor-resources/package.json @@ -45,6 +45,7 @@ "@hcengineering/analytics": "workspace:^0.7.17", "@hcengineering/presentation": "workspace:^0.7.0", "@hcengineering/core": "workspace:^0.7.24", + "@hcengineering/converter": "workspace:^0.7.0", "@hcengineering/highlight": "workspace:^0.7.0", "@hcengineering/view": "workspace:^0.7.0", "@hcengineering/text": "workspace:^0.7.18", diff --git a/plugins/text-editor-resources/src/components/extension/table/refreshTable.ts b/plugins/text-editor-resources/src/components/extension/table/refreshTable.ts index 06567f0718..43ddd8ee6d 100644 --- a/plugins/text-editor-resources/src/components/extension/table/refreshTable.ts +++ b/plugins/text-editor-resources/src/components/extension/table/refreshTable.ts @@ -14,11 +14,12 @@ import type { Client, Doc } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' -import view, { type BuildMarkdownTableMetadata, type TableMetadata } from '@hcengineering/view' +import converter from '@hcengineering/converter' +import { type BuildMarkdownTableMetadata, type TableMetadata } from '@hcengineering/view' /** * Build markdown table string from documents and metadata - * Uses the extension point function from view-resources via view plugin + * Uses the extension point function from converter-resources via converter plugin */ export async function buildMarkdownTableFromDocs ( docs: Doc[], @@ -26,7 +27,7 @@ export async function buildMarkdownTableFromDocs ( client: Client ): Promise { try { - const buildFunction = await getResource(view.function.BuildMarkdownTableFromDocs) + const buildFunction = await getResource(converter.function.BuildMarkdownTableFromMetadata) // Extract only the BuildMarkdownTableMetadata fields from TableMetadata const buildMetadata: BuildMarkdownTableMetadata = { cardClass: metadata.cardClass, @@ -37,8 +38,8 @@ export async function buildMarkdownTableFromDocs ( } return await buildFunction(docs, buildMetadata, client) } catch (error) { - // Function not available (view-resources not loaded) - console.warn('BuildMarkdownTableFromDocs function not available:', error) + // Function not available (converter-resources not loaded) + console.warn('BuildMarkdownTableFromMetadata function not available:', error) return '' } } diff --git a/plugins/tracker-resources/package.json b/plugins/tracker-resources/package.json index 5340d5757c..2f023dfb15 100644 --- a/plugins/tracker-resources/package.json +++ b/plugins/tracker-resources/package.json @@ -70,6 +70,8 @@ "@hcengineering/ui": "workspace:^0.7.0", "@hcengineering/view": "workspace:^0.7.0", "@hcengineering/view-resources": "workspace:^0.7.0", + "@hcengineering/converter": "workspace:^0.7.0", + "@hcengineering/converter-resources": "workspace:^0.7.0", "@hcengineering/workbench": "workspace:^0.7.0", "@hcengineering/workbench-resources": "workspace:^0.7.0", "fast-equals": "^5.2.2", diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index 3ae81ee064..071a13ac73 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -101,7 +101,7 @@ import { resolveLocation } from './issues' import tracker from './plugin' -import './issueTableFormatter' +import { formatIssueValue } from './issueTableFormatter' import MilestoneEditor from './components/milestones/MilestoneEditor.svelte' import MilestonePresenter from './components/milestones/MilestonePresenter.svelte' @@ -521,7 +521,8 @@ export default async (): Promise => ({ GetIssueStatusCategories: getIssueStatusCategories, SetComponentStore: setStore, ComponentFilterFunction: filterComponents, - OpenIssuesOfTaskType: openIssuesOfTaskType + OpenIssuesOfTaskType: openIssuesOfTaskType, + FormatIssueMarkdownValue: formatIssueValue }, actionImpl: { Move: move, diff --git a/plugins/tracker-resources/src/issueTableFormatter.ts b/plugins/tracker-resources/src/issueTableFormatter.ts index 61470086ec..ec74c7de6f 100644 --- a/plugins/tracker-resources/src/issueTableFormatter.ts +++ b/plugins/tracker-resources/src/issueTableFormatter.ts @@ -17,7 +17,6 @@ import { type Class, type Doc, type Hierarchy, type Ref, type PersonId } from '@ import trackerPlugin, { type Component, type IssueStatus, type Milestone, type Project } from '@hcengineering/tracker' import { type AttributeModel } from '@hcengineering/view' import { getClient } from '@hcengineering/presentation' -import { registerValueFormatterForClass } from '@hcengineering/view-resources' import { getName, getPersonByPersonId } from '@hcengineering/contact' /** @@ -184,7 +183,7 @@ async function loadPersonName (personId: PersonId): Promise { * Value formatter for issue fields * Handles special cases for status, assignee, component, space (project), and milestone fields */ -async function formatIssueValue ( +export async function formatIssueValue ( attr: AttributeModel, card: Doc, hierarchy: Hierarchy, @@ -312,5 +311,4 @@ async function formatIssueValue ( return undefined } -// Register the formatter for Issue class -registerValueFormatterForClass(trackerPlugin.class.Issue, formatIssueValue) +// Formatter is registered via MarkdownValueFormatter mixin in models/tracker diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index a91d079f93..4d767bad2e 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -29,6 +29,7 @@ import { type Viewlet, type ViewletDescriptor } from '@hcengineering/view' +import type { ValueFormatter } from '@hcengineering/converter' export default mergeIds(trackerId, tracker, { viewlet: { @@ -398,7 +399,8 @@ export default mergeIds(trackerId, tracker, { IssueChatTitleProvider: '' as Resource<(object: Doc) => string>, GetIssueStatusCategories: '' as Resource<(project: ProjectType) => Array>>, GetIssueIdByIdentifier: '' as Resource<(id: string) => Promise | undefined>>, - OpenIssuesOfTaskType: '' as Resource<(taskType: TaskType) => Promise> + OpenIssuesOfTaskType: '' as Resource<(taskType: TaskType) => Promise>, + FormatIssueMarkdownValue: '' as Resource }, aggregation: { CreateComponentAggregationManager: '' as CreateAggregationManagerFunc, diff --git a/plugins/view-resources/package.json b/plugins/view-resources/package.json index 8a87a91c29..5bef5c43a1 100644 --- a/plugins/view-resources/package.json +++ b/plugins/view-resources/package.json @@ -47,6 +47,7 @@ "@hcengineering/panel": "workspace:^0.7.0", "@hcengineering/guest": "workspace:^0.7.0", "@hcengineering/core": "workspace:^0.7.24", + "@hcengineering/converter": "workspace:^0.7.0", "@hcengineering/view": "workspace:^0.7.0", "@hcengineering/ui": "workspace:^0.7.0", "@hcengineering/task": "workspace:^0.7.0", diff --git a/plugins/view-resources/src/__tests__/copyAsMarkdownTable.test.ts b/plugins/view-resources/src/__tests__/copyAsMarkdownTable.test.ts deleted file mode 100644 index 639beef7b0..0000000000 --- a/plugins/view-resources/src/__tests__/copyAsMarkdownTable.test.ts +++ /dev/null @@ -1,446 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -// Mock platform plugin function first (before any imports) -// Import after mocks are set up -import { CopyAsMarkdownTable } from '../copyAsMarkdownTable' -import { isIntlString } from '../markdownTableUtils' -import core, { type Class, type Doc, type Ref } from '@hcengineering/core' -import { type IntlString } from '@hcengineering/platform' -import { getClient } from '@hcengineering/presentation' -import { getCurrentLanguage } from '@hcengineering/theme' -import { copyMarkdown } from '../actionImpl' -import { addNotification } from '@hcengineering/ui' -import { buildModel } from '../utils' - -jest.mock('@hcengineering/platform', () => { - const actual = jest.requireActual('@hcengineering/platform') - return { - ...actual, - plugin: jest.fn((id: string, def: any) => def), - translate: jest.fn(async (str: any) => await Promise.resolve(`translated:${String(str)}`)), - getMetadata: jest.fn((key: unknown) => { - if (key != null && String(key).includes('FrontUrl')) { - return 'http://huly.local:8080' - } - return undefined - }) - } -}) - -jest.mock('@hcengineering/presentation', () => ({ - getClient: jest.fn() -})) - -jest.mock('@hcengineering/theme', () => ({ - getCurrentLanguage: jest.fn(() => 'en') -})) - -jest.mock('../actionImpl', () => ({ - copyText: jest.fn(), - copyMarkdown: jest.fn() -})) - -jest.mock('@hcengineering/ui', () => ({ - addNotification: jest.fn(), - NotificationSeverity: { - Success: 'success' - }, - locationToUrl: jest.fn((loc: unknown) => { - if (loc != null && typeof loc === 'object' && 'path' in loc && Array.isArray((loc as { path: string[] }).path)) { - return (loc as { path: string[] }).path.join('/') - } - return 'workbench/w3/card/test-id' - }) -})) - -const mockGetObjectLinkFragment = jest.fn() -jest.mock('../utils', () => ({ - buildModel: jest.fn(), - buildConfigLookup: jest.fn(() => ({})), - getObjectLinkFragment: (...args: any[]) => mockGetObjectLinkFragment(...args) -})) - -jest.mock('../plugin', () => ({ - default: { - component: { - EditDoc: 'view:component:EditDoc' - } - }, - string: { - Copied: 'view:string:Copied' as any, - TableCopiedToClipboard: 'view:string:TableCopiedToClipboard' as any, - TableCopyFailed: 'view:string:TableCopyFailed' as any - } -})) - -jest.mock('../components/SimpleNotification.svelte', () => ({ - default: jest.fn() -})) - -// We'll use jest.spyOn in individual tests to mock these functions - -describe('copyAsMarkdownTable', () => { - let mockClient: any - let mockHierarchy: any - let mockCardClass: any - let mockDoc: Doc - - beforeEach(() => { - jest.clearAllMocks() - mockGetObjectLinkFragment.mockReset() - - mockCardClass = { - _id: 'card:class:Card' as Ref>, - label: 'card:string:Card' as IntlString - } - - mockHierarchy = { - getClass: jest.fn((ref: Ref>): any => { - if (ref === 'card:class:Card') { - return mockCardClass - } - return null - }), - findAttribute: jest.fn(() => ({ - type: { - _class: core.class.TypeString - } - })), - as: jest.fn((doc: Doc): Doc => doc) - } - - mockClient = { - getHierarchy: jest.fn(() => mockHierarchy), - getModel: jest.fn(() => ({ - findAllSync: jest.fn(() => []) - })), - findAll: jest.fn(async () => []) - } - - const mockDocValue = { - _id: 'doc1', - _class: 'card:class:Card', - title: 'Test Card', - masterTag: 'card:types:Document', - tags: ['tag1', 'tag2'], - createdOn: 1732178763949, - modifiedOn: 1732178770252 - } as unknown as Doc - mockDoc = mockDocValue - ;(getClient as jest.Mock).mockReturnValue(mockClient) - ;(getCurrentLanguage as jest.Mock).mockReturnValue('en') - void (buildModel as jest.Mock).mockResolvedValue([ - { - key: '', - label: 'card:string:Card' as IntlString, - displayProps: {} - }, - { - key: 'masterTag', - label: 'card:string:MasterTag' as IntlString, - displayProps: {} - }, - { - key: 'tags', - label: 'card:string:Tags' as IntlString, - displayProps: {} - }, - { - key: 'modifiedOn', - label: 'core:string:ModifiedDate' as IntlString, - displayProps: {} - } - ]) - }) - - describe('CopyAsMarkdownTable', () => { - it('should return early if no docs provided', async () => { - const undefinedDoc: Doc = undefined as unknown as Doc - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(undefinedDoc, mockEvent, { - cardClass - }) - - expect(getClient).not.toHaveBeenCalled() - }) - - it('should return early if card class not found', async () => { - mockHierarchy.getClass.mockReturnValue(null) - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(mockDoc, mockEvent, { - cardClass - }) - - expect(copyMarkdown).not.toHaveBeenCalled() - }) - - it('should return early if displayableModel is empty', async () => { - void (buildModel as jest.Mock).mockResolvedValue([]) - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(mockDoc, mockEvent, { - cardClass - }) - - expect(copyMarkdown).not.toHaveBeenCalled() - }) - - it('should copy markdown table and show notification', async () => { - // getObjectValue and getDisplayTime will use their actual implementations - mockHierarchy.findAttribute.mockReturnValue({ - type: { - _class: core.class.TypeTimestamp - } - }) - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(mockDoc, mockEvent, { - cardClass - }) - - expect(copyMarkdown).toHaveBeenCalled() - expect(addNotification).toHaveBeenCalledWith( - 'translated:view:string:Copied', - 'translated:view:string:TableCopiedToClipboard', - expect.objectContaining({ default: expect.any(Function) }), - undefined, - 'success' - ) - - const markdownCall = (copyMarkdown as jest.Mock).mock.calls[0][0] - // copyMarkdown receives markdown as first argument, metadata as second - // The markdown may have metadata comment prepended, so check for table content - const markdownContent = typeof markdownCall === 'string' ? markdownCall : '' - expect(markdownContent).toContain('|') - expect(markdownContent).toContain('---') - expect(markdownContent).toContain('translated:card:string:Card') - expect(markdownContent).toContain('translated:card:string:MasterTag') - }) - - it('should translate IntlString values in table', async () => { - const docWithIntlString = { - ...mockDoc, - masterTag: 'card:types:Document' - } as unknown as Doc - - // getObjectValue will use its actual implementation - mockHierarchy.findAttribute.mockReturnValue({ - type: { - _class: core.class.TypeString - } - }) - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(docWithIntlString, mockEvent, { - cardClass - }) - - expect(copyMarkdown).toHaveBeenCalled() - const markdownCall = (copyMarkdown as jest.Mock).mock.calls[0][0] - const markdownContent = typeof markdownCall === 'string' ? markdownCall : '' - expect(markdownContent).toContain('translated:card:types:Document') - }) - - it('should handle multiple docs', async () => { - const mockDoc2 = { - ...mockDoc, - _id: 'doc2', - title: 'Test Card 2' - } as unknown as Doc - - // getObjectValue will use its actual implementation - mockHierarchy.findAttribute.mockReturnValue({ - type: { - _class: core.class.TypeString - } - }) - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable([mockDoc, mockDoc2], mockEvent, { - cardClass - }) - - expect(copyMarkdown).toHaveBeenCalled() - const markdownCall = (copyMarkdown as jest.Mock).mock.calls[0][0] - const markdownContent = typeof markdownCall === 'string' ? markdownCall : '' - const lines = markdownContent.split('\n').filter((line: string) => line.trim().length > 0) - expect(lines.length > 3).toBe(true) - }) - - it('should escape pipe characters and newlines in values', async () => { - const docWithSpecialChars = { - ...mockDoc, - title: 'Test | Card\nWith Newline' - } as unknown as Doc - - // getObjectValue will use its actual implementation - mockHierarchy.findAttribute.mockReturnValue({ - type: { - _class: core.class.TypeString - } - }) - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(docWithSpecialChars, mockEvent, { - cardClass - }) - - expect(copyMarkdown).toHaveBeenCalled() - const markdownCall = (copyMarkdown as jest.Mock).mock.calls[0][0] - const markdownContent = typeof markdownCall === 'string' ? markdownCall : '' - // Check that pipe is escaped - expect(markdownContent).toContain('\\|') - // Check that newlines in data are replaced with spaces - // The markdown table itself has newlines between rows, so we check data rows specifically - const dataRows = markdownContent.split('\n').filter((line: string) => line.includes('|') && !line.includes('---')) - dataRows.forEach((row: string) => { - // Each cell should not contain literal newline characters - const cells = row.split('|').map((cell: string) => cell.trim()) - cells.forEach((cell: string) => { - expect(cell).not.toContain('\n') - }) - }) - }) - - it('should handle empty array of docs', async () => { - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable([], mockEvent, { - cardClass - }) - - expect(getClient).not.toHaveBeenCalled() - }) - - it('should create markdown link for first column with empty key', async () => { - const mockLocation = { - path: ['workbench', 'w3', 'card', 'test-doc-id'], - fragment: undefined, - query: undefined - } - - mockGetObjectLinkFragment.mockResolvedValue(mockLocation) - - const uiModule = await import('@hcengineering/ui') - const mockLocationToUrl = uiModule.locationToUrl as jest.Mock - mockLocationToUrl.mockReturnValue('workbench/w3/card/test-doc-id') - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(mockDoc, mockEvent, { - cardClass - }) - - expect(copyMarkdown).toHaveBeenCalled() - const markdownCall = (copyMarkdown as jest.Mock).mock.calls[0][0] - const markdownContent = typeof markdownCall === 'string' ? markdownCall : '' - // Check that the first column contains a markdown link with full URL - // Note: getObjectLinkFragment may not be called if the condition isn't met, - // but we should still check the markdown output - if (mockGetObjectLinkFragment.mock.calls.length > 0) { - expect(mockGetObjectLinkFragment).toHaveBeenCalled() - expect(markdownContent).toMatch(/\[.*\]\(http:\/\/huly\.local:8080\/.*\)/) - expect(markdownContent).toContain('http://huly.local:8080') - } else { - // If link wasn't created, verify the markdown still contains the title - expect(markdownContent).toContain('Test Card') - } - }) - - it('should not create link for non-first column even with empty key', async () => { - // Create a model where the second column has an empty key (shouldn't happen in practice, but test the logic) - ;(buildModel as jest.Mock).mockResolvedValue([ - { - key: 'someKey', - label: 'card:string:SomeKey' as IntlString, - displayProps: {} - }, - { - key: '', - label: 'card:string:Title' as IntlString, - displayProps: {} - } - ]) - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(mockDoc, mockEvent, { - cardClass - }) - - expect(copyMarkdown).toHaveBeenCalled() - const markdownCall = (copyMarkdown as jest.Mock).mock.calls[0][0] - const markdownContent = typeof markdownCall === 'string' ? markdownCall : '' - // Should not contain markdown links (no empty key in first column) - expect(markdownContent).not.toMatch(/\[.*\]\(http:\/\/.*\)/) - }) - - it('should fall back to plain text if link generation fails', async () => { - mockGetObjectLinkFragment.mockRejectedValue(new Error('Link generation failed')) - - const mockEvent: Event = new Event('test') - const cardClass: Ref> = 'card:class:Card' as Ref> - await CopyAsMarkdownTable(mockDoc, mockEvent, { - cardClass - }) - - expect(copyMarkdown).toHaveBeenCalled() - const markdownCall = (copyMarkdown as jest.Mock).mock.calls[0][0] - const markdownContent = typeof markdownCall === 'string' ? markdownCall : '' - // Should not contain markdown links (fallback to plain text) - expect(markdownContent).not.toMatch(/\[.*\]\(http:\/\/.*\)/) - // Should contain the title as plain text - expect(markdownContent).toContain('Test Card') - }) - }) - - describe('isIntlString', () => { - it('should detect valid IntlString format strings', () => { - const testCases = [ - { input: 'card:string:Card', expected: true }, - { input: 'contact:class:UserProfile', expected: true }, - { input: 'card:types:Document', expected: true }, - { input: 'plugin:kind:key:subkey', expected: true }, - { input: 'simple string', expected: false }, - { input: 'plugin:key', expected: false }, - { input: 'plugin:', expected: false }, - { input: ':kind:key', expected: false }, - { input: 'plugin:kind:', expected: false }, - { input: '', expected: false }, - { input: 'plugin::key', expected: false } - ] - - testCases.forEach(({ input, expected }) => { - expect(isIntlString(input)).toBe(expected) - }) - }) - - it('should return false for non-string values', () => { - expect(isIntlString(null as unknown as string)).toBe(false) - expect(isIntlString(undefined as unknown as string)).toBe(false) - expect(isIntlString(123 as unknown as string)).toBe(false) - expect(isIntlString({} as unknown as string)).toBe(false) - }) - }) -}) diff --git a/plugins/view-resources/src/actionImpl.ts b/plugins/view-resources/src/actionImpl.ts index 65b5199fc7..1dc3f363ca 100644 --- a/plugins/view-resources/src/actionImpl.ts +++ b/plugins/view-resources/src/actionImpl.ts @@ -50,7 +50,7 @@ import { } from './selection' import { deleteObjects, getObjectId, getObjectLinkFragment, restrictionStore } from './utils' import workbenchPlugin from '@hcengineering/workbench' -import { CopyAsMarkdownTable } from './copyAsMarkdownTable' +import converter from '@hcengineering/converter' import { viewletContextStore } from './viewletContextStore' /** @@ -744,7 +744,8 @@ async function CopyAsMarkdownTableAction ( viewOptions: props.viewOptions ?? viewletContext?.viewOptions } - await CopyAsMarkdownTable(doc, evt, mergedProps) + const copyFn = await getResource(converter.actionImpl.CopyAsMarkdownTable) + await copyFn(doc, evt, mergedProps) } /** diff --git a/plugins/view-resources/src/components/RelationshipTable.svelte b/plugins/view-resources/src/components/RelationshipTable.svelte index 398877a95f..1876368e6e 100644 --- a/plugins/view-resources/src/components/RelationshipTable.svelte +++ b/plugins/view-resources/src/components/RelationshipTable.svelte @@ -51,7 +51,7 @@ import view from '../plugin' import { buildConfigAssociation, buildConfigLookup, buildModel, getAttributeValue, restrictionStore } from '../utils' import { getResultOptions, getResultQuery } from '../viewOptions' - import { CopyRelationshipTableAsMarkdown } from '../copyAsMarkdownTable' + import converter from '@hcengineering/converter' import IconUpDown from './icons/UpDown.svelte' import RelationsSelectorPopup from './RelationsSelectorPopup.svelte' @@ -541,7 +541,8 @@ async function handleCopyAsMarkdown (e: MouseEvent): Promise { if (model === undefined || viewModel.length === 0) return - await CopyRelationshipTableAsMarkdown(e, { + const copyFn = await getResource(converter.function.CopyRelationshipAsMarkdown) + await copyFn(e, { viewModel, model, objects, diff --git a/plugins/view-resources/src/copyAsMarkdownTable.ts b/plugins/view-resources/src/copyAsMarkdownTable.ts deleted file mode 100644 index 1671259374..0000000000 --- a/plugins/view-resources/src/copyAsMarkdownTable.ts +++ /dev/null @@ -1,1072 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type Class, - type Client, - type Doc, - type DocumentQuery, - type Hierarchy, - type Ref, - type PersonId, - concatLink, - getDisplayTime, - getObjectValue -} from '@hcengineering/core' -import { translate, type IntlString, getMetadata } from '@hcengineering/platform' -import { addNotification, NotificationSeverity, locationToUrl, getCurrentResolvedLocation } from '@hcengineering/ui' -import { getCurrentLanguage } from '@hcengineering/theme' -import viewPlugin, { - type Viewlet, - type AttributeModel, - type BuildModelKey, - type BuildMarkdownTableMetadata, - type TableMetadata -} from '@hcengineering/view' -import presentation, { getClient } from '@hcengineering/presentation' -import { getName, getPersonByPersonId } from '@hcengineering/contact' -import { buildModel, buildConfigLookup, buildConfigAssociation, getAttributeValue } from './utils' -import { - generateHeaders, - modelToConfig, - formatArrayValue, - extractObjectTitleOrName, - formatCustomAttributeValue, - escapeMarkdownLinkText, - createMarkdownLink, - isIntlString -} from './markdownTableUtils' -import view from './plugin' -import SimpleNotification from './components/SimpleNotification.svelte' -import { copyMarkdown } from './actionImpl' - -/** - * Value formatter function for custom field extraction - * @param attr - The attribute model - * @param card - The document object - * @param hierarchy - The hierarchy instance - * @param _class - The document class - * @param language - Current language - * @returns The formatted value, or undefined if this formatter doesn't apply - */ -export type ValueFormatter = ( - attr: AttributeModel, - card: Doc, - hierarchy: Hierarchy, - _class: Ref>, - language: string | undefined -) => Promise - -/** - * Registry for value formatters by document class - * Plugins can register custom formatters for specific document classes - */ -const valueFormattersByClass = new Map>, ValueFormatter[]>() - -/** - * Global formatters (checked for all classes) - */ -const globalValueFormatters: ValueFormatter[] = [] - -/** - * Register a value formatter for a specific document class - * @param _class - The document class this formatter applies to - * @param formatter - The formatter function to register - */ -export function registerValueFormatterForClass (_class: Ref>, formatter: ValueFormatter): void { - const formatters = valueFormattersByClass.get(_class) ?? [] - formatters.push(formatter) - valueFormattersByClass.set(_class, formatters) -} - -/** - * Register a global value formatter (applies to all classes) - * @param formatter - The formatter function to register - * @deprecated Use registerValueFormatterForClass for better performance and explicit class association - */ -export function registerValueFormatter (formatter: ValueFormatter): void { - globalValueFormatters.push(formatter) -} - -/** - * Get formatters for a specific class (including parent classes) - */ -function getFormattersForClass (hierarchy: Hierarchy, _class: Ref>): ValueFormatter[] { - const formatters: ValueFormatter[] = [] - - // Get formatters for this class and all parent classes - let currentClass: Ref> | undefined = _class - while (currentClass !== undefined) { - const classFormatters = valueFormattersByClass.get(currentClass) - if (classFormatters !== undefined) { - formatters.push(...classFormatters) - } - const classDef: Class | undefined = hierarchy.getClass(currentClass) - currentClass = classDef?.extends - } - - // Add global formatters - formatters.push(...globalValueFormatters) - - return formatters -} - -enum DocumentAttributeKey { - CreatedBy = 'createdBy', - CreatedOn = 'createdOn', - ModifiedBy = 'modifiedBy', - ModifiedOn = 'modifiedOn', - Title = 'title', - Name = 'name' -} - -enum DateFormatOption { - Numeric = 'numeric', - Short = 'short' -} - -async function buildTableModel ( - client: Client, - hierarchy: Hierarchy, - _class: Ref>, - viewlet: Viewlet | undefined -): Promise { - if (viewlet !== undefined) { - const preferences = await client.findAll(viewPlugin.class.ViewletPreference, { - space: core.space.Workspace, - attachedTo: viewlet._id - }) - const config = preferences.length > 0 && preferences[0].config.length > 0 ? preferences[0].config : viewlet.config - - const lookup = buildConfigLookup(hierarchy, _class, config, viewlet.options?.lookup) - const hiddenKeys = viewlet.configOptions?.hiddenKeys ?? [] - const model = await buildModel({ - client, - _class, - keys: config.filter((key: string | BuildModelKey) => { - if (typeof key === 'string') { - return !hiddenKeys.includes(key) - } - return !hiddenKeys.includes(key.key) && key.displayProps?.grow !== true - }), - lookup - }) - - return model.filter((attr) => attr.displayProps?.grow !== true) - } - - const defaultConfig: Array = [ - '', // Object presenter (title) - DocumentAttributeKey.CreatedBy, - DocumentAttributeKey.CreatedOn, - DocumentAttributeKey.ModifiedBy, - DocumentAttributeKey.ModifiedOn - ] - - const model = await buildModel({ - client, - _class, - keys: defaultConfig, - lookup: undefined - }) - - return model.filter((attr) => { - if ( - attr.key === DocumentAttributeKey.CreatedBy || - attr.key === DocumentAttributeKey.CreatedOn || - attr.key === DocumentAttributeKey.ModifiedBy || - attr.key === DocumentAttributeKey.ModifiedOn - ) { - return hierarchy.findAttribute(_class, attr.key) !== undefined - } - return true - }) -} - -/** - * Check if a string looks like an IntlString (format: plugin:kind:key) - * Examples: card:string:Card, contact:class:UserProfile, card:types:Document - * @public - */ - -async function loadPersonName ( - personId: PersonId, - hierarchy: Hierarchy, - userCache?: Map -): Promise { - if (userCache !== undefined) { - const cachedName = userCache.get(personId) - if (cachedName !== undefined) { - return cachedName - } - } - - try { - const client = getClient() - const person = await getPersonByPersonId(client, personId) - if (person !== null) { - const name = getName(hierarchy, person) - if (userCache !== undefined) { - userCache.set(personId, name) - } - return name - } - } catch (error) { - console.warn('Failed to lookup user name for PersonId:', personId, error) - } - - return personId -} - -/** - * Loads the actual viewlet configuration, including user preferences - * @param client - The client instance - * @param hierarchy - The hierarchy instance - * @param cardClass - The class to find viewlet for - * @param propsViewlet - Optional viewlet from props - * @param propsConfig - Optional config from props - * @returns The actual config to use, or undefined if no viewlet/config found - */ -async function loadViewletConfig ( - client: Client, - hierarchy: Hierarchy, - cardClass: Ref>, - propsViewlet?: Viewlet, - propsConfig?: Array -): Promise<{ viewlet: Viewlet | undefined, config: Array | undefined }> { - // If config is provided directly, use it - if (propsConfig !== undefined && propsConfig.length > 0) { - return { viewlet: propsViewlet, config: propsConfig } - } - - // Find viewlet if not provided - let viewlet: Viewlet | undefined = propsViewlet - if (viewlet === undefined) { - // Search for viewlets attached to this class or any of its ancestor classes - // Viewlets attached to a parent class apply to child classes - const allClasses = [cardClass] - let currentClass = hierarchy.getClass(cardClass) - while (currentClass?.extends !== undefined) { - allClasses.push(currentClass.extends) - currentClass = hierarchy.getClass(currentClass.extends) - } - const viewlets = await client.findAll(viewPlugin.class.Viewlet, { - attachTo: { $in: allClasses }, - descriptor: viewPlugin.viewlet.Table - }) - // Prefer viewlet attached directly to the class, then parent classes - viewlet = - viewlets.find((v) => v.attachTo === cardClass) ?? - viewlets.find((v) => allClasses.includes(v.attachTo)) ?? - viewlets[0] - } - - // Get user's viewlet preference to use the actual displayed config - let actualConfig: Array | undefined - if (viewlet !== undefined) { - const preferences = await client.findAll(viewPlugin.class.ViewletPreference, { - space: core.space.Workspace, - attachedTo: viewlet._id - }) - // Use preference config if available, otherwise fall back to viewlet config - actualConfig = preferences.length > 0 && preferences[0].config.length > 0 ? preferences[0].config : viewlet.config - } - - return { viewlet, config: actualConfig } -} - -async function formatValue ( - attr: AttributeModel, - card: Doc, - hierarchy: Hierarchy, - _class: Ref>, - language: string | undefined, - isFirstColumn: boolean = false, - userCache?: Map, - customFormatter?: ValueFormatter -): Promise { - // Try custom formatter first (from actionProps) - if (customFormatter !== undefined) { - const formattedValue = await customFormatter(attr, card, hierarchy, _class, language) - if (formattedValue !== undefined) { - return formattedValue - } - } - - // Try registered value formatters for this class - const formatters = getFormattersForClass(hierarchy, _class) - for (const formatter of formatters) { - const formattedValue = await formatter(attr, card, hierarchy, _class, language) - if (formattedValue !== undefined) { - return formattedValue - } - } - - let value: any - if (attr.castRequest != null) { - value = getObjectValue(attr.key.substring(attr.castRequest.length + 1), hierarchy.as(card, attr.castRequest)) - } else { - // Handle lookup keys properly - if (attr.key.startsWith('$lookup.')) { - const lookupKey = attr.key.replace('$lookup.', '') - const lookupParts = lookupKey.split('.') - const cardWithLookup = card as any - const lookupObj = cardWithLookup.$lookup?.[lookupParts[0]] - if (lookupObj !== undefined && lookupObj !== null) { - if (lookupParts.length > 1) { - value = getObjectValue(lookupParts.slice(1).join('.'), lookupObj) - } else { - value = lookupObj - } - } else { - value = undefined - } - } else { - value = getObjectValue(attr.key, card) - } - } - - // Handle custom attributes that failed to build properly in the model - // These have key: '' but the actual attribute name is in the label - if (attr.key === '' && !isFirstColumn) { - const labelStr = typeof attr.label === 'string' ? attr.label : '' - const isCustomAttribute = labelStr.startsWith('custom') - - if (isCustomAttribute) { - const customValue = (card as any)[labelStr] - if (customValue === null || customValue === undefined) { - return '' - } - - const docClass = card._class - let customAttr = hierarchy.findAttribute(docClass, labelStr) - - if (customAttr === undefined) { - const allAttrs = hierarchy.getAllAttributes(docClass) - customAttr = allAttrs.get(labelStr) - } - - return await formatCustomAttributeValue(customValue, customAttr, card, hierarchy, language) - } - - return '' - } - - if (value === null || value === undefined) { - return '' - } - - // Use attribute from model if available, otherwise try to find it - const attribute = attr.attribute ?? hierarchy.findAttribute(_class, attr.key) - const attrType = attribute?.type - - if (typeof value === 'number' && attrType?._class === core.class.TypeTimestamp) { - return getDisplayTime(value) - } - - if (value instanceof Date) { - const options: Intl.DateTimeFormatOptions = { - year: DateFormatOption.Numeric, - month: DateFormatOption.Short, - day: DateFormatOption.Numeric - } - return value.toLocaleDateString(language ?? 'default', options) - } - - if (typeof value === 'number' || typeof value === 'boolean') { - return String(value) - } - - if (typeof value === 'string') { - const isRef = attrType?._class === core.class.RefTo - if (isRef) { - const cardWithLookup = card as any - const lookupData = cardWithLookup.$lookup?.[attr.key] - if (lookupData !== undefined && lookupData !== null) { - const resolvedObj = lookupData - if (typeof resolvedObj === 'object' && resolvedObj !== null && 'title' in resolvedObj) { - const title = resolvedObj[DocumentAttributeKey.Title] ?? '' - if (typeof title === 'string' && isIntlString(title)) { - return await translate(title as unknown as IntlString, {}, language) - } - return String(title) - } - } - } - - if (isIntlString(value)) { - return await translate(value as unknown as IntlString, {}, language) - } - if (attr.key === DocumentAttributeKey.CreatedBy || attr.key === DocumentAttributeKey.ModifiedBy) { - return await loadPersonName(value as PersonId, hierarchy, userCache) - } - return value - } - - if (Array.isArray(value)) { - return await formatArrayValue(value, attrType, attribute, attr.key, card, language) - } - - if (typeof value === 'object' && value !== null) { - const obj = value as Record - const titleOrName = await extractObjectTitleOrName(obj, language) - return titleOrName !== '' ? titleOrName : String(value) - } - - return String(value) -} - -export interface CopyAsMarkdownTableProps { - cardClass: Ref> - viewlet?: Viewlet - config?: Array - valueFormatter?: ValueFormatter - query?: DocumentQuery // Original query used to fetch documents -} - -/** - * Interface for RelationshipTable's row and cell models - */ -export interface RelationshipCellModel { - attribute: AttributeModel - rowSpan: number - object: Doc | undefined - parentObject: Doc | undefined -} - -export interface RelationshipRowModel { - cells: RelationshipCellModel[] -} - -export interface CopyRelationshipTableAsMarkdownProps { - viewModel: RelationshipRowModel[] - model: AttributeModel[] - objects: Doc[] - cardClass: Ref> - valueFormatter?: ValueFormatter - query?: DocumentQuery // Original query used to fetch documents -} - -/** - * Build metadata object from props and documents - * If viewlet is not provided, tries to find a default viewlet for the class - */ -async function buildTableMetadata ( - props: CopyAsMarkdownTableProps, - docs: Doc[], - client?: Client -): Promise { - // If viewlet is not provided, try to find a default viewlet for the class - let viewletId: Ref | undefined = props.viewlet?._id - if (viewletId === undefined && client !== undefined) { - const { viewlet } = await loadViewletConfig(client, client.getHierarchy(), props.cardClass, undefined, props.config) - viewletId = viewlet?._id - } - - // Capture the original URL of the current page/view - let originalUrl: string | undefined - try { - const currentLocation = getCurrentResolvedLocation() - const relativeUrl = locationToUrl(currentLocation) - const frontUrl = - getMetadata(presentation.metadata.FrontUrl) ?? (typeof window !== 'undefined' ? window.location.origin : '') - originalUrl = concatLink(frontUrl, relativeUrl) - } catch (error) { - // If URL capture fails, continue without it - console.warn('Failed to capture original URL for table metadata:', error) - } - - return { - version: '1.0', - cardClass: props.cardClass, - viewletId, - config: props.config, - query: props.query, - documentIds: docs.map((d) => d._id), - timestamp: Date.now(), - originalUrl - } -} - -/** - * Check if a table metadata represents a relationship table - * Relationship tables have viewletId: undefined - */ -export function isRelationshipTable (metadata: TableMetadata): boolean { - return metadata.viewletId === undefined -} - -/** - * Build metadata object for relationship tables - */ -export function buildRelationshipTableMetadata ( - props: CopyRelationshipTableAsMarkdownProps, - docs: Doc[] -): TableMetadata { - let originalUrl: string | undefined - try { - const currentLocation = getCurrentResolvedLocation() - const relativeUrl = locationToUrl(currentLocation) - const frontUrl = - getMetadata(presentation.metadata.FrontUrl) ?? (typeof window !== 'undefined' ? window.location.origin : '') - originalUrl = concatLink(frontUrl, relativeUrl) - } catch (error) { - console.warn('Failed to capture original URL for relationship table metadata:', error) - } - - return { - version: '1.0', - cardClass: props.cardClass, - viewletId: undefined, // Relationship tables don't use viewlets - config: modelToConfig(props.model), // Preserve custom attributes by converting model to config - query: props.query, - documentIds: docs.map((d) => d._id), - timestamp: Date.now(), - originalUrl - } -} - -/** - * Rebuild relationship table viewModel from documents and metadata - * Recreates the hierarchical structure with row spans and separate rows for each associated child - */ -async function rebuildRelationshipTableViewModel ( - docs: Doc[], - model: AttributeModel[], - cardClass: Ref>, - hierarchy: Hierarchy, - client: Client -): Promise { - const viewModel: RelationshipRowModel[] = [] - - // Build association queries from config to fetch associations - const config = model.map((m) => m.key) - const associations = buildConfigAssociation(config) - const lookup = buildConfigLookup(hierarchy, cardClass, config) - - const associationAttrs = model.filter((attr) => attr.key.startsWith('$associations')) - - // Fetch documents with associations if needed - let docsWithAssociations: Doc[] = docs - if (associations !== undefined && associations.length > 0) { - // Re-fetch documents with associations - const docIds = docs.map((d) => d._id) - const query = { _id: { $in: docIds } } - docsWithAssociations = await client.findAll(cardClass, query, { lookup, associations }) - } - - // Process each parent document and create rows with proper hierarchy - for (const parentDoc of docsWithAssociations) { - const docWithAssoc = parentDoc as any - const parentAssociations = docWithAssoc.$associations ?? {} - - // Find the maximum number of children across all association columns - let maxChildren = 0 - for (const assocAttr of associationAttrs) { - const assocKey = assocAttr.key.replace('$associations.', '') - const children = parentAssociations[assocKey] - if (Array.isArray(children)) { - maxChildren = Math.max(maxChildren, children.length) - } else if (children !== undefined && children !== null) { - maxChildren = Math.max(maxChildren, 1) - } - } - - // If no children, create a single row for the parent - if (maxChildren === 0) { - const cells: RelationshipCellModel[] = [] - for (const attr of model) { - const isAssociationKey = attr.key.startsWith('$associations') - cells.push({ - attribute: attr, - rowSpan: 1, - object: isAssociationKey ? undefined : parentDoc, - parentObject: isAssociationKey ? parentDoc : undefined - }) - } - viewModel.push({ cells }) - continue - } - - // Create rows: first row has parent with rowSpan, then one row per child - for (let childIndex = 0; childIndex < maxChildren; childIndex++) { - const cells: RelationshipCellModel[] = [] - - for (const attr of model) { - const isAssociationKey = attr.key.startsWith('$associations') - - if (attr.key === '') { - // First column: parent document with row span - cells.push({ - attribute: attr, - rowSpan: maxChildren, // Span across all child rows - object: parentDoc, - parentObject: undefined - }) - } else if (isAssociationKey) { - // Association column: show child at current index - const assocKey = attr.key.replace('$associations.', '') - const children = parentAssociations[assocKey] - let childDoc: Doc | undefined - if (Array.isArray(children) && children.length > childIndex) { - childDoc = children[childIndex] as Doc - } else if (!Array.isArray(children) && children !== undefined && children !== null && childIndex === 0) { - childDoc = children as Doc - } - - cells.push({ - attribute: attr, - rowSpan: 1, - object: childDoc, - parentObject: parentDoc - }) - } else { - // Regular attribute: show parent value only in first row - // In subsequent rows, this will be empty (handled by row span logic in markdown generation) - cells.push({ - attribute: attr, - rowSpan: 1, - object: childIndex === 0 ? parentDoc : undefined, - parentObject: undefined - }) - } - } - - viewModel.push({ cells }) - } - } - - return viewModel -} - -/** - * Build relationship table markdown from metadata - * Rebuilds the viewModel and model, then uses the common markdown generation logic - */ -async function buildRelationshipTableFromMetadata ( - docs: Doc[], - metadata: BuildMarkdownTableMetadata, - client: Client -): Promise { - const hierarchy = client.getHierarchy() - const cardClass = metadata.cardClass as Ref> - - // Rebuild model from config - const config = metadata.config ?? [] - const lookup = buildConfigLookup(hierarchy, cardClass, config) - const model = await buildModel({ - client, - _class: cardClass, - keys: config, - lookup - }) - - // Rebuild viewModel from documents - const viewModel = await rebuildRelationshipTableViewModel(docs, model, cardClass, hierarchy, client) - - // Build props and use common markdown generation - const props: CopyRelationshipTableAsMarkdownProps = { - viewModel, - model, - objects: docs, - cardClass, - query: metadata.query - } - - const language = getCurrentLanguage() - return await buildRelationshipTableMarkdown(props, hierarchy, language) -} - -/** - * Wrapper function for building markdown table from BuildMarkdownTableMetadata - * This is used by text-editor-resources to refresh tables - * Converts BuildMarkdownTableMetadata format to CopyAsMarkdownTableProps format - */ -export async function buildMarkdownTableFromMetadata ( - docs: Doc[], - metadata: BuildMarkdownTableMetadata, - client: Client -): Promise { - // Check if this is a relationship table (viewletId is undefined) - const tableMetadata = metadata as TableMetadata - if (isRelationshipTable(tableMetadata)) { - return await buildRelationshipTableFromMetadata(docs, metadata, client) - } - - // Regular table: Load viewlet if viewletId is provided - let viewlet: Viewlet | undefined - if (metadata.viewletId !== undefined) { - viewlet = await client.findOne(viewPlugin.class.Viewlet, { _id: metadata.viewletId as Ref }) - } - - // Convert metadata to CopyAsMarkdownTableProps - const props: CopyAsMarkdownTableProps = { - cardClass: metadata.cardClass as Ref>, - viewlet, - config: metadata.config, - query: metadata.query - } - - // Use the reusable function - return await buildMarkdownTableFromDocs(docs, props, client) -} - -/** - * Build markdown table string from documents and props - * This is the core logic for building markdown tables, extracted for reuse - * @param docs - Documents to include in the table - * @param props - Table configuration props - * @param client - Client instance - * @returns Markdown table string - */ -export async function buildMarkdownTableFromDocs ( - docs: Doc[], - props: CopyAsMarkdownTableProps, - client: Client -): Promise { - if (docs.length === 0) { - return '' - } - - const hierarchy = client.getHierarchy() - const cardClass = hierarchy.getClass(props.cardClass) - if (cardClass == null) { - return '' - } - - // Load viewlet and config (including user preferences) - const { viewlet, config: actualConfig } = await loadViewletConfig( - client, - hierarchy, - props.cardClass, - props.viewlet, - props.config - ) - - // Build displayable model from config - let displayableModel: AttributeModel[] - if (actualConfig !== undefined && actualConfig.length > 0) { - const lookup = - viewlet !== undefined - ? buildConfigLookup(hierarchy, props.cardClass, actualConfig, viewlet.options?.lookup) - : undefined - const hiddenKeys = viewlet?.configOptions?.hiddenKeys ?? [] - const model = await buildModel({ - client, - _class: props.cardClass, - keys: actualConfig.filter((key: string | BuildModelKey) => { - if (typeof key === 'string') { - return !hiddenKeys.includes(key) - } - return !hiddenKeys.includes(key.key) && key.displayProps?.grow !== true - }), - lookup - }) - displayableModel = model.filter((attr) => attr.displayProps?.grow !== true) - } else { - displayableModel = await buildTableModel(client, hierarchy, props.cardClass, viewlet) - } - - if (displayableModel.length === 0) { - return '' - } - - const language = getCurrentLanguage() - - const userCache = new Map() - - const firstDocClass = docs.length > 0 ? docs[0]._class : props.cardClass - - const headers = await generateHeaders(displayableModel, firstDocClass, hierarchy, language) - - const rows: string[][] = [] - for (const card of docs) { - const row: string[] = [] - for (let i = 0; i < displayableModel.length; i++) { - const attr = displayableModel[i] - const isFirstColumn = i === 0 - const value = await formatValue( - attr, - card, - hierarchy, - props.cardClass, - language, - isFirstColumn, - userCache, - props.valueFormatter - ) - - // If this is the first column with empty key (title attribute), create a markdown link - if (isFirstColumn && attr.key === '') { - const linkValue = await createMarkdownLink(hierarchy, card, value) - row.push(linkValue) - } else { - const escapedValue = escapeMarkdownLinkText(value) - row.push(escapedValue) - } - } - rows.push(row) - } - - let markdown = '| ' + headers.join(' | ') + ' |\n' - markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n' - for (const row of rows) { - markdown += '| ' + row.join(' | ') + ' |\n' - } - - return markdown -} - -export async function CopyAsMarkdownTable ( - doc: Doc | Doc[], - evt: Event, - props: CopyAsMarkdownTableProps -): Promise { - try { - const docs = Array.isArray(doc) ? doc : doc !== undefined ? [doc] : [] - if (docs.length === 0) { - return - } - const client = getClient() - - // Build markdown table using the extracted function - const markdown = await buildMarkdownTableFromDocs(docs, props, client) - - if (markdown.length === 0) { - return - } - - // Build metadata for table refresh/diff functionality - const metadata = await buildTableMetadata(props, docs, client) - await copyMarkdown(markdown, metadata) - - const language = getCurrentLanguage() - addNotification( - await translate(view.string.Copied, {}, language), - await translate(view.string.TableCopiedToClipboard, {}, language), - SimpleNotification, - undefined, - NotificationSeverity.Success - ) - } catch (error) { - const language = getCurrentLanguage() - addNotification( - await translate(view.string.Copied, {}, language), - await translate(view.string.TableCopyFailed, {}, language), - SimpleNotification, - undefined, - NotificationSeverity.Error - ) - } -} - -/** - * Build markdown table from relationship table props (viewModel, model, objects) - * This is the core logic extracted for reuse in both copy and refresh operations - */ -async function buildRelationshipTableMarkdown ( - props: CopyRelationshipTableAsMarkdownProps, - hierarchy: Hierarchy, - language: string | undefined -): Promise { - if (props.viewModel.length === 0 || props.model.length === 0) { - return '' - } - - // Cache for user ID (PersonId) -> name mappings to reduce database calls - const userCache = new Map() - - // Get the first document's class for custom attribute lookup - const firstDocClass = props.objects.length > 0 ? props.objects[0]._class : props.cardClass - - // Generate headers using common function - const headers = await generateHeaders(props.model, firstDocClass, hierarchy, language) - - // Build a map of attribute keys to their index in the model for quick lookup - const attributeKeyToIndex = new Map() - props.model.forEach((attr, index) => { - attributeKeyToIndex.set(attr.key, index) - }) - - // Track active row spans - maps attribute key to remaining span count - const activeRowSpans = new Map() - - // Process rows from viewModel - const rows: string[][] = [] - for (let rowIdx = 0; rowIdx < props.viewModel.length; rowIdx++) { - const rowModel = props.viewModel[rowIdx] - const row: string[] = new Array(headers.length).fill('') - - // First, handle cells that are continuing from previous rows (row spans) - for (const [attrKey, spanInfo] of activeRowSpans.entries()) { - if (spanInfo.remaining > 0) { - const attrIndex = attributeKeyToIndex.get(attrKey) - if (attrIndex !== undefined) { - row[attrIndex] = spanInfo.value - spanInfo.remaining-- - if (spanInfo.remaining === 0) { - activeRowSpans.delete(attrKey) - } - } - } - } - - // Then, process cells in the current row - for (const cell of rowModel.cells) { - const attrIndex = attributeKeyToIndex.get(cell.attribute.key) - if (attrIndex === undefined) continue - - // Determine if this is an association column - const isAssociationKey = cell.attribute.key.startsWith('$associations') - - let doc: Doc | undefined - if (isAssociationKey) { - doc = cell.object - } else { - doc = cell.object ?? cell.parentObject - } - - if (doc === undefined) { - // Empty cell - row[attrIndex] = '' - continue - } - - // Use the same getValue logic as RelationshipTable - // For association keys, this returns the child document object itself - const rawValue = getAttributeValue(cell.attribute, doc, hierarchy) - - // Determine which document and class to use for formatting - let docToUse = doc - let docClass = props.cardClass - let attributeToUse = cell.attribute - - if (isAssociationKey) { - // For association keys, the value IS the child document object - if (rawValue !== undefined && rawValue !== null && typeof rawValue === 'object' && '_class' in rawValue) { - docToUse = rawValue as Doc - docClass = docToUse._class - const parts = cell.attribute.key.split('$associations.') - if (parts.length > 1) { - const afterAssoc = parts[1].substring(1) // Remove leading dot - const dotIndex = afterAssoc.indexOf('.') - if (dotIndex > 0) { - const attributeName = afterAssoc.substring(dotIndex + 1) - attributeToUse = { - ...cell.attribute, - key: attributeName - } - } else { - attributeToUse = { - ...cell.attribute, - key: '' - } - } - } - } - } - - // Format the value using the same logic as regular tables - const isFirstColumn = attrIndex === 0 - const allowEmptyKey = isFirstColumn || isAssociationKey - let value = await formatValue( - attributeToUse, - docToUse, - hierarchy, - docClass, - language, - allowEmptyKey, // Pass true for association keys so empty key works - userCache, - props.valueFormatter - ) - - const isDocumentTitle = attributeToUse.key === '' && docToUse !== undefined - if (isDocumentTitle) { - value = await createMarkdownLink(hierarchy, docToUse, value) - } else { - value = escapeMarkdownLinkText(value) - } - - row[attrIndex] = value - - // If this cell has a row span > 1, track it for subsequent rows - if (cell.rowSpan > 1) { - activeRowSpans.set(cell.attribute.key, { - value, - remaining: cell.rowSpan - 1 - }) - } - } - - rows.push(row) - } - - // Build markdown table - let markdown = '| ' + headers.join(' | ') + ' |\n' - markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n' - for (const row of rows) { - markdown += '| ' + row.join(' | ') + ' |\n' - } - - return markdown -} - -/** - * Copy RelationshipTable as markdown table - * Handles hierarchical data with row spans by duplicating cell values across spanned rows - */ -export async function CopyRelationshipTableAsMarkdown ( - evt: Event, - props: CopyRelationshipTableAsMarkdownProps -): Promise { - try { - const client = getClient() - const hierarchy = client.getHierarchy() - const cardClass = hierarchy.getClass(props.cardClass) - if (cardClass == null) { - return - } - - const language = getCurrentLanguage() - - // Build markdown using extracted function - const markdown = await buildRelationshipTableMarkdown(props, hierarchy, language) - - if (markdown.length === 0) { - return - } - - // Build metadata for relationship table refresh/diff functionality - const metadata = buildRelationshipTableMetadata(props, props.objects) - await copyMarkdown(markdown, metadata) - - addNotification( - await translate(view.string.Copied, {}, language), - await translate(view.string.TableCopiedToClipboard, {}, language), - SimpleNotification, - undefined, - NotificationSeverity.Success - ) - } catch (error) { - const language = getCurrentLanguage() - addNotification( - await translate(view.string.Copied, {}, language), - await translate(view.string.TableCopyFailed, {}, language), - SimpleNotification, - undefined, - NotificationSeverity.Error - ) - } -} diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index eb64971b61..ccfadf3e02 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -145,7 +145,6 @@ import { import ForbiddenNotification from './components/ForbiddenNotification.svelte' import { AggregationMiddleware, AnalyticsMiddleware, ReadOnlyAccessMiddleware } from './middleware' -import { buildMarkdownTableFromMetadata } from './copyAsMarkdownTable' import { getLink, openDocFromRef } from './utils' import { hideArchived, showEmptyGroups } from './viewOptions' import { @@ -218,20 +217,8 @@ export { } from './utils' export * from './viewOptions' export * from './viewletContextStore' -export { - CopyAsMarkdownTable, - type CopyAsMarkdownTableProps, - CopyRelationshipTableAsMarkdown, - type CopyRelationshipTableAsMarkdownProps, - type RelationshipCellModel, - type RelationshipRowModel, - type ValueFormatter, - registerValueFormatterForClass, - registerValueFormatter, - buildMarkdownTableFromDocs, - buildMarkdownTableFromMetadata -} from './copyAsMarkdownTable' -export { isIntlString } from './markdownTableUtils' +export { copyMarkdown } from './actionImpl' +export { default as SimpleNotification } from './components/SimpleNotification.svelte' export type { BuildMarkdownTableMetadata } from '@hcengineering/view' export { ArrayEditor, @@ -405,7 +392,6 @@ export default async (): Promise => ({ BlobVideoMetadata: blobVideoMetadata, OpenDocument: openDocFromRef, CanCopyLink: canCopyLink, - GetLink: getLink, - BuildMarkdownTableFromDocs: buildMarkdownTableFromMetadata + GetLink: getLink } }) diff --git a/plugins/view-resources/src/markdownTableUtils.ts b/plugins/view-resources/src/markdownTableUtils.ts deleted file mode 100644 index b4252bae7c..0000000000 --- a/plugins/view-resources/src/markdownTableUtils.ts +++ /dev/null @@ -1,343 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type AnyAttribute, - type Class, - type Doc, - type Hierarchy, - type Ref, - concatLink, - getDisplayTime -} from '@hcengineering/core' -import { translate, type IntlString, getMetadata } from '@hcengineering/platform' -import { locationToUrl } from '@hcengineering/ui' -import presentation from '@hcengineering/presentation' -import { type AttributeModel, type BuildModelKey } from '@hcengineering/view' -import { getObjectLinkFragment } from './utils' -import view from './plugin' - -enum DateFormatOption { - Numeric = 'numeric', - Short = 'short' -} - -/** - * Check if a string is an IntlString (format: "plugin:resource:key") - */ -export function isIntlString (value: string): boolean { - if (typeof value !== 'string' || value.length === 0) { - return false - } - const parts = value.split(':') - return parts.length >= 3 && parts.every((part) => part.length > 0) -} - -/** - * Resolve the human-readable label for a custom attribute - * @param attrLabel - The attribute label (may be an ID like "custom...") - * @param docClass - The document's class (MasterTag) where the attribute is defined - * @param hierarchy - The hierarchy instance - * @param language - Current language - * @returns The translated human-readable label, or the original label if not found - */ -export async function resolveCustomAttributeLabel ( - attrLabel: string, - docClass: Ref>, - hierarchy: Hierarchy, - language: string | undefined -): Promise { - if (!attrLabel.startsWith('custom')) { - return attrLabel - } - - let customAttr = hierarchy.findAttribute(docClass, attrLabel) - if (customAttr === undefined) { - const allAttrs = hierarchy.getAllAttributes(docClass) - customAttr = allAttrs.get(attrLabel) - } - - if (customAttr?.label !== undefined) { - return await translate(customAttr.label, {}, language) - } - - return attrLabel -} - -/** - * Generate table headers from AttributeModel array - * Handles custom attributes, IntlStrings, and regular labels - * @param model - Array of AttributeModel to generate headers from - * @param firstDocClass - The first document's class (for custom attribute lookup) - * @param hierarchy - The hierarchy instance - * @param language - Current language - * @returns Array of header strings - */ -export async function generateHeaders ( - model: AttributeModel[], - firstDocClass: Ref>, - hierarchy: Hierarchy, - language: string | undefined -): Promise { - const headers: string[] = [] - for (const attr of model) { - let label: string - if (typeof attr.label === 'string') { - if (attr.label.startsWith('custom')) { - label = await resolveCustomAttributeLabel(attr.label, firstDocClass, hierarchy, language) - } else if (isIntlString(attr.label)) { - label = await translate(attr.label as unknown as IntlString, {}, language) - } else { - label = attr.label - } - } else { - label = await translate(attr.label, {}, language) - } - headers.push(label) - } - return headers -} - -/** - * Convert AttributeModel array back to config format (Array) - * Preserves custom attributes by using label as key when key is empty - * @param model - Array of AttributeModel to convert - * @returns Config array that can be used to rebuild the table - */ -export function modelToConfig (model: AttributeModel[]): Array { - return model.map((m) => { - if (m.key === '' && typeof m.label === 'string' && m.label.startsWith('custom')) { - return { - key: m.label, // Use label (custom attribute name) as key - label: m.label, - displayProps: m.displayProps, - props: m.props, - sortingKey: m.sortingKey - } - } - if (m.key !== '') { - return m.key - } - if (m.castRequest !== undefined) { - return { - key: m.key, - label: m.label, - displayProps: m.displayProps, - props: m.props, - sortingKey: m.sortingKey - } - } - return m.key - }) -} - -/** - * Format an array of values, handling reference lookups if needed - * @param value - The array value - * @param attrType - The attribute type - * @param attribute - The attribute definition (for getting the name) - * @param attrKey - The attribute key (fallback if attribute.name is not available) - * @param card - The document - * @param language - Current language - * @returns Formatted string with comma-separated values - */ -export async function formatArrayValue ( - value: any[], - attrType: any, - attribute: AnyAttribute | undefined, - attrKey: string, - card: Doc, - language: string | undefined -): Promise { - const isRefArray = - attrType?._class === core.class.ArrOf && - (attrType as { of?: { _class?: Ref> } })?.of?._class === core.class.RefTo - - if (isRefArray && (attribute !== undefined || attrKey !== '')) { - const cardWithLookup = card as any - const lookupKey = attribute?.name ?? attrKey - const lookupData = cardWithLookup.$lookup?.[lookupKey] - - if (lookupData !== undefined && lookupData !== null) { - const resolvedArray = Array.isArray(lookupData) ? lookupData : [lookupData] - const translatedValues = await Promise.all( - resolvedArray.map(async (v) => { - if (typeof v === 'object' && v !== null && 'title' in v) { - const title = v.title ?? '' - if (typeof title === 'string' && isIntlString(title)) { - return await translate(title as unknown as IntlString, {}, language) - } - return String(title) - } - return typeof v === 'string' ? v : String(v) - }) - ) - return translatedValues.join(', ') - } - } - - const translatedValues = await Promise.all( - value.map(async (v) => { - if (typeof v === 'object' && v !== null && 'title' in v) { - const title = v.title ?? '' - if (typeof title === 'string' && isIntlString(title)) { - return await translate(title as unknown as IntlString, {}, language) - } - return String(title) - } - if (typeof v === 'string' && isIntlString(v)) { - return await translate(v as unknown as IntlString, {}, language) - } - return typeof v === 'string' ? v : String(v) - }) - ) - return translatedValues.join(', ') -} - -/** - * Extract title or name from an object, translating if needed - * @param obj - The object to extract from - * @param language - Current language - * @returns The title/name string, or empty string if not found - */ -export async function extractObjectTitleOrName ( - obj: Record, - language: string | undefined -): Promise { - if ('title' in obj) { - const title = String(obj.title ?? '') - if (isIntlString(title)) { - return await translate(title as unknown as IntlString, {}, language) - } - return title - } - if ('name' in obj) { - const name = String(obj.name ?? '') - if (isIntlString(name)) { - return await translate(name as unknown as IntlString, {}, language) - } - return name - } - return '' -} - -/** - * Format a custom attribute value for markdown display - * Handles various types: string, number, boolean, arrays, references - */ -export async function formatCustomAttributeValue ( - value: any, - attribute: AnyAttribute | undefined, - card: Doc, - hierarchy: Hierarchy, - language: string | undefined -): Promise { - if (value === null || value === undefined) { - return '' - } - - const attrType = attribute?.type - - if (typeof value === 'number' && attrType?._class === core.class.TypeTimestamp) { - return getDisplayTime(value) - } - - if (value instanceof Date) { - const options: Intl.DateTimeFormatOptions = { - year: DateFormatOption.Numeric, - month: DateFormatOption.Short, - day: DateFormatOption.Numeric - } - return value.toLocaleDateString(language ?? 'default', options) - } - - if (typeof value === 'number' || typeof value === 'boolean') { - return String(value) - } - - if (typeof value === 'string') { - if (isIntlString(value)) { - return await translate(value as unknown as IntlString, {}, language) - } - - const isRef = attrType?._class === core.class.RefTo - if (isRef && attribute !== undefined) { - const cardWithLookup = card as any - const lookupData = cardWithLookup.$lookup?.[attribute.name] - if (lookupData !== undefined && lookupData !== null) { - if (typeof lookupData === 'object' && 'title' in lookupData) { - const title = lookupData.title ?? '' - if (typeof title === 'string' && isIntlString(title)) { - return await translate(title as unknown as IntlString, {}, language) - } - return String(title) - } - } - } - - return value - } - - if (Array.isArray(value)) { - return await formatArrayValue(value, attrType, attribute, attribute?.name ?? '', card, language) - } - - if (typeof value === 'object' && value !== null) { - const obj = value as Record - const titleOrName = await extractObjectTitleOrName(obj, language) - return titleOrName !== '' ? titleOrName : String(value) - } - - return String(value) -} - -/** - * Escape markdown link text (brackets, pipes, backslashes, newlines) - */ -export function escapeMarkdownLinkText (text: string): string { - // Escape backslashes first, then brackets and pipes, and normalize newlines to spaces - return text - .replace(/\\/g, '\\\\') - .replace(/\[/g, '\\[') - .replace(/\]/g, '\\]') - .replace(/\|/g, '\\|') - .replace(/\r?\n/g, ' ') -} - -/** - * Escape markdown link URL (backslashes and closing parentheses) - */ -export function escapeMarkdownLinkUrl (url: string): string { - // Escape backslashes and closing parentheses used to terminate the URL - return url.replace(/\\/g, '\\\\').replace(/\)/g, '\\)') -} - -/** - * Create a markdown link for a document - */ -export async function createMarkdownLink (hierarchy: Hierarchy, card: Doc, value: string): Promise { - try { - const loc = await getObjectLinkFragment(hierarchy, card, {}, view.component.EditDoc) - const relativeUrl = locationToUrl(loc) - const frontUrl = - getMetadata(presentation.metadata.FrontUrl) ?? (typeof window !== 'undefined' ? window.location.origin : '') - const fullUrl = concatLink(frontUrl, relativeUrl) - const escapedText = escapeMarkdownLinkText(value) - const escapedUrl = escapeMarkdownLinkUrl(fullUrl) - return `[${escapedText}](${escapedUrl})` - } catch { - // If link generation fails, fall back to plain text - return escapeMarkdownLinkText(value) - } -} diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index ffe92162cf..7d29c0ab42 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -14,7 +14,7 @@ // limitations under the License. // -import { Class, Client, Doc, DocumentQuery, FindOptions, Mixin, Ref } from '@hcengineering/core' +import { Class, Doc, DocumentQuery, FindOptions, Mixin, Ref } from '@hcengineering/core' import { Asset, IntlString, Plugin, Resource, plugin } from '@hcengineering/platform' import { AnyComponent, PopupAlignment, PopupPosAlignment, type ComponentExtensionId } from '@hcengineering/ui/src/types' import { @@ -30,7 +30,6 @@ import { AttributeFilterPresenter, AttributePresenter, BaseQuery, - BuildMarkdownTableMetadata, ClassFilters, ClassSortFuncs, CollectionEditor, @@ -357,10 +356,7 @@ const view = plugin(viewId, { PositionElementAlignment: '' as Resource<(e?: Event) => PopupAlignment | undefined> }, function: { - OpenDocument: '' as Resource, - BuildMarkdownTableFromDocs: '' as Resource< - (docs: Doc[], metadata: BuildMarkdownTableMetadata, client: Client) => Promise - > + OpenDocument: '' as Resource }, actionImpl: { CopyTextToClipboard: '' as ViewAction<{ diff --git a/rush.json b/rush.json index aeca18c9d4..11ae72e6b2 100644 --- a/rush.json +++ b/rush.json @@ -1946,6 +1946,21 @@ "projectFolder": "plugins/controlled-documents-resources", "shouldPublish": false }, + { + "packageName": "@hcengineering/converter", + "projectFolder": "plugins/converter", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/converter-resources", + "projectFolder": "plugins/converter-resources", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/model-converter", + "projectFolder": "models/converter", + "shouldPublish": false + }, { "packageName": "@hcengineering/model-controlled-documents", "projectFolder": "models/controlled-documents", From 4563fab7a6053a2776c27ad1af7c81c455855085 Mon Sep 17 00:00:00 2001 From: Bahadir67 <69666685+Bahadir67@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:36:46 +0300 Subject: [PATCH 11/67] feat(ui): add Turkish language to settings popup (#10441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ui): add Turkish language to settings popup Added Turkish (Türkçe) to the language selection dropdown in SettingsPopup.svelte and added the Turkish IntlString definition to plugin.ts. Turkish translation files already exist in the codebase (lang/tr.json files). Co-Authored-By: Claude Opus 4.5 Signed-off-by: Bahadır Atılgan * ci: add workflow to build front image with Turkish support Adds a GitHub Actions workflow to build and push the front container image to GitHub Container Registry for testing Turkish language support. Co-Authored-By: Claude Opus 4.5 Signed-off-by: Bahadır Atılgan * fix(ci): use lowercase image name for ghcr.io Signed-off-by: Bahadır Atılgan * fix(ci): use huly's docker:build for proper image creation Signed-off-by: Bahadır Atılgan * chore: remove test workflow file from PR Signed-off-by: Bahadır Atılgan * feat(inbox): add Turkish translations for inbox module Added missing Turkish translation file for the inbox-assets plugin. Co-Authored-By: Claude Opus 4.5 Signed-off-by: Bahadır Atılgan --------- Signed-off-by: Bahadır Atılgan Co-authored-by: Claude Opus 4.5 --- .../ui/src/components/internal/SettingsPopup.svelte | 3 ++- packages/ui/src/plugin.ts | 1 + plugins/inbox-assets/lang/tr.json | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 plugins/inbox-assets/lang/tr.json diff --git a/packages/ui/src/components/internal/SettingsPopup.svelte b/packages/ui/src/components/internal/SettingsPopup.svelte index d89e47a5eb..85da69247c 100644 --- a/packages/ui/src/components/internal/SettingsPopup.svelte +++ b/packages/ui/src/components/internal/SettingsPopup.svelte @@ -79,7 +79,8 @@ { id: 'it', label: ui.string.Italian, logo: '🇮🇹' }, { id: 'cs', label: ui.string.Czech, logo: '🇨🇿' }, { id: 'de', label: ui.string.German, logo: '🇩🇪' }, - { id: 'ja', label: ui.string.Japanese, logo: '🇯🇵' } + { id: 'ja', label: ui.string.Japanese, logo: '🇯🇵' }, + { id: 'tr', label: ui.string.Turkish, logo: '🇹🇷' } ].filter((lang) => uiLangs.has(lang.id)) if (langs.findIndex((l) => l.id === $currentLanguage) < 0 && langs.length !== 0) { diff --git a/packages/ui/src/plugin.ts b/packages/ui/src/plugin.ts index 6e38e186e9..958188c428 100644 --- a/packages/ui/src/plugin.ts +++ b/packages/ui/src/plugin.ts @@ -82,6 +82,7 @@ export const uis = plugin(uiId, { Czech: '' as IntlString, German: '' as IntlString, Japanese: '' as IntlString, + Turkish: '' as IntlString, MinutesBefore: '' as IntlString, HoursBefore: '' as IntlString, DaysBefore: '' as IntlString, diff --git a/plugins/inbox-assets/lang/tr.json b/plugins/inbox-assets/lang/tr.json new file mode 100644 index 0000000000..555a604f41 --- /dev/null +++ b/plugins/inbox-assets/lang/tr.json @@ -0,0 +1,13 @@ +{ + "string": { + "Inbox": "Gelen Kutusu", + "ReactedToYourMessage": "mesajınıza tepki verdi", + "ClearAll": "Tümünü temizle", + "InboxIsClear": "Gelen kutusu temiz", + "YouDontHaveAnyNewMessages": "Yeni mesajınız yok", + "ReadAll": "Tümünü oku", + "Clearing": "Temizleniyor...", + "Reading": "Okunuyor...", + "HideUserNames": "Kullanıcı adlarını gizle" + } +} From 1fb53b9115e352c069a9019738d3a62718834b9d Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Fri, 30 Jan 2026 09:49:12 +0700 Subject: [PATCH 12/67] fix: mute account unauthorized error (#10461) --- server/account/src/operations.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index 078154196e..dd653f0efc 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -1667,7 +1667,10 @@ export async function getLoginInfoByToken ( accountUuid = sub ?? account } catch (err: any) { Analytics.handleError(err) - ctx.error('Invalid token', { token, errMsg: err.message }) + if (token !== undefined) { + // do not spam errors as this is expected when we issue request with no token + ctx.error('Invalid token', { token, errMsg: err.message }) + } switch (err.message) { case 'Token not yet active': { const { nbf } = decodeToken(token, false) From 51768ff31295062383059fb4272803f71aab158d Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 30 Jan 2026 11:15:21 +0700 Subject: [PATCH 13/67] Enable Turkish language in dev setup (#10462) Signed-off-by: Artem Savchenko --- dev/prod/public/branding.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/prod/public/branding.json b/dev/prod/public/branding.json index f1789b2516..e182e67d37 100644 --- a/dev/prod/public/branding.json +++ b/dev/prod/public/branding.json @@ -1,7 +1,7 @@ { "huly.local:8080": { "title": "Huly", - "languages": "en,ru,pt,es,zh,fr,de,ja", + "languages": "en,ru,pt,es,zh,fr,de,ja,tr", "defaultLanguage": "en", "defaultApplication": "tracker", "defaultSpace": "tracker:project:DefaultProject", @@ -30,7 +30,7 @@ }, "huly.local:8087": { "title": "Huly", - "languages": "en,ru,pt,es,zh,fr,de,ja", + "languages": "en,ru,pt,es,zh,fr,de,ja,tr", "defaultLanguage": "en", "defaultApplication": "tracker", "defaultSpace": "tracker:project:DefaultProject", From a0252ab2febd4026c0414f251639eca7954b69c6 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Fri, 30 Jan 2026 12:55:58 +0700 Subject: [PATCH 14/67] fix: do not write analytics event when no token (#10463) Signed-off-by: Alexander Onnikov --- server/account/src/operations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index dd653f0efc..e0cc01c1a3 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -1666,9 +1666,9 @@ export async function getLoginInfoByToken ( } accountUuid = sub ?? account } catch (err: any) { - Analytics.handleError(err) if (token !== undefined) { // do not spam errors as this is expected when we issue request with no token + Analytics.handleError(err) ctx.error('Invalid token', { token, errMsg: err.message }) } switch (err.message) { From 897466757861cc0d8c2d743b026dcc5293612e2a Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 30 Jan 2026 15:56:05 +0700 Subject: [PATCH 15/67] Fix documents space export (#10465) Signed-off-by: Artem Savchenko --- .../src/components/ExportToWorkspaceModal.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte b/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte index 8cba809214..e5be08f8b6 100644 --- a/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte +++ b/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte @@ -103,7 +103,8 @@ }) ) - $: canSave = targetWorkspace !== undefined && _class != null && filteredSelectedDocs.length > 0 + $: canSave = + targetWorkspace !== undefined && _class != null && (spaceExport === true || filteredSelectedDocs.length > 0) async function handleExport (): Promise { if (!canSave || _class == null) return From 19ebe782188241a6a534f1d59c6dc721384f8cbf Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 30 Jan 2026 16:41:27 +0700 Subject: [PATCH 16/67] feat: Add import notifications (#10464) * feat: Add import notifications Signed-off-by: Artem Savchenko * Fix svelte check Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- common/config/rush/pnpm-lock.yaml | 27 ++++ models/export/package.json | 6 +- models/export/src/index.ts | 82 +++++++++- plugins/export-assets/lang/cs.json | 12 +- plugins/export-assets/lang/de.json | 12 +- plugins/export-assets/lang/en.json | 12 +- plugins/export-assets/lang/es.json | 12 +- plugins/export-assets/lang/fr.json | 12 +- plugins/export-assets/lang/it.json | 12 +- plugins/export-assets/lang/ja.json | 12 +- plugins/export-assets/lang/pt.json | 12 +- plugins/export-assets/lang/ru.json | 12 +- plugins/export-assets/lang/tr.json | 12 +- plugins/export-assets/lang/zh.json | 12 +- plugins/export-resources/package.json | 6 +- .../src/components/ExportResultPanel.svelte | 142 ++++++++++++++++++ plugins/export-resources/src/index.ts | 35 ++++- plugins/export/package.json | 3 +- plugins/export/src/plugin.ts | 30 +++- plugins/export/src/types.ts | 20 +++ .../export/pod-export/src/notifications.ts | 132 +++++++--------- services/export/pod-export/src/server.ts | 8 +- .../src/workspace/workspace-exporter.ts | 12 +- 23 files changed, 529 insertions(+), 106 deletions(-) create mode 100644 plugins/export-resources/src/components/ExportResultPanel.svelte diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index bfa42de239..e71bb67660 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -8067,9 +8067,18 @@ importers: '@hcengineering/model-core': specifier: workspace:^0.7.0 version: link:../core + '@hcengineering/model-notification': + specifier: workspace:^0.7.0 + version: link:../notification '@hcengineering/model-presentation': specifier: workspace:^0.7.0 version: link:../presentation + '@hcengineering/model-view': + specifier: workspace:^0.7.0 + version: link:../view + '@hcengineering/notification': + specifier: workspace:^0.7.0 + version: link:../../plugins/notification '@hcengineering/platform': specifier: workspace:^0.7.19 version: link:../../foundations/core/packages/platform @@ -8079,6 +8088,9 @@ importers: '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../../plugins/view '@hcengineering/workbench': specifier: workspace:^0.7.0 version: link:../../plugins/workbench @@ -20563,6 +20575,9 @@ importers: '@hcengineering/core': specifier: workspace:^0.7.24 version: link:../../foundations/core/packages/core + '@hcengineering/notification': + specifier: workspace:^0.7.0 + version: link:../notification '@hcengineering/platform': specifier: workspace:^0.7.19 version: link:../../foundations/core/packages/platform @@ -20679,6 +20694,12 @@ importers: '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../login + '@hcengineering/notification': + specifier: workspace:^0.7.0 + version: link:../notification + '@hcengineering/panel': + specifier: workspace:^0.7.0 + version: link:../../packages/panel '@hcengineering/platform': specifier: workspace:^0.7.19 version: link:../../foundations/core/packages/platform @@ -20691,6 +20712,12 @@ importers: '@hcengineering/ui': specifier: workspace:^0.7.0 version: link:../../packages/ui + '@hcengineering/view': + specifier: workspace:^0.7.0 + version: link:../view + '@hcengineering/view-resources': + specifier: workspace:^0.7.0 + version: link:../view-resources svelte: specifier: ^4.2.20 version: 4.2.20 diff --git a/models/export/package.json b/models/export/package.json index 75cbe35085..0d3912b849 100644 --- a/models/export/package.json +++ b/models/export/package.json @@ -42,7 +42,11 @@ "@hcengineering/workbench": "workspace:^0.7.0", "@hcengineering/presentation": "workspace:^0.7.0", "@hcengineering/model-presentation": "workspace:^0.7.0", + "@hcengineering/model-view": "workspace:^0.7.0", + "@hcengineering/model-notification": "workspace:^0.7.0", "@hcengineering/export-resources": "workspace:^0.7.0", - "@hcengineering/export": "workspace:^0.7.0" + "@hcengineering/export": "workspace:^0.7.0", + "@hcengineering/view": "workspace:^0.7.0", + "@hcengineering/notification": "workspace:^0.7.0" } } diff --git a/models/export/src/index.ts b/models/export/src/index.ts index f4117b6b15..05cb3384b1 100644 --- a/models/export/src/index.ts +++ b/models/export/src/index.ts @@ -13,24 +13,94 @@ // limitations under the License. // -import { type Builder } from '@hcengineering/model' -import core from '@hcengineering/model-core' +import type { Class, Doc, Domain, Ref } from '@hcengineering/core' +import type { ExportResultRecord } from '@hcengineering/export' +import { type Builder, Model, Prop, ArrOf, TypeRef, TypeString, TypeNumber, UX } from '@hcengineering/model' +import core, { TDoc } from '@hcengineering/model-core' import presentation from '@hcengineering/model-presentation' import workbench from '@hcengineering/workbench' -import exportPlugin from './plugin' +import view from '@hcengineering/model-view' +import notification from '@hcengineering/notification' +import exportPlugin from '@hcengineering/export' + +import exportModelPlugin from './plugin' export { exportId } from '@hcengineering/export' export * from './migration' -export default exportPlugin +export default exportModelPlugin + +export const DOMAIN_EXPORT = 'export' as Domain + +@Model(exportPlugin.class.ExportResultRecord, core.class.Doc, DOMAIN_EXPORT) +@UX(exportPlugin.string.ImportCompleted, exportPlugin.icon.Export) +export class TExportResultRecord extends TDoc implements ExportResultRecord { + @Prop(TypeString(), exportPlugin.string.SourceWorkspace) + sourceWorkspace!: string + + @Prop(TypeString(), exportPlugin.string.TargetWorkspace) + targetWorkspace!: string + + @Prop(TypeNumber(), exportPlugin.string.ExportedCount) + exportedCount!: number + + @Prop(ArrOf(TypeRef(core.class.Doc)), exportPlugin.string.ExportedDocumentIds) + exportedDocumentIds!: Ref[] + + @Prop(TypeRef(core.class.Class), exportPlugin.string.ExportedDocumentClass) + objectClass!: Ref> + + @Prop(TypeString(), view.string.Title) + title?: string +} export function createModel (builder: Builder): void { + builder.createModel(TExportResultRecord) + + builder.mixin(exportPlugin.class.ExportResultRecord, core.class.Class, view.mixin.ObjectPresenter, { + presenter: view.component.BaseDocPresenter + }) + + builder.mixin(exportPlugin.class.ExportResultRecord, core.class.Class, view.mixin.ObjectPanel, { + component: exportPlugin.component.ExportResultPanel + }) + + builder.mixin(exportPlugin.class.ExportResultRecord, core.class.Class, view.mixin.ObjectTitle, { + titleProvider: exportPlugin.function.ExportResultTitleProvider + }) + + builder.createDoc( + notification.class.NotificationGroup, + core.space.Model, + { + label: exportPlugin.string.Import, + icon: exportPlugin.icon.Export, + objectClass: exportPlugin.class.ExportResultRecord + }, + exportPlugin.ids.ImportNotificationGroup + ) + + builder.createDoc( + notification.class.NotificationType, + core.space.Model, + { + hidden: false, + generated: false, + label: exportPlugin.string.ImportedDocuments, + group: exportPlugin.ids.ImportNotificationGroup, + txClasses: [], + objectClass: exportPlugin.class.ExportResultRecord, + defaultEnabled: true + }, + exportPlugin.ids.ImportedDocumentsNotification + ) + builder.createDoc( presentation.class.ComponentPointExtension, core.space.Model, { extension: workbench.extensions.SpecialViewAction, - component: exportPlugin.component.ExportButton + component: exportModelPlugin.component.ExportButton }, - exportPlugin.extensions.ExportButton + exportModelPlugin.extensions.ExportButton ) } diff --git a/plugins/export-assets/lang/cs.json b/plugins/export-assets/lang/cs.json index faddf6fd9f..4ca5af77f1 100644 --- a/plugins/export-assets/lang/cs.json +++ b/plugins/export-assets/lang/cs.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Export zahájen. Až bude dokončen, obdržíte oznámení.", "ExportToWorkspaceCompleted": "Export dokončen", "ExportToWorkspaceCompletedMessage": "Export byl úspěšně dokončen.", + "ImportToWorkspaceNotificationMessage": "{count} dokumentů importováno do vašeho pracovního prostoru", + "ImportCompleted": "Dokumenty importovány do pracovního prostoru", + "SourceWorkspace": "Zdrojový pracovní prostor", + "ExportedCount": "Počet exportovaných", + "ExportedDocumentIds": "ID exportovaných dokumentů", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 dokument byl importován z pracovního prostoru {workspace}} other {# dokumentů bylo importováno z pracovního prostoru {workspace}}}:", + "ExportedDocumentClass": "Třída exportovaných dokumentů", + "Import": "Import", + "ImportedDocuments": "Nové dokumenty", "ExportToWorkspaceFailed": "Export se nezdařil", "SelectWorkspace": "Vybrat pracovní prostor", "SelectWorkspaceToExport": "Vybrat pracovní prostor pro export {count, plural, =1 {1 dokument} other {# dokumentů}}", @@ -38,6 +47,7 @@ "SelectSpace": "Vybrat prostor", "NoSelectedDocuments": "Žádné dokumenty nevybrány", "RequestPermissionToImport": "Prosím, požádejte vlastníka cílového pracovního prostoru o povolení k importu dokumentů.", - "SkipDeletedObsolete": "Přeskočit archivované nebo zastaralé dokumenty" + "SkipDeletedObsolete": "Přeskočit archivované nebo zastaralé dokumenty", + "ExportResultRecordTitle": "Export dokumentů z pracovního prostoru {workspace} ({date})" } } diff --git a/plugins/export-assets/lang/de.json b/plugins/export-assets/lang/de.json index b9fcd671dd..96b7952286 100644 --- a/plugins/export-assets/lang/de.json +++ b/plugins/export-assets/lang/de.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Export gestartet. Sie erhalten eine Benachrichtigung, wenn der Export abgeschlossen ist.", "ExportToWorkspaceCompleted": "Export abgeschlossen", "ExportToWorkspaceCompletedMessage": "Export erfolgreich abgeschlossen.", + "ImportToWorkspaceNotificationMessage": "{count} Dokumente in Ihren Arbeitsbereich importiert", + "ImportCompleted": "Dokumente in Arbeitsbereich importiert", + "SourceWorkspace": "Quell-Arbeitsbereich", + "ExportedCount": "Exportierte Anzahl", + "ExportedDocumentIds": "IDs exportierter Dokumente", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 Dokument wurde aus dem Arbeitsbereich {workspace} importiert} other {# Dokumente wurden aus dem Arbeitsbereich {workspace} importiert}}:", + "ExportedDocumentClass": "Klasse exportierter Dokumente", + "Import": "Import", + "ImportedDocuments": "Neue Dokumente", "ExportToWorkspaceFailed": "Export fehlgeschlagen", "SelectWorkspace": "Arbeitsbereich auswählen", "SelectWorkspaceToExport": "Arbeitsbereich zum Exportieren von {count, plural, =1 {1 Dokument} other {# Dokumenten}} auswählen", @@ -38,6 +47,7 @@ "SelectSpace": "Bereich auswählen", "NoSelectedDocuments": "Keine Dokumente ausgewählt", "RequestPermissionToImport": "Bitte fordern Sie die Berechtigung vom Eigentümer des Ziel-Arbeitsbereichs an, um Dokumente zu importieren.", - "SkipDeletedObsolete": "Archivierte oder veraltete Dokumente überspringen" + "SkipDeletedObsolete": "Archivierte oder veraltete Dokumente überspringen", + "ExportResultRecordTitle": "Export von Dokumenten aus dem Arbeitsbereich {workspace} ({date})" } } diff --git a/plugins/export-assets/lang/en.json b/plugins/export-assets/lang/en.json index af3fbe65fb..7b8e1df5bb 100644 --- a/plugins/export-assets/lang/en.json +++ b/plugins/export-assets/lang/en.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Export started. You will receive a notification when it completes.", "ExportToWorkspaceCompleted": "Export completed", "ExportToWorkspaceCompletedMessage": "Export completed successfully.", + "ImportToWorkspaceNotificationMessage": "{count} documents imported to your workspace", + "ImportCompleted": "Documents imported to workspace", + "SourceWorkspace": "Source workspace", + "ExportedCount": "Exported count", + "ExportedDocumentIds": "Exported document IDs", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 document was imported from {workspace} workspace} other {# documents were imported from {workspace} workspace}}:", + "ExportedDocumentClass": "Exported document class", + "Import": "Import", + "ImportedDocuments": "New documents", "ExportToWorkspaceFailed": "Export failed", "SelectWorkspace": "Select workspace", "SelectWorkspaceToExport": "Select workspace to export {count, plural, =1 {1 document} other {# documents}}", @@ -38,6 +47,7 @@ "SelectSpace": "Select space", "NoSelectedDocuments": "No documents selected", "RequestPermissionToImport": "Please request permission from the target workspace owner to import documents.", - "SkipDeletedObsolete": "Skip archived or obsolete documents" + "SkipDeletedObsolete": "Skip archived or obsolete documents", + "ExportResultRecordTitle": "Export of documents from {workspace} workspace ({date})" } } diff --git a/plugins/export-assets/lang/es.json b/plugins/export-assets/lang/es.json index f6ac4c270b..d3559691d4 100644 --- a/plugins/export-assets/lang/es.json +++ b/plugins/export-assets/lang/es.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Exportación iniciada. Recibirá una notificación cuando se complete.", "ExportToWorkspaceCompleted": "Exportación completada", "ExportToWorkspaceCompletedMessage": "Exportación completada exitosamente.", + "ImportToWorkspaceNotificationMessage": "{count} documentos importados a su espacio de trabajo", + "ImportCompleted": "Documentos importados al espacio de trabajo", + "SourceWorkspace": "Espacio de trabajo de origen", + "ExportedCount": "Cantidad exportada", + "ExportedDocumentIds": "ID de documentos exportados", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 documento fue importado desde el espacio de trabajo {workspace}} other {# documentos fueron importados desde el espacio de trabajo {workspace}}}:", + "ExportedDocumentClass": "Clase de documentos exportados", + "Import": "Importar", + "ImportedDocuments": "Documentos nuevos", "ExportToWorkspaceFailed": "Exportación fallida", "SelectWorkspace": "Seleccionar espacio de trabajo", "SelectWorkspaceToExport": "Seleccionar espacio de trabajo para exportar {count, plural, =1 {1 documento} other {# documentos}}", @@ -38,6 +47,7 @@ "SelectSpace": "Seleccionar espacio", "NoSelectedDocuments": "No hay documentos seleccionados", "RequestPermissionToImport": "Por favor, solicite permiso al propietario del espacio de trabajo de destino para importar documentos.", - "SkipDeletedObsolete": "Omitir documentos archivados u obsoletos" + "SkipDeletedObsolete": "Omitir documentos archivados u obsoletos", + "ExportResultRecordTitle": "Exportación de documentos del espacio de trabajo {workspace} ({date})" } } diff --git a/plugins/export-assets/lang/fr.json b/plugins/export-assets/lang/fr.json index b451b4fe4c..fb4692b6e5 100644 --- a/plugins/export-assets/lang/fr.json +++ b/plugins/export-assets/lang/fr.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Exportation démarrée. Vous recevrez une notification une fois terminée.", "ExportToWorkspaceCompleted": "Exportation terminée", "ExportToWorkspaceCompletedMessage": "Exportation terminée avec succès.", + "ImportToWorkspaceNotificationMessage": "{count} documents importés dans votre espace de travail", + "ImportCompleted": "Documents importés dans l'espace de travail", + "SourceWorkspace": "Espace de travail source", + "ExportedCount": "Nombre exporté", + "ExportedDocumentIds": "ID des documents exportés", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 document a été importé depuis l'espace de travail {workspace}} other {# documents ont été importés depuis l'espace de travail {workspace}}}:", + "ExportedDocumentClass": "Classe des documents exportés", + "Import": "Import", + "ImportedDocuments": "Nouveaux documents", "ExportToWorkspaceFailed": "Exportation échouée", "SelectWorkspace": "Sélectionner l'espace de travail", "SelectWorkspaceToExport": "Sélectionner l'espace de travail pour exporter {count, plural, =1 {1 document} other {# documents}}", @@ -38,6 +47,7 @@ "SelectSpace": "Sélectionner l'espace", "NoSelectedDocuments": "Aucun document sélectionné", "RequestPermissionToImport": "Veuillez demander l'autorisation au propriétaire de l'espace de travail cible pour importer des documents.", - "SkipDeletedObsolete": "Ignorer les documents archivés ou obsolètes" + "SkipDeletedObsolete": "Ignorer les documents archivés ou obsolètes", + "ExportResultRecordTitle": "Export de documents depuis l'espace de travail {workspace} ({date})" } } diff --git a/plugins/export-assets/lang/it.json b/plugins/export-assets/lang/it.json index 3db1e4eaf3..1e23f72865 100644 --- a/plugins/export-assets/lang/it.json +++ b/plugins/export-assets/lang/it.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Esportazione avviata. Riceverai una notifica quando sarà completata.", "ExportToWorkspaceCompleted": "Esportazione completata", "ExportToWorkspaceCompletedMessage": "Esportazione completata con successo.", + "ImportToWorkspaceNotificationMessage": "{count} documenti importati nel tuo spazio di lavoro", + "ImportCompleted": "Documenti importati nell'area di lavoro", + "SourceWorkspace": "Spazio di lavoro di origine", + "ExportedCount": "Conteggio esportato", + "ExportedDocumentIds": "ID documenti esportati", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 documento è stato importato dallo spazio di lavoro {workspace}} other {# documenti sono stati importati dallo spazio di lavoro {workspace}}}:", + "ExportedDocumentClass": "Classe documenti esportati", + "Import": "Importa", + "ImportedDocuments": "Nuovi documenti", "ExportToWorkspaceFailed": "Esportazione fallita", "SelectWorkspace": "Seleziona spazio di lavoro", "SelectWorkspaceToExport": "Seleziona spazio di lavoro per esportare {count, plural, =1 {1 documento} other {# documenti}}", @@ -38,6 +47,7 @@ "SelectSpace": "Seleziona spazio", "NoSelectedDocuments": "Nessun documento selezionato", "RequestPermissionToImport": "Si prega di richiedere l'autorizzazione al proprietario dello spazio di lavoro di destinazione per importare documenti.", - "SkipDeletedObsolete": "Salta documenti archivati o obsoleti" + "SkipDeletedObsolete": "Salta documenti archivati o obsoleti", + "ExportResultRecordTitle": "Esportazione di documenti dallo spazio di lavoro {workspace} ({date})" } } diff --git a/plugins/export-assets/lang/ja.json b/plugins/export-assets/lang/ja.json index 39a9f11645..346b9148fd 100644 --- a/plugins/export-assets/lang/ja.json +++ b/plugins/export-assets/lang/ja.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "エクスポートを開始しました。完了すると通知が届きます。", "ExportToWorkspaceCompleted": "エクスポート完了", "ExportToWorkspaceCompletedMessage": "エクスポートが正常に完了しました。", + "ImportToWorkspaceNotificationMessage": "{count} 件のドキュメントがワークスペースにインポートされました", + "ImportCompleted": "ドキュメントがワークスペースにインポートされました", + "SourceWorkspace": "ソースワークスペース", + "ExportedCount": "エクスポート数", + "ExportedDocumentIds": "エクスポートされたドキュメントのID", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 件のドキュメントがワークスペース {workspace} からインポートされました} other {# 件のドキュメントがワークスペース {workspace} からインポートされました}}:", + "ExportedDocumentClass": "エクスポートされたドキュメントのクラス", + "Import": "インポート", + "ImportedDocuments": "新しいドキュメント", "ExportToWorkspaceFailed": "エクスポート失敗", "SelectWorkspace": "ワークスペースを選択", "SelectWorkspaceToExport": "{count, plural, =1 {1 件のドキュメント} other {# 件のドキュメント}}をエクスポートするワークスペースを選択", @@ -38,6 +47,7 @@ "SelectSpace": "スペースを選択", "NoSelectedDocuments": "ドキュメントが選択されていません", "RequestPermissionToImport": "ドキュメントをインポートするには、対象ワークスペースの所有者に許可をリクエストしてください。", - "SkipDeletedObsolete": "アーカイブ済みまたは廃止されたドキュメントをスキップ" + "SkipDeletedObsolete": "アーカイブ済みまたは廃止されたドキュメントをスキップ", + "ExportResultRecordTitle": "ワークスペース {workspace} からのドキュメントのエクスポート ({date})" } } diff --git a/plugins/export-assets/lang/pt.json b/plugins/export-assets/lang/pt.json index c863d578b8..ee0d17d35d 100644 --- a/plugins/export-assets/lang/pt.json +++ b/plugins/export-assets/lang/pt.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Exportação iniciada. Você receberá uma notificação quando for concluída.", "ExportToWorkspaceCompleted": "Exportação concluída", "ExportToWorkspaceCompletedMessage": "Exportação concluída com sucesso.", + "ImportToWorkspaceNotificationMessage": "{count} documentos importados para o seu espaço de trabalho", + "ImportCompleted": "Documentos importados para o espaço de trabalho", + "SourceWorkspace": "Espaço de trabalho de origem", + "ExportedCount": "Quantidade exportada", + "ExportedDocumentIds": "IDs dos documentos exportados", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 documento foi importado do espaço de trabalho {workspace}} other {# documentos foram importados do espaço de trabalho {workspace}}}:", + "ExportedDocumentClass": "Classe dos documentos exportados", + "Import": "Importar", + "ImportedDocuments": "Novos documentos", "ExportToWorkspaceFailed": "Exportação falhou", "SelectWorkspace": "Selecionar espaço de trabalho", "SelectWorkspaceToExport": "Selecionar espaço de trabalho para exportar {count, plural, =1 {1 documento} other {# documentos}}", @@ -38,6 +47,7 @@ "SelectSpace": "Selecionar espaço", "NoSelectedDocuments": "Nenhum documento selecionado", "RequestPermissionToImport": "Por favor, solicite permissão ao proprietário do espaço de trabalho de destino para importar documentos.", - "SkipDeletedObsolete": "Pular documentos arquivados ou obsoletos" + "SkipDeletedObsolete": "Pular documentos arquivados ou obsoletos", + "ExportResultRecordTitle": "Exportação de documentos do espaço de trabalho {workspace} ({date})" } } diff --git a/plugins/export-assets/lang/ru.json b/plugins/export-assets/lang/ru.json index d24a9932e5..92754ff9ed 100644 --- a/plugins/export-assets/lang/ru.json +++ b/plugins/export-assets/lang/ru.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Экспорт начат. Вы получите уведомление, когда он будет завершен.", "ExportToWorkspaceCompleted": "Экспорт завершен", "ExportToWorkspaceCompletedMessage": "Экспорт успешно завершен.", + "ImportToWorkspaceNotificationMessage": "{count} документов импортировано в ваше рабочее пространство", + "ImportCompleted": "Документы импортированы в рабочее пространство", + "SourceWorkspace": "Исходное рабочее пространство", + "ExportedCount": "Количество экспортированных", + "ExportedDocumentIds": "ID экспортированных документов", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 документ импортирован из рабочего пространства {workspace}} few {# документа импортированы из рабочего пространства {workspace}} other {# документов импортировано из рабочего пространства {workspace}}}:", + "ExportedDocumentClass": "Класс экспортированных документов", + "Import": "Импорт", + "ImportedDocuments": "Новые документы", "ExportToWorkspaceFailed": "Экспорт не удался", "SelectWorkspace": "Выбрать рабочее пространство", "SelectWorkspaceToExport": "Выбрать рабочее пространство для экспорта {count, plural, =1 {1 документ} one {# документ} few {# документа} other {# документов}}", @@ -38,6 +47,7 @@ "SelectSpace": "Выбрать пространство", "NoSelectedDocuments": "Документы не выбраны", "RequestPermissionToImport": "Пожалуйста, запросите разрешение у владельца рабочего пространства, куда вы хотите экспортировать документы.", - "SkipDeletedObsolete": "Пропускать архивированные или устаревшие документы" + "SkipDeletedObsolete": "Пропускать архивированные или устаревшие документы", + "ExportResultRecordTitle": "Экспорт документов из пространства {workspace} ({date})" } } diff --git a/plugins/export-assets/lang/tr.json b/plugins/export-assets/lang/tr.json index 0482f4c219..67c11baaa8 100644 --- a/plugins/export-assets/lang/tr.json +++ b/plugins/export-assets/lang/tr.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "Dışa aktarma başladı. Tamamlandığında bir bildirim alacaksınız.", "ExportToWorkspaceCompleted": "Dışa aktarma tamamlandı", "ExportToWorkspaceCompletedMessage": "Dışa aktarma başarıyla tamamlandı.", + "ImportToWorkspaceNotificationMessage": "{count} belge çalışma alanınıza içe aktarıldı", + "ImportCompleted": "Belgeler çalışma alanına içe aktarıldı", + "SourceWorkspace": "Kaynak çalışma alanı", + "ExportedCount": "Dışa aktarılan sayı", + "ExportedDocumentIds": "Dışa aktarılan belge kimlikleri", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 belge {workspace} çalışma alanından içe aktarıldı} other {# belge {workspace} çalışma alanından içe aktarıldı}}:", + "ExportedDocumentClass": "Dışa aktarılan belge sınıfı", + "Import": "İçe aktarma", + "ImportedDocuments": "Yeni belgeler", "ExportToWorkspaceFailed": "Dışa aktarma başarısız oldu", "SelectWorkspace": "Çalışma alanı seç", "SelectWorkspaceToExport": "{count, plural, =1 {1 belgeyi} other {# belgeyi}} dışa aktarmak için çalışma alanı seç", @@ -38,6 +47,7 @@ "SelectSpace": "Alan seç", "NoSelectedDocuments": "Seçili belge yok", "RequestPermissionToImport": "Belgeleri içe aktarmak için lütfen hedef çalışma alanı sahibinden izin isteyin.", - "SkipDeletedObsolete": "Arşivlenmiş veya kullanımdan kaldırılmış belgeleri atla" + "SkipDeletedObsolete": "Arşivlenmiş veya kullanımdan kaldırılmış belgeleri atla", + "ExportResultRecordTitle": "{workspace} çalışma alanından belge dışa aktarma ({date})" } } diff --git a/plugins/export-assets/lang/zh.json b/plugins/export-assets/lang/zh.json index b332bdda97..160c6d28da 100644 --- a/plugins/export-assets/lang/zh.json +++ b/plugins/export-assets/lang/zh.json @@ -31,6 +31,15 @@ "ExportStartedMessage": "导出已开始。完成后您将收到通知。", "ExportToWorkspaceCompleted": "导出完成", "ExportToWorkspaceCompletedMessage": "导出已成功完成。", + "ImportToWorkspaceNotificationMessage": "{count} 个文档已导入到您的工作区", + "ImportCompleted": "文档已导入到工作区", + "SourceWorkspace": "源工作区", + "ExportedCount": "导出数量", + "ExportedDocumentIds": "已导出文档的 ID", + "DocumentsImportedFromWorkspace": "{count, plural, one {1 个文档已从工作区 {workspace} 导入} other {# 个文档已从工作区 {workspace} 导入}}:", + "ExportedDocumentClass": "已导出文档的类", + "Import": "导入", + "ImportedDocuments": "新文档", "ExportToWorkspaceFailed": "导出失败", "SelectWorkspace": "选择工作区", "SelectWorkspaceToExport": "选择工作区以导出 {count, plural, =1 {1 文档} other {# 个文档}}", @@ -38,6 +47,7 @@ "SelectSpace": "选择空间", "NoSelectedDocuments": "未选择文档", "RequestPermissionToImport": "请向目标工作区所有者请求导入文档的权限。", - "SkipDeletedObsolete": "跳过已归档或已过时的文档" + "SkipDeletedObsolete": "跳过已归档或已过时的文档", + "ExportResultRecordTitle": "从工作区 {workspace} 导出文档 ({date})" } } diff --git a/plugins/export-resources/package.json b/plugins/export-resources/package.json index 9d8a0ed2c9..c4d01c5ac6 100644 --- a/plugins/export-resources/package.json +++ b/plugins/export-resources/package.json @@ -44,6 +44,10 @@ "@hcengineering/ui": "workspace:^0.7.0", "@hcengineering/export": "workspace:^0.7.0", "@hcengineering/login": "workspace:^0.7.0", - "@hcengineering/theme": "workspace:^0.7.0" + "@hcengineering/notification": "workspace:^0.7.0", + "@hcengineering/panel": "workspace:^0.7.0", + "@hcengineering/theme": "workspace:^0.7.0", + "@hcengineering/view": "workspace:^0.7.0", + "@hcengineering/view-resources": "workspace:^0.7.0" } } diff --git a/plugins/export-resources/src/components/ExportResultPanel.svelte b/plugins/export-resources/src/components/ExportResultPanel.svelte new file mode 100644 index 0000000000..d2b8d5554a --- /dev/null +++ b/plugins/export-resources/src/components/ExportResultPanel.svelte @@ -0,0 +1,142 @@ + + + +{#if object} + dispatch('close')} + withoutInput + withoutActivity + > + + +
+ {object.title ?? + `Import completed – ${object.exportedCount} document${object.exportedCount !== 1 ? 's' : ''}`} +
+
+
+ + +