Browse Source

refactor: converted songs admin page to composition API

Kristian Vos 2 years ago
parent
commit
fbc092aa8a
1 changed files with 518 additions and 519 deletions
  1. 518 519
      frontend/src/pages/Admin/Songs/index.vue

+ 518 - 519
frontend/src/pages/Admin/Songs/index.vue

@@ -1,3 +1,517 @@
+<script setup lang="ts">
+import { ref, computed, onMounted } from "vue";
+import { useStore } from "vuex";
+import { useRoute } from "vue-router";
+
+import Toast from "toasters";
+
+import AdvancedTable from "@/components/AdvancedTable.vue";
+import RunJobDropdown from "@/components/RunJobDropdown.vue";
+
+const store = useStore();
+const route = useRoute();
+
+const { socket } = store.state.websockets;
+
+const columnDefault = ref({
+	sortable: true,
+	hidable: true,
+	defaultVisibility: "shown",
+	draggable: true,
+	resizable: true,
+	minWidth: 200,
+	maxWidth: 600
+});
+const columns = ref([
+	{
+		name: "options",
+		displayName: "Options",
+		properties: ["_id", "verified", "youtubeId"],
+		sortable: false,
+		hidable: false,
+		resizable: false,
+		minWidth: 129,
+		defaultWidth: 129
+	},
+	{
+		name: "thumbnailImage",
+		displayName: "Thumb",
+		properties: ["thumbnail"],
+		sortable: false,
+		minWidth: 75,
+		defaultWidth: 75,
+		maxWidth: 75,
+		resizable: false
+	},
+	{
+		name: "title",
+		displayName: "Title",
+		properties: ["title"],
+		sortProperty: "title"
+	},
+	{
+		name: "artists",
+		displayName: "Artists",
+		properties: ["artists"],
+		sortable: false
+	},
+	{
+		name: "genres",
+		displayName: "Genres",
+		properties: ["genres"],
+		sortable: false
+	},
+	{
+		name: "tags",
+		displayName: "Tags",
+		properties: ["tags"],
+		sortable: false
+	},
+	{
+		name: "_id",
+		displayName: "Song ID",
+		properties: ["_id"],
+		sortProperty: "_id",
+		minWidth: 215,
+		defaultWidth: 215
+	},
+	{
+		name: "youtubeId",
+		displayName: "YouTube ID",
+		properties: ["youtubeId"],
+		sortProperty: "youtubeId",
+		minWidth: 120,
+		defaultWidth: 120
+	},
+	{
+		name: "verified",
+		displayName: "Verified",
+		properties: ["verified"],
+		sortProperty: "verified",
+		minWidth: 120,
+		defaultWidth: 120
+	},
+	{
+		name: "thumbnailUrl",
+		displayName: "Thumbnail (URL)",
+		properties: ["thumbnail"],
+		sortProperty: "thumbnail",
+		defaultVisibility: "hidden"
+	},
+	{
+		name: "duration",
+		displayName: "Duration",
+		properties: ["duration"],
+		sortProperty: "duration",
+		defaultWidth: 200,
+		defaultVisibility: "hidden"
+	},
+	{
+		name: "skipDuration",
+		displayName: "Skip Duration",
+		properties: ["skipDuration"],
+		sortProperty: "skipDuration",
+		defaultWidth: 200,
+		defaultVisibility: "hidden"
+	},
+	{
+		name: "requestedBy",
+		displayName: "Requested By",
+		properties: ["requestedBy"],
+		sortProperty: "requestedBy",
+		defaultWidth: 200,
+		defaultVisibility: "hidden"
+	},
+	{
+		name: "requestedAt",
+		displayName: "Requested At",
+		properties: ["requestedAt"],
+		sortProperty: "requestedAt",
+		defaultWidth: 200,
+		defaultVisibility: "hidden"
+	},
+	{
+		name: "verifiedBy",
+		displayName: "Verified By",
+		properties: ["verifiedBy"],
+		sortProperty: "verifiedBy",
+		defaultWidth: 200,
+		defaultVisibility: "hidden"
+	},
+	{
+		name: "verifiedAt",
+		displayName: "Verified At",
+		properties: ["verifiedAt"],
+		sortProperty: "verifiedAt",
+		defaultWidth: 200,
+		defaultVisibility: "hidden"
+	}
+]);
+const filters = ref([
+	{
+		name: "_id",
+		displayName: "Song ID",
+		property: "_id",
+		filterTypes: ["exact"],
+		defaultFilterType: "exact"
+	},
+	{
+		name: "youtubeId",
+		displayName: "YouTube ID",
+		property: "youtubeId",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains"
+	},
+	{
+		name: "title",
+		displayName: "Title",
+		property: "title",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains"
+	},
+	{
+		name: "artists",
+		displayName: "Artists",
+		property: "artists",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains",
+		autosuggest: true,
+		autosuggestDataAction: "songs.getArtists"
+	},
+	{
+		name: "genres",
+		displayName: "Genres",
+		property: "genres",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains",
+		autosuggest: true,
+		autosuggestDataAction: "songs.getGenres"
+	},
+	{
+		name: "tags",
+		displayName: "Tags",
+		property: "tags",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains",
+		autosuggest: true,
+		autosuggestDataAction: "songs.getTags"
+	},
+	{
+		name: "thumbnail",
+		displayName: "Thumbnail",
+		property: "thumbnail",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains"
+	},
+	{
+		name: "requestedBy",
+		displayName: "Requested By",
+		property: "requestedBy",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains"
+	},
+	{
+		name: "requestedAt",
+		displayName: "Requested At",
+		property: "requestedAt",
+		filterTypes: ["datetimeBefore", "datetimeAfter"],
+		defaultFilterType: "datetimeBefore"
+	},
+	{
+		name: "verifiedBy",
+		displayName: "Verified By",
+		property: "verifiedBy",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains"
+	},
+	{
+		name: "verifiedAt",
+		displayName: "Verified At",
+		property: "verifiedAt",
+		filterTypes: ["datetimeBefore", "datetimeAfter"],
+		defaultFilterType: "datetimeBefore"
+	},
+	{
+		name: "verified",
+		displayName: "Verified",
+		property: "verified",
+		filterTypes: ["boolean"],
+		defaultFilterType: "boolean"
+	},
+	{
+		name: "duration",
+		displayName: "Duration",
+		property: "duration",
+		filterTypes: [
+			"numberLesserEqual",
+			"numberLesser",
+			"numberGreater",
+			"numberGreaterEqual",
+			"numberEquals"
+		],
+		defaultFilterType: "numberLesser"
+	},
+	{
+		name: "skipDuration",
+		displayName: "Skip Duration",
+		property: "skipDuration",
+		filterTypes: [
+			"numberLesserEqual",
+			"numberLesser",
+			"numberGreater",
+			"numberGreaterEqual",
+			"numberEquals"
+		],
+		defaultFilterType: "numberLesser"
+	}
+]);
+const events = ref({
+	adminRoom: "songs",
+	updated: {
+		event: "admin.song.updated",
+		id: "song._id",
+		item: "song"
+	},
+	removed: {
+		event: "admin.song.removed",
+		id: "songId"
+	}
+});
+const jobs = ref([
+	{
+		name: "Update all songs",
+		socket: "songs.updateAll"
+	},
+	{
+		name: "Recalculate all ratings",
+		socket: "media.recalculateAllRatings"
+	}
+]);
+
+const song = computed(() => store.state.modals.editSong.song);
+
+const openModal = payload =>
+	store.dispatch("modalVisibility/openModal", payload);
+
+const create = () => {
+	openModal({
+		modal: "editSong",
+		data: { song: { newSong: true } }
+	});
+};
+
+const editOne = song => {
+	openModal({
+		modal: "editSong",
+		data: { song }
+	});
+};
+
+const editMany = selectedRows => {
+	if (selectedRows.length === 1) editOne(selectedRows[0]);
+	else {
+		const songs = selectedRows.map(row => ({
+			youtubeId: row.youtubeId
+		}));
+		openModal({ modal: "editSongs", data: { songs } });
+	}
+};
+
+const verifyOne = songId => {
+	socket.dispatch("songs.verify", songId, res => {
+		new Toast(res.message);
+	});
+};
+
+const verifyMany = selectedRows => {
+	let id;
+	let title;
+
+	socket.dispatch(
+		"songs.verifyMany",
+		selectedRows.map(row => row._id),
+		{
+			cb: () => {},
+			onProgress: res => {
+				if (res.status === "started") {
+					id = res.id;
+					title = res.title;
+				}
+
+				if (id)
+					setJob({
+						id,
+						name: title,
+						...res
+					});
+			}
+		}
+	);
+};
+
+const unverifyOne = songId => {
+	socket.dispatch("songs.unverify", songId, res => {
+		new Toast(res.message);
+	});
+};
+
+const unverifyMany = selectedRows => {
+	let id;
+	let title;
+
+	socket.dispatch(
+		"songs.unverifyMany",
+		selectedRows.map(row => row._id),
+		{
+			cb: () => {},
+			onProgress: res => {
+				if (res.status === "started") {
+					id = res.id;
+					title = res.title;
+				}
+
+				if (id)
+					setJob({
+						id,
+						name: title,
+						...res
+					});
+			}
+		}
+	);
+};
+
+const importAlbum = selectedRows => {
+	const youtubeIds = selectedRows.map(({ youtubeId }) => youtubeId);
+	socket.dispatch("songs.getSongsFromYoutubeIds", youtubeIds, res => {
+		if (res.status === "success") {
+			openModal({
+				modal: "importAlbum",
+				data: { songs: res.data.songs }
+			});
+		}
+	});
+};
+
+const setTags = selectedRows => {
+	openModal({
+		modal: "bulkActions",
+		data: {
+			type: {
+				name: "tags",
+				action: "songs.editTags",
+				items: selectedRows.map(row => row._id),
+				regex: /^[a-zA-Z0-9_]{1,64}$|^[a-zA-Z0-9_]{1,64}\[[a-zA-Z0-9_]{1,64}\]$/,
+				autosuggest: true,
+				autosuggestDataAction: "songs.getTags"
+			}
+		}
+	});
+};
+
+const setArtists = selectedRows => {
+	openModal({
+		modal: "bulkActions",
+		data: {
+			type: {
+				name: "artists",
+				action: "songs.editArtists",
+				items: selectedRows.map(row => row._id),
+				regex: /^(?=.{1,64}$).*$/,
+				autosuggest: true,
+				autosuggestDataAction: "songs.getArtists"
+			}
+		}
+	});
+};
+
+const setGenres = selectedRows => {
+	openModal({
+		modal: "bulkActions",
+		data: {
+			type: {
+				name: "genres",
+				action: "songs.editGenres",
+				items: selectedRows.map(row => row._id),
+				regex: /^[\x00-\x7F]{1,32}$/,
+				autosuggest: true,
+				autosuggestDataAction: "songs.getGenres"
+			}
+		}
+	});
+};
+
+const deleteOne = songId => {
+	socket.dispatch("songs.remove", songId, res => {
+		new Toast(res.message);
+	});
+};
+
+const deleteMany = selectedRows => {
+	let id;
+	let title;
+
+	socket.dispatch(
+		"songs.removeMany",
+		selectedRows.map(row => row._id),
+		{
+			cb: () => {},
+			onProgress: res => {
+				if (res.status === "started") {
+					id = res.id;
+					title = res.title;
+				}
+
+				if (id)
+					setJob({
+						id,
+						name: title,
+						...res
+					});
+			}
+		}
+	);
+};
+
+const getDateFormatted = createdAt => {
+	const date = new Date(createdAt);
+	const year = date.getFullYear();
+	const month = `${date.getMonth() + 1}`.padStart(2, 0);
+	const day = `${date.getDate()}`.padStart(2, 0);
+	const hour = `${date.getHours()}`.padStart(2, 0);
+	const minute = `${date.getMinutes()}`.padStart(2, 0);
+	return `${year}-${month}-${day} ${hour}:${minute}`;
+};
+
+const handleConfirmed = ({ action, params }) => {
+	if (typeof action === "function") {
+		if (params) action(params);
+		else action();
+	}
+};
+
+const confirmAction = ({ message, action, params }) => {
+	openModal({
+		modal: "confirm",
+		data: {
+			message,
+			action,
+			params,
+			onCompleted: handleConfirmed
+		}
+	});
+};
+
+onMounted(() => {
+	if (route.query.songId) {
+		socket.dispatch("songs.getSongFromSongId", route.query.songId, res => {
+			if (res.status === "success") editMany([res.data.song]);
+			else new Toast("Song with that ID not found");
+		});
+	}
+});
+</script>
+
 <template>
 	<div class="admin-tab">
 		<page-metadata title="Admin | Songs" />
@@ -67,7 +581,7 @@
 							confirmAction({
 								message:
 									'Removing this song will remove it from all playlists and cause a ratings recalculation.',
-								action: 'deleteOne',
+								action: deleteOne,
 								params: slotProps.item._id
 							})
 						"
@@ -139,7 +653,7 @@
 				}}</span>
 			</template>
 			<template #column-requestedBy="slotProps">
-				<user-link :user-id="slotProps.item.requestedBy" />
+				<UserLink :user-id="slotProps.item.requestedBy" />
 			</template>
 			<template #column-requestedAt="slotProps">
 				<span :title="new Date(slotProps.item.requestedAt)">{{
@@ -147,7 +661,7 @@
 				}}</span>
 			</template>
 			<template #column-verifiedBy="slotProps">
-				<user-link :user-id="slotProps.item.verifiedBy" />
+				<UserLink :user-id="slotProps.item.verifiedBy" />
 			</template>
 			<template #column-verifiedAt="slotProps">
 				<span :title="new Date(slotProps.item.verifiedAt)">{{
@@ -229,7 +743,7 @@
 							confirmAction({
 								message:
 									'Removing these songs will remove them from all playlists and cause a ratings recalculation.',
-								action: 'deleteMany',
+								action: deleteMany,
 								params: slotProps.item
 							})
 						"
@@ -245,521 +759,6 @@
 	</div>
 </template>
 
-<script>
-import { mapState, mapActions, mapGetters } from "vuex";
-
-import Toast from "toasters";
-
-import AdvancedTable from "@/components/AdvancedTable.vue";
-import RunJobDropdown from "@/components/RunJobDropdown.vue";
-
-export default {
-	components: {
-		AdvancedTable,
-		RunJobDropdown
-	},
-	data() {
-		return {
-			columnDefault: {
-				sortable: true,
-				hidable: true,
-				defaultVisibility: "shown",
-				draggable: true,
-				resizable: true,
-				minWidth: 200,
-				maxWidth: 600
-			},
-			columns: [
-				{
-					name: "options",
-					displayName: "Options",
-					properties: ["_id", "verified", "youtubeId"],
-					sortable: false,
-					hidable: false,
-					resizable: false,
-					minWidth: 129,
-					defaultWidth: 129
-				},
-				{
-					name: "thumbnailImage",
-					displayName: "Thumb",
-					properties: ["thumbnail"],
-					sortable: false,
-					minWidth: 75,
-					defaultWidth: 75,
-					maxWidth: 75,
-					resizable: false
-				},
-				{
-					name: "title",
-					displayName: "Title",
-					properties: ["title"],
-					sortProperty: "title"
-				},
-				{
-					name: "artists",
-					displayName: "Artists",
-					properties: ["artists"],
-					sortable: false
-				},
-				{
-					name: "genres",
-					displayName: "Genres",
-					properties: ["genres"],
-					sortable: false
-				},
-				{
-					name: "tags",
-					displayName: "Tags",
-					properties: ["tags"],
-					sortable: false
-				},
-				{
-					name: "_id",
-					displayName: "Song ID",
-					properties: ["_id"],
-					sortProperty: "_id",
-					minWidth: 215,
-					defaultWidth: 215
-				},
-				{
-					name: "youtubeId",
-					displayName: "YouTube ID",
-					properties: ["youtubeId"],
-					sortProperty: "youtubeId",
-					minWidth: 120,
-					defaultWidth: 120
-				},
-				{
-					name: "verified",
-					displayName: "Verified",
-					properties: ["verified"],
-					sortProperty: "verified",
-					minWidth: 120,
-					defaultWidth: 120
-				},
-				{
-					name: "thumbnailUrl",
-					displayName: "Thumbnail (URL)",
-					properties: ["thumbnail"],
-					sortProperty: "thumbnail",
-					defaultVisibility: "hidden"
-				},
-				{
-					name: "duration",
-					displayName: "Duration",
-					properties: ["duration"],
-					sortProperty: "duration",
-					defaultWidth: 200,
-					defaultVisibility: "hidden"
-				},
-				{
-					name: "skipDuration",
-					displayName: "Skip Duration",
-					properties: ["skipDuration"],
-					sortProperty: "skipDuration",
-					defaultWidth: 200,
-					defaultVisibility: "hidden"
-				},
-				{
-					name: "requestedBy",
-					displayName: "Requested By",
-					properties: ["requestedBy"],
-					sortProperty: "requestedBy",
-					defaultWidth: 200,
-					defaultVisibility: "hidden"
-				},
-				{
-					name: "requestedAt",
-					displayName: "Requested At",
-					properties: ["requestedAt"],
-					sortProperty: "requestedAt",
-					defaultWidth: 200,
-					defaultVisibility: "hidden"
-				},
-				{
-					name: "verifiedBy",
-					displayName: "Verified By",
-					properties: ["verifiedBy"],
-					sortProperty: "verifiedBy",
-					defaultWidth: 200,
-					defaultVisibility: "hidden"
-				},
-				{
-					name: "verifiedAt",
-					displayName: "Verified At",
-					properties: ["verifiedAt"],
-					sortProperty: "verifiedAt",
-					defaultWidth: 200,
-					defaultVisibility: "hidden"
-				}
-			],
-			filters: [
-				{
-					name: "_id",
-					displayName: "Song ID",
-					property: "_id",
-					filterTypes: ["exact"],
-					defaultFilterType: "exact"
-				},
-				{
-					name: "youtubeId",
-					displayName: "YouTube ID",
-					property: "youtubeId",
-					filterTypes: ["contains", "exact", "regex"],
-					defaultFilterType: "contains"
-				},
-				{
-					name: "title",
-					displayName: "Title",
-					property: "title",
-					filterTypes: ["contains", "exact", "regex"],
-					defaultFilterType: "contains"
-				},
-				{
-					name: "artists",
-					displayName: "Artists",
-					property: "artists",
-					filterTypes: ["contains", "exact", "regex"],
-					defaultFilterType: "contains",
-					autosuggest: true,
-					autosuggestDataAction: "songs.getArtists"
-				},
-				{
-					name: "genres",
-					displayName: "Genres",
-					property: "genres",
-					filterTypes: ["contains", "exact", "regex"],
-					defaultFilterType: "contains",
-					autosuggest: true,
-					autosuggestDataAction: "songs.getGenres"
-				},
-				{
-					name: "tags",
-					displayName: "Tags",
-					property: "tags",
-					filterTypes: ["contains", "exact", "regex"],
-					defaultFilterType: "contains",
-					autosuggest: true,
-					autosuggestDataAction: "songs.getTags"
-				},
-				{
-					name: "thumbnail",
-					displayName: "Thumbnail",
-					property: "thumbnail",
-					filterTypes: ["contains", "exact", "regex"],
-					defaultFilterType: "contains"
-				},
-				{
-					name: "requestedBy",
-					displayName: "Requested By",
-					property: "requestedBy",
-					filterTypes: ["contains", "exact", "regex"],
-					defaultFilterType: "contains"
-				},
-				{
-					name: "requestedAt",
-					displayName: "Requested At",
-					property: "requestedAt",
-					filterTypes: ["datetimeBefore", "datetimeAfter"],
-					defaultFilterType: "datetimeBefore"
-				},
-				{
-					name: "verifiedBy",
-					displayName: "Verified By",
-					property: "verifiedBy",
-					filterTypes: ["contains", "exact", "regex"],
-					defaultFilterType: "contains"
-				},
-				{
-					name: "verifiedAt",
-					displayName: "Verified At",
-					property: "verifiedAt",
-					filterTypes: ["datetimeBefore", "datetimeAfter"],
-					defaultFilterType: "datetimeBefore"
-				},
-				{
-					name: "verified",
-					displayName: "Verified",
-					property: "verified",
-					filterTypes: ["boolean"],
-					defaultFilterType: "boolean"
-				},
-				{
-					name: "duration",
-					displayName: "Duration",
-					property: "duration",
-					filterTypes: [
-						"numberLesserEqual",
-						"numberLesser",
-						"numberGreater",
-						"numberGreaterEqual",
-						"numberEquals"
-					],
-					defaultFilterType: "numberLesser"
-				},
-				{
-					name: "skipDuration",
-					displayName: "Skip Duration",
-					property: "skipDuration",
-					filterTypes: [
-						"numberLesserEqual",
-						"numberLesser",
-						"numberGreater",
-						"numberGreaterEqual",
-						"numberEquals"
-					],
-					defaultFilterType: "numberLesser"
-				}
-			],
-			events: {
-				adminRoom: "songs",
-				updated: {
-					event: "admin.song.updated",
-					id: "song._id",
-					item: "song"
-				},
-				removed: {
-					event: "admin.song.removed",
-					id: "songId"
-				}
-			},
-			jobs: [
-				{
-					name: "Update all songs",
-					socket: "songs.updateAll"
-				},
-				{
-					name: "Recalculate all ratings",
-					socket: "media.recalculateAllRatings"
-				}
-			]
-		};
-	},
-	computed: {
-		...mapState("modals/editSong", {
-			song: state => state.song
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	mounted() {
-		if (this.$route.query.songId) {
-			this.socket.dispatch(
-				"songs.getSongFromSongId",
-				this.$route.query.songId,
-				res => {
-					if (res.status === "success")
-						this.editMany([res.data.song]);
-					else new Toast("Song with that ID not found");
-				}
-			);
-		}
-	},
-	methods: {
-		create() {
-			this.openModal({
-				modal: "editSong",
-				data: { song: { newSong: true } }
-			});
-		},
-		editOne(song) {
-			this.openModal({
-				modal: "editSong",
-				data: { song }
-			});
-		},
-		editMany(selectedRows) {
-			if (selectedRows.length === 1) this.editOne(selectedRows[0]);
-			else {
-				const songs = selectedRows.map(row => ({
-					youtubeId: row.youtubeId
-				}));
-				this.openModal({ modal: "editSongs", data: { songs } });
-			}
-		},
-		verifyOne(songId) {
-			this.socket.dispatch("songs.verify", songId, res => {
-				new Toast(res.message);
-			});
-		},
-		verifyMany(selectedRows) {
-			let id;
-			let title;
-
-			this.socket.dispatch(
-				"songs.verifyMany",
-				selectedRows.map(row => row._id),
-				{
-					cb: () => {},
-					onProgress: res => {
-						if (res.status === "started") {
-							id = res.id;
-							title = res.title;
-						}
-
-						if (id)
-							this.setJob({
-								id,
-								name: title,
-								...res
-							});
-					}
-				}
-			);
-		},
-		unverifyOne(songId) {
-			this.socket.dispatch("songs.unverify", songId, res => {
-				new Toast(res.message);
-			});
-		},
-		unverifyMany(selectedRows) {
-			let id;
-			let title;
-
-			this.socket.dispatch(
-				"songs.unverifyMany",
-				selectedRows.map(row => row._id),
-				{
-					cb: () => {},
-					onProgress: res => {
-						if (res.status === "started") {
-							id = res.id;
-							title = res.title;
-						}
-
-						if (id)
-							this.setJob({
-								id,
-								name: title,
-								...res
-							});
-					}
-				}
-			);
-		},
-		importAlbum(selectedRows) {
-			const youtubeIds = selectedRows.map(({ youtubeId }) => youtubeId);
-			this.socket.dispatch(
-				"songs.getSongsFromYoutubeIds",
-				youtubeIds,
-				res => {
-					if (res.status === "success") {
-						this.openModal({
-							modal: "importAlbum",
-							data: { songs: res.data.songs }
-						});
-					}
-				}
-			);
-		},
-		setTags(selectedRows) {
-			this.openModal({
-				modal: "bulkActions",
-				data: {
-					type: {
-						name: "tags",
-						action: "songs.editTags",
-						items: selectedRows.map(row => row._id),
-						regex: /^[a-zA-Z0-9_]{1,64}$|^[a-zA-Z0-9_]{1,64}\[[a-zA-Z0-9_]{1,64}\]$/,
-						autosuggest: true,
-						autosuggestDataAction: "songs.getTags"
-					}
-				}
-			});
-		},
-		setArtists(selectedRows) {
-			this.openModal({
-				modal: "bulkActions",
-				data: {
-					type: {
-						name: "artists",
-						action: "songs.editArtists",
-						items: selectedRows.map(row => row._id),
-						regex: /^(?=.{1,64}$).*$/,
-						autosuggest: true,
-						autosuggestDataAction: "songs.getArtists"
-					}
-				}
-			});
-		},
-		setGenres(selectedRows) {
-			this.openModal({
-				modal: "bulkActions",
-				data: {
-					type: {
-						name: "genres",
-						action: "songs.editGenres",
-						items: selectedRows.map(row => row._id),
-						regex: /^[\x00-\x7F]{1,32}$/,
-						autosuggest: true,
-						autosuggestDataAction: "songs.getGenres"
-					}
-				}
-			});
-		},
-		deleteOne(songId) {
-			this.socket.dispatch("songs.remove", songId, res => {
-				new Toast(res.message);
-			});
-		},
-		deleteMany(selectedRows) {
-			let id;
-			let title;
-
-			this.socket.dispatch(
-				"songs.removeMany",
-				selectedRows.map(row => row._id),
-				{
-					cb: () => {},
-					onProgress: res => {
-						if (res.status === "started") {
-							id = res.id;
-							title = res.title;
-						}
-
-						if (id)
-							this.setJob({
-								id,
-								name: title,
-								...res
-							});
-					}
-				}
-			);
-		},
-		getDateFormatted(createdAt) {
-			const date = new Date(createdAt);
-			const year = date.getFullYear();
-			const month = `${date.getMonth() + 1}`.padStart(2, 0);
-			const day = `${date.getDate()}`.padStart(2, 0);
-			const hour = `${date.getHours()}`.padStart(2, 0);
-			const minute = `${date.getMinutes()}`.padStart(2, 0);
-			return `${year}-${month}-${day} ${hour}:${minute}`;
-		},
-		confirmAction({ message, action, params }) {
-			this.openModal({
-				modal: "confirm",
-				data: {
-					message,
-					action,
-					params,
-					onCompleted: this.handleConfirmed
-				}
-			});
-		},
-		handleConfirmed({ action, params }) {
-			if (typeof this[action] === "function") {
-				if (params) this[action](params);
-				else this[action]();
-			}
-		},
-		...mapActions("modalVisibility", ["openModal"])
-	}
-};
-</script>
-
 <style lang="less" scoped>
 :deep(.song-thumbnail) {
 	width: 50px;