shutro-erp 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +181 -3
- package/dist/account_portal/module.d.ts +359 -6
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +181 -3
- package/dist/accounting/index.js +214 -2
- package/dist/accounting/module.d.ts +217 -5
- package/dist/accounting/module.js +23 -2
- package/dist/calendar/.kosh/bindings.d.ts +22 -0
- package/dist/calendar/.kosh/bindings.js +15 -0
- package/dist/calendar/.kosh/schema.d.ts +795 -0
- package/dist/calendar/demo.d.ts +6 -0
- package/dist/calendar/demo.js +24 -0
- package/dist/calendar/index.d.ts +3 -0
- package/dist/calendar/index.js +291 -0
- package/dist/calendar/module.d.ts +1705 -0
- package/dist/calendar/module.js +180 -0
- package/dist/cli/init.js +48 -0
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +137 -0
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +205 -3
- package/dist/contacts/demo.js +30 -1
- package/dist/contacts/index.d.ts +1 -1
- package/dist/contacts/index.js +155 -9
- package/dist/contacts/module.d.ts +338 -2
- package/dist/contacts/module.js +82 -5
- package/dist/crm/.kosh/bindings.d.ts +22 -0
- package/dist/crm/.kosh/bindings.js +15 -0
- package/dist/crm/.kosh/schema.d.ts +799 -0
- package/dist/crm/demo.d.ts +6 -0
- package/dist/crm/demo.js +45 -0
- package/dist/crm/index.d.ts +3 -0
- package/dist/crm/index.js +361 -0
- package/dist/crm/module.d.ts +1843 -0
- package/dist/crm/module.js +207 -0
- package/dist/event/.kosh/bindings.d.ts +22 -0
- package/dist/event/.kosh/bindings.js +15 -0
- package/dist/event/.kosh/schema.d.ts +843 -0
- package/dist/event/demo.d.ts +6 -0
- package/dist/event/demo.js +65 -0
- package/dist/event/index.d.ts +14 -0
- package/dist/event/index.js +333 -0
- package/dist/event/mails.d.ts +15 -0
- package/dist/event/mails.js +112 -0
- package/dist/event/module.d.ts +2800 -0
- package/dist/event/module.js +323 -0
- package/dist/event/web/CheckInBox.d.ts +9 -0
- package/dist/event/web/CheckInBox.js +87 -0
- package/dist/event/web/index.d.ts +11 -0
- package/dist/event/web/index.js +16 -0
- package/dist/event/web/messages.d.ts +2 -0
- package/dist/event/web/messages.js +23 -0
- package/dist/event_account/.kosh/bindings.d.ts +22 -0
- package/dist/event_account/.kosh/bindings.js +15 -0
- package/dist/event_account/.kosh/schema.d.ts +1639 -0
- package/dist/event_account/index.d.ts +3 -0
- package/dist/event_account/index.js +28 -0
- package/dist/event_account/module.d.ts +6968 -0
- package/dist/event_account/module.js +90 -0
- package/dist/hr/.kosh/bindings.d.ts +22 -0
- package/dist/hr/.kosh/bindings.js +15 -0
- package/dist/hr/.kosh/schema.d.ts +878 -0
- package/dist/hr/demo.d.ts +6 -0
- package/dist/hr/demo.js +60 -0
- package/dist/hr/index.d.ts +3 -0
- package/dist/hr/index.js +483 -0
- package/dist/hr/module.d.ts +2611 -0
- package/dist/hr/module.js +350 -0
- package/dist/integrations/index.js +12 -0
- package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
- package/dist/integrations/web/OAuthAppsPage.js +79 -0
- package/dist/integrations/web/index.d.ts +1 -0
- package/dist/integrations/web/index.js +3 -0
- package/dist/integrations/web/messages.js +58 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +181 -3
- package/dist/l10n_bd/module.d.ts +359 -6
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +181 -3
- package/dist/l10n_in/module.d.ts +380 -7
- package/dist/maintenance/.kosh/bindings.d.ts +22 -0
- package/dist/maintenance/.kosh/bindings.js +15 -0
- package/dist/maintenance/.kosh/schema.d.ts +911 -0
- package/dist/maintenance/demo.d.ts +6 -0
- package/dist/maintenance/demo.js +60 -0
- package/dist/maintenance/index.d.ts +3 -0
- package/dist/maintenance/index.js +422 -0
- package/dist/maintenance/module.d.ts +3545 -0
- package/dist/maintenance/module.js +446 -0
- package/dist/maintenance_stock/.kosh/bindings.d.ts +22 -0
- package/dist/maintenance_stock/.kosh/bindings.js +15 -0
- package/dist/maintenance_stock/.kosh/schema.d.ts +1371 -0
- package/dist/maintenance_stock/index.d.ts +9 -0
- package/dist/maintenance_stock/index.js +52 -0
- package/dist/maintenance_stock/module.d.ts +6953 -0
- package/dist/maintenance_stock/module.js +192 -0
- package/dist/messaging/web/index.js +1 -1
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/manifest.d.ts +70 -0
- package/dist/modules-sdk/manifest.js +35 -1
- package/dist/mrp/.kosh/bindings.d.ts +22 -0
- package/dist/mrp/.kosh/bindings.js +15 -0
- package/dist/mrp/.kosh/schema.d.ts +1378 -0
- package/dist/mrp/demo.d.ts +7 -0
- package/dist/mrp/demo.js +42 -0
- package/dist/mrp/index.d.ts +3 -0
- package/dist/mrp/index.js +392 -0
- package/dist/mrp/module.d.ts +8608 -0
- package/dist/mrp/module.js +695 -0
- package/dist/mrp_account/.kosh/bindings.d.ts +22 -0
- package/dist/mrp_account/.kosh/bindings.js +15 -0
- package/dist/mrp_account/.kosh/schema.d.ts +2106 -0
- package/dist/mrp_account/index.d.ts +3 -0
- package/dist/mrp_account/index.js +22 -0
- package/dist/mrp_account/module.d.ts +20840 -0
- package/dist/mrp_account/module.js +123 -0
- package/dist/notify/web/index.js +1 -1
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +181 -3
- package/dist/payment/module.d.ts +359 -6
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +205 -3
- package/dist/portal/module.d.ts +166 -2
- package/dist/pos/.kosh/bindings.d.ts +22 -0
- package/dist/pos/.kosh/bindings.js +15 -0
- package/dist/pos/.kosh/schema.d.ts +1690 -0
- package/dist/pos/accounting.d.ts +10 -0
- package/dist/pos/accounting.js +149 -0
- package/dist/pos/demo.d.ts +6 -0
- package/dist/pos/demo.js +90 -0
- package/dist/pos/index.d.ts +9 -0
- package/dist/pos/index.js +349 -0
- package/dist/pos/module.d.ts +9636 -0
- package/dist/pos/module.js +423 -0
- package/dist/pos/routes.d.ts +39 -0
- package/dist/pos/routes.js +368 -0
- package/dist/pos/web/Register.d.ts +9 -0
- package/dist/pos/web/Register.js +252 -0
- package/dist/pos/web/index.d.ts +11 -0
- package/dist/pos/web/index.js +16 -0
- package/dist/pos/web/messages.d.ts +2 -0
- package/dist/pos/web/messages.js +87 -0
- package/dist/pos_stock/.kosh/bindings.d.ts +22 -0
- package/dist/pos_stock/.kosh/bindings.js +15 -0
- package/dist/pos_stock/.kosh/schema.d.ts +2015 -0
- package/dist/pos_stock/index.d.ts +9 -0
- package/dist/pos_stock/index.js +35 -0
- package/dist/pos_stock/module.d.ts +11935 -0
- package/dist/pos_stock/module.js +115 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +178 -1
- package/dist/product/index.js +81 -4
- package/dist/product/module.d.ts +43 -1
- package/dist/product/module.js +9 -2
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +181 -3
- package/dist/project/index.js +147 -7
- package/dist/project/module.d.ts +210 -2
- package/dist/project/module.js +52 -5
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +181 -3
- package/dist/project_portal/module.d.ts +353 -4
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +181 -3
- package/dist/purchase/index.js +163 -3
- package/dist/purchase/module.d.ts +215 -3
- package/dist/purchase/module.js +21 -3
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +182 -3
- package/dist/purchase_account/index.js +26 -1
- package/dist/purchase_account/module.d.ts +393 -7
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +181 -3
- package/dist/purchase_portal/module.d.ts +366 -5
- package/dist/purchase_stock/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_stock/.kosh/bindings.js +15 -0
- package/dist/purchase_stock/.kosh/schema.d.ts +1310 -0
- package/dist/purchase_stock/index.d.ts +7 -0
- package/dist/purchase_stock/index.js +91 -0
- package/dist/purchase_stock/module.d.ts +6763 -0
- package/dist/purchase_stock/module.js +144 -0
- package/dist/purchase_stock_account/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_stock_account/.kosh/bindings.js +15 -0
- package/dist/purchase_stock_account/.kosh/schema.d.ts +1959 -0
- package/dist/purchase_stock_account/index.d.ts +3 -0
- package/dist/purchase_stock_account/index.js +24 -0
- package/dist/purchase_stock_account/module.d.ts +35366 -0
- package/dist/purchase_stock_account/module.js +162 -0
- package/dist/recruitment/.kosh/bindings.d.ts +22 -0
- package/dist/recruitment/.kosh/bindings.js +15 -0
- package/dist/recruitment/.kosh/schema.d.ts +990 -0
- package/dist/recruitment/demo.d.ts +6 -0
- package/dist/recruitment/demo.js +45 -0
- package/dist/recruitment/index.d.ts +3 -0
- package/dist/recruitment/index.js +385 -0
- package/dist/recruitment/module.d.ts +3247 -0
- package/dist/recruitment/module.js +193 -0
- package/dist/recruitment/web/HireButton.d.ts +9 -0
- package/dist/recruitment/web/HireButton.js +32 -0
- package/dist/recruitment/web/index.d.ts +11 -0
- package/dist/recruitment/web/index.js +16 -0
- package/dist/recruitment/web/messages.d.ts +2 -0
- package/dist/recruitment/web/messages.js +19 -0
- package/dist/reporting/documents.d.ts +5 -0
- package/dist/reporting/documents.js +28 -1
- package/dist/reporting/index.d.ts +1 -1
- package/dist/reporting/index.js +4 -2
- package/dist/reporting/models.js +9 -0
- package/dist/reporting/overviews.d.ts +16 -0
- package/dist/reporting/overviews.js +56 -0
- package/dist/reporting/routes-documents.js +1 -1
- package/dist/reporting/routes.js +138 -80
- package/dist/reporting/web/DashboardView.d.ts +11 -0
- package/dist/reporting/web/DashboardView.js +2 -2
- package/dist/reporting/web/DesignerView.js +5 -0
- package/dist/reporting/web/Overview.d.ts +9 -0
- package/dist/reporting/web/Overview.js +53 -0
- package/dist/reporting/web/index.d.ts +1 -0
- package/dist/reporting/web/index.js +4 -1
- package/dist/reporting/web/messages.js +9 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +186 -3
- package/dist/sale_account/index.js +22 -1
- package/dist/sale_account/module.d.ts +461 -7
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +185 -3
- package/dist/sale_portal/module.d.ts +434 -5
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +187 -3
- package/dist/sale_project/module.d.ts +476 -6
- package/dist/sale_stock/.kosh/bindings.d.ts +22 -0
- package/dist/sale_stock/.kosh/bindings.js +15 -0
- package/dist/sale_stock/.kosh/schema.d.ts +1228 -0
- package/dist/sale_stock/index.d.ts +7 -0
- package/dist/sale_stock/index.js +84 -0
- package/dist/sale_stock/module.d.ts +6791 -0
- package/dist/sale_stock/module.js +146 -0
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +185 -3
- package/dist/sales/index.js +191 -4
- package/dist/sales/module.d.ts +355 -3
- package/dist/sales/module.js +78 -3
- package/dist/server/api/metadata.d.ts +5 -0
- package/dist/server/api/metadata.js +5 -0
- package/dist/server/api/rest.js +64 -1
- package/dist/server/api/views.js +1 -0
- package/dist/server/app-settings.d.ts +7 -0
- package/dist/server/app-settings.js +10 -0
- package/dist/server/auth/base.js +85 -0
- package/dist/server/auth/oauth.d.ts +21 -0
- package/dist/server/auth/oauth.js +579 -0
- package/dist/server/auth/service.d.ts +4 -0
- package/dist/server/auth/service.js +17 -0
- package/dist/server/identity.d.ts +2 -0
- package/dist/server/images.d.ts +12 -0
- package/dist/server/images.js +139 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.js +9 -1
- package/dist/settings/app-settings.d.ts +2 -0
- package/dist/settings/app-settings.js +518 -0
- package/dist/settings/company.js +3 -1
- package/dist/settings/index.d.ts +1 -0
- package/dist/settings/index.js +15 -0
- package/dist/settings/people.js +32 -6
- package/dist/settings/web/AppSettingsPage.d.ts +11 -0
- package/dist/settings/web/AppSettingsPage.js +210 -0
- package/dist/settings/web/CompaniesPage.js +2 -2
- package/dist/settings/web/CompanyPage.js +13 -2
- package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
- package/dist/settings/web/ConnectedAppsPage.js +33 -0
- package/dist/settings/web/OverviewPage.js +28 -4
- package/dist/settings/web/PeoplePage.js +23 -18
- package/dist/settings/web/ProfilePage.js +9 -7
- package/dist/settings/web/index.d.ts +2 -0
- package/dist/settings/web/index.js +8 -0
- package/dist/settings/web/messages.js +146 -2
- package/dist/stock/.kosh/bindings.d.ts +22 -0
- package/dist/stock/.kosh/bindings.js +15 -0
- package/dist/stock/.kosh/schema.d.ts +1147 -0
- package/dist/stock/demo.d.ts +7 -0
- package/dist/stock/demo.js +55 -0
- package/dist/stock/index.d.ts +3 -0
- package/dist/stock/index.js +674 -0
- package/dist/stock/module.d.ts +6330 -0
- package/dist/stock/module.js +1325 -0
- package/dist/stock/web/ScanBox.d.ts +8 -0
- package/dist/stock/web/ScanBox.js +90 -0
- package/dist/stock/web/index.d.ts +11 -0
- package/dist/stock/web/index.js +16 -0
- package/dist/stock/web/messages.d.ts +2 -0
- package/dist/stock/web/messages.js +25 -0
- package/dist/stock_account/.kosh/bindings.d.ts +22 -0
- package/dist/stock_account/.kosh/bindings.js +15 -0
- package/dist/stock_account/.kosh/schema.d.ts +1875 -0
- package/dist/stock_account/index.d.ts +3 -0
- package/dist/stock_account/index.js +130 -0
- package/dist/stock_account/module.d.ts +10655 -0
- package/dist/stock_account/module.js +339 -0
- package/dist/storage/attachments.d.ts +5 -0
- package/dist/storage/attachments.js +12 -1
- package/dist/storage/models.js +3 -0
- package/dist/storage/routes.js +13 -2
- package/dist/todo/.kosh/bindings.d.ts +22 -0
- package/dist/todo/.kosh/bindings.js +15 -0
- package/dist/todo/.kosh/schema.d.ts +732 -0
- package/dist/todo/demo.d.ts +6 -0
- package/dist/todo/demo.js +35 -0
- package/dist/todo/index.d.ts +3 -0
- package/dist/todo/index.js +187 -0
- package/dist/todo/module.d.ts +1180 -0
- package/dist/todo/module.js +130 -0
- package/dist/trade_finance/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance/.kosh/bindings.js +15 -0
- package/dist/trade_finance/.kosh/schema.d.ts +884 -0
- package/dist/trade_finance/demo.d.ts +6 -0
- package/dist/trade_finance/demo.js +63 -0
- package/dist/trade_finance/index.d.ts +3 -0
- package/dist/trade_finance/index.js +413 -0
- package/dist/trade_finance/module.d.ts +3779 -0
- package/dist/trade_finance/module.js +505 -0
- package/dist/trade_finance_account/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_account/.kosh/bindings.js +15 -0
- package/dist/trade_finance_account/.kosh/schema.d.ts +1711 -0
- package/dist/trade_finance_account/index.d.ts +3 -0
- package/dist/trade_finance_account/index.js +92 -0
- package/dist/trade_finance_account/module.d.ts +7928 -0
- package/dist/trade_finance_account/module.js +244 -0
- package/dist/trade_finance_purchase/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_purchase/.kosh/bindings.js +15 -0
- package/dist/trade_finance_purchase/.kosh/schema.d.ts +1153 -0
- package/dist/trade_finance_purchase/index.d.ts +8 -0
- package/dist/trade_finance_purchase/index.js +50 -0
- package/dist/trade_finance_purchase/module.d.ts +4252 -0
- package/dist/trade_finance_purchase/module.js +44 -0
- package/dist/trade_finance_sale/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_sale/.kosh/bindings.js +15 -0
- package/dist/trade_finance_sale/.kosh/schema.d.ts +1071 -0
- package/dist/trade_finance_sale/index.d.ts +8 -0
- package/dist/trade_finance_sale/index.js +50 -0
- package/dist/trade_finance_sale/module.d.ts +4280 -0
- package/dist/trade_finance_sale/module.js +44 -0
- package/dist/ui/api.d.ts +6 -1
- package/dist/ui/api.js +2 -1
- package/dist/ui/app.js +51 -17
- package/dist/ui/components/AppIcon.js +33 -1
- package/dist/ui/components/Avatar.d.ts +21 -0
- package/dist/ui/components/Avatar.js +71 -0
- package/dist/ui/components/primitives.d.ts +2 -0
- package/dist/ui/components/primitives.js +7 -1
- package/dist/ui/i18n/messages.d.ts +53 -0
- package/dist/ui/i18n/messages.js +98 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/offline/queue.d.ts +5 -1
- package/dist/ui/offline/queue.js +2 -0
- package/dist/ui/registry.d.ts +12 -1
- package/dist/ui/registry.js +10 -1
- package/dist/ui/search/ViewTabs.d.ts +14 -0
- package/dist/ui/search/ViewTabs.js +42 -0
- package/dist/ui/shell/Consent.d.ts +5 -0
- package/dist/ui/shell/Consent.js +35 -0
- package/dist/ui/shell/Home.js +136 -16
- package/dist/ui/shell/Sidebar.js +15 -22
- package/dist/ui/views/FormView.js +98 -17
- package/dist/ui/views/KanbanView.d.ts +2 -0
- package/dist/ui/views/KanbanView.js +32 -3
- package/dist/ui/views/ListView.js +155 -31
- package/dist/ui/views/StatsRow.d.ts +14 -0
- package/dist/ui/views/StatsRow.js +88 -0
- package/dist/ui/views/widgets.d.ts +2 -1
- package/dist/ui/views/widgets.js +194 -1
- package/dist/views/builders.d.ts +19 -2
- package/dist/views/builders.js +21 -1
- package/dist/views/condition.d.ts +6 -0
- package/dist/views/condition.js +20 -0
- package/dist/views/documents.d.ts +8 -0
- package/dist/views/extend.d.ts +2 -2
- package/dist/views/extend.js +11 -1
- package/dist/views/index.d.ts +2 -2
- package/dist/views/index.js +2 -2
- package/dist/views/types.d.ts +52 -5
- package/dist/views/validate.js +39 -1
- package/dist/website/.kosh/bindings.d.ts +22 -0
- package/dist/website/.kosh/bindings.js +15 -0
- package/dist/website/.kosh/schema.d.ts +881 -0
- package/dist/website/demo.d.ts +6 -0
- package/dist/website/demo.js +115 -0
- package/dist/website/index.d.ts +9 -0
- package/dist/website/index.js +250 -0
- package/dist/website/module.d.ts +2987 -0
- package/dist/website/module.js +281 -0
- package/dist/website/render.d.ts +45 -0
- package/dist/website/render.js +102 -0
- package/dist/website/routes.d.ts +12 -0
- package/dist/website/routes.js +246 -0
- package/dist/website/web/PagePreview.d.ts +7 -0
- package/dist/website/web/PagePreview.js +33 -0
- package/dist/website/web/index.d.ts +11 -0
- package/dist/website/web/index.js +16 -0
- package/dist/website/web/messages.d.ts +2 -0
- package/dist/website/web/messages.js +15 -0
- package/dist/website_crm/.kosh/bindings.d.ts +22 -0
- package/dist/website_crm/.kosh/bindings.js +15 -0
- package/dist/website_crm/.kosh/schema.d.ts +966 -0
- package/dist/website_crm/index.d.ts +3 -0
- package/dist/website_crm/index.js +39 -0
- package/dist/website_crm/module.d.ts +2955 -0
- package/dist/website_crm/module.js +44 -0
- package/package.json +118 -1
package/dist/settings/people.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { createHash, randomBytes } from 'node:crypto';
|
|
19
19
|
import { layout } from '../mail/index.js';
|
|
20
|
-
import { audit, HttpProblem, isAdministratorUser } from '../server/index.js';
|
|
20
|
+
import { audit, HttpProblem, imageVersions, isAdministratorUser, } from '../server/index.js';
|
|
21
21
|
/** How long an invitation works. */
|
|
22
22
|
export const INVITATION_DAYS = 7;
|
|
23
23
|
const ref = (env, name) => env.model(name);
|
|
@@ -164,6 +164,7 @@ export function registerPeopleRoutes(context, options) {
|
|
|
164
164
|
const invitations = await rows(ref(su, 'settings.invitation').search({ where: { user: { in: ids }, usedAt: null } }), ['user', 'expiresAt']);
|
|
165
165
|
const known = await knownGroups();
|
|
166
166
|
const admin = adminGroups(su);
|
|
167
|
+
const photos = await imageVersions(su, 'user', ids);
|
|
167
168
|
// "Purchase · Manager": a group named by the app level that gives it, else by its own label.
|
|
168
169
|
const levelLabel = new Map();
|
|
169
170
|
for (const m of modules)
|
|
@@ -184,6 +185,7 @@ export function registerPeopleRoutes(context, options) {
|
|
|
184
185
|
name: String(u['name'] ?? ''),
|
|
185
186
|
login: String(u['login'] ?? ''),
|
|
186
187
|
email: typeof u['email'] === 'string' ? u['email'] : null,
|
|
188
|
+
avatar: photos[String(id)] ?? null,
|
|
187
189
|
active: u['active'] !== false,
|
|
188
190
|
state: u['active'] === false
|
|
189
191
|
? 'deactivated'
|
|
@@ -278,16 +280,29 @@ export function registerPeopleRoutes(context, options) {
|
|
|
278
280
|
};
|
|
279
281
|
});
|
|
280
282
|
});
|
|
283
|
+
// An invitation (the person chooses their password), or — with `password` — a person added directly: the
|
|
284
|
+
// manager sets the password and passes it on; the login may then be a plain name (`rahim`) and the email optional.
|
|
281
285
|
app.post('/api/v1/settings/people', async (request, reply) => {
|
|
282
286
|
const body = (request.body ?? {});
|
|
283
287
|
const name = String(body['name'] ?? '').trim();
|
|
284
|
-
const
|
|
288
|
+
const address = String(body['email'] ?? '')
|
|
285
289
|
.trim()
|
|
286
290
|
.toLowerCase();
|
|
291
|
+
const password = typeof body['password'] === 'string' ? body['password'] : undefined;
|
|
292
|
+
const direct = password !== undefined;
|
|
293
|
+
const login = direct
|
|
294
|
+
? String(body['login'] ?? address)
|
|
295
|
+
.trim()
|
|
296
|
+
.toLowerCase()
|
|
297
|
+
: address;
|
|
287
298
|
if (name === '' || name.length > 200)
|
|
288
299
|
throw bad('The name is 1–200 characters.');
|
|
289
|
-
if (!EMAIL.test(login))
|
|
300
|
+
if (!direct && !EMAIL.test(login))
|
|
290
301
|
throw bad('The email is their login and where the invitation goes.');
|
|
302
|
+
if (direct && !/^[a-z0-9._@+-]{3,100}$/.test(login))
|
|
303
|
+
throw bad('The login is 3–100 letters, digits, or . _ @ + -.');
|
|
304
|
+
if (direct && address !== '' && !EMAIL.test(address))
|
|
305
|
+
throw bad('The email is not valid.');
|
|
291
306
|
const made = await guarded(request, async (env, su, manager) => {
|
|
292
307
|
const assignments = await checkAssignments(su, body['assignments'] ?? [], manager);
|
|
293
308
|
const taken = await ref(withArchived(su), 'res.users').search({ where: { login }, limit: 1 });
|
|
@@ -301,16 +316,27 @@ export function registerPeopleRoutes(context, options) {
|
|
|
301
316
|
{
|
|
302
317
|
login,
|
|
303
318
|
name,
|
|
304
|
-
email:
|
|
319
|
+
email: address === '' ? null : address,
|
|
305
320
|
company: home,
|
|
306
321
|
companies: { set: companies.length === 0 ? [env.companyId] : companies },
|
|
307
322
|
},
|
|
308
323
|
]);
|
|
309
324
|
const user = created.ids[0];
|
|
310
|
-
//
|
|
311
|
-
await auth.setPassword(su, user, randomBytes(32).toString('base64url'));
|
|
325
|
+
// Added directly: the manager's password (checked for length); invited: none usable until the link is used.
|
|
326
|
+
await auth.setPassword(su, user, password ?? randomBytes(32).toString('base64url'));
|
|
312
327
|
if (assignments.length > 0)
|
|
313
328
|
await ref(su, 'auth.user_group').create(assignments.map((a) => ({ user, group: a.group, company: a.company })));
|
|
329
|
+
if (direct) {
|
|
330
|
+
await audit(env, {
|
|
331
|
+
kind: 'people.created',
|
|
332
|
+
summary: `${name} (${login}) added with a password and ${assignments.length} role(s).`,
|
|
333
|
+
targetModel: 'res.users',
|
|
334
|
+
targetId: user,
|
|
335
|
+
details: { assignments },
|
|
336
|
+
ip: request.ip,
|
|
337
|
+
});
|
|
338
|
+
return { id: user, login, activationUrl: null, expiresAt: null, emailed: false };
|
|
339
|
+
}
|
|
314
340
|
const link = await invite(su, user, env.uid);
|
|
315
341
|
await audit(env, {
|
|
316
342
|
kind: 'people.invited',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings › App settings (RFC 0020): the apps' own settings, declared in their manifests. The apps you may see on
|
|
3
|
+
* the left; an app's sections as cards — each setting with its help, a mark when it differs from the default with
|
|
4
|
+
* *Reset*, and *Save* per section, sending only what changed. Company settings name the company they apply to. The
|
|
5
|
+
* server checks every value and who may change it, and audits each change. Each setting opens its history (who, when,
|
|
6
|
+
* old → new); administrators copy a company's settings to another, export them as a file and import one, every one
|
|
7
|
+
* of those previewed before anything is written.
|
|
8
|
+
*/
|
|
9
|
+
import { type PageProps } from '../../ui/index.js';
|
|
10
|
+
import { type ReactElement } from 'react';
|
|
11
|
+
export declare function AppSettingsPage({ api }: PageProps): ReactElement;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Settings › App settings (RFC 0020): the apps' own settings, declared in their manifests. The apps you may see on
|
|
4
|
+
* the left; an app's sections as cards — each setting with its help, a mark when it differs from the default with
|
|
5
|
+
* *Reset*, and *Save* per section, sending only what changed. Company settings name the company they apply to. The
|
|
6
|
+
* server checks every value and who may change it, and audits each change. Each setting opens its history (who, when,
|
|
7
|
+
* old → new); administrators copy a company's settings to another, export them as a file and import one, every one
|
|
8
|
+
* of those previewed before anything is written.
|
|
9
|
+
*/
|
|
10
|
+
import { Alert, ApiError, AppIcon, Badge, Button, Checkbox, DateField, Dialog, Input, Label, Select, Textarea, useKosh, } from '../../ui/index.js';
|
|
11
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
12
|
+
import { Copy, Download, History, RotateCcw, Upload } from 'lucide-react';
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { useTranslation } from 'react-i18next';
|
|
15
|
+
const shown = (value) => value === null || value === ''
|
|
16
|
+
? '—'
|
|
17
|
+
: value === true
|
|
18
|
+
? '✓'
|
|
19
|
+
: value === false
|
|
20
|
+
? '✗'
|
|
21
|
+
: String(value);
|
|
22
|
+
const BASE = '/api/v1/settings/app-settings';
|
|
23
|
+
const message = (error) => (error instanceof ApiError ? error.message : String(error));
|
|
24
|
+
/** The app shown: `?app=` in the address, else the first. */
|
|
25
|
+
function useChosenApp(apps) {
|
|
26
|
+
const [chosen, setChosen] = useState(() => new URLSearchParams(window.location.search).get('app') ?? undefined);
|
|
27
|
+
const set = (app) => {
|
|
28
|
+
setChosen(app);
|
|
29
|
+
const url = new URL(window.location.href);
|
|
30
|
+
url.searchParams.set('app', app);
|
|
31
|
+
window.history.replaceState(window.history.state, '', url);
|
|
32
|
+
};
|
|
33
|
+
return [apps.some((a) => a.app === chosen) ? chosen : apps[0]?.app, set];
|
|
34
|
+
}
|
|
35
|
+
function Control({ setting, value, disabled, onChange, }) {
|
|
36
|
+
const id = `setting-${setting.key}`;
|
|
37
|
+
const { t } = useTranslation('settings');
|
|
38
|
+
switch (setting.type) {
|
|
39
|
+
case 'boolean':
|
|
40
|
+
return (_jsx(Checkbox, { id: id, checked: value === true, disabled: disabled, onCheckedChange: (on) => onChange(on === true) }));
|
|
41
|
+
case 'selection':
|
|
42
|
+
return (_jsx(Select, { id: id, label: setting.label, value: typeof value === 'string' ? value : 'none', disabled: disabled, options: [{ value: 'none', label: t('appSettings.notSet') }, ...(setting.options ?? [])], onChange: (v) => onChange(v === 'none' ? null : v) }));
|
|
43
|
+
case 'text':
|
|
44
|
+
return (_jsx(Textarea, { id: id, rows: 4, disabled: disabled, value: typeof value === 'string' ? value : '', onChange: (e) => onChange(e.target.value) }));
|
|
45
|
+
case 'date':
|
|
46
|
+
return disabled ? (_jsx(Input, { id: id, disabled: true, className: "w-48", value: typeof value === 'string' ? value : '', readOnly: true })) : (_jsx(DateField, { id: id, value: typeof value === 'string' ? value : null, onChange: onChange }));
|
|
47
|
+
default:
|
|
48
|
+
return (_jsx(Input, { id: id, className: setting.type === 'char' ? undefined : 'w-48', disabled: disabled, type: setting.type === 'integer' ? 'number' : 'text', inputMode: setting.type === 'decimal' ? 'decimal' : undefined, min: setting.min ?? undefined, max: setting.max ?? undefined, value: value === null ? '' : String(value), onChange: (e) => {
|
|
49
|
+
const raw = e.target.value;
|
|
50
|
+
onChange(setting.type === 'integer'
|
|
51
|
+
? raw === ''
|
|
52
|
+
? null
|
|
53
|
+
: Number.parseInt(raw, 10)
|
|
54
|
+
: raw === ''
|
|
55
|
+
? null
|
|
56
|
+
: raw);
|
|
57
|
+
} }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Who changed a setting, when, from what to what — the audit log of this section, newest first. */
|
|
61
|
+
function HistoryDialog({ api, app, section, setting, onClose, }) {
|
|
62
|
+
const { t } = useTranslation('settings');
|
|
63
|
+
const { format } = useKosh();
|
|
64
|
+
const history = useQuery({
|
|
65
|
+
queryKey: ['settings', 'app-settings', app, section.key, 'history', setting?.key],
|
|
66
|
+
queryFn: () => api.call('GET', `${BASE}/${app}/${section.key}/history?key=${encodeURIComponent(setting?.key ?? '')}`),
|
|
67
|
+
enabled: setting !== null,
|
|
68
|
+
});
|
|
69
|
+
return (_jsx(Dialog, { open: setting !== null, onOpenChange: (open) => {
|
|
70
|
+
if (!open)
|
|
71
|
+
onClose();
|
|
72
|
+
}, title: t('appSettings.historyOf', { setting: setting?.label ?? '' }), children: history.error !== null ? (_jsx(Alert, { tone: "danger", children: message(history.error) })) : history.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : history.data.events.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('appSettings.neverChanged') })) : (_jsx("ol", { className: "grid gap-2 text-sm", children: history.data.events.map((e) => (_jsxs("li", { className: "grid gap-0.5 rounded-md border p-2", children: [_jsxs("span", { className: "text-xs text-muted-foreground", children: [format.datetime(e.at), " \u00B7 ", e.actor?.name ?? t('appSettings.system'), " \u00B7", ' ', t(`appSettings.how.${e.how}`)] }), e.changes.map((c) => (_jsxs("span", { className: "tabular-nums", children: [_jsx("s", { className: "text-muted-foreground", children: shown(c.from) }), " \u2192 ", _jsx("b", { children: shown(c.to) })] }, c.key)))] }, e.at))) })) }));
|
|
73
|
+
}
|
|
74
|
+
/** The changes a copy or an import would make, section by section. */
|
|
75
|
+
function ChangesList({ preview }) {
|
|
76
|
+
const { t } = useTranslation('settings');
|
|
77
|
+
return (_jsxs("div", { className: "grid gap-3 text-sm", children: [preview.sections.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: t('appSettings.nothingToChange') })) : (preview.sections.map((s) => (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "font-medium", children: [s.appLabel, " \u203A ", s.label] }), _jsx("ul", { className: "grid gap-0.5 ps-4", children: s.changes.map((c) => (_jsxs("li", { className: "tabular-nums", children: [c.label, ": ", _jsx("s", { className: "text-muted-foreground", children: shown(c.from) }), " \u2192", ' ', _jsx("b", { children: shown(c.to) })] }, c.key))) })] }, `${s.app}.${s.section}`)))), (preview.skipped ?? []).length === 0 ? null : (_jsx("p", { className: "text-xs text-muted-foreground", children: t('appSettings.skipped', { what: (preview.skipped ?? []).join(', ') }) }))] }));
|
|
78
|
+
}
|
|
79
|
+
/** Administrators: copy this company's settings to another, export them, import a file — each previewed first. */
|
|
80
|
+
function Tools({ api, onDone }) {
|
|
81
|
+
const { t } = useTranslation('settings');
|
|
82
|
+
const { companyId } = useKosh();
|
|
83
|
+
const [copying, setCopying] = useState(false);
|
|
84
|
+
const [to, setTo] = useState('');
|
|
85
|
+
const [importing, setImporting] = useState(null);
|
|
86
|
+
const [error, setError] = useState(null);
|
|
87
|
+
const [done, setDone] = useState(null);
|
|
88
|
+
const file = useRef(null);
|
|
89
|
+
const companies = useQuery({
|
|
90
|
+
queryKey: ['settings', 'companies'],
|
|
91
|
+
queryFn: () => api.call('GET', '/api/v1/settings/companies'),
|
|
92
|
+
enabled: copying,
|
|
93
|
+
});
|
|
94
|
+
const targets = (companies.data?.companies ?? []).filter((c) => c.id !== companyId && c.member);
|
|
95
|
+
const copyPreview = useQuery({
|
|
96
|
+
queryKey: ['settings', 'app-settings', 'copy', companyId, to],
|
|
97
|
+
queryFn: () => api.call('POST', `${BASE}/copy`, { from: companyId, to: Number(to), preview: true }),
|
|
98
|
+
enabled: copying && to !== '',
|
|
99
|
+
});
|
|
100
|
+
const copy = useMutation({
|
|
101
|
+
mutationFn: () => api.call('POST', `${BASE}/copy`, { from: companyId, to: Number(to) }),
|
|
102
|
+
onSuccess: (r) => {
|
|
103
|
+
setCopying(false);
|
|
104
|
+
setDone(t('appSettings.copied', { count: r.sections.length }));
|
|
105
|
+
onDone();
|
|
106
|
+
},
|
|
107
|
+
onError: (e) => setError(message(e)),
|
|
108
|
+
});
|
|
109
|
+
const importPreview = useQuery({
|
|
110
|
+
queryKey: ['settings', 'app-settings', 'import', importing],
|
|
111
|
+
queryFn: () => api.call('POST', `${BASE}/import`, { file: importing, preview: true }),
|
|
112
|
+
enabled: importing !== null,
|
|
113
|
+
retry: false,
|
|
114
|
+
});
|
|
115
|
+
const doImport = useMutation({
|
|
116
|
+
mutationFn: () => api.call('POST', `${BASE}/import`, { file: importing }),
|
|
117
|
+
onSuccess: (r) => {
|
|
118
|
+
setImporting(null);
|
|
119
|
+
setDone(t('appSettings.imported', { count: r.sections.length }));
|
|
120
|
+
onDone();
|
|
121
|
+
},
|
|
122
|
+
onError: (e) => setError(message(e)),
|
|
123
|
+
});
|
|
124
|
+
const exportFile = async () => {
|
|
125
|
+
try {
|
|
126
|
+
const data = await api.call('GET', `${BASE}/export`);
|
|
127
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
128
|
+
const url = URL.createObjectURL(blob);
|
|
129
|
+
const a = document.createElement('a');
|
|
130
|
+
a.href = url;
|
|
131
|
+
a.download = `app-settings-${data.company.name.replaceAll(/[^\w-]+/g, '_')}.json`;
|
|
132
|
+
a.click();
|
|
133
|
+
URL.revokeObjectURL(url);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
setError(message(e));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const read = (f) => {
|
|
140
|
+
if (f === undefined)
|
|
141
|
+
return;
|
|
142
|
+
f.text().then((text) => {
|
|
143
|
+
try {
|
|
144
|
+
setImporting(JSON.parse(text));
|
|
145
|
+
setError(null);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
setError(t('appSettings.notJson'));
|
|
149
|
+
}
|
|
150
|
+
}, (e) => setError(message(e)));
|
|
151
|
+
};
|
|
152
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { size: "sm", variant: "secondary", onClick: () => setCopying(true), children: [_jsx(Copy, { "aria-hidden": true }), t('appSettings.copyTo')] }), _jsxs(Button, { size: "sm", variant: "secondary", onClick: () => void exportFile(), children: [_jsx(Download, { "aria-hidden": true }), t('appSettings.export')] }), _jsxs(Button, { size: "sm", variant: "secondary", onClick: () => file.current?.click(), children: [_jsx(Upload, { "aria-hidden": true }), t('appSettings.import')] }), _jsx("input", { ref: file, type: "file", accept: "application/json,.json", className: "sr-only", "aria-label": t('appSettings.importFile'), onChange: (e) => {
|
|
153
|
+
read(e.target.files?.[0]);
|
|
154
|
+
e.target.value = '';
|
|
155
|
+
} }), done === null ? null : _jsx("span", { className: "text-sm text-muted-foreground", children: done }), error === null ? null : _jsx(Alert, { tone: "danger", children: error }), _jsx(Dialog, { open: copying, onOpenChange: setCopying, title: t('appSettings.copyTitle'), description: t('appSettings.copyIntro'), footer: _jsx(Button, { variant: "primary", disabled: to === '' || (copyPreview.data?.sections.length ?? 0) === 0, busy: copy.isPending, onClick: () => copy.mutate(), children: t('appSettings.copyNow') }), children: _jsxs("div", { className: "grid gap-3", children: [_jsx(Select, { id: "copy-to", label: t('appSettings.toCompany'), value: to === '' ? 'none' : to, options: [
|
|
156
|
+
{ value: 'none', label: t('appSettings.chooseCompany') },
|
|
157
|
+
...targets.map((c) => ({ value: String(c.id), label: c.name })),
|
|
158
|
+
], onChange: (v) => setTo(v === 'none' ? '' : v) }), copyPreview.error !== null ? (_jsx(Alert, { tone: "danger", children: message(copyPreview.error) })) : copyPreview.data === undefined ? null : (_jsx(ChangesList, { preview: copyPreview.data }))] }) }), _jsx(Dialog, { open: importing !== null, onOpenChange: (open) => {
|
|
159
|
+
if (!open)
|
|
160
|
+
setImporting(null);
|
|
161
|
+
}, title: t('appSettings.importTitle'), description: t('appSettings.importIntro'), footer: _jsx(Button, { variant: "primary", disabled: (importPreview.data?.sections.length ?? 0) === 0, busy: doImport.isPending, onClick: () => doImport.mutate(), children: t('appSettings.importNow') }), children: importPreview.error !== null ? (_jsx(Alert, { tone: "danger", children: message(importPreview.error) })) : importPreview.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (_jsx(ChangesList, { preview: importPreview.data })) })] }));
|
|
162
|
+
}
|
|
163
|
+
function SectionCard({ api, app, section, }) {
|
|
164
|
+
const { t } = useTranslation('settings');
|
|
165
|
+
const queryClient = useQueryClient();
|
|
166
|
+
const [draft, setDraft] = useState({});
|
|
167
|
+
const [history, setHistory] = useState(null);
|
|
168
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: a reloaded section starts a clean draft
|
|
169
|
+
useEffect(() => setDraft({}), [section]);
|
|
170
|
+
const refresh = () => void queryClient.invalidateQueries({ queryKey: ['settings', 'app-settings', app] });
|
|
171
|
+
const save = useMutation({
|
|
172
|
+
mutationFn: () => api.call('PATCH', `${BASE}/${app}/${section.key}`, { values: draft }),
|
|
173
|
+
onSuccess: refresh,
|
|
174
|
+
});
|
|
175
|
+
const reset = useMutation({
|
|
176
|
+
mutationFn: (key) => api.call('POST', `${BASE}/${app}/${section.key}/reset`, { keys: [key] }),
|
|
177
|
+
onSuccess: refresh,
|
|
178
|
+
});
|
|
179
|
+
const currentOf = (s) => (s.key in draft ? draft[s.key] : s.value);
|
|
180
|
+
const dirty = Object.keys(draft).length > 0;
|
|
181
|
+
return (_jsxs("section", { id: `section-${section.key}`, "aria-labelledby": `section-${section.key}-title`, className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("h2", { id: `section-${section.key}-title`, className: "font-semibold", children: section.label }), section.description === null ? null : (_jsx("p", { className: "text-sm text-muted-foreground", children: section.description }))] }), _jsx(Badge, { tone: "neutral", children: section.scope === 'company' ? t('appSettings.perCompany') : t('appSettings.everywhere') })] }), _jsx("div", { className: "grid gap-5", children: section.settings
|
|
182
|
+
.filter((s) => {
|
|
183
|
+
if (s.visibleWhen === null)
|
|
184
|
+
return true;
|
|
185
|
+
const other = section.settings.find((x) => x.key === s.visibleWhen?.key);
|
|
186
|
+
return other !== undefined && currentOf(other) === s.visibleWhen.equals;
|
|
187
|
+
})
|
|
188
|
+
.map((s) => (_jsxs("div", { className: "grid gap-1.5 sm:grid-cols-[16rem_1fr] sm:gap-6", children: [_jsxs("div", { className: "grid content-start gap-1", children: [_jsxs(Label, { htmlFor: `setting-${s.key}`, className: "flex flex-wrap items-center gap-2", children: [s.label, s.changed && !(s.key in draft) ? (_jsx(Badge, { tone: "info", children: t('appSettings.changed') })) : null] }), s.help === null ? null : _jsx("p", { className: "text-xs text-muted-foreground", children: s.help })] }), _jsxs("div", { className: "flex flex-wrap items-start gap-2", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(Control, { setting: s, value: currentOf(s), disabled: !section.editable, onChange: (v) => setDraft((d) => ({ ...d, [s.key]: v })) }) }), _jsx(Button, { size: "icon", variant: "ghost", "aria-label": t('appSettings.historyOf', { setting: s.label }), title: t('appSettings.history'), onClick: () => setHistory(s), children: _jsx(History, { "aria-hidden": true }) }), s.changed && section.editable && !(s.key in draft) ? (_jsxs(Button, { size: "sm", variant: "ghost", busy: reset.isPending && reset.variables === s.key, "aria-label": t('appSettings.resetOne', { setting: s.label }), title: t('appSettings.defaultIs', {
|
|
189
|
+
value: s.default === null || s.default === '' ? '—' : String(s.default),
|
|
190
|
+
}), onClick: () => reset.mutate(s.key), children: [_jsx(RotateCcw, { "aria-hidden": true }), t('appSettings.reset')] })) : null] })] }, s.key))) }), save.error === null && reset.error === null ? null : (_jsx(Alert, { tone: "danger", children: message(save.error ?? reset.error) })), _jsx(HistoryDialog, { api: api, app: app, section: section, setting: history, onClose: () => setHistory(null) }), section.editable ? (_jsxs("div", { className: "flex items-center justify-end gap-2", children: [save.isSuccess && !dirty ? (_jsx("span", { className: "text-sm text-muted-foreground", children: t('appSettings.saved') })) : null, dirty ? (_jsx(Button, { variant: "ghost", onClick: () => setDraft({}), children: t('appSettings.discard') })) : null, _jsx(Button, { variant: "primary", disabled: !dirty, busy: save.isPending, onClick: () => save.mutate(), children: t('appSettings.save') })] })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: t('appSettings.readOnly') }))] }));
|
|
191
|
+
}
|
|
192
|
+
export function AppSettingsPage({ api }) {
|
|
193
|
+
const { t } = useTranslation('settings');
|
|
194
|
+
const { companyId } = useKosh();
|
|
195
|
+
const queryClient = useQueryClient();
|
|
196
|
+
const list = useQuery({
|
|
197
|
+
queryKey: ['settings', 'app-settings'],
|
|
198
|
+
queryFn: () => api.call('GET', BASE),
|
|
199
|
+
});
|
|
200
|
+
const apps = list.data?.apps ?? [];
|
|
201
|
+
const [app, choose] = useChosenApp(apps);
|
|
202
|
+
const settings = useQuery({
|
|
203
|
+
queryKey: ['settings', 'app-settings', app, companyId],
|
|
204
|
+
queryFn: () => api.call('GET', `${BASE}/${app}`),
|
|
205
|
+
enabled: app !== undefined,
|
|
206
|
+
});
|
|
207
|
+
if (list.error !== null)
|
|
208
|
+
return _jsx(Alert, { tone: "danger", children: message(list.error) });
|
|
209
|
+
return (_jsxs("div", { className: "mx-auto grid max-w-5xl gap-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('appSettings.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('appSettings.intro') })] }), list.data?.administrator === true ? (_jsx(Tools, { api: api, onDone: () => void queryClient.invalidateQueries({ queryKey: ['settings', 'app-settings'] }) })) : null] }), list.data !== undefined && apps.length === 0 ? (_jsx("p", { className: "rounded-lg border bg-surface p-6 text-center text-sm text-muted-foreground", children: t('appSettings.none') })) : (_jsxs("div", { className: "grid gap-6 md:grid-cols-[13rem_1fr]", children: [_jsx("nav", { "aria-label": t('appSettings.apps'), className: "md:sticky md:top-16 md:self-start", children: _jsx("ul", { className: "flex gap-1 overflow-x-auto md:grid", children: apps.map((a) => (_jsx("li", { children: _jsxs("button", { type: "button", "aria-current": a.app === app ? 'page' : undefined, onClick: () => choose(a.app), className: `flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm hover:bg-muted ${a.app === app ? 'bg-accent font-medium text-accent-foreground' : ''}`, children: [_jsx(AppIcon, { name: a.icon ?? a.app, className: "size-6 rounded-md [&_svg]:size-3.5" }), a.label] }) }, a.app))) }) }), _jsx("div", { className: "grid gap-4", children: settings.error !== null ? (_jsx(Alert, { tone: "danger", children: message(settings.error) })) : settings.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (settings.data.sections.map((s) => (_jsx(SectionCard, { api: api, app: settings.data.app, section: s }, s.key)))) })] }))] }));
|
|
210
|
+
}
|
|
@@ -4,7 +4,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
4
4
|
* in it; switching to one; and, for administrators, a new company — its apps' defaults (journals, sequences, taxes…)
|
|
5
5
|
* are installed with it, and the administrators can switch to it at once.
|
|
6
6
|
*/
|
|
7
|
-
import { Alert, ApiError, Badge, Button, Dialog, Input, Label, Select, useKosh, } from '../../ui/index.js';
|
|
7
|
+
import { Alert, ApiError, Avatar, Badge, Button, Dialog, Input, imageUrl, Label, Select, useKosh, } from '../../ui/index.js';
|
|
8
8
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
9
9
|
import { Building2, Plus } from 'lucide-react';
|
|
10
10
|
import { useEffect, useId, useState } from 'react';
|
|
@@ -48,5 +48,5 @@ export function CompaniesPage({ api }) {
|
|
|
48
48
|
if (list.error !== null)
|
|
49
49
|
return _jsx(Alert, { tone: "danger", children: message(list.error) });
|
|
50
50
|
const data = list.data;
|
|
51
|
-
return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('companies.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('companies.intro') })] }), data?.canCreate === true ? (_jsxs(Button, { variant: "primary", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4", "aria-hidden": true }), t('companies.new')] })) : null] }), data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (_jsx("ul", { className: "divide-y rounded-lg border bg-surface", children: data.companies.map((c) => (_jsxs("li", { className: "flex flex-wrap items-center gap-3 px-4 py-3", children: [_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Building2, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "flex items-center gap-2 font-medium", children: [_jsx("span", { className: "truncate", children: c.name }), c.current ? _jsx(Badge, { tone: "info", children: t('companies.current') }) : null] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [c.currency, " \u00B7 ", t('companies.people', { count: c.people })] })] }), c.current || !c.member || switchCompany === undefined ? null : (_jsx(Button, { size: "sm", "aria-label": t('companies.switchTo', { name: c.name }), onClick: () => switchCompany(c.id), children: t('companies.switch') }))] }, c.id))) })), data === undefined ? null : (_jsx(NewCompanyDialog, { api: api, currencies: data.currencies, open: creating, onOpenChange: setCreating }))] }));
|
|
51
|
+
return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('companies.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('companies.intro') })] }), data?.canCreate === true ? (_jsxs(Button, { variant: "primary", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4", "aria-hidden": true }), t('companies.new')] })) : null] }), data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (_jsx("ul", { className: "divide-y rounded-lg border bg-surface", children: data.companies.map((c) => (_jsxs("li", { className: "flex flex-wrap items-center gap-3 px-4 py-3", children: [c.logo === null ? (_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Building2, { className: "size-4" }) })) : (_jsx(Avatar, { name: c.name, src: imageUrl('company', c.id, c.logo), shape: "square", className: "size-9 border" })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "flex items-center gap-2 font-medium", children: [_jsx("span", { className: "truncate", children: c.name }), c.current ? _jsx(Badge, { tone: "info", children: t('companies.current') }) : null] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [c.currency, " \u00B7 ", t('companies.people', { count: c.people })] })] }), c.current || !c.member || switchCompany === undefined ? null : (_jsx(Button, { size: "sm", "aria-label": t('companies.switchTo', { name: c.name }), onClick: () => switchCompany(c.id), children: t('companies.switch') }))] }, c.id))) })), data === undefined ? null : (_jsx(NewCompanyDialog, { api: api, currencies: data.currencies, open: creating, onOpenChange: setCreating }))] }));
|
|
52
52
|
}
|
|
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* apps add (tax ids, fiscal year…), as the server offers them. The preview shows the colour as buttons, links and
|
|
6
6
|
* selections take it, in light and dark; the themes keep text readable whatever colour is chosen.
|
|
7
7
|
*/
|
|
8
|
-
import { Alert, ApiError, accentVariables, Badge, Button, Checkbox, Input, Label, Select, Textarea, } from '../../ui/index.js';
|
|
8
|
+
import { Alert, ApiError, accentVariables, Badge, Button, Checkbox, ImagePicker, Input, imageUrl, Label, Select, Textarea, } from '../../ui/index.js';
|
|
9
9
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
10
10
|
import { Check } from 'lucide-react';
|
|
11
11
|
import { useId, useState } from 'react';
|
|
@@ -58,6 +58,17 @@ function CompanyForm({ api, company }) {
|
|
|
58
58
|
const [grouping, setGrouping] = useState(company.numberGrouping);
|
|
59
59
|
const [color, setColor] = useState(company.accentColor);
|
|
60
60
|
const [typed, setTyped] = useState(company.accentColor ?? '');
|
|
61
|
+
const me = useQuery({
|
|
62
|
+
queryKey: ['me'],
|
|
63
|
+
queryFn: () => api.call('GET', '/api/v1/auth/me'),
|
|
64
|
+
});
|
|
65
|
+
const logo = useMutation({
|
|
66
|
+
mutationFn: (image) => image === null
|
|
67
|
+
? api.call('DELETE', `/api/v1/images/company/${company.id}`)
|
|
68
|
+
: api.call('PUT', `/api/v1/images/company/${company.id}`, { image }),
|
|
69
|
+
// The sidebar and the switcher show the logo from the user's details.
|
|
70
|
+
onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['me'] }),
|
|
71
|
+
});
|
|
61
72
|
// Only the details changed are sent.
|
|
62
73
|
const [details, setDetails] = useState({});
|
|
63
74
|
const save = useMutation({
|
|
@@ -81,7 +92,7 @@ function CompanyForm({ api, company }) {
|
|
|
81
92
|
return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('company.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('company.intro') })] }), _jsxs("form", { className: "grid gap-6", onSubmit: (e) => {
|
|
82
93
|
e.preventDefault();
|
|
83
94
|
save.mutate();
|
|
84
|
-
}, children: [_jsxs("section", { className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsx("h2", { className: "font-semibold", children: t('company.identity') }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-name`, children: t('company.name') }), _jsx(Input, { id: `${id}-name`, value: name, required: true, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-grouping`, children: t('company.grouping') }), _jsx(Select, { id: `${id}-grouping`, label: t('company.grouping'), value: grouping, options: [
|
|
95
|
+
}, children: [_jsxs("section", { className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsx("h2", { className: "font-semibold", children: t('company.identity') }), _jsx(ImagePicker, { name: name, src: imageUrl('company', company.id, me.data?.logos?.[String(company.id)]), fit: "logo", busy: logo.isPending, onChoose: (image) => logo.mutate(image), onRemove: () => logo.mutate(null) }), logo.error === null ? null : (_jsx(Alert, { tone: "danger", children: logo.error instanceof ApiError ? logo.error.message : String(logo.error) })), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-name`, children: t('company.name') }), _jsx(Input, { id: `${id}-name`, value: name, required: true, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-grouping`, children: t('company.grouping') }), _jsx(Select, { id: `${id}-grouping`, label: t('company.grouping'), value: grouping, options: [
|
|
85
96
|
{ value: 'international', label: t('company.international') },
|
|
86
97
|
{ value: 'lakh', label: t('company.lakh') },
|
|
87
98
|
], onChange: (v) => setGrouping(v === 'lakh' ? 'lakh' : 'international') })] })] }), company.details.length === 0 ? null : (_jsxs("section", { className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold", children: t('company.details') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('company.detailsHint', { currency: company.currency }) })] }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: company.details.map((d) => (_jsx(DetailField, { detail: d, value: d.key in details ? details[d.key] : d.value, onChange: (v) => setDetails((all) => ({ ...all, [d.key]: v })) }, d.key))) })] })), _jsxs("section", { className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold", children: t('company.brand') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('company.brandHint') })] }), _jsxs("fieldset", { className: "grid gap-3", children: [_jsx("legend", { className: "sr-only", children: t('company.brand') }), _jsx("div", { className: "flex flex-wrap gap-2", children: SWATCHES.map((swatch) => (_jsx("button", { type: "button", "aria-label": swatch, "aria-pressed": color === swatch, onClick: () => pick(swatch), className: "grid size-8 place-items-center rounded-full border-2 border-surface ring-1 ring-border", style: { background: swatch }, children: color === swatch ? _jsx(Check, { className: "size-4 text-white", "aria-hidden": true }) : null }, swatch))) }), _jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-hex`, children: t('company.hex') }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "color", "aria-label": t('company.picker'), value: color ?? '#4f46e5', onChange: (e) => pick(e.target.value), className: "size-9 cursor-pointer rounded-md border bg-surface p-1" }), _jsx(Input, { id: `${id}-hex`, className: "w-32 font-mono", value: typed, placeholder: "#4f46e5", "aria-invalid": typed !== '' && !HEX.test(typed) ? true : undefined, onChange: (e) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connected apps (RFC 0017 §5.2), from the account menu: the apps you allowed to act for you through OAuth, what each
|
|
3
|
+
* may do in words, when it last did, and *Remove access* — which ends every token the app holds for you at once.
|
|
4
|
+
*/
|
|
5
|
+
import { type PageProps } from '../../ui/index.js';
|
|
6
|
+
import type { ReactElement } from 'react';
|
|
7
|
+
export declare function ConnectedAppsPage({ api }: PageProps): ReactElement;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Connected apps (RFC 0017 §5.2), from the account menu: the apps you allowed to act for you through OAuth, what each
|
|
4
|
+
* may do in words, when it last did, and *Remove access* — which ends every token the app holds for you at once.
|
|
5
|
+
*/
|
|
6
|
+
import { Alert, ApiError, Button, useKosh } from '../../ui/index.js';
|
|
7
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import { ExternalLink, Plug } from 'lucide-react';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
export function ConnectedAppsPage({ api }) {
|
|
11
|
+
const { t } = useTranslation('settings');
|
|
12
|
+
const { format } = useKosh();
|
|
13
|
+
const queryClient = useQueryClient();
|
|
14
|
+
const grants = useQuery({
|
|
15
|
+
queryKey: ['oauth', 'grants'],
|
|
16
|
+
queryFn: () => api.call('GET', '/api/v1/auth/oauth/grants'),
|
|
17
|
+
});
|
|
18
|
+
const revoke = useMutation({
|
|
19
|
+
mutationFn: (id) => api.call('POST', `/api/v1/auth/oauth/grants/${id}/revoke`),
|
|
20
|
+
onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['oauth', 'grants'] }),
|
|
21
|
+
});
|
|
22
|
+
const error = grants.error ?? revoke.error;
|
|
23
|
+
return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('connected.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('connected.intro') })] }), error === null ? null : (_jsx(Alert, { tone: "danger", children: error instanceof ApiError ? error.message : String(error) })), grants.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : grants.data.grants.length === 0 ? (_jsx("p", { className: "rounded-lg border bg-surface p-6 text-center text-sm text-muted-foreground", children: t('connected.none') })) : (_jsx("ul", { className: "grid gap-3", children: grants.data.grants.map((g) => (_jsxs("li", { className: "flex flex-wrap items-start gap-3 rounded-lg border bg-surface p-4", children: [_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Plug, { className: "size-4" }) }), _jsxs("div", { className: "grid min-w-0 flex-1 gap-1", children: [_jsxs("p", { className: "flex items-center gap-2 font-medium", children: [g.app, g.homepage === null ? null : (_jsx("a", { href: g.homepage, target: "_blank", rel: "noreferrer noopener", "aria-label": t('connected.homepage', { app: g.app }), className: "text-muted-foreground hover:text-foreground", children: _jsx(ExternalLink, { className: "size-3.5", "aria-hidden": true }) }))] }), _jsx("ul", { className: "flex flex-wrap gap-x-3 text-sm text-muted-foreground", children: g.scopes.map((s) => (_jsx("li", { children: s.label }, s.scope))) }), _jsx("p", { className: "text-xs text-muted-foreground", children: [
|
|
24
|
+
g.createdAt === null
|
|
25
|
+
? null
|
|
26
|
+
: t('connected.since', { when: format.date(g.createdAt.slice(0, 10)) }),
|
|
27
|
+
g.lastUsedAt === null
|
|
28
|
+
? t('connected.neverUsed')
|
|
29
|
+
: t('connected.lastUsed', { when: format.datetime(g.lastUsedAt) }),
|
|
30
|
+
]
|
|
31
|
+
.filter((x) => x !== null)
|
|
32
|
+
.join(' · ') })] }), _jsx(Button, { size: "sm", variant: "danger", busy: revoke.isPending && revoke.variables === g.id, onClick: () => revoke.mutate(g.id), "aria-label": t('connected.revokeApp', { app: g.app }), children: t('connected.revoke') })] }, g.id))) }))] }));
|
|
33
|
+
}
|
|
@@ -12,15 +12,39 @@ import { Search, UserRound } from 'lucide-react';
|
|
|
12
12
|
import { useId, useState } from 'react';
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
14
14
|
const normalise = (s) => s.toLocaleLowerCase().normalize('NFKD');
|
|
15
|
+
/** Each section's icon (AppIcon names); a module's own section shows the Settings gear. */
|
|
16
|
+
const SECTION_ICONS = {
|
|
17
|
+
'settings.action_company': 'company',
|
|
18
|
+
'settings.action_companies': 'companies',
|
|
19
|
+
'settings.action_people': 'users',
|
|
20
|
+
'settings.action_roles': 'shield',
|
|
21
|
+
'settings.action_security': 'security',
|
|
22
|
+
'settings.action_audit': 'audit',
|
|
23
|
+
'settings.action_apps': 'apps',
|
|
24
|
+
'settings.action_app_settings': 'settings',
|
|
25
|
+
'assistant.action_settings': 'assistant',
|
|
26
|
+
};
|
|
15
27
|
export function OverviewPage({ api }) {
|
|
16
28
|
const { t } = useTranslation('settings');
|
|
17
29
|
const id = useId();
|
|
18
30
|
const [query, setQuery] = useState('');
|
|
19
31
|
const menus = useQuery({ queryKey: ['menus', 'settings'], queryFn: () => api.menus('settings') });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
// The apps' own settings (RFC 0020): their sections' words make App settings findable by them.
|
|
33
|
+
const appSettings = useQuery({
|
|
34
|
+
queryKey: ['settings', 'app-settings'],
|
|
35
|
+
queryFn: () => api.call('GET', '/api/v1/settings/app-settings'),
|
|
23
36
|
});
|
|
37
|
+
const about = (action) => {
|
|
38
|
+
const words = action === 'settings.action_app_settings'
|
|
39
|
+
? (appSettings.data?.apps ?? [])
|
|
40
|
+
.flatMap((a) => [a.label, ...a.sections.flatMap((s) => [s.label, s.keywords])])
|
|
41
|
+
.join(' ')
|
|
42
|
+
: '';
|
|
43
|
+
return {
|
|
44
|
+
description: t(`overview.about.${action}.description`, { defaultValue: '' }),
|
|
45
|
+
keywords: `${t(`overview.about.${action}.keywords`, { defaultValue: '' })} ${words}`,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
24
48
|
const entry = (n) => n.action === undefined || n.action === 'settings.action_overview'
|
|
25
49
|
? null
|
|
26
50
|
: { label: n.label, action: n.action, ...about(n.action) };
|
|
@@ -62,5 +86,5 @@ export function OverviewPage({ api }) {
|
|
|
62
86
|
const showProfile = matches(profile);
|
|
63
87
|
return (_jsxs("div", { className: "mx-auto grid max-w-4xl gap-6", children: [_jsxs("header", { className: "grid gap-3", children: [_jsx("h1", { className: "text-2xl font-semibold", children: t('overview.title') }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": true }), _jsx("label", { htmlFor: `${id}-q`, className: "sr-only", children: t('overview.search') }), _jsx("input", { id: `${id}-q`, type: "search",
|
|
64
88
|
// biome-ignore lint/a11y/noAutofocus: the page is a search first; it opens on purpose
|
|
65
|
-
autoFocus: true, value: query, placeholder: t('overview.search'), onChange: (e) => setQuery(e.target.value), className: "h-11 w-full rounded-lg border border-input bg-surface ps-10 pe-3 text-base shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring" })] })] }), shown.length === 0 && !showProfile ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('overview.nothing', { query }) })) : null, shown.map((g) => (_jsxs("section", { "aria-labelledby": `${id}-${g.id}`, className: "grid gap-2", children: [_jsx("h2", { id: `${id}-${g.id}`, className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: g.label }), _jsx("ul", { className: "grid gap-3 sm:grid-cols-2", children: g.entries.map((e) => card(e, _jsxs(Link, { to: "/$app/a/$action", params: { app: 'settings', action: e.action }, className: "flex h-full items-start gap-3 rounded-lg border bg-surface p-4 hover:border-primary/40 hover:bg-accent/40", children: [_jsx(AppIcon, { name:
|
|
89
|
+
autoFocus: true, value: query, placeholder: t('overview.search'), onChange: (e) => setQuery(e.target.value), className: "h-11 w-full rounded-lg border border-input bg-surface ps-10 pe-3 text-base shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring" })] })] }), shown.length === 0 && !showProfile ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('overview.nothing', { query }) })) : null, shown.map((g) => (_jsxs("section", { "aria-labelledby": `${id}-${g.id}`, className: "grid gap-2", children: [_jsx("h2", { id: `${id}-${g.id}`, className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: g.label }), _jsx("ul", { className: "grid gap-3 sm:grid-cols-2", children: g.entries.map((e) => card(e, _jsxs(Link, { to: "/$app/a/$action", params: { app: 'settings', action: e.action }, className: "flex h-full items-start gap-3 rounded-lg border bg-surface p-4 hover:border-primary/40 hover:bg-accent/40", children: [_jsx(AppIcon, { name: SECTION_ICONS[e.action] ?? 'settings', className: "size-9" }), _jsxs("span", { className: "grid gap-0.5", children: [_jsx("span", { className: "font-medium", children: e.label }), e.description === '' ? null : (_jsx("span", { className: "text-sm text-muted-foreground", children: e.description }))] })] }))) })] }, g.id))), showProfile ? (_jsxs("section", { "aria-labelledby": `${id}-you`, className: "grid gap-2", children: [_jsx("h2", { id: `${id}-you`, className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: t('overview.you') }), _jsx("ul", { className: "grid gap-3 sm:grid-cols-2", children: card(profile, _jsxs(Link, { to: "/me/$page", params: { page: 'settings.Profile' }, className: "flex h-full items-start gap-3 rounded-lg border bg-surface p-4 hover:border-primary/40 hover:bg-accent/40", children: [_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-lg bg-accent text-accent-foreground", children: _jsx(UserRound, { className: "size-4" }) }), _jsxs("span", { className: "grid gap-0.5", children: [_jsx("span", { className: "font-medium", children: profile.label }), _jsx("span", { className: "text-sm text-muted-foreground", children: profile.description })] })] })) })] })) : null] }));
|
|
66
90
|
}
|