nucleus-core-ts 0.9.846 → 0.9.847
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/ConnectionDelete.js +1 -0
- package/dist/fe/components/IntegrationsPage/components/IntegrationsPage.js +6 -1
- package/dist/fe/components/IntegrationsPage/store/index.js +16 -0
- package/dist/fe/components/IntegrationsPage/store/state.d.ts +12 -0
- package/dist/fe/components/IntegrationsPage/store/upsertInto.d.ts +20 -0
- package/dist/fe/components/IntegrationsPage/store/upsertInto.js +21 -0
- package/dist/fe/components/IntegrationsPage/store/upsertInto.test.d.ts +1 -0
- package/dist/fe/components/IntegrationsPage/store/upsertInto.test.js +67 -0
- package/package.json +1 -1
|
@@ -43,7 +43,12 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
|
|
|
43
43
|
* detail treated the missing record as still loading and sat on skeletons
|
|
44
44
|
* until the page was reloaded by hand. The write itself had succeeded.
|
|
45
45
|
*/ const [sourcesReloadToken, setSourcesReloadToken] = useState(0);
|
|
46
|
-
const handleSourceSaved = ()=>{
|
|
46
|
+
const handleSourceSaved = (saved)=>{
|
|
47
|
+
// Into the store FIRST. Bumping the token only tells the rail to refetch,
|
|
48
|
+
// and choosing a connection unmounts the rail — so on a NEW connection the
|
|
49
|
+
// token reached nobody and the detail below could not find the record it
|
|
50
|
+
// had just been switched to.
|
|
51
|
+
store.upsertSource(saved);
|
|
47
52
|
setSourcesReloadToken((token)=>token + 1);
|
|
48
53
|
};
|
|
49
54
|
/**
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { batch, createStore } from 'h-state';
|
|
3
3
|
import { initial } from './state';
|
|
4
|
+
import { isNewTo, upsertInto } from './upsertInto';
|
|
4
5
|
export const { useStore: useIntegrationsStore } = createStore(initial, {
|
|
5
6
|
setSources: (store)=>(items, total)=>batch(()=>{
|
|
6
7
|
store.sources = items;
|
|
7
8
|
store.sourcesTotal = total;
|
|
8
9
|
store.sourcesLoading = false;
|
|
9
10
|
}),
|
|
11
|
+
upsertSource: (store)=>(source)=>batch(()=>{
|
|
12
|
+
// The count moves only for an addition — an edit that bumped the total
|
|
13
|
+
// would make the rail claim a connection nobody can find.
|
|
14
|
+
if (isNewTo(store.sources, source)) store.sourcesTotal = store.sourcesTotal + 1;
|
|
15
|
+
store.sources = upsertInto(store.sources, source);
|
|
16
|
+
}),
|
|
17
|
+
removeSource: (store)=>(id)=>batch(()=>{
|
|
18
|
+
const kept = store.sources.filter((candidate)=>candidate.id !== id);
|
|
19
|
+
// Only when it was actually on this page — the count must not fall for a
|
|
20
|
+
// record the list never held.
|
|
21
|
+
if (kept.length !== store.sources.length) {
|
|
22
|
+
store.sourcesTotal = Math.max(0, store.sourcesTotal - 1);
|
|
23
|
+
store.sources = kept;
|
|
24
|
+
}
|
|
25
|
+
}),
|
|
10
26
|
setSourcesSearch: (store)=>(search)=>batch(()=>{
|
|
11
27
|
store.sourcesSearch = search;
|
|
12
28
|
// A new search is a new result set; staying on page 4 would show an empty
|
|
@@ -58,6 +58,18 @@ export type State = {
|
|
|
58
58
|
export type Actions = {
|
|
59
59
|
[key: string]: unknown;
|
|
60
60
|
setSources: (items: IntegrationSource[], total: number) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Put one connection into the list, whether or not it is already there.
|
|
63
|
+
*
|
|
64
|
+
* The rail is what fetches connections, and choosing one UNMOUNTS the rail —
|
|
65
|
+
* so a connection created from the form was selected into a list that had
|
|
66
|
+
* never heard of it, and every panel below sat on skeletons until the page
|
|
67
|
+
* was reloaded by hand. The save already returns the record; this is where it
|
|
68
|
+
* goes so the detail can find it.
|
|
69
|
+
*/
|
|
70
|
+
upsertSource: (source: IntegrationSource) => void;
|
|
71
|
+
/** Drop one connection from the list, for when it has just been deleted. */
|
|
72
|
+
removeSource: (id: string) => void;
|
|
61
73
|
setSourcesSearch: (search: string) => void;
|
|
62
74
|
setSourcesLoading: (loading: boolean) => void;
|
|
63
75
|
selectSource: (id: string | null) => void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a saved record goes in a list that may or may not already hold it.
|
|
3
|
+
*
|
|
4
|
+
* Kept as a function so the rule can be tested apart from the store: the panel
|
|
5
|
+
* unmounts the connection rail the moment a connection is chosen, so a newly
|
|
6
|
+
* created connection is selected into a list that never fetched it. Putting the
|
|
7
|
+
* returned record in by hand is what stops the detail below from waiting for a
|
|
8
|
+
* fetch that no mounted component will make.
|
|
9
|
+
*
|
|
10
|
+
* A new record goes to the FRONT — it is the one just made, and the operator is
|
|
11
|
+
* about to be switched to it; an existing one is replaced where it already sits,
|
|
12
|
+
* so an edit never reorders a list somebody is reading.
|
|
13
|
+
*/
|
|
14
|
+
export declare function upsertInto<T extends {
|
|
15
|
+
id: string;
|
|
16
|
+
}>(items: T[], record: T): T[];
|
|
17
|
+
/** Whether `record` would be an addition rather than a replacement. */
|
|
18
|
+
export declare function isNewTo<T extends {
|
|
19
|
+
id: string;
|
|
20
|
+
}>(items: T[], record: T): boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a saved record goes in a list that may or may not already hold it.
|
|
3
|
+
*
|
|
4
|
+
* Kept as a function so the rule can be tested apart from the store: the panel
|
|
5
|
+
* unmounts the connection rail the moment a connection is chosen, so a newly
|
|
6
|
+
* created connection is selected into a list that never fetched it. Putting the
|
|
7
|
+
* returned record in by hand is what stops the detail below from waiting for a
|
|
8
|
+
* fetch that no mounted component will make.
|
|
9
|
+
*
|
|
10
|
+
* A new record goes to the FRONT — it is the one just made, and the operator is
|
|
11
|
+
* about to be switched to it; an existing one is replaced where it already sits,
|
|
12
|
+
* so an edit never reorders a list somebody is reading.
|
|
13
|
+
*/ export function upsertInto(items, record) {
|
|
14
|
+
return items.some((candidate)=>candidate.id === record.id) ? items.map((candidate)=>candidate.id === record.id ? record : candidate) : [
|
|
15
|
+
record,
|
|
16
|
+
...items
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
/** Whether `record` would be an addition rather than a replacement. */ export function isNewTo(items, record) {
|
|
20
|
+
return !items.some((candidate)=>candidate.id === record.id);
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { isNewTo, upsertInto } from './upsertInto';
|
|
3
|
+
const a = {
|
|
4
|
+
id: 'a',
|
|
5
|
+
name: 'First'
|
|
6
|
+
};
|
|
7
|
+
const b = {
|
|
8
|
+
id: 'b',
|
|
9
|
+
name: 'Second'
|
|
10
|
+
};
|
|
11
|
+
describe('upsertInto', ()=>{
|
|
12
|
+
it('puts a new record at the front', ()=>{
|
|
13
|
+
expect(upsertInto([
|
|
14
|
+
a
|
|
15
|
+
], b)).toEqual([
|
|
16
|
+
b,
|
|
17
|
+
a
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
it('adds to an empty list', ()=>{
|
|
21
|
+
expect(upsertInto([], a)).toEqual([
|
|
22
|
+
a
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
it('replaces an existing record where it already sits', ()=>{
|
|
26
|
+
const renamed = {
|
|
27
|
+
id: 'a',
|
|
28
|
+
name: 'Renamed'
|
|
29
|
+
};
|
|
30
|
+
expect(upsertInto([
|
|
31
|
+
b,
|
|
32
|
+
a
|
|
33
|
+
], renamed)).toEqual([
|
|
34
|
+
b,
|
|
35
|
+
renamed
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
it('never duplicates — saving the same record twice leaves one', ()=>{
|
|
39
|
+
const once = upsertInto([], a);
|
|
40
|
+
expect(upsertInto(once, {
|
|
41
|
+
id: 'a',
|
|
42
|
+
name: 'Again'
|
|
43
|
+
})).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
it('leaves the original array untouched', ()=>{
|
|
46
|
+
const items = [
|
|
47
|
+
a
|
|
48
|
+
];
|
|
49
|
+
upsertInto(items, b);
|
|
50
|
+
expect(items).toEqual([
|
|
51
|
+
a
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('isNewTo', ()=>{
|
|
56
|
+
it('is true only for a record the list does not hold', ()=>{
|
|
57
|
+
expect(isNewTo([
|
|
58
|
+
a
|
|
59
|
+
], b)).toBe(true);
|
|
60
|
+
expect(isNewTo([
|
|
61
|
+
a
|
|
62
|
+
], {
|
|
63
|
+
id: 'a',
|
|
64
|
+
name: 'Renamed'
|
|
65
|
+
})).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.847",
|
|
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",
|