nucleus-core-ts 0.9.815 → 0.9.817
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/dist/fe/components/IntegrationsPage/components/IntegrationsPage.d.ts +1 -1
- package/dist/fe/components/IntegrationsPage/components/IntegrationsPage.js +33 -1
- package/dist/fe/components/IntegrationsPage/components/MappingEditor.d.ts +15 -1
- package/dist/fe/components/IntegrationsPage/components/MappingEditor.js +8 -1
- package/dist/fe/components/IntegrationsPage/components/MappingsTab.js +74 -2
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.d.ts +7 -2
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.js +7 -2
- package/dist/fe/components/IntegrationsPage/store/index.js +4 -0
- package/dist/fe/components/IntegrationsPage/store/state.d.ts +12 -0
- package/dist/fe/components/IntegrationsPage/store/state.js +2 -0
- package/dist/fe/components/IntegrationsPage/types/actions.d.ts +16 -0
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { IntegrationsPageProps } from '../types';
|
|
2
|
-
export declare function IntegrationsPage({ title, subtitle, className, actions, labels: labelsOverride, onSubscribeProgress, }: IntegrationsPageProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function IntegrationsPage({ title, subtitle, className, actions, labels: labelsOverride, focusTargetEntity, focusStep, onSubscribeProgress, }: IntegrationsPageProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -25,7 +25,7 @@ import { RunsTab } from './RunsTab';
|
|
|
25
25
|
* page from the server rather than being handed a list to slice.
|
|
26
26
|
*/ gsap.registerPlugin(useGSAP);
|
|
27
27
|
const theme = integrationsPageTheme;
|
|
28
|
-
export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect an external service, describe the data you want and import it on your terms.', className, actions, labels: labelsOverride, onSubscribeProgress }) {
|
|
28
|
+
export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect an external service, describe the data you want and import it on your terms.', className, actions, labels: labelsOverride, focusTargetEntity, focusStep, onSubscribeProgress }) {
|
|
29
29
|
const store = useIntegrationsStore();
|
|
30
30
|
// Resolved here rather than read from the context, because this component is
|
|
31
31
|
// the one that PROVIDES it — and passed down so a control four levels deep
|
|
@@ -83,6 +83,38 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
|
|
|
83
83
|
store.selectedSourceId,
|
|
84
84
|
sourcesReloadToken
|
|
85
85
|
]);
|
|
86
|
+
/**
|
|
87
|
+
* Somebody arrived from the table they want filled.
|
|
88
|
+
*
|
|
89
|
+
* They have already said which table; being shown a connection chooser and
|
|
90
|
+
* four mappings is asking the same question again. So the entity is handed to
|
|
91
|
+
* the mappings step, and — when there is only one connection to be meant — it
|
|
92
|
+
* is chosen for them. With several, the choice is real and stays theirs.
|
|
93
|
+
*/ const takeFocus = useEffectEvent(()=>{
|
|
94
|
+
if (!focusTargetEntity) return;
|
|
95
|
+
store.setFocusTargetEntity(focusTargetEntity, focusStep ?? 'mapping');
|
|
96
|
+
});
|
|
97
|
+
useEffect(()=>{
|
|
98
|
+
takeFocus();
|
|
99
|
+
}, [
|
|
100
|
+
focusTargetEntity,
|
|
101
|
+
focusStep
|
|
102
|
+
]);
|
|
103
|
+
const followFocus = useEffectEvent(()=>{
|
|
104
|
+
if (!store.focusTargetEntity) return;
|
|
105
|
+
if (!store.selectedSourceId) {
|
|
106
|
+
if (store.sources.length === 1 && store.sources[0]) store.selectSource(store.sources[0].id);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (store.tab !== 'mappings') store.setTab('mappings');
|
|
110
|
+
});
|
|
111
|
+
useEffect(()=>{
|
|
112
|
+
followFocus();
|
|
113
|
+
}, [
|
|
114
|
+
store.focusTargetEntity,
|
|
115
|
+
store.selectedSourceId,
|
|
116
|
+
store.sources.length
|
|
117
|
+
]);
|
|
86
118
|
// One measured entrance. Anything longer makes an operator wait to read counts
|
|
87
119
|
// they came here to check.
|
|
88
120
|
useGSAP(()=>{
|
|
@@ -26,5 +26,19 @@ export type MappingEditorProps = {
|
|
|
26
26
|
onSave: (draft: MappingDraft) => void;
|
|
27
27
|
onDelete: (id: string) => void;
|
|
28
28
|
onCancel: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Go and import with this mapping.
|
|
31
|
+
*
|
|
32
|
+
* The import lives on its own step, and it should: it is the destructive one,
|
|
33
|
+
* and it wants the whole width for a preview. But somebody who has just
|
|
34
|
+
* finished describing a mapping wants to RUN it, and leaving them to find the
|
|
35
|
+
* step themselves is the panel forgetting what they were doing. Absent for an
|
|
36
|
+
* unsaved mapping, because there is nothing to run yet.
|
|
37
|
+
*
|
|
38
|
+
* The DRAFT is handed over, not the id: what is on screen is what the operator
|
|
39
|
+
* means to run, so edits they have not saved yet are saved on the way rather
|
|
40
|
+
* than quietly ignored by an import of the older version.
|
|
41
|
+
*/
|
|
42
|
+
onRunImport?: (draft: MappingDraft) => void;
|
|
29
43
|
};
|
|
30
|
-
export declare function MappingEditor({ mapping, targets, targetsLoading, endpoints, freshSample, saving, deleting, onSave, onDelete, onCancel, }: MappingEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
44
|
+
export declare function MappingEditor({ mapping, targets, targetsLoading, endpoints, freshSample, saving, deleting, onSave, onDelete, onCancel, onRunImport, }: MappingEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -21,7 +21,7 @@ import { CardSkeleton } from './Skeletons';
|
|
|
21
21
|
* Mount it with a `key` of the mapping id so switching rows starts a fresh
|
|
22
22
|
* draft instead of pouring one mapping's edits into another.
|
|
23
23
|
*/ const theme = integrationsPageTheme;
|
|
24
|
-
export function MappingEditor({ mapping, targets, targetsLoading, endpoints, freshSample, saving, deleting, onSave, onDelete, onCancel }) {
|
|
24
|
+
export function MappingEditor({ mapping, targets, targetsLoading, endpoints, freshSample, saving, deleting, onSave, onDelete, onCancel, onRunImport }) {
|
|
25
25
|
const labels = useLabels();
|
|
26
26
|
const uid = useId();
|
|
27
27
|
const [draft, setDraft] = useState(mapping);
|
|
@@ -236,6 +236,13 @@ export function MappingEditor({ mapping, targets, targetsLoading, endpoints, fre
|
|
|
236
236
|
type: "button",
|
|
237
237
|
children: "Cancel"
|
|
238
238
|
}),
|
|
239
|
+
draft.id && onRunImport ? /*#__PURE__*/ _jsx("button", {
|
|
240
|
+
className: theme.button.secondary,
|
|
241
|
+
disabled: busy,
|
|
242
|
+
onClick: ()=>onRunImport(draft),
|
|
243
|
+
type: "button",
|
|
244
|
+
children: "Preview and import"
|
|
245
|
+
}) : null,
|
|
239
246
|
/*#__PURE__*/ _jsx("button", {
|
|
240
247
|
className: theme.button.primary,
|
|
241
248
|
disabled: busy || issues.length > 0,
|
|
@@ -29,6 +29,7 @@ export function MappingsTab({ actions }) {
|
|
|
29
29
|
const [saving, setSaving] = useState(false);
|
|
30
30
|
const [deleting, setDeleting] = useState(false);
|
|
31
31
|
const [reloadToken, setReloadToken] = useState(0);
|
|
32
|
+
/** Set only when a new mapping is being started for a table named on arrival. */ const [pendingTarget, setPendingTarget] = useState('');
|
|
32
33
|
const sourceId = store.selectedSourceId ?? '';
|
|
33
34
|
const newMappingEndpointId = store.selectedEndpointId ?? store.endpoints[0]?.id ?? '';
|
|
34
35
|
const loadTargets = useEffectEvent(()=>{
|
|
@@ -72,6 +73,7 @@ export function MappingsTab({ actions }) {
|
|
|
72
73
|
]);
|
|
73
74
|
const startNew = ()=>{
|
|
74
75
|
setSelected(null);
|
|
76
|
+
setPendingTarget('');
|
|
75
77
|
setCreating(true);
|
|
76
78
|
store.selectMapping(null);
|
|
77
79
|
};
|
|
@@ -80,12 +82,61 @@ export function MappingsTab({ actions }) {
|
|
|
80
82
|
setSelected(mapping);
|
|
81
83
|
store.selectMapping(mapping.id);
|
|
82
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Opens the mapping for the table somebody arrived asking about.
|
|
87
|
+
*
|
|
88
|
+
* Asked of the SERVER by target entity rather than searched in the page this
|
|
89
|
+
* tab happens to be holding: the list is paged, so the mapping being looked
|
|
90
|
+
* for is frequently not in it. If there is none yet, a new one is started
|
|
91
|
+
* already aimed at that table — which is the other half of the answer.
|
|
92
|
+
*
|
|
93
|
+
* Consumed once. Left set, it would drag the selection back every time the
|
|
94
|
+
* operator clicked a different mapping.
|
|
95
|
+
*/ const followFocus = useEffectEvent(()=>{
|
|
96
|
+
const entity = store.focusTargetEntity;
|
|
97
|
+
if (!entity || sourceId === '') return;
|
|
98
|
+
const step = store.focusStep;
|
|
99
|
+
store.setFocusTargetEntity(null);
|
|
100
|
+
actions.listMappings(sourceId, {
|
|
101
|
+
page: 1,
|
|
102
|
+
limit: 1,
|
|
103
|
+
filters: [
|
|
104
|
+
{
|
|
105
|
+
field: 'targetEntity',
|
|
106
|
+
operator: 'eq',
|
|
107
|
+
value: entity
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}).then((result)=>{
|
|
111
|
+
const found = result.items[0];
|
|
112
|
+
if (found) {
|
|
113
|
+
openMapping(found);
|
|
114
|
+
// "Fill this table now" is a different errand from "describe how it is
|
|
115
|
+
// filled", and both are offered from the table itself.
|
|
116
|
+
if (step === 'import') store.setTab('runs');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
setSelected(null);
|
|
120
|
+
setPendingTarget(entity);
|
|
121
|
+
setCreating(true);
|
|
122
|
+
store.selectMapping(null);
|
|
123
|
+
}).catch((error)=>{
|
|
124
|
+
store.setError(messageOf(error));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
useEffect(()=>{
|
|
128
|
+
followFocus();
|
|
129
|
+
}, [
|
|
130
|
+
store.focusTargetEntity,
|
|
131
|
+
sourceId
|
|
132
|
+
]);
|
|
83
133
|
const closeEditor = ()=>{
|
|
84
134
|
setCreating(false);
|
|
135
|
+
setPendingTarget('');
|
|
85
136
|
setSelected(null);
|
|
86
137
|
store.selectMapping(null);
|
|
87
138
|
};
|
|
88
|
-
const save = (draft)=>{
|
|
139
|
+
const save = (draft, then)=>{
|
|
89
140
|
setSaving(true);
|
|
90
141
|
store.setError(null);
|
|
91
142
|
actions.saveMapping(draft).then((saved)=>{
|
|
@@ -95,11 +146,31 @@ export function MappingsTab({ actions }) {
|
|
|
95
146
|
setSelected(saved);
|
|
96
147
|
store.selectMapping(saved.id);
|
|
97
148
|
setReloadToken((token)=>token + 1);
|
|
149
|
+
then?.(saved);
|
|
98
150
|
}).catch((error)=>{
|
|
99
151
|
setSaving(false);
|
|
100
152
|
store.setError(messageOf(error));
|
|
101
153
|
});
|
|
102
154
|
};
|
|
155
|
+
/**
|
|
156
|
+
* Take this mapping to the import step.
|
|
157
|
+
*
|
|
158
|
+
* Saved first when it differs from what is stored: the operator is looking at
|
|
159
|
+
* the version they mean to run, and importing the older one instead — with no
|
|
160
|
+
* sign that it happened — is the worst kind of quiet.
|
|
161
|
+
*/ const runImport = (draft)=>{
|
|
162
|
+
const stored = selected ? toMappingDraft(selected) : null;
|
|
163
|
+
const dirty = !stored || JSON.stringify(stored) !== JSON.stringify(draft);
|
|
164
|
+
const go = (id)=>{
|
|
165
|
+
store.selectMapping(id);
|
|
166
|
+
store.setTab('runs');
|
|
167
|
+
};
|
|
168
|
+
if (!dirty && draft.id) {
|
|
169
|
+
go(draft.id);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
save(draft, (saved)=>go(saved.id));
|
|
173
|
+
};
|
|
103
174
|
const remove = (id)=>{
|
|
104
175
|
if (id === '') return;
|
|
105
176
|
setDeleting(true);
|
|
@@ -114,7 +185,7 @@ export function MappingsTab({ actions }) {
|
|
|
114
185
|
store.setError(messageOf(error));
|
|
115
186
|
});
|
|
116
187
|
};
|
|
117
|
-
const draft = creating ? blankMappingDraft(newMappingEndpointId) : selected ? toMappingDraft(selected) : null;
|
|
188
|
+
const draft = creating ? blankMappingDraft(newMappingEndpointId, pendingTarget) : selected ? toMappingDraft(selected) : null;
|
|
118
189
|
// A sample describes ONE endpoint, so it travels with the endpoint it belongs
|
|
119
190
|
// to and the editor matches it against the mapping's own choice. The editor
|
|
120
191
|
// falls back to the endpoint's STORED sample — which is why the sample is
|
|
@@ -166,6 +237,7 @@ export function MappingsTab({ actions }) {
|
|
|
166
237
|
mapping: draft,
|
|
167
238
|
onCancel: closeEditor,
|
|
168
239
|
onDelete: remove,
|
|
240
|
+
onRunImport: runImport,
|
|
169
241
|
onSave: save,
|
|
170
242
|
saving: saving,
|
|
171
243
|
targets: targets,
|
|
@@ -31,8 +31,13 @@ export type MappingDraft = {
|
|
|
31
31
|
passwordTargetColumn?: string | null;
|
|
32
32
|
enabled?: boolean;
|
|
33
33
|
};
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Upsert and ignore: the two choices that cannot lose a row by themselves.
|
|
36
|
+
*
|
|
37
|
+
* `targetEntity` is filled in when the operator arrived from the table itself —
|
|
38
|
+
* they have already said which one, and asking again is the panel forgetting.
|
|
39
|
+
*/
|
|
40
|
+
export declare function blankMappingDraft(endpointId: string, targetEntity?: string): MappingDraft;
|
|
36
41
|
/** Copies field by field so a column the server added cannot ride along unseen. */
|
|
37
42
|
export declare function toMappingDraft(mapping: IntegrationMapping): MappingDraft;
|
|
38
43
|
/**
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { asArray } from './jsonArray';
|
|
2
2
|
import { missingRecordFieldsIncomplete } from './MappingWriteModeFields';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Upsert and ignore: the two choices that cannot lose a row by themselves.
|
|
5
|
+
*
|
|
6
|
+
* `targetEntity` is filled in when the operator arrived from the table itself —
|
|
7
|
+
* they have already said which one, and asking again is the panel forgetting.
|
|
8
|
+
*/ export function blankMappingDraft(endpointId, targetEntity = '') {
|
|
4
9
|
return {
|
|
5
10
|
endpointId,
|
|
6
11
|
name: '',
|
|
7
|
-
targetEntity
|
|
12
|
+
targetEntity,
|
|
8
13
|
fieldMappings: [],
|
|
9
14
|
referenceChecks: [],
|
|
10
15
|
writeMode: 'upsert',
|
|
@@ -41,6 +41,10 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
|
|
|
41
41
|
store.plan = null;
|
|
42
42
|
store.error = null;
|
|
43
43
|
}),
|
|
44
|
+
setFocusTargetEntity: (store)=>(entity, step)=>{
|
|
45
|
+
store.focusTargetEntity = entity;
|
|
46
|
+
if (step) store.focusStep = step;
|
|
47
|
+
},
|
|
44
48
|
setTab: (store)=>(tab)=>{
|
|
45
49
|
store.tab = tab;
|
|
46
50
|
},
|
|
@@ -20,6 +20,17 @@ export type State = {
|
|
|
20
20
|
sourcesLoading: boolean;
|
|
21
21
|
selectedSourceId: string | null;
|
|
22
22
|
tab: ConnectionTab;
|
|
23
|
+
/**
|
|
24
|
+
* The table somebody arrived here asking about.
|
|
25
|
+
*
|
|
26
|
+
* A management screen links straight in — "fill this table from a source" —
|
|
27
|
+
* and the panel is no use if it then asks which of four mappings was meant.
|
|
28
|
+
* Consumed once, on arrival: it steers the first render and is cleared, so a
|
|
29
|
+
* later click on another mapping is not dragged back.
|
|
30
|
+
*/
|
|
31
|
+
focusTargetEntity: string | null;
|
|
32
|
+
/** Whether that caller wanted to DESCRIBE the import or RUN it. */
|
|
33
|
+
focusStep: 'mapping' | 'import';
|
|
23
34
|
endpoints: IntegrationEndpoint[];
|
|
24
35
|
endpointsTotal: number;
|
|
25
36
|
endpointsPage: number;
|
|
@@ -56,6 +67,7 @@ export type Actions = {
|
|
|
56
67
|
setSourcesLoading: (loading: boolean) => void;
|
|
57
68
|
selectSource: (id: string | null) => void;
|
|
58
69
|
setTab: (tab: ConnectionTab) => void;
|
|
70
|
+
setFocusTargetEntity: (entity: string | null, step?: 'mapping' | 'import') => void;
|
|
59
71
|
setEndpoints: (items: IntegrationEndpoint[], total: number) => void;
|
|
60
72
|
/**
|
|
61
73
|
* Just the count, without the rows.
|
|
@@ -98,6 +98,22 @@ export type IntegrationsPageProps = {
|
|
|
98
98
|
subtitle?: string;
|
|
99
99
|
className?: string;
|
|
100
100
|
actions: IntegrationsActions;
|
|
101
|
+
/**
|
|
102
|
+
* The table a caller arrived asking about, e.g. from "fill this from a
|
|
103
|
+
* source" on a management screen.
|
|
104
|
+
*
|
|
105
|
+
* The panel opens on that table's mapping — or on a new one already aimed at
|
|
106
|
+
* it — instead of on a chooser the caller has already answered.
|
|
107
|
+
*/
|
|
108
|
+
focusTargetEntity?: string;
|
|
109
|
+
/**
|
|
110
|
+
* What they came to do with it.
|
|
111
|
+
*
|
|
112
|
+
* "Describe how this table is filled" and "fill it now" are two different
|
|
113
|
+
* errands, and a management screen offers both. Landing on the wrong one
|
|
114
|
+
* means finding the right step by hand — which is what the button was for.
|
|
115
|
+
*/
|
|
116
|
+
focusStep?: 'mapping' | 'import';
|
|
101
117
|
/**
|
|
102
118
|
* The panel's own words, in the app's language.
|
|
103
119
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.817",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|