@@ -5,15 +5,13 @@ import { describe, expect, it } from 'vitest'
55import type { ForkDependentReconfig } from '@/lib/api/contracts/workspace-fork'
66import {
77 applyDependentRepick ,
8- DEPENDENT_CLEARED_BY_PARENT ,
98 dependentKey ,
109 effectiveCopyDependentValue ,
1110 effectiveDependentValue ,
1211 getActionableDependentFields ,
1312 getDisplayedDependentFields ,
14- isDependentClearedByParent ,
1513 isDependentConfigurationActionable ,
16- submittedDependentValue ,
14+ isDependentInvalidated ,
1715} from '@/ee/workspace-forking/components/fork-sync/dependent-value'
1816
1917const field = ( overrides : Partial < ForkDependentReconfig > = { } ) : ForkDependentReconfig => ( {
@@ -34,6 +32,11 @@ const field = (overrides: Partial<ForkDependentReconfig> = {}): ForkDependentRec
3432 ...overrides ,
3533} )
3634
35+ const mappedRepickContext = ( previousValue : string ) => ( {
36+ previousValue,
37+ baselineValueFor : ( dependent : ForkDependentReconfig ) => dependent . currentValue ,
38+ } )
39+
3740describe ( 'dependentKey' , ( ) => {
3841 it ( 'keys by target workflow + block + subblock' , ( ) => {
3942 expect (
@@ -141,14 +144,15 @@ describe('applyDependentRepick', () => {
141144 previous ,
142145 site ,
143146 [ site , drive , spreadsheet , sheet , unrelated ] ,
144- 'site-new'
147+ 'site-new' ,
148+ mappedRepickContext ( 'site-old' )
145149 )
146150
147151 expect ( next ) . toEqual ( {
148152 [ dependentKey ( site ) ] : 'site-new' ,
149- [ dependentKey ( drive ) ] : DEPENDENT_CLEARED_BY_PARENT ,
150- [ dependentKey ( spreadsheet ) ] : DEPENDENT_CLEARED_BY_PARENT ,
151- [ dependentKey ( sheet ) ] : DEPENDENT_CLEARED_BY_PARENT ,
153+ [ dependentKey ( drive ) ] : null ,
154+ [ dependentKey ( spreadsheet ) ] : null ,
155+ [ dependentKey ( sheet ) ] : null ,
152156 [ dependentKey ( unrelated ) ] : 'still-keep-me' ,
153157 } )
154158 expect ( effectiveDependentValue ( drive , next , false ) ) . toBe ( '' )
@@ -172,10 +176,10 @@ describe('applyDependentRepick', () => {
172176 spreadsheet ,
173177 [ spreadsheet , range ] ,
174178 'sheet-doc' ,
175- effectiveDependentValue ( spreadsheet , { } , false )
179+ mappedRepickContext ( effectiveDependentValue ( spreadsheet , { } , false ) )
176180 )
177181
178- expect ( next ) . toEqual ( { [ dependentKey ( spreadsheet ) ] : 'sheet-doc' } )
182+ expect ( next ) . toEqual ( { } )
179183 expect ( effectiveDependentValue ( range , next , false ) ) . toBe ( 'Sheet1!A1:D' )
180184 } )
181185
@@ -188,7 +192,8 @@ describe('applyDependentRepick', () => {
188192 { [ dependentKey ( unrelated ) ] : 'still-keep-me' } ,
189193 leaf ,
190194 [ leaf , unrelated ] ,
191- 'ISSUE-2'
195+ 'ISSUE-2' ,
196+ mappedRepickContext ( 'ISSUE-1' )
192197 )
193198 ) . toEqual ( {
194199 [ dependentKey ( leaf ) ] : 'ISSUE-2' ,
@@ -227,97 +232,105 @@ describe('applyDependentRepick', () => {
227232 previous ,
228233 projectOne ,
229234 [ projectOne , issueOne , projectTwo , issueTwo ] ,
230- 'P1-NEW'
235+ 'P1-NEW' ,
236+ mappedRepickContext ( 'INBOX' )
231237 )
232238 ) . toEqual ( {
233239 [ dependentKey ( projectOne ) ] : 'P1-NEW' ,
234- [ dependentKey ( issueOne ) ] : DEPENDENT_CLEARED_BY_PARENT ,
240+ [ dependentKey ( issueOne ) ] : null ,
235241 [ dependentKey ( issueTwo ) ] : 'P2-1' ,
236242 } )
237243 } )
238- } )
239244
240- describe ( 'submittedDependentValue' , ( ) => {
241- const mappedParent = { copying : false , parentChanged : false }
242-
243- it ( 'omits an optional descendant a parent re-pick blanked, so the target keeps its value' , ( ) => {
245+ it ( 'restores the stored chain when a provider is changed and then returned to baseline' , ( ) => {
244246 const spreadsheet = field ( {
245247 subBlockKey : 'spreadsheetId' ,
246248 currentValue : 'doc-old' ,
247249 providesContextKey : 'spreadsheetId' ,
248250 } )
249- const sheet = field ( {
250- subBlockKey : 'sheetName' ,
251- currentValue : 'Sheet1' ,
252- required : true ,
253- consumesContextKeys : [ 'spreadsheetId' ] ,
254- } )
255251 const range = field ( {
256252 subBlockKey : 'range' ,
257253 currentValue : 'A1:D50' ,
258- required : false ,
259254 consumesContextKeys : [ 'spreadsheetId' ] ,
260255 } )
261256
262- const next = applyDependentRepick (
257+ const changed = applyDependentRepick (
263258 { } ,
264259 spreadsheet ,
265- [ spreadsheet , sheet , range ] ,
260+ [ spreadsheet , range ] ,
266261 'doc-new' ,
267- effectiveDependentValue ( spreadsheet , { } , false )
262+ mappedRepickContext ( 'doc-old' )
263+ )
264+ const restored = applyDependentRepick (
265+ changed ,
266+ spreadsheet ,
267+ [ spreadsheet , range ] ,
268+ 'doc-old' ,
269+ mappedRepickContext ( 'doc-new' )
268270 )
269271
270- expect ( isDependentClearedByParent ( range , next ) ) . toBe ( true )
271- expect ( submittedDependentValue ( range , next , mappedParent ) ) . toBeUndefined ( )
272- expect ( submittedDependentValue ( sheet , next , mappedParent ) ) . toBeUndefined ( )
273- expect ( submittedDependentValue ( spreadsheet , next , mappedParent ) ) . toBe ( 'doc-new' )
272+ expect ( changed ) . toEqual ( {
273+ [ dependentKey ( spreadsheet ) ] : 'doc-new' ,
274+ [ dependentKey ( range ) ] : null ,
275+ } )
276+ expect ( restored ) . toEqual ( { } )
277+ expect ( effectiveDependentValue ( range , restored , false ) ) . toBe ( 'A1:D50' )
274278 } )
275279
276- it ( 'submits an empty value the user picked themselves, so an explicit clear still clears ' , ( ) => {
280+ it ( 'keeps an intentional empty pick distinct from automatic invalidation ' , ( ) => {
277281 const label = field ( { subBlockKey : 'label' , currentValue : 'INBOX' } )
278282
279- const next = applyDependentRepick ( { } , label , [ label ] , '' , 'INBOX' )
283+ const next = applyDependentRepick ( { } , label , [ label ] , '' , mappedRepickContext ( 'INBOX' ) )
280284
281- expect ( isDependentClearedByParent ( label , next ) ) . toBe ( false )
282- expect ( submittedDependentValue ( label , next , mappedParent ) ) . toBe ( '' )
285+ expect ( isDependentInvalidated ( label , next ) ) . toBe ( false )
286+ expect ( next [ dependentKey ( label ) ] ) . toBe ( '' )
287+ expect ( effectiveDependentValue ( label , next , false ) ) . toBe ( '' )
283288 } )
284289
285- it ( 'submits a re-picked descendant once the user chooses a replacement' , ( ) => {
290+ it ( 'does not restore an Excel sheet while its drive still differs from baseline' , ( ) => {
291+ const drive = field ( {
292+ subBlockKey : 'driveId' ,
293+ currentValue : 'drive-old' ,
294+ providesContextKey : 'driveId' ,
295+ } )
286296 const spreadsheet = field ( {
287297 subBlockKey : 'spreadsheetId' ,
288- currentValue : 'doc-old' ,
298+ currentValue : 'workbook-old' ,
299+ consumesContextKeys : [ 'driveId' ] ,
289300 providesContextKey : 'spreadsheetId' ,
290301 } )
291- const range = field ( {
292- subBlockKey : 'range ' ,
293- currentValue : 'A1:D50 ' ,
294- consumesContextKeys : [ 'spreadsheetId' ] ,
302+ const sheet = field ( {
303+ subBlockKey : 'sheetName ' ,
304+ currentValue : 'Sheet1 ' ,
305+ consumesContextKeys : [ 'driveId' , ' spreadsheetId'] ,
295306 } )
296307
297- const cleared = applyDependentRepick (
308+ const driveChanged = applyDependentRepick (
298309 { } ,
310+ drive ,
311+ [ drive , spreadsheet , sheet ] ,
312+ 'drive-new' ,
313+ mappedRepickContext ( 'drive-old' )
314+ )
315+ const spreadsheetRepicked = applyDependentRepick (
316+ driveChanged ,
299317 spreadsheet ,
300- [ spreadsheet , range ] ,
301- 'doc -new' ,
302- effectiveDependentValue ( spreadsheet , { } , false )
318+ [ drive , spreadsheet , sheet ] ,
319+ 'workbook -new' ,
320+ mappedRepickContext ( '' )
303321 )
304- const repicked = applyDependentRepick (
305- cleared ,
306- range ,
307- [ spreadsheet , range ] ,
308- 'A1:Z ' ,
309- effectiveDependentValue ( range , cleared , false )
322+ const spreadsheetRestored = applyDependentRepick (
323+ spreadsheetRepicked ,
324+ spreadsheet ,
325+ [ drive , spreadsheet , sheet ] ,
326+ 'workbook-old ' ,
327+ mappedRepickContext ( 'workbook-new' )
310328 )
311329
312- expect ( submittedDependentValue ( range , repicked , mappedParent ) ) . toBe ( 'A1:Z' )
313- } )
314-
315- it ( 'falls back to the stored value under an unchanged parent and to the source when copying' , ( ) => {
316- const untouched = field ( { subBlockKey : 'label' , currentValue : 'INBOX' } )
317- const copied = field ( { subBlockKey : 'documentSelector' , currentValue : '' , sourceValue : 'doc' } )
318-
319- expect ( submittedDependentValue ( untouched , { } , mappedParent ) ) . toBe ( 'INBOX' )
320- expect ( submittedDependentValue ( copied , { } , { copying : true , parentChanged : false } ) ) . toBe ( 'doc' )
330+ expect ( spreadsheetRestored ) . toEqual ( {
331+ [ dependentKey ( drive ) ] : 'drive-new' ,
332+ [ dependentKey ( sheet ) ] : null ,
333+ } )
321334 } )
322335} )
323336
@@ -409,7 +422,7 @@ describe('isDependentConfigurationActionable', () => {
409422 spreadsheet ,
410423 [ spreadsheet , sheet ] ,
411424 'doc-new' ,
412- effectiveDependentValue ( spreadsheet , { } , false )
425+ mappedRepickContext ( effectiveDependentValue ( spreadsheet , { } , false ) )
413426 )
414427
415428 // The sync gate reads the same blank the selector shows, so the field gates and is visible.
@@ -425,7 +438,7 @@ describe('isDependentConfigurationActionable', () => {
425438
426439 it ( 'shows a required field the user emptied themselves (it blocks Sync)' , ( ) => {
427440 const sheet = field ( { subBlockKey : 'sheetName' , required : true , currentValue : 'Sheet1' } )
428- const next = applyDependentRepick ( { } , sheet , [ sheet ] , '' , 'Sheet1' )
441+ const next = applyDependentRepick ( { } , sheet , [ sheet ] , '' , mappedRepickContext ( 'Sheet1' ) )
429442
430443 expect ( effectiveDependentValue ( sheet , next , false ) ) . toBe ( '' )
431444 expect (
0 commit comments