|
@@ -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>
|