nucleus-core-ts 0.9.792 → 0.9.794
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/MappingEditor.d.ts +18 -3
- package/dist/fe/components/IntegrationsPage/components/MappingEditor.js +30 -1
- package/dist/fe/components/IntegrationsPage/components/MappingsTab.js +38 -6
- package/dist/fe/components/IntegrationsPage/components/formSupport.d.ts +11 -6
- package/dist/fe/components/IntegrationsPage/components/formSupport.js +37 -1
- package/dist/fe/components/IntegrationsPage/components/formSupport.test.js +98 -1
- package/package.json +1 -1
|
@@ -1,15 +1,30 @@
|
|
|
1
|
+
import type { IntegrationEndpoint } from '../types';
|
|
1
2
|
import { type MappingDraft, type MappingTarget } from './mappingDraft';
|
|
2
3
|
export type MappingEditorProps = {
|
|
3
4
|
/** Starting values. Read once — remount with a new `key` to load another. */
|
|
4
5
|
mapping: MappingDraft;
|
|
5
6
|
targets: MappingTarget[];
|
|
6
7
|
targetsLoading: boolean;
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The connection's endpoints. A mapping reads from exactly one, and which one
|
|
10
|
+
* is half of what a mapping IS — so it is chosen here rather than inherited
|
|
11
|
+
* from whatever happens to be selected on another tab.
|
|
12
|
+
*/
|
|
13
|
+
endpoints: IntegrationEndpoint[];
|
|
14
|
+
/**
|
|
15
|
+
* The sample taken in this session and the endpoint it describes.
|
|
16
|
+
*
|
|
17
|
+
* Fresher than the endpoint's stored sample, but only for that one endpoint —
|
|
18
|
+
* offering it against any other would suggest columns that never arrive.
|
|
19
|
+
*/
|
|
20
|
+
freshSample: {
|
|
21
|
+
endpointId: string;
|
|
22
|
+
fields: string[];
|
|
23
|
+
} | null;
|
|
9
24
|
saving: boolean;
|
|
10
25
|
deleting: boolean;
|
|
11
26
|
onSave: (draft: MappingDraft) => void;
|
|
12
27
|
onDelete: (id: string) => void;
|
|
13
28
|
onCancel: () => void;
|
|
14
29
|
};
|
|
15
|
-
export declare function MappingEditor({ mapping, targets, targetsLoading,
|
|
30
|
+
export declare function MappingEditor({ mapping, targets, targetsLoading, endpoints, freshSample, saving, deleting, onSave, onDelete, onCancel, }: MappingEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId, useState } from 'react';
|
|
4
4
|
import { integrationsPageTheme } from '../theme';
|
|
5
5
|
import { FieldMappingRows } from './FieldMappingRows';
|
|
6
|
+
import { endpointLabel, fieldsOfStoredSample } from './formSupport';
|
|
6
7
|
import { MappingWriteModeFields } from './MappingWriteModeFields';
|
|
7
8
|
import { mappingDraftIssues } from './mappingDraft';
|
|
8
9
|
import { Field } from './Primitives';
|
|
@@ -17,7 +18,7 @@ import { CardSkeleton } from './Skeletons';
|
|
|
17
18
|
* Mount it with a `key` of the mapping id so switching rows starts a fresh
|
|
18
19
|
* draft instead of pouring one mapping's edits into another.
|
|
19
20
|
*/ const theme = integrationsPageTheme;
|
|
20
|
-
export function MappingEditor({ mapping, targets, targetsLoading,
|
|
21
|
+
export function MappingEditor({ mapping, targets, targetsLoading, endpoints, freshSample, saving, deleting, onSave, onDelete, onCancel }) {
|
|
21
22
|
const uid = useId();
|
|
22
23
|
const [draft, setDraft] = useState(mapping);
|
|
23
24
|
const patch = (next)=>{
|
|
@@ -45,6 +46,10 @@ export function MappingEditor({ mapping, targets, targetsLoading, sourceFields,
|
|
|
45
46
|
const targetFields = targets.find((option)=>option.entity === draft.targetEntity)?.fields ?? [];
|
|
46
47
|
const issues = mappingDraftIssues(draft);
|
|
47
48
|
const busy = saving || deleting;
|
|
49
|
+
// Follows the endpoint CHOSEN here, so changing it changes the field list
|
|
50
|
+
// rather than leaving the previous endpoint's names on offer.
|
|
51
|
+
const readsFrom = endpoints.find((endpoint)=>endpoint.id === draft.endpointId);
|
|
52
|
+
const sourceFields = freshSample && freshSample.endpointId === draft.endpointId && freshSample.fields.length > 0 ? freshSample.fields : fieldsOfStoredSample(readsFrom?.sampleResponse);
|
|
48
53
|
return /*#__PURE__*/ _jsxs("section", {
|
|
49
54
|
className: theme.card.wrapper,
|
|
50
55
|
children: [
|
|
@@ -58,6 +63,30 @@ export function MappingEditor({ mapping, targets, targetsLoading, sourceFields,
|
|
|
58
63
|
/*#__PURE__*/ _jsxs("div", {
|
|
59
64
|
className: theme.card.body,
|
|
60
65
|
children: [
|
|
66
|
+
/*#__PURE__*/ _jsx(Field, {
|
|
67
|
+
hint: endpoints.length === 0 ? 'This connection has no endpoints yet. Add one on the Endpoints tab.' : readsFrom?.path ?? 'Every mapping reads from exactly one endpoint.',
|
|
68
|
+
htmlFor: `${uid}-endpoint`,
|
|
69
|
+
label: "Reads from",
|
|
70
|
+
children: /*#__PURE__*/ _jsxs("select", {
|
|
71
|
+
className: theme.field.input,
|
|
72
|
+
disabled: busy || endpoints.length === 0,
|
|
73
|
+
id: `${uid}-endpoint`,
|
|
74
|
+
onChange: (event)=>patch({
|
|
75
|
+
endpointId: event.target.value
|
|
76
|
+
}),
|
|
77
|
+
value: draft.endpointId,
|
|
78
|
+
children: [
|
|
79
|
+
/*#__PURE__*/ _jsx("option", {
|
|
80
|
+
value: "",
|
|
81
|
+
children: "Not set"
|
|
82
|
+
}),
|
|
83
|
+
endpoints.map((endpoint)=>/*#__PURE__*/ _jsx("option", {
|
|
84
|
+
value: endpoint.id,
|
|
85
|
+
children: endpointLabel(endpoint)
|
|
86
|
+
}, endpoint.id))
|
|
87
|
+
]
|
|
88
|
+
})
|
|
89
|
+
}),
|
|
61
90
|
/*#__PURE__*/ _jsxs("div", {
|
|
62
91
|
className: "grid grid-cols-1 gap-3 sm:grid-cols-2",
|
|
63
92
|
children: [
|
|
@@ -42,6 +42,32 @@ export function MappingsTab({ actions }) {
|
|
|
42
42
|
useEffect(()=>{
|
|
43
43
|
loadTargets();
|
|
44
44
|
}, []);
|
|
45
|
+
/**
|
|
46
|
+
* The connection's endpoints, when nothing has fetched them yet.
|
|
47
|
+
*
|
|
48
|
+
* `store.endpoints` is filled by the Endpoints tab, so opening a connection
|
|
49
|
+
* and going straight to Mappings met "No endpoints on this connection yet"
|
|
50
|
+
* and a disabled New mapping — about a connection with three of them. The
|
|
51
|
+
* only way through was to visit the other tab first, which nothing said.
|
|
52
|
+
*
|
|
53
|
+
* One page is enough to offer a source and to name one; the Endpoints tab
|
|
54
|
+
* remains the place that pages through them all.
|
|
55
|
+
*/ const loadEndpointsIfMissing = useEffectEvent(()=>{
|
|
56
|
+
if (sourceId === '' || store.endpoints.length > 0) return;
|
|
57
|
+
actions.listEndpoints(sourceId, {
|
|
58
|
+
page: 1,
|
|
59
|
+
limit: 50
|
|
60
|
+
}).then((result)=>{
|
|
61
|
+
store.setEndpoints(result.items, result.meta?.totalItems ?? result.items.length);
|
|
62
|
+
}).catch((error)=>{
|
|
63
|
+
store.setError(messageOf(error));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
useEffect(()=>{
|
|
67
|
+
loadEndpointsIfMissing();
|
|
68
|
+
}, [
|
|
69
|
+
sourceId
|
|
70
|
+
]);
|
|
45
71
|
const startNew = ()=>{
|
|
46
72
|
setSelected(null);
|
|
47
73
|
setCreating(true);
|
|
@@ -87,9 +113,14 @@ export function MappingsTab({ actions }) {
|
|
|
87
113
|
});
|
|
88
114
|
};
|
|
89
115
|
const draft = creating ? blankMappingDraft(newMappingEndpointId) : selected ? toMappingDraft(selected) : null;
|
|
90
|
-
// A sample describes ONE endpoint
|
|
91
|
-
//
|
|
92
|
-
|
|
116
|
+
// A sample describes ONE endpoint, so it travels with the endpoint it belongs
|
|
117
|
+
// to and the editor matches it against the mapping's own choice. The editor
|
|
118
|
+
// falls back to the endpoint's STORED sample — which is why the sample is
|
|
119
|
+
// persisted at all, and what a connection revisited tomorrow has.
|
|
120
|
+
const freshSample = store.selectedEndpointId && store.sample ? {
|
|
121
|
+
endpointId: store.selectedEndpointId,
|
|
122
|
+
fields: store.sample.fields ?? []
|
|
123
|
+
} : null;
|
|
93
124
|
return /*#__PURE__*/ _jsxs("section", {
|
|
94
125
|
className: "flex flex-col gap-4",
|
|
95
126
|
children: [
|
|
@@ -113,9 +144,9 @@ export function MappingsTab({ actions }) {
|
|
|
113
144
|
]
|
|
114
145
|
}),
|
|
115
146
|
newMappingEndpointId === '' ? /*#__PURE__*/ _jsx(Notice, {
|
|
116
|
-
title: "
|
|
147
|
+
title: "No endpoints to read from",
|
|
117
148
|
tone: "info",
|
|
118
|
-
children: "
|
|
149
|
+
children: "A mapping turns one endpoint's response into rows, so there has to be an endpoint first. Add one on the Endpoints tab and it becomes selectable here."
|
|
119
150
|
}) : null,
|
|
120
151
|
/*#__PURE__*/ _jsxs("div", {
|
|
121
152
|
className: theme.layout.split,
|
|
@@ -134,12 +165,13 @@ export function MappingsTab({ actions }) {
|
|
|
134
165
|
className: theme.layout.main,
|
|
135
166
|
children: draft ? /*#__PURE__*/ _jsx(MappingEditor, {
|
|
136
167
|
deleting: deleting,
|
|
168
|
+
endpoints: store.endpoints,
|
|
169
|
+
freshSample: freshSample,
|
|
137
170
|
mapping: draft,
|
|
138
171
|
onCancel: closeEditor,
|
|
139
172
|
onDelete: remove,
|
|
140
173
|
onSave: save,
|
|
141
174
|
saving: saving,
|
|
142
|
-
sourceFields: sourceFields,
|
|
143
175
|
targets: targets,
|
|
144
176
|
targetsLoading: targetsLoading
|
|
145
177
|
}, draft.id ?? 'new-mapping') : /*#__PURE__*/ _jsx("p", {
|
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { IntegrationEndpoint } from '../types';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* The field names an endpoint's STORED sample reports.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* A sample is persisted on the endpoint precisely so the field list survives a
|
|
9
|
+
* reload, but the mapping editor was reading only the sample taken in this
|
|
10
|
+
* browser session. Come back to a connection tomorrow and it said "sample the
|
|
11
|
+
* endpoint first" about an endpoint it had already sampled and saved — so the
|
|
12
|
+
* source names had to be typed from memory, which is the one thing the sample
|
|
13
|
+
* exists to prevent.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the engine's `flattenKeys` rather than calling it: that lives in the
|
|
16
|
+
* server bundle, which this kit cannot import.
|
|
13
17
|
*/
|
|
18
|
+
export declare function fieldsOfStoredSample(sample: IntegrationEndpoint['sampleResponse']): string[];
|
|
14
19
|
export declare function endpointLabel(endpoint: IntegrationEndpoint): string;
|
|
15
20
|
/**
|
|
16
21
|
* The ids a control should point `aria-describedby` at.
|
|
@@ -8,7 +8,43 @@
|
|
|
8
8
|
* three identical options and no way to tell which one supplied the value.
|
|
9
9
|
* The name is what distinguishes them; the path is what distinguishes them when
|
|
10
10
|
* nobody typed a name, since it is the one field that cannot be blank.
|
|
11
|
-
*/
|
|
11
|
+
*/ /** Same caps as the engine's `flattenKeys`: this feeds a dropdown, not an analysis. */ const MAX_FIELDS_LISTED = 200;
|
|
12
|
+
const MAX_FIELD_DEPTH = 4;
|
|
13
|
+
function walkKeys(value, prefix, out, depth) {
|
|
14
|
+
if (out.length > MAX_FIELDS_LISTED || depth > MAX_FIELD_DEPTH) return out;
|
|
15
|
+
if (value == null || typeof value !== 'object') return out;
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
if (prefix) out.push(prefix);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
for (const [key, child] of Object.entries(value)){
|
|
21
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
22
|
+
if (child != null && typeof child === 'object' && !Array.isArray(child)) {
|
|
23
|
+
walkKeys(child, path, out, depth + 1);
|
|
24
|
+
} else {
|
|
25
|
+
out.push(path);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The field names an endpoint's STORED sample reports.
|
|
32
|
+
*
|
|
33
|
+
* A sample is persisted on the endpoint precisely so the field list survives a
|
|
34
|
+
* reload, but the mapping editor was reading only the sample taken in this
|
|
35
|
+
* browser session. Come back to a connection tomorrow and it said "sample the
|
|
36
|
+
* endpoint first" about an endpoint it had already sampled and saved — so the
|
|
37
|
+
* source names had to be typed from memory, which is the one thing the sample
|
|
38
|
+
* exists to prevent.
|
|
39
|
+
*
|
|
40
|
+
* Mirrors the engine's `flattenKeys` rather than calling it: that lives in the
|
|
41
|
+
* server bundle, which this kit cannot import.
|
|
42
|
+
*/ export function fieldsOfStoredSample(sample) {
|
|
43
|
+
const first = sample?.records?.[0];
|
|
44
|
+
if (!first || typeof first !== 'object') return [];
|
|
45
|
+
return walkKeys(first, '', [], 0);
|
|
46
|
+
}
|
|
47
|
+
export function endpointLabel(endpoint) {
|
|
12
48
|
const name = endpoint.name?.trim();
|
|
13
49
|
if (name) return name;
|
|
14
50
|
const path = endpoint.path?.trim();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { describedBy, endpointLabel, errorText } from './formSupport';
|
|
2
|
+
import { describedBy, endpointLabel, errorText, fieldsOfStoredSample } from './formSupport';
|
|
3
3
|
const endpoint = (over)=>({
|
|
4
4
|
id: 'e1',
|
|
5
5
|
sourceId: 's1',
|
|
@@ -38,6 +38,103 @@ const endpoint = (over)=>({
|
|
|
38
38
|
}))).toBe('corporate-portal');
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
+
/**
|
|
42
|
+
* The sample is persisted on the endpoint so the field list survives a reload.
|
|
43
|
+
* The mapping editor was reading only the sample taken in the current browser
|
|
44
|
+
* session, so a connection revisited later said "sample the endpoint first"
|
|
45
|
+
* about an endpoint it had already sampled and stored.
|
|
46
|
+
*/ describe('fieldsOfStoredSample', ()=>{
|
|
47
|
+
test('reads the field names off the stored first record', ()=>{
|
|
48
|
+
const fields = fieldsOfStoredSample({
|
|
49
|
+
records: [
|
|
50
|
+
{
|
|
51
|
+
companyCode: 'TAT',
|
|
52
|
+
companyName: 'TATMETAL ÇELİK SAN. VE TİC.A.Ş.',
|
|
53
|
+
organizationCode: '010',
|
|
54
|
+
organizationName: 'YÖNETİM KURULU',
|
|
55
|
+
parentOrganizationCode: '*'
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
expect(fields).toEqual([
|
|
60
|
+
'companyCode',
|
|
61
|
+
'companyName',
|
|
62
|
+
'organizationCode',
|
|
63
|
+
'organizationName',
|
|
64
|
+
'parentOrganizationCode'
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
test('nested objects become dot-paths, the way the engine reports them', ()=>{
|
|
68
|
+
const fields = fieldsOfStoredSample({
|
|
69
|
+
records: [
|
|
70
|
+
{
|
|
71
|
+
a: 1,
|
|
72
|
+
b: {
|
|
73
|
+
c: 2,
|
|
74
|
+
d: {
|
|
75
|
+
e: 3
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
expect(fields).toEqual([
|
|
82
|
+
'a',
|
|
83
|
+
'b.c',
|
|
84
|
+
'b.d.e'
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
test('an array is a leaf — its contents are a shape, not a field name', ()=>{
|
|
88
|
+
const fields = fieldsOfStoredSample({
|
|
89
|
+
records: [
|
|
90
|
+
{
|
|
91
|
+
organizationName: 'GENEL MÜDÜRLÜK',
|
|
92
|
+
subOrganization: [
|
|
93
|
+
{
|
|
94
|
+
x: 1
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
expect(fields).toEqual([
|
|
101
|
+
'organizationName',
|
|
102
|
+
'subOrganization'
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
test('nothing stored yields nothing, rather than throwing', ()=>{
|
|
106
|
+
expect(fieldsOfStoredSample(null)).toEqual([]);
|
|
107
|
+
expect(fieldsOfStoredSample(undefined)).toEqual([]);
|
|
108
|
+
expect(fieldsOfStoredSample({
|
|
109
|
+
records: []
|
|
110
|
+
})).toEqual([]);
|
|
111
|
+
expect(fieldsOfStoredSample({
|
|
112
|
+
records: [
|
|
113
|
+
'not an object'
|
|
114
|
+
]
|
|
115
|
+
})).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
test('depth is capped, so a deep document cannot flood the picker', ()=>{
|
|
118
|
+
const deep = {
|
|
119
|
+
l1: {
|
|
120
|
+
l2: {
|
|
121
|
+
l3: {
|
|
122
|
+
l4: {
|
|
123
|
+
l5: {
|
|
124
|
+
l6: 'too far'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
expect(fieldsOfStoredSample({
|
|
132
|
+
records: [
|
|
133
|
+
deep
|
|
134
|
+
]
|
|
135
|
+
})).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
41
138
|
describe('describedBy', ()=>{
|
|
42
139
|
test('names only the ids that were rendered', ()=>{
|
|
43
140
|
expect(describedBy('f', true, true)).toBe('f-hint f-error');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.794",
|
|
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",
|