@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.
- package/AiAssist/AiFieldAssist.tsx +3 -3
- package/AiAssist/AiReviewModal.tsx +38 -23
- package/CHANGELOG.md +14 -0
- package/Table/Rows/Rows.tsx +25 -2
- package/Table/Rows/styles.ts +5 -1
- package/Table/Table.tsx +11 -1
- package/Viewer/Data.tsx +23 -7
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
96
|
+
}, { onSuccess });
|
|
80
97
|
|
|
81
98
|
return;
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
Improve({ provider: activeProvider, model: activeModel, text: text ?? '', instruction }, { onSuccess
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
?
|
|
122
|
-
: (mode === 'generate' ?
|
|
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 ?
|
|
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>{
|
|
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>{
|
|
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>{
|
|
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
|
-
{
|
|
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 ?
|
|
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
|
-
{
|
|
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={
|
|
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
|
package/Table/Rows/Rows.tsx
CHANGED
|
@@ -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
|
|
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 && (
|
package/Table/Rows/styles.ts
CHANGED
|
@@ -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
|
|