nucleus-core-ts 0.9.804 → 0.9.806

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.
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useEffectEvent, useState } from 'react';
4
4
  import { useIntegrationsStore } from '../store';
5
+ import { useLabels } from '../text/context';
5
6
  import { integrationsPageTheme } from '../theme';
6
7
  import { ConnectionAuthFields } from './ConnectionAuthFields';
7
8
  import { ConnectionBasicFields } from './ConnectionBasicFields';
@@ -17,6 +18,7 @@ import { CardSkeleton } from './Skeletons';
17
18
  * of what was stored and what is being typed.
18
19
  */ const theme = integrationsPageTheme;
19
20
  export function ConnectionForm({ actions, source, loading = false, onSaved }) {
21
+ const labels = useLabels();
20
22
  const store = useIntegrationsStore();
21
23
  const [draft, setDraft] = useState(()=>source ? draftFromSource(source) : emptyConnectionDraft);
22
24
  const [errors, setErrors] = useState({});
@@ -73,11 +75,11 @@ export function ConnectionForm({ actions, source, loading = false, onSaved }) {
73
75
  children: [
74
76
  /*#__PURE__*/ _jsx("h2", {
75
77
  className: theme.card.title,
76
- children: source ? 'Edit connection' : 'New connection'
78
+ children: source ? labels.connections.editTitle : labels.connections.newTitle
77
79
  }),
78
80
  source ? /*#__PURE__*/ _jsx(Badge, {
79
81
  tone: source.enabled === false ? 'neutral' : 'success',
80
- children: source.enabled === false ? 'Disabled' : 'Enabled'
82
+ children: source.enabled === false ? labels.connections.disabledBadge : labels.connections.enabledBadge
81
83
  }) : null
82
84
  ]
83
85
  }),
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useId } from 'react';
3
4
  import { cn } from '../../../utils/cn';
4
5
  import { useLabels } from '../text/context';
5
6
  import { integrationsPageTheme } from '../theme';
@@ -20,6 +21,14 @@ const METHODS = [
20
21
  ];
21
22
  export function EndpointFormFields({ draft, disabled, errors, onChange }) {
22
23
  const labels = useLabels();
24
+ /**
25
+ * Unique per instance.
26
+ *
27
+ * These ids were literals — `endpoint-tag`, `endpoint-name` — which is fine
28
+ * for one form on a page and wrong the moment there are two: every `<label
29
+ * htmlFor>` binds to whichever rendered first, so clicking a label focuses the
30
+ * OTHER record's field. `useId` makes that structurally impossible.
31
+ */ const uid = useId();
23
32
  return /*#__PURE__*/ _jsxs(_Fragment, {
24
33
  children: [
25
34
  /*#__PURE__*/ _jsxs("div", {
@@ -27,13 +36,13 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
27
36
  children: [
28
37
  /*#__PURE__*/ _jsx(Field, {
29
38
  hint: labels.endpoints.tagHint,
30
- htmlFor: "endpoint-tag",
39
+ htmlFor: `${uid}-tag`,
31
40
  label: labels.endpoints.tag,
32
41
  children: /*#__PURE__*/ _jsx("input", {
33
- "aria-describedby": "endpoint-tag-hint",
42
+ "aria-describedby": `${uid}-tag-hint`,
34
43
  className: theme.field.input,
35
44
  disabled: disabled,
36
- id: "endpoint-tag",
45
+ id: `${uid}-tag`,
37
46
  onChange: (event)=>onChange({
38
47
  tag: event.target.value
39
48
  }),
@@ -43,12 +52,12 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
43
52
  })
44
53
  }),
45
54
  /*#__PURE__*/ _jsx(Field, {
46
- htmlFor: "endpoint-name",
55
+ htmlFor: `${uid}-name`,
47
56
  label: labels.endpoints.name,
48
57
  children: /*#__PURE__*/ _jsx("input", {
49
58
  className: theme.field.input,
50
59
  disabled: disabled,
51
- id: "endpoint-name",
60
+ id: `${uid}-name`,
52
61
  onChange: (event)=>onChange({
53
62
  name: event.target.value
54
63
  }),
@@ -65,12 +74,12 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
65
74
  /*#__PURE__*/ _jsx("div", {
66
75
  className: "sm:w-32",
67
76
  children: /*#__PURE__*/ _jsx(Field, {
68
- htmlFor: "endpoint-method",
77
+ htmlFor: `${uid}-method`,
69
78
  label: labels.endpoints.method,
70
79
  children: /*#__PURE__*/ _jsx("select", {
71
80
  className: theme.field.input,
72
81
  disabled: disabled,
73
- id: "endpoint-method",
82
+ id: `${uid}-method`,
74
83
  onChange: (event)=>{
75
84
  for (const method of METHODS){
76
85
  if (method === event.target.value) onChange({
@@ -90,13 +99,13 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
90
99
  className: "min-w-0 flex-1",
91
100
  children: /*#__PURE__*/ _jsx(Field, {
92
101
  hint: labels.endpoints.pathHint,
93
- htmlFor: "endpoint-path",
102
+ htmlFor: `${uid}-path`,
94
103
  label: labels.endpoints.path,
95
104
  children: /*#__PURE__*/ _jsx("input", {
96
- "aria-describedby": "endpoint-path-hint",
105
+ "aria-describedby": `${uid}-path-hint`,
97
106
  className: theme.field.input,
98
107
  disabled: disabled,
99
- id: "endpoint-path",
108
+ id: `${uid}-path`,
100
109
  onChange: (event)=>onChange({
101
110
  path: event.target.value
102
111
  }),
@@ -110,13 +119,13 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
110
119
  }),
111
120
  /*#__PURE__*/ _jsx(Field, {
112
121
  hint: labels.endpoints.rootPathHint,
113
- htmlFor: "endpoint-root",
122
+ htmlFor: `${uid}-root`,
114
123
  label: labels.endpoints.rootPath,
115
124
  children: /*#__PURE__*/ _jsx("input", {
116
- "aria-describedby": "endpoint-root-hint",
125
+ "aria-describedby": `${uid}-root-hint`,
117
126
  className: theme.field.input,
118
127
  disabled: disabled,
119
- id: "endpoint-root",
128
+ id: `${uid}-root`,
120
129
  onChange: (event)=>onChange({
121
130
  responseRootPath: event.target.value
122
131
  }),
@@ -128,13 +137,13 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
128
137
  /*#__PURE__*/ _jsx(Field, {
129
138
  error: errors?.queryParams,
130
139
  hint: labels.endpoints.queryParamsHint,
131
- htmlFor: "endpoint-query",
140
+ htmlFor: `${uid}-query`,
132
141
  label: labels.endpoints.queryParams,
133
142
  children: /*#__PURE__*/ _jsx("textarea", {
134
- "aria-describedby": describedBy('endpoint-query', true, Boolean(errors?.queryParams)),
143
+ "aria-describedby": describedBy(`${uid}-query`, true, Boolean(errors?.queryParams)),
135
144
  className: cn(theme.field.input, 'h-auto min-h-16 py-2 font-mono text-xs'),
136
145
  disabled: disabled,
137
- id: "endpoint-query",
146
+ id: `${uid}-query`,
138
147
  onChange: (event)=>onChange({
139
148
  queryParams: event.target.value
140
149
  }),
@@ -146,13 +155,13 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
146
155
  draft.method === 'GET' ? null : /*#__PURE__*/ _jsx(Field, {
147
156
  error: errors?.requestBody,
148
157
  hint: labels.endpoints.requestBodyHint,
149
- htmlFor: "endpoint-body",
158
+ htmlFor: `${uid}-body`,
150
159
  label: labels.endpoints.requestBody,
151
160
  children: /*#__PURE__*/ _jsx("textarea", {
152
- "aria-describedby": describedBy('endpoint-body', true, Boolean(errors?.requestBody)),
161
+ "aria-describedby": describedBy(`${uid}-body`, true, Boolean(errors?.requestBody)),
153
162
  className: cn(theme.field.input, 'h-auto min-h-20 py-2 font-mono text-xs'),
154
163
  disabled: disabled,
155
- id: "endpoint-body",
164
+ id: `${uid}-body`,
156
165
  onChange: (event)=>onChange({
157
166
  requestBody: event.target.value
158
167
  }),
@@ -162,12 +171,12 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
162
171
  })
163
172
  }),
164
173
  /*#__PURE__*/ _jsx(Field, {
165
- htmlFor: "endpoint-description",
174
+ htmlFor: `${uid}-description`,
166
175
  label: labels.endpoints.description,
167
176
  children: /*#__PURE__*/ _jsx("textarea", {
168
177
  className: cn(theme.field.input, 'h-auto min-h-20 py-2'),
169
178
  disabled: disabled,
170
- id: "endpoint-description",
179
+ id: `${uid}-description`,
171
180
  onChange: (event)=>onChange({
172
181
  description: event.target.value
173
182
  }),
@@ -178,13 +187,13 @@ export function EndpointFormFields({ draft, disabled, errors, onChange }) {
178
187
  }),
179
188
  /*#__PURE__*/ _jsxs("label", {
180
189
  className: "flex items-center gap-2",
181
- htmlFor: "endpoint-enabled",
190
+ htmlFor: `${uid}-enabled`,
182
191
  children: [
183
192
  /*#__PURE__*/ _jsx("input", {
184
193
  checked: draft.enabled,
185
194
  className: "size-4 shrink-0 accent-sky-600 dark:accent-sky-500",
186
195
  disabled: disabled,
187
- id: "endpoint-enabled",
196
+ id: `${uid}-enabled`,
188
197
  onChange: (event)=>onChange({
189
198
  enabled: event.target.checked
190
199
  }),
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useEffectEvent, useState } from 'react';
4
4
  import { useIntegrationsStore } from '../store';
5
5
  import { useLabels } from '../text/context';
@@ -18,14 +18,24 @@ export function EndpointsTab({ actions }) {
18
18
  const labels = useLabels();
19
19
  const store = useIntegrationsStore();
20
20
  const sourceId = store.selectedSourceId;
21
- const [selected, setSelected] = useState(null);
22
21
  const [creating, setCreating] = useState(false);
23
22
  const [reloadToken, setReloadToken] = useState(0);
24
- // Another connection is another set of endpoints. A form left open across the
23
+ /**
24
+ * The selection is the STORE's, and only the store's.
25
+ *
26
+ * It used to be kept here as well, and the two were reconciled by comparing
27
+ * them on every render. Clicking a second call then left the first form on
28
+ * screen and added the new one under it — two live editors of two different
29
+ * records, both writing to the same hard-coded field ids, growing by one card
30
+ * per click. One source of truth, and one keyed container below, is what makes
31
+ * that impossible rather than unlikely.
32
+ */ const selectedId = store.selectedEndpointId;
33
+ const editing = selectedId ? store.endpoints.find((e)=>e.id === selectedId) ?? null : null;
34
+ // Another connection is another set of calls. A form left open across the
25
35
  // change would be editing a record this connection does not own.
26
36
  const reset = useEffectEvent(()=>{
27
- setSelected(null);
28
37
  setCreating(false);
38
+ store.selectEndpoint(null);
29
39
  });
30
40
  useEffect(()=>{
31
41
  reset();
@@ -33,29 +43,26 @@ export function EndpointsTab({ actions }) {
33
43
  sourceId
34
44
  ]);
35
45
  const handleNew = ()=>{
36
- setSelected(null);
46
+ store.selectEndpoint(null);
37
47
  setCreating(true);
38
48
  };
39
49
  const handleSelect = (endpoint)=>{
40
- setSelected(endpoint);
41
50
  setCreating(false);
51
+ store.selectEndpoint(endpoint.id);
42
52
  };
43
53
  const handleSaved = (saved)=>{
44
- setSelected(saved);
45
54
  setCreating(false);
46
- // Re-selecting drops the old sample: it described the endpoint as it was
55
+ // Re-selecting drops the old sample: it described the call as it was
47
56
  // configured a moment ago, and the mapping would be built from it.
48
57
  store.selectEndpoint(saved.id);
49
58
  setReloadToken((token)=>token + 1);
50
59
  };
51
60
  const handleDeleted = ()=>{
52
- setSelected(null);
53
61
  setCreating(false);
54
62
  store.selectEndpoint(null);
55
63
  setReloadToken((token)=>token + 1);
56
64
  };
57
65
  const handleCancel = ()=>{
58
- setSelected(null);
59
66
  setCreating(false);
60
67
  store.selectEndpoint(null);
61
68
  };
@@ -65,9 +72,6 @@ export function EndpointsTab({ actions }) {
65
72
  children: labels.endpoints.selectConnection
66
73
  });
67
74
  }
68
- // Guarded against the store: the selection is the store's, and a record left
69
- // over from the previous one must not be what the form shows.
70
- const editing = selected && selected.id === store.selectedEndpointId ? selected : null;
71
75
  return /*#__PURE__*/ _jsxs("div", {
72
76
  className: "grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,18rem)_minmax(0,1fr)] xl:gap-5",
73
77
  children: [
@@ -78,26 +82,28 @@ export function EndpointsTab({ actions }) {
78
82
  reloadToken: reloadToken,
79
83
  sourceId: sourceId
80
84
  }, sourceId),
81
- /*#__PURE__*/ _jsxs("div", {
85
+ /*#__PURE__*/ _jsx("div", {
82
86
  className: theme.layout.main,
83
- children: [
84
- editing || creating ? /*#__PURE__*/ _jsx(EndpointForm, {
85
- actions: actions,
86
- endpoint: editing,
87
- onCancel: handleCancel,
88
- onDeleted: handleDeleted,
89
- onSaved: handleSaved,
90
- sourceId: sourceId
91
- }, editing?.id ?? 'new') : /*#__PURE__*/ _jsx("p", {
92
- className: theme.list.empty,
93
- children: "Select an endpoint to edit it, or add one. An endpoint is a single call — a method, a path, and where in the answer the records sit."
94
- }),
95
- editing ? /*#__PURE__*/ _jsx(EndpointSample, {
96
- actions: actions,
97
- endpoint: editing
98
- }, editing.id) : null
99
- ]
100
- })
87
+ children: editing || creating ? /*#__PURE__*/ _jsxs(_Fragment, {
88
+ children: [
89
+ /*#__PURE__*/ _jsx(EndpointForm, {
90
+ actions: actions,
91
+ endpoint: editing,
92
+ onCancel: handleCancel,
93
+ onDeleted: handleDeleted,
94
+ onSaved: handleSaved,
95
+ sourceId: sourceId
96
+ }),
97
+ editing ? /*#__PURE__*/ _jsx(EndpointSample, {
98
+ actions: actions,
99
+ endpoint: editing
100
+ }) : null
101
+ ]
102
+ }) : /*#__PURE__*/ _jsx("p", {
103
+ className: theme.list.empty,
104
+ children: labels.endpoints.pickOne
105
+ })
106
+ }, editing?.id ?? (creating ? 'new' : 'none'))
101
107
  ]
102
108
  });
103
109
  }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from '../../../utils/cn';
4
+ import { useLabels } from '../text/context';
4
5
  import { integrationsPageTheme } from '../theme';
5
6
  import { FIELD_MAPPING_GRID, PARENT_PREFIX, TRANSFORM_LABEL, TRANSFORMS, toTransform } from './fieldMappingOptions';
6
7
  /**
@@ -25,6 +26,7 @@ import { FIELD_MAPPING_GRID, PARENT_PREFIX, TRANSFORM_LABEL, TRANSFORMS, toTrans
25
26
  });
26
27
  }
27
28
  export function FieldMappingRow({ row, index, sourceListId, targetFields, isFirst, isLast, disabled, onPatch, onMove, onRemove }) {
29
+ const labels = useLabels();
28
30
  const base = `${sourceListId}-${index}`;
29
31
  return /*#__PURE__*/ _jsxs("li", {
30
32
  className: cn(theme.card.wrapper, 'p-3'),
@@ -34,7 +36,7 @@ export function FieldMappingRow({ row, index, sourceListId, targetFields, isFirs
34
36
  children: [
35
37
  /*#__PURE__*/ _jsx(Cell, {
36
38
  id: `${base}-source`,
37
- label: "Source",
39
+ label: labels.mappings.source,
38
40
  children: /*#__PURE__*/ _jsx("input", {
39
41
  className: theme.field.input,
40
42
  disabled: disabled,
@@ -43,13 +45,13 @@ export function FieldMappingRow({ row, index, sourceListId, targetFields, isFirs
43
45
  onChange: (event)=>onPatch({
44
46
  source: event.target.value
45
47
  }),
46
- placeholder: "employeeNo",
48
+ placeholder: labels.mappings.sourcePlaceholder,
47
49
  value: row.source
48
50
  })
49
51
  }),
50
52
  /*#__PURE__*/ _jsx(Cell, {
51
53
  id: `${base}-target`,
52
- label: "Target",
54
+ label: labels.mappings.targetColumn,
53
55
  children: targetFields.length > 0 ? /*#__PURE__*/ _jsxs("select", {
54
56
  className: theme.field.input,
55
57
  disabled: disabled,
@@ -75,13 +77,13 @@ export function FieldMappingRow({ row, index, sourceListId, targetFields, isFirs
75
77
  onChange: (event)=>onPatch({
76
78
  target: event.target.value
77
79
  }),
78
- placeholder: "employee_no",
80
+ placeholder: labels.mappings.sourcePlaceholder,
79
81
  value: row.target
80
82
  })
81
83
  }),
82
84
  /*#__PURE__*/ _jsx(Cell, {
83
85
  id: `${base}-transform`,
84
- label: "Transform",
86
+ label: labels.mappings.transform,
85
87
  children: /*#__PURE__*/ _jsx("select", {
86
88
  className: theme.field.input,
87
89
  disabled: disabled,
@@ -98,7 +100,7 @@ export function FieldMappingRow({ row, index, sourceListId, targetFields, isFirs
98
100
  }),
99
101
  /*#__PURE__*/ _jsx(Cell, {
100
102
  id: `${base}-constant`,
101
- label: "Constant",
103
+ label: labels.mappings.constant,
102
104
  children: /*#__PURE__*/ _jsx("input", {
103
105
  className: theme.field.input,
104
106
  disabled: disabled,
@@ -106,13 +108,13 @@ export function FieldMappingRow({ row, index, sourceListId, targetFields, isFirs
106
108
  onChange: (event)=>onPatch({
107
109
  constant: event.target.value
108
110
  }),
109
- placeholder: "Same value on every record",
111
+ placeholder: labels.mappings.constantPlaceholder,
110
112
  value: row.constant ?? ''
111
113
  })
112
114
  }),
113
115
  /*#__PURE__*/ _jsx(Cell, {
114
116
  id: `${base}-fallback`,
115
- label: "Fallback",
117
+ label: labels.mappings.fallback,
116
118
  children: /*#__PURE__*/ _jsx("input", {
117
119
  className: theme.field.input,
118
120
  disabled: disabled,
@@ -120,7 +122,7 @@ export function FieldMappingRow({ row, index, sourceListId, targetFields, isFirs
120
122
  onChange: (event)=>onPatch({
121
123
  fallback: event.target.value
122
124
  }),
123
- placeholder: "Used when the source is empty",
125
+ placeholder: labels.mappings.fallbackPlaceholder,
124
126
  value: row.fallback ?? ''
125
127
  })
126
128
  }),
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
4
  import { cn } from '../../../utils/cn';
5
+ import { useLabels } from '../text/context';
5
6
  import { integrationsPageTheme } from '../theme';
6
7
  import { FieldMappingRow } from './FieldMappingRow';
7
8
  import { EMPTY_FIELD_MAPPING, FIELD_MAPPING_COLUMNS, FIELD_MAPPING_GRID, PARENT_PREFIX } from './fieldMappingOptions';
@@ -14,6 +15,7 @@ import { Notice } from './Primitives';
14
15
  * operator can see the order and change it.
15
16
  */ const theme = integrationsPageTheme;
16
17
  export function FieldMappingRows({ rows, sourceFields, targetFields, disabled, onChange }) {
18
+ const labels = useLabels();
17
19
  const uid = useId();
18
20
  const listId = `${uid}-source-fields`;
19
21
  const patchRow = (index, patch)=>{
@@ -39,7 +41,7 @@ export function FieldMappingRows({ rows, sourceFields, targetFields, disabled, o
39
41
  children: [
40
42
  /*#__PURE__*/ _jsx("h3", {
41
43
  className: theme.card.title,
42
- children: "Field mappings"
44
+ children: labels.mappings.fieldsHeading
43
45
  }),
44
46
  /*#__PURE__*/ _jsx("button", {
45
47
  className: theme.button.secondary,
@@ -54,7 +56,7 @@ export function FieldMappingRows({ rows, sourceFields, targetFields, disabled, o
54
56
  ]
55
57
  }),
56
58
  sourceFields.length === 0 ? /*#__PURE__*/ _jsx(Notice, {
57
- title: "Sample the endpoint first",
59
+ title: labels.mappings.sampleFirstTitle,
58
60
  tone: "warning",
59
61
  children: "No field names are known yet. Sampling the selected endpoint lists what the response really returns; until then a source has to be typed from memory, and a typo shows up only as an empty column once the import has already run."
60
62
  }) : null,
@@ -60,7 +60,15 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
60
60
  limit: 1
61
61
  };
62
62
  const total = (result)=>result.meta?.totalItems ?? 0;
63
- actions.listEndpoints(sourceId, one).then((r)=>store.setEndpointsTotal(total(r))).catch(()=>{});
63
+ // A PAGE of endpoints, not a count. The strip says how many calls have been
64
+ // tried, and "tried" lives on the row — counting alone reported "5 calls,
65
+ // 0 tried" for five endpoints that had all been sampled. It also warms the
66
+ // store for every tab, which is what made the mappings tab depend on having
67
+ // visited the endpoints tab first.
68
+ actions.listEndpoints(sourceId, {
69
+ page: 1,
70
+ limit: 50
71
+ }).then((r)=>store.setEndpoints(r.items, r.meta?.totalItems ?? r.items.length)).catch(()=>{});
64
72
  actions.listMappings(sourceId, one).then((r)=>store.setMappingsTotal(total(r))).catch(()=>{});
65
73
  // Runs are deliberately NOT counted here. A run records the mapping it came
66
74
  // from and nothing about the connection, so the only count available is
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId, useState } from 'react';
4
+ import { useLabels } from '../text/context';
4
5
  import { integrationsPageTheme } from '../theme';
5
6
  import { FieldMappingRows } from './FieldMappingRows';
6
7
  import { endpointLabel, fieldsOfStoredSample } from './formSupport';
@@ -19,6 +20,7 @@ import { CardSkeleton } from './Skeletons';
19
20
  * draft instead of pouring one mapping's edits into another.
20
21
  */ const theme = integrationsPageTheme;
21
22
  export function MappingEditor({ mapping, targets, targetsLoading, endpoints, freshSample, saving, deleting, onSave, onDelete, onCancel }) {
23
+ const labels = useLabels();
22
24
  const uid = useId();
23
25
  const [draft, setDraft] = useState(mapping);
24
26
  const patch = (next)=>{
@@ -57,16 +59,16 @@ export function MappingEditor({ mapping, targets, targetsLoading, endpoints, fre
57
59
  className: theme.card.header,
58
60
  children: /*#__PURE__*/ _jsx("h2", {
59
61
  className: theme.card.title,
60
- children: draft.id ? draft.name || 'Mapping' : 'New mapping'
62
+ children: draft.id ? draft.name || labels.mappings.editTitle : labels.mappings.newTitle
61
63
  })
62
64
  }),
63
65
  /*#__PURE__*/ _jsxs("div", {
64
66
  className: theme.card.body,
65
67
  children: [
66
68
  /*#__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.',
69
+ hint: endpoints.length === 0 ? labels.mappings.noEndpointsBody : readsFrom?.path ?? labels.mappings.readsFromHint,
68
70
  htmlFor: `${uid}-endpoint`,
69
- label: "Reads from",
71
+ label: labels.mappings.readsFrom,
70
72
  children: /*#__PURE__*/ _jsxs("select", {
71
73
  className: theme.field.input,
72
74
  disabled: busy || endpoints.length === 0,
@@ -78,7 +80,7 @@ export function MappingEditor({ mapping, targets, targetsLoading, endpoints, fre
78
80
  children: [
79
81
  /*#__PURE__*/ _jsx("option", {
80
82
  value: "",
81
- children: "Not set"
83
+ children: labels.mappings.notSet
82
84
  }),
83
85
  endpoints.map((endpoint)=>/*#__PURE__*/ _jsx("option", {
84
86
  value: endpoint.id,
@@ -92,7 +94,7 @@ export function MappingEditor({ mapping, targets, targetsLoading, endpoints, fre
92
94
  children: [
93
95
  /*#__PURE__*/ _jsx(Field, {
94
96
  htmlFor: `${uid}-name`,
95
- label: "Name",
97
+ label: labels.mappings.name,
96
98
  children: /*#__PURE__*/ _jsx("input", {
97
99
  className: theme.field.input,
98
100
  disabled: busy,
@@ -100,14 +102,14 @@ export function MappingEditor({ mapping, targets, targetsLoading, endpoints, fre
100
102
  onChange: (event)=>patch({
101
103
  name: event.target.value
102
104
  }),
103
- placeholder: "Staff directory import",
105
+ placeholder: labels.mappings.namePlaceholder,
104
106
  value: draft.name
105
107
  })
106
108
  }),
107
109
  /*#__PURE__*/ _jsx(Field, {
108
- hint: targets.length === 0 ? 'This app exposes no writable entities.' : undefined,
110
+ hint: targets.length === 0 ? labels.mappings.targetNone : undefined,
109
111
  htmlFor: `${uid}-target`,
110
- label: "Target entity",
112
+ label: labels.mappings.target,
111
113
  children: /*#__PURE__*/ _jsxs("select", {
112
114
  className: theme.field.input,
113
115
  disabled: busy || targets.length === 0,
@@ -117,7 +119,7 @@ export function MappingEditor({ mapping, targets, targetsLoading, endpoints, fre
117
119
  children: [
118
120
  /*#__PURE__*/ _jsx("option", {
119
121
  value: "",
120
- children: "Not set"
122
+ children: labels.mappings.notSet
121
123
  }),
122
124
  targets.map((option)=>/*#__PURE__*/ _jsx("option", {
123
125
  value: option.entity,
@@ -129,9 +131,9 @@ export function MappingEditor({ mapping, targets, targetsLoading, endpoints, fre
129
131
  ]
130
132
  }),
131
133
  /*#__PURE__*/ _jsx(Field, {
132
- hint: targetFields.length === 0 ? 'Choose a target entity to list its columns.' : 'The column an incoming record is matched on. Without it nothing can be updated.',
134
+ hint: targetFields.length === 0 ? labels.mappings.dedupHintNoTarget : labels.mappings.dedupHint,
133
135
  htmlFor: `${uid}-dedup`,
134
- label: "Deduplication column",
136
+ label: labels.mappings.dedup,
135
137
  children: /*#__PURE__*/ _jsxs("select", {
136
138
  "aria-describedby": `${uid}-dedup-hint`,
137
139
  className: theme.field.input,
@@ -144,7 +146,7 @@ export function MappingEditor({ mapping, targets, targetsLoading, endpoints, fre
144
146
  children: [
145
147
  /*#__PURE__*/ _jsx("option", {
146
148
  value: "",
147
- children: "Not set"
149
+ children: labels.mappings.notSet
148
150
  }),
149
151
  targetFields.map((field)=>/*#__PURE__*/ _jsx("option", {
150
152
  value: field,
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useEffect, useEffectEvent, useId, useState } from 'react';
4
4
  import { cn } from '../../../utils/cn';
5
5
  import { useIntegrationsStore } from '../store';
6
+ import { useLabels } from '../text/context';
6
7
  import { integrationsPageTheme } from '../theme';
7
8
  import { Badge, Field, Notice } from './Primitives';
8
9
  import { ListSkeleton } from './Skeletons';
@@ -27,6 +28,7 @@ function describeLastRun(value) {
27
28
  return Number.isNaN(when.getTime()) ? 'Never run' : `Last run ${when.toLocaleString()}`;
28
29
  }
29
30
  export function MappingList({ actions, sourceId, selectedId, reloadToken, onSelect }) {
31
+ const labels = useLabels();
30
32
  const store = useIntegrationsStore();
31
33
  const uid = useId();
32
34
  const search = store.mappingsSearch;
@@ -98,18 +100,18 @@ export function MappingList({ actions, sourceId, selectedId, reloadToken, onSele
98
100
  children: [
99
101
  /*#__PURE__*/ _jsx(Field, {
100
102
  htmlFor: `${uid}-search`,
101
- label: "Search mappings",
103
+ label: labels.mappings.searchLabel,
102
104
  children: /*#__PURE__*/ _jsx("input", {
103
105
  className: theme.field.input,
104
106
  id: `${uid}-search`,
105
107
  onChange: (event)=>setTerm(event.target.value),
106
- placeholder: "Name or target entity",
108
+ placeholder: labels.mappings.searchPlaceholder,
107
109
  type: "search",
108
110
  value: term
109
111
  })
110
112
  }),
111
113
  list.error ? /*#__PURE__*/ _jsxs(Notice, {
112
- title: "This list could not be loaded",
114
+ title: labels.mappings.loadFailed,
113
115
  tone: "danger",
114
116
  children: [
115
117
  list.error,
@@ -184,7 +186,7 @@ export function MappingList({ actions, sourceId, selectedId, reloadToken, onSele
184
186
  }),
185
187
  list.loading && list.items.length > 0 ? /*#__PURE__*/ _jsx("p", {
186
188
  className: theme.list.itemMeta,
187
- children: "Loading more…"
189
+ children: labels.mappings.loadingMore
188
190
  }) : null
189
191
  ]
190
192
  });
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useLabels } from '../text/context';
3
4
  import { integrationsPageTheme } from '../theme';
4
5
  import { Field, Notice } from './Primitives';
5
6
  /**
@@ -49,6 +50,7 @@ function toMissingPolicy(value) {
49
50
  return policy === 'deactivate' && (column.trim() === '' || value.trim() === '');
50
51
  }
51
52
  export function MappingWriteModeFields({ idPrefix, writeMode, missingRecordPolicy, missingRecordColumn, missingRecordValue, dedupConfigured, disabled, onWriteModeChange, onMissingRecordPolicyChange, onMissingRecordColumnChange, onMissingRecordValueChange }) {
53
+ const labels = useLabels();
52
54
  const modeId = `${idPrefix}-write-mode`;
53
55
  const policyId = `${idPrefix}-missing-policy`;
54
56
  const columnId = `${idPrefix}-missing-column`;
@@ -60,8 +62,8 @@ export function MappingWriteModeFields({ idPrefix, writeMode, missingRecordPolic
60
62
  children: [
61
63
  /*#__PURE__*/ _jsx(Field, {
62
64
  htmlFor: modeId,
63
- hint: WRITE_MODE_HINT[writeMode],
64
- label: "Write mode",
65
+ hint: labels.writeModeHint[writeMode],
66
+ label: labels.mappings.writeModeLabel,
65
67
  children: /*#__PURE__*/ _jsx("select", {
66
68
  "aria-describedby": `${modeId}-hint`,
67
69
  className: theme.field.input,
@@ -71,12 +73,12 @@ export function MappingWriteModeFields({ idPrefix, writeMode, missingRecordPolic
71
73
  value: writeMode,
72
74
  children: WRITE_MODES.map((mode)=>/*#__PURE__*/ _jsx("option", {
73
75
  value: mode,
74
- children: WRITE_MODE_LABEL[mode]
76
+ children: labels.writeModeFull[mode]
75
77
  }, mode))
76
78
  })
77
79
  }),
78
80
  writeMode === 'replace' ? /*#__PURE__*/ _jsxs(Notice, {
79
- title: "Replace rebuilds the table on every run",
81
+ title: labels.mappings.replaceWarnTitle,
80
82
  tone: "danger",
81
83
  children: [
82
84
  /*#__PURE__*/ _jsx("p", {
@@ -90,8 +92,8 @@ export function MappingWriteModeFields({ idPrefix, writeMode, missingRecordPolic
90
92
  }) : null,
91
93
  /*#__PURE__*/ _jsx(Field, {
92
94
  htmlFor: policyId,
93
- hint: MISSING_POLICY_HINT[missingRecordPolicy],
94
- label: "Records the source no longer returns",
95
+ hint: labels.missingPolicyHint[missingRecordPolicy],
96
+ label: labels.mappings.missingPolicyLabel,
95
97
  children: /*#__PURE__*/ _jsx("select", {
96
98
  "aria-describedby": `${policyId}-hint`,
97
99
  className: theme.field.input,
@@ -101,12 +103,12 @@ export function MappingWriteModeFields({ idPrefix, writeMode, missingRecordPolic
101
103
  value: missingRecordPolicy,
102
104
  children: MISSING_POLICIES.map((policy)=>/*#__PURE__*/ _jsx("option", {
103
105
  value: policy,
104
- children: MISSING_POLICY_LABEL[policy]
106
+ children: labels.missingPolicyFull[policy]
105
107
  }, policy))
106
108
  })
107
109
  }),
108
110
  missingRecordPolicy === 'delete' ? /*#__PURE__*/ _jsx(Notice, {
109
- title: "Deleting is not reversible",
111
+ title: labels.mappings.deleteWarnTitle,
110
112
  tone: "warning",
111
113
  children: "A source that answers with a short page — a filter left on, a timeout, an expired token honoured with an empty list — looks exactly like records that went away."
112
114
  }) : null,
@@ -115,9 +117,9 @@ export function MappingWriteModeFields({ idPrefix, writeMode, missingRecordPolic
115
117
  children: [
116
118
  /*#__PURE__*/ _jsx(Field, {
117
119
  error: columnMissing ? 'Required while Deactivate is selected.' : null,
118
- hint: "A column on the target entity.",
120
+ hint: labels.mappings.deactivateColumnHint,
119
121
  htmlFor: columnId,
120
- label: "Deactivation column",
122
+ label: labels.mappings.deactivateColumn,
121
123
  children: /*#__PURE__*/ _jsx("input", {
122
124
  "aria-describedby": `${columnId}-hint`,
123
125
  className: theme.field.input,
@@ -130,9 +132,9 @@ export function MappingWriteModeFields({ idPrefix, writeMode, missingRecordPolic
130
132
  }),
131
133
  /*#__PURE__*/ _jsx(Field, {
132
134
  error: valueMissing ? 'Required while Deactivate is selected.' : null,
133
- hint: "Written into that column.",
135
+ hint: labels.mappings.deactivateValueHint,
134
136
  htmlFor: valueId,
135
- label: "Deactivation value",
137
+ label: labels.mappings.deactivateValue,
136
138
  children: /*#__PURE__*/ _jsx("input", {
137
139
  "aria-describedby": `${valueId}-hint`,
138
140
  className: theme.field.input,
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useEffectEvent, useState } from 'react';
4
4
  import { useIntegrationsStore } from '../store';
5
+ import { useLabels } from '../text/context';
5
6
  import { integrationsPageTheme } from '../theme';
6
7
  import { MappingEditor } from './MappingEditor';
7
8
  import { MappingList } from './MappingList';
@@ -19,6 +20,7 @@ function messageOf(error) {
19
20
  return error instanceof Error ? error.message : 'The request failed.';
20
21
  }
21
22
  export function MappingsTab({ actions }) {
23
+ const labels = useLabels();
22
24
  const store = useIntegrationsStore();
23
25
  const [targets, setTargets] = useState([]);
24
26
  const [targetsLoading, setTargetsLoading] = useState(true);
@@ -124,29 +126,23 @@ export function MappingsTab({ actions }) {
124
126
  return /*#__PURE__*/ _jsxs("section", {
125
127
  className: "flex flex-col gap-4",
126
128
  children: [
127
- /*#__PURE__*/ _jsxs("div", {
129
+ /*#__PURE__*/ _jsx("div", {
128
130
  className: theme.header.wrapper,
129
- children: [
130
- /*#__PURE__*/ _jsx("p", {
131
- className: theme.header.subtitle,
132
- children: "A mapping turns one endpoint's response into rows of one entity."
133
- }),
134
- /*#__PURE__*/ _jsx("div", {
135
- className: theme.header.actions,
136
- children: /*#__PURE__*/ _jsx("button", {
137
- className: theme.button.primary,
138
- disabled: newMappingEndpointId === '',
139
- onClick: startNew,
140
- type: "button",
141
- children: "New mapping"
142
- })
131
+ children: /*#__PURE__*/ _jsx("div", {
132
+ className: theme.header.actions,
133
+ children: /*#__PURE__*/ _jsx("button", {
134
+ className: theme.button.primary,
135
+ disabled: newMappingEndpointId === '',
136
+ onClick: startNew,
137
+ type: "button",
138
+ children: labels.mappings.create
143
139
  })
144
- ]
140
+ })
145
141
  }),
146
142
  newMappingEndpointId === '' ? /*#__PURE__*/ _jsx(Notice, {
147
- title: "No endpoints to read from",
143
+ title: labels.mappings.noEndpointsTitle,
148
144
  tone: "info",
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."
145
+ children: labels.mappings.noEndpointsBody
150
146
  }) : null,
151
147
  /*#__PURE__*/ _jsxs("div", {
152
148
  className: theme.layout.split,
@@ -176,7 +172,7 @@ export function MappingsTab({ actions }) {
176
172
  targetsLoading: targetsLoading
177
173
  }, draft.id ?? 'new-mapping') : /*#__PURE__*/ _jsx("p", {
178
174
  className: theme.list.empty,
179
- children: "Pick a mapping to edit it, or create one to describe what an import writes."
175
+ children: labels.mappings.pickOne
180
176
  })
181
177
  })
182
178
  ]
@@ -58,6 +58,10 @@ export type IntegrationsLabels = {
58
58
  off: string;
59
59
  /** Returns to the chooser once a connection is being worked in. */
60
60
  change: string;
61
+ editTitle: string;
62
+ newTitle: string;
63
+ enabledBadge: string;
64
+ disabledBadge: string;
61
65
  };
62
66
  runs: {
63
67
  /** The one-line summary above the buttons. */
@@ -95,6 +99,7 @@ export type IntegrationsLabels = {
95
99
  loadFailed: string;
96
100
  loadingMore: string;
97
101
  selectConnection: string;
102
+ pickOne: string;
98
103
  editTitle: string;
99
104
  newTitle: string;
100
105
  tag: string;
@@ -171,6 +176,76 @@ export type IntegrationsLabels = {
171
176
  timeout: string;
172
177
  timeoutHint: string;
173
178
  };
179
+ /** Step three: which field lands in which column. */
180
+ mappings: {
181
+ heading: string;
182
+ searchLabel: string;
183
+ searchPlaceholder: string;
184
+ create: string;
185
+ loadFailed: string;
186
+ loadingMore: string;
187
+ empty: string;
188
+ noEndpointsTitle: string;
189
+ noEndpointsBody: string;
190
+ pickOne: string;
191
+ editTitle: string;
192
+ newTitle: string;
193
+ readsFrom: string;
194
+ readsFromHint: string;
195
+ notSet: string;
196
+ name: string;
197
+ namePlaceholder: string;
198
+ target: string;
199
+ targetNone: string;
200
+ dedup: string;
201
+ dedupHintNoTarget: string;
202
+ dedupHint: string;
203
+ writeModeLabel: string;
204
+ missingPolicyLabel: string;
205
+ replaceWarnTitle: string;
206
+ deleteWarnTitle: string;
207
+ deactivateColumn: string;
208
+ deactivateColumnHint: string;
209
+ deactivateValue: string;
210
+ deactivateValueHint: string;
211
+ fieldsHeading: string;
212
+ addField: string;
213
+ sampleFirstTitle: string;
214
+ sampleFirstBody: string;
215
+ nothingMapped: string;
216
+ parentPrefixHint: string;
217
+ templateHint: string;
218
+ source: string;
219
+ sourcePlaceholder: string;
220
+ targetColumn: string;
221
+ transform: string;
222
+ constant: string;
223
+ constantPlaceholder: string;
224
+ fallback: string;
225
+ fallbackPlaceholder: string;
226
+ save: string;
227
+ };
228
+ /** The long form of each write mode, for the picker. */
229
+ writeModeFull: {
230
+ upsert: string;
231
+ append: string;
232
+ replace: string;
233
+ };
234
+ writeModeHint: {
235
+ upsert: string;
236
+ append: string;
237
+ replace: string;
238
+ };
239
+ missingPolicyFull: {
240
+ ignore: string;
241
+ deactivate: string;
242
+ delete: string;
243
+ };
244
+ missingPolicyHint: {
245
+ ignore: string;
246
+ deactivate: string;
247
+ delete: string;
248
+ };
174
249
  writeMode: {
175
250
  upsert: string;
176
251
  append: string;
@@ -51,7 +51,11 @@
51
51
  loadFailed: 'This list could not be loaded',
52
52
  retry: 'Try again',
53
53
  off: 'Off',
54
- change: 'Change'
54
+ change: 'Change',
55
+ editTitle: 'Connection',
56
+ newTitle: 'New connection',
57
+ enabledBadge: 'Enabled',
58
+ disabledBadge: 'Off'
55
59
  },
56
60
  runs: {
57
61
  summary: (target, writeMode, missing)=>`Writes into ${target} · ${writeMode} · missing rows: ${missing}`,
@@ -87,6 +91,7 @@
87
91
  loadFailed: 'This list could not be loaded',
88
92
  loadingMore: 'Loading more…',
89
93
  selectConnection: 'Select a connection to see its calls.',
94
+ pickOne: 'Select a call to edit it, or add one. A call is a single request — a method, a path, and where in the answer the records sit.',
90
95
  editTitle: 'Edit call',
91
96
  newTitle: 'New call',
92
97
  tag: 'Tag',
@@ -162,6 +167,74 @@
162
167
  timeout: 'Timeout (ms)',
163
168
  timeoutHint: 'How long one request may take before it is given up on. Empty means the server default.'
164
169
  },
170
+ mappings: {
171
+ heading: 'Mappings',
172
+ searchLabel: 'Search',
173
+ searchPlaceholder: 'Name or target table',
174
+ create: 'New mapping',
175
+ loadFailed: 'This list could not be loaded',
176
+ loadingMore: 'Loading more…',
177
+ empty: 'No mappings match. A mapping is what turns a response into rows.',
178
+ noEndpointsTitle: 'No calls to read from',
179
+ noEndpointsBody: "A mapping turns one call's response into rows, so there has to be a call first. Add one on the calls step and it becomes selectable here.",
180
+ pickOne: 'Pick a mapping to edit it, or create one to describe what an import writes.',
181
+ editTitle: 'Mapping',
182
+ newTitle: 'New mapping',
183
+ readsFrom: 'Reads from',
184
+ readsFromHint: 'Every mapping reads from exactly one call.',
185
+ notSet: 'Not set',
186
+ name: 'Name',
187
+ namePlaceholder: 'Staff directory import',
188
+ target: 'Writes into',
189
+ targetNone: 'This app exposes no writable tables.',
190
+ dedup: 'Matched on',
191
+ dedupHintNoTarget: 'Choose a target table to list its columns.',
192
+ dedupHint: 'The column an incoming record is matched on. Without it nothing can be updated.',
193
+ writeModeLabel: 'What a run does',
194
+ missingPolicyLabel: 'Records the source no longer returns',
195
+ replaceWarnTitle: 'Replace rebuilds the table on every run',
196
+ deleteWarnTitle: 'Deleting is not reversible',
197
+ deactivateColumn: 'Deactivation column',
198
+ deactivateColumnHint: 'A column on the target table.',
199
+ deactivateValue: 'Deactivation value',
200
+ deactivateValueHint: 'Written into that column.',
201
+ fieldsHeading: 'Field mappings',
202
+ addField: 'Add field',
203
+ sampleFirstTitle: 'Try the call first',
204
+ sampleFirstBody: 'No field names are known yet. Trying the selected call lists what the response really returns; until then a source has to be typed from memory, and a typo shows up only as an empty column once the import has already run.',
205
+ nothingMapped: 'Nothing is mapped yet, so a run would write empty records. Add a field to begin.',
206
+ parentPrefixHint: 'to take the value from the parent call’s record instead of this one.',
207
+ templateHint: 'to build one value from several — use it when no single field is unique on its own, which is what the matching column needs.',
208
+ source: 'Source',
209
+ sourcePlaceholder: 'employeeNo',
210
+ targetColumn: 'Target',
211
+ transform: 'Transform',
212
+ constant: 'Constant',
213
+ constantPlaceholder: 'Same value on every record',
214
+ fallback: 'Fallback',
215
+ fallbackPlaceholder: 'Used when the source is empty',
216
+ save: 'Save mapping'
217
+ },
218
+ writeModeFull: {
219
+ upsert: 'Upsert — update what matches, insert the rest',
220
+ append: 'Append — insert every fetched record',
221
+ replace: 'Replace — rebuild the table from this response'
222
+ },
223
+ writeModeHint: {
224
+ upsert: 'Rows keep their identity, so whatever points at them keeps working.',
225
+ append: 'Nothing is matched, so running this import twice stores every record twice.',
226
+ replace: 'The table is emptied and refilled on every run.'
227
+ },
228
+ missingPolicyFull: {
229
+ ignore: 'Ignore — leave them exactly as they are',
230
+ deactivate: 'Deactivate — mark them with a column value',
231
+ delete: 'Delete — remove them from the table'
232
+ },
233
+ missingPolicyHint: {
234
+ ignore: 'Safest. Records removed at the source stay in the table until someone acts.',
235
+ deactivate: 'Reversible. The row survives and history that refers to it stays readable.',
236
+ delete: 'Permanent. A source that returns a short page for any reason takes rows with it.'
237
+ },
165
238
  writeMode: {
166
239
  upsert: 'update what matches, insert the rest',
167
240
  append: 'always insert, never match',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.804",
3
+ "version": "0.9.806",
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",