浏览代码

feat: Started adding base and news model store

Owen Diffey 1 年之前
父节点
当前提交
9cf304a0cf

+ 34 - 27
frontend/src/components/modals/EditNews.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, onMounted } from "vue";
+import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from "vue";
 import { marked } from "marked";
 import DOMPurify from "dompurify";
 import Toast from "toasters";
 import { formatDistance } from "date-fns";
 import { useWebsocketStore } from "@/stores/websocket";
 import { useModalsStore } from "@/stores/modals";
+import { useNewsModelStore } from "@/stores/models/news";
 import { useForm } from "@/composables/useForm";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
@@ -23,10 +24,12 @@ const props = defineProps({
 	sector: { type: String, default: "admin" }
 });
 
-const { onReady, runJob } = useWebsocketStore();
+const { onReady, removeReadyCallback } = useWebsocketStore();
 
 const { closeCurrentModal } = useModalsStore();
 
+const { create, findById, updateById, unregisterModels } = useNewsModelStore();
+
 const createdBy = ref();
 const createdAt = ref(0);
 
@@ -75,12 +78,10 @@ const { inputs, save, setOriginalValue } = useForm(
 				showToNewUsers: values.showToNewUsers
 			};
 
-			runJob(`data.news.${props.createNews ? "create" : "updateById"}`, {
-				_id: props.createNews ? null : props.newsId,
-				query
-			})
-				.then(resolve)
-				.catch(reject);
+			const method = props.createNews
+				? create(query)
+				: updateById(props.newsId, query);
+			method.then(resolve).catch(reject);
 		} else {
 			if (status === "unchanged") new Toast(messages.unchanged);
 			else if (status === "error")
@@ -95,6 +96,24 @@ const { inputs, save, setOriginalValue } = useForm(
 	}
 );
 
+const onReadyCallback = async () => {
+	if (props.newsId && !props.createNews) {
+		const { value: data } = await findById(props.newsId).catch(() => {
+			new Toast("News with that ID not found.");
+			closeCurrentModal();
+		});
+
+		setOriginalValue({
+			markdown: data.markdown,
+			status: data.status,
+			showToNewUsers: data.showToNewUsers
+		});
+
+		createdBy.value = data.createdBy;
+		createdAt.value = data.createdAt;
+	}
+};
+
 onMounted(async () => {
 	marked.use({
 		renderer: {
@@ -107,25 +126,13 @@ onMounted(async () => {
 		}
 	});
 
-	await onReady(async () => {
-		if (props.newsId && !props.createNews) {
-			const data = await runJob(`data.news.findById`, {
-				_id: props.newsId
-			}).catch(() => {
-				new Toast("News with that ID not found.");
-				closeCurrentModal();
-			});
-
-			setOriginalValue({
-				markdown: data.markdown,
-				status: data.status,
-				showToNewUsers: data.showToNewUsers
-			});
-
-			createdBy.value = data.createdBy;
-			createdAt.value = data.createdAt;
-		}
-	});
+	await onReady(onReadyCallback);
+});
+
+onBeforeUnmount(async () => {
+	if (props.newsId && !props.createNews) await unregisterModels(props.newsId);
+
+	removeReadyCallback(onReadyCallback);
 });
 </script>
 

+ 5 - 9
frontend/src/pages/Admin/News.vue

@@ -1,11 +1,9 @@
 <script setup lang="ts">
 import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
-import { GenericResponse } from "@musare_types/actions/GenericActions";
-import { useWebsocketStore } from "@/stores/websocket";
 import { useModalsStore } from "@/stores/modals";
-import { useUserAuthStore } from "@/stores/userAuth";
 import { TableColumn, TableFilter } from "@/types/advancedTable";
+import { useNewsModelStore } from "@/stores/models/news";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -17,8 +15,6 @@ const UserLink = defineAsyncComponent(
 	() => import("@/components/UserLink.vue")
 );
 
-const { runJob } = useWebsocketStore();
-
 const columnDefault = ref<TableColumn>({
 	sortable: true,
 	hidable: true,
@@ -113,10 +109,10 @@ const filters = ref<TableFilter[]>([
 
 const { openModal } = useModalsStore();
 
-const { hasPermission } = useUserAuthStore();
+const { deleteById, hasPermission } = useNewsModelStore();
 
 const remove = async (_id: string) => {
-	const res = await runJob(`data.news.deleteById`, { _id });
+	const res = await deleteById(_id);
 	new Toast(res.message);
 };
 </script>
@@ -154,7 +150,7 @@ const remove = async (_id: string) => {
 			<template #column-options="slotProps">
 				<div class="row-options">
 					<button
-						v-if="hasPermission('news.update')"
+						v-if="hasPermission('news.update', slotProps.item._id)"
 						class="button is-primary icon-with-button material-icons"
 						@click="
 							openModal({
@@ -168,7 +164,7 @@ const remove = async (_id: string) => {
 						edit
 					</button>
 					<quick-confirm
-						v-if="hasPermission('news.remove')"
+						v-if="hasPermission('news.remove', slotProps.item._id)"
 						@confirm="remove(slotProps.item._id)"
 						:disabled="slotProps.item.removed"
 					>

+ 24 - 9
frontend/src/pages/News.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, onMounted } from "vue";
+import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from "vue";
 
 import { formatDistance } from "date-fns";
 import { marked } from "marked";
@@ -12,6 +12,7 @@ import {
 } from "@musare_types/events/NewsEvents";
 import { GetPublishedNewsResponse } from "@musare_types/actions/NewsActions";
 import { useWebsocketStore } from "@/stores/websocket";
+import { useNewsModelStore } from "@/stores/models/news";
 
 const MainHeader = defineAsyncComponent(
 	() => import("@/components/MainHeader.vue")
@@ -23,12 +24,22 @@ const UserLink = defineAsyncComponent(
 	() => import("@/components/UserLink.vue")
 );
 
-const { onReady, runJob, subscribe } = useWebsocketStore();
+const { onReady, subscribe, unsubscribe, removeReadyCallback } =
+	useWebsocketStore();
+const { newest, registerModels, unregisterModels } = useNewsModelStore();
 
 const news = ref<NewsModel[]>([]);
 
 const { sanitize } = DOMPurify;
 
+const onCreated = async ({ doc }) => {
+	news.value.unshift(...(await registerModels(doc)));
+};
+
+const onReadyCallback = async () => {
+	news.value = await newest();
+};
+
 onMounted(async () => {
 	marked.use({
 		renderer: {
@@ -41,13 +52,9 @@ onMounted(async () => {
 		}
 	});
 
-	await onReady(async () => {
-		news.value = await runJob("data.news.newest", {});
+	await onReady(onReadyCallback);
 
-		await subscribe("model.news.created", ({ doc }) => {
-			news.value.unshift(doc);
-		});
-	});
+	await subscribe("model.news.created", onCreated);
 
 	// TODO: Subscribe to loaded model updated/deleted events
 	// socket.on("event:news.updated", (res: NewsUpdatedResponse) => {
@@ -71,6 +78,14 @@ onMounted(async () => {
 	// 	news.value = news.value.filter(item => item._id !== res.data.newsId);
 	// });
 });
+
+onBeforeUnmount(async () => {
+	await unregisterModels(news.value.map(model => model.value._id));
+
+	await unsubscribe("model.news.created", onCreated);
+
+	removeReadyCallback(onReadyCallback);
+});
 </script>
 
 <template>
@@ -81,7 +96,7 @@ onMounted(async () => {
 			<div class="content-wrapper">
 				<h1 class="has-text-centered page-title">News</h1>
 				<div
-					v-for="item in news"
+					v-for="{ value: item } in news"
 					:key="item._id"
 					class="section news-item"
 				>

+ 147 - 0
frontend/src/stores/models/model.ts

@@ -0,0 +1,147 @@
+import { Ref, ref } from "vue";
+import { useWebsocketStore } from "../websocket";
+
+export const createModelStore = modelName => {
+	const { runJob, subscribe, unsubscribe } = useWebsocketStore();
+
+	const models = ref<Ref<any>[]>([]);
+	const permissions = ref(null);
+	const modelPermissions = ref({});
+
+	const onUpdated = async ({ doc }) => {
+		const index = models.value.findIndex(
+			model => model.value._id === doc._id
+		);
+		if (index > -1) models.value[index].value = doc;
+	};
+
+	const onDeleted = async ({ oldDoc }) => {
+		const index = models.value.findIndex(
+			model => model.value._id === oldDoc._id
+		);
+		if (index > -1) await unregisterModels(oldDoc._id);
+
+		if (modelPermissions.value[oldDoc._id])
+			delete modelPermissions.value[oldDoc._id];
+	};
+
+	const registerModels = async docs =>
+		Promise.all(
+			(Array.isArray(docs) ? docs : [docs]).map(async _doc => {
+				const docRef = ref(_doc);
+
+				if (!models.value.find(model => model.value._id === _doc._id))
+					models.value.push(docRef);
+
+				await subscribe(
+					`model.${modelName}.updated.${_doc._id}`,
+					onUpdated
+				);
+
+				await subscribe(
+					`model.${modelName}.deleted.${_doc._id}`,
+					onDeleted
+				);
+
+				return docRef;
+			})
+		);
+
+	const unregisterModels = async modelIds =>
+		Promise.all(
+			(Array.isArray(modelIds) ? modelIds : [modelIds]).map(
+				async modelId => {
+					if (
+						models.value.findIndex(
+							model => model.value._id === modelId
+						) === -1
+					)
+						return;
+
+					await unsubscribe(
+						`model.${modelName}.updated.${modelId}`,
+						onUpdated
+					);
+
+					await unsubscribe(
+						`model.${modelName}.deleted.${modelId}`,
+						onDeleted
+					);
+
+					models.value.splice(
+						models.value.findIndex(
+							model => model.value._id === modelId
+						),
+						1
+					);
+
+					if (modelPermissions.value[modelId])
+						delete modelPermissions.value[modelId];
+				}
+			)
+		);
+
+	const getUserModelPermissions = async (_id?: string) => {
+		if (!_id && permissions.value) return permissions.value;
+
+		if (_id && modelPermissions.value[_id])
+			return modelPermissions.value[_id];
+
+		const data = await runJob("api.getUserModelPermissions", {
+			modelName,
+			modelId: _id
+		});
+
+		if (_id) {
+			modelPermissions.value[_id] = data;
+
+			return modelPermissions.value[_id];
+		}
+
+		permissions.value = data;
+
+		return permissions.value;
+	};
+
+	const hasPermission = async (permission: string, _id?: string) => {
+		const data = await getUserModelPermissions(_id);
+
+		return !!data[permission];
+	};
+
+	const create = async query => runJob(`data.${modelName}.create`, { query });
+
+	const findById = async _id => {
+		const existingModel = models.value.find(
+			model => model.value._id === _id
+		);
+
+		if (existingModel) return existingModel;
+
+		const data = await runJob(`data.${modelName}.findById`, { _id });
+
+		const [model] = await registerModels(data);
+
+		return model;
+	};
+
+	const updateById = async (_id, query) =>
+		runJob(`data.${modelName}.updateById`, { _id, query });
+
+	const deleteById = async _id =>
+		runJob(`data.${modelName}.deleteById`, { _id });
+
+	return {
+		models,
+		permissions,
+		modelPermissions,
+		registerModels,
+		unregisterModels,
+		getUserModelPermissions,
+		hasPermission,
+		create,
+		findById,
+		updateById,
+		deleteById
+	};
+};

+ 23 - 0
frontend/src/stores/models/news.ts

@@ -0,0 +1,23 @@
+import { defineStore } from "pinia";
+import { useWebsocketStore } from "../websocket";
+import { createModelStore } from "./model";
+
+export const useNewsModelStore = defineStore("newsModel", () => {
+	const { runJob } = useWebsocketStore();
+
+	const modelStore = createModelStore("news");
+
+	const published = async () => runJob("data.news.published", {});
+
+	const newest = async (showToNewUsers?) => {
+		const data = await runJob("data.news.newest", { showToNewUsers });
+
+		return modelStore.registerModels(data);
+	};
+
+	return {
+		...modelStore,
+		published,
+		newest
+	};
+});