Compare commits

..

No commits in common. "main" and "fix/asset-edit-prefill" have entirely different histories.

4 changed files with 49 additions and 77 deletions

View File

@ -144,8 +144,6 @@ export const ASSET_FIELDS = [
}, },
{ id: 'field_59', label: 'Asset Brand', type: 'text' }, { id: 'field_59', label: 'Asset Brand', type: 'text' },
{ id: 'field_61', label: 'Asset Location', type: 'text' }, { id: 'field_61', label: 'Asset Location', type: 'text' },
// `label` keeps the builder's spelling; `formLabel` is what the app shows.
{ id: 'field_73', label: 'Monthly allowence', formLabel: 'Monthly allowance', type: 'number' },
] ]
/** INIT's photo field, whose uploads the runtime always accepts. */ /** INIT's photo field, whose uploads the runtime always accepts. */
@ -192,7 +190,8 @@ export const fetchAssetActivityFieldDefs = (activityId) =>
/** /**
* "Asset Owner": a lookup into the employee table, like ASSIGNED's "Employee * "Asset Owner": a lookup into the employee table, like ASSIGNED's "Employee
* data". Sent as { id, first_name } — see employeeLookup. * data". The API sample shows only { id }; it is sent as { id, first_name }
* because the runtime checks lookups against their storage columns.
*/ */
export const ASSET_OWNER_FIELD = 'field_51' export const ASSET_OWNER_FIELD = 'field_51'
@ -265,9 +264,11 @@ export const ASSET_ASSIGN_FIELDS = [
] ]
/** /**
* "Employee data": a lookup into the employee table, sent as { id, first_name } * "Employee data": a lookup into the employee table. The runtime checks it
* like every employee lookup (see employeeLookup). It is picked from a list * against the lookup's storage columns (a Retired attempt failed with
* of active employees, so it is sent alongside the form data. * "missing required storage column 'first_name'"), so it carries
* { id, first_name }, not the { id, name } the API sample shows. It is picked
* from a list of active employees, so it is sent alongside the form data.
*/ */
export const ASSIGN_EMPLOYEE_FIELD = 'field_22' export const ASSIGN_EMPLOYEE_FIELD = 'field_22'
@ -438,17 +439,8 @@ export const ASSET_LOST_FIELDS = [
/** "Last owner": a lookup into the employee table, like "Employee data". */ /** "Last owner": a lookup into the employee table, like "Employee data". */
export const LOST_EMPLOYEE_FIELD = 'field_35' export const LOST_EMPLOYEE_FIELD = 'field_35'
/** /** An employee lookup value, in the shape the runtime validates. */
* An employee lookup value, in the shape the runtime validates: { id, const employeeLookup = (employee) => ({ id: employee.id, first_name: employee.first_name })
* first_name }, the lookup's storage columns. The employee table has no "name"
* column, so { id, name } fails with "… has an incomplete selection"
* (invalid_type), and omitting first_name fails with "missing required
* storage column 'first_name'".
*/
const employeeLookup = (employee) => ({
id: employee.id,
first_name: employee.first_name || employee.name,
})
/** /**
* Run a later activity (ASSIGNED, RETURNED, …) on an existing asset instance. * Run a later activity (ASSIGNED, RETURNED, …) on an existing asset instance.
@ -592,23 +584,21 @@ const EDITED_FIELDS = [
* with the asset's instance_id. * with the asset's instance_id.
* *
* POST {APP_API_BASE_URL}/app/{APP_ID}/activity * POST {APP_API_BASE_URL}/app/{APP_ID}/activity
* { activity_id, workflow_uuid, instance_id, data: { field_80 … field_83 } } * { activity_id, workflow_uuid, instance_id, data: { field_73 … field_76 } }
* { activity_id, workflow_uuid, instance_id, data: { field_77 … field_79 } } * { activity_id, workflow_uuid, instance_id, data: { field_77 … field_79 } }
* *
* PLACEHOLDER ids: add an "Under Repair" state and these two activities, with * PLACEHOLDER ids: add an "Under Repair" state and these two activities, with
* these fields, in the Zino builder, deploy, and put their ids here. Until * these fields, in the Zino builder, deploy, and put their ids here. Until
* then, running them fails. (The send fields were field_73 … field_76, but the * then, running them fails.
* builder has since given field_73 to INIT's "Monthly allowence", so they
* moved to field_80 …; match whatever ids the builder assigns when adding.)
*/ */
export const ASSET_REPAIR_ACTIVITY_ID = 'TODO-send-for-repair-activity-id' export const ASSET_REPAIR_ACTIVITY_ID = 'TODO-send-for-repair-activity-id'
/** Send for Repair fields. */ /** Send for Repair fields. */
export const ASSET_REPAIR_FIELDS = [ export const ASSET_REPAIR_FIELDS = [
{ id: 'field_80', label: 'Repair date', type: 'date' }, { id: 'field_73', label: 'Repair date', type: 'date' },
{ id: 'field_81', label: 'Issue details', type: 'longtext' }, { id: 'field_74', label: 'Issue details', type: 'longtext' },
{ id: 'field_82', label: 'Repair vendor', type: 'text' }, { id: 'field_75', label: 'Repair vendor', type: 'text' },
{ id: 'field_83', label: 'Estimated cost', type: 'number' }, { id: 'field_76', label: 'Estimated cost', type: 'number' },
] ]
export const ASSET_REPAIRED_ACTIVITY_ID = 'TODO-repair-completed-activity-id' export const ASSET_REPAIRED_ACTIVITY_ID = 'TODO-repair-completed-activity-id'
@ -1219,7 +1209,7 @@ export const ASSET_RICH_COLUMNS = [
title: ['Warranty Status'], title: ['Warranty Status'],
subtitle: [ subtitle: [
{ label: 'Warranty Start date', prefix: 'From' }, { label: 'Warranty Start date', prefix: 'From' },
'Warranty End date', { label: 'Warranty End date', prefix: 'to' },
], ],
}, },
] ]
@ -1680,13 +1670,13 @@ export function assetEvents(config, row) {
) )
} }
if (present(val('field_80'))) { if (present(val('field_73'))) {
add( add(
HISTORY_ACTION.REPAIR, HISTORY_ACTION.REPAIR,
val('field_80'), val('field_73'),
'', '',
joinParts(val('field_82'), present(val('field_83')) && `Estimate ${val('field_83')}`), joinParts(val('field_75'), present(val('field_76')) && `Estimate ${val('field_76')}`),
val('field_81'), val('field_74'),
) )
} }

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { import {
AssetStateBadge, AssetStateBadge,
Avatar, Avatar,
Button,
EmployeeStatusBadge, EmployeeStatusBadge,
EmptyState, EmptyState,
HistoryActionBadge, HistoryActionBadge,
@ -16,16 +17,11 @@ import { FileLinks, imageSources } from './FieldValue'
/** /**
* Renders a record view straight from the backend response: the columns are * Renders a record view straight from the backend response: the columns are
* whatever `config.fields` returns, in that order, with the backend's own * whatever `config.fields` returns, in that order, with the backend's own
* labels. Sorting and search are handled by the server; instead of page * labels. Paging, sorting and search are handled by the server.
* buttons, a "Rows" selector sets how many records the server returns
* (search_query.limit), so picking 5 shows 5 rows and picking 100 shows 100.
* `toolbar` adds controls (e.g. a filter) next to the search box. A field with * `toolbar` adds controls (e.g. a filter) next to the search box. A field with
* `sortable: false` can't be sorted. A rich column (see api/richColumns.js) * `sortable: false` can't be sorted. A rich column (see api/richColumns.js)
* shows several fields in one cell and sorts by its `sort_key`. * shows several fields in one cell and sorts by its `sort_key`.
*/ */
/** How many rows the "Rows" selector can ask the server for. */
const ROW_OPTIONS = [5, 10, 25, 50, 100]
export default function RecordView({ export default function RecordView({
fields, fields,
rows, rows,
@ -33,7 +29,7 @@ export default function RecordView({
loading, loading,
error, error,
query, query,
setLimit, setPage,
setSearch, setSearch,
setSort, setSort,
rowKey = 'instance_id', rowKey = 'instance_id',
@ -50,8 +46,6 @@ export default function RecordView({
return () => clearTimeout(id) return () => clearTimeout(id)
}, [term, query.search, setSearch]) }, [term, query.search, setSearch])
// Names the placeholder only: the backend searches its own columns whatever
// is_search says (it returns false on every field), so the box always shows.
// A rich column's fields stay searchable. // A rich column's fields stay searchable.
const searchable = fields const searchable = fields
.flatMap((f) => (f.data_type === 'rich' ? [...f.title, ...f.subtitle] : [f])) .flatMap((f) => (f.data_type === 'rich' ? [...f.title, ...f.subtitle] : [f]))
@ -65,20 +59,20 @@ export default function RecordView({
setSort(sortKey(field), dir) setSort(sortKey(field), dir)
} }
const page = pagination?.page ?? 1
const totalPages = pagination?.total_pages ?? 1
const total = pagination?.total_count ?? rows.length const total = pagination?.total_count ?? rows.length
return ( return (
<> <>
<Toolbar count={total} countLabel="records"> <Toolbar count={total} countLabel="records">
<SearchInput {searchable.length > 0 && (
value={term} <SearchInput
onChange={setTerm} value={term}
placeholder={ onChange={setTerm}
searchable.length > 0 placeholder={`Search by ${searchable.map((f) => displayLabel(f.output_label)).join(', ')}…`}
? `Search by ${searchable.map((f) => displayLabel(f.output_label)).join(', ')}…` />
: 'Search…' )}
}
/>
{toolbar} {toolbar}
{loading && <span className="toolbar__count">Loading…</span>} {loading && <span className="toolbar__count">Loading…</span>}
</Toolbar> </Toolbar>
@ -145,26 +139,26 @@ export default function RecordView({
</table> </table>
</div> </div>
{total > 0 && ( {totalPages > 1 && (
<div className="pager"> <div className="pager">
<label className="pager__label"> <Button
Rows{' '} size="sm"
<select icon="chevronLeft"
className="pager__select" disabled={page <= 1 || loading}
value={query.limit} onClick={() => setPage(page - 1)}
disabled={loading} >
onChange={(e) => setLimit(Number(e.target.value))} Previous
> </Button>
{ROW_OPTIONS.map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</label>
<span className="pager__label"> <span className="pager__label">
Showing {Math.min(rows.length, total)} of {total} records Page {page} of {totalPages}
</span> </span>
<Button
size="sm"
disabled={page >= totalPages || loading}
onClick={() => setPage(page + 1)}
>
Next
</Button>
</div> </div>
)} )}
</> </>

View File

@ -71,7 +71,6 @@ export default function useRecordView(
error, error,
query, query,
setPage: (page) => setQuery((q) => ({ ...q, page })), setPage: (page) => setQuery((q) => ({ ...q, page })),
setLimit: (limit) => setQuery((q) => ({ ...q, limit, page: 1 })),
setSearch: (search) => setQuery((q) => ({ ...q, search, page: 1 })), setSearch: (search) => setQuery((q) => ({ ...q, search, page: 1 })),
setSort: (sortBy, sortDir) => setQuery((q) => ({ ...q, sortBy, sortDir, page: 1 })), setSort: (sortBy, sortDir) => setQuery((q) => ({ ...q, sortBy, sortDir, page: 1 })),
setFilter: (filter) => setQuery((q) => ({ ...q, filter, page: 1 })), setFilter: (filter) => setQuery((q) => ({ ...q, filter, page: 1 })),

View File

@ -1197,17 +1197,6 @@ button {
color: var(--text-muted); color: var(--text-muted);
} }
.pager__select {
margin-left: 4px;
padding: 4px 8px;
font-size: 12.5px;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
}
.cell-clamp { .cell-clamp {
max-width: 220px; max-width: 220px;
} }