Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da86e5a147 |
@@ -3189,6 +3189,7 @@ msgstr "Knipbord vereis 'n veilige verbinding (HTTPS). Gebruik asseblief hierdie
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Maak toe"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Sluit banier"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Maak sypaneel toe"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Meer"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Maak Outlook oop"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Maak sypaneel oop"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "تتطلب الحافظة اتصالاً آمناً (HTTPS). يُرجى
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "إغلاق"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "إغلاق اللافتة"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "إغلاق اللوحة الجانبية"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "المزيد"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "فتح Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "فتح اللوحة الجانبية"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "El porta-retalls requereix una connexió segura (HTTPS). Accedeix a aque
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Tanca"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Tanca el bàner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Tanca el panell lateral"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Més"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Obrir en Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Obre el panell lateral"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Schránka vyžaduje zabezpečené připojení (HTTPS). Pro povolení kop
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Zavřít"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Zavřít banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Zavřít postranní panel"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Více"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Otevřít Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Otevřít postranní panel"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Udklipsholderen kræver en sikker forbindelse (HTTPS). Tilgå venligst d
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Luk"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Luk banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Luk sidepanelet"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Mere"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Åbn Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Åbn sidepanelet"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Die Zwischenablage erfordert eine sichere Verbindung (HTTPS). Bitte grei
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Banner schließen"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Seitenpanel schließen"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Mehr"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Outlook öffnen"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Seitenpanel öffnen"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Το Πρόχειρο απαιτεί ασφαλή σύνδεση (HTTPS)
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Κλείσιμο"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Κλείσιμο banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Κλείσιμο πλευρικού πάνελ"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Περισσότερα"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Άνοιγμα με Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Άνοιγμα πλευρικού πάνελ"
|
||||
|
||||
@@ -3184,6 +3184,7 @@ msgstr "Clipboard requires a secure connection (HTTPS). Please access this app o
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Close"
|
||||
|
||||
@@ -3194,6 +3195,7 @@ msgid "Close banner"
|
||||
msgstr "Close banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Close side panel"
|
||||
@@ -9274,6 +9276,7 @@ msgstr "months"
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "More"
|
||||
|
||||
@@ -10716,6 +10719,7 @@ msgid "Open Outlook"
|
||||
msgstr "Open Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Open side panel"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "El portapapeles requiere una conexión segura (HTTPS). Accede a esta apl
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Cerrar banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Cerrar panel lateral"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Más"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Abrir Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Abrir panel lateral"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Leikepöytä edellyttää suojattua yhteyttä (HTTPS). Avaa tämä sovel
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Sulje"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Sulje banneri"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Sulje sivupaneeli"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Lisää"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Avaa Outlookissa"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Avaa sivupaneeli"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Le presse-papiers nécessite une connexion sécurisée (HTTPS). Veuillez
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Fermer"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Fermer la bannière"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Fermer le panneau latéral"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Plus"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Ouvrir Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Ouvrir le panneau latéral"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "לוח הגזירים דורש חיבור מאובטח (HTTPS). יש ל
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "סגור"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "סגור באנר"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "סגור את חלונית הצד"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "עוד"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "פתח ב-Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "פתח את חלונית הצד"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr ""
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Bezár"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Banner bezárása"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Oldalsó panel bezárása"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Több"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Outlook megnyitása"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Oldalsó panel megnyitása"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Gli appunti richiedono una connessione sicura (HTTPS). Accedi a questa a
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Chiudi"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Chiudi banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Chiudi il pannello laterale"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Altro"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Aprire Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Apri il pannello laterale"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "クリップボードの使用にはセキュアな接続 (HTTPS) が必
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "閉じる"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "バナーを閉じる"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "サイドパネルを閉じる"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "もっと"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Outlookで開く"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "サイドパネルを開く"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "클립보드는 보안 연결(HTTPS)이 필요합니다. 복사 기능
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "닫기"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "배너 닫기"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "사이드 패널 닫기"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "더보기"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Outlook 열기"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "사이드 패널 열기"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr ""
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Sluiten"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Banner sluiten"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Zijpaneel sluiten"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Meer"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Open Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Zijpaneel openen"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Utklippstavlen krever en sikker tilkobling (HTTPS). Åpne denne appen ov
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Lukk"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Lukk banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Lukk sidepanelet"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Mer"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Åpne Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Åpne sidepanelet"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Schowek wymaga bezpiecznego połączenia (HTTPS). Otwórz tę aplikację
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Zamknij"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Zamknij baner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Zamknij panel boczny"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Więcej"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Otwórz Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Otwórz panel boczny"
|
||||
|
||||
@@ -3184,6 +3184,7 @@ msgstr ""
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
@@ -3194,6 +3195,7 @@ msgid "Close banner"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr ""
|
||||
@@ -9274,6 +9276,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr ""
|
||||
|
||||
@@ -10716,6 +10719,7 @@ msgid "Open Outlook"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr ""
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "A área de transferência requer uma conexão segura (HTTPS). Acesse est
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Fechar"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Fechar banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr ""
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Mais"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Abrir Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr ""
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "A área de transferência requer uma conexão segura (HTTPS). Acesse est
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Fechar"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Fechar banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Fechar painel lateral"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Mais"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Abrir Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Abrir painel lateral"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Clipboardul necesită o conexiune securizată (HTTPS). Accesați aceast
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Închide"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Închide bannerul"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Închide panoul lateral"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Mai mult"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Deschide Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Deschide panoul lateral"
|
||||
|
||||
Binary file not shown.
@@ -3189,6 +3189,7 @@ msgstr "Клипборд захтева безбедну везу (HTTPS). Пр
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Затвори"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Затвори банер"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Затвори бочни панел"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Још"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Отвори Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Отвори бочни панел"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Urklipp kräver en säker anslutning (HTTPS). Öppna den här appen via
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Stäng"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Stäng banner"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Stäng sidopanelen"
|
||||
@@ -9281,6 +9283,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Mer"
|
||||
|
||||
@@ -10723,6 +10726,7 @@ msgid "Open Outlook"
|
||||
msgstr "Öppna Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Öppna sidopanelen"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Pano güvenli bir bağlantı (HTTPS) gerektirir. Kopyalamayı etkinleşt
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Kapat"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Afişi kapat"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Yan paneli kapat"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Daha Fazla"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Outlook'u Aç"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Yan paneli aç"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Буфер обміну вимагає захищеного з'єдна
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Закрити"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Закрити банер"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Закрити бічну панель"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Більше"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Відкрити Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Відкрити бічну панель"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "Bộ nhớ tạm yêu cầu kết nối bảo mật (HTTPS). Vui lòng t
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "Đóng"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "Đóng biểu ngữ"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "Đóng ngăn bên"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "Thêm nữa"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "Mở Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "Mở ngăn bên"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "剪贴板功能需要安全连接 (HTTPS)。请通过 HTTPS 访问此应
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "关闭"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "关闭横幅"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "关闭侧边栏"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "更多"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "打开Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "打开侧边栏"
|
||||
|
||||
@@ -3189,6 +3189,7 @@ msgstr "剪貼簿需要安全連線 (HTTPS)。請透過 HTTPS 存取此應用程
|
||||
|
||||
#. js-lingui-id: yz7wBu
|
||||
#: src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close"
|
||||
msgstr "關閉"
|
||||
|
||||
@@ -3199,6 +3200,7 @@ msgid "Close banner"
|
||||
msgstr "關閉橫幅"
|
||||
|
||||
#. js-lingui-id: saip/v
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Close side panel"
|
||||
msgstr "關閉側邊面板"
|
||||
@@ -9279,6 +9281,7 @@ msgstr ""
|
||||
#. js-lingui-id: 2FYpfJ
|
||||
#: src/pages/settings/ai/SettingsAI.tsx
|
||||
#: src/modules/ui/layout/tab-list/components/TabMoreButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "More"
|
||||
msgstr "更多"
|
||||
|
||||
@@ -10721,6 +10724,7 @@ msgid "Open Outlook"
|
||||
msgstr "打開Outlook"
|
||||
|
||||
#. js-lingui-id: bY2Xkv
|
||||
#: src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
|
||||
#: src/modules/side-panel/components/SidePanelToggleButton.tsx
|
||||
msgid "Open side panel"
|
||||
msgstr "開啟側邊面板"
|
||||
|
||||
-6
@@ -29,12 +29,6 @@ export const CommandMenuItemErrorBoundary = ({
|
||||
const { captureException } = await import('@sentry/react');
|
||||
captureException(error, (scope) => {
|
||||
scope.setTag('component', 'CommandMenuItem');
|
||||
scope.setTag('commandMenuItemId', commandMenuItemId);
|
||||
scope.setLevel('warning');
|
||||
scope.setFingerprint([
|
||||
'command-menu-item-error',
|
||||
error.message,
|
||||
]);
|
||||
|
||||
return scope;
|
||||
});
|
||||
|
||||
+3
-13
@@ -11,14 +11,13 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconSquareCheck,
|
||||
IconSquareX,
|
||||
} from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const DROPDOWN_ID = 'command-menu-edit-record-selection-dropdown';
|
||||
|
||||
@@ -56,7 +55,6 @@ export const CommandMenuItemEditRecordSelectionDropdown = ({
|
||||
isRecordPage = false,
|
||||
}: CommandMenuItemEditRecordSelectionDropdownProps) => {
|
||||
const { t } = useLingui();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const mainContextStoreHasSelectedRecords = useAtomStateValue(
|
||||
@@ -94,17 +92,9 @@ export const CommandMenuItemEditRecordSelectionDropdown = ({
|
||||
disabled={isRecordPage}
|
||||
data-click-outside-id={COMMAND_MENU_DROPDOWN_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
<TriggerIcon
|
||||
size={16}
|
||||
color={theme.font.color.primary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
<TriggerIcon size={16} />
|
||||
<StyledLabel>{triggerLabel}</StyledLabel>
|
||||
<IconChevronDown
|
||||
size={16}
|
||||
color={theme.font.color.primary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
<IconChevronDown size={16} />
|
||||
</StyledClickableArea>
|
||||
}
|
||||
dropdownPlacement="bottom-start"
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export const SeeVersionWorkflowRunSingleRecordCommand = () => {
|
||||
!isDefined(selectedRecord) ||
|
||||
!isDefined(selectedRecord?.workflowVersion?.id)
|
||||
) {
|
||||
return null;
|
||||
throw new Error('Selected record is required to see version workflow run');
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+2
-5
@@ -1,5 +1,5 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useDeferredValue, useState, type ReactNode } from 'react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
@@ -25,15 +25,12 @@ export const NavigationMenuItemFolderLayout = ({
|
||||
}: NavigationMenuItemFolderLayoutProps) => {
|
||||
const [skipInitialExpandAnimation] = useState(() => isOpen);
|
||||
|
||||
const deferredIsOpen = useDeferredValue(isOpen);
|
||||
const isExpandedForAnimation = isOpen ? deferredIsOpen : false;
|
||||
|
||||
return (
|
||||
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
|
||||
{header}
|
||||
<StyledFolderExpandableWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isExpandedForAnimation}
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
|
||||
@@ -1,48 +1,15 @@
|
||||
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { widgetCreationTargetTabIdComponentState } from '@/page-layout/states/widgetCreationTargetTabIdComponentState';
|
||||
import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
export const useNavigateToMoreWidgets = () => {
|
||||
const { tabId } = usePageLayoutContentContext();
|
||||
|
||||
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
|
||||
|
||||
const pageLayoutEditingWidgetIdState = useAtomComponentStateCallbackState(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
);
|
||||
|
||||
const widgetCreationTargetTabIdState = useAtomComponentStateCallbackState(
|
||||
widgetCreationTargetTabIdComponentState,
|
||||
);
|
||||
|
||||
const widgetInsertionContextState = useAtomComponentStateCallbackState(
|
||||
widgetInsertionContextComponentState,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const navigateToMoreWidgets = useCallback(() => {
|
||||
store.set(pageLayoutEditingWidgetIdState, null);
|
||||
store.set(widgetInsertionContextState, null);
|
||||
store.set(widgetCreationTargetTabIdState, tabId);
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordPageWidgetTypeSelect,
|
||||
});
|
||||
}, [
|
||||
navigatePageLayoutSidePanel,
|
||||
pageLayoutEditingWidgetIdState,
|
||||
store,
|
||||
tabId,
|
||||
widgetCreationTargetTabIdState,
|
||||
widgetInsertionContextState,
|
||||
]);
|
||||
}, [navigatePageLayoutSidePanel]);
|
||||
|
||||
return { navigateToMoreWidgets };
|
||||
};
|
||||
|
||||
@@ -7,12 +7,10 @@ import { type PageLayoutAddTabStrategy } from '@/page-layout/types/PageLayoutAdd
|
||||
import { isReactivatableTab } from '@/page-layout/utils/isReactivatableTab';
|
||||
import { shouldEnableTabEditingFeatures } from '@/page-layout/utils/shouldEnableTabEditingFeatures';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -42,17 +40,8 @@ export const usePageLayoutAddTabStrategy = ({
|
||||
|
||||
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
|
||||
|
||||
const { isInSidePanel } = useLayoutRenderingContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onCreate = useCallback(() => {
|
||||
const newTabId = createPageLayoutTab(t`Untitled`);
|
||||
|
||||
if (!isInSidePanel) {
|
||||
navigate(`#${newTabId}`);
|
||||
}
|
||||
|
||||
setPageLayoutTabSettingsOpenTabId(newTabId);
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutTabSettings,
|
||||
@@ -60,8 +49,6 @@ export const usePageLayoutAddTabStrategy = ({
|
||||
});
|
||||
}, [
|
||||
createPageLayoutTab,
|
||||
isInSidePanel,
|
||||
navigate,
|
||||
setPageLayoutTabSettingsOpenTabId,
|
||||
navigatePageLayoutSidePanel,
|
||||
]);
|
||||
|
||||
+47
-37
@@ -15,7 +15,9 @@ import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layo
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { evictViewMetadataForViewIds } from '@/page-layout/utils/evictViewMetadataForViewIds';
|
||||
import { toDraftPageLayout } from '@/page-layout/utils/toDraftPageLayout';
|
||||
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
@@ -74,47 +76,55 @@ export const useRefreshPageLayoutAfterReset = (
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const refreshPageLayoutAfterReset = useCallback(async () => {
|
||||
const { data } = await client.query({
|
||||
query: FindOnePageLayoutDocument,
|
||||
variables: { id: pageLayoutId },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const refreshPageLayoutAfterReset = useCallback(
|
||||
async (collectAffectedViewIds: (layout: PageLayout) => Set<string>) => {
|
||||
const { data } = await client.query({
|
||||
query: FindOnePageLayoutDocument,
|
||||
variables: { id: pageLayoutId },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (isDefined(data?.getPageLayout)) {
|
||||
const freshLayout = transformPageLayout(data.getPageLayout);
|
||||
let affectedViewIds = new Set<string>();
|
||||
|
||||
store.set(pageLayoutPersistedState, freshLayout);
|
||||
store.set(pageLayoutDraftState, toDraftPageLayout(freshLayout));
|
||||
store.set(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
convertPageLayoutToTabLayouts(freshLayout),
|
||||
);
|
||||
}
|
||||
if (isDefined(data?.getPageLayout)) {
|
||||
const freshLayout = transformPageLayout(data.getPageLayout);
|
||||
|
||||
store.set(fieldsWidgetGroupsDraftState, {});
|
||||
store.set(fieldsWidgetUngroupedFieldsDraftState, {});
|
||||
store.set(fieldsWidgetEditorModeDraftState, {});
|
||||
store.set(hasInitializedFieldsWidgetGroupsDraftState, {});
|
||||
affectedViewIds = collectAffectedViewIds(freshLayout);
|
||||
|
||||
setIsPageLayoutInEditMode(false);
|
||||
exitLayoutCustomizationMode();
|
||||
invalidateMetadataStore();
|
||||
}, [
|
||||
client,
|
||||
pageLayoutId,
|
||||
store,
|
||||
pageLayoutPersistedState,
|
||||
pageLayoutDraftState,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
fieldsWidgetGroupsDraftState,
|
||||
fieldsWidgetUngroupedFieldsDraftState,
|
||||
fieldsWidgetEditorModeDraftState,
|
||||
hasInitializedFieldsWidgetGroupsDraftState,
|
||||
setIsPageLayoutInEditMode,
|
||||
exitLayoutCustomizationMode,
|
||||
invalidateMetadataStore,
|
||||
]);
|
||||
store.set(pageLayoutPersistedState, freshLayout);
|
||||
store.set(pageLayoutDraftState, toDraftPageLayout(freshLayout));
|
||||
store.set(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
convertPageLayoutToTabLayouts(freshLayout),
|
||||
);
|
||||
}
|
||||
|
||||
store.set(fieldsWidgetGroupsDraftState, {});
|
||||
store.set(fieldsWidgetUngroupedFieldsDraftState, {});
|
||||
store.set(fieldsWidgetEditorModeDraftState, {});
|
||||
store.set(hasInitializedFieldsWidgetGroupsDraftState, {});
|
||||
|
||||
setIsPageLayoutInEditMode(false);
|
||||
exitLayoutCustomizationMode();
|
||||
evictViewMetadataForViewIds(store, affectedViewIds);
|
||||
invalidateMetadataStore();
|
||||
},
|
||||
[
|
||||
client,
|
||||
pageLayoutId,
|
||||
store,
|
||||
pageLayoutPersistedState,
|
||||
pageLayoutDraftState,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
fieldsWidgetGroupsDraftState,
|
||||
fieldsWidgetUngroupedFieldsDraftState,
|
||||
fieldsWidgetEditorModeDraftState,
|
||||
hasInitializedFieldsWidgetGroupsDraftState,
|
||||
setIsPageLayoutInEditMode,
|
||||
exitLayoutCustomizationMode,
|
||||
invalidateMetadataStore,
|
||||
],
|
||||
);
|
||||
|
||||
return { refreshPageLayoutAfterReset };
|
||||
};
|
||||
|
||||
+6
-1
@@ -7,6 +7,7 @@ import { CrudOperationType } from 'twenty-shared/types';
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { RESET_PAGE_LAYOUT_TAB_TO_DEFAULT } from '@/page-layout/graphql/mutations/resetPageLayoutTabToDefault';
|
||||
import { useRefreshPageLayoutAfterReset } from '@/page-layout/hooks/useRefreshPageLayoutAfterReset';
|
||||
import { collectViewIdsFromWidgets } from '@/page-layout/utils/collectViewIdsFromWidgets';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
export const useResetPageLayoutTabToDefault = (
|
||||
@@ -23,7 +24,11 @@ export const useResetPageLayoutTabToDefault = (
|
||||
async (tabId: string) => {
|
||||
try {
|
||||
await resetMutation({ variables: { id: tabId } });
|
||||
await refreshPageLayoutAfterReset();
|
||||
await refreshPageLayoutAfterReset((layout) =>
|
||||
collectViewIdsFromWidgets(
|
||||
layout.tabs.find((tab) => tab.id === tabId)?.widgets ?? [],
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
handleMetadataError(error, {
|
||||
|
||||
@@ -10,6 +10,9 @@ import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidate
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { RESET_PAGE_LAYOUT_TO_DEFAULT } from '@/page-layout/graphql/mutations/resetPageLayoutToDefault';
|
||||
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { collectViewIdsFromWidgets } from '@/page-layout/utils/collectViewIdsFromWidgets';
|
||||
import { evictViewMetadataForViewIds } from '@/page-layout/utils/evictViewMetadataForViewIds';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
export const useResetPageLayoutToDefault = () => {
|
||||
@@ -20,10 +23,22 @@ export const useResetPageLayoutToDefault = () => {
|
||||
const store = useStore();
|
||||
|
||||
const resetPageLayoutToDefault = useCallback(
|
||||
async ({ pageLayoutId }: { pageLayoutId: string }) => {
|
||||
async ({
|
||||
pageLayoutId,
|
||||
pageLayout,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
pageLayout: PageLayout;
|
||||
}) => {
|
||||
const preResetViewIds = collectViewIdsFromWidgets(
|
||||
pageLayout.tabs.flatMap((tab) => tab.widgets),
|
||||
);
|
||||
|
||||
try {
|
||||
await resetMutation({ variables: { id: pageLayoutId } });
|
||||
|
||||
evictViewMetadataForViewIds(store, preResetViewIds);
|
||||
|
||||
if (isDefined(pageLayoutId)) {
|
||||
store.set(
|
||||
pageLayoutIsInitializedComponentState.atomFamily({
|
||||
|
||||
+8
-1
@@ -7,6 +7,7 @@ import { ResetPageLayoutWidgetToDefaultDocument } from '~/generated-metadata/gra
|
||||
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useRefreshPageLayoutAfterReset } from '@/page-layout/hooks/useRefreshPageLayoutAfterReset';
|
||||
import { collectViewIdsFromWidgets } from '@/page-layout/utils/collectViewIdsFromWidgets';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
export const useResetPageLayoutWidgetToDefault = (
|
||||
@@ -23,7 +24,13 @@ export const useResetPageLayoutWidgetToDefault = (
|
||||
async (widgetId: string) => {
|
||||
try {
|
||||
await resetMutation({ variables: { id: widgetId } });
|
||||
await refreshPageLayoutAfterReset();
|
||||
await refreshPageLayoutAfterReset((layout) =>
|
||||
collectViewIdsFromWidgets(
|
||||
layout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.filter((widget) => widget.id === widgetId),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
handleMetadataError(error, {
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const widgetCreationTargetTabIdComponentState = createAtomComponentState<
|
||||
string | null
|
||||
>({
|
||||
key: 'widgetCreationTargetTabIdComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
import { createDefaultFieldWidget } from '@/page-layout/utils/createDefaultFieldWidget';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('createDefaultFieldWidget', () => {
|
||||
it('should return a FIELD widget with CARD display mode by default', () => {
|
||||
const widget = createDefaultFieldWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Company Name',
|
||||
fieldMetadataId: 'field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
positionIndex: 0,
|
||||
});
|
||||
|
||||
expect(widget).toMatchObject({
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Company Name',
|
||||
isActive: true,
|
||||
type: WidgetType.FIELD,
|
||||
configuration: {
|
||||
__typename: 'FieldConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: 'field-1',
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: 0,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
deletedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the provided fieldDisplayMode', () => {
|
||||
const widget = createDefaultFieldWidget({
|
||||
id: 'widget-2',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Description',
|
||||
fieldMetadataId: 'field-2',
|
||||
fieldDisplayMode: FieldDisplayMode.EDITOR,
|
||||
objectMetadataId: 'object-1',
|
||||
positionIndex: 1,
|
||||
});
|
||||
|
||||
expect(widget.configuration).toMatchObject({
|
||||
fieldDisplayMode: FieldDisplayMode.EDITOR,
|
||||
});
|
||||
expect(widget.position).toMatchObject({ index: 1 });
|
||||
});
|
||||
});
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('createDefaultFieldsWidget', () => {
|
||||
it('should return a FIELDS widget with correct properties', () => {
|
||||
const widget = createDefaultFieldsWidget({
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
viewId: 'view-1',
|
||||
objectMetadataId: 'object-1',
|
||||
positionIndex: 3,
|
||||
});
|
||||
|
||||
expect(widget).toMatchObject({
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Fields',
|
||||
isActive: true,
|
||||
type: WidgetType.FIELDS,
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: 'view-1',
|
||||
},
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: 3,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
deletedAt: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { createDefaultFrontComponentWidget } from '@/page-layout/utils/createDefaultFrontComponentWidget';
|
||||
import {
|
||||
type GridPosition,
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('createDefaultFrontComponentWidget', () => {
|
||||
it('should return a FRONT_COMPONENT widget with grid position mapped to position', () => {
|
||||
const gridPosition: GridPosition = {
|
||||
__typename: 'GridPosition',
|
||||
row: 2,
|
||||
column: 3,
|
||||
rowSpan: 4,
|
||||
columnSpan: 6,
|
||||
};
|
||||
|
||||
const widget = createDefaultFrontComponentWidget(
|
||||
'widget-1',
|
||||
'tab-1',
|
||||
'My Component',
|
||||
'front-comp-1',
|
||||
gridPosition,
|
||||
);
|
||||
|
||||
expect(widget).toMatchObject({
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'My Component',
|
||||
isActive: true,
|
||||
type: WidgetType.FRONT_COMPONENT,
|
||||
configuration: {
|
||||
__typename: 'FrontComponentConfiguration',
|
||||
configurationType: WidgetConfigurationType.FRONT_COMPONENT,
|
||||
frontComponentId: 'front-comp-1',
|
||||
},
|
||||
gridPosition,
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetGridPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 2,
|
||||
column: 3,
|
||||
rowSpan: 4,
|
||||
columnSpan: 6,
|
||||
},
|
||||
objectMetadataId: null,
|
||||
deletedAt: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { isReactivatableTab } from '@/page-layout/utils/isReactivatableTab';
|
||||
|
||||
const makeTab = (overrides: Partial<PageLayoutTab> = {}): PageLayoutTab =>
|
||||
({
|
||||
id: 'tab-1',
|
||||
applicationId: 'app-1',
|
||||
title: 'Tab',
|
||||
isActive: true,
|
||||
position: 0,
|
||||
pageLayoutId: '',
|
||||
widgets: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
...overrides,
|
||||
}) as unknown as PageLayoutTab;
|
||||
|
||||
describe('isReactivatableTab', () => {
|
||||
it('should return true when tab is inactive and applicationId matches', () => {
|
||||
const tab = makeTab({ isActive: false, applicationId: 'app-1' });
|
||||
|
||||
expect(isReactivatableTab({ tab, objectApplicationId: 'app-1' })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when tab is active', () => {
|
||||
const tab = makeTab({ isActive: true, applicationId: 'app-1' });
|
||||
|
||||
expect(isReactivatableTab({ tab, objectApplicationId: 'app-1' })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when applicationId does not match', () => {
|
||||
const tab = makeTab({ isActive: false, applicationId: 'app-1' });
|
||||
|
||||
expect(isReactivatableTab({ tab, objectApplicationId: 'app-2' })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when objectApplicationId is undefined', () => {
|
||||
const tab = makeTab({ isActive: false, applicationId: 'app-1' });
|
||||
|
||||
expect(isReactivatableTab({ tab, objectApplicationId: undefined })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
|
||||
export const collectViewIdsFromWidgets = (
|
||||
widgets: PageLayoutWidget[],
|
||||
): Set<string> => {
|
||||
const viewIds = new Set<string>();
|
||||
|
||||
for (const widget of widgets) {
|
||||
const viewId = getWidgetConfigurationViewId(widget.configuration);
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
viewIds.add(viewId);
|
||||
}
|
||||
}
|
||||
|
||||
return viewIds;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type MetadataEntityKey,
|
||||
metadataStoreState,
|
||||
} from '@/metadata-store/states/metadataStoreState';
|
||||
|
||||
const VIEW_RELATED_METADATA_KEYS: MetadataEntityKey[] = [
|
||||
'viewFields',
|
||||
'viewFieldGroups',
|
||||
];
|
||||
|
||||
export const evictViewMetadataForViewIds = (
|
||||
store: ReturnType<typeof useStore>,
|
||||
viewIds: Set<string>,
|
||||
) => {
|
||||
if (viewIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of VIEW_RELATED_METADATA_KEYS) {
|
||||
store.set(metadataStoreState.atomFamily(key), (prev) => ({
|
||||
...prev,
|
||||
current: (prev.current as { viewId?: string }[]).filter(
|
||||
(item) => !isDefined(item.viewId) || !viewIds.has(item.viewId),
|
||||
),
|
||||
currentCollectionHash: undefined,
|
||||
}));
|
||||
}
|
||||
};
|
||||
-1
@@ -36,7 +36,6 @@ const StyledHeader = styled.div`
|
||||
`;
|
||||
|
||||
const StyledMenuItemList = styled.div`
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
margin-top: ${themeCssVariables.spacing[2]};
|
||||
|
||||
+1
@@ -77,6 +77,7 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
|
||||
|
||||
await resetPageLayoutToDefault({
|
||||
pageLayoutId: pageLayout.id,
|
||||
pageLayout,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+28
-140
@@ -1,165 +1,53 @@
|
||||
import { SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE } from '@/side-panel/constants/SidePanelTopBarHeightMobile';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderSidePanelButtonClickOutsideId';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useContext } from 'react';
|
||||
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/display';
|
||||
import {
|
||||
AppTooltip,
|
||||
IconLayoutSidebarRightExpand,
|
||||
IconX,
|
||||
TooltipDelay,
|
||||
TooltipPosition,
|
||||
} from 'twenty-ui/display';
|
||||
import { AnimatedButton } from 'twenty-ui/input';
|
||||
import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledButtonWrapper = styled.div<{ alignToTop: boolean }>`
|
||||
align-items: ${({ alignToTop }) => (alignToTop ? 'center' : 'initial')};
|
||||
display: ${({ alignToTop }) => (alignToTop ? 'flex' : 'block')};
|
||||
height: ${({ alignToTop }) =>
|
||||
alignToTop ? `${SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE}px` : 'auto'};
|
||||
position: ${({ alignToTop }) => (alignToTop ? 'fixed' : 'static')};
|
||||
right: ${({ alignToTop }) =>
|
||||
alignToTop ? themeCssVariables.spacing[3] : 'auto'};
|
||||
top: ${({ alignToTop }) => (alignToTop ? '0' : 'auto')};
|
||||
z-index: ${RootStackingContextZIndices.SidePanelButton};
|
||||
`;
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { AnimatedIconCrossfade, useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledTooltipWrapper = styled.div`
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
`;
|
||||
|
||||
const xPaths = {
|
||||
topLeft: `M12 12 L6 6`,
|
||||
topRight: `M12 12 L18 6`,
|
||||
bottomLeft: `M12 12 L6 18`,
|
||||
bottomRight: `M12 12 L18 18`,
|
||||
};
|
||||
|
||||
const AnimatedIcon = ({
|
||||
isSidePanelOpened,
|
||||
}: {
|
||||
isSidePanelOpened: boolean;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={theme.icon.size.sm}
|
||||
height={theme.icon.size.sm}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={theme.icon.stroke.md}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ display: 'block' }}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<motion.circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r="1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
scale: isSidePanelOpened ? 0 : 1,
|
||||
opacity: isSidePanelOpened ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
}}
|
||||
/>
|
||||
|
||||
{Object.values(xPaths).map((path, index) => (
|
||||
<motion.path
|
||||
key={index}
|
||||
d={path}
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{
|
||||
pathLength: isSidePanelOpened ? 1 : 0,
|
||||
opacity: isSidePanelOpened ? 1 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
ease: 'easeInOut',
|
||||
delay: isSidePanelOpened ? 0.1 : 0,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<motion.circle
|
||||
cx="12"
|
||||
cy="5"
|
||||
r="1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
scale: isSidePanelOpened ? 0 : 1,
|
||||
opacity: isSidePanelOpened ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
}}
|
||||
/>
|
||||
|
||||
<motion.circle
|
||||
cx="12"
|
||||
cy="19"
|
||||
r="1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
scale: isSidePanelOpened ? 0 : 1,
|
||||
opacity: isSidePanelOpened ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidePanelToggleButton = () => {
|
||||
const { toggleSidePanelMenu } = useSidePanelMenu();
|
||||
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const alignWithSidePanelTopBar =
|
||||
isMobile && isLayoutCustomizationModeEnabled && isSidePanelOpened;
|
||||
|
||||
const ariaLabel = isSidePanelOpened
|
||||
? t`Close side panel`
|
||||
: t`Open side panel`;
|
||||
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return (
|
||||
<StyledButtonWrapper alignToTop={alignWithSidePanelTopBar}>
|
||||
<div id="toggle-side-panel-button">
|
||||
<AnimatedButton
|
||||
animatedSvg={<AnimatedIcon isSidePanelOpened={isSidePanelOpened} />}
|
||||
dataClickOutsideId={PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID}
|
||||
dataTestId="page-header-side-panel-button"
|
||||
size={isMobile ? 'medium' : 'small'}
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
hotkeys={[getOsControlSymbol(), 'K']}
|
||||
ariaLabel={ariaLabel}
|
||||
onClick={toggleSidePanelMenu}
|
||||
animate={{
|
||||
rotate: isSidePanelOpened ? 90 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="toggle-side-panel-button">
|
||||
<AnimatedButton
|
||||
animatedSvg={
|
||||
<AnimatedIconCrossfade
|
||||
isActive={isSidePanelOpened}
|
||||
ActiveIcon={IconX}
|
||||
InactiveIcon={IconLayoutSidebarRightExpand}
|
||||
/>
|
||||
}
|
||||
dataClickOutsideId={PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID}
|
||||
dataTestId="page-header-side-panel-button"
|
||||
size={isMobile ? 'medium' : 'small'}
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={isSidePanelOpened ? t`Close` : t`More`}
|
||||
ariaLabel={ariaLabel}
|
||||
onClick={toggleSidePanelMenu}
|
||||
/>
|
||||
<StyledTooltipWrapper>
|
||||
<AppTooltip
|
||||
anchorSelect="#toggle-side-panel-button"
|
||||
@@ -170,6 +58,6 @@ export const SidePanelToggleButton = () => {
|
||||
noArrow
|
||||
/>
|
||||
</StyledTooltipWrapper>
|
||||
</StyledButtonWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+39
-12
@@ -4,12 +4,12 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useInsertCreatedWidgetAtContext } from '@/page-layout/hooks/useInsertCreatedWidgetAtContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { widgetCreationTargetTabIdComponentState } from '@/page-layout/states/widgetCreationTargetTabIdComponentState';
|
||||
import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
|
||||
import { createDefaultFieldWidget } from '@/page-layout/utils/createDefaultFieldWidget';
|
||||
import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget';
|
||||
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
|
||||
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
|
||||
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
|
||||
import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields';
|
||||
@@ -20,8 +20,8 @@ import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { getFrontComponentWidgetTypeSelectItemId } from '@/side-panel/pages/page-layout/utils/getFrontComponentWidgetTypeSelectItemId';
|
||||
import { resolveWidgetTypeSelectTargetTabId } from '@/side-panel/pages/page-layout/utils/resolveWidgetTypeSelectTargetTabId';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
@@ -41,8 +41,11 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
|
||||
const { pageLayoutId, objectNameSingular: targetObjectNameSingular } =
|
||||
usePageLayoutIdFromContextStore();
|
||||
const {
|
||||
pageLayoutId,
|
||||
recordId,
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
} = usePageLayoutIdFromContextStore();
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
@@ -69,9 +72,15 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widgetCreationTargetTabId = useAtomComponentStateValue(
|
||||
widgetCreationTargetTabIdComponentState,
|
||||
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
|
||||
pageLayoutId,
|
||||
layoutType: pageLayoutDraft.type,
|
||||
targetRecordIdentifier: { id: recordId, targetObjectNameSingular: '' },
|
||||
});
|
||||
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
tabListInstanceId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
@@ -87,11 +96,13 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
|
||||
targetObjectNameSingular,
|
||||
);
|
||||
|
||||
const tabId = resolveWidgetTypeSelectTargetTabId({
|
||||
pageLayoutEditingWidgetId,
|
||||
tabs: pageLayoutDraft.tabs,
|
||||
widgetCreationTargetTabId,
|
||||
});
|
||||
const editingWidgetTab = isDefined(pageLayoutEditingWidgetId)
|
||||
? pageLayoutDraft.tabs.find((tab) =>
|
||||
tab.widgets.some((widget) => widget.id === pageLayoutEditingWidgetId),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const tabId = editingWidgetTab?.id ?? activeTabId;
|
||||
|
||||
const isReplaceMode =
|
||||
isDefined(pageLayoutEditingWidgetId) && !isDefined(widgetInsertionContext);
|
||||
@@ -115,7 +126,11 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
|
||||
}, [existingWidget]);
|
||||
|
||||
const removeExistingWidgetIfReplacing = useCallback(() => {
|
||||
if (!isReplaceMode || !isDefined(pageLayoutEditingWidgetId)) {
|
||||
if (
|
||||
!isReplaceMode ||
|
||||
!isDefined(pageLayoutEditingWidgetId) ||
|
||||
!isDefined(tabId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,6 +160,10 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
|
||||
);
|
||||
|
||||
const handleCreateFieldsWidget = useCallback(() => {
|
||||
if (!isDefined(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacePositionIndex = getExistingWidgetPositionIndex();
|
||||
|
||||
const viewId = uuidv4();
|
||||
@@ -191,6 +210,10 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
|
||||
]);
|
||||
|
||||
const handleCreateFieldWidget = useCallback(() => {
|
||||
if (!isDefined(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacePositionIndex = getExistingWidgetPositionIndex();
|
||||
removeExistingWidgetIfReplacing();
|
||||
|
||||
@@ -264,6 +287,10 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
|
||||
|
||||
const handleCreateFrontComponentWidget = useCallback(
|
||||
(frontComponent: FrontComponent) => {
|
||||
if (!isDefined(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacePositionIndex = getExistingWidgetPositionIndex();
|
||||
removeExistingWidgetIfReplacing();
|
||||
|
||||
|
||||
+1
-7
@@ -14,7 +14,6 @@ import { RegularTabSettingsContent } from '@/side-panel/pages/page-layout/compon
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
@@ -32,8 +31,6 @@ export const SidePanelPageLayoutTabSettingsContent = ({
|
||||
}: SidePanelPageLayoutTabSettingsContentProps) => {
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
@@ -132,10 +129,7 @@ export const SidePanelPageLayoutTabSettingsContent = ({
|
||||
onMoveLeft={() => moveLeft(tab.id)}
|
||||
onMoveRight={() => moveRight(tab.id)}
|
||||
onSetAsPinned={() => setAsPinnedTab(tab.id)}
|
||||
onDuplicate={() => {
|
||||
const newTabId = duplicateTab(tab.id);
|
||||
navigate(`#${newTabId}`);
|
||||
}}
|
||||
onDuplicate={() => duplicateTab(tab.id)}
|
||||
onResetToDefault={handleResetToDefault}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { resolveWidgetTypeSelectTargetTabId } from '@/side-panel/pages/page-layout/utils/resolveWidgetTypeSelectTargetTabId';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
const makeTab = (id: string, widgetIds: string[] = []): PageLayoutTab =>
|
||||
({
|
||||
id,
|
||||
applicationId: '',
|
||||
title: id,
|
||||
isActive: true,
|
||||
position: 0,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
pageLayoutId: '',
|
||||
widgets: widgetIds.map((wId) => ({ id: wId })),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
}) as unknown as PageLayoutTab;
|
||||
|
||||
describe('resolveWidgetTypeSelectTargetTabId', () => {
|
||||
it('should return the tab containing the editing widget', () => {
|
||||
const tabs = [
|
||||
makeTab('tab-1', ['widget-a']),
|
||||
makeTab('tab-2', ['widget-b']),
|
||||
];
|
||||
|
||||
const result = resolveWidgetTypeSelectTargetTabId({
|
||||
pageLayoutEditingWidgetId: 'widget-b',
|
||||
tabs,
|
||||
widgetCreationTargetTabId: null,
|
||||
});
|
||||
|
||||
expect(result).toBe('tab-2');
|
||||
});
|
||||
|
||||
it('should throw when the editing widget is not found in any tab', () => {
|
||||
const tabs = [makeTab('tab-1', ['widget-a'])];
|
||||
|
||||
expect(() =>
|
||||
resolveWidgetTypeSelectTargetTabId({
|
||||
pageLayoutEditingWidgetId: 'non-existent',
|
||||
tabs,
|
||||
widgetCreationTargetTabId: null,
|
||||
}),
|
||||
).toThrow('Cannot find tab containing editing widget non-existent');
|
||||
});
|
||||
|
||||
it('should return widgetCreationTargetTabId when no editing widget is set', () => {
|
||||
const tabs = [makeTab('tab-1', ['widget-a'])];
|
||||
|
||||
const result = resolveWidgetTypeSelectTargetTabId({
|
||||
pageLayoutEditingWidgetId: null,
|
||||
tabs,
|
||||
widgetCreationTargetTabId: 'tab-1',
|
||||
});
|
||||
|
||||
expect(result).toBe('tab-1');
|
||||
});
|
||||
|
||||
it('should throw when both pageLayoutEditingWidgetId and widgetCreationTargetTabId are null', () => {
|
||||
const tabs = [makeTab('tab-1')];
|
||||
|
||||
expect(() =>
|
||||
resolveWidgetTypeSelectTargetTabId({
|
||||
pageLayoutEditingWidgetId: null,
|
||||
tabs,
|
||||
widgetCreationTargetTabId: null,
|
||||
}),
|
||||
).toThrow(
|
||||
'widgetCreationTargetTabId must be set when navigating to widget type select without an editing widget',
|
||||
);
|
||||
});
|
||||
});
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const resolveWidgetTypeSelectTargetTabId = ({
|
||||
pageLayoutEditingWidgetId,
|
||||
tabs,
|
||||
widgetCreationTargetTabId,
|
||||
}: {
|
||||
pageLayoutEditingWidgetId: string | null;
|
||||
tabs: PageLayoutTab[];
|
||||
widgetCreationTargetTabId: string | null;
|
||||
}): string => {
|
||||
if (isDefined(pageLayoutEditingWidgetId)) {
|
||||
const editingWidgetTab = tabs.find((tab) =>
|
||||
tab.widgets.some((widget) => widget.id === pageLayoutEditingWidgetId),
|
||||
);
|
||||
|
||||
if (!isDefined(editingWidgetTab)) {
|
||||
throw new Error(
|
||||
`Cannot find tab containing editing widget ${pageLayoutEditingWidgetId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return editingWidgetTab.id;
|
||||
}
|
||||
|
||||
if (!isDefined(widgetCreationTargetTabId)) {
|
||||
throw new Error(
|
||||
'widgetCreationTargetTabId must be set when navigating to widget type select without an editing widget',
|
||||
);
|
||||
}
|
||||
|
||||
return widgetCreationTargetTabId;
|
||||
};
|
||||
+42
-38
@@ -9,9 +9,9 @@ import {
|
||||
useContext,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
HorizontalSeparator,
|
||||
IconAlertTriangle,
|
||||
IconInfoCircle,
|
||||
IconSquareRoundedCheck,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from 'twenty-ui/display';
|
||||
import { ProgressBar, useProgressAnimation } from 'twenty-ui/feedback';
|
||||
import { LightButton, LightIconButton } from 'twenty-ui/input';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import {
|
||||
MOBILE_VIEWPORT,
|
||||
ThemeContext,
|
||||
@@ -40,9 +39,9 @@ export type SnackBarProps = Pick<ComponentPropsWithoutRef<'div'>, 'id'> & {
|
||||
duration?: number;
|
||||
icon?: ReactNode;
|
||||
message: string;
|
||||
buttonLabel?: string;
|
||||
buttonOnClick?: () => void;
|
||||
buttonTo?: string;
|
||||
actionText?: string;
|
||||
actionOnClick?: () => void;
|
||||
actionTo?: string;
|
||||
detailedMessage?: string;
|
||||
onCancel?: () => void;
|
||||
onClose?: () => void;
|
||||
@@ -57,9 +56,9 @@ const StyledContainer = styled.div`
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
margin-top: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]}
|
||||
${themeCssVariables.spacing[1]};
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
position: relative;
|
||||
width: 296px;
|
||||
|
||||
@@ -71,15 +70,12 @@ const StyledContainer = styled.div`
|
||||
|
||||
const StyledProgressBarContainer = styled.div`
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
& > [role='progressbar'] {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
@@ -116,15 +112,24 @@ const StyledDescription = styled.div`
|
||||
width: 200px;
|
||||
`;
|
||||
|
||||
const StyledBottomActionContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[2]};
|
||||
const StyledLinkContainer = styled.div`
|
||||
> a {
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
display: block;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
padding-left: ${themeCssVariables.spacing[6]};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledBottomAction = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: ${themeCssVariables.spacing[1]};
|
||||
const StyledActionButton = styled.div`
|
||||
padding-left: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const defaultAriaLabelByVariant: Record<
|
||||
@@ -146,9 +151,9 @@ export const SnackBar = ({
|
||||
id,
|
||||
message,
|
||||
detailedMessage,
|
||||
buttonLabel,
|
||||
buttonOnClick,
|
||||
buttonTo,
|
||||
actionText,
|
||||
actionOnClick,
|
||||
actionTo,
|
||||
onCancel,
|
||||
onClose,
|
||||
role = 'status',
|
||||
@@ -200,11 +205,15 @@ export const SnackBar = ({
|
||||
}, [iconComponent, variant, i18n, theme.icon.size.md, theme.snackBar]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
progressAnimation?.pause();
|
||||
if (progressAnimation?.state === 'running') {
|
||||
progressAnimation.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
progressAnimation?.play();
|
||||
if (progressAnimation?.state === 'paused') {
|
||||
progressAnimation.play();
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizedMessage = sanitizeMessageToRenderInSnackbar(message);
|
||||
@@ -242,21 +251,16 @@ export const SnackBar = ({
|
||||
{isDefined(sanitizedDetailedMessage) && (
|
||||
<StyledDescription>{sanitizedDetailedMessage}</StyledDescription>
|
||||
)}
|
||||
{isDefined(buttonLabel) &&
|
||||
(isDefined(buttonOnClick) || isDefined(buttonTo)) && (
|
||||
<StyledBottomActionContainer>
|
||||
<HorizontalSeparator noMargin />
|
||||
<StyledBottomAction>
|
||||
{isDefined(buttonTo) ? (
|
||||
<UndecoratedLink to={buttonTo}>
|
||||
<LightButton title={buttonLabel} />
|
||||
</UndecoratedLink>
|
||||
) : (
|
||||
<LightButton title={buttonLabel} onClick={buttonOnClick} />
|
||||
)}
|
||||
</StyledBottomAction>
|
||||
</StyledBottomActionContainer>
|
||||
)}
|
||||
{actionText && actionTo && (
|
||||
<StyledLinkContainer>
|
||||
<Link to={actionTo}>{actionText}</Link>
|
||||
</StyledLinkContainer>
|
||||
)}
|
||||
{actionText && actionOnClick && !actionTo && (
|
||||
<StyledActionButton>
|
||||
<LightButton title={actionText} onClick={actionOnClick} />
|
||||
</StyledActionButton>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-6
@@ -58,9 +58,9 @@ export const SnackBarProvider = ({ children }: React.PropsWithChildren) => {
|
||||
message,
|
||||
detailedMessage,
|
||||
variant,
|
||||
buttonLabel,
|
||||
buttonOnClick,
|
||||
buttonTo,
|
||||
actionText,
|
||||
actionOnClick,
|
||||
actionTo,
|
||||
}) => (
|
||||
<motion.div
|
||||
key={id}
|
||||
@@ -78,9 +78,9 @@ export const SnackBarProvider = ({ children }: React.PropsWithChildren) => {
|
||||
message,
|
||||
detailedMessage,
|
||||
variant,
|
||||
buttonLabel,
|
||||
buttonOnClick,
|
||||
buttonTo,
|
||||
actionText,
|
||||
actionOnClick,
|
||||
actionTo,
|
||||
}}
|
||||
onClose={() => handleSnackBarClose(id)}
|
||||
/>
|
||||
|
||||
-46
@@ -41,33 +41,6 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithBottomButton: Story = {
|
||||
args: {
|
||||
variant: SnackBarVariant.Error,
|
||||
message: 'An error has occurred',
|
||||
detailedMessage: 'Error during useFindManyRecord...',
|
||||
buttonLabel: 'Open Record',
|
||||
buttonOnClick: fn(),
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
};
|
||||
|
||||
export const SuccessWithButton: Story = {
|
||||
args: {
|
||||
variant: SnackBarVariant.Success,
|
||||
message: 'Record created successfully',
|
||||
buttonLabel: 'View Record',
|
||||
buttonOnClick: fn(),
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof SnackBar> = {
|
||||
args: {
|
||||
onCancel: fn(),
|
||||
@@ -90,22 +63,3 @@ export const Catalog: CatalogStory<Story, typeof SnackBar> = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CatalogWithButton: CatalogStory<Story, typeof SnackBar> = {
|
||||
args: {
|
||||
buttonLabel: 'Open Record',
|
||||
buttonOnClick: fn(),
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
parameters: {
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'variants',
|
||||
values: Object.values(SnackBarVariant),
|
||||
props: (variant: SnackBarVariant) => ({ variant }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import { type SnackBarOptions } from '@/ui/feedback/snack-bar-manager/states/sna
|
||||
|
||||
export const buildErrorAction = (
|
||||
apolloError?: ErrorLike,
|
||||
): Pick<SnackBarOptions, 'buttonLabel' | 'buttonTo'> | null => {
|
||||
): Pick<SnackBarOptions, 'actionText' | 'actionTo'> | null => {
|
||||
if (!apolloError) {
|
||||
return null;
|
||||
}
|
||||
@@ -16,8 +16,8 @@ export const buildErrorAction = (
|
||||
|
||||
if (isDefined(conflictingRecord)) {
|
||||
return {
|
||||
buttonLabel: t`View existing record`,
|
||||
buttonTo: getAppPath(AppPath.RecordShowPage, {
|
||||
actionText: t`View existing record`,
|
||||
actionTo: getAppPath(AppPath.RecordShowPage, {
|
||||
objectNameSingular: conflictingRecord.conflictingObjectNameSingular,
|
||||
objectRecordId: conflictingRecord.conflictingRecordId,
|
||||
}),
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE } from '@/side-panel/constants/SidePanelTopBarHeightMobile';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderSidePanelButtonClickOutsideId';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useContext } from 'react';
|
||||
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/display';
|
||||
import { AnimatedButton } from 'twenty-ui/input';
|
||||
import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
const StyledButtonWrapper = styled.div<{ alignToTop: boolean }>`
|
||||
align-items: ${({ alignToTop }) => (alignToTop ? 'center' : 'initial')};
|
||||
display: ${({ alignToTop }) => (alignToTop ? 'flex' : 'block')};
|
||||
height: ${({ alignToTop }) =>
|
||||
alignToTop ? `${SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE}px` : 'auto'};
|
||||
position: ${({ alignToTop }) => (alignToTop ? 'fixed' : 'static')};
|
||||
right: ${({ alignToTop }) =>
|
||||
alignToTop ? themeCssVariables.spacing[3] : 'auto'};
|
||||
top: ${({ alignToTop }) => (alignToTop ? '0' : 'auto')};
|
||||
z-index: ${RootStackingContextZIndices.SidePanelButton};
|
||||
`;
|
||||
|
||||
const StyledTooltipWrapper = styled.div`
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
`;
|
||||
|
||||
const xPaths = {
|
||||
topLeft: `M12 12 L6 6`,
|
||||
topRight: `M12 12 L18 6`,
|
||||
bottomLeft: `M12 12 L6 18`,
|
||||
bottomRight: `M12 12 L18 18`,
|
||||
};
|
||||
|
||||
const AnimatedIcon = ({
|
||||
isSidePanelOpened,
|
||||
}: {
|
||||
isSidePanelOpened: boolean;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={theme.icon.size.sm}
|
||||
height={theme.icon.size.sm}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={theme.icon.stroke.md}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ display: 'block' }}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
{/* Center dot */}
|
||||
<motion.circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r="1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
scale: isSidePanelOpened ? 0 : 1,
|
||||
opacity: isSidePanelOpened ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* X lines expanding from center */}
|
||||
{Object.values(xPaths).map((path, index) => (
|
||||
<motion.path
|
||||
key={index}
|
||||
d={path}
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{
|
||||
pathLength: isSidePanelOpened ? 1 : 0,
|
||||
opacity: isSidePanelOpened ? 1 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
ease: 'easeInOut',
|
||||
delay: isSidePanelOpened ? 0.1 : 0,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Top dot */}
|
||||
<motion.circle
|
||||
cx="12"
|
||||
cy="5"
|
||||
r="1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
scale: isSidePanelOpened ? 0 : 1,
|
||||
opacity: isSidePanelOpened ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bottom dot */}
|
||||
<motion.circle
|
||||
cx="12"
|
||||
cy="19"
|
||||
r="1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
scale: isSidePanelOpened ? 0 : 1,
|
||||
opacity: isSidePanelOpened ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageHeaderToggleSidePanelButton = () => {
|
||||
const { toggleSidePanelMenu } = useSidePanelMenu();
|
||||
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const alignWithSidePanelTopBar =
|
||||
isMobile && isLayoutCustomizationModeEnabled && isSidePanelOpened;
|
||||
|
||||
const ariaLabel = isSidePanelOpened
|
||||
? t`Close side panel`
|
||||
: t`Open side panel`;
|
||||
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return (
|
||||
<StyledButtonWrapper alignToTop={alignWithSidePanelTopBar}>
|
||||
<div id="toggle-side-panel-button">
|
||||
<AnimatedButton
|
||||
animatedSvg={<AnimatedIcon isSidePanelOpened={isSidePanelOpened} />}
|
||||
dataClickOutsideId={PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID}
|
||||
dataTestId="page-header-side-panel-button"
|
||||
size={isMobile ? 'medium' : 'small'}
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
hotkeys={[getOsControlSymbol(), 'K']}
|
||||
ariaLabel={ariaLabel}
|
||||
onClick={toggleSidePanelMenu}
|
||||
animate={{
|
||||
rotate: isSidePanelOpened ? 90 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StyledTooltipWrapper>
|
||||
<AppTooltip
|
||||
anchorSelect="#toggle-side-panel-button"
|
||||
content={ariaLabel}
|
||||
delay={TooltipDelay.longDelay}
|
||||
place={TooltipPosition.Bottom}
|
||||
offset={5}
|
||||
noArrow
|
||||
/>
|
||||
</StyledTooltipWrapper>
|
||||
</StyledButtonWrapper>
|
||||
);
|
||||
};
|
||||
@@ -4,10 +4,7 @@ import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_EMPTY_WORKSPACE_3_ID,
|
||||
SEED_EMPTY_WORKSPACE_4_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
SeededEmptyWorkspacesIds,
|
||||
SeededWorkspacesIds,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
|
||||
@@ -45,21 +42,12 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
|
||||
? [SEED_APPLE_WORKSPACE_ID]
|
||||
: [SEED_APPLE_WORKSPACE_ID, SEED_YCOMBINATOR_WORKSPACE_ID];
|
||||
|
||||
const emptyWorkspaceIds: SeededEmptyWorkspacesIds[] = [
|
||||
SEED_EMPTY_WORKSPACE_3_ID,
|
||||
SEED_EMPTY_WORKSPACE_4_ID,
|
||||
];
|
||||
|
||||
try {
|
||||
for (const workspaceId of workspaceIds) {
|
||||
await this.devSeederService.seedDev(workspaceId, {
|
||||
light: options.light,
|
||||
});
|
||||
}
|
||||
|
||||
for (const workspaceId of emptyWorkspaceIds) {
|
||||
await this.devSeederService.seedEmptyWorkspace(workspaceId);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.logger.error(error.stack);
|
||||
|
||||
@@ -62,9 +62,6 @@ export class RunInstanceCommandsCommand extends CommandRunner {
|
||||
await this.checkWorkspaceVersionSafety(options);
|
||||
await this.runLegacyPendingTypeOrmMigrations();
|
||||
|
||||
const activeOrSuspendedWorkspaceIds =
|
||||
await this.workspaceVersionService.getActiveOrSuspendedWorkspaceIds();
|
||||
|
||||
for (const {
|
||||
command,
|
||||
name,
|
||||
@@ -82,6 +79,9 @@ export class RunInstanceCommandsCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
if (options.includeSlow) {
|
||||
const hasWorkspaces =
|
||||
await this.workspaceVersionService.hasActiveOrSuspendedWorkspaces();
|
||||
|
||||
for (const {
|
||||
command,
|
||||
name,
|
||||
@@ -90,7 +90,7 @@ export class RunInstanceCommandsCommand extends CommandRunner {
|
||||
await this.instanceUpgradeService.runSlowInstanceCommand({
|
||||
command,
|
||||
name,
|
||||
skipDataMigration: activeOrSuspendedWorkspaceIds.length === 0,
|
||||
skipDataMigration: !hasWorkspaces,
|
||||
});
|
||||
|
||||
if (result.status === 'failed') {
|
||||
@@ -119,10 +119,10 @@ export class RunInstanceCommandsCommand extends CommandRunner {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeOrSuspendedWorkspaceIds =
|
||||
const activeWorkspaceIds =
|
||||
await this.workspaceVersionService.getActiveOrSuspendedWorkspaceIds();
|
||||
|
||||
if (activeOrSuspendedWorkspaceIds.length === 0) {
|
||||
if (activeWorkspaceIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export class RunInstanceCommandsCommand extends CommandRunner {
|
||||
const allAtPreviousVersion =
|
||||
await this.upgradeMigrationService.areAllWorkspacesAtCommand({
|
||||
commandName: lastWorkspaceCommand.name,
|
||||
workspaceIds: activeOrSuspendedWorkspaceIds,
|
||||
workspaceIds: activeWorkspaceIds,
|
||||
});
|
||||
|
||||
if (!allAtPreviousVersion) {
|
||||
|
||||
-9
@@ -36,9 +36,6 @@ export class AddStandalonePageFastInstanceCommand
|
||||
await queryRunner.query(
|
||||
"CREATE TYPE \"core\".\"navigationMenuItem_type_enum\" AS ENUM('VIEW', 'FOLDER', 'LINK', 'OBJECT', 'RECORD', 'PAGE_LAYOUT')",
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "type" DROP DEFAULT',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "type" TYPE "core"."navigationMenuItem_type_enum" USING "type"::"text"::"core"."navigationMenuItem_type_enum"',
|
||||
);
|
||||
@@ -72,15 +69,9 @@ export class AddStandalonePageFastInstanceCommand
|
||||
await queryRunner.query(
|
||||
"CREATE TYPE \"core\".\"navigationMenuItem_type_enum_old\" AS ENUM('FOLDER', 'LINK', 'OBJECT', 'RECORD', 'VIEW')",
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "type" DROP DEFAULT',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "type" TYPE "core"."navigationMenuItem_type_enum_old" USING "type"::"text"::"core"."navigationMenuItem_type_enum_old"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "type" SET DEFAULT \'VIEW\'',
|
||||
);
|
||||
await queryRunner.query('DROP TYPE "core"."navigationMenuItem_type_enum"');
|
||||
await queryRunner.query(
|
||||
'ALTER TYPE "core"."navigationMenuItem_type_enum_old" RENAME TO "navigationMenuItem_type_enum"',
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { BackfillRecordPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000001500-backfill-record-page-layouts.command';
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000001000-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { UpdateGlobalObjectContextCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000005000-update-global-object-context-command-menu-items.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
@@ -17,7 +17,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [
|
||||
BackfillRecordPageLayoutsCommand,
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
UpdateGlobalObjectContextCommandMenuItemsCommand,
|
||||
],
|
||||
})
|
||||
|
||||
+833
@@ -0,0 +1,833 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
FeatureFlagKey,
|
||||
FieldMetadataType,
|
||||
PageLayoutTabLayoutMode,
|
||||
ViewType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
|
||||
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
|
||||
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
|
||||
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import {
|
||||
GRID_POSITIONS,
|
||||
VERTICAL_LIST_LAYOUT_POSITIONS,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
|
||||
const HOME_TAB_POSITION = 10;
|
||||
|
||||
const isActivityTargetField = (
|
||||
fieldName: string,
|
||||
objectNameSingular: string,
|
||||
): boolean =>
|
||||
(objectNameSingular === CoreObjectNameSingular.Note &&
|
||||
fieldName === 'noteTargets') ||
|
||||
(objectNameSingular === CoreObjectNameSingular.Task &&
|
||||
fieldName === 'taskTargets');
|
||||
|
||||
const isJunctionRelationField = (field: FlatFieldMetadata): boolean => {
|
||||
const settings = field.settings as
|
||||
| { junctionTargetFieldId?: string }
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
isDefined(settings?.junctionTargetFieldId) &&
|
||||
typeof settings?.junctionTargetFieldId === 'string' &&
|
||||
settings.junctionTargetFieldId.length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const isRelationTargetAvailable = (
|
||||
targetObject: FlatObjectMetadata | undefined,
|
||||
): boolean => {
|
||||
if (!isDefined(targetObject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetObject.isRemote) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
targetObject.isSystem &&
|
||||
targetObject.nameSingular !== CoreObjectNameSingular.WorkspaceMember
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@RegisteredWorkspaceCommand('1.23.0', 1780000001000)
|
||||
@Command({
|
||||
name: 'upgrade:1-23:backfill-page-layouts-and-fields-widget-view-fields',
|
||||
description:
|
||||
'Backfill RECORD_PAGE page layouts, sync FIELDS_WIDGET view fields, create FIELD widgets, and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
})
|
||||
export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of page layouts and field widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isAlreadyEnabled) {
|
||||
this.logger.log(
|
||||
`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create page layouts, sync view fields, create FIELD widgets and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
await this.backfillStandardPageLayoutsAndSyncViews({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
await this.backfillCustomObjectPageLayouts({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
await this.backfillFieldWidgets({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled page layouts and field widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillStandardPageLayoutsAndSyncViews({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
shouldIncludeRecordPageLayouts: true,
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const recordPageLayoutUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const recordPageLayoutsFromStandard = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((pageLayout) => {
|
||||
if (pageLayout.type !== PageLayoutType.RECORD_PAGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
recordPageLayoutUniversalIdentifiers.add(
|
||||
pageLayout.universalIdentifier,
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const tabUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const tabsFromStandard = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((tab) => {
|
||||
if (
|
||||
!recordPageLayoutUniversalIdentifiers.has(
|
||||
tab.pageLayoutUniversalIdentifier,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tabUniversalIdentifiers.add(tab.universalIdentifier);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const widgetsFromStandard = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((widget) =>
|
||||
tabUniversalIdentifiers.has(widget.pageLayoutTabUniversalIdentifier),
|
||||
);
|
||||
|
||||
const {
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatPageLayoutMaps: existingFlatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
]);
|
||||
|
||||
// Filter out page layouts, tabs, and widgets that already exist in the workspace
|
||||
const pageLayoutsToCreate = recordPageLayoutsFromStandard.filter(
|
||||
(pageLayout) =>
|
||||
!isDefined(
|
||||
existingFlatPageLayoutMaps.byUniversalIdentifier[
|
||||
pageLayout.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const pageLayoutTabsToCreate = tabsFromStandard.filter(
|
||||
(tab) =>
|
||||
!isDefined(
|
||||
existingFlatPageLayoutTabMaps.byUniversalIdentifier[
|
||||
tab.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const pageLayoutWidgetsToCreate = widgetsFromStandard.filter(
|
||||
(widget) =>
|
||||
!isDefined(
|
||||
existingFlatPageLayoutWidgetMaps.byUniversalIdentifier[
|
||||
widget.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Collect all standard FIELDS_WIDGET view universal identifiers
|
||||
// This includes both new views to create AND existing views (created by 1-19)
|
||||
const allStandardFieldsWidgetViewUniversalIds = new Set<string>();
|
||||
|
||||
const viewsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((view) => {
|
||||
if (view.type !== ViewType.FIELDS_WIDGET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
allStandardFieldsWidgetViewUniversalIds.add(view.universalIdentifier);
|
||||
|
||||
if (
|
||||
isDefined(
|
||||
existingFlatViewMaps.byUniversalIdentifier[
|
||||
view.universalIdentifier
|
||||
],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Delete all existing viewFields for standard FIELDS_WIDGET views and recreate from current standard.
|
||||
// This ensures positions, visibility, and groups match the current standard definition.
|
||||
const viewFieldsToDelete = Object.values(
|
||||
existingFlatViewFieldMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewField) =>
|
||||
allStandardFieldsWidgetViewUniversalIds.has(
|
||||
viewField.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewField) =>
|
||||
allStandardFieldsWidgetViewUniversalIds.has(
|
||||
viewField.viewUniversalIdentifier,
|
||||
),
|
||||
)
|
||||
// Skip viewFields whose referenced fieldMetadata doesn't exist in the workspace yet
|
||||
// (e.g. fields added to the standard definition but not yet synced to this workspace)
|
||||
.filter((viewField) =>
|
||||
isDefined(
|
||||
existingFlatFieldMetadataMaps.byUniversalIdentifier[
|
||||
viewField.fieldMetadataUniversalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Same delete-and-recreate approach for viewFieldGroups
|
||||
const viewFieldGroupsToDelete = Object.values(
|
||||
existingFlatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewFieldGroup) =>
|
||||
allStandardFieldsWidgetViewUniversalIds.has(
|
||||
viewFieldGroup.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldGroupsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewFieldGroup) =>
|
||||
allStandardFieldsWidgetViewUniversalIds.has(
|
||||
viewFieldGroup.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Creating ${pageLayoutsToCreate.length} page layouts, ${viewsToCreate.length} views, deleting ${viewFieldsToDelete.length} and creating ${viewFieldsToCreate.length} view fields, deleting ${viewFieldGroupsToDelete.length} and creating ${viewFieldGroupsToCreate.length} view field groups for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: pageLayoutsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: pageLayoutTabsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: pageLayoutWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: viewsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: viewFieldsToCreate,
|
||||
flatEntityToDelete: viewFieldsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewFieldGroup: {
|
||||
flatEntityToCreate: viewFieldGroupsToCreate,
|
||||
flatEntityToDelete: viewFieldGroupsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create standard page layouts and sync view fields:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create standard page layouts and sync view fields for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async backfillCustomObjectPageLayouts({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
]);
|
||||
|
||||
const existingPageLayouts = Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
).filter(isDefined);
|
||||
|
||||
const objectIdsWithRecordPageLayout = new Set(
|
||||
existingPageLayouts
|
||||
.filter(
|
||||
(layout: FlatPageLayout) =>
|
||||
layout.type === PageLayoutType.RECORD_PAGE &&
|
||||
isDefined(layout.objectMetadataId),
|
||||
)
|
||||
.map((layout: FlatPageLayout) => layout.objectMetadataId),
|
||||
);
|
||||
|
||||
const customObjectsWithoutPageLayout = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.isCustom &&
|
||||
!objectMetadata.isRemote &&
|
||||
!objectIdsWithRecordPageLayout.has(objectMetadata.id),
|
||||
);
|
||||
|
||||
if (customObjectsWithoutPageLayout.length === 0) {
|
||||
this.logger.log(
|
||||
`No custom objects without page layouts found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Creating page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const allCustomPageLayoutsToCreate: FlatPageLayout[] = [];
|
||||
const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = [];
|
||||
const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const allCustomViewsToCreate: (UniversalFlatView & { id: string })[] = [];
|
||||
const allCustomViewFieldsToCreate: UniversalFlatViewField[] = [];
|
||||
|
||||
for (const customObject of customObjectsWithoutPageLayout) {
|
||||
const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({
|
||||
objectMetadata: customObject,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
const objectFieldMetadatas = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((field) => field.objectMetadataId === customObject.id);
|
||||
|
||||
const viewFields = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: objectFieldMetadatas,
|
||||
viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
excludeLabelIdentifier: true,
|
||||
});
|
||||
|
||||
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
|
||||
computeFlatDefaultRecordPageLayoutToCreate({
|
||||
objectMetadata: customObject,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
recordPageFieldsView: flatRecordPageFieldsView,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
allCustomPageLayoutsToCreate.push(...pageLayouts);
|
||||
allCustomPageLayoutTabsToCreate.push(...pageLayoutTabs);
|
||||
allCustomPageLayoutWidgetsToCreate.push(...pageLayoutWidgets);
|
||||
allCustomViewsToCreate.push(flatRecordPageFieldsView);
|
||||
allCustomViewFieldsToCreate.push(...viewFields);
|
||||
}
|
||||
|
||||
const customValidateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: allCustomPageLayoutsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: allCustomPageLayoutTabsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: allCustomPageLayoutWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: allCustomViewsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: allCustomViewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (customValidateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create custom object page layouts:\n${JSON.stringify(customValidateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create custom object page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillFieldWidgets({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
]);
|
||||
|
||||
// Build a set of fieldMetadataIds that already have a FIELD widget
|
||||
const existingFieldWidgetFieldIds = new Set<string>();
|
||||
|
||||
for (const widget of Object.values(
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(widget) &&
|
||||
widget.configuration?.configurationType ===
|
||||
WidgetConfigurationType.FIELD &&
|
||||
isDefined(widget.configuration.fieldMetadataId)
|
||||
) {
|
||||
existingFieldWidgetFieldIds.add(widget.configuration.fieldMetadataId);
|
||||
}
|
||||
}
|
||||
|
||||
// Build object ID -> objectMetadata map
|
||||
const objectById = new Map<string, FlatObjectMetadata>();
|
||||
|
||||
for (const obj of Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (isDefined(obj)) {
|
||||
objectById.set(obj.id, obj);
|
||||
}
|
||||
}
|
||||
|
||||
// Build object ID -> RECORD_PAGE layout map
|
||||
const recordPageLayoutByObjectId = new Map<
|
||||
string,
|
||||
{ id: string; universalIdentifier: string }
|
||||
>();
|
||||
|
||||
for (const layout of Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(layout) &&
|
||||
layout.type === PageLayoutType.RECORD_PAGE &&
|
||||
isDefined(layout.objectMetadataId)
|
||||
) {
|
||||
recordPageLayoutByObjectId.set(layout.objectMetadataId, {
|
||||
id: layout.id,
|
||||
universalIdentifier: layout.universalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build pageLayoutId -> home tab map
|
||||
const homeTabByPageLayoutId = new Map<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
widgetCount: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const tab of Object.values(
|
||||
flatPageLayoutTabMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(tab) &&
|
||||
tab.position === HOME_TAB_POSITION &&
|
||||
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
|
||||
) {
|
||||
homeTabByPageLayoutId.set(tab.pageLayoutId, {
|
||||
id: tab.id,
|
||||
universalIdentifier: tab.universalIdentifier,
|
||||
widgetCount: tab.widgetIds?.length ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group fields by objectMetadataId
|
||||
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
|
||||
|
||||
// Map morphId -> all field IDs sharing that morphId (for dedup)
|
||||
const fieldIdsByMorphId = new Map<string, string[]>();
|
||||
|
||||
for (const field of Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(field) || !isDefined(field.objectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const list = fieldsByObjectId.get(field.objectMetadataId) ?? [];
|
||||
|
||||
list.push(field);
|
||||
fieldsByObjectId.set(field.objectMetadataId, list);
|
||||
|
||||
if (
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
isDefined(field.morphId)
|
||||
) {
|
||||
const morphFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
|
||||
|
||||
morphFieldIds.push(field.id);
|
||||
fieldIdsByMorphId.set(field.morphId, morphFieldIds);
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const widgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const processedMorphIds = new Set<string>();
|
||||
|
||||
for (const object of objectById.values()) {
|
||||
const pageLayout = recordPageLayoutByObjectId.get(object.id);
|
||||
|
||||
if (!isDefined(pageLayout)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const homeTab = homeTabByPageLayoutId.get(pageLayout.id);
|
||||
|
||||
if (!isDefined(homeTab)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fields = fieldsByObjectId.get(object.id) ?? [];
|
||||
let nextWidgetIndex = homeTab.widgetCount;
|
||||
|
||||
for (const field of fields) {
|
||||
if (
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isActivityTargetField(field.name, object.nameSingular)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isJunctionRelationField(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.type === FieldMetadataType.RELATION) {
|
||||
const targetObject = isDefined(field.relationTargetObjectMetadataId)
|
||||
? objectById.get(field.relationTargetObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
if (!isRelationTargetAvailable(targetObject)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// For morph relations, skip if any sibling field (same morphId)
|
||||
// already has a widget or was already processed in this run
|
||||
if (
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
isDefined(field.morphId)
|
||||
) {
|
||||
if (processedMorphIds.has(field.morphId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const siblingFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
|
||||
const alreadyHasWidget = siblingFieldIds.some((id) =>
|
||||
existingFieldWidgetFieldIds.has(id),
|
||||
);
|
||||
|
||||
if (alreadyHasWidget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedMorphIds.add(field.morphId);
|
||||
}
|
||||
|
||||
if (existingFieldWidgetFieldIds.has(field.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const widget: FlatPageLayoutWidget = {
|
||||
id: v4(),
|
||||
universalIdentifier: v4(),
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
pageLayoutTabId: homeTab.id,
|
||||
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
|
||||
title: field.label,
|
||||
type: WidgetType.FIELD,
|
||||
gridPosition: { ...GRID_POSITIONS.FULL_WIDTH },
|
||||
position: {
|
||||
...VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
|
||||
index: nextWidgetIndex,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: field.id,
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
universalConfiguration: {
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: field.universalIdentifier,
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
objectMetadataId: object.id,
|
||||
objectMetadataUniversalIdentifier: object.universalIdentifier,
|
||||
isActive: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
conditionalDisplay: null,
|
||||
conditionalAvailabilityExpression: null,
|
||||
overrides: null,
|
||||
universalOverrides: null,
|
||||
};
|
||||
|
||||
widgetsToCreate.push(widget);
|
||||
existingFieldWidgetFieldIds.add(field.id);
|
||||
nextWidgetIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (widgetsToCreate.length === 0) {
|
||||
this.logger.log(
|
||||
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${widgetsToCreate.length} FIELD widget(s) to create for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: widgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create FIELD widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created ${widgetsToCreate.length} FIELD widget(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-534
@@ -1,534 +0,0 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { FeatureFlagKey, ViewType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
|
||||
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
|
||||
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
|
||||
@RegisteredWorkspaceCommand('1.23.0', 1780000001500)
|
||||
@Command({
|
||||
name: 'upgrade:1-23:backfill-record-page-layouts',
|
||||
description:
|
||||
'Delete and recreate all record page layouts from standard config, backfill custom objects, and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
})
|
||||
export class BackfillRecordPageLayoutsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isAlreadyEnabled) {
|
||||
this.logger.log(
|
||||
`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would recreate all record page layouts and enable feature flag for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
await this.deleteAllRecordPageLayoutEntities({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
await this.createStandardRecordPageLayouts({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
await this.createCustomObjectPageLayouts({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled record page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async deleteAllRecordPageLayoutEntities({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
]);
|
||||
|
||||
const recordPageLayouts = Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((layout) => layout.type === PageLayoutType.RECORD_PAGE);
|
||||
|
||||
const recordPageLayoutIds = new Set(
|
||||
recordPageLayouts.map((layout) => layout.id),
|
||||
);
|
||||
|
||||
const tabs = Object.values(flatPageLayoutTabMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter((tab) => recordPageLayoutIds.has(tab.pageLayoutId));
|
||||
|
||||
const tabIds = new Set(tabs.map((tab) => tab.id));
|
||||
|
||||
const widgets = Object.values(
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((widget) => tabIds.has(widget.pageLayoutTabId));
|
||||
|
||||
const fieldsWidgetViews = Object.values(flatViewMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter((view) => view.type === ViewType.FIELDS_WIDGET);
|
||||
|
||||
const fieldsWidgetViewUniversalIdentifiers = new Set(
|
||||
fieldsWidgetViews.map((view) => view.universalIdentifier),
|
||||
);
|
||||
|
||||
const viewFields = Object.values(flatViewFieldMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter((viewField) =>
|
||||
fieldsWidgetViewUniversalIdentifiers.has(
|
||||
viewField.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldGroups = Object.values(
|
||||
flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewFieldGroup) =>
|
||||
fieldsWidgetViewUniversalIdentifiers.has(
|
||||
viewFieldGroup.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
if (recordPageLayouts.length === 0 && fieldsWidgetViews.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleting ${recordPageLayouts.length} page layouts, ${tabs.length} tabs, ${widgets.length} widgets, ${fieldsWidgetViews.length} views, ${viewFields.length} view fields, ${viewFieldGroups.length} view field groups for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
viewField: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: viewFields,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewFieldGroup: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: viewFieldGroups,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: fieldsWidgetViews,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: widgets,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: tabs,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayout: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: recordPageLayouts,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to delete record page layout entities:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to delete record page layout entities for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createStandardRecordPageLayouts({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const { allFlatEntityMaps: standardMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
shouldIncludeRecordPageLayouts: true,
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const recordPageLayoutUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const pageLayouts = Object.values(
|
||||
standardMaps.flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((pageLayout) => {
|
||||
if (pageLayout.type !== PageLayoutType.RECORD_PAGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
recordPageLayoutUniversalIdentifiers.add(
|
||||
pageLayout.universalIdentifier,
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const tabUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const tabs = Object.values(
|
||||
standardMaps.flatPageLayoutTabMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((tab) => {
|
||||
if (
|
||||
!recordPageLayoutUniversalIdentifiers.has(
|
||||
tab.pageLayoutUniversalIdentifier,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tabUniversalIdentifiers.add(tab.universalIdentifier);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const widgets = Object.values(
|
||||
standardMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((widget) =>
|
||||
tabUniversalIdentifiers.has(widget.pageLayoutTabUniversalIdentifier),
|
||||
);
|
||||
|
||||
const fieldsWidgetViewUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const views = Object.values(standardMaps.flatViewMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter((view) => {
|
||||
if (view.type !== ViewType.FIELDS_WIDGET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fieldsWidgetViewUniversalIdentifiers.add(view.universalIdentifier);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const { flatFieldMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const viewFields = Object.values(
|
||||
standardMaps.flatViewFieldMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewField) =>
|
||||
fieldsWidgetViewUniversalIdentifiers.has(
|
||||
viewField.viewUniversalIdentifier,
|
||||
),
|
||||
)
|
||||
.filter((viewField) =>
|
||||
isDefined(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
viewField.fieldMetadataUniversalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldGroups = Object.values(
|
||||
standardMaps.flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewFieldGroup) =>
|
||||
fieldsWidgetViewUniversalIdentifiers.has(
|
||||
viewFieldGroup.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Creating ${pageLayouts.length} standard page layouts, ${views.length} views, ${viewFields.length} view fields for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: pageLayouts,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: tabs,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: widgets,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: views,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: viewFields,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewFieldGroup: {
|
||||
flatEntityToCreate: viewFieldGroups,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create standard record page layouts:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create standard record page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createCustomObjectPageLayouts({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
]);
|
||||
|
||||
const objectIdsWithRecordPageLayout = new Set(
|
||||
Object.values(flatPageLayoutMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(layout) =>
|
||||
layout.type === PageLayoutType.RECORD_PAGE &&
|
||||
isDefined(layout.objectMetadataId),
|
||||
)
|
||||
.map((layout) => layout.objectMetadataId),
|
||||
);
|
||||
|
||||
const customObjectsWithoutPageLayout = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.isCustom &&
|
||||
!objectMetadata.isRemote &&
|
||||
!objectIdsWithRecordPageLayout.has(objectMetadata.id),
|
||||
);
|
||||
|
||||
if (customObjectsWithoutPageLayout.length === 0) {
|
||||
this.logger.log(
|
||||
`No custom objects without page layouts found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Creating page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const allPageLayouts: FlatPageLayout[] = [];
|
||||
const allTabs: FlatPageLayoutTab[] = [];
|
||||
const allWidgets: FlatPageLayoutWidget[] = [];
|
||||
const allViews: (UniversalFlatView & { id: string })[] = [];
|
||||
const allViewFields: UniversalFlatViewField[] = [];
|
||||
|
||||
for (const customObject of customObjectsWithoutPageLayout) {
|
||||
const fieldsView = computeFlatRecordPageFieldsViewToCreate({
|
||||
objectMetadata: customObject,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
const objectFieldMetadatas = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((field) => field.objectMetadataId === customObject.id);
|
||||
|
||||
const viewFields = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: objectFieldMetadatas,
|
||||
viewUniversalIdentifier: fieldsView.universalIdentifier,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
excludeLabelIdentifier: true,
|
||||
});
|
||||
|
||||
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
|
||||
computeFlatDefaultRecordPageLayoutToCreate({
|
||||
objectMetadata: customObject,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
recordPageFieldsView: fieldsView,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
allPageLayouts.push(...pageLayouts);
|
||||
allTabs.push(...pageLayoutTabs);
|
||||
allWidgets.push(...pageLayoutWidgets);
|
||||
allViews.push(fieldsView);
|
||||
allViewFields.push(...viewFields);
|
||||
}
|
||||
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: allPageLayouts,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: allTabs,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: allWidgets,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: allViews,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: allViewFields,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create custom object page layouts:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create custom object page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Misluk"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "Funksiekode is onveranderd."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Te veel rekords om gelyktydig op te dateer."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Te veel versoeke. Probeer asseblief later weer."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "فشل"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "لم تتغير شيفرة الدالة."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "عدد السجلات المراد تحديثها دفعة واحدة كبير جداً."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "عدد كبير جداً من الطلبات. يُرجى المحاولة لاحقاً."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Error"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "El codi de la funció no ha canviat."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Massa registres per actualitzar d'una vegada."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Massa sol·licituds. Torna-ho a provar més tard."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Selhalo"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "Kód funkce se nezměnil."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Příliš mnoho záznamů k aktualizaci najednou."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Příliš mnoho požadavků. Zkuste to prosím později."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Mislykkedes"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "Funktionskoden er uændret."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "For mange poster at opdatere på én gang."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "For mange forespørgsler. Prøv igen senere."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Fehlgeschlagen"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "Funktionscode ist unverändert."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Zu viele Datensätze, um sie auf einmal zu aktualisieren."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Απέτυχε"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "Ο κώδικας της συνάρτησης δεν έχει αλλάξει."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Πάρα πολλές εγγραφές για ενημέρωση ταυτόχρονα."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Πάρα πολλά αιτήματα. Δοκιμάστε ξανά αργότερα."
|
||||
|
||||
@@ -2197,11 +2197,6 @@ msgstr "Failed"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr "Failed Insufficient Permissions"
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr "Failed to build function dependencies."
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2751,11 +2746,6 @@ msgstr "Function code failed to compile."
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "Function code is unchanged."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr "Function execution failed."
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7230,7 +7220,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Too many records to update at once."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Too many requests. Please try again later."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Fallido"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "El código de la función no ha cambiado."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Demasiados registros para actualizar de una sola vez."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Demasiadas solicitudes. Por favor, inténtelo de nuevo más tarde."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Epäonnistui"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "Funktion koodia ei ole muutettu."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Liian monta tietuetta päivitettäväksi kerralla."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Liian monta pyyntöä. Yritä myöhemmin uudelleen."
|
||||
|
||||
@@ -2202,11 +2202,6 @@ msgstr "Échec"
|
||||
msgid "Failed Insufficient Permissions"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: rl11Jd
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Failed to build function dependencies."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: E7Dsiq
|
||||
#: src/engine/metadata-modules/ai/ai-agent-monitor/resolvers/agent-turn.resolver.ts
|
||||
msgid "Failed to create evaluation. Please try again."
|
||||
@@ -2756,11 +2751,6 @@ msgstr ""
|
||||
msgid "Function code is unchanged."
|
||||
msgstr "Le code de la fonction est inchangé."
|
||||
|
||||
#. js-lingui-id: rqDhd1
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution failed."
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: Ho3UAo
|
||||
#: src/engine/metadata-modules/logic-function/logic-function.exception.ts
|
||||
msgid "Function execution limit reached."
|
||||
@@ -7235,7 +7225,6 @@ msgid "Too many records to update at once."
|
||||
msgstr "Trop d'enregistrements à mettre à jour en une seule fois."
|
||||
|
||||
#. js-lingui-id: M68C8m
|
||||
#: src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts
|
||||
#: src/engine/core-modules/email-verification/email-verification.exception.ts
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Trop de requêtes. Veuillez réessayer plus tard."
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user