nucleus-core-ts 0.9.859 → 0.9.861

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.
@@ -64,11 +64,15 @@ export function ConnectionList({ actions, onNewConnection, reloadToken = 0 }) {
64
64
  // which is where the editor looks for the record it is editing. Saving
65
65
  // triggers a reset, so saving is when the record vanished.
66
66
  useEffect(()=>{
67
- if (list.settled) publishItems();
67
+ // And not when it FAILED. `settled` means the request finished, which a
68
+ // refused one also did — publishing that emptied the store on every
69
+ // failed refetch and took the record the editor was showing with it.
70
+ if (list.settled && !list.error) publishItems();
68
71
  }, [
69
72
  list.items,
70
73
  list.total,
71
- list.settled
74
+ list.settled,
75
+ list.error
72
76
  ]);
73
77
  useEffect(()=>{
74
78
  publishLoading();
@@ -77,11 +77,15 @@ export function EndpointList({ sourceId, actions, onNewEndpoint, onSelect, reloa
77
77
  // which is where the editor looks for the record it is editing. Saving
78
78
  // triggers a reset, so saving is when the record vanished.
79
79
  useEffect(()=>{
80
- if (list.settled) publishItems();
80
+ // And not when it FAILED. `settled` means the request finished, which a
81
+ // refused one also did — publishing that emptied the store on every
82
+ // failed refetch and took the record the editor was showing with it.
83
+ if (list.settled && !list.error) publishItems();
81
84
  }, [
82
85
  list.items,
83
86
  list.total,
84
- list.settled
87
+ list.settled,
88
+ list.error
85
89
  ]);
86
90
  useEffect(()=>{
87
91
  publishLoading();
@@ -88,11 +88,15 @@ export function MappingList({ actions, sourceId, selectedId, reloadToken, onSele
88
88
  // which is where the editor looks for the record it is editing. Saving
89
89
  // triggers a reset, so saving is when the record vanished.
90
90
  useEffect(()=>{
91
- if (list.settled) publishItems();
91
+ // And not when it FAILED. `settled` means the request finished, which a
92
+ // refused one also did — publishing that emptied the store on every
93
+ // failed refetch and took the record the editor was showing with it.
94
+ if (list.settled && !list.error) publishItems();
92
95
  }, [
93
96
  list.items,
94
97
  list.total,
95
- list.settled
98
+ list.settled,
99
+ list.error
96
100
  ]);
97
101
  useEffect(()=>{
98
102
  publishLoading();
@@ -31,7 +31,8 @@ export function MappingsTab({ actions }) {
31
31
  const [reloadToken, setReloadToken] = useState(0);
32
32
  /** Set only when a new mapping is being started for a table named on arrival. */ const [pendingTarget, setPendingTarget] = useState('');
33
33
  const sourceId = store.selectedSourceId ?? '';
34
- const newMappingEndpointId = store.selectedEndpointId ?? store.endpoints[0]?.id ?? '';
34
+ const [pickableEndpoints, setPickableEndpoints] = useState([]);
35
+ const newMappingEndpointId = store.selectedEndpointId ?? pickableEndpoints[0]?.id ?? '';
35
36
  const loadTargets = useEffectEvent(()=>{
36
37
  setTargetsLoading(true);
37
38
  actions.listTargets().then((result)=>{
@@ -53,21 +54,44 @@ export function MappingsTab({ actions }) {
53
54
  * and a disabled New mapping — about a connection with three of them. The
54
55
  * only way through was to visit the other tab first, which nothing said.
55
56
  *
56
- * One page is enough to offer a source and to name one; the Endpoints tab
57
- * remains the place that pages through them all.
58
- */ const loadEndpointsIfMissing = useEffectEvent(()=>{
59
- if (sourceId === '' || store.endpoints.length > 0) return;
60
- actions.listEndpoints(sourceId, {
61
- page: 1,
62
- limit: 50
63
- }).then((result)=>{
64
- store.setEndpoints(result.items, result.meta?.totalItems ?? result.items.length);
65
- }).catch((error)=>{
66
- store.setError(messageOf(error));
67
- });
57
+ * Its OWN list, not the one the Endpoints step is showing.
58
+ *
59
+ * `store.endpoints` holds whatever that step last fetched — including what a
60
+ * SEARCH narrowed it to. A mapping reads one call, and the picker offering
61
+ * them was reading that same array: typing "employee" into the endpoints
62
+ * search left the mapping editor able to choose only the calls matching it,
63
+ * and `loadEndpointsIfMissing` then declined to fetch because the array was
64
+ * not empty. Nothing said the list was a filtered one.
65
+ *
66
+ * Read to exhaustion rather than one page: a picker that silently stops at
67
+ * fifty is the same defect one step down.
68
+ */ const loadPickableEndpoints = useEffectEvent(()=>{
69
+ if (sourceId === '') {
70
+ setPickableEndpoints([]);
71
+ return;
72
+ }
73
+ const PAGE = 200;
74
+ const MAX_PAGES = 20;
75
+ const read = (page, collected)=>{
76
+ actions.listEndpoints(sourceId, {
77
+ page,
78
+ limit: PAGE
79
+ }).then((result)=>{
80
+ const all = collected.concat(result.items);
81
+ const total = result.meta?.totalItems ?? all.length;
82
+ if (result.items.length === PAGE && all.length < total && page < MAX_PAGES) {
83
+ read(page + 1, all);
84
+ return;
85
+ }
86
+ setPickableEndpoints(all);
87
+ }).catch((error)=>{
88
+ store.setError(messageOf(error));
89
+ });
90
+ };
91
+ read(1, []);
68
92
  });
69
93
  useEffect(()=>{
70
- loadEndpointsIfMissing();
94
+ loadPickableEndpoints();
71
95
  }, [
72
96
  sourceId
73
97
  ]);
@@ -245,7 +269,7 @@ export function MappingsTab({ actions }) {
245
269
  className: theme.layout.main,
246
270
  children: draft ? /*#__PURE__*/ _jsx(MappingEditor, {
247
271
  deleting: deleting,
248
- endpoints: store.endpoints,
272
+ endpoints: pickableEndpoints,
249
273
  freshSample: freshSample,
250
274
  mapping: draft,
251
275
  onCancel: closeEditor,
@@ -55,11 +55,15 @@ export function RunHistory({ actions, mappingId, refreshKey, onRefresh }) {
55
55
  // reset empties the hook's items before the first page is asked for, and
56
56
  // publishing that emptied the shared store.
57
57
  useEffect(()=>{
58
- if (list.settled) publishRuns();
58
+ // And not when it FAILED. `settled` means the request finished, which a
59
+ // refused one also did — publishing that emptied the store on every
60
+ // failed refetch and took the record the editor was showing with it.
61
+ if (list.settled && !list.error) publishRuns();
59
62
  }, [
60
63
  list.items,
61
64
  list.total,
62
- list.settled
65
+ list.settled,
66
+ list.error
63
67
  ]);
64
68
  useEffect(()=>{
65
69
  publishLoading();
@@ -44,13 +44,21 @@ export function RunPanel({ mapping, actions, onSubscribeProgress, onRunSettled }
44
44
  const needsPassword = passwordIsNeeded(mapping);
45
45
  const passwordReady = !needsPassword || sharedPassword.length >= MIN_SHARED_PASSWORD;
46
46
  const activeRunId = store.activeRunId;
47
- const plan = store.plan;
47
+ /**
48
+ * Only a plan computed for THIS mapping.
49
+ *
50
+ * A preview reads a whole source, so it can land long after the operator has
51
+ * moved on — and it used to land under whatever was selected by then. The
52
+ * counts an operator reads before pressing Run must describe the mapping they
53
+ * are looking at.
54
+ */ const plan = store.planMappingId === mapping.id ? store.plan : null;
48
55
  const preview = ()=>{
56
+ const requestedFor = mapping.id;
49
57
  store.setError(null);
50
58
  store.setPlanning(true);
51
- actions.previewRun(mapping.id).then((result)=>{
59
+ actions.previewRun(requestedFor).then((result)=>{
52
60
  if (result.error) store.setError(result.error);
53
- store.setPlan(result.plan);
61
+ store.setPlan(result.plan, requestedFor);
54
62
  }).catch((error)=>{
55
63
  store.setPlanning(false);
56
64
  store.setError(errorText(error, 'The preview could not be computed.'));
@@ -33,6 +33,10 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
33
33
  store.sourcesLoading = loading;
34
34
  },
35
35
  selectSource: (store)=>(id)=>batch(()=>{
36
+ // Same rule as selectMapping: everything below discards what belonged to
37
+ // the previous connection, so re-choosing the current one would empty the
38
+ // step somebody is working in for no reason.
39
+ if (store.selectedSourceId === id) return;
36
40
  store.selectedSourceId = id;
37
41
  // Everything below a connection belongs to THAT connection. Leaving the
38
42
  // previous one's endpoints on screen under a new name is worse than empty.
@@ -55,6 +59,7 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
55
59
  store.sampleEndpointId = null;
56
60
  store.sampling = false;
57
61
  store.plan = null;
62
+ store.planMappingId = null;
58
63
  store.planning = false;
59
64
  store.activeRunId = null;
60
65
  store.progress = null;
@@ -97,6 +102,9 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
97
102
  store.endpointsLoading = loading;
98
103
  },
99
104
  selectEndpoint: (store)=>(id)=>batch(()=>{
105
+ // Re-choosing the call already open would throw away the sample just
106
+ // taken of it — and taking a sample is a real request to somebody's API.
107
+ if (store.selectedEndpointId === id) return;
100
108
  store.selectedEndpointId = id;
101
109
  // The sample describes ONE endpoint; carrying it over would offer the
102
110
  // wrong field list to the next mapping.
@@ -150,6 +158,7 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
150
158
  // A plan belongs to the mapping it was computed for. Showing it against a
151
159
  // different one would have an operator approve counts from another import.
152
160
  store.plan = null;
161
+ store.planMappingId = null;
153
162
  // And the WAIT for a plan goes with it. Discarding the plan while leaving
154
163
  // this set put the next mapping's panel on a counting skeleton for a
155
164
  // preview that had been computed for a different mapping and thrown away.
@@ -177,8 +186,9 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
177
186
  setSampling: (store)=>(sampling)=>{
178
187
  store.sampling = sampling;
179
188
  },
180
- setPlan: (store)=>(plan)=>batch(()=>{
189
+ setPlan: (store)=>(plan, mappingId)=>batch(()=>{
181
190
  store.plan = plan;
191
+ store.planMappingId = plan ? mappingId : null;
182
192
  store.planning = false;
183
193
  }),
184
194
  setPlanning: (store)=>(planning)=>{
@@ -61,6 +61,17 @@ export type State = {
61
61
  sampling: boolean;
62
62
  /** What a run would do. Cleared whenever the mapping changes underneath it. */
63
63
  plan: RunPlanValue | null;
64
+ /**
65
+ * The mapping the plan in hand was computed for.
66
+ *
67
+ * A preview reads a whole source and can take a while. Switching mapping
68
+ * while one is in flight used to land the answer under whatever was selected
69
+ * when it arrived — so an operator read "880 to create, 3 to skip" about a
70
+ * mapping that had never been previewed, and pressing Run beside it started
71
+ * an import against those numbers. Same shape as the endpoint sample, and the
72
+ * same fix: the answer is tagged, and only its own mapping reads it.
73
+ */
74
+ planMappingId: string | null;
64
75
  planning: boolean;
65
76
  /** Live progress of the run this panel started. */
66
77
  progress: RunProgressValue | null;
@@ -129,7 +140,7 @@ export type Actions = {
129
140
  setRunsLoading: (loading: boolean) => void;
130
141
  setSample: (sample: EndpointSampleValue | null, endpointId: string | null) => void;
131
142
  setSampling: (sampling: boolean) => void;
132
- setPlan: (plan: RunPlanValue | null) => void;
143
+ setPlan: (plan: RunPlanValue | null, mappingId: string | null) => void;
133
144
  setPlanning: (planning: boolean) => void;
134
145
  setProgress: (progress: RunProgressValue | null) => void;
135
146
  setActiveRun: (runId: string | null) => void;
@@ -24,6 +24,7 @@ export const initial = {
24
24
  sampleEndpointId: null,
25
25
  sampling: false,
26
26
  plan: null,
27
+ planMappingId: null,
27
28
  planning: false,
28
29
  progress: null,
29
30
  activeRunId: null,