diff --git a/src/stores/resources.ts b/src/stores/resources.ts index 6e648731ee92345303510a247e845a6eae20c9ad..986b90f7f7f0020be830ac6b0de110f1453c9204 100644 --- a/src/stores/resources.ts +++ b/src/stores/resources.ts @@ -5,9 +5,12 @@ import type { ResourceVersionIn, ResourceVersionOut, } from "@/client/resource"; -import { ResourceService, ResourceVersionService } from "@/client/resource"; +import { + ResourceService, + ResourceVersionService, + Status, +} from "@/client/resource"; import { useAuthStore } from "@/stores/users"; -import { Status } from "@/client/resource"; import { useNameStore } from "@/stores/names"; export const useResourceStore = defineStore({ @@ -16,9 +19,11 @@ export const useResourceStore = defineStore({ ({ resourceMapping: {}, ownResourceMapping: {}, + reviewableResourceMapping: {}, }) as { resourceMapping: Record<string, ResourceOut>; ownResourceMapping: Record<string, ResourceOut>; + reviewableResourceMapping: Record<string, ResourceOut>; }, getters: { resources(): ResourceOut[] { @@ -27,20 +32,45 @@ export const useResourceStore = defineStore({ ownResources(): ResourceOut[] { return Object.values(this.ownResourceMapping); }, + reviewableResources(): ResourceOut[] { + return Object.values(this.reviewableResourceMapping); + }, }, actions: { - __addNameToMapping(key: string, value: string) { - const nameStore = useNameStore(); - nameStore.addNameToMapping(key, value); - }, fetchResources( maintainerId?: string, versionStatus?: Status[], ): Promise<ResourceOut[]> { return ResourceService.resourceListResources(maintainerId, versionStatus); }, + fetchReviewableResources(onFinally?: () => void): Promise<ResourceOut[]> { + if (Object.keys(this.reviewableResourceMapping).length > 0) { + onFinally?.(); + } + return this.fetchResources(undefined, [ + Status.SYNC_REQUESTED, + Status.SYNCHRONIZING, + ]) + .then((resources) => { + const newMapping: Record<string, ResourceOut> = {}; + const nameStore = useNameStore(); + for (const resource of resources) { + newMapping[resource.resource_id] = resource; + nameStore.addNameToMapping(resource.resource_id, resource.name); + for (const version of resource.versions) { + nameStore.addNameToMapping( + version.resource_version_id, + version.release, + ); + } + } + this.reviewableResourceMapping = newMapping; + return resources; + }) + .finally(onFinally); + }, fetchPublicResources(onFinally?: () => void): Promise<ResourceOut[]> { - if (Object.keys(this.resourceMapping).length > 0) { + if (this.resources.length > 0) { onFinally?.(); } return this.fetchResources() @@ -89,7 +119,7 @@ export const useResourceStore = defineStore({ }, fetchOwnResources(onFinally?: () => void): Promise<ResourceOut[]> { const authStore = useAuthStore(); - if (Object.keys(this.ownResourceMapping).length > 0) { + if (this.ownResources.length > 0) { onFinally?.(); } return this.fetchResources(authStore.currentUID, Object.values(Status)) @@ -182,5 +212,36 @@ export const useResourceStore = defineStore({ return versionOut; }); }, + syncResource( + resourceVersion: ResourceVersionOut, + ): Promise<ResourceVersionOut> { + return ResourceVersionService.resourceVersionResourceVersionSync( + resourceVersion.resource_id, + resourceVersion.resource_version_id, + ).then((changedVersion) => { + if ( + this.reviewableResourceMapping[changedVersion.resource_id] == + undefined + ) { + return changedVersion; + } + const versionIndex = this.reviewableResourceMapping[ + changedVersion.resource_id + ].versions.findIndex( + (version) => + version.resource_version_id == changedVersion.resource_version_id, + ); + if (versionIndex > -1) { + this.reviewableResourceMapping[changedVersion.resource_id].versions[ + versionIndex + ] = changedVersion; + } else { + this.reviewableResourceMapping[ + changedVersion.resource_id + ].versions.push(changedVersion); + } + return changedVersion; + }); + }, }, }); diff --git a/src/views/resources/ReviewResourceView.vue b/src/views/resources/ReviewResourceView.vue index cb91f85b499515f2557f4d92bda43bce8cb03730..b1477b56a4c2dfdeca38b2ea188089348b0c6206 100644 --- a/src/views/resources/ReviewResourceView.vue +++ b/src/views/resources/ReviewResourceView.vue @@ -1,49 +1,88 @@ <script setup lang="ts"> import { useResourceStore } from "@/stores/resources"; import { computed, onMounted, reactive } from "vue"; -import { type ResourceOut, Status } from "@/client/resource"; +import { type ResourceVersionOut, Status } from "@/client/resource"; import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue"; +import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; +import { Tooltip } from "bootstrap"; const resourceRepository = useResourceStore(); +let refreshTimeout: NodeJS.Timeout | undefined = undefined; const resourceState = reactive<{ - reviewableResources: ResourceOut[]; + sendingRequest: boolean; loading: boolean; }>({ - reviewableResources: [], + sendingRequest: false, loading: true, }); const countItems = computed<number>(() => - resourceState.reviewableResources.reduce( + resourceRepository.reviewableResources.reduce( (previousValue, currentValue) => previousValue + currentValue.versions.length, 0, ), ); +function fetchResources() { + resourceRepository.fetchReviewableResources().finally(() => { + resourceState.loading = false; + }); +} + +function clickRefreshResources() { + clearTimeout(refreshTimeout); + refreshTimeout = setTimeout(() => { + fetchResources(); + }, 500); +} + +function syncResource(resourceVersion: ResourceVersionOut) { + resourceState.sendingRequest = true; + resourceRepository.syncResource(resourceVersion).finally(() => { + resourceState.sendingRequest = false; + }); +} + onMounted(() => { - resourceRepository - .fetchResources(undefined, [Status.SYNC_REQUESTED, Status.SYNCHRONIZING]) - .then((resources) => { - resourceState.reviewableResources = resources; - }) - .finally(() => { - resourceState.loading = false; - }); + fetchResources(); + new Tooltip("#refreshReviewableResourcesButton"); }); </script> <template> - <div class="row m-2 border-bottom mb-4"> - <h2 class="mb-2">Resource Requests</h2> + <div + class="row m-2 border-bottom mb-4 justify-content-between align-items-center" + > + <h2 class="w-fit">Resource Requests</h2> + <span + class="w-fit" + tabindex="0" + data-bs-title="Refresh Reviewable Resources" + data-bs-toggle="tooltip" + id="refreshReviewableResourcesButton" + > + <button + type="button" + class="btn btn-primary btn-light me-2 shadow-sm border w-fit" + :disabled="resourceState.loading" + @click="clickRefreshResources" + > + <font-awesome-icon icon="fa-solid fa-arrow-rotate-right" /> + <span class="visually-hidden">Refresh Reviewable Resources</span> + </button> + </span> </div> <div v-if="resourceState.loading" class="text-center mt-5"> <div class="spinner-border" style="width: 3rem; height: 3rem" role="status"> <span class="visually-hidden">Loading...</span> </div> </div> - <table class="table caption-top table-striped table-hover align-middle"> + <table + class="table caption-top table-striped table-hover align-middle" + v-else-if="resourceRepository.reviewableResources.length > 0" + > <caption> Display {{ @@ -62,7 +101,7 @@ onMounted(() => { </thead> <tbody> <template - v-for="resource in resourceState.reviewableResources" + v-for="resource in resourceRepository.reviewableResources" :key="resource.resource_id" > <tr @@ -91,10 +130,17 @@ onMounted(() => { v-if="version.status === Status.SYNC_REQUESTED" class="btn-group" > - <button type="button" class="btn btn-success btn-sm"> + <button + type="button" + class="btn btn-success btn-sm" + :disabled="resourceState.sendingRequest" + @click="syncResource(version)" + > Synchronize </button> - <button type="button" class="btn btn-danger btn-sm">Deny</button> + <button type="button" class="btn btn-danger btn-sm" disabled> + Deny + </button> </div> <div v-else-if="version.status === Status.SYNCHRONIZING" @@ -115,6 +161,9 @@ onMounted(() => { </template> </tbody> </table> + <div v-else> + <p>Hello</p> + </div> </template> <style scoped></style>