@autobusal/common 1.28.0 → 1.30.0
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 +72 -0
- package/AiAssist/AiReviewModal.tsx +191 -0
- package/AiAssist/services.ts +84 -0
- package/AiAssist/styles.ts +94 -0
- package/AiAssist/types.ts +12 -0
- package/CHANGELOG.md +29 -0
- package/Viewer/Data.tsx +8 -2
- package/Viewer/styles.ts +12 -6
- package/Viewer/types.ts +10 -1
- package/index.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { useAiAvailable } from './services';
|
|
4
|
+
import AiReviewModal from './AiReviewModal';
|
|
5
|
+
import { Trigger } from './styles';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** The field's own current value - the base text for "Improve". */
|
|
9
|
+
text?: string
|
|
10
|
+
/** The English source text, for a "Translate from EN" trigger. */
|
|
11
|
+
sourceText?: string
|
|
12
|
+
/** This field's locale, e.g. "sq" - required for the translate trigger. */
|
|
13
|
+
targetLocale?: string
|
|
14
|
+
onInsert: (content: string) => void
|
|
15
|
+
t: TFunction<'common'>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sits next to one content field (a 'component'-type Viewer row - see
|
|
20
|
+
* Data.tsx's render-prop support for that type) and renders nothing at all
|
|
21
|
+
* when no AI provider is enabled for this label, which is the whole
|
|
22
|
+
* visibility gate the plan calls for - no separate flag to keep in sync.
|
|
23
|
+
*
|
|
24
|
+
* Ferjolt Ozuni - Date: 2026-08-08
|
|
25
|
+
*/
|
|
26
|
+
const AiFieldAssist = ({ text, sourceText, targetLocale, onInsert, t }: Props): JSX.Element | null => {
|
|
27
|
+
const { data: available } = useAiAvailable();
|
|
28
|
+
|
|
29
|
+
const [ mode, setMode ] = useState<'generate' | 'improve' | 'translate' | null>(null);
|
|
30
|
+
|
|
31
|
+
if (!available || Object.keys(available).length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hasText = (text ?? '').trim() !== '';
|
|
36
|
+
const canTranslate = (sourceText ?? '').trim() !== '' && !!targetLocale;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<Trigger>
|
|
41
|
+
<button type="button" onClick={ () => setMode('generate') }>
|
|
42
|
+
{ t('ai_assist.trigger.generate') }
|
|
43
|
+
</button>
|
|
44
|
+
|
|
45
|
+
{ hasText && (
|
|
46
|
+
<button type="button" onClick={ () => setMode('improve') }>
|
|
47
|
+
{ t('ai_assist.trigger.improve') }
|
|
48
|
+
</button>
|
|
49
|
+
) }
|
|
50
|
+
|
|
51
|
+
{ canTranslate && (
|
|
52
|
+
<button type="button" onClick={ () => setMode('translate') }>
|
|
53
|
+
{ t('ai_assist.trigger.translate') }
|
|
54
|
+
</button>
|
|
55
|
+
) }
|
|
56
|
+
</Trigger>
|
|
57
|
+
|
|
58
|
+
{ mode && (
|
|
59
|
+
<AiReviewModal
|
|
60
|
+
mode={ mode }
|
|
61
|
+
text={ mode === 'translate' ? sourceText : text }
|
|
62
|
+
targetLocale={ targetLocale }
|
|
63
|
+
onInsert={ onInsert }
|
|
64
|
+
onClose={ () => setMode(null) }
|
|
65
|
+
t={ t }
|
|
66
|
+
/>
|
|
67
|
+
) }
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default AiFieldAssist;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import Modal from '../Modal/Modal';
|
|
4
|
+
import { useAiAvailable, useAiGenerate, useAiTranslate, useAiImprove } from './services';
|
|
5
|
+
import { Row, Field, Compare, Pane, Draft, Actions, Error } from './styles';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
mode: 'generate' | 'improve' | 'translate'
|
|
9
|
+
text?: string
|
|
10
|
+
targetLocale?: string
|
|
11
|
+
sourceLocale?: string
|
|
12
|
+
onInsert: (content: string) => void
|
|
13
|
+
onClose: () => void
|
|
14
|
+
t: TFunction<'common'>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The one AI-review surface every content admin form shares - a draft
|
|
19
|
+
* NEVER lands in a field on its own. It renders in here first, the admin
|
|
20
|
+
* can hand-edit it or ask for a revision (which re-runs improve() with the
|
|
21
|
+
* CURRENT draft as the base text, regardless of what mode this opened in),
|
|
22
|
+
* and only an explicit Insert writes it back to the caller's field. Cancel
|
|
23
|
+
* discards everything; the form's own Save is a separate, second step.
|
|
24
|
+
*
|
|
25
|
+
* Ferjolt Ozuni - Date: 2026-08-08
|
|
26
|
+
*/
|
|
27
|
+
const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClose, t }: Props): JSX.Element => {
|
|
28
|
+
const { data: available, isLoading: loadingAvailable } = useAiAvailable();
|
|
29
|
+
|
|
30
|
+
const providers = Object.keys(available ?? {});
|
|
31
|
+
|
|
32
|
+
const [ provider, setProvider ] = useState<string>('');
|
|
33
|
+
const [ model, setModel ] = useState<string>('');
|
|
34
|
+
const [ instruction, setInstruction ] = useState<string>('');
|
|
35
|
+
const [ draft, setDraft ] = useState<string | null>(null);
|
|
36
|
+
const [ error, setError ] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
const activeProvider = provider || providers[0] || '';
|
|
39
|
+
const activeModel = model || available?.[activeProvider]?.default_model || '';
|
|
40
|
+
|
|
41
|
+
const { mutate: Generate, isPending: generating } = useAiGenerate();
|
|
42
|
+
const { mutate: Translate, isPending: translating } = useAiTranslate();
|
|
43
|
+
const { mutate: Improve, isPending: improving } = useAiImprove();
|
|
44
|
+
|
|
45
|
+
const running = generating || translating || improving;
|
|
46
|
+
|
|
47
|
+
const onRun = (): void => {
|
|
48
|
+
setError(null);
|
|
49
|
+
|
|
50
|
+
const onError = (err: Error): void => setError(err.message);
|
|
51
|
+
const onSuccess = (result: { content: string }): void => {
|
|
52
|
+
setDraft(result.content);
|
|
53
|
+
setInstruction('');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Once a draft exists, EVERY run (regardless of the mode this modal
|
|
57
|
+
// opened in) is a revision on that draft - improve() is the one
|
|
58
|
+
// endpoint that takes existing text + an instruction, which is exactly
|
|
59
|
+
// what a revision is.
|
|
60
|
+
if (draft !== null) {
|
|
61
|
+
Improve({ provider: activeProvider, model: activeModel, text: draft, instruction }, { onSuccess, onError });
|
|
62
|
+
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (mode === 'generate') {
|
|
67
|
+
Generate({ provider: activeProvider, model: activeModel, prompt: instruction }, { onSuccess, onError });
|
|
68
|
+
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (mode === 'translate') {
|
|
73
|
+
Translate({
|
|
74
|
+
provider: activeProvider,
|
|
75
|
+
model: activeModel,
|
|
76
|
+
text: text ?? '',
|
|
77
|
+
target_locale: targetLocale ?? '',
|
|
78
|
+
source_locale: sourceLocale
|
|
79
|
+
}, { onSuccess, onError });
|
|
80
|
+
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Improve({ provider: activeProvider, model: activeModel, text: text ?? '', instruction }, { onSuccess, onError });
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const onInsertClick = (): void => {
|
|
88
|
+
if (draft !== null) {
|
|
89
|
+
onInsert(draft);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onClose();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const content = (
|
|
96
|
+
<>
|
|
97
|
+
{ error && <Error>{ error }</Error> }
|
|
98
|
+
|
|
99
|
+
<Row>
|
|
100
|
+
<Field>
|
|
101
|
+
{ t('ai_assist.provider') }
|
|
102
|
+
|
|
103
|
+
<select value={ activeProvider } onChange={ event => { setProvider(event.target.value); setModel(''); } }>
|
|
104
|
+
{ providers.map(key => <option key={ key } value={ key }>{ key }</option>) }
|
|
105
|
+
</select>
|
|
106
|
+
</Field>
|
|
107
|
+
|
|
108
|
+
<Field>
|
|
109
|
+
{ t('ai_assist.model') }
|
|
110
|
+
|
|
111
|
+
<select value={ activeModel } onChange={ event => setModel(event.target.value) }>
|
|
112
|
+
{ (available?.[activeProvider]?.models ?? []).map(name => <option key={ name } value={ name }>{ name }</option>) }
|
|
113
|
+
</select>
|
|
114
|
+
</Field>
|
|
115
|
+
</Row>
|
|
116
|
+
|
|
117
|
+
{ mode !== 'translate' && (
|
|
118
|
+
<Row>
|
|
119
|
+
<Field>
|
|
120
|
+
{ draft !== null
|
|
121
|
+
? t('ai_assist.revise_instruction')
|
|
122
|
+
: (mode === 'generate' ? t('ai_assist.generate_prompt') : t('ai_assist.improve_instruction')) }
|
|
123
|
+
|
|
124
|
+
<input
|
|
125
|
+
type="text"
|
|
126
|
+
value={ instruction }
|
|
127
|
+
onChange={ event => setInstruction(event.target.value) }
|
|
128
|
+
placeholder={ draft !== null ? t('ai_assist.revise_placeholder') : undefined }
|
|
129
|
+
/>
|
|
130
|
+
</Field>
|
|
131
|
+
</Row>
|
|
132
|
+
) }
|
|
133
|
+
|
|
134
|
+
{ draft === null && mode === 'translate' && (
|
|
135
|
+
<Pane>
|
|
136
|
+
<h4>{ t('ai_assist.source') }</h4>
|
|
137
|
+
|
|
138
|
+
<div dangerouslySetInnerHTML={ { __html: text ?? '' } } />
|
|
139
|
+
</Pane>
|
|
140
|
+
) }
|
|
141
|
+
|
|
142
|
+
{ draft !== null && mode === 'translate' && (
|
|
143
|
+
<Compare>
|
|
144
|
+
<Pane>
|
|
145
|
+
<h4>{ t('ai_assist.source') }</h4>
|
|
146
|
+
|
|
147
|
+
<div dangerouslySetInnerHTML={ { __html: text ?? '' } } />
|
|
148
|
+
</Pane>
|
|
149
|
+
|
|
150
|
+
<Pane>
|
|
151
|
+
<h4>{ t('ai_assist.draft') }</h4>
|
|
152
|
+
|
|
153
|
+
<div dangerouslySetInnerHTML={ { __html: draft } } />
|
|
154
|
+
</Pane>
|
|
155
|
+
</Compare>
|
|
156
|
+
) }
|
|
157
|
+
|
|
158
|
+
{ draft !== null && (
|
|
159
|
+
<Draft value={ draft } onChange={ event => setDraft(event.target.value) } />
|
|
160
|
+
) }
|
|
161
|
+
|
|
162
|
+
<Actions>
|
|
163
|
+
<button type="button" onClick={ onClose } disabled={ running }>
|
|
164
|
+
{ t('ai_assist.cancel') }
|
|
165
|
+
</button>
|
|
166
|
+
|
|
167
|
+
<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') }
|
|
169
|
+
</button>
|
|
170
|
+
|
|
171
|
+
{ draft !== null && (
|
|
172
|
+
<button type="button" onClick={ onInsertClick } disabled={ running }>
|
|
173
|
+
{ t('ai_assist.insert') }
|
|
174
|
+
</button>
|
|
175
|
+
) }
|
|
176
|
+
</Actions>
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Modal
|
|
182
|
+
loading={ loadingAvailable }
|
|
183
|
+
width={ 720 }
|
|
184
|
+
title={ t(`ai_assist.title.${ mode }`) }
|
|
185
|
+
content={ content }
|
|
186
|
+
onClose={ onClose }
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default AiReviewModal;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useQuery, UseQueryResult, useMutation, UseMutationResult } from '@tanstack/react-query';
|
|
2
|
+
import { apiClient } from '@autobusal/providers';
|
|
3
|
+
import { AiAvailable, AiCompletion } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The providers+models this label may actually use right now - empty means
|
|
7
|
+
* no provider is enabled, which is the signal every AI-assist entry point
|
|
8
|
+
* hides itself on. Cached briefly (not Infinity like the settings query -
|
|
9
|
+
* this is admin-only and an admin who just enabled a provider should not
|
|
10
|
+
* have to reload the whole app to see it).
|
|
11
|
+
*/
|
|
12
|
+
export const useAiAvailable = (): UseQueryResult<AiAvailable> => (
|
|
13
|
+
useQuery({
|
|
14
|
+
queryKey: [ 'ai-available' ],
|
|
15
|
+
queryFn: async () => (
|
|
16
|
+
await apiClient
|
|
17
|
+
.get('/api/ai/admin/available')
|
|
18
|
+
.then(response => (
|
|
19
|
+
response.data
|
|
20
|
+
))
|
|
21
|
+
),
|
|
22
|
+
staleTime: 60000
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
interface GenerateInput {
|
|
27
|
+
provider?: string
|
|
28
|
+
model?: string
|
|
29
|
+
prompt: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const useAiGenerate = (): UseMutationResult<AiCompletion, Error, GenerateInput, unknown> => (
|
|
33
|
+
useMutation({
|
|
34
|
+
mutationKey: [ 'ai-generate' ],
|
|
35
|
+
mutationFn: async (data: GenerateInput) => (
|
|
36
|
+
await apiClient
|
|
37
|
+
.post('/api/ai/admin/generate', data)
|
|
38
|
+
.then(response => (
|
|
39
|
+
response.data
|
|
40
|
+
))
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
interface TranslateInput {
|
|
46
|
+
provider?: string
|
|
47
|
+
model?: string
|
|
48
|
+
text: string
|
|
49
|
+
target_locale: string
|
|
50
|
+
source_locale?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const useAiTranslate = (): UseMutationResult<AiCompletion, Error, TranslateInput, unknown> => (
|
|
54
|
+
useMutation({
|
|
55
|
+
mutationKey: [ 'ai-translate' ],
|
|
56
|
+
mutationFn: async (data: TranslateInput) => (
|
|
57
|
+
await apiClient
|
|
58
|
+
.post('/api/ai/admin/translate', data)
|
|
59
|
+
.then(response => (
|
|
60
|
+
response.data
|
|
61
|
+
))
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
interface ImproveInput {
|
|
67
|
+
provider?: string
|
|
68
|
+
model?: string
|
|
69
|
+
text: string
|
|
70
|
+
instruction: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const useAiImprove = (): UseMutationResult<AiCompletion, Error, ImproveInput, unknown> => (
|
|
74
|
+
useMutation({
|
|
75
|
+
mutationKey: [ 'ai-improve' ],
|
|
76
|
+
mutationFn: async (data: ImproveInput) => (
|
|
77
|
+
await apiClient
|
|
78
|
+
.post('/api/ai/admin/improve', data)
|
|
79
|
+
.then(response => (
|
|
80
|
+
response.data
|
|
81
|
+
))
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Trigger = styled.div`
|
|
4
|
+
display: flex;
|
|
5
|
+
gap: 6px;
|
|
6
|
+
margin: 6px 0 12px;
|
|
7
|
+
|
|
8
|
+
button {
|
|
9
|
+
font-size: ${ props => props.theme.size.xs };
|
|
10
|
+
padding: 4px 10px;
|
|
11
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
12
|
+
border: 1px solid ${ props => props.theme.inputs.border };
|
|
13
|
+
background: ${ props => props.theme.background.box };
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
|
|
16
|
+
&:hover {
|
|
17
|
+
background: ${ props => props.theme.background.neutral };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
export const Row = styled.div`
|
|
23
|
+
display: flex;
|
|
24
|
+
gap: 10px;
|
|
25
|
+
align-items: flex-end;
|
|
26
|
+
margin-bottom: 14px;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export const Field = styled.label`
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: 4px;
|
|
34
|
+
font-size: ${ props => props.theme.size.s };
|
|
35
|
+
flex: 1;
|
|
36
|
+
min-width: 160px;
|
|
37
|
+
|
|
38
|
+
select, input, textarea {
|
|
39
|
+
padding: 8px 10px;
|
|
40
|
+
border: 1px solid ${ props => props.theme.inputs.border };
|
|
41
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
42
|
+
font-size: ${ props => props.theme.size.s };
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
export const Compare = styled.div`
|
|
47
|
+
display: flex;
|
|
48
|
+
gap: 16px;
|
|
49
|
+
margin-bottom: 14px;
|
|
50
|
+
|
|
51
|
+
> div {
|
|
52
|
+
flex: 1;
|
|
53
|
+
min-width: 0;
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
export const Pane = styled.div`
|
|
58
|
+
border: 1px solid ${ props => props.theme.inputs.border };
|
|
59
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
60
|
+
padding: 10px;
|
|
61
|
+
max-height: 320px;
|
|
62
|
+
overflow-y: auto;
|
|
63
|
+
font-size: ${ props => props.theme.size.s };
|
|
64
|
+
|
|
65
|
+
h4 {
|
|
66
|
+
margin: 0 0 8px;
|
|
67
|
+
color: ${ props => props.theme.font.faded };
|
|
68
|
+
font-size: ${ props => props.theme.size.xs };
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
export const Draft = styled.textarea`
|
|
74
|
+
width: 100%;
|
|
75
|
+
min-height: 260px;
|
|
76
|
+
padding: 10px;
|
|
77
|
+
border: 1px solid ${ props => props.theme.inputs.border };
|
|
78
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
79
|
+
font-family: monospace;
|
|
80
|
+
font-size: ${ props => props.theme.size.s };
|
|
81
|
+
margin-bottom: 14px;
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export const Actions = styled.div`
|
|
85
|
+
display: flex;
|
|
86
|
+
justify-content: flex-end;
|
|
87
|
+
gap: 10px;
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
export const Error = styled.p`
|
|
91
|
+
color: ${ props => props.theme.font.error };
|
|
92
|
+
font-size: ${ props => props.theme.size.s };
|
|
93
|
+
margin-bottom: 10px;
|
|
94
|
+
`;
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## 1.30.0
|
|
5
|
+
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- **`Viewer`'s field grid stays at two columns on every screen width.** It
|
|
9
|
+
used to widen to three columns at >=1300px - fine for a form of
|
|
10
|
+
independent, unrelated fields, but a form built for a two-column rhythm
|
|
11
|
+
(one field, a divider row, a related pair, a divider row, ...) reflows
|
|
12
|
+
into three and starts mixing fields from different logical groups onto
|
|
13
|
+
the same row, with nothing to say which group a given row belongs to.
|
|
14
|
+
Affects every Viewer-based form in the app, not just admin-label's.
|
|
15
|
+
|
|
16
|
+
## 1.29.0
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`AiFieldAssist` + the AI review modal.** The one AI-review surface every
|
|
21
|
+
content admin form shares - Generate/Improve/Translate triggers that open
|
|
22
|
+
a modal where a draft is always reviewed (and can be hand-edited or
|
|
23
|
+
revised on a follow-up instruction) before an explicit Insert writes it
|
|
24
|
+
into the calling field. Renders nothing at all when no AI provider is
|
|
25
|
+
enabled for the label - the visibility gate the AI plan calls for, with
|
|
26
|
+
no separate flag to keep in sync.
|
|
27
|
+
- **`Viewer`'s `'component'` field type accepts a function** of the form's
|
|
28
|
+
own `setValue`, not just plain JSX - the only way a component embedded in
|
|
29
|
+
the field list (AiFieldAssist writing a draft into a SIBLING field like
|
|
30
|
+
content_en) can reach state Viewer has always kept internal. Existing
|
|
31
|
+
callers passing plain JSX are unaffected.
|
|
32
|
+
|
|
4
33
|
## 1.28.0
|
|
5
34
|
|
|
6
35
|
### Added
|
package/Viewer/Data.tsx
CHANGED
|
@@ -66,7 +66,11 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
|
|
|
66
66
|
);
|
|
67
67
|
|
|
68
68
|
case 'component':
|
|
69
|
-
return
|
|
69
|
+
return (
|
|
70
|
+
<div className="row-data">
|
|
71
|
+
{ typeof item.value === 'function' ? item.value(onUpdate) : item.value }
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
70
74
|
|
|
71
75
|
case 'display':
|
|
72
76
|
return <Display className="row-data">{ value.string }</Display>;
|
|
@@ -115,7 +119,9 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
|
|
|
115
119
|
return <input type="number" defaultValue={ value.number } { ...refs(item.name, Validate(String(item.rules), t)) } step="0.01" />;
|
|
116
120
|
|
|
117
121
|
case 'link':
|
|
118
|
-
|
|
122
|
+
// The function-value variant only ever arrives on a 'component' row -
|
|
123
|
+
// see ViewData.value's docblock.
|
|
124
|
+
return <Linked id={ id } value={ item.value as (string | number | JSX.Element | undefined) } url={ item.url } />;
|
|
119
125
|
|
|
120
126
|
case 'number':
|
|
121
127
|
return <input type="number" defaultValue={ value.number } { ...refs(item.name, Validate(String(item.rules), t)) } />
|
package/Viewer/styles.ts
CHANGED
|
@@ -28,7 +28,7 @@ export const Container = styled.div`
|
|
|
28
28
|
|
|
29
29
|
/*
|
|
30
30
|
* Edited: Ferjolt Ozuni - Date: 2026-08-07
|
|
31
|
-
* A rich-text editor gets the full row
|
|
31
|
+
* A rich-text editor gets the full row - even at half the width it has
|
|
32
32
|
* less room for its own toolbar than for the text, and every consumer
|
|
33
33
|
* that uses one wants it wide - so this belongs here rather than as a
|
|
34
34
|
* flag each caller has to remember to pass.
|
|
@@ -38,11 +38,17 @@ export const Container = styled.div`
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
/*
|
|
42
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-08
|
|
43
|
+
* DELIBERATELY no wider breakpoint pushing this to three columns - it
|
|
44
|
+
* used to (>=1300px, flex-basis 33%), and the result was a grid with no
|
|
45
|
+
* visual boundary between one logical group's fields and the next: a
|
|
46
|
+
* form built for a two-column rhythm (field, spacer-row, field pair,
|
|
47
|
+
* spacer-row, ...) reflowed into three columns and mixed unrelated
|
|
48
|
+
* fields onto the same row. Every admin-label tab (and every other
|
|
49
|
+
* Viewer-based form in the app) now stays at the same two-column layout
|
|
50
|
+
* from 540px up, at any screen width.
|
|
51
|
+
*/
|
|
46
52
|
`;
|
|
47
53
|
|
|
48
54
|
export const ContainerActions = styled.div`
|
package/Viewer/types.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
import { UseFormSetValue } from 'react-hook-form';
|
|
1
2
|
import { DropdownData } from '@autobusal/providers/types/other';
|
|
2
3
|
|
|
3
4
|
export interface ViewData {
|
|
4
5
|
label?: string
|
|
5
6
|
name?: string
|
|
6
7
|
type?: 'text' | 'select' | 'date' | 'dob' | 'picker' | 'textarea' | 'textarea-html' | 'file' | 'files' | 'email' | 'password' | 'display' | 'component' | 'checkbox' | 'checkbox-list' | 'checkbox-list-all' | 'link' | 'number' | 'float' | 'date-picker'
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-08
|
|
10
|
+
* A 'component' row's value may be a function of the form's own
|
|
11
|
+
* setValue instead of plain JSX - the only way a component embedded in
|
|
12
|
+
* the field list (e.g. AiFieldAssist, writing an AI draft into a
|
|
13
|
+
* SIBLING field like content_en) can reach the Viewer's internal
|
|
14
|
+
* react-hook-form state, which nothing outside Viewer has access to.
|
|
15
|
+
*/
|
|
16
|
+
value?: string | number | JSX.Element | ((onUpdate: UseFormSetValue<any>) => JSX.Element)
|
|
8
17
|
/**
|
|
9
18
|
* Edited: Ferjolt Ozuni - Date: 2026-08-06
|
|
10
19
|
* Leading empty option for a select, for "nothing chosen". Without one a
|
package/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import AiFieldAssist from './AiAssist/AiFieldAssist';
|
|
1
2
|
import ApiDetails from './ApiDetails/ApiDetails';
|
|
2
3
|
import Autocomplete from './Autocomplete/Autocomplete';
|
|
3
4
|
import Avatar from './Avatar';
|
|
@@ -45,6 +46,7 @@ import useUnderConstruction from './UnderConstruction/useUnderConstruction';
|
|
|
45
46
|
import Viewer from './Viewer/Viewer';
|
|
46
47
|
|
|
47
48
|
export {
|
|
49
|
+
AiFieldAssist,
|
|
48
50
|
ApiDetails,
|
|
49
51
|
Autocomplete,
|
|
50
52
|
Avatar,
|