Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/kit/src/runtime/state/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export function setStateValue(
if (newKey && newKey !== field) {
const raw = toRaw(unwrapped)
if (Array.isArray(raw) || raw instanceof Set) return false
if (raw instanceof Map ? raw.has(newKey) : hasOwn(unwrapped as object, newKey)) return false
if (raw instanceof Map ? hasMapPreviewKey(raw, newKey) : hasOwn(unwrapped as object, newKey))
return false
}
const written = writeEditableValue(unwrapped, field, value)
if (!written || !newKey || newKey === field) return written
Expand Down Expand Up @@ -89,6 +90,13 @@ export function addStateValue(target: object, path: string[], value: unknown): b
return addEditableValue(unwrapEditableValue(object), value)
}

function hasMapPreviewKey(target: Map<unknown, unknown>, field: string): boolean {
for (const key of target.keys()) {
if (formatPreviewKey(key) === field) return true
}
return false
}

function resolveMapKey(
target: Map<unknown, unknown>,
field: string,
Expand Down Expand Up @@ -201,7 +209,7 @@ function renameEditableValue(target: unknown, previousField: string, nextField:
const rawTarget = toRaw(target)
if (rawTarget instanceof Map) {
const key = resolveMapKey(rawTarget, previousField)
if (!key.found || rawTarget.has(nextField)) return false
if (!key.found || hasMapPreviewKey(rawTarget, nextField)) return false
const collection = target as Map<unknown, unknown>
const value = collection.get(key.key)
collection.delete(key.key)
Expand Down Expand Up @@ -300,8 +308,9 @@ function getNextAvailableObjectKey(target: object): string {
}

function getNextAvailableMapKey(target: Map<unknown, unknown>): string {
const labels = new Set(Array.from(target.keys(), formatPreviewKey))
for (let i = 1; ; i++) {
const key = `newProp${i}`
if (!target.has(key)) return key
if (!labels.has(key)) return key
}
}
62 changes: 62 additions & 0 deletions tests/unit/runtime/state-edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,68 @@ describe('collection state editing', () => {
])
})

it.each([
{ key: 1, label: '1' },
{ key: true, label: 'true' },
{ key: 1n, label: '1' },
{ key: Symbol('key'), label: 'key' },
{ key: { id: 1 }, label: 'Object' },
{ key: null, label: 'null' },
{ key: undefined, label: 'undefined' },
])('rejects renaming to an existing key label $label before writing', ({ key, label }) => {
const map = new Map<unknown, unknown>([
[key, 'existing'],
['source', 'original'],
])

expect(setStateValue({ map }, ['map', 'source'], 'replacement', label)).toBe(false)
expect([...map]).toEqual([
[key, 'existing'],
['source', 'original'],
])
})

it('does not notify reactive effects when a rename would create a duplicate label', () => {
const map = reactive(
new Map<unknown, unknown>([
[1, 'existing'],
['source', 'original'],
]),
)
let updates = 0
effect(() => {
map.get('source')
updates++
})

expect(setStateValue({ map }, ['map', 'source'], 'replacement', '1')).toBe(false)
expect(updates).toBe(1)
expect(map.get('source')).toBe('original')
})

it('still renames a non-string Map key to an unused label', () => {
const map = new Map<unknown, unknown>([[1, 'original']])

expect(setStateValue({ map }, ['map', '1'], 'replacement', 'count')).toBe(true)
expect([...map]).toEqual([['count', 'replacement']])
})

it('skips existing preview labels when generating a new Map key', () => {
const key = Symbol('newProp1')
const map = new Map<unknown, unknown>([
[key, 'symbol'],
['newProp2', 'string'],
])

expect(addStateValue({ map }, ['map'], 'added')).toBe(true)
expect([...map]).toEqual([
[key, 'symbol'],
['newProp2', 'string'],
['newProp3', 'added'],
])
expect(resolvePath({ map }, ['map', 'newProp1'])).toEqual({ found: true, value: 'symbol' })
})

it.each(['native', 'legacy'])('notifies effects through %s collection edits', (mode) => {
const map = reactive(new Map([['x', 1]]))
const set = reactive(new Set([1]))
Expand Down
Loading