shutro-erp 0.3.4 → 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 +54 -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 +54 -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 +78 -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/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +54 -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 +54 -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/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/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +54 -3
- package/dist/payment/module.d.ts +359 -6
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +78 -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 +51 -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 +54 -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 +54 -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 +54 -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 +55 -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 +54 -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 +59 -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 +58 -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 +60 -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 +58 -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 +9 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/settings/app-settings.d.ts +2 -0
- package/dist/settings/app-settings.js +518 -0
- package/dist/settings/index.d.ts +1 -0
- package/dist/settings/index.js +15 -0
- package/dist/settings/people.js +29 -5
- package/dist/settings/web/AppSettingsPage.d.ts +11 -0
- package/dist/settings/web/AppSettingsPage.js +210 -0
- package/dist/settings/web/OverviewPage.js +16 -3
- package/dist/settings/web/PeoplePage.js +20 -6
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +3 -0
- package/dist/settings/web/messages.js +118 -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 +2 -1
- package/dist/ui/api.js +2 -1
- package/dist/ui/app.js +2 -1
- package/dist/ui/components/AppIcon.js +23 -1
- package/dist/ui/components/primitives.d.ts +2 -0
- package/dist/ui/components/primitives.js +7 -1
- package/dist/ui/i18n/messages.d.ts +23 -0
- package/dist/ui/i18n/messages.js +38 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/offline/queue.d.ts +5 -1
- package/dist/ui/offline/queue.js +2 -0
- package/dist/ui/registry.d.ts +5 -0
- package/dist/ui/shell/Home.js +55 -4
- 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 +108 -11
- 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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data layer of Sites (RFC 0021): websites per company, pages built from blocks (published or draft, in English or
|
|
3
|
+
* Bangla, translations linked), and forms whose submissions become records of the apps that allow it — a contact
|
|
4
|
+
* here, a lead with `website_crm`. The public rendering is the Next.js site's; this is the content and its API.
|
|
5
|
+
*/
|
|
6
|
+
import { contactsShutro } from '../contacts/index.js';
|
|
7
|
+
import { messagingShutro } from '../messaging/index.js';
|
|
8
|
+
import { orm } from '../server/orm.js';
|
|
9
|
+
import { check, defineModel, defineModule, f, method, records, } from './.kosh/bindings.js';
|
|
10
|
+
const ALL = ['read', 'write', 'create', 'unlink'];
|
|
11
|
+
const LANGS = { en: 'English', bn: 'Bangla' };
|
|
12
|
+
/** A site's key and a form's key: what URLs name them by. */
|
|
13
|
+
export const KEY = /^[a-z0-9](?:[a-z0-9-]{0,62})$/;
|
|
14
|
+
/** A page's path: `/`, `/about`, `/products/rice`. */
|
|
15
|
+
export const PATH = /^\/(?:[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*)?$/;
|
|
16
|
+
/** A form field's input name. */
|
|
17
|
+
export const INPUT = /^[a-z][a-z0-9_]{0,39}$/;
|
|
18
|
+
export const WebsiteSite = defineModel('website.site', {
|
|
19
|
+
label: 'Site',
|
|
20
|
+
recName: 'name',
|
|
21
|
+
order: ['name asc'],
|
|
22
|
+
companyScoped: { required: true },
|
|
23
|
+
archivable: true,
|
|
24
|
+
access: {
|
|
25
|
+
'website.editor': ['read'],
|
|
26
|
+
'website.manager': [...ALL, 'export'],
|
|
27
|
+
},
|
|
28
|
+
fields: {
|
|
29
|
+
name: f.char({ required: true }),
|
|
30
|
+
/** The slug the public API names the site by (`padma`): `/api/v1/public/sites/padma/…`. */
|
|
31
|
+
key: f.char({ required: true, size: 63 }),
|
|
32
|
+
domain: f.char({
|
|
33
|
+
label: 'Domain',
|
|
34
|
+
help: 'Where the public site is served, e.g. www.example.com',
|
|
35
|
+
}),
|
|
36
|
+
lang: f.selection(LANGS, { required: true, default: 'en', label: 'Default language' }),
|
|
37
|
+
pages: f.one2many('website.page', 'site'),
|
|
38
|
+
},
|
|
39
|
+
sqlConstraints: {
|
|
40
|
+
oneKey: { unique: ['key'], message: 'Another site has this key.' },
|
|
41
|
+
},
|
|
42
|
+
constraints: {
|
|
43
|
+
keyShape: {
|
|
44
|
+
...check('website.site', ['key'], async (sites) => sites.filter((s) => !KEY.test(s.key)).map((s) => s.id)),
|
|
45
|
+
message: 'A key is lower-case letters, digits and dashes, e.g. padma-traders.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
export const WebsitePage = defineModel('website.page', {
|
|
50
|
+
label: 'Page',
|
|
51
|
+
recName: 'title',
|
|
52
|
+
order: ['site asc', 'sequence asc', 'path asc'],
|
|
53
|
+
companyScoped: { required: true },
|
|
54
|
+
tracking: ['state', 'path', 'title'],
|
|
55
|
+
access: {
|
|
56
|
+
'website.editor': ['read', 'write', 'create', 'unlink'],
|
|
57
|
+
'website.manager': [...ALL, 'export', 'import'],
|
|
58
|
+
},
|
|
59
|
+
fields: {
|
|
60
|
+
site: f.many2one('website.site', { required: true, ondelete: 'cascade' }),
|
|
61
|
+
path: f.char({ required: true, default: '/', help: '/ for the home page, /about, /contact' }),
|
|
62
|
+
lang: f.selection(LANGS, { required: true, default: 'en', label: 'Language' }),
|
|
63
|
+
title: f.char({ required: true }),
|
|
64
|
+
/** For search engines and link previews. */
|
|
65
|
+
description: f.text({ label: 'Summary' }),
|
|
66
|
+
inMenu: f.boolean({ default: true, label: 'In the menu' }),
|
|
67
|
+
sequence: f.integer({ required: true, default: 10 }),
|
|
68
|
+
/** The page this one translates (its other languages link to the same original). */
|
|
69
|
+
translationOf: f.many2one('website.page', { ondelete: 'set null', label: 'Translation of' }),
|
|
70
|
+
blocks: f.one2many('website.block', 'page', { label: 'Content' }),
|
|
71
|
+
state: f.state({
|
|
72
|
+
states: { draft: 'Draft', published: 'Published' },
|
|
73
|
+
initial: 'draft',
|
|
74
|
+
transitions: {
|
|
75
|
+
publish: {
|
|
76
|
+
from: ['draft'],
|
|
77
|
+
to: 'published',
|
|
78
|
+
after: records('website.page', async (pages) => {
|
|
79
|
+
await pages.write({ publishedAt: new Date() });
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
unpublish: { from: ['published'], to: 'draft' },
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
publishedAt: f.datetime({ label: 'Published on' }),
|
|
86
|
+
},
|
|
87
|
+
sqlConstraints: {
|
|
88
|
+
onePath: {
|
|
89
|
+
unique: ['site', 'path', 'lang'],
|
|
90
|
+
message: 'The site already has a page at this path in this language.',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
constraints: {
|
|
94
|
+
pathShape: {
|
|
95
|
+
...check('website.page', ['path'], async (pages) => pages.filter((p) => !PATH.test(p.path)).map((p) => p.id)),
|
|
96
|
+
message: 'A path starts with / and has letters, digits, dashes, dots or underscores: /about-us.',
|
|
97
|
+
},
|
|
98
|
+
sameSite: {
|
|
99
|
+
...check('website.page', ['translationOf', 'site'], async (pages) => (await pages.load({ translationOf: true }))
|
|
100
|
+
.filter((p) => p.translationOf !== null &&
|
|
101
|
+
(p.translationOf.id === p.id || p.translationOf.site.id !== p.site.id))
|
|
102
|
+
.map((p) => p.id)),
|
|
103
|
+
message: 'A page translates another page of the same site.',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
/** A piece of a page, in order: what the public site renders, one component per kind. */
|
|
108
|
+
export const WebsiteBlock = defineModel('website.block', {
|
|
109
|
+
label: 'Block',
|
|
110
|
+
order: ['sequence asc', 'id asc'],
|
|
111
|
+
access: {
|
|
112
|
+
'website.editor': ['read', 'write', 'create', 'unlink'],
|
|
113
|
+
'website.manager': ALL,
|
|
114
|
+
},
|
|
115
|
+
fields: {
|
|
116
|
+
page: f.many2one('website.page', { required: true, ondelete: 'cascade' }),
|
|
117
|
+
sequence: f.integer({ required: true, default: 10 }),
|
|
118
|
+
kind: f.selection({ hero: 'Hero', text: 'Text', image: 'Image', cta: 'Call to action', form: 'Form' }, { required: true, default: 'text' }),
|
|
119
|
+
title: f.char(),
|
|
120
|
+
body: f.text({ label: 'Text' }),
|
|
121
|
+
imageUrl: f.char({ label: 'Image URL', size: 1000 }),
|
|
122
|
+
buttonLabel: f.char({ label: 'Button' }),
|
|
123
|
+
buttonUrl: f.char({ label: 'Button link', size: 1000 }),
|
|
124
|
+
form: f.many2one('website.form', { ondelete: 'restrict' }),
|
|
125
|
+
},
|
|
126
|
+
constraints: {
|
|
127
|
+
formBlock: {
|
|
128
|
+
...check('website.block', ['kind', 'form'], async (blocks) => blocks.filter((b) => b.kind === 'form' && b.form === null).map((b) => b.id)),
|
|
129
|
+
message: 'A form block names its form.',
|
|
130
|
+
},
|
|
131
|
+
safeLinks: {
|
|
132
|
+
...check('website.block', ['imageUrl', 'buttonUrl'], async (blocks) => blocks
|
|
133
|
+
.filter((b) => [b.imageUrl, b.buttonUrl].some((u) => u !== null && !safeUrl(u)))
|
|
134
|
+
.map((b) => b.id)),
|
|
135
|
+
message: 'A link is a path on the site (/contact) or an http(s) address.',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
/** The models website forms may create (`targets()` on `website.form`, as bridges extend it). */
|
|
140
|
+
export async function formTargets(env) {
|
|
141
|
+
return env.model('website.form').browse([]).targets();
|
|
142
|
+
}
|
|
143
|
+
/** What a record of `target` made from a submission starts with (`submissionValues()`, as bridges extend it). */
|
|
144
|
+
export async function submissionValues(env, target) {
|
|
145
|
+
return env.model('website.form').browse([]).submissionValues(target);
|
|
146
|
+
}
|
|
147
|
+
/** A link a public page may carry: a path on the site, or http(s) — never `javascript:` or `data:`. */
|
|
148
|
+
export function safeUrl(url) {
|
|
149
|
+
if (url.startsWith('/') && !url.startsWith('//'))
|
|
150
|
+
return true;
|
|
151
|
+
return /^https?:\/\/[^\s]+$/i.test(url);
|
|
152
|
+
}
|
|
153
|
+
export const WebsiteForm = defineModel('website.form', {
|
|
154
|
+
label: 'Website form',
|
|
155
|
+
recName: 'name',
|
|
156
|
+
order: ['name asc'],
|
|
157
|
+
companyScoped: { required: true },
|
|
158
|
+
archivable: true,
|
|
159
|
+
access: {
|
|
160
|
+
'website.editor': ['read'],
|
|
161
|
+
'website.manager': [...ALL, 'export'],
|
|
162
|
+
},
|
|
163
|
+
fields: {
|
|
164
|
+
name: f.char({ required: true }),
|
|
165
|
+
/** What the public API names it by: `POST /api/v1/public/sites/:site/forms/:key`. */
|
|
166
|
+
key: f.char({ required: true, size: 63 }),
|
|
167
|
+
/** The model a submission becomes; one of `targets()`. */
|
|
168
|
+
target: f.char({ required: true, default: 'res.partner', label: 'Creates' }),
|
|
169
|
+
fields: f.one2many('website.form.field', 'form', { label: 'Fields' }),
|
|
170
|
+
successMessage: f.text({
|
|
171
|
+
required: true,
|
|
172
|
+
default: 'Thank you. We will be in touch soon.',
|
|
173
|
+
label: 'Message after sending',
|
|
174
|
+
}),
|
|
175
|
+
submissions: f.one2many('website.submission', 'form'),
|
|
176
|
+
},
|
|
177
|
+
methods: {
|
|
178
|
+
/** The models a form may create: contacts here; bridges add theirs (`website_crm`: `crm.lead`). */
|
|
179
|
+
targets: method('website.form', async () => ['res.partner']),
|
|
180
|
+
/**
|
|
181
|
+
* What a record made from a submission starts with, before the form's inputs (which win): nothing here; bridges
|
|
182
|
+
* set their own (`website_crm`: a deal from the website, for no salesperson yet).
|
|
183
|
+
*/
|
|
184
|
+
submissionValues: method('website.form', async (_target) => ({})),
|
|
185
|
+
},
|
|
186
|
+
sqlConstraints: {
|
|
187
|
+
oneKey: { unique: ['company', 'key'], message: 'Another form has this key.' },
|
|
188
|
+
},
|
|
189
|
+
constraints: {
|
|
190
|
+
keyShape: {
|
|
191
|
+
...check('website.form', ['key'], async (forms) => forms.filter((x) => !KEY.test(x.key)).map((x) => x.id)),
|
|
192
|
+
message: 'A key is lower-case letters, digits and dashes, e.g. contact-us.',
|
|
193
|
+
},
|
|
194
|
+
allowedTarget: {
|
|
195
|
+
...check('website.form', ['target'], async (forms) => {
|
|
196
|
+
const allowed = forms.length === 0 ? [] : await formTargets(forms.env);
|
|
197
|
+
return forms.filter((x) => !allowed.includes(x.target)).map((x) => x.id);
|
|
198
|
+
}),
|
|
199
|
+
message: 'A website form creates contacts, or what an installed app allows (a lead with Pipeline).',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
export const WebsiteFormField = defineModel('website.form.field', {
|
|
204
|
+
label: 'Form field',
|
|
205
|
+
order: ['sequence asc', 'id asc'],
|
|
206
|
+
access: {
|
|
207
|
+
'website.editor': ['read'],
|
|
208
|
+
'website.manager': ALL,
|
|
209
|
+
},
|
|
210
|
+
fields: {
|
|
211
|
+
form: f.many2one('website.form', { required: true, ondelete: 'cascade' }),
|
|
212
|
+
sequence: f.integer({ required: true, default: 10 }),
|
|
213
|
+
/** The input's name in what the visitor sends (`email`). */
|
|
214
|
+
name: f.char({ required: true, size: 40, label: 'Input name' }),
|
|
215
|
+
label: f.char({ required: true }),
|
|
216
|
+
kind: f.selection({
|
|
217
|
+
text: 'Text',
|
|
218
|
+
email: 'Email',
|
|
219
|
+
phone: 'Phone',
|
|
220
|
+
textarea: 'Long text',
|
|
221
|
+
number: 'Number',
|
|
222
|
+
date: 'Date',
|
|
223
|
+
}, { required: true, default: 'text', label: 'Type' }),
|
|
224
|
+
required: f.boolean({ default: false }),
|
|
225
|
+
/** The field of the created record it fills (`email`); plain fields only, checked on submission. */
|
|
226
|
+
targetField: f.char({ required: true, size: 80, label: 'Fills' }),
|
|
227
|
+
},
|
|
228
|
+
sqlConstraints: {
|
|
229
|
+
oneName: { unique: ['form', 'name'], message: 'The form already has an input with this name.' },
|
|
230
|
+
},
|
|
231
|
+
constraints: {
|
|
232
|
+
nameShape: {
|
|
233
|
+
...check('website.form.field', ['name'], async (fields) => fields.filter((x) => !INPUT.test(x.name) || x.name === HONEYPOT).map((x) => x.id)),
|
|
234
|
+
message: 'An input name is lower-case letters, digits and underscores, starting with a letter.',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
/** The hidden input a person leaves empty and a bot fills in. */
|
|
239
|
+
export const HONEYPOT = 'website_url';
|
|
240
|
+
/** What a visitor sent through a form, and what it became. */
|
|
241
|
+
export const WebsiteSubmission = defineModel('website.submission', {
|
|
242
|
+
label: 'Submission',
|
|
243
|
+
order: ['id desc'],
|
|
244
|
+
companyScoped: { required: true },
|
|
245
|
+
access: {
|
|
246
|
+
'website.editor': ['read'],
|
|
247
|
+
'website.manager': ['read', 'unlink', 'export'],
|
|
248
|
+
},
|
|
249
|
+
fields: {
|
|
250
|
+
form: f.many2one('website.form', { required: true, ondelete: 'cascade' }),
|
|
251
|
+
site: f.many2one('website.site', { ondelete: 'set null' }),
|
|
252
|
+
/** The values as sent (the form's inputs only). */
|
|
253
|
+
data: f.json({ required: true, default: {} }),
|
|
254
|
+
state: f.selection({ received: 'Received', spam: 'Spam' }, { required: true, default: 'received' }),
|
|
255
|
+
resModel: f.char({ label: 'Created (model)' }),
|
|
256
|
+
resId: f.integer({ label: 'Created (id)' }),
|
|
257
|
+
/** The created record's name, as it was. */
|
|
258
|
+
resName: f.char({ label: 'Created' }),
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
export const websiteShutro = defineModule({
|
|
262
|
+
name: 'website',
|
|
263
|
+
version: '0.1.0',
|
|
264
|
+
depends: [contactsShutro, messagingShutro],
|
|
265
|
+
groups: [
|
|
266
|
+
orm.defineGroup({ code: 'website.editor', label: 'Sites editor' }),
|
|
267
|
+
orm.defineGroup({
|
|
268
|
+
code: 'website.manager',
|
|
269
|
+
label: 'Sites manager',
|
|
270
|
+
implies: ['website.editor'],
|
|
271
|
+
}),
|
|
272
|
+
],
|
|
273
|
+
models: [
|
|
274
|
+
WebsiteSite,
|
|
275
|
+
WebsitePage,
|
|
276
|
+
WebsiteBlock,
|
|
277
|
+
WebsiteForm,
|
|
278
|
+
WebsiteFormField,
|
|
279
|
+
WebsiteSubmission,
|
|
280
|
+
],
|
|
281
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a page is to the outside (RFC 0021, Sites): its blocks in order with the forms they show, as the public API
|
|
3
|
+
* returns it, and as HTML for the editors' preview — every text escaped, links only to the site or http(s).
|
|
4
|
+
*/
|
|
5
|
+
import type { Id } from '../server/orm.js';
|
|
6
|
+
import type { Env } from './.kosh/bindings.js';
|
|
7
|
+
export interface PublicForm {
|
|
8
|
+
readonly key: string;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly fields: readonly {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly kind: string;
|
|
14
|
+
readonly required: boolean;
|
|
15
|
+
}[];
|
|
16
|
+
/** The hidden input to leave empty (a bot fills it in). */
|
|
17
|
+
readonly honeypot: string;
|
|
18
|
+
readonly successMessage: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PublicBlock {
|
|
21
|
+
readonly kind: string;
|
|
22
|
+
readonly title: string | null;
|
|
23
|
+
readonly body: string | null;
|
|
24
|
+
readonly imageUrl: string | null;
|
|
25
|
+
readonly buttonLabel: string | null;
|
|
26
|
+
readonly buttonUrl: string | null;
|
|
27
|
+
readonly form: PublicForm | null;
|
|
28
|
+
}
|
|
29
|
+
export interface PublicPage {
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly lang: string;
|
|
32
|
+
readonly title: string;
|
|
33
|
+
readonly description: string | null;
|
|
34
|
+
readonly publishedAt: string | null;
|
|
35
|
+
readonly blocks: readonly PublicBlock[];
|
|
36
|
+
/** The same page in its other languages (published ones, for the public API). */
|
|
37
|
+
readonly translations: readonly {
|
|
38
|
+
readonly lang: string;
|
|
39
|
+
readonly path: string;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
42
|
+
/** A page with its blocks, forms and translations; `published` keeps translations to published pages. */
|
|
43
|
+
export declare function pageOf(env: Env, id: Id<'website.page'>, published: boolean): Promise<PublicPage | null>;
|
|
44
|
+
/** The page as a stand-alone document (no scripts): the editors' preview. */
|
|
45
|
+
export declare function pageHtml(page: PublicPage): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { HONEYPOT, safeUrl } from './module.js';
|
|
2
|
+
/** A page with its blocks, forms and translations; `published` keeps translations to published pages. */
|
|
3
|
+
export async function pageOf(env, id, published) {
|
|
4
|
+
const [page] = await env.model('website.page').browse([id]).load({ translationOf: true });
|
|
5
|
+
if (page === undefined)
|
|
6
|
+
return null;
|
|
7
|
+
const blocks = [
|
|
8
|
+
...(await (await env.model('website.block').search({ where: { page: page.id } })).load({
|
|
9
|
+
form: true,
|
|
10
|
+
})),
|
|
11
|
+
];
|
|
12
|
+
const formIds = [...new Set(blocks.flatMap((b) => (b.form === null ? [] : [b.form.id])))];
|
|
13
|
+
const fields = formIds.length === 0
|
|
14
|
+
? []
|
|
15
|
+
: [...(await env.model('website.form.field').search({ where: { form: { in: formIds } } }))];
|
|
16
|
+
const formOf = (b) => b.form === null
|
|
17
|
+
? null
|
|
18
|
+
: {
|
|
19
|
+
key: b.form.key,
|
|
20
|
+
name: b.form.name,
|
|
21
|
+
fields: fields
|
|
22
|
+
.filter((x) => x.form.id === b.form?.id)
|
|
23
|
+
.map((x) => ({ name: x.name, label: x.label, kind: x.kind, required: x.required })),
|
|
24
|
+
honeypot: HONEYPOT,
|
|
25
|
+
successMessage: b.form.successMessage,
|
|
26
|
+
};
|
|
27
|
+
const original = page.translationOf?.id ?? page.id;
|
|
28
|
+
const family = await env.model('website.page').search({
|
|
29
|
+
where: {
|
|
30
|
+
or: [{ id: original }, { translationOf: original }],
|
|
31
|
+
...(published ? { state: 'published' } : {}),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
path: page.path,
|
|
36
|
+
lang: page.lang,
|
|
37
|
+
title: page.title,
|
|
38
|
+
description: page.description,
|
|
39
|
+
publishedAt: page.publishedAt?.toISOString() ?? null,
|
|
40
|
+
blocks: blocks.map((b) => ({
|
|
41
|
+
kind: b.kind,
|
|
42
|
+
title: b.title,
|
|
43
|
+
body: b.body,
|
|
44
|
+
imageUrl: b.imageUrl !== null && safeUrl(b.imageUrl) ? b.imageUrl : null,
|
|
45
|
+
buttonLabel: b.buttonLabel,
|
|
46
|
+
buttonUrl: b.buttonUrl !== null && safeUrl(b.buttonUrl) ? b.buttonUrl : null,
|
|
47
|
+
form: formOf(b),
|
|
48
|
+
})),
|
|
49
|
+
translations: [...family]
|
|
50
|
+
.filter((p) => p.id !== page.id)
|
|
51
|
+
.map((p) => ({ lang: p.lang, path: p.path })),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c);
|
|
55
|
+
const paragraphs = (text) => (text ?? '')
|
|
56
|
+
.split(/\n{2,}/)
|
|
57
|
+
.filter((p) => p.trim() !== '')
|
|
58
|
+
.map((p) => `<p>${escapeHtml(p).replace(/\n/g, '<br>')}</p>`)
|
|
59
|
+
.join('');
|
|
60
|
+
const button = (b) => b.buttonLabel !== null && b.buttonUrl !== null
|
|
61
|
+
? `<a class="button" href="${escapeHtml(b.buttonUrl)}">${escapeHtml(b.buttonLabel)}</a>`
|
|
62
|
+
: '';
|
|
63
|
+
function blockHtml(b) {
|
|
64
|
+
const title = b.title === null ? '' : escapeHtml(b.title);
|
|
65
|
+
switch (b.kind) {
|
|
66
|
+
case 'hero':
|
|
67
|
+
return `<section class="hero">${title === '' ? '' : `<h1>${title}</h1>`}${paragraphs(b.body)}${button(b)}</section>`;
|
|
68
|
+
case 'image':
|
|
69
|
+
return b.imageUrl === null
|
|
70
|
+
? ''
|
|
71
|
+
: `<figure><img src="${escapeHtml(b.imageUrl)}" alt="${title}">${title === '' ? '' : `<figcaption>${title}</figcaption>`}</figure>`;
|
|
72
|
+
case 'cta':
|
|
73
|
+
return `<section class="cta">${title === '' ? '' : `<h2>${title}</h2>`}${paragraphs(b.body)}${button(b)}</section>`;
|
|
74
|
+
case 'form': {
|
|
75
|
+
const form = b.form;
|
|
76
|
+
if (form === null)
|
|
77
|
+
return '';
|
|
78
|
+
const inputs = form.fields
|
|
79
|
+
.map((x) => {
|
|
80
|
+
const tag = x.kind === 'textarea'
|
|
81
|
+
? `<textarea name="${escapeHtml(x.name)}" disabled></textarea>`
|
|
82
|
+
: `<input name="${escapeHtml(x.name)}" type="${x.kind === 'phone' ? 'tel' : escapeHtml(x.kind)}" disabled>`;
|
|
83
|
+
return `<label>${escapeHtml(x.label)}${x.required ? ' *' : ''}${tag}</label>`;
|
|
84
|
+
})
|
|
85
|
+
.join('');
|
|
86
|
+
return `<section class="form">${title === '' ? '' : `<h2>${title}</h2>`}${paragraphs(b.body)}<form>${inputs}<button type="button" disabled>${escapeHtml(b.buttonLabel ?? 'Send')}</button></form></section>`;
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
return `<section>${title === '' ? '' : `<h2>${title}</h2>`}${paragraphs(b.body)}${button(b)}</section>`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const STYLE = `body{font:16px/1.6 system-ui,sans-serif;margin:0;color:#1f2933;background:#fff}
|
|
93
|
+
main{max-width:760px;margin:0 auto;padding:24px 16px}section,figure{margin:0 0 32px}
|
|
94
|
+
.hero{padding:32px 0}.hero h1{font-size:2.2em;line-height:1.2;margin:0 0 12px}
|
|
95
|
+
.cta{padding:24px;border-radius:12px;background:#f1f5f9}img{max-width:100%;border-radius:8px}
|
|
96
|
+
.button{display:inline-block;padding:8px 16px;border-radius:8px;background:#2563eb;color:#fff;text-decoration:none}
|
|
97
|
+
form{display:grid;gap:12px}label{display:grid;gap:4px;font-weight:600}input,textarea{font:inherit;padding:6px 8px}
|
|
98
|
+
@media (prefers-color-scheme: dark){body{background:#111827;color:#e5e7eb}.cta{background:#1f2937}}`;
|
|
99
|
+
/** The page as a stand-alone document (no scripts): the editors' preview. */
|
|
100
|
+
export function pageHtml(page) {
|
|
101
|
+
return `<!doctype html><html lang="${escapeHtml(page.lang)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(page.title)}</title><style>${STYLE}</style></head><body><main>${page.blocks.map(blockHtml).join('')}</main></body></html>`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sites' routes (RFC 0021). The public ones need no sign-in but stay rate limited per address (`auth: false`); they
|
|
3
|
+
* read in a system scope across companies — published pages only — and keep what they read for a while per server.
|
|
4
|
+
* A submission is checked value by value and becomes its record in an audited system scope of the site's company.
|
|
5
|
+
*/
|
|
6
|
+
import { type ModuleContext } from '../server/index.js';
|
|
7
|
+
export declare const SUBMITTED = "website.form.submitted";
|
|
8
|
+
export interface SitesOptions {
|
|
9
|
+
/** How long a server keeps a public read (default 30 s; 0: never). */
|
|
10
|
+
readonly cacheMs?: number | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerSitesRoutes(context: ModuleContext, options?: SitesOptions): void;
|