Selaa lähdekoodia

feat: added confirm prompt when closing EditSong with unsaved changes or EditSongs with todo items left

Kristian Vos 3 vuotta sitten
vanhempi
commit
b817641f28

+ 4 - 1
frontend/src/App.vue

@@ -159,7 +159,10 @@ export default {
 			shift: false,
 			ctrl: false,
 			handler: () => {
-				if (Object.keys(this.currentlyActive).length !== 0)
+				if (
+					Object.keys(this.currentlyActive).length !== 0 &&
+					this.currentlyActive[0] !== "editSong" && this.currentlyActive[0] !== "editSongs"
+				)
 					this.closeCurrentModal();
 			}
 		});

+ 11 - 3
frontend/src/components/Modal.vue

@@ -1,6 +1,6 @@
 <template>
 	<div class="modal is-active">
-		<div class="modal-background" @click="closeCurrentModal()" />
+		<div class="modal-background" @click="closeCurrentModalClick()" />
 		<slot name="sidebar" />
 		<div
 			:class="{
@@ -15,7 +15,9 @@
 				<h2 class="modal-card-title is-marginless">
 					{{ title }}
 				</h2>
-				<span class="delete material-icons" @click="closeCurrentModal()"
+				<span
+					class="delete material-icons"
+					@click="closeCurrentModalClick()"
 					>highlight_off</span
 				>
 				<christmas-lights v-if="christmas" small :lights="5" />
@@ -48,8 +50,10 @@ export default {
 	props: {
 		title: { type: String, default: "Modal" },
 		size: { type: String, default: null },
-		split: { type: Boolean, default: false }
+		split: { type: Boolean, default: false },
+		interceptClose: { type: Boolean, default: false }
 	},
+	emits: ["close"],
 	data() {
 		return {
 			christmas: false
@@ -65,6 +69,10 @@ export default {
 		this.christmas = await lofig.get("siteSettings.christmas");
 	},
 	methods: {
+		closeCurrentModalClick() {
+			if (this.interceptClose) this.$emit("close");
+			else this.closeCurrentModal();
+		},
 		toCamelCase: str =>
 			str
 				.toLowerCase()

+ 15 - 4
frontend/src/components/modals/Confirm.vue

@@ -22,21 +22,32 @@ import Modal from "../Modal.vue";
 
 export default {
 	components: { Modal },
+	emits: ["confirmed"],
+	data() {
+		return {
+			modalName: ""
+		};
+	},
 	computed: {
+		...mapState("modalVisibility", {
+			currentlyActive: state => state.currentlyActive
+		}),
 		...mapState("modals/confirm", {
 			message: state => state.message
 		})
 	},
-	beforeUnmount() {
-		this.updateConfirmMessage("");
+	mounted() {
+		// eslint-disable-next-line
+		this.modalName = this.currentlyActive[0];
 	},
 	methods: {
 		confirmAction() {
+			this.updateConfirmMessage("");
 			this.$emit("confirmed");
-			this.closeCurrentModal();
+			this.closeModal(this.modalName);
 		},
 		...mapActions("modals/confirm", ["updateConfirmMessage"]),
-		...mapActions("modalVisibility", ["closeCurrentModal"])
+		...mapActions("modalVisibility", ["closeModal"])
 	}
 };
 </script>

+ 55 - 15
frontend/src/components/modals/EditSong/index.vue

@@ -5,6 +5,8 @@
 			class="song-modal"
 			:size="'wide'"
 			:split="true"
+			:intercept-close="true"
+			@close="onCloseModal"
 		>
 			<template #toggleMobileSidebar>
 				<slot name="toggleMobileSidebar" />
@@ -580,7 +582,14 @@ export default {
 		sector: { type: String, default: "admin" },
 		bulk: { type: Boolean, default: false }
 	},
-	emits: ["error", "savedSuccess", "savedError", "flagSong", "nextSong"],
+	emits: [
+		"error",
+		"savedSuccess",
+		"savedError",
+		"flagSong",
+		"nextSong",
+		"close"
+	],
 	data() {
 		return {
 			songDataLoaded: false,
@@ -626,7 +635,8 @@ export default {
 			reports: state => state.reports
 		}),
 		...mapState("modalVisibility", {
-			modals: state => state.modals
+			modals: state => state.modals,
+			currentlyActive: state => state.currentlyActive
 		}),
 		...mapGetters({
 			socket: "websockets/getSocket"
@@ -791,17 +801,6 @@ export default {
 			}
 		});
 
-		keyboardShortcuts.registerShortcut("editSong.close", {
-			keyCode: 115,
-			preventDefault: true,
-			handler: () => {
-				this.closeModal("editSong");
-				setTimeout(() => {
-					window.focusedElementBefore.focus();
-				}, 500);
-			}
-		});
-
 		keyboardShortcuts.registerShortcut("editSong.focusTitle", {
 			keyCode: 36,
 			preventDefault: true,
@@ -823,6 +822,18 @@ export default {
 			}
 		});
 
+		keyboardShortcuts.registerShortcut("editSong.closeModal", {
+			keyCode: 27,
+			handler: () => {
+				if (
+					this.currentlyActive[0] === "editSong" ||
+					this.currentlyActive[0] === "editSongs"
+				) {
+					this.onCloseModal();
+				}
+			}
+		});
+
 		/*
 
 		editSong.pauseResume - Num 5 - Pause/resume song
@@ -871,8 +882,8 @@ export default {
 			"editSong.save",
 			"editSong.saveClose",
 			"editSong.saveVerifyClose",
-			"editSong.close",
-			"editSong.useAllDiscogs"
+			"editSong.useAllDiscogs",
+			"editSong.closeModal"
 		];
 
 		shortcutNames.forEach(shortcutName => {
@@ -1337,6 +1348,8 @@ export default {
 					return;
 				}
 
+				this.updateOriginalSong(song);
+
 				if (verify) {
 					saveButtonRef.status = "verifying";
 					this.verify(this.song._id, success => {
@@ -1645,6 +1658,32 @@ export default {
 				params: null
 			};
 		},
+		onCloseModal() {
+			const songStringified = JSON.stringify({
+				...this.song,
+				verified: null
+			});
+			const originalSongStringified = JSON.stringify({
+				...this.originalSong,
+				verified: null
+			});
+			const unsavedChanges = songStringified !== originalSongStringified;
+
+			if (unsavedChanges) {
+				return this.confirmAction({
+					message:
+						"You have unsaved changes. Are you sure you want to discard unsaved changes?",
+					action: "closeThisModal",
+					params: null
+				});
+			}
+
+			return this.closeThisModal();
+		},
+		closeThisModal() {
+			if (this.bulk) this.$emit("close");
+			else this.closeModal("editSong");
+		},
 		...mapActions("modals/importAlbum", ["selectDiscogsAlbum"]),
 		...mapActions({
 			showTab(dispatch, payload) {
@@ -1661,6 +1700,7 @@ export default {
 			"getCurrentTime",
 			"setSong",
 			"resetSong",
+			"updateOriginalSong",
 			"updateSongField",
 			"updateReports"
 		]),

+ 252 - 173
frontend/src/components/modals/EditSongs.vue

@@ -1,173 +1,200 @@
 <template>
-	<edit-song
-		:bulk="true"
-		v-if="!closed && currentSong"
-		@savedSuccess="onSavedSuccess"
-		@savedError="onSavedError"
-		@saving="onSaving"
-		@toggleFlag="toggleFlag"
-		@nextSong="editNextSong"
-	>
-		<template #toggleMobileSidebar>
-			<i
-				class="material-icons toggle-sidebar-icon"
-				:content="`${
-					sidebarMobileActive ? 'Close' : 'Open'
-				} Edit Queue`"
-				v-tippy
-				@click="toggleMobileSidebar()"
-				>expand_circle_down</i
-			>
-		</template>
-		<template v-if="items.length > 1" #sidebar>
-			<div class="sidebar" :class="{ active: sidebarMobileActive }">
-				<header class="sidebar-head">
-					<h2 class="sidebar-title is-marginless">Edit Queue</h2>
-					<i
-						class="material-icons toggle-sidebar-icon"
-						:content="`${
-							sidebarMobileActive ? 'Close' : 'Open'
-						} Edit Queue`"
-						v-tippy
-						@click="toggleMobileSidebar()"
-						>expand_circle_down</i
-					>
-				</header>
-				<section class="sidebar-body">
-					<div
-						class="item"
-						v-for="(
-							{ status, flagged, song }, index
-						) in filteredItems"
-						:key="song._id"
-					>
-						<song-item
-							:song="song"
-							:thumbnail="false"
-							:duration="false"
-							:disabled-actions="
-								song.removed ? ['all'] : ['report', 'edit']
-							"
-							:class="{
-								updated: song.updated,
-								removed: song.removed
-							}"
+	<div>
+		<edit-song
+			:bulk="true"
+			v-if="currentSong"
+			@savedSuccess="onSavedSuccess"
+			@savedError="onSavedError"
+			@saving="onSaving"
+			@toggleFlag="toggleFlag"
+			@nextSong="editNextSong"
+			@close="onClose"
+		>
+			<template #toggleMobileSidebar>
+				<i
+					class="material-icons toggle-sidebar-icon"
+					:content="`${
+						sidebarMobileActive ? 'Close' : 'Open'
+					} Edit Queue`"
+					v-tippy
+					@click="toggleMobileSidebar()"
+					>expand_circle_down</i
+				>
+			</template>
+			<template v-if="items.length > 1" #sidebar>
+				<div class="sidebar" :class="{ active: sidebarMobileActive }">
+					<header class="sidebar-head">
+						<h2 class="sidebar-title is-marginless">Edit Queue</h2>
+						<i
+							class="material-icons toggle-sidebar-icon"
+							:content="`${
+								sidebarMobileActive ? 'Close' : 'Open'
+							} Edit Queue`"
+							v-tippy
+							@click="toggleMobileSidebar()"
+							>expand_circle_down</i
 						>
-							<template #leftIcon>
-								<i
-									v-if="currentSong._id === song._id"
-									class="
-										material-icons
-										item-icon
-										editing-icon
-									"
-									content="Currently editing song"
-									v-tippy="{ theme: 'info' }"
-									@click="toggleDone(index)"
-									>edit</i
-								>
-								<i
-									v-else-if="song.removed"
-									class="
-										material-icons
-										item-icon
-										removed-icon
-									"
-									content="Song removed"
-									v-tippy="{ theme: 'info' }"
-									>delete_forever</i
-								>
-								<i
-									v-else-if="status === 'error'"
-									class="material-icons item-icon error-icon"
-									content="Error saving song"
-									v-tippy="{ theme: 'info' }"
-									@click="toggleDone(index)"
-									>error</i
-								>
-								<i
-									v-else-if="status === 'saving'"
-									class="material-icons item-icon saving-icon"
-									content="Currently saving song"
-									v-tippy="{ theme: 'info' }"
-									>pending</i
-								>
-								<i
-									v-else-if="flagged"
-									class="material-icons item-icon flag-icon"
-									content="Song flagged"
-									v-tippy="{ theme: 'info' }"
-									@click="toggleDone(index)"
-									>flag_circle</i
-								>
-								<i
-									v-else-if="status === 'done'"
-									class="material-icons item-icon done-icon"
-									content="Song marked complete"
-									v-tippy="{ theme: 'info' }"
-									@click="toggleDone(index)"
-									>check_circle</i
-								>
-								<i
-									v-else-if="status === 'todo'"
-									class="material-icons item-icon todo-icon"
-									content="Song marked todo"
-									v-tippy="{ theme: 'info' }"
-									@click="toggleDone(index)"
-									>cancel</i
-								>
-							</template>
-							<template v-if="!song.removed" #actions>
-								<i
-									class="material-icons edit-icon"
-									content="Edit Song"
-									v-tippy
-									@click="pickSong(song)"
-								>
-									edit
-								</i>
-							</template>
-							<template #tippyActions>
-								<i
-									class="material-icons flag-icon"
-									:class="{ flagged }"
-									content="Toggle Flag"
-									v-tippy
-									@click="toggleFlag(index)"
-								>
-									flag_circle
-								</i>
-							</template>
-						</song-item>
-					</div>
-					<p v-if="filteredItems.length === 0" class="no-items">
-						{{
-							flagFilter
-								? "No flagged songs queued"
-								: "No songs queued"
-						}}
-					</p>
-				</section>
-				<footer class="sidebar-foot">
-					<button
-						@click="toggleFlagFilter()"
-						class="button is-primary"
-					>
-						{{
-							flagFilter
-								? "Show All Songs"
-								: "Show Only Flagged Songs"
-						}}
-					</button>
-				</footer>
-			</div>
-			<div
-				v-if="sidebarMobileActive"
-				class="sidebar-overlay"
-				@click="toggleMobileSidebar()"
-			></div>
-		</template>
-	</edit-song>
+					</header>
+					<section class="sidebar-body">
+						<div
+							class="item"
+							v-for="(
+								{ status, flagged, song }, index
+							) in filteredItems"
+							:key="song._id"
+						>
+							<song-item
+								:song="song"
+								:thumbnail="false"
+								:duration="false"
+								:disabled-actions="
+									song.removed ? ['all'] : ['report', 'edit']
+								"
+								:class="{
+									updated: song.updated,
+									removed: song.removed
+								}"
+							>
+								<template #leftIcon>
+									<i
+										v-if="currentSong._id === song._id"
+										class="
+											material-icons
+											item-icon
+											editing-icon
+										"
+										content="Currently editing song"
+										v-tippy="{ theme: 'info' }"
+										@click="toggleDone(index)"
+										>edit</i
+									>
+									<i
+										v-else-if="song.removed"
+										class="
+											material-icons
+											item-icon
+											removed-icon
+										"
+										content="Song removed"
+										v-tippy="{ theme: 'info' }"
+										>delete_forever</i
+									>
+									<i
+										v-else-if="status === 'error'"
+										class="
+											material-icons
+											item-icon
+											error-icon
+										"
+										content="Error saving song"
+										v-tippy="{ theme: 'info' }"
+										@click="toggleDone(index)"
+										>error</i
+									>
+									<i
+										v-else-if="status === 'saving'"
+										class="
+											material-icons
+											item-icon
+											saving-icon
+										"
+										content="Currently saving song"
+										v-tippy="{ theme: 'info' }"
+										>pending</i
+									>
+									<i
+										v-else-if="flagged"
+										class="
+											material-icons
+											item-icon
+											flag-icon
+										"
+										content="Song flagged"
+										v-tippy="{ theme: 'info' }"
+										@click="toggleDone(index)"
+										>flag_circle</i
+									>
+									<i
+										v-else-if="status === 'done'"
+										class="
+											material-icons
+											item-icon
+											done-icon
+										"
+										content="Song marked complete"
+										v-tippy="{ theme: 'info' }"
+										@click="toggleDone(index)"
+										>check_circle</i
+									>
+									<i
+										v-else-if="status === 'todo'"
+										class="
+											material-icons
+											item-icon
+											todo-icon
+										"
+										content="Song marked todo"
+										v-tippy="{ theme: 'info' }"
+										@click="toggleDone(index)"
+										>cancel</i
+									>
+								</template>
+								<template v-if="!song.removed" #actions>
+									<i
+										class="material-icons edit-icon"
+										content="Edit Song"
+										v-tippy
+										@click="pickSong(song)"
+									>
+										edit
+									</i>
+								</template>
+								<template #tippyActions>
+									<i
+										class="material-icons flag-icon"
+										:class="{ flagged }"
+										content="Toggle Flag"
+										v-tippy
+										@click="toggleFlag(index)"
+									>
+										flag_circle
+									</i>
+								</template>
+							</song-item>
+						</div>
+						<p v-if="filteredItems.length === 0" class="no-items">
+							{{
+								flagFilter
+									? "No flagged songs queued"
+									: "No songs queued"
+							}}
+						</p>
+					</section>
+					<footer class="sidebar-foot">
+						<button
+							@click="toggleFlagFilter()"
+							class="button is-primary"
+						>
+							{{
+								flagFilter
+									? "Show All Songs"
+									: "Show Only Flagged Songs"
+							}}
+						</button>
+					</footer>
+				</div>
+				<div
+					v-if="sidebarMobileActive"
+					class="sidebar-overlay"
+					@click="toggleMobileSidebar()"
+				></div>
+			</template>
+		</edit-song>
+		<confirm
+			v-if="modals.editSongsConfirm"
+			@confirmed="handleConfirmed()"
+		/>
+	</div>
 </template>
 
 <script>
@@ -182,6 +209,9 @@ export default {
 		EditSong: defineAsyncComponent(() =>
 			import("@/components/modals/EditSong")
 		),
+		Confirm: defineAsyncComponent(() =>
+			import("@/components/modals/Confirm.vue")
+		),
 		SongItem
 	},
 	props: {},
@@ -189,9 +219,13 @@ export default {
 		return {
 			items: [],
 			currentSong: {},
-			closed: false,
 			flagFilter: false,
-			sidebarMobileActive: false
+			sidebarMobileActive: false,
+			confirm: {
+				message: "",
+				action: "",
+				params: null
+			}
 		};
 	},
 	computed: {
@@ -222,6 +256,9 @@ export default {
 			songIds: state => state.songIds,
 			songPrefillData: state => state.songPrefillData
 		}),
+		...mapState("modalVisibility", {
+			modals: state => state.modals
+		}),
 		...mapGetters({
 			socket: "websockets/getSocket"
 		})
@@ -273,9 +310,6 @@ export default {
 			// 	this.items.findIndex(item => item.song._id === song._id)
 			// ].status = "editing";
 		},
-		closeEditSongs() {
-			this.closed = true;
-		},
 		editNextSong() {
 			const currentlyEditingSongIndex = this.filteredEditingItemIndex;
 			let newEditingSongIndex = -1;
@@ -332,6 +366,51 @@ export default {
 		toggleMobileSidebar() {
 			this.sidebarMobileActive = !this.sidebarMobileActive;
 		},
+		confirmAction(confirm) {
+			this.confirm = confirm;
+			this.updateConfirmMessage(confirm.message);
+			this.openModal("editSongsConfirm");
+		},
+		handleConfirmed() {
+			const { action, params } = this.confirm;
+			if (typeof this[action] === "function") {
+				if (params) this[action](params);
+				else this[action]();
+			}
+			this.confirm = {
+				message: "",
+				action: "",
+				params: null
+			};
+		},
+		onClose() {
+			const doneItems = this.items.filter(
+				item => item.status === "done"
+			).length;
+			const flaggedItems = this.items.filter(item => item.flagged).length;
+			const notDoneItems = this.items.length - doneItems;
+
+			if (doneItems > 0 && notDoneItems > 0)
+				this.confirmAction({
+					message:
+						"You have songs which are not done yet. Are you sure you want to stop editing songs?",
+					action: "closeThisModal",
+					params: null
+				});
+			else if (flaggedItems > 0)
+				this.confirmAction({
+					message:
+						"You have songs which are flagged. Are you sure you want to stop editing songs?",
+					action: "closeThisModal",
+					params: null
+				});
+			else this.closeThisModal();
+		},
+		closeThisModal() {
+			this.closeModal("editSongs");
+		},
+		...mapActions("modals/confirm", ["updateConfirmMessage"]),
+		...mapActions("modalVisibility", ["openModal", "closeModal"]),
 		...mapActions("modals/editSong", ["editSong"])
 	}
 };

+ 3 - 1
frontend/src/store/modules/modalVisibility.js

@@ -22,6 +22,7 @@ const state = {
 		viewPunishment: false,
 		confirm: false,
 		editSongConfirm: false,
+		editSongsConfirm: false,
 		bulkActions: false
 	},
 	currentlyActive: []
@@ -49,7 +50,8 @@ const actions = {
 const mutations = {
 	closeModal(state, modal) {
 		state.modals[modal] = false;
-		if (state.currentlyActive[0] === modal) state.currentlyActive.shift();
+		const index = state.currentlyActive.indexOf(modal);
+		if (index > -1) state.currentlyActive.splice(index, 1);
 	},
 	openModal(state, modal) {
 		state.modals[modal] = true;

+ 5 - 0
frontend/src/store/modules/modals/editSong.js

@@ -21,6 +21,8 @@ export default {
 		showTab: ({ commit }, tab) => commit("showTab", tab),
 		editSong: ({ commit }, song) => commit("editSong", song),
 		setSong: ({ commit }, song) => commit("setSong", song),
+		updateOriginalSong: ({ commit }, song) =>
+			commit("updateOriginalSong", song),
 		resetSong: ({ commit }, songId) => commit("resetSong", songId),
 		stopVideo: ({ commit }) => commit("stopVideo"),
 		loadVideoById: ({ commit }, id, skipDuration) =>
@@ -56,6 +58,9 @@ export default {
 			state.originalSong = JSON.parse(JSON.stringify(song));
 			state.song = { ...song };
 		},
+		updateOriginalSong(state, song) {
+			state.originalSong = JSON.parse(JSON.stringify(song));
+		},
 		resetSong(state, songId) {
 			state.originalSong = {};
 			state.song = {};