@xeplr/ui-factory 1.8.3 → 1.8.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/AUTHORING.md +21 -1
- package/README.md +170 -6
- package/bin/xeplr-factory.js +27 -1
- package/package.json +3 -1
- package/src/buttons.js +153 -0
- package/src/controls.js +48 -1
- package/src/designs/BuilderSample.jsx +1 -0
- package/src/designs/ButtonsView.jsx +108 -0
- package/src/designs/ControlView.jsx +4 -1
- package/src/designs/FedList.jsx +248 -0
- package/src/designs/ListView.jsx +6 -1
- package/src/designs/PropertyPanel.jsx +337 -4
- package/src/designs/ScreenSample.jsx +12 -1
- package/src/designs/factory.css +143 -0
- package/src/events.js +21 -0
- package/src/model.js +8 -1
- package/src/pagesInventory.js +91 -0
- package/src/remote.js +17 -2
- package/src/urlState.js +2 -2
- package/src/useFactoryBuilder.js +9 -1
- package/src/useFactoryScreen.js +103 -8
- package/src/useUrlPlaces.js +13 -0
- package/src/validateDocument.js +147 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { boxStyle, fieldStyle } from './styles.js'
|
|
4
|
+
|
|
5
|
+
// A PANEL OF BUTTONS. Each runs a function of the app on the server, with the
|
|
6
|
+
// parameters the screen gives it; which ones show, and which are greyed out,
|
|
7
|
+
// the controller works out from the record (useFactoryScreen.buttonsFor).
|
|
8
|
+
//
|
|
9
|
+
// A button that asks first opens a small dialog: its message, and a note box
|
|
10
|
+
// when it takes a note. Presentation only — the press is the controller's.
|
|
11
|
+
//
|
|
12
|
+
// On the canvas (design) the buttons are drawn as they will look, inert.
|
|
13
|
+
|
|
14
|
+
export default function ButtonsView({ node, design, panel }) {
|
|
15
|
+
const p = node.props || {}
|
|
16
|
+
const style = { ...fieldStyle(p.style), ...boxStyle(p.style) }
|
|
17
|
+
const [asking, setAsking] = useState(null) // the button whose dialog is open
|
|
18
|
+
|
|
19
|
+
if (design || !panel) {
|
|
20
|
+
const all = p.buttons || []
|
|
21
|
+
return (
|
|
22
|
+
<div className="xeplr-factory-buttons xeplr-factory-buttons--design" style={style}>
|
|
23
|
+
{p.title && <span className="xeplr-factory-buttons-title">{p.title}</span>}
|
|
24
|
+
{all.length === 0 && <span className="xeplr-factory-hint">No buttons yet — add them in Properties.</span>}
|
|
25
|
+
{all.map((b) => <span key={b.id} className={`xeplr-factory-button xeplr-factory-button--${b.variant || 'default'}`}>{b.label}</span>)}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const run = (b) => (b.ask ? setAsking(b) : panel.press(b))
|
|
31
|
+
const note = panel.note
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="xeplr-factory-buttons" style={style} role="group" aria-label={p.title || 'Actions'}>
|
|
35
|
+
{p.title && <span className="xeplr-factory-buttons-title">{p.title}</span>}
|
|
36
|
+
{panel.buttons.map((b) => (
|
|
37
|
+
<button
|
|
38
|
+
key={b.id}
|
|
39
|
+
type="button"
|
|
40
|
+
className={`xeplr-factory-button xeplr-factory-button--${b.variant || 'default'}${b.pressed ? ' xeplr-factory-button--pressed' : ''}`}
|
|
41
|
+
aria-pressed={b.pressedWhen ? Boolean(b.pressed) : undefined}
|
|
42
|
+
disabled={b.disabled || Boolean(panel.pressing)}
|
|
43
|
+
aria-busy={panel.pressing === b.id}
|
|
44
|
+
onClick={() => run(b)}
|
|
45
|
+
>
|
|
46
|
+
{panel.pressing === b.id ? `${b.label}…` : b.label}
|
|
47
|
+
</button>
|
|
48
|
+
))}
|
|
49
|
+
{note && !asking && (
|
|
50
|
+
<span className={`xeplr-factory-buttons-note xeplr-factory-buttons-note--${note.kind}`} role={note.kind === 'error' ? 'alert' : 'status'}>
|
|
51
|
+
{note.text}
|
|
52
|
+
<button type="button" className="xeplr-factory-buttons-note-close" aria-label="Dismiss" onClick={panel.clearNote}>×</button>
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
{asking && (
|
|
56
|
+
<AskDialog
|
|
57
|
+
button={asking}
|
|
58
|
+
busy={panel.pressing === asking.id}
|
|
59
|
+
error={note && note.kind === 'error' && note.button === asking.id ? note.text : null}
|
|
60
|
+
onCancel={() => { setAsking(null); panel.clearNote() }}
|
|
61
|
+
onConfirm={async (typed) => { if (await panel.press(asking, typed)) setAsking(null) }}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** "Change status to Packed?" — the button's message, and a note when it takes one. */
|
|
69
|
+
function AskDialog({ button, busy, error, onCancel, onConfirm }) {
|
|
70
|
+
const ask = button.ask || {}
|
|
71
|
+
const [typed, setTyped] = useState('')
|
|
72
|
+
const ref = useRef(null)
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
ref.current?.focus()
|
|
75
|
+
const onKey = (e) => { if (e.key === 'Escape' && !busy) onCancel() }
|
|
76
|
+
window.addEventListener('keydown', onKey)
|
|
77
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
78
|
+
}, [busy, onCancel])
|
|
79
|
+
const titleId = `xf-ask-${button.id}`
|
|
80
|
+
// At the page's root: inside the screen, a control's own box could clip it.
|
|
81
|
+
return createPortal(
|
|
82
|
+
<div className="xeplr-factory-modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget && !busy) onCancel() }}>
|
|
83
|
+
<div ref={ref} className="xeplr-factory-modal xeplr-factory-ask" role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1}>
|
|
84
|
+
<header className="xeplr-factory-modal-head">
|
|
85
|
+
<span className="xeplr-factory-modal-title" id={titleId}>{ask.title || `${button.label}?`}</span>
|
|
86
|
+
<button type="button" className="xeplr-factory-modal-close" onClick={onCancel} disabled={busy} aria-label="Close">×</button>
|
|
87
|
+
</header>
|
|
88
|
+
<div className="xeplr-factory-modal-body">
|
|
89
|
+
{ask.message && <p className="xeplr-factory-ask-message">{ask.message}</p>}
|
|
90
|
+
{ask.note && (
|
|
91
|
+
<label className="xeplr-factory-ask-field" htmlFor={`${titleId}-note`}>
|
|
92
|
+
<span className="xeplr-factory-ask-label">{ask.note}</span>
|
|
93
|
+
<textarea id={`${titleId}-note`} className="xeplr-factory-ask-note" rows={3} maxLength={1000} value={typed} onChange={(e) => setTyped(e.target.value)} />
|
|
94
|
+
</label>
|
|
95
|
+
)}
|
|
96
|
+
{error && <p className="xeplr-factory-ask-error" role="alert">{error}</p>}
|
|
97
|
+
</div>
|
|
98
|
+
<footer className="xeplr-factory-ask-foot">
|
|
99
|
+
<button type="button" className="xeplr-factory-secondary" onClick={onCancel} disabled={busy}>Cancel</button>
|
|
100
|
+
<button type="button" className={button.variant === 'danger' ? 'xeplr-factory-danger' : 'xeplr-factory-primary'} onClick={() => onConfirm(typed)} disabled={busy}>
|
|
101
|
+
{busy ? 'Working…' : button.label}
|
|
102
|
+
</button>
|
|
103
|
+
</footer>
|
|
104
|
+
</div>
|
|
105
|
+
</div>,
|
|
106
|
+
document.body
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
|
|
2
2
|
import { tableColumns, LABEL_PRESETS } from '../controls.js'
|
|
3
3
|
import { stepsOf } from '../document.js'
|
|
4
4
|
import ListView from './ListView.jsx'
|
|
5
|
+
import ButtonsView from './ButtonsView.jsx'
|
|
5
6
|
import { fieldStyle, labelStyle, boxStyle } from './styles.js'
|
|
6
7
|
|
|
7
8
|
// How each control LOOKS — the one renderer, used both on the builder's
|
|
@@ -13,7 +14,7 @@ import { fieldStyle, labelStyle, boxStyle } from './styles.js'
|
|
|
13
14
|
|
|
14
15
|
const ids = (node) => `xeplr-factory-${node.id}`
|
|
15
16
|
|
|
16
|
-
export default function ControlView({ node, mode = 'live', value, error, options, onChange, onBlur, disabled, list, upload, stepper, subform, rows }) {
|
|
17
|
+
export default function ControlView({ node, mode = 'live', value, error, options, onChange, onBlur, disabled, list, upload, stepper, subform, rows, buttons }) {
|
|
17
18
|
const View = VIEWS[node.type]
|
|
18
19
|
if (!View) {
|
|
19
20
|
return <div className="xeplr-factory-unknown">Unknown control “{node.type}”</div>
|
|
@@ -35,6 +36,7 @@ export default function ControlView({ node, mode = 'live', value, error, options
|
|
|
35
36
|
stepper={stepper}
|
|
36
37
|
subform={subform}
|
|
37
38
|
rows={rows}
|
|
39
|
+
buttons={buttons}
|
|
38
40
|
/>
|
|
39
41
|
)
|
|
40
42
|
}
|
|
@@ -538,6 +540,7 @@ export const VIEWS = {
|
|
|
538
540
|
stepper: StepperView,
|
|
539
541
|
label: LabelView,
|
|
540
542
|
list: ListControl,
|
|
543
|
+
buttons: ({ node, design, buttons }) => <ButtonsView node={node} design={design} panel={buttons} />,
|
|
541
544
|
table: TableView,
|
|
542
545
|
subform: SubformView
|
|
543
546
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { XeplrTable } from '@xeplr/ui-table'
|
|
4
|
+
import { boxStyle, fieldStyle } from './styles.js'
|
|
5
|
+
|
|
6
|
+
// A LIST WHOSE ROWS A FUNCTION WORKS OUT (rowsFrom) — live, in @xeplr/ui-table.
|
|
7
|
+
//
|
|
8
|
+
// Everything it shows is TEXT THE SERVER WROTE: a column sorts by its `field`
|
|
9
|
+
// and shows its `text` field, so "Today, 4:00 pm" or "2d 3h" is said once, by
|
|
10
|
+
// the app that knows its time zone and its words, and reads the same in any
|
|
11
|
+
// renderer. What this file draws is only how: a second line, badges, the
|
|
12
|
+
// pictures, an activity line with its hover box.
|
|
13
|
+
//
|
|
14
|
+
// Above the rows: the search box and the filter chips, each chip with the
|
|
15
|
+
// count the function gave it. Presentation only — the query, the rows and
|
|
16
|
+
// what a click does arrive as props.
|
|
17
|
+
|
|
18
|
+
const OPEN_DELAY_MS = 250
|
|
19
|
+
const CLOSE_DELAY_MS = 200
|
|
20
|
+
const BOX_WIDTH = 380
|
|
21
|
+
const BOX_MAX_HEIGHT = 360
|
|
22
|
+
const MIN_BELOW = 200
|
|
23
|
+
|
|
24
|
+
export default function FedList({ node, list, query, onOpen, rowDetails, recordKey = 'id', extraActions = [] }) {
|
|
25
|
+
const p = node.props || {}
|
|
26
|
+
const columns = Array.isArray(p.columns) ? p.columns : []
|
|
27
|
+
const rows = list.rows || []
|
|
28
|
+
const counts = list.counts || {}
|
|
29
|
+
const chips = Array.isArray(p.filters) ? p.filters : []
|
|
30
|
+
const active = query.filter || (chips[0] && chips[0].key) || ''
|
|
31
|
+
const pageSize = p.pageSize || 10
|
|
32
|
+
const style = { ...fieldStyle(p.style), ...boxStyle(p.style) }
|
|
33
|
+
|
|
34
|
+
// The search box shows what is typed at once; the controller asks after a pause.
|
|
35
|
+
const [typed, setTyped] = useState(query.q)
|
|
36
|
+
useEffect(() => { setTyped(query.q) }, [query.q])
|
|
37
|
+
|
|
38
|
+
const open = p.rowOpen || (p.actions || []).includes('edit') ? onOpen : null
|
|
39
|
+
|
|
40
|
+
const rowActions = []
|
|
41
|
+
if ((p.actions || []).includes('edit')) rowActions.push({ key: 'edit', label: 'Edit', onClick: (row) => onOpen(row) })
|
|
42
|
+
extraActions.forEach((a, i) => rowActions.push({ key: 'extra-' + i + '-' + a.label, label: a.label, variant: a.variant, onClick: (row) => a.onClick(row) }))
|
|
43
|
+
|
|
44
|
+
const schema = {
|
|
45
|
+
0: {
|
|
46
|
+
key: node.id,
|
|
47
|
+
columns: columns.map((c, i) => ({
|
|
48
|
+
accessor: c.field,
|
|
49
|
+
header: c.label,
|
|
50
|
+
enableSorting: c.sortable !== false,
|
|
51
|
+
cell: ({ row }) => (
|
|
52
|
+
<Cell column={c} row={row.original} index={row.index} first={i === 0} open={open}
|
|
53
|
+
details={c.display === 'activity' && c.details ? () => rowDetails(node, c, row.original[recordKey]) : null}
|
|
54
|
+
onOpenAt={open ? (where) => open(row.original, where) : null} />
|
|
55
|
+
)
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A click anywhere on a row opens it; the first cell is also a real button,
|
|
61
|
+
// for the keyboard. Clicks on the row's own buttons and links are theirs.
|
|
62
|
+
function openFromRow(e) {
|
|
63
|
+
if (!open || e.target.closest('button, a, input, select, .xeplr-factory-activity-box')) return
|
|
64
|
+
const tr = e.target.closest('tbody tr')
|
|
65
|
+
const marker = tr && tr.querySelector('[data-xf-row]')
|
|
66
|
+
if (marker) open(rows[Number(marker.getAttribute('data-xf-row'))])
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="xeplr-factory-list xeplr-factory-list--fed" style={style}>
|
|
71
|
+
<div className="xeplr-factory-list-tools">
|
|
72
|
+
{p.title && <span className="xeplr-factory-list-title">{p.title}</span>}
|
|
73
|
+
{p.search && p.search.placeholder && (
|
|
74
|
+
<input
|
|
75
|
+
type="search"
|
|
76
|
+
className="xeplr-factory-list-search"
|
|
77
|
+
aria-label={p.search.placeholder}
|
|
78
|
+
placeholder={p.search.placeholder}
|
|
79
|
+
value={typed}
|
|
80
|
+
maxLength={200}
|
|
81
|
+
onChange={(e) => { setTyped(e.target.value); query.setQ(e.target.value) }}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
{chips.length > 0 && (
|
|
85
|
+
<div className="xeplr-factory-chips" role="tablist" aria-label={p.title ? `Filter ${p.title}` : 'Filter'}>
|
|
86
|
+
{chips.map((f) => (
|
|
87
|
+
<button
|
|
88
|
+
key={f.key}
|
|
89
|
+
type="button"
|
|
90
|
+
role="tab"
|
|
91
|
+
aria-selected={f.key === active}
|
|
92
|
+
className={'xeplr-factory-chip' + (f.key === active ? ' is-on' : '')}
|
|
93
|
+
onClick={() => query.setFilter(f.key === (chips[0] && chips[0].key) ? '' : f.key)}
|
|
94
|
+
>
|
|
95
|
+
{f.label}
|
|
96
|
+
{counts[f.key] !== undefined && <span className="xeplr-factory-chip-count">{counts[f.key]}</span>}
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
{list.loading && <span className="xeplr-factory-hint">Loading…</span>}
|
|
102
|
+
{list.error && <span className="xeplr-factory-error-inline" role="alert">{list.error}</span>}
|
|
103
|
+
</div>
|
|
104
|
+
{rows.length === 0
|
|
105
|
+
? !list.loading && !list.error && <div className="xeplr-factory-list-empty">{query.q ? 'Nothing matches that search.' : 'Nothing here yet.'}</div>
|
|
106
|
+
: (
|
|
107
|
+
<div className={'xeplr-factory-fed-table' + (open ? ' is-openable' : '')} onClick={openFromRow}>
|
|
108
|
+
<XeplrTable
|
|
109
|
+
data={rows}
|
|
110
|
+
schema={schema}
|
|
111
|
+
showToolbar={false}
|
|
112
|
+
enableFiltering={false}
|
|
113
|
+
pageSize={pageSize}
|
|
114
|
+
enablePagination={rows.length > pageSize}
|
|
115
|
+
rowActions={rowActions.length ? rowActions : undefined}
|
|
116
|
+
rowClassName={(row) => (p.highlight && row && row[p.highlight] ? 'xeplr-factory-row-marked' : null)}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One cell: the server's text, and how this column draws it. */
|
|
125
|
+
function Cell({ column: c, row, index, first, open, details, onOpenAt }) {
|
|
126
|
+
const value = row[c.field]
|
|
127
|
+
const shown = c.text ? row[c.text] : value
|
|
128
|
+
const badges = (c.badges || []).filter((b) => row[b.field])
|
|
129
|
+
const marker = first ? { 'data-xf-row': index } : {}
|
|
130
|
+
|
|
131
|
+
if (c.display === 'images') {
|
|
132
|
+
const pics = Array.isArray(value) ? value.filter((x) => x && x.src) : []
|
|
133
|
+
const more = c.more ? Number(row[c.more]) || 0 : 0
|
|
134
|
+
return (
|
|
135
|
+
<span className="xeplr-factory-thumbs" {...marker}>
|
|
136
|
+
{pics.map((img, n) => <img key={n} src={img.src} alt={img.name || ''} title={img.name || ''} loading="lazy" />)}
|
|
137
|
+
{more > 0 && <small>+{more}</small>}
|
|
138
|
+
</span>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (c.display === 'activity') {
|
|
143
|
+
return <Activity text={text(shown)} details={details} onOpenAt={onOpenAt} marker={marker} />
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const body = (
|
|
147
|
+
<>
|
|
148
|
+
<span className="xeplr-factory-cell-main">{text(shown)}</span>
|
|
149
|
+
{badges.map((b) => <span key={b.field} className={`xeplr-factory-badge xeplr-factory-badge--${b.tone || 'accent'}`}>{b.label}</span>)}
|
|
150
|
+
{c.sub && row[c.sub] !== undefined && row[c.sub] !== null && row[c.sub] !== '' && <span className="xeplr-factory-cell-sub">{text(row[c.sub])}</span>}
|
|
151
|
+
</>
|
|
152
|
+
)
|
|
153
|
+
// The first cell of a row that opens is a button: the keyboard's way in.
|
|
154
|
+
if (first && open) {
|
|
155
|
+
return <button type="button" className="xeplr-factory-cell xeplr-factory-cell-open" onClick={(e) => { e.stopPropagation(); open(row) }} {...marker}>{body}</button>
|
|
156
|
+
}
|
|
157
|
+
return <span className="xeplr-factory-cell" {...marker}>{body}</span>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function text(v) {
|
|
161
|
+
if (v === undefined || v === null) return ''
|
|
162
|
+
return typeof v === 'object' ? JSON.stringify(v) : String(v)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* WHAT LAST HAPPENED, and a box with the latest few — on hover or focus,
|
|
167
|
+
* loaded the first time and kept until the line changes. Drawn at the page's
|
|
168
|
+
* root so the table's scrolling never clips it; under its cell, or above it
|
|
169
|
+
* near the bottom of the window. A tap on a phone opens the row instead.
|
|
170
|
+
*/
|
|
171
|
+
function Activity({ text: line, details, onOpenAt, marker }) {
|
|
172
|
+
const cellRef = useRef(null)
|
|
173
|
+
const timer = useRef(null)
|
|
174
|
+
const [box, setBox] = useState(null) // { top, left, maxHeight, above }
|
|
175
|
+
const [items, setItems] = useState(null) // { line, list, error }
|
|
176
|
+
|
|
177
|
+
useEffect(() => () => clearTimeout(timer.current), [])
|
|
178
|
+
useEffect(() => { setItems(null) }, [line])
|
|
179
|
+
|
|
180
|
+
if (!details) return <span className="xeplr-factory-cell xeplr-factory-activity" {...marker}>{line}</span>
|
|
181
|
+
|
|
182
|
+
function show() {
|
|
183
|
+
clearTimeout(timer.current)
|
|
184
|
+
timer.current = setTimeout(() => {
|
|
185
|
+
const r = cellRef.current && cellRef.current.getBoundingClientRect()
|
|
186
|
+
if (!r) return
|
|
187
|
+
const below = window.innerHeight - r.bottom - 16
|
|
188
|
+
const above = below < MIN_BELOW
|
|
189
|
+
const left = Math.max(8, Math.min(r.left, window.innerWidth - BOX_WIDTH - 8))
|
|
190
|
+
setBox(above
|
|
191
|
+
? { left, bottom: window.innerHeight - r.top + 8, maxHeight: Math.min(BOX_MAX_HEIGHT, r.top - 16), above }
|
|
192
|
+
: { left, top: r.bottom + 8, maxHeight: Math.min(BOX_MAX_HEIGHT, below), above })
|
|
193
|
+
if (!items || items.line !== line) {
|
|
194
|
+
details().then(
|
|
195
|
+
(list) => setItems({ line, list: Array.isArray(list) ? list : [], error: null }),
|
|
196
|
+
(err) => setItems({ line, list: [], error: err.message || 'Could not load' })
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}, OPEN_DELAY_MS)
|
|
200
|
+
}
|
|
201
|
+
function hide() {
|
|
202
|
+
clearTimeout(timer.current)
|
|
203
|
+
timer.current = setTimeout(() => setBox(null), CLOSE_DELAY_MS)
|
|
204
|
+
}
|
|
205
|
+
// Touch has no hover: a tap is the row's, which opens it.
|
|
206
|
+
const hoverable = typeof window === 'undefined' || !window.matchMedia || !window.matchMedia('(hover: none)').matches
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<span
|
|
210
|
+
ref={cellRef}
|
|
211
|
+
className="xeplr-factory-cell xeplr-factory-activity"
|
|
212
|
+
tabIndex={hoverable ? 0 : undefined}
|
|
213
|
+
onMouseEnter={hoverable ? show : undefined}
|
|
214
|
+
onMouseLeave={hoverable ? hide : undefined}
|
|
215
|
+
onFocus={hoverable ? show : undefined}
|
|
216
|
+
onBlur={hoverable ? hide : undefined}
|
|
217
|
+
{...marker}
|
|
218
|
+
>
|
|
219
|
+
{line}
|
|
220
|
+
{box && typeof document !== 'undefined' && createPortal(
|
|
221
|
+
<div
|
|
222
|
+
className="xeplr-factory-activity-box"
|
|
223
|
+
role="dialog"
|
|
224
|
+
aria-label="Latest activity"
|
|
225
|
+
style={{ left: box.left, top: box.top, bottom: box.bottom, maxHeight: box.maxHeight, width: BOX_WIDTH }}
|
|
226
|
+
onMouseEnter={() => clearTimeout(timer.current)}
|
|
227
|
+
onMouseLeave={hide}
|
|
228
|
+
>
|
|
229
|
+
{!items && <div className="xeplr-factory-hint">Loading…</div>}
|
|
230
|
+
{items && items.error && <div className="xeplr-factory-error-inline" role="alert">{items.error}</div>}
|
|
231
|
+
{items && !items.error && !items.list.length && <div className="xeplr-factory-hint">Nothing yet.</div>}
|
|
232
|
+
{items && items.list.map((it, i) => {
|
|
233
|
+
const body = (
|
|
234
|
+
<>
|
|
235
|
+
<span className="xeplr-factory-activity-text">{text(it.text)}</span>
|
|
236
|
+
{(it.sub || it.at) && <span className="xeplr-factory-activity-sub">{[it.sub, it.at].filter(Boolean).map(text).join(' · ')}</span>}
|
|
237
|
+
</>
|
|
238
|
+
)
|
|
239
|
+
return it.open && onOpenAt
|
|
240
|
+
? <button key={i} type="button" className="xeplr-factory-activity-item" onClick={(e) => { e.stopPropagation(); setBox(null); onOpenAt(it.open) }}>{body}</button>
|
|
241
|
+
: <div key={i} className="xeplr-factory-activity-item">{body}</div>
|
|
242
|
+
})}
|
|
243
|
+
</div>,
|
|
244
|
+
document.body
|
|
245
|
+
)}
|
|
246
|
+
</span>
|
|
247
|
+
)
|
|
248
|
+
}
|
package/src/designs/ListView.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { XeplrTable } from '@xeplr/ui-table'
|
|
2
2
|
import { displayValue, listColumns, listSource } from '../values.js'
|
|
3
3
|
import { boxStyle, fieldStyle } from './styles.js'
|
|
4
|
+
import FedList from './FedList.jsx'
|
|
4
5
|
|
|
5
6
|
// A list of the saved records, in @xeplr/ui-table.
|
|
6
7
|
//
|
|
@@ -9,8 +10,12 @@ import { boxStyle, fieldStyle } from './styles.js'
|
|
|
9
10
|
// Edit and New open the list's edit screen in a popup (`editScreen`), or, for a
|
|
10
11
|
// list on a form, load the row into the form's own fields. Delete removes it.
|
|
11
12
|
|
|
12
|
-
export default function ListView({ node, doc, design, list, onEdit, onDelete, onNew, currentId, recordKey = 'id', canDelete, fieldNodes, optionsFor, extraActions = [] }) {
|
|
13
|
+
export default function ListView({ node, doc, design, list, onEdit, onDelete, onNew, currentId, recordKey = 'id', canDelete, fieldNodes, optionsFor, extraActions = [], query, rowDetails, onOpen }) {
|
|
13
14
|
const p = node.props || {}
|
|
15
|
+
// Rows a function works out: their own view — search, chips, the server's text.
|
|
16
|
+
if (!design && p.rowsFrom && list) {
|
|
17
|
+
return <FedList node={node} list={list} query={query || { q: '', filter: '', setQ: () => {}, setFilter: () => {} }} onOpen={onOpen || onEdit} rowDetails={rowDetails} recordKey={recordKey} extraActions={extraActions} />
|
|
18
|
+
}
|
|
14
19
|
// The rows are passed in because a list of ANOTHER table has no other
|
|
15
20
|
// description of itself — see listColumns.
|
|
16
21
|
const columns = listColumns(doc, node, undefined, (list && list.rows) || [])
|