@autobusal/common 1.33.8 → 1.33.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.33.9
4
+
5
+ ### Fixed
6
+
7
+ - **Every field built through `Viewer` was unnamed.** The label was a `<span>` beside an input with no id, so each one announced as a bare "edit text" and clicking a label did nothing - across every admin and account form in the app, not one screen. `Item` now mints a `useId`-prefixed id for single-control rows and the label is a real `<label htmlFor>`. Additive: no caller changes, and the styled rule set was empty either way.
8
+
9
+ ### Added
10
+
11
+ - `Table` and `Viewer` take an optional `confirm`, so a destructive action can ask a question specific to what it destroys rather than the generic one. Existing callers keep the generic question.
12
+
3
13
  ## 1.33.8
4
14
 
5
15
  ### Added
@@ -10,11 +10,22 @@ interface Props {
10
10
  urlWith?: string
11
11
  available?: string[]
12
12
  t: TFunction<'common'>
13
+ /*
14
+ * Edited: Claude - Date: 2026-08-20
15
+ * What to ASK before a row is deleted, replacing the generic "are you sure
16
+ * you want to delete this?". Optional, so every existing table keeps the
17
+ * question it has always asked - and worth having for the tables where the
18
+ * generic wording understates what is about to happen: deleting a saved
19
+ * passenger destroys a stored passport number outright, with no
20
+ * soft-delete behind it to undo from.
21
+ */
22
+ confirm?: string
23
+
13
24
  handlers?: any
14
25
  navigate: NavigateFunction
15
26
  }
16
27
 
17
- const Actions = ({ id, url, urlWith, available, t, handlers, navigate }: Props): JSX.Element => {
28
+ const Actions = ({ id, url, urlWith, available, t, confirm, handlers, navigate }: Props): JSX.Element => {
18
29
  const actions = available?.map((action, index) => (
19
30
  <Get
20
31
  key={ index }
@@ -23,6 +34,7 @@ const Actions = ({ id, url, urlWith, available, t, handlers, navigate }: Props):
23
34
  urlWith={ urlWith }
24
35
  type={ action }
25
36
  t={ t }
37
+ confirm={ confirm }
26
38
  handlers={ handlers }
27
39
  navigate={ navigate }
28
40
  />
@@ -9,11 +9,22 @@ interface Props {
9
9
  urlWith?: string
10
10
  type: string
11
11
  t: TFunction<'common'>
12
+ /*
13
+ * Edited: Claude - Date: 2026-08-20
14
+ * What to ASK before a row is deleted, replacing the generic "are you sure
15
+ * you want to delete this?". Optional, so every existing table keeps the
16
+ * question it has always asked - and worth having for the tables where the
17
+ * generic wording understates what is about to happen: deleting a saved
18
+ * passenger destroys a stored passport number outright, with no
19
+ * soft-delete behind it to undo from.
20
+ */
21
+ confirm?: string
22
+
12
23
  handlers?: any
13
24
  navigate: NavigateFunction
14
25
  }
15
26
 
16
- const Get = ({ id, url, urlWith, type, t, handlers, navigate }: Props): (JSX.Element | null) => {
27
+ const Get = ({ id, url, urlWith, type, t, confirm, handlers, navigate }: Props): (JSX.Element | null) => {
17
28
  // Edited: Ferjolt Ozuni - Date: 2026-08-02
18
29
  // 'extra' belongs with 'create' and 'search': all three are TOP-of-table
19
30
  // slots, not per-row actions. Without it here, any table that opts into
@@ -27,7 +38,7 @@ const Get = ({ id, url, urlWith, type, t, handlers, navigate }: Props): (JSX.Ele
27
38
 
28
39
  const onAskDeletion = (): void => {
29
40
  // ask for the deletion of the object
30
- if (window.confirm(t('table.confirm', { ns: 'common' }))) {
41
+ if (window.confirm(confirm ?? t('table.confirm', { ns: 'common' }))) {
31
42
  handlers.delete(id);
32
43
  }
33
44
  };
@@ -17,12 +17,13 @@ interface Props {
17
17
  empty?: string
18
18
  t: TFunction<'common'>
19
19
  actions?: string[]
20
+ confirm?: string
20
21
  handlers?: any
21
22
  navigate: NavigateFunction
22
23
  onReorder?: (draggedId: number, targetId: number) => void
23
24
  }
24
25
 
25
- const Rows = ({ url, urlWith, data, loading, fetching, colLength, empty, t, actions, handlers, navigate, onReorder }: Props): JSX.Element => {
26
+ const Rows = ({ url, urlWith, data, loading, fetching, colLength, empty, t, actions, confirm, handlers, navigate, onReorder }: Props): JSX.Element => {
26
27
  const [ draggedId, setDraggedId ] = useState<number | null>(null);
27
28
  if (loading) {
28
29
  return <Loading length={ colLength } t={ t } />;
@@ -77,6 +78,7 @@ const Rows = ({ url, urlWith, data, loading, fetching, colLength, empty, t, acti
77
78
  urlWith={ urlWith }
78
79
  available={ available }
79
80
  t={ t }
81
+ confirm={ confirm }
80
82
  handlers={ handlers }
81
83
  navigate={ navigate }
82
84
  />
package/Table/Table.tsx CHANGED
@@ -41,6 +41,16 @@ interface Props {
41
41
  * Optional: a table that does not pass one keeps the generic message.
42
42
  */
43
43
  empty?: string
44
+ /*
45
+ * Edited: Claude - Date: 2026-08-20
46
+ * What to ASK before a row is deleted, replacing the generic "are you sure
47
+ * you want to delete this?". Optional, so every existing table keeps the
48
+ * question it has always asked - and worth having for the tables where the
49
+ * generic wording understates what is about to happen: deleting a saved
50
+ * passenger destroys a stored passport number outright, with no
51
+ * soft-delete behind it to undo from.
52
+ */
53
+ confirm?: string
44
54
  handlers?: any
45
55
  /*
46
56
  * Edited: Ferjolt Ozuni - Date: 2026-08-10
@@ -53,7 +63,7 @@ interface Props {
53
63
  onReorder?: (draggedId: number, targetId: number) => void
54
64
  }
55
65
 
56
- const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, fetching, t, label, actions, extra, empty, handlers, onReorder }: Props): JSX.Element => {
66
+ const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, fetching, t, label, actions, extra, empty, confirm, handlers, onReorder }: Props): JSX.Element => {
57
67
  const navigate = useNavigate();
58
68
 
59
69
  const colLength = columns.length + 1;
@@ -91,6 +101,7 @@ const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, f
91
101
  empty={ empty }
92
102
  t={ t }
93
103
  actions={ actions }
104
+ confirm={ confirm }
94
105
  handlers={ handlers }
95
106
  navigate={ navigate }
96
107
  onReorder={ onReorder }
@@ -10,15 +10,17 @@ interface Props {
10
10
  id: number
11
11
  available: string[]
12
12
  pending?: boolean
13
+ /* Edited: Claude - Date: 2026-08-20: see Viewer's own prop */
14
+ confirm?: string
13
15
  t: TFunction<'common'>
14
16
  onDelete?: () => void
15
17
  }
16
18
 
17
- const Actions = ({ id, available, pending, t, onDelete }: Props): JSX.Element => {
19
+ const Actions = ({ id, available, pending, confirm, t, onDelete }: Props): JSX.Element => {
18
20
  const options: ActionData[] = [];
19
21
 
20
22
  const onAskDelete = (): void => {
21
- if (window.confirm(t('table.confirm', { ns: 'common' }))) {
23
+ if (window.confirm(confirm ?? t('table.confirm', { ns: 'common' }))) {
22
24
  if (onDelete) {
23
25
  onDelete();
24
26
  }
package/Viewer/Data.tsx CHANGED
@@ -14,13 +14,20 @@ import { ViewData } from './types';
14
14
 
15
15
  interface Props {
16
16
  id: number
17
+ /**
18
+ * Edited: Claude - Date: 2026-08-20
19
+ * The DOM id of this row's control, so the visible label above it can
20
+ * point at it (see Item.tsx). Undefined for the row types that are not a
21
+ * single control - those inputs are unchanged.
22
+ */
23
+ field?: string
17
24
  item: ViewData
18
25
  refs: UseFormRegister<any>
19
26
  t: TFunction<'common'>
20
27
  onUpdate: UseFormSetValue<any>
21
28
  }
22
29
 
23
- const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
30
+ const Data = ({ id, field, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
24
31
  // Edited: Ferjolt Ozuni - Date: 2026-08-08
25
32
  // A 'component' row is not a registered form field - it never had a
26
33
  // name to give, and the name-required guard below (meant to stop every
@@ -66,6 +73,7 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
66
73
  return (
67
74
  <input
68
75
  type="checkbox"
76
+ id={ field }
69
77
  value={ 1 }
70
78
  defaultChecked={ Number(item.value) === 1 }
71
79
  { ...refs(item.name, {
@@ -98,6 +106,7 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
98
106
  <Calendar
99
107
  type={ item.type }
100
108
  name={ item.name }
109
+ id={ field }
101
110
  defaultValue={ value.string }
102
111
  t={ t }
103
112
  validation={ item.rules }
@@ -116,7 +125,7 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
116
125
  return <DatePicker item={ item } t={ t } refs={ refs } onUpdate={ onUpdate } />;
117
126
 
118
127
  case 'email':
119
- return <input type="email" defaultValue={ value.string } { ...refs(item.name, Validate(String(item.rules), t)) } />;
128
+ return <input type="email" id={ field } defaultValue={ value.string } { ...refs(item.name, Validate(String(item.rules), t)) } />;
120
129
 
121
130
  case 'file':
122
131
  case 'files':
@@ -132,7 +141,7 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
132
141
  );
133
142
 
134
143
  case 'float':
135
- return <input type="number" defaultValue={ value.number } { ...refs(item.name, Validate(String(item.rules), t)) } step="0.01" />;
144
+ return <input type="number" id={ field } defaultValue={ value.number } { ...refs(item.name, Validate(String(item.rules), t)) } step="0.01" />;
136
145
 
137
146
  case 'link':
138
147
  // The function-value variant only ever arrives on a 'component' row -
@@ -140,19 +149,19 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
140
149
  return <Linked id={ id } value={ item.value as (string | number | JSX.Element | undefined) } url={ item.url } />;
141
150
 
142
151
  case 'number':
143
- return <input type="number" defaultValue={ value.number } { ...refs(item.name, Validate(String(item.rules), t)) } />
152
+ return <input type="number" id={ field } defaultValue={ value.number } { ...refs(item.name, Validate(String(item.rules), t)) } />
144
153
 
145
154
  case 'password':
146
- return <Password name={ item.name } validation={ item.rules } defaultValue={ value.string } t={ t } refs={ refs } />;
155
+ return <Password name={ item.name } id={ field } validation={ item.rules } defaultValue={ value.string } t={ t } refs={ refs } />;
147
156
 
148
157
  case 'select':
149
- return <Dropdown item={ item } refs={ refs } t={ t } />;
158
+ return <Dropdown item={ item } field={ field } refs={ refs } t={ t } />;
150
159
 
151
160
  case 'text':
152
- return <input type="text" defaultValue={ value.string } { ...refs(item.name, Validate(String(item.rules), t)) } />;
161
+ return <input type="text" id={ field } defaultValue={ value.string } { ...refs(item.name, Validate(String(item.rules), t)) } />;
153
162
 
154
163
  case 'textarea':
155
- return <textarea defaultValue={ value.string } { ...refs(item.name, Validate(String(item.rules), t)) }></textarea>;
164
+ return <textarea id={ field } defaultValue={ value.string } { ...refs(item.name, Validate(String(item.rules), t)) }></textarea>;
156
165
 
157
166
  case 'textarea-html':
158
167
  return (
package/Viewer/Item.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { useId } from 'react';
1
2
  import { FieldErrors, UseFormRegister, UseFormSetValue } from 'react-hook-form';
2
3
  import { Display } from '@autobusal/utilities';
3
4
  import Data from './Data';
@@ -14,8 +15,46 @@ interface Props {
14
15
  onUpdate: UseFormSetValue<any>
15
16
  }
16
17
 
17
- const Item = ({ id, data, refs, t, errors, onUpdate }: Props): JSX.Element => (
18
- <div className={ `row row-${ data.type } ${ data.label === undefined && 'spacer' }` }>
18
+ /**
19
+ * The row types that ARE exactly one control, and can therefore be pointed
20
+ * at by a label.
21
+ *
22
+ * Edited: Claude - Date: 2026-08-20
23
+ *
24
+ * Everything else in this switch is either not a control at all ('display',
25
+ * 'link', 'component') or is several of them ('checkbox-list', 'files') -
26
+ * and `htmlFor` naming one arbitrary member of a group is worse than no
27
+ * association, because it reads as a promise that the group has a name.
28
+ * Those rows keep the label they had; this list is the set that gains one.
29
+ */
30
+ const SINGLE = [ 'text', 'email', 'password', 'number', 'float', 'textarea', 'select', 'date', 'dob', 'picker', 'checkbox' ];
31
+
32
+ /**
33
+ * Edited: Claude - Date: 2026-08-20
34
+ *
35
+ * EVERY FIELD IN THIS FORM WAS UNNAMED. The label was a <span> beside an
36
+ * <input> with no id, so the two were adjacent and nothing more: a screen
37
+ * reader announced the whole of any admin or account form as a run of
38
+ * anonymous "edit text", and clicking a label did nothing. Fixed here, once,
39
+ * rather than in each of the dozens of screens that build their fields
40
+ * through this component.
41
+ *
42
+ * The id comes from useId and is therefore page-unique. The field NAME would
43
+ * have been the obvious thing to use and is the wrong thing: names belong to
44
+ * the form payload and repeat across forms ('email' is a field on three
45
+ * screens), while ids are page-global - two forms on one page would emit
46
+ * duplicate ids, which is its own defect. This is the same rule
47
+ * @autobusal/routes-order's passenger block and @autobusal/auth follow.
48
+ */
49
+ const Item = ({ id, data, refs, t, errors, onUpdate }: Props): JSX.Element => {
50
+ const uid = useId();
51
+
52
+ const field = data.type !== undefined && SINGLE.includes(data.type) && data.name !== undefined
53
+ ? `${ uid }${ data.name }`
54
+ : undefined;
55
+
56
+ return (
57
+ <div className={ `row row-${ data.type } ${ data.label === undefined && 'spacer' }` }>
19
58
  {/*
20
59
  * Edited: Ferjolt Ozuni - Date: 2026-08-07
21
60
  *
@@ -31,26 +70,28 @@ const Item = ({ id, data, refs, t, errors, onUpdate }: Props): JSX.Element => (
31
70
  * spacer, and an empty element there would take a flex slot and open a
32
71
  * gap where the divider should be.
33
72
  */}
34
- { data.label !== undefined && (
35
- <Label>
36
- { data.label }
37
-
38
- { data.rules?.includes('required') && <Required aria-hidden="true">*</Required> }
39
- </Label>
40
- ) }
41
-
42
- <Data
43
- id={ id }
44
- item={ data }
45
- refs={ refs }
46
- t={ t }
47
- onUpdate={ onUpdate }
48
- />
49
-
50
- { data.name && Display(errors[data.name]) }
51
-
52
- { data.notice && <Notice>* { data.notice }</Notice> }
53
- </div>
54
- );
73
+ { data.label !== undefined && (
74
+ <Label htmlFor={ field }>
75
+ { data.label }
76
+
77
+ { data.rules?.includes('required') && <Required aria-hidden="true">*</Required> }
78
+ </Label>
79
+ ) }
80
+
81
+ <Data
82
+ id={ id }
83
+ field={ field }
84
+ item={ data }
85
+ refs={ refs }
86
+ t={ t }
87
+ onUpdate={ onUpdate }
88
+ />
89
+
90
+ { data.name && Display(errors[data.name]) }
91
+
92
+ { data.notice && <Notice>* { data.notice }</Notice> }
93
+ </div>
94
+ );
95
+ };
55
96
 
56
97
  export default Item;
@@ -5,11 +5,13 @@ import { ViewData } from '../types';
5
5
 
6
6
  interface Props {
7
7
  item: ViewData
8
+ /* Edited: Claude - Date: 2026-08-20: the id the row's label points at */
9
+ field?: string
8
10
  refs: UseFormRegister<any>
9
11
  t: TFunction<'common'>
10
12
  }
11
13
 
12
- const Dropdown = ({ item, refs, t }: Props): (JSX.Element | null) => {
14
+ const Dropdown = ({ item, field, refs, t }: Props): (JSX.Element | null) => {
13
15
  if (item.name === undefined) {
14
16
  return null;
15
17
  }
@@ -46,7 +48,7 @@ const Dropdown = ({ item, refs, t }: Props): (JSX.Element | null) => {
46
48
  * want.
47
49
  */
48
50
  return (
49
- <select defaultValue={ item?.value ?? '' } { ...refs(item.name, rules) }>
51
+ <select id={ field } defaultValue={ item?.value ?? '' } { ...refs(item.name, rules) }>
50
52
  { item.placeholder !== undefined && (
51
53
  <option value="">{ item.placeholder }</option>
52
54
  ) }
package/Viewer/Viewer.tsx CHANGED
@@ -13,6 +13,13 @@ interface Props {
13
13
  fetching?: boolean
14
14
  pending?: boolean
15
15
  actions?: string[]
16
+ /*
17
+ * Edited: Claude - Date: 2026-08-20
18
+ * What to ASK before the delete button acts, replacing the generic
19
+ * question. Same prop, same reason, as Table's - a form that destroys an
20
+ * identity document should say so.
21
+ */
22
+ confirm?: string
16
23
  t: TFunction<'common'>
17
24
  onSave?: (data: any) => void
18
25
  onDelete?: (data?: any) => void
@@ -23,7 +30,7 @@ interface Props {
23
30
  onTest?: (data: any) => void
24
31
  }
25
32
 
26
- const Viewer = ({ id, type, data, fetching, pending, actions, t, onSave, onDelete, onTest }: Props): JSX.Element => {
33
+ const Viewer = ({ id, type, data, fetching, pending, actions, confirm, t, onSave, onDelete, onTest }: Props): JSX.Element => {
27
34
  const { register, handleSubmit, formState: { errors }, setValue } = useForm<any>();
28
35
 
29
36
  if (fetching) {
@@ -81,6 +88,7 @@ const Viewer = ({ id, type, data, fetching, pending, actions, t, onSave, onDelet
81
88
  id={ id }
82
89
  available={ actions }
83
90
  pending={ pending }
91
+ confirm={ confirm }
84
92
  t={ t }
85
93
  onDelete={ onDelete }
86
94
  />
package/Viewer/styles.ts CHANGED
@@ -108,7 +108,14 @@ export const Notice = styled.div`
108
108
  * used - the requirement is already carried to assistive tech by the
109
109
  * field's own validation, and a lone "*" read aloud says nothing.
110
110
  */
111
- export const Label = styled.span``;
111
+ /**
112
+ * Edited: Claude - Date: 2026-08-20
113
+ * A real <label>, not a span. It carries `htmlFor` now (see Item.tsx), and
114
+ * only a label element makes that an association rather than an attribute
115
+ * the browser ignores. Visually identical - both are inline elements, and
116
+ * this rule set was and remains empty.
117
+ */
118
+ export const Label = styled.label``;
112
119
 
113
120
  export const Required = styled.span`
114
121
  margin-left: 3px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "1.33.8",
3
+ "version": "1.33.9",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"