Skip to content

Commit fd91f88

Browse files
nielslyngsoeAndyButland
authored andcommitted
Item Repository: Sort statuses by order of unique (#20603)
* utility * ability to replace * deprecate removeStatus * no need to call this any longer * Sort statuses and ensure not appending statuses, only updating them # Conflicts: # src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts
1 parent 13c164d commit fd91f88

File tree

6 files changed

+119
-39
lines changed

6 files changed

+119
-39
lines changed

src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe('ArrayState', () => {
55
type ObjectType = { key: string; another: string };
66
type ArrayType = ObjectType[];
77

8-
let subject: UmbArrayState<ObjectType>;
8+
let state: UmbArrayState<ObjectType>;
99
let initialData: ArrayType;
1010

1111
beforeEach(() => {
@@ -14,12 +14,12 @@ describe('ArrayState', () => {
1414
{ key: '2', another: 'myValue2' },
1515
{ key: '3', another: 'myValue3' },
1616
];
17-
subject = new UmbArrayState(initialData, (x) => x.key);
17+
state = new UmbArrayState(initialData, (x) => x.key);
1818
});
1919

2020
it('replays latests, no matter the amount of subscriptions.', (done) => {
2121
let amountOfCallbacks = 0;
22-
const observer = subject.asObservable();
22+
const observer = state.asObservable();
2323
observer.subscribe((value) => {
2424
amountOfCallbacks++;
2525
expect(value).to.be.equal(initialData);
@@ -36,26 +36,26 @@ describe('ArrayState', () => {
3636
it('remove method, removes the one with the key', (done) => {
3737
const expectedData = [initialData[0], initialData[2]];
3838

39-
subject.remove(['2']);
40-
const observer = subject.asObservable();
39+
state.remove(['2']);
40+
const observer = state.asObservable();
4141
observer.subscribe((value) => {
4242
expect(JSON.stringify(value)).to.be.equal(JSON.stringify(expectedData));
4343
done();
4444
});
4545
});
4646

4747
it('getHasOne method, return true when key exists', () => {
48-
expect(subject.getHasOne('2')).to.be.true;
48+
expect(state.getHasOne('2')).to.be.true;
4949
});
5050
it('getHasOne method, return false when key does not exists', () => {
51-
expect(subject.getHasOne('1337')).to.be.false;
51+
expect(state.getHasOne('1337')).to.be.false;
5252
});
5353

5454
it('filter method, removes anything that is not true of the given predicate method', (done) => {
5555
const expectedData = [initialData[0], initialData[2]];
5656

57-
subject.filter((x) => x.key !== '2');
58-
const observer = subject.asObservable();
57+
state.filter((x) => x.key !== '2');
58+
const observer = state.asObservable();
5959
observer.subscribe((value) => {
6060
expect(JSON.stringify(value)).to.be.equal(JSON.stringify(expectedData));
6161
done();
@@ -64,11 +64,11 @@ describe('ArrayState', () => {
6464

6565
it('add new item via appendOne method.', (done) => {
6666
const newItem = { key: '4', another: 'myValue4' };
67-
subject.appendOne(newItem);
67+
state.appendOne(newItem);
6868

6969
const expectedData = [...initialData, newItem];
7070

71-
const observer = subject.asObservable();
71+
const observer = state.asObservable();
7272
observer.subscribe((value) => {
7373
expect(value.length).to.be.equal(expectedData.length);
7474
expect(value[3].another).to.be.equal(expectedData[3].another);
@@ -78,9 +78,25 @@ describe('ArrayState', () => {
7878

7979
it('partially update an existing item via updateOne method.', (done) => {
8080
const newItem = { another: 'myValue2.2' };
81-
subject.updateOne('2', newItem);
81+
state.updateOne('2', newItem);
8282

83-
const observer = subject.asObservable();
83+
const observer = state.asObservable();
84+
observer.subscribe((value) => {
85+
expect(value.length).to.be.equal(initialData.length);
86+
expect(value[0].another).to.be.equal('myValue1');
87+
expect(value[1].another).to.be.equal('myValue2.2');
88+
done();
89+
});
90+
});
91+
92+
it('replaces only existing items via replace method.', (done) => {
93+
const newItems = [
94+
{ key: '2', another: 'myValue2.2' },
95+
{ key: '4', another: 'myValue4.4' },
96+
];
97+
state.replace(newItems);
98+
99+
const observer = state.asObservable();
84100
observer.subscribe((value) => {
85101
expect(value.length).to.be.equal(initialData.length);
86102
expect(value[0].another).to.be.equal('myValue1');
@@ -90,7 +106,7 @@ describe('ArrayState', () => {
90106
});
91107

92108
it('getObservablePart for a specific entry of array', (done) => {
93-
const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === '2'));
109+
const subObserver = state.asObservablePart((data) => data.find((x) => x.key === '2'));
94110
subObserver.subscribe((entry) => {
95111
if (entry) {
96112
expect(entry.another).to.be.equal(initialData[1].another);
@@ -103,7 +119,7 @@ describe('ArrayState', () => {
103119
let amountOfCallbacks = 0;
104120
const newItem = { key: '4', another: 'myValue4' };
105121

106-
const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === newItem.key));
122+
const subObserver = state.asObservablePart((data) => data.find((x) => x.key === newItem.key));
107123
subObserver.subscribe((entry) => {
108124
amountOfCallbacks++;
109125
if (amountOfCallbacks === 1) {
@@ -118,16 +134,16 @@ describe('ArrayState', () => {
118134
}
119135
});
120136

121-
subject.appendOne(newItem);
137+
state.appendOne(newItem);
122138
});
123139

124140
it('asObservable returns the replaced item', (done) => {
125141
const newItem = { key: '2', another: 'myValue4' };
126-
subject.appendOne(newItem);
142+
state.appendOne(newItem);
127143

128144
const expectedData = [initialData[0], newItem, initialData[2]];
129145

130-
const observer = subject.asObservable();
146+
const observer = state.asObservable();
131147
observer.subscribe((value) => {
132148
expect(value.length).to.be.equal(expectedData.length);
133149
expect(value[1].another).to.be.equal(newItem.another);
@@ -137,9 +153,9 @@ describe('ArrayState', () => {
137153

138154
it('getObservablePart returns the replaced item', (done) => {
139155
const newItem = { key: '2', another: 'myValue4' };
140-
subject.appendOne(newItem);
156+
state.appendOne(newItem);
141157

142-
const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === newItem.key));
158+
const subObserver = state.asObservablePart((data) => data.find((x) => x.key === newItem.key));
143159
subObserver.subscribe((entry) => {
144160
expect(entry).to.be.equal(newItem); // Second callback should give us the right data:
145161
if (entry) {
@@ -152,7 +168,7 @@ describe('ArrayState', () => {
152168
it('getObservablePart replays existing data to any amount of subscribers.', (done) => {
153169
let amountOfCallbacks = 0;
154170

155-
const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === '2'));
171+
const subObserver = state.asObservablePart((data) => data.find((x) => x.key === '2'));
156172
subObserver.subscribe((entry) => {
157173
if (entry) {
158174
amountOfCallbacks++;
@@ -173,7 +189,7 @@ describe('ArrayState', () => {
173189
it('getObservablePart replays existing data to any amount of subscribers.', (done) => {
174190
let amountOfCallbacks = 0;
175191

176-
const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === '2'));
192+
const subObserver = state.asObservablePart((data) => data.find((x) => x.key === '2'));
177193
subObserver.subscribe((entry) => {
178194
if (entry) {
179195
amountOfCallbacks++;
@@ -194,7 +210,7 @@ describe('ArrayState', () => {
194210
it('append only updates observable if changes item', (done) => {
195211
let count = 0;
196212

197-
const observer = subject.asObservable();
213+
const observer = state.asObservable();
198214
observer.subscribe((value) => {
199215
count++;
200216
if (count === 1) {
@@ -212,12 +228,12 @@ describe('ArrayState', () => {
212228

213229
Promise.resolve().then(() => {
214230
// Despite how many times this happens it should not trigger any change.
215-
subject.append(initialData);
216-
subject.append(initialData);
217-
subject.append(initialData);
231+
state.append(initialData);
232+
state.append(initialData);
233+
state.append(initialData);
218234

219235
Promise.resolve().then(() => {
220-
subject.appendOne({ key: '4', another: 'myValue4' });
236+
state.appendOne({ key: '4', another: 'myValue4' });
221237
});
222238
});
223239
});

src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { partialUpdateFrozenArray } from '../utils/partial-update-frozen-array.function.js';
22
import { pushAtToUniqueArray } from '../utils/push-at-to-unique-array.function.js';
33
import { pushToUniqueArray } from '../utils/push-to-unique-array.function.js';
4+
import { replaceInUniqueArray } from '../utils/replace-in-unique-array.function.js';
45
import { UmbDeepState } from './deep-state.js';
56

67
/**
@@ -262,6 +263,38 @@ export class UmbArrayState<T, U = unknown> extends UmbDeepState<T[]> {
262263
return this;
263264
}
264265

266+
/**
267+
* @function replace
268+
* @param {Partial<T>} entires - data of entries to be replaced.
269+
* @returns {UmbArrayState<T>} Reference to it self.
270+
* @description - Replaces one or more entries, requires the ArrayState to be constructed with a getUnique method.
271+
* @example <caption>Example append some data.</caption>
272+
* const data = [
273+
* { key: 1, value: 'foo'},
274+
* { key: 2, value: 'bar'}
275+
* ];
276+
* const myState = new UmbArrayState(data, (x) => x.key);
277+
* const updates = [
278+
* { key: 1, value: 'foo2'},
279+
* { key: 3, value: 'bar2'}
280+
* ];
281+
* myState.replace(updates);
282+
* // Only the existing item gets replaced:
283+
* myState.getValue(); // -> [{ key: 1, value: 'foo2'}, { key: 2, value: 'bar'}]
284+
*/
285+
replace(entries: Array<T>): UmbArrayState<T> {
286+
if (this.getUniqueMethod) {
287+
const next = [...this.getValue()];
288+
entries.forEach((entry) => {
289+
replaceInUniqueArray(next, entry as T, this.getUniqueMethod!);
290+
});
291+
this.setValue(next);
292+
} else {
293+
throw new Error("Can't replace entries of an ArrayState without a getUnique method provided when constructed.");
294+
}
295+
return this;
296+
}
297+
265298
/**
266299
* @function updateOne
267300
* @param {U} unique - Unique value to find entry to update.

src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export * from './observe-multiple.function.js';
1212
export * from './partial-update-frozen-array.function.js';
1313
export * from './push-at-to-unique-array.function.js';
1414
export * from './push-to-unique-array.function.js';
15+
export * from './replace-in-unique-array.function.js';
1516
export * from './simple-hash-code.function.js';
1617
export * from './strict-equality-memoization.function.js';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @function replaceInUniqueArray
3+
* @param {T[]} data - An array of objects.
4+
* @param {T} entry - The object to replace with.
5+
* @param {getUniqueMethod: (entry: T) => unknown} [getUniqueMethod] - Method to get the unique value of an entry.
6+
* @description - Replaces an item of an Array.
7+
* @example <caption>Example replace an entry of an Array. Where the key is unique and the item will only be replaced if matched with existing.</caption>
8+
* const data = [{key: 'myKey', value:'initialValue'}];
9+
* const entry = {key: 'myKey', value: 'replacedValue'};
10+
* const newDataSet = replaceInUniqueArray(data, entry, x => x.key === key);
11+
*/
12+
export function replaceInUniqueArray<T>(data: T[], entry: T, getUniqueMethod: (entry: T) => unknown): T[] {
13+
const unique = getUniqueMethod(entry);
14+
const indexToReplace = data.findIndex((x) => getUniqueMethod(x) === unique);
15+
if (indexToReplace !== -1) {
16+
data[indexToReplace] = entry;
17+
}
18+
return data;
19+
}

src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ export class UmbPickerInputContext<
133133
#removeItem(unique: string) {
134134
const newSelection = this.getSelection().filter((value) => value !== unique);
135135
this.setSelection(newSelection);
136-
this.#itemManager.removeStatus(unique);
137136
this.getHostElement().dispatchEvent(new UmbChangeEvent());
138137
}
139138
}

src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,20 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
7575
(uniques) => {
7676
if (uniques.length === 0) {
7777
this.#items.setValue([]);
78+
this.#statuses.setValue([]);
7879
return;
7980
}
8081

8182
// TODO: This could be optimized so we only load the appended items, but this requires that the response checks that an item is still present in uniques. [NL]
82-
// Check if we already have the items, and then just sort them:
83-
const items = this.#items.getValue();
83+
// Check if we already have the statuses, and then just sort them:
84+
const statuses = this.#statuses.getValue();
8485
if (
85-
uniques.length === items.length &&
86-
uniques.every((unique) => items.find((item) => this.#getUnique(item) === unique))
86+
uniques.length === statuses.length &&
87+
uniques.every((unique) => statuses.find((status) => status.unique === unique))
8788
) {
89+
const items = this.#items.getValue();
8890
this.#items.setValue(this.#sortByUniques(items));
91+
this.#statuses.setValue(this.#sortByUniques(statuses));
8992
} else {
9093
// We need to load new items, so ...
9194
this.#requestItems();
@@ -124,9 +127,17 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
124127
return this.#items.asObservablePart((items) => items.find((item) => this.#getUnique(item) === unique));
125128
}
126129

130+
/**
131+
* @deprecated - This is resolved by setUniques, no need to update statuses.
132+
* @param unique {string} - The unique identifier of the item to remove the status of.
133+
*/
127134
removeStatus(unique: string) {
128-
const newStatuses = this.#statuses.getValue().filter((status) => status.unique !== unique);
129-
this.#statuses.setValue(newStatuses);
135+
new UmbDeprecation({
136+
removeInVersion: '18.0.0',
137+
deprecated: 'removeStatus',
138+
solution: 'Statuses are removed automatically when setting uniques',
139+
}).warn();
140+
this.#statuses.filter((status) => status.unique !== unique);
130141
}
131142

132143
async getItemByUnique(unique: string) {
@@ -144,6 +155,7 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
144155
const requestedUniques = this.getUniques();
145156

146157
this.#statuses.setValue(
158+
// No need to do sorting here as we just got the unique in the right order above.
147159
requestedUniques.map((unique) => ({
148160
state: {
149161
type: 'loading',
@@ -164,7 +176,7 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
164176
}
165177

166178
if (error) {
167-
this.#statuses.append(
179+
this.#statuses.replace(
168180
requestedUniques.map((unique) => ({
169181
state: {
170182
type: 'error',
@@ -185,7 +197,7 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
185197
const resolvedUniques = requestedUniques.filter((unique) => !rejectedUniques.includes(unique));
186198
this.#items.remove(rejectedUniques);
187199

188-
this.#statuses.append([
200+
this.#statuses.replace([
189201
...rejectedUniques.map(
190202
(unique) =>
191203
({
@@ -226,12 +238,11 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
226238
const { data, error } = await this.repository.requestItems([unique]);
227239

228240
if (error) {
229-
this.#statuses.appendOne({
241+
this.#statuses.updateOne(unique, {
230242
state: {
231243
type: 'error',
232244
error: '#general_notFound',
233245
},
234-
unique,
235246
} as UmbRepositoryItemsStatus);
236247
}
237248

@@ -244,11 +255,12 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
244255
const newItems = [...items];
245256
newItems[index] = data[0];
246257
this.#items.setValue(this.#sortByUniques(newItems));
258+
// No need to update statuses here, as the item is the same, just updated.
247259
}
248260
}
249261
}
250262

251-
#sortByUniques(data?: Array<ItemType>): Array<ItemType> {
263+
#sortByUniques<T extends Pick<ItemType, 'unique'>>(data?: Array<T>): Array<T> {
252264
if (!data) return [];
253265
const uniques = this.getUniques();
254266
return [...data].sort((a, b) => {

0 commit comments

Comments
 (0)