@autobusal/common 1.32.0 → 1.33.1

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.
@@ -39,18 +39,18 @@ const AiFieldAssist = ({ text, sourceText, targetLocale, onInsert, t }: Props):
39
39
  <>
40
40
  <Trigger>
41
41
  <button type="button" onClick={ () => setMode('generate') }>
42
- { t('ai_assist.trigger.generate') }
42
+ { t('ai_assist.trigger.generate', { ns: 'common' }) }
43
43
  </button>
44
44
 
45
45
  { hasText && (
46
46
  <button type="button" onClick={ () => setMode('improve') }>
47
- { t('ai_assist.trigger.improve') }
47
+ { t('ai_assist.trigger.improve', { ns: 'common' }) }
48
48
  </button>
49
49
  ) }
50
50
 
51
51
  { canTranslate && (
52
52
  <button type="button" onClick={ () => setMode('translate') }>
53
- { t('ai_assist.trigger.translate') }
53
+ { t('ai_assist.trigger.translate', { ns: 'common' }) }
54
54
  </button>
55
55
  ) }
56
56
  </Trigger>
@@ -2,7 +2,7 @@ import { useState } from 'react';
2
2
  import { TFunction } from 'i18next';
3
3
  import Modal from '../Modal/Modal';
4
4
  import { useAiAvailable, useAiGenerate, useAiTranslate, useAiImprove } from './services';
5
- import { Row, Field, Compare, Pane, Draft, Actions, Error } from './styles';
5
+ import { Row, Field, Compare, Pane, Draft, Actions } from './styles';
6
6
 
7
7
  interface Props {
8
8
  mode: 'generate' | 'improve' | 'translate'
@@ -25,6 +25,17 @@ interface Props {
25
25
  * Ferjolt Ozuni - Date: 2026-08-08
26
26
  */
27
27
  const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClose, t }: Props): JSX.Element => {
28
+ // Edited: Ferjolt Ozuni - Date: 2026-08-08
29
+ // Every ai_assist.* key lives in the 'common' namespace (obtapi's
30
+ // public/languages/{locale}/common.json), but the `t` this component
31
+ // receives is whatever namespace the CALLING page's own useTranslation()
32
+ // defaulted to (admin-pages/admin-label both default to 'normal') - a
33
+ // bare t('ai_assist.x') looked the key up in the wrong namespace, found
34
+ // nothing, and i18next's fallback is to render the raw key string. Every
35
+ // OTHER cross-namespace lookup in this codebase passes {ns:'common'}
36
+ // per call; this wrapper does it once instead of thirteen times.
37
+ const tc = (key: string): string => t(key, { ns: 'common' });
38
+
28
39
  const { data: available, isLoading: loadingAvailable } = useAiAvailable();
29
40
 
30
41
  const providers = Object.keys(available ?? {});
@@ -33,7 +44,6 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
33
44
  const [ model, setModel ] = useState<string>('');
34
45
  const [ instruction, setInstruction ] = useState<string>('');
35
46
  const [ draft, setDraft ] = useState<string | null>(null);
36
- const [ error, setError ] = useState<string | null>(null);
37
47
 
38
48
  const activeProvider = provider || providers[0] || '';
39
49
  const activeModel = model || available?.[activeProvider]?.default_model || '';
@@ -44,10 +54,17 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
44
54
 
45
55
  const running = generating || translating || improving;
46
56
 
57
+ // Edited: Ferjolt Ozuni - Date: 2026-08-08
58
+ // No local onError/error state here - apiClient's own response
59
+ // interceptor already surfaces the real backend message as a toast on
60
+ // any 422 (obtapi's Ai\AdminController returns provider errors that
61
+ // way). A local onError reading a plain react-query Error's .message
62
+ // would only ever see axios's generic "Request failed with status code
63
+ // 422", never the actual "Incorrect API key provided..." the backend
64
+ // sent - found live, the modal showed exactly that generic text while
65
+ // the real message went to the toast every other form in this app
66
+ // already relies on.
47
67
  const onRun = (): void => {
48
- setError(null);
49
-
50
- const onError = (err: Error): void => setError(err.message);
51
68
  const onSuccess = (result: { content: string }): void => {
52
69
  setDraft(result.content);
53
70
  setInstruction('');
@@ -58,13 +75,13 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
58
75
  // endpoint that takes existing text + an instruction, which is exactly
59
76
  // what a revision is.
60
77
  if (draft !== null) {
61
- Improve({ provider: activeProvider, model: activeModel, text: draft, instruction }, { onSuccess, onError });
78
+ Improve({ provider: activeProvider, model: activeModel, text: draft, instruction }, { onSuccess });
62
79
 
63
80
  return;
64
81
  }
65
82
 
66
83
  if (mode === 'generate') {
67
- Generate({ provider: activeProvider, model: activeModel, prompt: instruction }, { onSuccess, onError });
84
+ Generate({ provider: activeProvider, model: activeModel, prompt: instruction }, { onSuccess });
68
85
 
69
86
  return;
70
87
  }
@@ -76,12 +93,12 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
76
93
  text: text ?? '',
77
94
  target_locale: targetLocale ?? '',
78
95
  source_locale: sourceLocale
79
- }, { onSuccess, onError });
96
+ }, { onSuccess });
80
97
 
81
98
  return;
82
99
  }
83
100
 
84
- Improve({ provider: activeProvider, model: activeModel, text: text ?? '', instruction }, { onSuccess, onError });
101
+ Improve({ provider: activeProvider, model: activeModel, text: text ?? '', instruction }, { onSuccess });
85
102
  };
86
103
 
87
104
  const onInsertClick = (): void => {
@@ -94,11 +111,9 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
94
111
 
95
112
  const content = (
96
113
  <>
97
- { error && <Error>{ error }</Error> }
98
-
99
114
  <Row>
100
115
  <Field>
101
- { t('ai_assist.provider') }
116
+ { tc('ai_assist.provider') }
102
117
 
103
118
  <select value={ activeProvider } onChange={ event => { setProvider(event.target.value); setModel(''); } }>
104
119
  { providers.map(key => <option key={ key } value={ key }>{ key }</option>) }
@@ -106,7 +121,7 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
106
121
  </Field>
107
122
 
108
123
  <Field>
109
- { t('ai_assist.model') }
124
+ { tc('ai_assist.model') }
110
125
 
111
126
  <select value={ activeModel } onChange={ event => setModel(event.target.value) }>
112
127
  { (available?.[activeProvider]?.models ?? []).map(name => <option key={ name } value={ name }>{ name }</option>) }
@@ -118,14 +133,14 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
118
133
  <Row>
119
134
  <Field>
120
135
  { draft !== null
121
- ? t('ai_assist.revise_instruction')
122
- : (mode === 'generate' ? t('ai_assist.generate_prompt') : t('ai_assist.improve_instruction')) }
136
+ ? tc('ai_assist.revise_instruction')
137
+ : (mode === 'generate' ? tc('ai_assist.generate_prompt') : tc('ai_assist.improve_instruction')) }
123
138
 
124
139
  <input
125
140
  type="text"
126
141
  value={ instruction }
127
142
  onChange={ event => setInstruction(event.target.value) }
128
- placeholder={ draft !== null ? t('ai_assist.revise_placeholder') : undefined }
143
+ placeholder={ draft !== null ? tc('ai_assist.revise_placeholder') : undefined }
129
144
  />
130
145
  </Field>
131
146
  </Row>
@@ -133,7 +148,7 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
133
148
 
134
149
  { draft === null && mode === 'translate' && (
135
150
  <Pane>
136
- <h4>{ t('ai_assist.source') }</h4>
151
+ <h4>{ tc('ai_assist.source') }</h4>
137
152
 
138
153
  <div dangerouslySetInnerHTML={ { __html: text ?? '' } } />
139
154
  </Pane>
@@ -142,13 +157,13 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
142
157
  { draft !== null && mode === 'translate' && (
143
158
  <Compare>
144
159
  <Pane>
145
- <h4>{ t('ai_assist.source') }</h4>
160
+ <h4>{ tc('ai_assist.source') }</h4>
146
161
 
147
162
  <div dangerouslySetInnerHTML={ { __html: text ?? '' } } />
148
163
  </Pane>
149
164
 
150
165
  <Pane>
151
- <h4>{ t('ai_assist.draft') }</h4>
166
+ <h4>{ tc('ai_assist.draft') }</h4>
152
167
 
153
168
  <div dangerouslySetInnerHTML={ { __html: draft } } />
154
169
  </Pane>
@@ -161,16 +176,16 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
161
176
 
162
177
  <Actions>
163
178
  <button type="button" onClick={ onClose } disabled={ running }>
164
- { t('ai_assist.cancel') }
179
+ { tc('ai_assist.cancel') }
165
180
  </button>
166
181
 
167
182
  <button type="button" onClick={ onRun } disabled={ running || !activeProvider || (mode !== 'translate' && draft === null && instruction.trim() === '') }>
168
- { draft !== null ? t('ai_assist.revise') : t('ai_assist.run') }
183
+ { draft !== null ? tc('ai_assist.revise') : tc('ai_assist.run') }
169
184
  </button>
170
185
 
171
186
  { draft !== null && (
172
187
  <button type="button" onClick={ onInsertClick } disabled={ running }>
173
- { t('ai_assist.insert') }
188
+ { tc('ai_assist.insert') }
174
189
  </button>
175
190
  ) }
176
191
  </Actions>
@@ -181,7 +196,7 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
181
196
  <Modal
182
197
  loading={ loadingAvailable }
183
198
  width={ 720 }
184
- title={ t(`ai_assist.title.${ mode }`) }
199
+ title={ tc(`ai_assist.title.${ mode }`) }
185
200
  content={ content }
186
201
  onClose={ onClose }
187
202
  />
package/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## 1.33.1
5
+
6
+ ### Added
7
+
8
+ - **`Table` accepts an optional `onReorder(draggedId, targetId)` prop.** When set, every row becomes an HTML5 drag source/drop target (dims to 40% opacity while being dragged) and a completed drag reports the dragged row's id and the row it was dropped on - the caller decides what "reorder" means for its own data. Omitted by every existing table, so this is purely additive; first consumer is `@autobusal/admin-menu` 1.1.2.
9
+
10
+ ## 1.33.0
11
+
12
+ ### Fixed
13
+
14
+ - **`Viewer`'s `type: 'component'` rows never actually rendered, anywhere.** `Data.tsx` returned `null` for any item whose `name` was `undefined` before the switch statement that handled `'component'` ever ran - and a component row never HAS a name, it isn't a registered form field. Every SectionHeading divider, every `AiFieldAssist` Generate/Improve/Translate trigger, and every description paragraph moved inside a form's own box (SMS/Flex, this cycle) had silently never rendered, in any tab that used one, despite compiling clean and sitting right there in the bundle. Moved the `'component'` case to an early return above the name guard instead of weakening the guard's condition, which would have broken TypeScript's narrowing of `item.name` to `string` for every other case below it.
15
+ - **AI trigger buttons and the review modal rendered raw i18n keys** (`ai_assist.trigger.generate`) instead of translated text. `ai_assist.*` lives in the `common` namespace, but the `t` passed into `AiFieldAssist`/`AiReviewModal` defaults to whatever namespace the calling page's own `useTranslation()` uses (`normal`/`whitelabel`) - every call now passes `{ ns: 'common' }`.
16
+ - **`AiReviewModal` showed a generic "Request failed with status code 422"** instead of the real provider error on a failed generate/translate/improve. Removed its local error state/display entirely - `apiClient`'s existing global response interceptor already toasts `error.response.data.message` on any 422, the same mechanism every other form in the app already relies on; a local `onError` reading react-query's plain `Error.message` could only ever see Axios's generic text, never the backend's.
17
+
4
18
  ## 1.32.0
5
19
 
6
20
  ### Added
@@ -1,4 +1,5 @@
1
1
  import { TFunction } from 'i18next';
2
+ import { useState } from 'react';
2
3
  import { NavigateFunction } from 'react-router-dom';
3
4
  import Loading from './Loading';
4
5
  import NotFound from './NotFound';
@@ -18,9 +19,11 @@ interface Props {
18
19
  actions?: string[]
19
20
  handlers?: any
20
21
  navigate: NavigateFunction
22
+ onReorder?: (draggedId: number, targetId: number) => void
21
23
  }
22
24
 
23
- const Rows = ({ url, urlWith, data, loading, fetching, colLength, empty, t, actions, handlers, navigate }: Props): JSX.Element => {
25
+ const Rows = ({ url, urlWith, data, loading, fetching, colLength, empty, t, actions, handlers, navigate, onReorder }: Props): JSX.Element => {
26
+ const [ draggedId, setDraggedId ] = useState<number | null>(null);
24
27
  if (loading) {
25
28
  return <Loading length={ colLength } t={ t } />;
26
29
  }
@@ -43,8 +46,28 @@ const Rows = ({ url, urlWith, data, loading, fetching, colLength, empty, t, acti
43
46
  // table's list, which is what every existing caller relies on.
44
47
  const available = item.props.actions ?? actions;
45
48
 
49
+ const id = item.props.id;
50
+
46
51
  return (
47
- <Tr key={ index } $viewable={ isViewable } onClick={ () => onClick(item.props.id) }>
52
+ <Tr
53
+ key={ index }
54
+ $viewable={ isViewable }
55
+ $dragging={ onReorder !== undefined && draggedId === id }
56
+ draggable={ onReorder !== undefined }
57
+ onClick={ () => onClick(id) }
58
+ onDragStart={ onReorder && (() => setDraggedId(id)) }
59
+ onDragOver={ onReorder && (event => event.preventDefault()) }
60
+ onDragEnd={ onReorder && (() => setDraggedId(null)) }
61
+ onDrop={ onReorder && (event => {
62
+ event.preventDefault();
63
+
64
+ if (draggedId !== null && draggedId !== id) {
65
+ onReorder(draggedId, id);
66
+ }
67
+
68
+ setDraggedId(null);
69
+ }) }
70
+ >
48
71
  { item }
49
72
 
50
73
  { rowActions(available).length > 0 && (
@@ -19,11 +19,15 @@ export const Tbody = styled.tbody<{ $fetching: boolean }>`
19
19
  ` }
20
20
  `;
21
21
 
22
- export const Tr = styled.tr<{ $viewable?: boolean }>`
22
+ export const Tr = styled.tr<{ $viewable?: boolean, $dragging?: boolean }>`
23
23
  ${ props => props.$viewable && css`
24
24
  cursor: pointer;
25
25
  ` }
26
26
 
27
+ ${ props => props.$dragging && css`
28
+ opacity: .4;
29
+ ` }
30
+
27
31
  text-align: center;
28
32
 
29
33
  & > td:first-of-type {
package/Table/Table.tsx CHANGED
@@ -33,9 +33,18 @@ interface Props {
33
33
  */
34
34
  empty?: string
35
35
  handlers?: any
36
+ /*
37
+ * Edited: Ferjolt Ozuni - Date: 2026-08-10
38
+ * Turns every row into an HTML5 drag source/drop target and reports a
39
+ * completed drag as (the dragged row's id, the row it was dropped on).
40
+ * The caller decides what "reorder" means for its own data (e.g.
41
+ * recomputing a full id list to persist) - Table only reports the drag.
42
+ * Omitted by every other table, so this is purely additive.
43
+ */
44
+ onReorder?: (draggedId: number, targetId: number) => void
36
45
  }
37
46
 
38
- const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, fetching, t, actions, extra, empty, handlers }: Props): JSX.Element => {
47
+ const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, fetching, t, actions, extra, empty, handlers, onReorder }: Props): JSX.Element => {
39
48
  const navigate = useNavigate();
40
49
 
41
50
  const colLength = columns.length + 1;
@@ -75,6 +84,7 @@ const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, f
75
84
  actions={ actions }
76
85
  handlers={ handlers }
77
86
  navigate={ navigate }
87
+ onReorder={ onReorder }
78
88
  />
79
89
 
80
90
  <Paginate data={ pages } colLength={ colLength } t={ t } />
package/Viewer/Data.tsx CHANGED
@@ -21,6 +21,29 @@ interface Props {
21
21
  }
22
22
 
23
23
  const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
24
+ // Edited: Ferjolt Ozuni - Date: 2026-08-08
25
+ // A 'component' row is not a registered form field - it never had a
26
+ // name to give, and the name-required guard below (meant to stop every
27
+ // OTHER type from registering with react-hook-form under an undefined
28
+ // key) was silently returning null for every one of them before ever
29
+ // reaching the 'component' case that used to sit in the switch further
30
+ // down. Found live: a whole category of content - SectionHeading
31
+ // dividers, AiFieldAssist's Generate/Improve/Translate triggers,
32
+ // description text moved inside a form's own box - had never actually
33
+ // rendered, in any of the tabs that used it, despite compiling clean
34
+ // and being right there in the production bundle. Handled here, before
35
+ // the name guard, rather than by weakening that guard's condition -
36
+ // TypeScript narrows item.name to a definite string for every case
37
+ // below FROM the guard, and a conditional guard cannot narrow it the
38
+ // same way.
39
+ if (item.type === 'component') {
40
+ return (
41
+ <div className="row-data">
42
+ { typeof item.value === 'function' ? item.value(onUpdate) : item.value }
43
+ </div>
44
+ );
45
+ }
46
+
24
47
  if (item.name === undefined) {
25
48
  return null;
26
49
  }
@@ -65,13 +88,6 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
65
88
  />
66
89
  );
67
90
 
68
- case 'component':
69
- return (
70
- <div className="row-data">
71
- { typeof item.value === 'function' ? item.value(onUpdate) : item.value }
72
- </div>
73
- );
74
-
75
91
  case 'display':
76
92
  return <Display className="row-data">{ value.string }</Display>;
77
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "1.32.0",
3
+ "version": "1.33.1",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"