瀏覽代碼

feat(Admin): Started integrating advanced table into admin/stations

Owen Diffey 3 年之前
父節點
當前提交
b0a75f0861
共有 3 個文件被更改,包括 320 次插入131 次删除
  1. 48 0
      backend/logic/actions/stations.js
  2. 66 0
      backend/logic/stations.js
  3. 206 131
      frontend/src/pages/Admin/tabs/Stations.vue

+ 48 - 0
backend/logic/actions/stations.js

@@ -610,6 +610,54 @@ export default {
 		);
 	},
 
+	/**
+	 * Gets stations, used in the admin stations 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 station
+	 * @param sort - the sort object
+	 * @param queries - the queries array
+	 * @param operator - the operator for queries
+	 * @param cb
+	 */
+	getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
+		async.waterfall(
+			[
+				next => {
+					StationsModule.runJob(
+						"GET_DATA",
+						{
+							page,
+							pageSize,
+							properties,
+							sort,
+							queries,
+							operator
+						},
+						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", "STATIONS_GET_DATA", `Failed to get data from stations. "${err}"`);
+					return cb({ status: "error", message: err });
+				}
+				this.log("SUCCESS", "STATIONS_GET_DATA", `Got data from stations successfully.`);
+				return cb({ status: "success", message: "Successfully got data from stations.", data: response });
+			}
+		);
+	}),
+
 	/**
 	 * Obtains basic metadata of a station in order to format an activity
 	 *

+ 66 - 0
backend/logic/stations.js

@@ -391,6 +391,72 @@ class _StationsModule extends CoreClass {
 		);
 	}
 
+	/**
+	 * Gets stations 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 station
+	 * @param {string} payload.sort - the sort object
+	 * @param {string} payload.queries - the queries array
+	 * @param {string} payload.operator - the operator for queries
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	GET_DATA(payload) {
+		return new Promise((resolve, reject) => {
+			const { page, pageSize, properties, sort, queries, operator } = payload;
+
+			console.log("GET_DATA", payload);
+
+			const newQueries = queries.map(query => {
+				const { data, filter, filterType } = query;
+				const newQuery = {};
+				if (filterType === "regex") {
+					newQuery[filter.property] = new RegExp(`${data.slice(1, data.length - 1)}`, "i");
+				} else if (filterType === "contains") {
+					newQuery[filter.property] = new RegExp(`${data.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i");
+				} else if (filterType === "exact") {
+					newQuery[filter.property] = data.toString();
+				}
+				return newQuery;
+			});
+
+			const queryObject = {};
+			if (newQueries.length > 0) {
+				if (operator === "and") queryObject.$and = newQueries;
+				else if (operator === "or") queryObject.$or = newQueries;
+				else if (operator === "nor") queryObject.$nor = newQueries;
+			}
+
+			async.waterfall(
+				[
+					next => {
+						StationsModule.stationModel.find(queryObject).count((err, count) => {
+							next(err, count);
+						});
+					},
+
+					(count, next) => {
+						StationsModule.stationModel
+							.find(queryObject)
+							.sort(sort)
+							.skip(pageSize * (page - 1))
+							.limit(pageSize)
+							.select(properties.join(" "))
+							.exec((err, stations) => {
+								next(err, count, stations);
+							});
+					}
+				],
+				(err, count, stations) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({ data: stations, count });
+				}
+			);
+		});
+	}
+
 	/**
 	 * Updates the station in cache from mongo or deletes station in cache if no longer in mongo.
 	 *

+ 206 - 131
frontend/src/pages/Admin/tabs/Stations.vue

@@ -1,7 +1,7 @@
 <template>
 	<div>
 		<page-metadata title="Admin | Stations" />
-		<div class="container">
+		<div class="admin-tab">
 			<div class="button-row">
 				<button
 					class="button is-primary"
@@ -11,67 +11,73 @@
 				</button>
 				<run-job-dropdown :jobs="jobs" />
 			</div>
-			<table class="table">
-				<thead>
-					<tr>
-						<td>ID</td>
-						<td>Name</td>
-						<td>Type</td>
-						<td>Display Name</td>
-						<td>Description</td>
-						<td>Owner</td>
-						<td>Options</td>
-					</tr>
-				</thead>
-				<tbody>
-					<tr v-for="(station, index) in stations" :key="station._id">
-						<td>
-							<span>{{ station._id }}</span>
-						</td>
-						<td>
-							<span>
-								<router-link
-									:to="{
-										name: 'station',
-										params: { id: station.name }
-									}"
-								>
-									{{ station.name }}
-								</router-link>
-							</span>
-						</td>
-						<td>
-							<span>{{ station.type }}</span>
-						</td>
-						<td>
-							<span>{{ station.displayName }}</span>
-						</td>
-						<td>
-							<span>{{ station.description }}</span>
-						</td>
-						<td>
-							<span
-								v-if="station.type === 'official'"
-								title="Musare"
-								>Musare</span
+			<advanced-table
+				:column-default="columnDefault"
+				:columns="columns"
+				:filters="filters"
+				data-action="stations.getData"
+				name="admin-stations"
+			>
+				<template #column-_id="slotProps">
+					<span :title="slotProps.item._id">{{
+						slotProps.item._id
+					}}</span>
+				</template>
+				<template #column-name="slotProps">
+					<span :title="slotProps.item.name">{{
+						slotProps.item.name
+					}}</span>
+				</template>
+				<template #column-displayName="slotProps">
+					<span :title="slotProps.item.displayName">{{
+						slotProps.item.displayName
+					}}</span>
+				</template>
+				<template #column-type="slotProps">
+					<span :title="slotProps.item.type">{{
+						slotProps.item.type
+					}}</span>
+				</template>
+				<template #column-privacy="slotProps">
+					<span :title="slotProps.item.privacy">{{
+						slotProps.item.privacy
+					}}</span>
+				</template>
+				<template #column-owner="slotProps">
+					<span v-if="slotProps.item.type === 'official'"
+						>Musare</span
+					>
+					<user-id-to-username
+						v-else
+						:user-id="slotProps.item.owner"
+						:link="true"
+					/>
+				</template>
+				<template #bulk-actions="slotProps">
+					<div class="station-bulk-actions">
+						<i
+							class="material-icons edit-stations-icon"
+							@click.prevent="editMany(slotProps.item)"
+							content="Edit Stations"
+							v-tippy
+						>
+							edit
+						</i>
+						<quick-confirm
+							placement="left"
+							@confirm="deleteMany(slotProps.item)"
+						>
+							<i
+								class="material-icons delete-stations-icon"
+								content="Delete Stations"
+								v-tippy
 							>
-							<user-id-to-username
-								v-else
-								:user-id="station.owner"
-								:link="true"
-							/>
-						</td>
-						<td>
-							<a class="button is-info" @click="manage(station)"
-								>Manage</a
-							>
-							<quick-confirm @confirm="removeStation(index)">
-								<a class="button is-danger">Remove</a>
-							</quick-confirm>
-						</td>
-					</tr>
-				</tbody>
-			</table>
+								delete_forever
+							</i>
+						</quick-confirm>
+					</div>
+				</template>
+			</advanced-table>
 		</div>
 
 		<request-song v-if="modals.requestSong" />
@@ -93,10 +99,10 @@ import { mapState, mapActions, mapGetters } from "vuex";
 import { defineAsyncComponent } from "vue";
 
 import Toast from "toasters";
+import AdvancedTable from "@/components/AdvancedTable.vue";
 import UserIdToUsername from "@/components/UserIdToUsername.vue";
 import QuickConfirm from "@/components/QuickConfirm.vue";
 import RunJobDropdown from "@/components/RunJobDropdown.vue";
-import ws from "@/ws";
 
 export default {
 	components: {
@@ -121,6 +127,7 @@ export default {
 		CreateStation: defineAsyncComponent(() =>
 			import("@/components/modals/CreateStation.vue")
 		),
+		AdvancedTable,
 		UserIdToUsername,
 		QuickConfirm,
 		RunJobDropdown
@@ -128,6 +135,100 @@ export default {
 	data() {
 		return {
 			editingStationId: "",
+			columnDefault: {
+				sortable: true,
+				hidable: true,
+				defaultVisibility: "shown",
+				draggable: true,
+				resizable: true,
+				minWidth: 150,
+				maxWidth: 600
+			},
+			columns: [
+				{
+					name: "_id",
+					displayName: "ID",
+					properties: ["_id"],
+					sortProperty: "_id",
+					minWidth: 230,
+					defaultWidth: 230
+				},
+				{
+					name: "name",
+					displayName: "Name",
+					properties: ["name"],
+					sortProperty: "name"
+				},
+				{
+					name: "displayName",
+					displayName: "Display Name",
+					properties: ["displayName"],
+					sortProperty: "displayName"
+				},
+				{
+					name: "type",
+					displayName: "Type",
+					properties: ["type"],
+					sortProperty: "type"
+				},
+				{
+					name: "privacy",
+					displayName: "Privacy",
+					properties: ["privacy"],
+					sortProperty: "privacy"
+				},
+				{
+					name: "owner",
+					displayName: "Owner",
+					properties: ["owner", "type"],
+					sortProperty: "owner",
+					defaultWidth: 150
+				}
+			],
+			filters: [
+				{
+					name: "_id",
+					displayName: "ID",
+					property: "_id",
+					filterTypes: ["exact"],
+					defaultFilterType: "exact"
+				},
+				{
+					name: "name",
+					displayName: "Name",
+					property: "name",
+					filterTypes: ["contains", "exact", "regex"],
+					defaultFilterType: "contains"
+				},
+				{
+					name: "displayName",
+					displayName: "Display Name",
+					property: "displayName",
+					filterTypes: ["contains", "exact", "regex"],
+					defaultFilterType: "contains"
+				},
+				{
+					name: "type",
+					displayName: "Type",
+					property: "type",
+					filterTypes: ["contains", "exact", "regex"],
+					defaultFilterType: "contains"
+				},
+				{
+					name: "privacy",
+					displayName: "Privacy",
+					property: "privacy",
+					filterTypes: ["contains", "exact", "regex"],
+					defaultFilterType: "contains"
+				},
+				{
+					name: "owner",
+					displayName: "Owner",
+					property: "owner",
+					filterTypes: ["contains", "exact", "regex"],
+					defaultFilterType: "contains"
+				}
+			],
 			jobs: [
 				{
 					name: "Clear every station queue",
@@ -137,9 +238,6 @@ export default {
 		};
 	},
 	computed: {
-		...mapState("admin/stations", {
-			stations: state => state.stations
-		}),
 		...mapState("modalVisibility", {
 			modals: state => state.modals
 		}),
@@ -148,85 +246,62 @@ export default {
 		})
 	},
 	mounted() {
-		ws.onConnect(this.init);
-
-		this.socket.on("event:admin.station.created", res =>
-			this.stationAdded(res.data.station)
-		);
-
-		this.socket.on("event:admin.station.deleted", res =>
-			this.stationRemoved(res.data.stationId)
-		);
+		// TODO
+		// this.socket.on("event:admin.station.created", res =>
+		// 	this.stationAdded(res.data.station)
+		// );
+		// this.socket.on("event:admin.station.deleted", res =>
+		// 	this.stationRemoved(res.data.stationId)
+		// );
 	},
 	methods: {
-		removeStation(index) {
-			this.socket.dispatch(
-				"stations.remove",
-				this.stations[index]._id,
-				res => new Toast(res.message)
-			);
-		},
-		manage(station) {
-			this.editingStationId = station._id;
-			this.openModal("manageStation");
+		editMany(selectedRows) {
+			if (selectedRows.length === 1) {
+				this.editingStationId = selectedRows[0]._id;
+				this.openModal("manageStation");
+			} else {
+				new Toast("Bulk editing not yet implemented.");
+			}
 		},
-		init() {
-			this.socket.dispatch("stations.index", res => {
-				if (res.status === "success")
-					this.loadStations(res.data.stations);
-			});
-
-			this.socket.dispatch("apis.joinAdminRoom", "stations", () => {});
+		deleteMany(selectedRows) {
+			if (selectedRows.length === 1) {
+				this.socket.dispatch(
+					"stations.remove",
+					selectedRows[0]._id,
+					res => new Toast(res.message)
+				);
+			} else {
+				new Toast("Bulk deleting not yet implemented.");
+			}
 		},
-		...mapActions("modalVisibility", ["openModal"]),
-		...mapActions("admin/stations", [
-			"manageStation",
-			"loadStations",
-			"stationRemoved",
-			"stationAdded"
-		])
+		...mapActions("modalVisibility", ["openModal"])
 	}
 };
 </script>
 
 <style lang="scss" scoped>
-.night-mode {
-	.table {
-		color: var(--light-grey-2);
-		background-color: var(--dark-grey-3);
+.bulk-popup {
+	.station-bulk-actions {
+		display: flex;
+		flex-direction: row;
+		width: 100%;
+		justify-content: space-evenly;
 
-		thead tr {
-			background: var(--dark-grey-3);
-			td {
-				color: var(--white);
-			}
-		}
-
-		tbody tr:hover {
-			background-color: var(--dark-grey-4) !important;
-		}
+		.material-icons {
+			position: relative;
+			top: 6px;
+			margin-left: 5px;
+			cursor: pointer;
+			color: var(--primary-color);
 
-		tbody tr:nth-child(even) {
-			background-color: var(--dark-grey-2);
+			&:hover,
+			&:focus {
+				filter: brightness(90%);
+			}
 		}
-
-		strong {
-			color: var(--light-grey-2);
+		.delete-stations-icon {
+			color: var(--dark-red);
 		}
 	}
 }
-
-td {
-	word-wrap: break-word;
-	max-width: 10vw;
-	vertical-align: middle;
-
-	& > div {
-		display: inline-flex;
-	}
-}
-
-.is-info:focus {
-	background-color: var(--primary-color);
-}
 </style>