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.
@@ -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
- /** Upsert and ignore: the two choices that cannot lose a row by themselves. */
35
- export declare function blankMappingDraft(endpointId: string): MappingDraft;
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
- /** Upsert and ignore: the two choices that cannot lose a row by themselves. */ export function blankMappingDraft(endpointId) {
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.
@@ -6,6 +6,8 @@ export const initial = {
6
6
  sourcesLoading: false,
7
7
  selectedSourceId: null,
8
8
  tab: 'connection',
9
+ focusTargetEntity: null,
10
+ focusStep: 'mapping',
9
11
  endpoints: [],
10
12
  endpointsTotal: 0,
11
13
  endpointsPage: 1,
@@ -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.815",
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",