diff --git a/packages/core/src/response.ts b/packages/core/src/response.ts index c787498e2..f3442ca5f 100644 --- a/packages/core/src/response.ts +++ b/packages/core/src/response.ts @@ -211,17 +211,17 @@ export class Response { const propsToMerge = pageResponse.mergeProps || [] const propsToDeepMerge = pageResponse.deepMergeProps || [] - const mergeStrategies = pageResponse.mergeStrategies || [] + const matchPropsOn = pageResponse.matchPropsOn || [] propsToMerge.forEach((prop) => { const incomingProp = pageResponse.props[prop] if (Array.isArray(incomingProp)) { - pageResponse.props[prop] = mergeArrayWithStrategy( + pageResponse.props[prop] = this.mergeOrMatchItems( (currentPage.get().props[prop] || []) as any[], incomingProp, prop, - mergeStrategies + matchPropsOn, ) } else if (typeof incomingProp === 'object' && incomingProp !== null) { pageResponse.props[prop] = { @@ -235,10 +235,10 @@ export class Response { const incomingProp = pageResponse.props[prop] const currentProp = currentPage.get().props[prop] - // Deep merge function to handle nested objects and arrays + // Function to recursively merge objects and arrays const deepMerge = (target: any, source: any, currentKey: string) => { if (Array.isArray(source)) { - return mergeArrayWithStrategy(target, source, currentKey, mergeStrategies) + return this.mergeOrMatchItems(target, source, currentKey, matchPropsOn) } if (typeof source === 'object' && source !== null) { @@ -252,69 +252,75 @@ export class Response { ) } - // If the source is neither an array nor an object, return it directly + // f the source is neither an array nor an object, simply return the it return source } - // Assign the deeply merged result back to props. + // Apply the deep merge and update the page response pageResponse.props[prop] = deepMerge(currentProp, incomingProp, prop) }) pageResponse.props = { ...currentPage.get().props, ...pageResponse.props } } - protected async setRememberedState(pageResponse: Page): Promise { - const rememberedState = await history.getState(history.rememberedState, {}) - - if ( - this.requestParams.all().preserveState && - rememberedState && - pageResponse.component === currentPage.get().component - ) { - pageResponse.rememberedState = rememberedState - } - } + protected mergeOrMatchItems(target: any[], source: any[], currentKey: string, matchPropsOn: string[]) { + // Determine if there's a specific key to match items. + // E.g.: matchPropsOn = ['posts.data.id'] and currentKey = 'posts.data' will match. + const matchOn = matchPropsOn.find((key) => { + const path = key.split('.').slice(0, -1).join('.') + return path === currentKey + }) - protected getScopedErrors(errors: Errors & ErrorBag): Errors { - if (!this.requestParams.all().errorBag) { - return errors + if (!matchOn) { + // No key found to match on, just concatenate the arrays + return [...(Array.isArray(target) ? target : []), ...source] } - return errors[this.requestParams.all().errorBag || ''] || {} - } -} - -function mergeArrayWithStrategy(target: any[], source: any[], currentKey: string, mergeStrategies: string[]) { - // Find the mergeStrategy that matches the currentKey - // For example: posts.data.id matches posts.data - const mergeStrategy = mergeStrategies.find((strategy) => { - const path = strategy.split('.').slice(0, -1).join('.') - return path === currentKey - }) - - if (mergeStrategy) { - const uniqueProperty = mergeStrategy.split('.').pop() || '' + // Extract the unique property name to match items (e.g., 'id' from 'posts.data.id'). + const uniqueProperty = matchOn.split('.').pop() || '' const targetArray = Array.isArray(target) ? target : [] const map = new Map() - targetArray.forEach(item => { + // Populate the map with items from the target array, using the unique property as the key. + // If an item doesn't have the unique property or isn't an object, a unique Symbol is used as the key. + targetArray.forEach((item) => { if (item && typeof item === 'object' && uniqueProperty in item) { map.set(item[uniqueProperty], item) } else { map.set(Symbol(), item) } }) - - source.forEach(item => { + + // Iterate through the source array. If an item's unique property matches an existing key in the map, + // update the item. Otherwise, add the new item to the map. + source.forEach((item) => { if (item && typeof item === 'object' && uniqueProperty in item) { map.set(item[uniqueProperty], item) } else { map.set(Symbol(), item) } }) - + return Array.from(map.values()) } - // No mergeStrategy: default to concatenation - return [...(Array.isArray(target) ? target : []), ...source] + + protected async setRememberedState(pageResponse: Page): Promise { + const rememberedState = await history.getState(history.rememberedState, {}) + + if ( + this.requestParams.all().preserveState && + rememberedState && + pageResponse.component === currentPage.get().component + ) { + pageResponse.rememberedState = rememberedState + } + } + + protected getScopedErrors(errors: Errors & ErrorBag): Errors { + if (!this.requestParams.all().errorBag) { + return errors + } + + return errors[this.requestParams.all().errorBag || ''] || {} + } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d1df8bb01..32c40c06a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -63,7 +63,7 @@ export interface Page { deferredProps?: Record mergeProps?: string[] deepMergeProps?: string[] - mergeStrategies?: string[] + matchPropsOn?: string[] /** @internal */ rememberedState: Record diff --git a/packages/react/test-app/Pages/MergeStrategies.jsx b/packages/react/test-app/Pages/MatchPropsOnKey.jsx similarity index 100% rename from packages/react/test-app/Pages/MergeStrategies.jsx rename to packages/react/test-app/Pages/MatchPropsOnKey.jsx diff --git a/packages/svelte/test-app/Pages/MergeStrategies.svelte b/packages/svelte/test-app/Pages/MatchPropsOnKey.svelte similarity index 100% rename from packages/svelte/test-app/Pages/MergeStrategies.svelte rename to packages/svelte/test-app/Pages/MatchPropsOnKey.svelte diff --git a/packages/vue3/src/app.ts b/packages/vue3/src/app.ts index 97658ea64..51340187a 100755 --- a/packages/vue3/src/app.ts +++ b/packages/vue3/src/app.ts @@ -136,7 +136,7 @@ export function usePage(): Page { deferredProps: computed(() => page.value?.deferredProps), mergeProps: computed(() => page.value?.mergeProps), deepMergeProps: computed(() => page.value?.deepMergeProps), - mergeStrategies: computed(() => page.value?.mergeStrategies), + matchPropsOn: computed(() => page.value?.matchPropsOn), rememberedState: computed(() => page.value?.rememberedState), encryptHistory: computed(() => page.value?.encryptHistory), }) diff --git a/packages/vue3/test-app/Pages/MergeStrategies.vue b/packages/vue3/test-app/Pages/MatchPropsOnKey.vue similarity index 100% rename from packages/vue3/test-app/Pages/MergeStrategies.vue rename to packages/vue3/test-app/Pages/MatchPropsOnKey.vue diff --git a/tests/app/server.js b/tests/app/server.js index 3cc3da425..8b82e6a1b 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -336,7 +336,7 @@ app.get('/deep-merge-props', (req, res) => { }) }) -app.get('/merge-strategies', (req, res) => { +app.get('/match-props-on-key', (req, res) => { const labels = ['first', 'second', 'third', 'fourth', 'fifth'] const perPage = 5 @@ -358,7 +358,7 @@ app.get('/merge-strategies', (req, res) => { })) inertia.render(req, res, { - component: 'MergeStrategies', + component: 'MatchPropsOnKey', props: { bar: new Array(perPage).fill(1), baz: new Array(perPage).fill(1), @@ -377,7 +377,7 @@ app.get('/merge-strategies', (req, res) => { ? {} : { deepMergeProps: ['foo', 'baz'], - mergeStrategies: ['foo.data.id', 'foo.companies.otherId', 'foo.teams.uuid'], + matchPropsOn: ['foo.data.id', 'foo.companies.otherId', 'foo.teams.uuid'], }), }) }) diff --git a/tests/merge-strategies.spec.ts b/tests/match-props-on-key.spec.ts similarity index 98% rename from tests/merge-strategies.spec.ts rename to tests/match-props-on-key.spec.ts index 5e56579d1..d90a32216 100644 --- a/tests/merge-strategies.spec.ts +++ b/tests/match-props-on-key.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test' -test('can merge props with custom strategies', async ({ page }) => { - await page.goto('/merge-strategies') +test('can match props by a key', async ({ page }) => { + await page.goto('/match-props-on-key') await expect(page.getByText('bar count is 5')).toBeVisible() await expect(page.getByText('baz count is 5')).toBeVisible()