@@ -76,7 +76,13 @@ import { ObjectViewRecordOverlay } from './objectViewRecordOverlay.js';
7676import { useObjectViewListRenderer } from './objectViewListRenderer.js' ;
7777import { ListPersonalizedMarker } from './ListPersonalizedMarker.js' ;
7878import { useObjectViewViewCrud } from './objectViewViewCrud.js' ;
79- import { PERSONAL_VIEW_OBJECT , buildPersonalViewConfig , personalViewEntry , usePersonalViews } from './personalViews.js' ;
79+ import {
80+ PERSONAL_VIEW_OBJECT ,
81+ buildPersonalViewConfig ,
82+ personalViewEntry ,
83+ personalViewPanelFilters ,
84+ usePersonalViews ,
85+ } from './personalViews.js' ;
8086import { PersonalViewNameDialog } from './PersonalViewNameDialog.js' ;
8187import { parseListStateParams } from './userFilterUrlState.js' ;
8288import { FILTER_FOLD_REFUSAL_KEYS } from './viewFilterFold.js' ;
@@ -657,6 +663,22 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
657663 const activeView = viewDraft && viewDraft . id === baseView ?. id
658664 ? { ...baseView , ...viewDraft }
659665 : baseView ;
666+ // The list's toolbar state is read through this field gate wherever it is
667+ // read (the seed, Save as my view, 保存): a field the object does not
668+ // declare, or one this reader cannot read, is no list state.
669+ const acceptListField = ( field : string ) =>
670+ ! ! objectDef . fields ?. [ field ]
671+ && ( ! perms . isLoaded || perms . checkField ( objectDef . name , field , 'read' ) ) ;
672+ // objectstack#30073 — a personal view's own conditions are Filter panel
673+ // rows (`personalViewPanelFilters`), edited there and saved back by 保存.
674+ // Keyed on the stored conditions' content (ui/AGENTS.md #10).
675+ const activeIsPersonal = ! ! activeView ?. id && personalViewIds . has ( activeView . id ) ;
676+ const panelFiltersKey = activeIsPersonal ? JSON . stringify ( ( activeView as any ) ?. filter ?? null ) : '' ;
677+ const viewPanelFilters = useMemo (
678+ ( ) => ( panelFiltersKey ? personalViewPanelFilters ( activeView , acceptListField ) : undefined ) ,
679+ // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on the content the fold reads
680+ [ panelFiltersKey , objectDef , perms . isLoaded ] ,
681+ ) ;
660682
661683 // objectstack#25992 slice 8: the header toolbar actions, the nav `runAction` deep link
662684 // and the `action:bar` node moved to `./objectViewHeaderToolbar.ts`.
@@ -675,6 +697,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
675697 handleListPersonalizationChange,
676698 listPersonalizedParts,
677699 handleResetView,
700+ clearSourceListState,
678701 } = useObjectViewListUrlState ( {
679702 objectName,
680703 objectDef,
@@ -689,8 +712,42 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
689712 // objectstack#28950 — the 「Personalized」 marker beside the list title (the
690713 // page header's badge slot, and the phone title): drawn only while the
691714 // reader's list differs from the view, and the one way back to it.
715+ // objectstack#30073 — 保存: the list on screen written back into the open
716+ // personal view, through the body Save as my view builds (so the stored
717+ // shape is the same), then the reader's list state for it is cleared and
718+ // the list opens again on the saved view. A panel the fold cannot write
719+ // losslessly keeps the refusal Save as my view gives.
720+ const canUpdatePersonalView = activeIsPersonal
721+ && personalViews . status === 'ready'
722+ && ( ! perms . isLoaded || perms . can ( PERSONAL_VIEW_OBJECT , 'update' ) ) ;
723+ const handleSaveToPersonalView = async ( ) => {
724+ if ( ! activeView || ! activeIsPersonal ) return ;
725+ const built = buildPersonalViewConfig ( objectDef . name , activeView , {
726+ url : parseListStateParams ( searchParams , acceptListField ) ,
727+ preference : listPrefs . preference . perView ?. [ activeView . id ] ,
728+ panelFilters : viewPanelFilters ,
729+ } ) ;
730+ if ( ! built . ok ) {
731+ toast . error ( t ( 'form.saveError' ) , { description : t ( FILTER_FOLD_REFUSAL_KEYS [ built . reason ] ) } ) ;
732+ return ;
733+ }
734+ try {
735+ await personalViews . update ( activeView . id , built . config ) ;
736+ handleResetView ( ) ;
737+ } catch ( err ) {
738+ console . error ( '[ObjectView] Failed to save a personal view:' , err ) ;
739+ toast . error ( t ( 'form.saveError' ) , {
740+ description : err instanceof Error ? err . message : String ( err ) ,
741+ classNames : { description : 'whitespace-pre-line' } ,
742+ } ) ;
743+ }
744+ } ;
692745 const personalizedMarker = ! isEnvironmentList && listPersonalizedParts . length > 0 ? (
693- < ListPersonalizedMarker parts = { listPersonalizedParts } onReset = { handleResetView } />
746+ < ListPersonalizedMarker
747+ parts = { listPersonalizedParts }
748+ onReset = { handleResetView }
749+ onSave = { canUpdatePersonalView ? ( ) => { void handleSaveToPersonalView ( ) ; } : undefined }
750+ />
694751 ) : null ;
695752
696753 // objectstack#25992 slice 11b: the view-config save and create handlers, view switching
@@ -750,19 +807,22 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
750807 if ( ! activeView ) return false ;
751808 // The list's toolbar state is the address's (`uf_*`), through the same
752809 // field gate its seed reads it with.
753- const acceptField = ( field : string ) =>
754- ! ! objectDef . fields ?. [ field ]
755- && ( ! perms . isLoaded || perms . checkField ( objectDef . name , field , 'read' ) ) ;
756810 const built = buildPersonalViewConfig ( objectDef . name , activeView , {
757- url : parseListStateParams ( searchParams , acceptField ) ,
811+ url : parseListStateParams ( searchParams , acceptListField ) ,
758812 preference : listPrefs . preference . perView ?. [ activeView . id ] ,
813+ panelFilters : viewPanelFilters ,
759814 } ) ;
760815 if ( ! built . ok ) {
761816 toast . error ( t ( 'form.saveError' ) , { description : t ( FILTER_FOLD_REFUSAL_KEYS [ built . reason ] ) } ) ;
762817 return false ;
763818 }
764819 try {
765- openPersonalView ( await personalViews . save ( label , built . config ) ) ;
820+ const savedId = await personalViews . save ( label , built . config ) ;
821+ // objectstack#30073 — the list now lives in the saved view; the
822+ // view it came from returns to its own (no temporary conditions,
823+ // not personalized).
824+ clearSourceListState ( activeView . id ) ;
825+ openPersonalView ( savedId ) ;
766826 return true ;
767827 } catch ( err ) {
768828 console . error ( '[ObjectView] Failed to save a personal view:' , err ) ;
@@ -1006,6 +1066,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
10061066 initialUfSelections,
10071067 handleUserFilterSelectionsChange,
10081068 handleListTotalChange,
1069+ viewPanelFilters,
10091070 } ) ;
10101071
10111072 // objectstack#25992 slice 9: the merged views and the plugin's `ObjectViewSchema`
0 commit comments