@@ -12,20 +12,30 @@ import {
1212} from ' @ui' ;
1313import { injectListingContext } from ' ../Listing/Listing.vue' ;
1414import { computed , ref , watch } from ' vue' ;
15+ import { deepClone } from ' @/util/clone.js' ;
1516
1617const { preferencesPrefix , activeFilters , searchQuery , setFilters , clearFilters , setSearchQuery , clearSearchQuery } =
1718 injectListingContext ();
1819const preferencesKey = ref (` ${ preferencesPrefix .value } .filters` );
1920const presets = ref (getPresets ());
2021const activePreset = ref (getPresetFromActiveFilters ());
2122const activePresetPayload = computed (() => presets .value [activePreset .value ]);
23+ const selectedPreset = ref (activePreset .value );
24+ const selectedPresetPayload = computed (() => presets .value [selectedPreset .value ]);
2225const savingPresetName = ref (null );
2326const savingPresetHandle = computed (() => snake_case (savingPresetName .value ));
2427const isCreating = ref (false );
2528const isRenaming = ref (false );
2629const isConfirmingDeletion = ref (false );
2730
28- watch ([activeFilters, searchQuery], () => (activePreset .value = getPresetFromActiveFilters ()), { deep: true });
31+ watch (
32+ [activeFilters, searchQuery],
33+ () => {
34+ activePreset .value = getPresetFromActiveFilters ();
35+ selectedPreset .value ?? = activePreset .value ;
36+ },
37+ { deep: true },
38+ );
2939
3040function getPresets () {
3141 return Statamic .$preferences .get (preferencesKey .value , {});
@@ -37,13 +47,15 @@ function refreshPresets() {
3747
3848function viewAll () {
3949 activePreset .value = null ;
50+ selectedPreset .value = null ;
4051 clearFilters ();
4152 clearSearchQuery ();
4253}
4354
4455function selectPreset (handle ) {
4556 activePreset .value = handle;
46- setFilters (activePresetPayload .value .filters );
57+ selectedPreset .value = handle;
58+ setFilters (deepClone (activePresetPayload .value .filters ?? {}));
4759 setSearchQuery (activePresetPayload .value .query );
4860}
4961
@@ -52,23 +64,70 @@ function createPreset() {
5264 isCreating .value = true ;
5365}
5466
55- function canRenamePreset (handle ) {
67+ function canSavePreset (handle ) {
5668 return ! Statamic .$preferences .hasDefault (` ${ preferencesKey .value } .${ handle} ` );
5769}
5870
5971function canDeletePreset (handle ) {
60- return canRenamePreset (handle);
72+ return canSavePreset (handle);
6173}
6274
6375function renamePreset () {
64- savingPresetName .value = activePresetPayload .value .display ;
76+ savingPresetName .value = selectedPresetPayload .value .display ;
6577 isRenaming .value = true ;
6678}
6779
6880const canSaveNewPreset = computed (() => {
69- return ! activePreset .value && (Object .keys (activeFilters .value ).length > 0 || (searchQuery .value ?? ' ' ) !== ' ' );
81+ if (selectedPreset .value ) return isDirty .value ;
82+
83+ return (Object .keys (activeFilters .value ).length > 0 || (searchQuery .value ?? ' ' ) !== ' ' );
84+ });
85+
86+ const isDirty = computed (() => {
87+ if (! selectedPreset .value || ! selectedPresetPayload .value ) return false ;
88+
89+ const savedFilters = selectedPresetPayload .value .filters ?? {};
90+ const savedQuery = selectedPresetPayload .value .query ?? ' ' ;
91+ const currentQuery = searchQuery .value ?? ' ' ;
92+
93+ if (savedQuery !== currentQuery) return true ;
94+
95+ const savedKeys = Object .keys (savedFilters);
96+ const currentKeys = Object .keys (activeFilters .value );
97+
98+ if (savedKeys .length !== currentKeys .length ) return true ;
99+
100+ for (const key of savedKeys) {
101+ if (JSON .stringify (savedFilters[key]) !== JSON .stringify (activeFilters .value [key])) {
102+ return true ;
103+ }
104+ }
105+
106+ return false ;
70107});
71108
109+ function savePreset () {
110+ const payload = { display: selectedPresetPayload .value .display };
111+
112+ if (searchQuery .value ) payload .query = searchQuery .value ;
113+ if (Object .entries (activeFilters .value ).length ) payload .filters = deepClone (activeFilters .value );
114+
115+ Statamic .$preferences
116+ .set (` ${ preferencesKey .value } .${ selectedPreset .value } ` , payload)
117+ .then (() => {
118+ Statamic .$toast .success (__ (' View saved' ));
119+ refreshPresets ();
120+ })
121+ .catch (() => {
122+ Statamic .$toast .error (__ (' Unable to save view' ));
123+ });
124+ }
125+
126+ function resetPreset () {
127+ setFilters (deepClone (selectedPresetPayload .value .filters ?? {}));
128+ setSearchQuery (selectedPresetPayload .value .query ?? ' ' );
129+ }
130+
72131function getPresetFromActiveFilters () {
73132 for (const [handle , preset ] of Object .entries (presets .value )) {
74133 const a = {
@@ -88,7 +147,7 @@ function getPresetFromActiveFilters() {
88147}
89148
90149const currentTab = computed ({
91- get : () => activePreset .value || ' all' ,
150+ get : () => selectedPreset .value || ' all' ,
92151 set : (value ) => {
93152 if (value === ' all' ) {
94153 viewAll ();
@@ -100,7 +159,7 @@ const currentTab = computed({
100159
101160const presetPreferencesPayload = computed (() => {
102161 let payload = {
103- display: savingPresetName .value || activePresetPayload .value .display || ' ' ,
162+ display: savingPresetName .value || selectedPresetPayload .value ? .display || ' ' ,
104163 };
105164
106165 if (searchQuery .value ) payload .query = searchQuery .value ;
@@ -133,7 +192,7 @@ function saveExisting() {
133192
134193 preference = Object .fromEntries (
135194 Object .entries (preference).map (([key , value ]) => {
136- if (key === activePreset .value ) {
195+ if (key === selectedPreset .value ) {
137196 return [savingPresetHandle .value , presetPreferencesPayload .value ];
138197 }
139198
@@ -157,7 +216,7 @@ function saveExisting() {
157216
158217function deletePreset () {
159218 Statamic .$preferences
160- .remove (` ${ preferencesKey .value } .${ activePreset .value } ` )
219+ .remove (` ${ preferencesKey .value } .${ selectedPreset .value } ` )
161220 .then ((response ) => {
162221 Statamic .$toast .success (__ (' View deleted' ));
163222 isConfirmingDeletion .value = false ;
@@ -182,15 +241,15 @@ function deletePreset() {
182241 : name= " handle"
183242 >
184243 {{ preset .display }}
185- < template v- if = " handle === activePreset " >
244+ < template v- if = " handle === selectedPreset " >
186245 < Dropdown class = " w-48!" >
187246 < template #trigger>
188247 < Button class = " absolute! top-0.25 -right-4 starting-style-transition starting-style-transition--slow" variant= " ghost" size= " xs" icon= " chevron-down" / >
189248 < / template>
190249 < DropdownMenu>
191250 < DropdownItem : text= " __('Duplicate')" icon= " duplicate" @click= " createPreset" / >
192251 < DropdownItem
193- v- if = " canRenamePreset (handle)"
252+ v- if = " canSavePreset (handle)"
194253 : text= " __('Rename')"
195254 icon= " rename"
196255 @click= " renamePreset"
@@ -208,8 +267,27 @@ function deletePreset() {
208267 < / template>
209268 < / PresetTrigger>
210269 < / TabList>
211- < div v- if = " canSaveNewPreset" class = " border-b border-gray-200 dark:border-gray-700 relative -top-[2px] hover:border-transparent ps-2" >
270+ < div v- if = " isDirty || canSaveNewPreset" class = " border-b border-gray-200 dark:border-gray-700 relative -top-[2px] hover:border-transparent ps-2 flex gap-1" >
271+ < Button
272+ v- if = " isDirty && canSavePreset(selectedPreset)"
273+ @click= " savePreset"
274+ variant= " ghost"
275+ size= " sm"
276+ : text= " __('Save')"
277+ icon= " save"
278+ class = " [&_svg]:size-4"
279+ / >
280+ < Button
281+ v- if = " isDirty"
282+ @click= " resetPreset"
283+ variant= " ghost"
284+ size= " sm"
285+ : text= " __('Reset')"
286+ icon= " sync"
287+ class = " [&_svg]:size-4"
288+ / >
212289 < Button
290+ v- if = " canSaveNewPreset"
213291 @click= " createPreset"
214292 variant= " ghost"
215293 size= " sm"
@@ -248,7 +326,7 @@ function deletePreset() {
248326 < div
249327 v- if = "
250328 Object.keys(presets)
251- .filter((preset) => preset !== activePreset )
329+ .filter((preset) => preset !== selectedPreset )
252330 .includes(savingPresetHandle)
253331 "
254332 >
0 commit comments