@protobase/cli 0.63.5 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/package.js +1 -1
- package/package.json +7 -7
- package/skills/protobase/references/features/lists.md +1 -0
- package/skills/protobase/references/features/navigation.md +1 -0
- package/skills/protobase/references/guides/billing-page.md +4 -4
- package/skills/protobase/references/guides/first-composed-page.md +5 -2
- package/skills/protobase/references/guides/table-to-admin.md +1 -1
- package/skills/protobase/references/reference/history.md +1 -0
- package/skills/protobase/references/reference/sidebar.md +11 -1
- package/skills/protobase/references/reference/ui-config.md +32 -9
- package/skills/protobase/references/reference/user-menu.md +1 -1
package/dist/cli/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@better-auth/core": "^1.7.12",
|
|
44
44
|
"@hono/node-server": "^2.1.3",
|
|
45
|
-
"@protobase/config": "0.
|
|
46
|
-
"@protobase/layout": "0.
|
|
47
|
-
"@protobase/query": "0.
|
|
48
|
-
"@protobase/schema": "0.
|
|
49
|
-
"@protobase/server": "0.
|
|
50
|
-
"@protobase/ui": "0.
|
|
45
|
+
"@protobase/config": "0.65.0",
|
|
46
|
+
"@protobase/layout": "0.65.0",
|
|
47
|
+
"@protobase/query": "0.65.0",
|
|
48
|
+
"@protobase/schema": "0.65.0",
|
|
49
|
+
"@protobase/server": "0.65.0",
|
|
50
|
+
"@protobase/ui": "0.65.0",
|
|
51
51
|
"@react-email/render": "^2.1.0",
|
|
52
52
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
53
53
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -9,6 +9,7 @@ The list page of each resource, with its columns, filters, charts, search, expor
|
|
|
9
9
|
- [Email, phone and web links](../reference/ui-config.md#names-and-columns): Fields marked as email, phone or URL show as links and are edited with the matching phone keyboard and browser autofill.
|
|
10
10
|
- [Markdown and long text](../reference/ui-config.md#names-and-columns): Long text is edited in a box that grows with it. Markdown fields have Write and Preview and show rendered on the record page.
|
|
11
11
|
- [Enum labels and colours](../reference/ui-config.md#names-and-columns): Enum values get readable labels and badge colours that show everywhere the value appears, and badges can be drawn as arrows, as on a rating scale.
|
|
12
|
+
- [Value tones and currencies](../reference/ui-config.md#value-tones-and-currencies): An enum value's badge takes the tone or colour its view gives it, and a money field shows in the ISO 4217 currency its view names.
|
|
12
13
|
- [Pinned title column](../reference/ui-config.md#names-and-columns): Columns size to their content, and on desktop the title column stays in place while the table scrolls sideways.
|
|
13
14
|
- [Pinned columns](../reference/ui-config.md#names-and-columns): A column's menu pins it to the left edge of the table, and the browser keeps the pins for each list.
|
|
14
15
|
- [Resizable columns](../reference/ui-config.md#names-and-columns): Dragging a table column header's right edge, or the arrow keys on it, makes that column wider or narrower, down to a usable minimum. The widths last until the page is left or reloaded.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
The sidebar, the user menu, the bottom bar on phones and the global search.
|
|
6
6
|
|
|
7
7
|
- [Sidebar](../reference/sidebar.md): Resources and pages in groups, in the order you set.
|
|
8
|
+
- [Sidebar icons](../reference/sidebar.md#sidebar-icons): Each resource shows the icon its view names in the sidebar, bottom bar and profile menu.
|
|
8
9
|
- [Recent records](../reference/sidebar.md#recent-records): A sidebar entry lists a few records under it, with status dots or as titles alone, and marked per reader, such as unread ones in bold with a count.
|
|
9
10
|
- [Create link](../reference/sidebar.md#create-link): A sidebar entry links to its create form, such as "New message" under Direct messages, for whoever may create.
|
|
10
11
|
- [The current organization](../reference/sidebar.md#the-current-organization): A sidebar entry opens its list on the organization you work in, which you can clear to see every organization you are a member of.
|
|
@@ -46,11 +46,11 @@ import type { billingProfiles, invoices, paymentMethods, plans, subscriptions }
|
|
|
46
46
|
|
|
47
47
|
// The views of the billing schema: names, labels, and the named actions the billing page's buttons run.
|
|
48
48
|
|
|
49
|
-
export const plansView = view<typeof plans>('plans').title((r) => r.name).nav({ hidden: true }).fields((r) => ({ price: r.price.prefix('€').decimals(2) }))
|
|
49
|
+
export const plansView = view<typeof plans>('plans').title((r) => r.name).nav({ hidden: true }).fields((r) => ({ price: r.price.prefix('€').currency('EUR').decimals(2) }))
|
|
50
50
|
|
|
51
51
|
export const subscriptionsView = view<typeof subscriptions>('subscriptions')
|
|
52
52
|
.nav({ hidden: true })
|
|
53
|
-
.fields((r) => ({ renews_at: r.renews_at.label('Renews on'), status: r.status.valueLabels({ active: 'Active', canceled: 'Canceled' }) }))
|
|
53
|
+
.fields((r) => ({ renews_at: r.renews_at.label('Renews on'), status: r.status.valueLabels({ active: 'Active', canceled: 'Canceled' }).valueColors({ active: 'green' }) }))
|
|
54
54
|
.actions((a) => [
|
|
55
55
|
a.update('cancel', { label: 'Cancel plan', set: { status: 'canceled' }, confirm: 'Your plan stays active until the end of the period.' }),
|
|
56
56
|
a.link('upgrade', { label: 'Upgrade', href: 'mailto:sales@example.com?subject=Upgrade%20from%20plan%20{plan}' }),
|
|
@@ -65,10 +65,10 @@ export const paymentMethodsView = view<typeof paymentMethods>('payment_methods')
|
|
|
65
65
|
a.remove('remove', { label: 'Remove', confirm: 'The card is removed from your account.' }),
|
|
66
66
|
])
|
|
67
67
|
|
|
68
|
-
export const invoicesView = view<typeof invoices>('invoices')
|
|
68
|
+
export const invoicesView = view<typeof invoices>('invoices').icon('file-text')
|
|
69
69
|
.title((r) => r.number)
|
|
70
70
|
.names({ singular: 'Invoice', plural: 'Invoices' })
|
|
71
|
-
.fields((r) => ({ total: r.total.prefix('€').decimals(2), issued_at: r.issued_at.label('Date'), status: r.status.valueLabels({ open: 'Open', paid: 'Paid' }) }))
|
|
71
|
+
.fields((r) => ({ number: r.number.format('code'), total: r.total.prefix('€').currency('EUR').decimals(2), issued_at: r.issued_at.label('Date'), status: r.status.valueLabels({ open: 'Open', paid: 'Paid' }).valueColors({ paid: 'green' }) }))
|
|
72
72
|
.list((r) => ({ columns: [r.number, r.issued_at, r.total, r.status] }))
|
|
73
73
|
.actions((a) => [a.action('download', { label: 'Download PDF' })])
|
|
74
74
|
```
|
|
@@ -122,6 +122,7 @@ import type { orders } from './data'
|
|
|
122
122
|
export const ordersView = view<typeof orders>('orders')
|
|
123
123
|
.title((r) => r.number)
|
|
124
124
|
.names({ singular: 'Order', plural: 'Orders' })
|
|
125
|
+
.icon('shopping-cart')
|
|
125
126
|
.nav({
|
|
126
127
|
recent: {
|
|
127
128
|
status: 'status',
|
|
@@ -132,9 +133,11 @@ export const ordersView = view<typeof orders>('orders')
|
|
|
132
133
|
},
|
|
133
134
|
})
|
|
134
135
|
.fields((r) => ({
|
|
136
|
+
number: r.number.format('code'),
|
|
135
137
|
status: r.status.help('Where the order is in its life. Moving it forward can notify the customer.').format('badge')
|
|
136
|
-
.valueLabels({ draft: 'Draft', confirmed: 'Confirmed', picking: 'Picking', shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled' })
|
|
137
|
-
|
|
138
|
+
.valueLabels({ draft: 'Draft', confirmed: 'Confirmed', picking: 'Picking', shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled' })
|
|
139
|
+
.valueColors({ confirmed: 'blue', picking: 'amber', shipped: 'violet', delivered: 'green', cancelled: 'red' }),
|
|
140
|
+
total: r.total.help('Order total including VAT.').prefix('€').currency('EUR').decimals(2),
|
|
138
141
|
discount_percent: r.discount_percent.label('Discount').help('Percentage taken off the order total.').format('percent'),
|
|
139
142
|
paid: r.paid.help('Set when the payment has been received.'),
|
|
140
143
|
created_at: r.created_at.label('Created').format('absolute'),
|
|
@@ -95,7 +95,7 @@ export const repairsView = view<typeof repairs>('repairs')
|
|
|
95
95
|
number: r.number.format('code'),
|
|
96
96
|
customer_id: r.customer_id.label('Customer'),
|
|
97
97
|
status: r.status.format('badge'),
|
|
98
|
-
estimate: r.estimate.prefix('€').decimals(2),
|
|
98
|
+
estimate: r.estimate.prefix('€').currency('EUR').decimals(2),
|
|
99
99
|
booked_on: r.booked_on.label('Booked on'),
|
|
100
100
|
}))
|
|
101
101
|
.list((r) => ({
|
|
@@ -26,6 +26,7 @@ export const orders = resource('orders')
|
|
|
26
26
|
|
|
27
27
|
export const ordersView = view<typeof orders>('orders')
|
|
28
28
|
.title((r) => r.number)
|
|
29
|
+
.fields((r) => ({ number: r.number.format('code'), status: r.status.valueColors({ paid: 'green', shipped: 'violet', cancelled: 'red' }) }))
|
|
29
30
|
.layout((r) => [
|
|
30
31
|
l.section('Order', [r.number, r.status, r.total, r.iban]),
|
|
31
32
|
// the last 10 changes, newest first, with Show more for the rest
|
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
# Sidebar
|
|
4
4
|
|
|
5
|
-
How sidebar entries are grouped, how an entry lists recent records, with status dots or without, and how it links to its create form.
|
|
5
|
+
How sidebar entries are grouped and which icon each shows, how an entry lists recent records, with status dots or without, and how it links to its create form.
|
|
6
6
|
|
|
7
7
|
The admin's sidebar has one entry per resource view, grouped by `nav.group` (default: the database schema, with no group name for `public`) and sorted by `nav.order`, then by the order `config/index.ts` exports them, in `protobase dev` and in a built bundle alike. The app opens on the first entry. A view's [named lists](ui-config.md#more-than-one-list) are entries of their own, placed by their own `nav`. `nav({ hidden: true })` leaves a resource out, for line and link tables; a resource in the [user menu](user-menu.md) is left out too. [Composed pages](layouts.md#setup) have entries of their own, and groups that hold pages come first. A [remote resource](remote-resources.md) has its entry where its source's view places it; while its source cannot be reached, it sits under the source's label, marked "Cannot reach" with the source's name.
|
|
8
8
|
|
|
9
|
+
## Sidebar icons
|
|
10
|
+
|
|
11
|
+
Each resource shows the icon its view names, in the sidebar, the [bottom bar](bottom-bar.md) and the [user menu](user-menu.md), and its [named lists](ui-config.md#more-than-one-list) show the same icon:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
export const ordersView = view<typeof orders>('orders').names({ singular: 'Order', plural: 'Orders' }).icon('shopping-cart')
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`icon` is one of `bell`, `book-open`, `bookmark`, `building`, `credit-card`, `external-link`, `file-text`, `globe`, `heart`, `key`, `layout-dashboard`, `life-buoy`, `mail`, `package`, `receipt`, `settings`, `shield`, `shopping-cart`, `tags`, `users` or `warehouse`. A view without one, or with a name not in this list, shows a table icon, whatever the resource is called. A bottom bar tab's or user menu item's own `icon` wins over the view's.
|
|
18
|
+
|
|
9
19
|
## The current organization
|
|
10
20
|
|
|
11
21
|
For a resource whose lists read [across organizations](access.md#lists-across-organizations), `nav({ tenant: 'current' })` opens its entry on the organization the session works in, while the person can still clear or widen it:
|
|
@@ -35,7 +35,7 @@ export const repairsView = view<typeof repairs>('repairs')
|
|
|
35
35
|
number: r.number.format('code'),
|
|
36
36
|
customer_id: r.customer_id.label('Customer'),
|
|
37
37
|
status: r.status.format('badge'),
|
|
38
|
-
estimate: r.estimate.prefix('€').decimals(2),
|
|
38
|
+
estimate: r.estimate.prefix('€').currency('EUR').decimals(2),
|
|
39
39
|
booked_on: r.booked_on.label('Booked on'),
|
|
40
40
|
}))
|
|
41
41
|
.list((r) => ({
|
|
@@ -55,7 +55,7 @@ export const repairsView = view<typeof repairs>('repairs')
|
|
|
55
55
|
| `.fields((r) => ({ name: r.name.label(...) }))` | Hints per field, below. Calls add up. |
|
|
56
56
|
| `.help(text)` | A line under the list's title. |
|
|
57
57
|
| `.nav({ group, order, hidden, recent, tenant })` | Where the resource sits in the sidebar, its [recent-record group](sidebar.md#recent-records), and with `tenant: 'current'` that it opens on [the current organization](sidebar.md#the-current-organization). |
|
|
58
|
-
| `.icon(name)` |
|
|
58
|
+
| `.icon(name)` | The resource's icon in the sidebar, bottom bar and user menu; a table icon without one. See [Sidebar icons](sidebar.md#sidebar-icons). |
|
|
59
59
|
| `.listOf((r) => r.listing_id)` | The list page shows the records this relation points at instead of the resource's own rows, such as the listings among a member's favorites. See [Favorites](#favorites). |
|
|
60
60
|
| `.signIn({ title, description, benefits?, icon?, signUp? })` | What a visitor who is not signed in is told about the resource, in place of its list and by a card's favorite button. See [Sign-in prompts](#sign-in-prompts). |
|
|
61
61
|
| `.form((r) => ({ name, fields, thanks, title?, perMinute? }))` | A public form at `/-/forms/<name>` that creates a record for anyone, signed in or not, behind a rate limit and a bot check. See [Public forms](forms.md). |
|
|
@@ -98,17 +98,36 @@ Field hints:
|
|
|
98
98
|
| `.label(text)` | Instead of the name in words (`customer_id` would be "Customer" anyway). |
|
|
99
99
|
| `.help(text, { when? })` | A line under the field on the record page and in forms. `when: 'editing'` shows it only while the record is edited; see [Help while editing](#help-while-editing). |
|
|
100
100
|
| `.confirm(question)` | Asks `question` before a person changes the value in a form or a list cell, and Cancel keeps it. See [Confirming a change](#confirming-a-change). |
|
|
101
|
-
| `.format(format)` | `badge` shows statuses as coloured badges, `code` sets identifiers in monospace, `percent` adds a % sign. On a text field, `email`, `phone` and `url` show the value as a link that writes to it (`mailto:`), calls it (`tel:`, its digits and a leading +) or opens it in a new tab (only an `http` or `https` address; anything else stays text), on the record page, in its sidebar and in the list's cells but the title's, which links to the record; forms edit it in an email, phone or web address input, which brings up the keyboard for it on a mobile screen and offers the browser's autofill. Another field type with one of them stops the server at startup. `relative` on a timestamp shows it as "5 minutes ago" or "in 2 hours" in lists and on the record page, with the exact time on hover; `absolute` and `compact` are accepted and carried in `/meta` for apps that render their own lists. <gap>The admin does not use the `absolute` and `compact` formats yet.</gap> |
|
|
101
|
+
| `.format(format)` | `badge` shows statuses as coloured badges, `code` sets identifiers in monospace, as the list's identifier column too, `percent` adds a % sign, and `signed` shows a whole number in a list with its sign, green above zero and red below, such as a stock change of `+12` or `−3`; it needs an integer field. On a text field, `email`, `phone` and `url` show the value as a link that writes to it (`mailto:`), calls it (`tel:`, its digits and a leading +) or opens it in a new tab (only an `http` or `https` address; anything else stays text), on the record page, in its sidebar and in the list's cells but the title's, which links to the record; forms edit it in an email, phone or web address input, which brings up the keyboard for it on a mobile screen and offers the browser's autofill. Another field type with one of them stops the server at startup. `relative` on a timestamp shows it as "5 minutes ago" or "in 2 hours" in lists and on the record page, with the exact time on hover; `absolute` and `compact` are accepted and carried in `/meta` for apps that render their own lists. <gap>The admin does not use the `absolute` and `compact` formats yet.</gap> |
|
|
102
102
|
| `.multiline()` | A text of several lines, such as a description: forms edit it in a box that grows with the text, so the page scrolls rather than the box, and the record page's sections, a page's field blocks and a card's subtitle keep its line breaks (a card shows the first three lines); the record's sidebar shows it on one line. On the record page and the create form it takes the whole width of its section. Only a text field takes it; the list's table shows the text on one line. |
|
|
103
103
|
| `.markdown()` | Markdown, such as a task's notes: forms edit its source in a box that grows with it, with a Write and Preview switch above it, and store it exactly as typed. The record page's sections and a page's field blocks show it rendered as GitHub does, line breaks kept; raw HTML shows as text, links and images keep only http(s), mailto, tel and relative addresses, and links open in a new tab. A card's subtitle shows its first three lines rendered, its links as text; the list's table, the record's sidebar and a card's other lines show its text on one line without the marks. On the record page and the create form it takes the whole width of its section. Only a text field takes it, and the renderer loads with the first markdown shown. |
|
|
104
|
-
| `.prefix(text)`, `.suffix(text)`, `.decimals(n)` |
|
|
104
|
+
| `.prefix(text)`, `.suffix(text)`, `.decimals(n)` | Text around a number and its decimals: `€` and `2` show `€1,234.50`, `.suffix(' m²')` shows `120 m²`. They show wherever the number does: records, cards, table cells, forms and the range filter. A prefix is plain text, so a list does not show `€` as money; `.currency()` does. |
|
|
105
|
+
| `.currency(code)` | The currency a decimal is an amount of, as an ISO 4217 code such as `EUR`: the list shows it as money in that currency, in its table and its range filter. See [Value tones and currencies](#value-tones-and-currencies). |
|
|
105
106
|
| `.icon(name)` | An icon before the value on a card's meta row: one of the names [actions](layouts.md#named-actions) take, such as `bed`, `calendar` or `cpu`, and above the value in a section of [key facts](#key-facts). Nowhere else shows it. |
|
|
106
107
|
| `.valueLabels({ value: text })` | How an enum's values read: `r.method.valueLabels({ ideal: 'iDEAL' })`. Only the enum's values are accepted. A value without a label shows as stored (`direct_debit`), so label every value that should read as words. |
|
|
107
|
-
| `.valueColors({ value: '#hex' })`, `.badgeShape('arrow')` | An enum's badges filled with a colour
|
|
108
|
+
| `.valueColors({ value: tone or '#hex' })`, `.badgeShape('arrow')` | An enum's badges in a tone per value, or filled with a colour (the text turns white or black, whichever reads), and drawn as arrows pointing right, as on a rating scale. See [Value tones and currencies](#value-tones-and-currencies). The property portal's energy label: `r.energy_label.format('badge').valueColors({ a: '#00a651', g: '#ed1c24' }).badgeShape('arrow')`. Only the enum's values are accepted. |
|
|
108
109
|
| `.widget(name, props?)` | Shows and edits the field with a [field type](plugins.md#field-types) of the project or a plugin, such as `r.status.widget('steps')` or `r.score.widget('rating', { max: 5 })`, or with the built-in [`members`](plugins.md#members), which picks an organization's members into a JSON array of user ids; `props` are plain data. Without it, or on a storage type it does not show, the field shows as it would without the hint. |
|
|
109
110
|
|
|
110
111
|
An enum's value labels show everywhere the admin shows its values: badges, select options, list and related-table cells, filter options and chips, record titles, the sidebar and search results. They are part of the view in `/api/meta`, so an app with its own screens can show them too, and [`enumLabel(value, labels)`](https://docs.protobase.net/api/schema/functions/enumlabel/) from `@protobase/schema` reads a value the same way. A label for a value the enum does not have stops the server at startup, so renaming a value cannot leave a stale label behind.
|
|
111
112
|
|
|
113
|
+
### Value tones and currencies
|
|
114
|
+
|
|
115
|
+
An enum value's badge takes the tone or colour its view gives it, and a money field shows in the ISO 4217 currency its view names. Nothing is read from the names of values or fields: a value the view gives no tone is `neutral`, and a decimal without a currency is a number.
|
|
116
|
+
|
|
117
|
+
`.valueColors(...)` gives each value one of the badge tones, `neutral`, `blue`, `green`, `amber`, `red` or `violet`, or a hex colour that fills the badge. The tone shows on the value's badges in lists, boards and the record page, in the record page's heading and in a list's [related dots](#related-records-in-a-list); a hex colour colours a chart's group too. Anything else throws when the view is built.
|
|
118
|
+
|
|
119
|
+
`.currency('EUR')` on a decimal field makes the list show it as money in that currency, with the currency's sign and grouping for the language, in the table and in its range filter. A currency on another field type stops the server at startup, as does `.format('signed')` on a field that is not an integer.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
view<typeof orders>('orders').fields((r) => ({
|
|
123
|
+
number: r.number.format('code'),
|
|
124
|
+
status: r.status.valueColors({ confirmed: 'blue', shipped: 'violet', delivered: 'green', cancelled: 'red' }),
|
|
125
|
+
total: r.total.prefix('€').currency('EUR').decimals(2),
|
|
126
|
+
}))
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
<gap>The record page, forms, cards, charts and exports cannot show a field in its currency because reading `.currency()` outside lists is not implemented.</gap> They show the field's `.prefix()`, so a money field takes both, as above.
|
|
130
|
+
|
|
112
131
|
The table's columns take their width from the first rows it shows, and keep it while it scrolls, filters and sorts: the `.title` column gets the largest share, other text a share as long as its values, and short values such as enums, booleans, numbers, amounts and dates their own width whole. A value cut short shows whole when hovered. On a desktop the title column is pinned to the left by default, with the column before it when it is the second, so it stays in view when the table scrolls sideways; the column menu unpins it, and pins any other column to the left edge, and the choice is kept in the browser for that list. A column of the table is made wider or narrower by dragging its header's right edge, or with the left and right arrow keys on that edge, down to 64 pixels; the other columns keep their width, a drag never sorts, and the widths last until the page is left or reloaded. The row menu stays at the table's right edge as it scrolls sideways. Embedded lists and a page's `Table` blocks size their columns the same way.
|
|
113
132
|
|
|
114
133
|
### Related records in a list
|
|
@@ -128,7 +147,7 @@ view<typeof objectives>('objectives').list((r, d, c) => ({
|
|
|
128
147
|
| Option | |
|
|
129
148
|
| --- | --- |
|
|
130
149
|
| `resource`, `field`, `through`, `filter` | Which records each row shows, as a [related section](#related-records) finds them: those whose relation `field` holds the row's key, or, through a join resource, those `through`'s records pick, so a task can serve several objectives. |
|
|
131
|
-
| `status` | An enum field of `resource`. Each dot is filled with the colour its value has in the resource's view (`.valueColors(...)`),
|
|
150
|
+
| `status` | An enum field of `resource`. Each dot is filled with the colour or tone its value has in the resource's view (`.valueColors(...)`), `neutral` without one. Required. |
|
|
132
151
|
| `sort` | The dots' order, AIP-132 on `resource`. |
|
|
133
152
|
| `label` | The column's header; default the name in words. The name must not be a field of the resource. |
|
|
134
153
|
|
|
@@ -211,6 +230,7 @@ export const productsView = view<typeof products>('products')
|
|
|
211
230
|
|
|
212
231
|
export const invoicesView = view<typeof invoices>('invoices')
|
|
213
232
|
.title((r) => r.number)
|
|
233
|
+
.fields((r) => ({ number: r.number.format('code') }))
|
|
214
234
|
.layout((r) => [
|
|
215
235
|
l.section('Invoice', [r.number, r.customer]),
|
|
216
236
|
l.related('Items', { resource: 'invoice_items', field: 'invoice_id', sort: 'position' }),
|
|
@@ -904,7 +924,7 @@ export const unitsView = view<typeof units>('units')
|
|
|
904
924
|
property_id: r.property_id.label('Property'),
|
|
905
925
|
area_m2: r.area_m2.label('Area').help('Living area in m².').suffix(' m²'),
|
|
906
926
|
energy_label: r.energy_label.format('badge'),
|
|
907
|
-
base_rent: r.base_rent.label('Market rent').help('What a new lease would ask today, per month.').prefix('€').decimals(2),
|
|
927
|
+
base_rent: r.base_rent.label('Market rent').help('What a new lease would ask today, per month.').prefix('€').currency('EUR').decimals(2),
|
|
908
928
|
status: r.status.help('Let while a lease runs; vacant or under renovation between leases.').format('badge').valueLabels({ let: 'Let', vacant: 'Vacant', renovation: 'Renovation' }),
|
|
909
929
|
}))
|
|
910
930
|
.list((r) => ({ columns: [r.label, r.property_id, r.status, r.bedrooms, r.area_m2, r.energy_label, r.base_rent], search: [r.label] }))
|
|
@@ -1016,6 +1036,7 @@ import type { orders } from './data'
|
|
|
1016
1036
|
export const ordersView = view<typeof orders>('orders')
|
|
1017
1037
|
.title((r) => r.number)
|
|
1018
1038
|
.names({ singular: 'Order', plural: 'Orders' })
|
|
1039
|
+
.icon('shopping-cart')
|
|
1019
1040
|
.nav({
|
|
1020
1041
|
recent: {
|
|
1021
1042
|
status: 'status',
|
|
@@ -1026,9 +1047,11 @@ export const ordersView = view<typeof orders>('orders')
|
|
|
1026
1047
|
},
|
|
1027
1048
|
})
|
|
1028
1049
|
.fields((r) => ({
|
|
1050
|
+
number: r.number.format('code'),
|
|
1029
1051
|
status: r.status.help('Where the order is in its life. Moving it forward can notify the customer.').format('badge')
|
|
1030
|
-
.valueLabels({ draft: 'Draft', confirmed: 'Confirmed', picking: 'Picking', shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled' })
|
|
1031
|
-
|
|
1052
|
+
.valueLabels({ draft: 'Draft', confirmed: 'Confirmed', picking: 'Picking', shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled' })
|
|
1053
|
+
.valueColors({ confirmed: 'blue', picking: 'amber', shipped: 'violet', delivered: 'green', cancelled: 'red' }),
|
|
1054
|
+
total: r.total.help('Order total including VAT.').prefix('€').currency('EUR').decimals(2),
|
|
1032
1055
|
discount_percent: r.discount_percent.label('Discount').help('Percentage taken off the order total.').format('percent'),
|
|
1033
1056
|
paid: r.paid.help('Set when the payment has been received.'),
|
|
1034
1057
|
created_at: r.created_at.label('Created').format('absolute'),
|
|
@@ -26,7 +26,7 @@ export const accountMenu = userMenu((m) => [
|
|
|
26
26
|
| `m.page(name, { label?, icon? })` | Opens a [composed page](layouts.md). The page leaves the sidebar. The label defaults to its title. |
|
|
27
27
|
| `m.link(label, href, { icon? })` | Opens `href` in a new tab. Only `https://`, `http://` and `mailto:` are accepted. |
|
|
28
28
|
|
|
29
|
-
`accounts` is the built-in [Users page](auth.md#users-page), which the config exports as `usersPage` from `@protobase/layout`; without the export, `createAdmin` refuses the item. Items keep their order. A line splits the menu into groups: these items, then the [organization](auth.md#organizations) pages when the app has organizations, then the person's own: "Account" for everyone (their [account page](auth.md#account-page): profile, [passkeys](auth.md#passkeys) and [two-factor authentication](auth.md#two-factor-authentication)) and "Sign-in policy" for admins (the [sign-in policy](auth.md#sign-in-policy), and the log of [staff sign-ins](auth.md#staff-sign-in)); then the [theme switch](theme.md#light-dark-or-the-system), and "Sign out" always comes last. `icon` is one of
|
|
29
|
+
`accounts` is the built-in [Users page](auth.md#users-page), which the config exports as `usersPage` from `@protobase/layout`; without the export, `createAdmin` refuses the item. Items keep their order. A line splits the menu into groups: these items, then the [organization](auth.md#organizations) pages when the app has organizations, then the person's own: "Account" for everyone (their [account page](auth.md#account-page): profile, [passkeys](auth.md#passkeys) and [two-factor authentication](auth.md#two-factor-authentication)) and "Sign-in policy" for admins (the [sign-in policy](auth.md#sign-in-policy), and the log of [staff sign-ins](auth.md#staff-sign-in)); then the [theme switch](theme.md#light-dark-or-the-system), and "Sign out" always comes last. `icon` is one of the names a view's [sidebar icon](sidebar.md#sidebar-icons) takes. A resource item without one uses the resource's sidebar icon, and a link item uses `external-link`.
|
|
30
30
|
|
|
31
31
|
Every caller gets the menu through `/meta`, without the resources and pages they cannot see, so a user who cannot read `organizations` has no "Organization" item. Links are shown to everyone. `createAdmin` refuses to start when an item names a resource or page that does not exist, and `configExports` refuses a config that exports two menus.
|
|
32
32
|
|