238238
239239 <template v-if =" ! restrictedRole && (editingField === ' labels' || labels .length > 0 || isEditable )" >
240240 <v-divider class =" my-4" ></v-divider >
241- <v-list-item class =" metadata__item" >
241+ <v-list-item class =" metadata__item meta-labels " >
242242 <div class =" text-subtitle-2" >{{ $gettext("Labels") }}</div >
243243 <template v-if =" isEditable " #append >
244- <v-icon
245- v-if =" editingField === 'labels'"
246- icon =" mdi-check"
247- size =" small"
248- class =" meta-inline-confirm"
249- @mousedown.prevent
250- @click.stop =" confirmLabels"
251- ></v-icon >
252- <v-icon v-else icon =" mdi-pencil-outline" size =" small" class =" meta-inline-pencil" @click.stop =" startChipEditing('labels')" ></v-icon >
244+ <p-sidebar-inline-toolbar :editing =" editingField === 'labels'" @confirm =" confirmLabels" @start =" startChipEditing('labels')" />
253245 </template >
254246 </v-list-item >
255- <v-list-item v-if =" labels.length > 0 || editingField === 'labels' " class =" metadata__item metadata__chips" >
247+ <v-list-item v-if =" labels.length > 0 || chipState.labels.additions.length > 0 " class =" metadata__item metadata__chips meta-labels " >
256248 <div class =" d-flex flex-wrap ga-1" >
257249 <span
258250 v-for =" l in labels"
259251 :key =" l.Label.UID"
260252 class =" meta-chip meta-chip--primary"
261- :class =" { 'meta-chip--pending-remove': isLabelPendingRemoval(l ) }"
262- @click.stop.prevent =" editingField !== 'labels' ? navigateToLabel(l.Label) : toggleLabelRemoval(l )"
253+ :class =" { 'meta-chip--pending-remove': isChipPendingRemoval('labels', l.Label.ID ) }"
254+ @click.stop.prevent =" editingField !== 'labels' ? navigateToLabel(l.Label) : togglePendingChipRemoval('labels', l.Label.ID )"
263255 >
264256 {{ l.Label.Name }}
265257 <v-icon
266258 v-if =" editingField === 'labels'"
267- :icon =" isLabelPendingRemoval(l ) ? 'mdi-undo' : 'mdi-close-circle'"
259+ :icon =" isChipPendingRemoval('labels', l.Label.ID ) ? 'mdi-undo' : 'mdi-close-circle'"
268260 size =" x-small"
269261 class =" ml-1"
270262 ></v-icon >
271263 </span >
272264 <span
273- v-for =" name in pendingLabelAdditions "
265+ v-for =" name in chipState.labels.additions "
274266 :key =" 'add-' + name"
275267 class =" meta-chip meta-chip--pending-add"
276- @click.stop.prevent =" removePendingLabelAdd( name)"
268+ @click.stop.prevent =" removePendingChipAdd('labels', name)"
277269 >
278270 {{ name }}
279271 <v-icon icon =" mdi-close-circle" size =" x-small" class =" ml-1" ></v-icon >
280272 </span >
281273 </div >
282274 </v-list-item >
283- <v-list-item v-else-if =" isEditable" class =" metadata__item" >
275+ <v-list-item v-else-if =" isEditable && editingField !== 'labels' " class =" metadata__item meta-labels " >
284276 <div class =" meta-add-prompt" @click.stop =" startChipEditing('labels')" >{{ $gettext("Add label") }}</div >
285277 </v-list-item >
286- <v-list-item v-if =" editingField === 'labels'" class =" metadata__item" >
278+ <v-list-item v-if =" editingField === 'labels'" class =" metadata__item meta-labels " >
287279 <v-combobox
288280 :key =" chipKey"
289281 v-model =" chipInput"
309301
310302 <template v-if =" ! restrictedRole && (editingField === ' albums' || albums .length > 0 || isEditable )" >
311303 <v-divider class =" my-4" ></v-divider >
312- <v-list-item class =" metadata__item" >
304+ <v-list-item class =" metadata__item meta-albums " >
313305 <div class =" text-subtitle-2" >{{ $gettext("Albums") }}</div >
314306 <template v-if =" isEditable " #append >
315- <v-icon
316- v-if =" editingField === 'albums'"
317- icon =" mdi-check"
318- size =" small"
319- class =" meta-inline-confirm"
320- @mousedown.prevent
321- @click.stop =" confirmAlbums"
322- ></v-icon >
323- <v-icon v-else icon =" mdi-pencil-outline" size =" small" class =" meta-inline-pencil" @click.stop =" startChipEditing('albums')" ></v-icon >
307+ <p-sidebar-inline-toolbar :editing =" editingField === 'albums'" @confirm =" confirmAlbums" @start =" startChipEditing('albums')" />
324308 </template >
325309 </v-list-item >
326- <v-list-item v-if =" albums.length > 0 || editingField === 'albums' " class =" metadata__item metadata__chips" >
310+ <v-list-item v-if =" albums.length > 0 || chipState.albums.additions.length > 0 " class =" metadata__item metadata__chips meta-albums " >
327311 <div class =" d-flex flex-wrap ga-1" >
328312 <span
329313 v-for =" a in albums"
330314 :key =" a.UID"
331315 class =" meta-chip meta-chip--primary"
332- :class =" { 'meta-chip--pending-remove': isAlbumPendingRemoval(a ) }"
333- @click.stop.prevent =" editingField !== 'albums' ? navigateToAlbum(a) : toggleAlbumRemoval(a )"
316+ :class =" { 'meta-chip--pending-remove': isChipPendingRemoval('albums', a.UID ) }"
317+ @click.stop.prevent =" editingField !== 'albums' ? navigateToAlbum(a) : togglePendingChipRemoval('albums', a.UID )"
334318 >
335319 {{ a.Title }}
336320 <v-icon
337321 v-if =" editingField === 'albums'"
338- :icon =" isAlbumPendingRemoval(a ) ? 'mdi-undo' : 'mdi-close-circle'"
322+ :icon =" isChipPendingRemoval('albums', a.UID ) ? 'mdi-undo' : 'mdi-close-circle'"
339323 size =" x-small"
340324 class =" ml-1"
341325 ></v-icon >
342326 </span >
343327 <span
344- v-for =" a in pendingAlbumAdditions "
328+ v-for =" a in chipState.albums.additions "
345329 :key =" 'add-' + a.UID"
346330 class =" meta-chip meta-chip--pending-add"
347- @click.stop.prevent =" removePendingAlbumAdd(a )"
331+ @click.stop.prevent =" removePendingChipAdd('albums', a.UID )"
348332 >
349333 {{ a.Title }}
350334 <v-icon icon =" mdi-close-circle" size =" x-small" class =" ml-1" ></v-icon >
351335 </span >
352336 </div >
353337 </v-list-item >
354- <v-list-item v-else-if =" isEditable" class =" metadata__item" >
338+ <v-list-item v-else-if =" isEditable && editingField !== 'albums' " class =" metadata__item meta-albums " >
355339 <div class =" meta-add-prompt" @click.stop =" startChipEditing('albums')" >{{ $gettext("Add to album") }}</div >
356340 </v-list-item >
357- <v-list-item v-if =" editingField === 'albums'" class =" metadata__item" >
341+ <v-list-item v-if =" editingField === 'albums'" class =" metadata__item meta-albums " >
358342 <v-autocomplete
359343 :key =" chipKey"
360344 v-model =" chipInput"
@@ -539,10 +523,14 @@ export default {
539523 chipKey: 0 ,
540524 labelOptions: [],
541525 albumOptions: [],
542- pendingLabelRemovals: [],
543- pendingLabelAdditions: [],
544- pendingAlbumRemovals: [],
545- pendingAlbumAdditions: [],
526+ // Pending chip mutations staged during edit mode. Labels are keyed by
527+ // Label.ID for removals and by typed name for additions; albums are
528+ // keyed by Album.UID for removals and stored as full album objects
529+ // for additions (the title is read off the object at confirm time).
530+ chipState: {
531+ labels: { additions: [], removals: [] },
532+ albums: { additions: [], removals: [] },
533+ },
546534 markerDrafts: {},
547535 markerNameRule : (v ) => ! v || v .length <= this .$config .get (" clip" ) || this .$gettext (" Text too long" ),
548536 markerMenuProps: {
@@ -1044,8 +1032,7 @@ export default {
10441032 if (! d) continue ;
10451033 if (this .unwrapMarkerName (d .current ).trim () !== (d .original || " " ).trim ()) return true ;
10461034 }
1047- if (this .pendingLabelAdditions .length || this .pendingLabelRemovals .length ) return true ;
1048- if (this .pendingAlbumAdditions .length || this .pendingAlbumRemovals .length ) return true ;
1035+ if (Object .values (this .chipState ).some ((s ) => s .additions .length || s .removals .length )) return true ;
10491036 return false ;
10501037 },
10511038 // Async guard used by the lightbox before closing / hiding / navigating.
@@ -1125,10 +1112,7 @@ export default {
11251112 this .editingField = null ;
11261113 this .editOriginal = null ;
11271114 this ._editStartedAt = null ;
1128- this .pendingLabelRemovals = [];
1129- this .pendingLabelAdditions = [];
1130- this .pendingAlbumRemovals = [];
1131- this .pendingAlbumAdditions = [];
1115+ this .resetChipState ();
11321116 },
11331117 // Blur handler for inline text fields (title/caption/subject/artist/
11341118 // copyright/license/keywords/notes). Commits the edit instead of
@@ -1209,6 +1193,36 @@ export default {
12091193 this .chipSearch = " " ;
12101194 this .chipKey ++ ;
12111195 },
1196+ // Generic chip-state helpers. Field is "labels" or "albums"; the key is
1197+ // whatever uniquely identifies a chip in that field's domain (Label.ID
1198+ // for labels, Album.UID for albums on the removals side; the typed name
1199+ // for label additions, Album.UID for album additions).
1200+ isChipPendingRemoval (field , key ) {
1201+ const state = this .chipState [field];
1202+ return Boolean (state && key != null && state .removals .includes (key));
1203+ },
1204+ togglePendingChipRemoval (field , key ) {
1205+ const state = this .chipState [field];
1206+ if (! state || key == null ) return ;
1207+ const idx = state .removals .indexOf (key);
1208+ if (idx >= 0 ) {
1209+ state .removals .splice (idx, 1 );
1210+ } else {
1211+ state .removals .push (key);
1212+ }
1213+ },
1214+ removePendingChipAdd (field , key ) {
1215+ const state = this .chipState [field];
1216+ if (! state || key == null ) return ;
1217+ const idx = field === " labels" ? state .additions .indexOf (key) : state .additions .findIndex ((a ) => a .UID === key);
1218+ if (idx >= 0 ) state .additions .splice (idx, 1 );
1219+ },
1220+ resetChipState () {
1221+ Object .values (this .chipState ).forEach ((s ) => {
1222+ s .additions = [];
1223+ s .removals = [];
1224+ });
1225+ },
12121226 addPendingLabel (rawName ) {
12131227 const name = (rawName || " " ).trim ();
12141228 if (! name) return false ;
@@ -1218,14 +1232,15 @@ export default {
12181232 }
12191233 const norm = this .$util .normalizeLabelTitle (name);
12201234 if (! norm) return false ;
1221- if (this .pendingLabelAdditions .some ((n ) => this .$util .normalizeLabelTitle (n) === norm)) return false ;
1235+ const additions = this .chipState .labels .additions ;
1236+ if (additions .some ((n ) => this .$util .normalizeLabelTitle (n) === norm)) return false ;
12221237 if (this .labels .some ((l ) => this .$util .normalizeLabelTitle (l? .Label ? .Name ) === norm)) return false ;
1223- this . pendingLabelAdditions .push (name);
1238+ additions .push (name);
12241239 return true ;
12251240 },
12261241 albumTitleConflicts (norm ) {
12271242 if (! norm) return true ;
1228- if (this .pendingAlbumAdditions .some ((a ) => this .$util .normalizeLabelTitle (a? .Title ) === norm)) return true ;
1243+ if (this .chipState . albums . additions .some ((a ) => this .$util .normalizeLabelTitle (a? .Title ) === norm)) return true ;
12291244 if (this .albums .some ((a ) => this .$util .normalizeLabelTitle (a? .Title ) === norm)) return true ;
12301245 return false ;
12311246 },
@@ -1237,12 +1252,13 @@ export default {
12371252 this .$notify .error (this .$gettext (" Name too long" ));
12381253 return false ;
12391254 }
1255+ const additions = this .chipState .albums .additions ;
12401256 if (album .UID ) {
1241- if (this . pendingAlbumAdditions .some ((a ) => a .UID === album .UID )) return false ;
1257+ if (additions .some ((a ) => a .UID === album .UID )) return false ;
12421258 if (this .albums .some ((a ) => a .UID === album .UID )) return false ;
12431259 }
12441260 if (this .albumTitleConflicts (this .$util .normalizeLabelTitle (title))) return false ;
1245- this . pendingAlbumAdditions .push (album);
1261+ additions .push (album);
12461262 return true ;
12471263 },
12481264 onLabelSelected (value ) {
@@ -1263,54 +1279,33 @@ export default {
12631279 this .clearChipInput ();
12641280 }
12651281 },
1266- removePendingLabelAdd (name ) {
1267- const idx = this .pendingLabelAdditions .indexOf (name);
1268- if (idx >= 0 ) this .pendingLabelAdditions .splice (idx, 1 );
1269- },
1270- isLabelPendingRemoval (label ) {
1271- return this .pendingLabelRemovals .includes (label .Label .ID );
1272- },
1273- toggleLabelRemoval (label ) {
1274- if (! label? .Label ? .ID ) return ;
1275- const id = label .Label .ID ;
1276- const idx = this .pendingLabelRemovals .indexOf (id);
1277- if (idx >= 0 ) {
1278- this .pendingLabelRemovals .splice (idx, 1 );
1279- } else {
1280- this .pendingLabelRemovals .push (id);
1281- }
1282- },
12831282 confirmLabels () {
12841283 if (! this .photo ) {
12851284 this .editingField = null ;
12861285 return ;
12871286 }
12881287
1289- const removals = this .pendingLabelRemovals .slice ();
1290- const additions = this .pendingLabelAdditions .slice ();
1288+ const state = this .chipState .labels ;
1289+ const removals = state .removals .slice ();
1290+ const additions = state .additions .slice ();
12911291 this .editingField = null ;
1292- this . pendingLabelRemovals = [];
1293- this . pendingLabelAdditions = [];
1292+ state . removals = [];
1293+ state . additions = [];
12941294
12951295 const promises = [];
12961296 removals .forEach ((id ) => promises .push (this .photo .removeLabel (id)));
12971297 additions .forEach ((name ) => promises .push (this .photo .addLabel (name)));
12981298
1299+ // Cache freshness: photo.addLabel / removeLabel patch this.photo.Labels
1300+ // locally on success, and the backend publishes photos.updated which
1301+ // evicts the cached entry via evictCachedFromEntities — see
1302+ // model/photo.js. confirmAlbums needs an explicit evict + re-find
1303+ // because Album mutations go through raw $api.delete/post and don't
1304+ // patch this.photo.Albums; that asymmetry is intentional.
12991305 if (promises .length ) {
1300- Promise .all (promises).catch (() => {});
1301- }
1302- },
1303- isAlbumPendingRemoval (album ) {
1304- return this .pendingAlbumRemovals .includes (album .UID );
1305- },
1306- toggleAlbumRemoval (album ) {
1307- if (! album? .UID ) return ;
1308- const uid = album .UID ;
1309- const idx = this .pendingAlbumRemovals .indexOf (uid);
1310- if (idx >= 0 ) {
1311- this .pendingAlbumRemovals .splice (idx, 1 );
1312- } else {
1313- this .pendingAlbumRemovals .push (uid);
1306+ Promise .all (promises).catch (() => {
1307+ this .$notify .error (this .$gettext (" Failed to save changes" ));
1308+ });
13141309 }
13151310 },
13161311 confirmAlbums () {
@@ -1319,11 +1314,12 @@ export default {
13191314 return ;
13201315 }
13211316
1322- const removals = this .pendingAlbumRemovals .slice ();
1323- const additions = this .pendingAlbumAdditions .slice ();
1317+ const state = this .chipState .albums ;
1318+ const removals = state .removals .slice ();
1319+ const additions = state .additions .slice ();
13241320 this .editingField = null ;
1325- this . pendingAlbumRemovals = [];
1326- this . pendingAlbumAdditions = [];
1321+ state . removals = [];
1322+ state . additions = [];
13271323
13281324 const promises = [];
13291325 removals .forEach ((uid ) => promises .push (this .$api .delete (` albums/${ uid} /photos` , { data: { photos: [this .photo .UID ] } })));
@@ -1332,13 +1328,19 @@ export default {
13321328 if (promises .length ) {
13331329 Promise .all (promises)
13341330 .then (() => {
1331+ // Album mutations don't patch this.photo.Albums locally and the
1332+ // backend publishes only albums.updated (not photos.updated) for
1333+ // membership changes, so we evict + re-find here so the sidebar
1334+ // reflects the saved state without waiting for navigation.
13351335 Photo .evictCache (this .photo .UID );
13361336 return this .photo .find (this .photo .UID );
13371337 })
13381338 .then ((photo ) => {
13391339 this .photo .setValues (photo .getValues ());
13401340 })
1341- .catch (() => {});
1341+ .catch (() => {
1342+ this .$notify .error (this .$gettext (" Failed to save changes" ));
1343+ });
13421344 }
13431345 },
13441346 onAlbumSelected (value ) {
@@ -1396,10 +1398,6 @@ export default {
13961398 this .clearChipInput ();
13971399 });
13981400 },
1399- removePendingAlbumAdd (album ) {
1400- const idx = this .pendingAlbumAdditions .findIndex ((a ) => a .UID === album .UID );
1401- if (idx >= 0 ) this .pendingAlbumAdditions .splice (idx, 1 );
1402- },
14031401 confirmDateTime (data ) {
14041402 this .dateTimeDialog = false ;
14051403
0 commit comments