Bladeren bron

feat: added basic implementation of AdvancedTable component

Kristian Vos 3 jaren geleden
bovenliggende
commit
12dde2d336

+ 40 - 0
backend/logic/actions/songs.js

@@ -201,6 +201,46 @@ export default {
 		);
 		);
 	}),
 	}),
 
 
+	/**
+	 * Gets songs, used in the admin songs page by the AdvancedTable component
+	 *
+	 * @param {object} session - the session object automatically added by the websocket
+	 * @param page - the page
+	 * @param pageSize - the size per page
+	 * @param properties - the properties to return for each song
+	 * @param sort - the sort object
+	 * @param cb
+	 */
+	 getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, cb) {
+		async.waterfall(
+			[
+				next => {
+					SongsModule.runJob("GET_DATA", {
+						page,
+						pageSize,
+						properties,
+						sort
+					}, this)
+					.then(response => {
+						next(null, response);
+					})
+					.catch(err => {
+						next(err);
+					});
+				}
+			],
+			async (err, response) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log("ERROR", "SONGS_GET_DATA", `Failed to get data from songs. "${err}"`);
+					return cb({ status: "error", message: err });
+				}
+				this.log("SUCCESS", "SONGS_GET_DATA", `Got data from songs successfully.`);
+				return cb({ status: "success", message: "Successfully got data from songs.", data: response });
+			}
+		);
+	}),
+
 	/**
 	/**
 	 * Updates all songs
 	 * Updates all songs
 	 *
 	 *

+ 44 - 0
backend/logic/songs.js

@@ -206,6 +206,50 @@ class _SongsModule extends CoreClass {
 		);
 		);
 	}
 	}
 
 
+	/**
+	 * Gets songs data
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {string} payload.page - the page
+	 * @param {string} payload.pageSize - the page size
+	 * @param {string} payload.properties - the properties to return for each song
+	 * @param {string} payload.sort - the sort object
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	 GET_DATA(payload) {
+		return new Promise((resolve, reject) => {
+			const { page, pageSize, properties, sort } = payload;
+			console.log("GET_DATA", payload);
+			async.waterfall(
+				[
+					next => {
+						SongsModule.SongModel
+							.find({})
+							.count((err, count) => {
+								next(err, count);
+							});
+					},
+
+					(count, next) => {
+						SongsModule.SongModel
+							.find({})
+							.sort(sort)
+							.skip(pageSize * (page - 1))
+							.limit(pageSize)
+							.select(properties.join(" "))
+							.exec((err, songs) => {
+								next(err, count, songs);
+							});
+					}
+				],
+				(err, count, songs) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({ data: songs, count });
+				}
+			)
+		});
+	}
+
 	/**
 	/**
 	 * Makes sure that if a song is not currently in the songs db, to add it
 	 * Makes sure that if a song is not currently in the songs db, to add it
 	 *
 	 *

+ 156 - 0
frontend/src/components/AdvancedTable.vue

@@ -0,0 +1,156 @@
+<template>
+	<div>
+		<table class="table">
+			<thead>
+				<tr>
+					<th
+						v-for="column in columns"
+						:key="column.name"
+						:class="{ sortable: column.sortable }"
+						@click="changeSort(column)"
+					>
+						{{ column.displayName }}
+						<span
+							v-if="column.sortable && sort[column.sortProperty]"
+							>({{ sort[column.sortProperty] }})</span
+						>
+					</th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr v-for="item in data" :key="item._id">
+					<td
+						v-for="column in columns"
+						:key="`${item._id}-${column.name}`"
+					>
+						<slot
+							:name="`column-${column.name}`"
+							:item="item"
+						></slot>
+					</td>
+				</tr>
+			</tbody>
+		</table>
+		<br />
+		<div class="control">
+			<label class="label">Items per page</label>
+			<p class="control select">
+				<select v-model.number="pageSize" @change="getData()">
+					<option value="10">10</option>
+					<option value="25">25</option>
+					<option value="50">50</option>
+					<option value="100">100</option>
+					<option value="250">250</option>
+					<option value="500">500</option>
+					<option value="1000">1000</option>
+				</select>
+			</p>
+		</div>
+		<br />
+		<p>Page {{ page }} / {{ lastPage }}</p>
+		<br />
+		<button class="button is-primary" @click="changePage(page - 1)">
+			Go to previous page</button
+		>&nbsp;
+		<button class="button is-primary" @click="changePage(page + 1)">
+			Go to next page</button
+		>&nbsp;
+		<button class="button is-primary" @click="changePage(1)">
+			Go to first page</button
+		>&nbsp;
+		<button class="button is-primary" @click="changePage(lastPage)">
+			Go to last page
+		</button>
+	</div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+
+import Toast from "toasters";
+
+import ws from "@/ws";
+
+export default {
+	props: {
+		columns: { type: Array, default: null },
+		dataAction: { type: String, default: null }
+	},
+	data() {
+		return {
+			page: 1,
+			pageSize: 10,
+			data: [],
+			count: 0, // TODO Rename
+			sort: {
+				title: "ascending"
+			}
+		};
+	},
+	computed: {
+		properties() {
+			return Array.from(
+				new Set(this.columns.flatMap(column => column.properties))
+			);
+		},
+		lastPage() {
+			return Math.ceil(this.count / this.pageSize);
+		},
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
+	mounted() {
+		ws.onConnect(this.init);
+	},
+	methods: {
+		init() {
+			this.getData();
+		},
+		getData() {
+			this.socket.dispatch(
+				this.dataAction,
+				this.page,
+				this.pageSize,
+				this.properties,
+				this.sort,
+				res => {
+					console.log(111, res);
+					new Toast(res.message);
+					if (res.status === "success") {
+						const { data, count } = res.data;
+						this.data = data;
+						this.count = count;
+					}
+				}
+			);
+		},
+		changePage(page) {
+			if (page < 1) return;
+			if (page > this.lastPage) return;
+			if (page === this.page) return;
+			this.page = page;
+			this.getData();
+		},
+		changeSort(column) {
+			if (column.sortable) {
+				if (this.sort[column.sortProperty] === undefined)
+					this.sort[column.sortProperty] = "ascending";
+				else if (this.sort[column.sortProperty] === "ascending")
+					this.sort[column.sortProperty] = "descending";
+				else if (this.sort[column.sortProperty] === "descending")
+					delete this.sort[column.sortProperty];
+				this.getData();
+			}
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.table {
+	.sortable {
+		cursor: pointer;
+	}
+}
+</style>

+ 15 - 0
frontend/src/pages/Admin/index.vue

@@ -3,6 +3,16 @@
 		<main-header />
 		<main-header />
 		<div class="tabs is-centered">
 		<div class="tabs is-centered">
 			<ul>
 			<ul>
+				<li
+					:class="{ 'is-active': currentTab == 'test' }"
+					ref="test-tab"
+					@click="showTab('test')"
+				>
+					<router-link class="tab test" to="/admin/test">
+						<i class="material-icons">music_note</i>
+						<span>&nbsp;Test</span>
+					</router-link>
+				</li>
 				<li
 				<li
 					:class="{ 'is-active': currentTab == 'songs' }"
 					:class="{ 'is-active': currentTab == 'songs' }"
 					ref="songs-tab"
 					ref="songs-tab"
@@ -89,6 +99,7 @@
 			</ul>
 			</ul>
 		</div>
 		</div>
 
 
+		<test v-if="currentTab == 'test'" />
 		<songs v-if="currentTab == 'songs'" />
 		<songs v-if="currentTab == 'songs'" />
 		<stations v-if="currentTab == 'stations'" />
 		<stations v-if="currentTab == 'stations'" />
 		<playlists v-if="currentTab == 'playlists'" />
 		<playlists v-if="currentTab == 'playlists'" />
@@ -109,6 +120,7 @@ import MainHeader from "@/components/layout/MainHeader.vue";
 export default {
 export default {
 	components: {
 	components: {
 		MainHeader,
 		MainHeader,
+		Test: defineAsyncComponent(() => import("./tabs/Test.vue")),
 		Songs: defineAsyncComponent(() => import("./tabs/Songs.vue")),
 		Songs: defineAsyncComponent(() => import("./tabs/Songs.vue")),
 		Stations: defineAsyncComponent(() => import("./tabs/Stations.vue")),
 		Stations: defineAsyncComponent(() => import("./tabs/Stations.vue")),
 		Playlists: defineAsyncComponent(() => import("./tabs/Playlists.vue")),
 		Playlists: defineAsyncComponent(() => import("./tabs/Playlists.vue")),
@@ -142,6 +154,9 @@ export default {
 	methods: {
 	methods: {
 		changeTab(path) {
 		changeTab(path) {
 			switch (path) {
 			switch (path) {
+				case "/admin/test":
+					this.showTab("test");
+					break;
 				case "/admin/songs":
 				case "/admin/songs":
 					this.showTab("songs");
 					this.showTab("songs");
 					break;
 					break;

+ 50 - 0
frontend/src/pages/Admin/tabs/Test.vue

@@ -0,0 +1,50 @@
+<template>
+	<div>
+		<page-metadata title="Admin | Test" />
+		<div class="container">
+			<advanced-table :columns="columns" data-action="songs.getData">
+				<template #column-column1="slotProps">
+					{{ slotProps.item.title }}
+				</template>
+				<template #column-column2="slotProps">
+					{{ slotProps.item.artists.join(", ") }}
+				</template>
+			</advanced-table>
+		</div>
+	</div>
+</template>
+
+<script>
+import AdvancedTable from "@/components/AdvancedTable.vue";
+
+export default {
+	components: {
+		AdvancedTable
+	},
+	data() {
+		return {
+			columns: [
+				{
+					name: "column1",
+					displayName: "Column 1",
+					properties: ["title"],
+					sortable: true,
+					sortProperty: "title"
+				},
+				{
+					name: "column2",
+					displayName: "Column 2",
+					properties: ["artists"],
+					sortable: true,
+					sortProperty: "artists"
+				}
+			]
+		};
+	},
+	mounted() {},
+	beforeUnmount() {},
+	methods: {}
+};
+</script>
+
+<style lang="scss" scoped></style>