@open-mercato/ui 0.6.7-develop.6677.1.beabb7ca12 → 0.6.7-develop.6685.1.77c0a5591b
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.
|
@@ -24,32 +24,27 @@ function arraysEqual(a, b) {
|
|
|
24
24
|
return true;
|
|
25
25
|
}
|
|
26
26
|
function useGroupOrder(pageType, defaultGroupIds) {
|
|
27
|
-
const [
|
|
28
|
-
const mounted = React.useRef(false);
|
|
27
|
+
const [savedOrder, setSavedOrder] = React.useState(null);
|
|
29
28
|
React.useEffect(() => {
|
|
30
|
-
mounted.current = true;
|
|
31
29
|
const saved = readJsonFromLocalStorage(getStorageKey(pageType), null);
|
|
32
|
-
|
|
33
|
-
setOrderedIds(mergeOrder(saved, defaultGroupIds));
|
|
34
|
-
}
|
|
30
|
+
setSavedOrder(Array.isArray(saved) ? saved : null);
|
|
35
31
|
}, [pageType]);
|
|
36
|
-
React.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
const mergedIds = React.useMemo(
|
|
33
|
+
() => savedOrder ? mergeOrder(savedOrder, defaultGroupIds) : defaultGroupIds,
|
|
34
|
+
[defaultGroupIds, savedOrder]
|
|
35
|
+
);
|
|
36
|
+
const stableIdsRef = React.useRef(mergedIds);
|
|
37
|
+
if (!arraysEqual(stableIdsRef.current, mergedIds)) {
|
|
38
|
+
stableIdsRef.current = mergedIds;
|
|
39
|
+
}
|
|
40
|
+
const orderedIds = stableIdsRef.current;
|
|
42
41
|
const reorder = React.useCallback(
|
|
43
42
|
(fromIndex, toIndex) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
writeJsonToLocalStorage(getStorageKey(pageType), next);
|
|
50
|
-
}
|
|
51
|
-
return next;
|
|
52
|
-
});
|
|
43
|
+
const next = [...stableIdsRef.current];
|
|
44
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
45
|
+
next.splice(toIndex, 0, moved);
|
|
46
|
+
writeJsonToLocalStorage(getStorageKey(pageType), next);
|
|
47
|
+
setSavedOrder(next);
|
|
53
48
|
},
|
|
54
49
|
[pageType]
|
|
55
50
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/crud/useGroupOrder.ts"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport {\n readJsonFromLocalStorage,\n writeJsonToLocalStorage,\n} from '@open-mercato/shared/lib/browser/safeLocalStorage'\n\nconst STORAGE_PREFIX = 'om:group-order:'\n\nfunction getStorageKey(pageType: string) {\n return `${STORAGE_PREFIX}${pageType}`\n}\n\nfunction mergeOrder(saved: string[], defaults: string[]): string[] {\n const known = new Set(defaults)\n const result = saved.filter((id) => known.has(id))\n for (const id of defaults) {\n if (!result.includes(id)) result.push(id)\n }\n return result\n}\n\nfunction arraysEqual(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n\n/**\n * Returns the group IDs in the user's preferred order.\n * Falls back to the default order when no preference is stored.\n */\nexport function useGroupOrder(pageType: string, defaultGroupIds: string[]) {\n const [
|
|
5
|
-
"mappings": ";AACA,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,iBAAiB;AAEvB,SAAS,cAAc,UAAkB;AACvC,SAAO,GAAG,cAAc,GAAG,QAAQ;AACrC;AAEA,SAAS,WAAW,OAAiB,UAA8B;AACjE,QAAM,QAAQ,IAAI,IAAI,QAAQ;AAC9B,QAAM,SAAS,MAAM,OAAO,CAAC,OAAO,MAAM,IAAI,EAAE,CAAC;AACjD,aAAW,MAAM,UAAU;AACzB,QAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO,KAAK,EAAE;AAAA,EAC1C;AACA,SAAO;AACT;AAEA,SAAS,YAAY,GAAa,GAAsB;AACtD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAAG;AACpC,QAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAAA,EAC5B;AACA,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport {\n readJsonFromLocalStorage,\n writeJsonToLocalStorage,\n} from '@open-mercato/shared/lib/browser/safeLocalStorage'\n\nconst STORAGE_PREFIX = 'om:group-order:'\n\nfunction getStorageKey(pageType: string) {\n return `${STORAGE_PREFIX}${pageType}`\n}\n\nfunction mergeOrder(saved: string[], defaults: string[]): string[] {\n const known = new Set(defaults)\n const result = saved.filter((id) => known.has(id))\n for (const id of defaults) {\n if (!result.includes(id)) result.push(id)\n }\n return result\n}\n\nfunction arraysEqual(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n\n/**\n * Returns the group IDs in the user's preferred order.\n * Falls back to the default order when no preference is stored.\n *\n * State holds only the saved preference; the visible order is derived during\n * render. Syncing derived order back into state via an effect looped forever\n * when a host recreated `defaultGroupIds` with different content on every\n * render (#4386), so no effect writes state from `defaultGroupIds` here.\n */\nexport function useGroupOrder(pageType: string, defaultGroupIds: string[]) {\n const [savedOrder, setSavedOrder] = React.useState<string[] | null>(null)\n\n React.useEffect(() => {\n const saved = readJsonFromLocalStorage<string[] | null>(getStorageKey(pageType), null)\n setSavedOrder(Array.isArray(saved) ? saved : null)\n }, [pageType])\n\n const mergedIds = React.useMemo(\n () => (savedOrder ? mergeOrder(savedOrder, defaultGroupIds) : defaultGroupIds),\n [defaultGroupIds, savedOrder],\n )\n\n const stableIdsRef = React.useRef(mergedIds)\n if (!arraysEqual(stableIdsRef.current, mergedIds)) {\n stableIdsRef.current = mergedIds\n }\n const orderedIds = stableIdsRef.current\n\n const reorder = React.useCallback(\n (fromIndex: number, toIndex: number) => {\n const next = [...stableIdsRef.current]\n const [moved] = next.splice(fromIndex, 1)\n next.splice(toIndex, 0, moved)\n writeJsonToLocalStorage(getStorageKey(pageType), next)\n setSavedOrder(next)\n },\n [pageType],\n )\n\n return { orderedIds, reorder }\n}\n"],
|
|
5
|
+
"mappings": ";AACA,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,iBAAiB;AAEvB,SAAS,cAAc,UAAkB;AACvC,SAAO,GAAG,cAAc,GAAG,QAAQ;AACrC;AAEA,SAAS,WAAW,OAAiB,UAA8B;AACjE,QAAM,QAAQ,IAAI,IAAI,QAAQ;AAC9B,QAAM,SAAS,MAAM,OAAO,CAAC,OAAO,MAAM,IAAI,EAAE,CAAC;AACjD,aAAW,MAAM,UAAU;AACzB,QAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO,KAAK,EAAE;AAAA,EAC1C;AACA,SAAO;AACT;AAEA,SAAS,YAAY,GAAa,GAAsB;AACtD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAAG;AACpC,QAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAAA,EAC5B;AACA,SAAO;AACT;AAWO,SAAS,cAAc,UAAkB,iBAA2B;AACzE,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAA0B,IAAI;AAExE,QAAM,UAAU,MAAM;AACpB,UAAM,QAAQ,yBAA0C,cAAc,QAAQ,GAAG,IAAI;AACrF,kBAAc,MAAM,QAAQ,KAAK,IAAI,QAAQ,IAAI;AAAA,EACnD,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,YAAY,MAAM;AAAA,IACtB,MAAO,aAAa,WAAW,YAAY,eAAe,IAAI;AAAA,IAC9D,CAAC,iBAAiB,UAAU;AAAA,EAC9B;AAEA,QAAM,eAAe,MAAM,OAAO,SAAS;AAC3C,MAAI,CAAC,YAAY,aAAa,SAAS,SAAS,GAAG;AACjD,iBAAa,UAAU;AAAA,EACzB;AACA,QAAM,aAAa,aAAa;AAEhC,QAAM,UAAU,MAAM;AAAA,IACpB,CAAC,WAAmB,YAAoB;AACtC,YAAM,OAAO,CAAC,GAAG,aAAa,OAAO;AACrC,YAAM,CAAC,KAAK,IAAI,KAAK,OAAO,WAAW,CAAC;AACxC,WAAK,OAAO,SAAS,GAAG,KAAK;AAC7B,8BAAwB,cAAc,QAAQ,GAAG,IAAI;AACrD,oBAAc,IAAI;AAAA,IACpB;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,SAAO,EAAE,YAAY,QAAQ;AAC/B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6685.1.77c0a5591b",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,13 +155,13 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.7-develop.6685.1.77c0a5591b",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
164
|
+
"@open-mercato/shared": "0.6.7-develop.6685.1.77c0a5591b",
|
|
165
165
|
"@testing-library/dom": "^10.4.1",
|
|
166
166
|
"@testing-library/jest-dom": "^6.9.1",
|
|
167
167
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @jest-environment jsdom */
|
|
2
|
-
import
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { act, render, renderHook, screen } from '@testing-library/react'
|
|
3
4
|
import { useGroupOrder } from '../useGroupOrder'
|
|
4
5
|
|
|
5
6
|
describe('useGroupOrder', () => {
|
|
@@ -60,4 +61,31 @@ describe('useGroupOrder', () => {
|
|
|
60
61
|
expect(spy).not.toHaveBeenCalledWith('om:group-order:p', expect.anything())
|
|
61
62
|
spy.mockRestore()
|
|
62
63
|
})
|
|
64
|
+
|
|
65
|
+
it('keeps a stable orderedIds identity when defaults are recreated with equal content', () => {
|
|
66
|
+
const { result, rerender } = renderHook(
|
|
67
|
+
({ ids }) => useGroupOrder('p', ids),
|
|
68
|
+
{ initialProps: { ids: ['a', 'b'] } },
|
|
69
|
+
)
|
|
70
|
+
const first = result.current.orderedIds
|
|
71
|
+
rerender({ ids: ['a', 'b'] })
|
|
72
|
+
expect(result.current.orderedIds).toBe(first)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('does not loop when the host recreates defaults with different content on every render (#4386)', () => {
|
|
76
|
+
localStorage.setItem('om:group-order:unstable', JSON.stringify(['b', 'a']))
|
|
77
|
+
let renders = 0
|
|
78
|
+
function UnstableHost() {
|
|
79
|
+
renders += 1
|
|
80
|
+
if (renders > 50) throw new Error('render loop detected')
|
|
81
|
+
const defaults = renders % 2 === 1 ? ['a', 'b'] : ['a', 'b', `extra-${renders}`]
|
|
82
|
+
const { orderedIds } = useGroupOrder('unstable', defaults)
|
|
83
|
+
return React.createElement('div', { 'data-testid': 'order' }, orderedIds.join(','))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render(React.createElement(UnstableHost))
|
|
87
|
+
|
|
88
|
+
expect(renders).toBeLessThanOrEqual(6)
|
|
89
|
+
expect(screen.getByTestId('order').textContent).toContain('b,a')
|
|
90
|
+
})
|
|
63
91
|
})
|
|
@@ -31,41 +31,38 @@ function arraysEqual(a: string[], b: string[]): boolean {
|
|
|
31
31
|
/**
|
|
32
32
|
* Returns the group IDs in the user's preferred order.
|
|
33
33
|
* Falls back to the default order when no preference is stored.
|
|
34
|
+
*
|
|
35
|
+
* State holds only the saved preference; the visible order is derived during
|
|
36
|
+
* render. Syncing derived order back into state via an effect looped forever
|
|
37
|
+
* when a host recreated `defaultGroupIds` with different content on every
|
|
38
|
+
* render (#4386), so no effect writes state from `defaultGroupIds` here.
|
|
34
39
|
*/
|
|
35
40
|
export function useGroupOrder(pageType: string, defaultGroupIds: string[]) {
|
|
36
|
-
const [
|
|
37
|
-
const mounted = React.useRef(false)
|
|
41
|
+
const [savedOrder, setSavedOrder] = React.useState<string[] | null>(null)
|
|
38
42
|
|
|
39
43
|
React.useEffect(() => {
|
|
40
|
-
mounted.current = true
|
|
41
44
|
const saved = readJsonFromLocalStorage<string[] | null>(getStorageKey(pageType), null)
|
|
42
|
-
|
|
43
|
-
setOrderedIds(mergeOrder(saved, defaultGroupIds))
|
|
44
|
-
}
|
|
45
|
-
// Intentionally only runs on mount (per pageType); defaultGroupIds changes are
|
|
46
|
-
// handled by the sync effect below.
|
|
47
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
|
+
setSavedOrder(Array.isArray(saved) ? saved : null)
|
|
48
46
|
}, [pageType])
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
const mergedIds = React.useMemo(
|
|
49
|
+
() => (savedOrder ? mergeOrder(savedOrder, defaultGroupIds) : defaultGroupIds),
|
|
50
|
+
[defaultGroupIds, savedOrder],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const stableIdsRef = React.useRef(mergedIds)
|
|
54
|
+
if (!arraysEqual(stableIdsRef.current, mergedIds)) {
|
|
55
|
+
stableIdsRef.current = mergedIds
|
|
56
|
+
}
|
|
57
|
+
const orderedIds = stableIdsRef.current
|
|
57
58
|
|
|
58
59
|
const reorder = React.useCallback(
|
|
59
60
|
(fromIndex: number, toIndex: number) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
writeJsonToLocalStorage(getStorageKey(pageType), next)
|
|
66
|
-
}
|
|
67
|
-
return next
|
|
68
|
-
})
|
|
61
|
+
const next = [...stableIdsRef.current]
|
|
62
|
+
const [moved] = next.splice(fromIndex, 1)
|
|
63
|
+
next.splice(toIndex, 0, moved)
|
|
64
|
+
writeJsonToLocalStorage(getStorageKey(pageType), next)
|
|
65
|
+
setSavedOrder(next)
|
|
69
66
|
},
|
|
70
67
|
[pageType],
|
|
71
68
|
)
|