Skip to content
Snippets Groups Projects
Verified Commit 81aa8cd6 authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Add function to request a resource synchronization

parent 7edac797
No related branches found
No related tags found
1 merge request!97Resolve "Adapt UI to new Resoruce creation flow"
This commit is part of merge request !97. Comments created here will be created in the context of that merge request.
...@@ -2,10 +2,13 @@ ...@@ -2,10 +2,13 @@
import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import BootstrapModal from "@/components/modals/BootstrapModal.vue";
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
type reasonfor = "request" | "rejection";
const props = defineProps<{ const props = defineProps<{
modalId: string; modalId: string;
modalLabel: string; modalLabel: string;
loading: boolean; loading: boolean;
purpose: reasonfor;
}>(); }>();
const formState = reactive<{ const formState = reactive<{
...@@ -48,7 +51,7 @@ function sendSaveEvent() { ...@@ -48,7 +51,7 @@ function sendSaveEvent() {
:class="{ 'was-validated': formState.validated }" :class="{ 'was-validated': formState.validated }"
> >
<label :for="'reason-modal-input-' + randomIDSuffix" class="form-label" <label :for="'reason-modal-input-' + randomIDSuffix" class="form-label"
>Reason for rejection</label >Reason for {{ props.purpose }}</label
> >
<textarea <textarea
class="form-control" class="form-control"
...@@ -56,7 +59,7 @@ function sendSaveEvent() { ...@@ -56,7 +59,7 @@ function sendSaveEvent() {
rows="3" rows="3"
minlength="16" minlength="16"
maxlength="512" maxlength="512"
placeholder="State your reason for rejection" :placeholder="'State your reason for the ' + props.purpose"
v-model="formState.reason" v-model="formState.reason"
></textarea> ></textarea>
</form> </form>
......
...@@ -437,7 +437,7 @@ onMounted(() => { ...@@ -437,7 +437,7 @@ onMounted(() => {
:checked="props.viewMode === 'simple'" :checked="props.viewMode === 'simple'"
@click=" @click="
router.replace({ router.replace({
query: { viewMode: 'simple' }, query: { ...route.query, viewMode: 'simple' },
hash: route.hash, hash: route.hash,
}) })
" "
...@@ -454,7 +454,7 @@ onMounted(() => { ...@@ -454,7 +454,7 @@ onMounted(() => {
:checked="props.viewMode === 'advanced'" :checked="props.viewMode === 'advanced'"
@click=" @click="
router.replace({ router.replace({
query: { viewMode: 'advanced' }, query: { ...route.query, viewMode: 'advanced' },
hash: route.hash, hash: route.hash,
}) })
" "
...@@ -471,7 +471,7 @@ onMounted(() => { ...@@ -471,7 +471,7 @@ onMounted(() => {
:checked="props.viewMode === 'expert'" :checked="props.viewMode === 'expert'"
@click=" @click="
router.replace({ router.replace({
query: { viewMode: 'expert' }, query: { ...route.query, viewMode: 'expert' },
hash: route.hash, hash: route.hash,
}) })
" "
......
...@@ -26,7 +26,7 @@ const props = defineProps<{ ...@@ -26,7 +26,7 @@ const props = defineProps<{
let refreshTimeout: NodeJS.Timeout | undefined = undefined; let refreshTimeout: NodeJS.Timeout | undefined = undefined;
const stateToUIMapping: Record<Status, string> = { const stateToUIMapping: Record<Status, string> = {
APPROVED: "", APPROVED: "Resource approved",
WAIT_FOR_REVIEW: "Wait for review", WAIT_FOR_REVIEW: "Wait for review",
CLUSTER_DELETE_ERROR: "Error deleting resource on cluster", CLUSTER_DELETE_ERROR: "Error deleting resource on cluster",
CLUSTER_DELETING: "Resource deletion on cluster in progress", CLUSTER_DELETING: "Resource deletion on cluster in progress",
...@@ -39,13 +39,14 @@ const stateToUIMapping: Record<Status, string> = { ...@@ -39,13 +39,14 @@ const stateToUIMapping: Record<Status, string> = {
S3_DELETED: "Tarball deleted in S3", S3_DELETED: "Tarball deleted in S3",
SYNCHRONIZED: "Resource available", SYNCHRONIZED: "Resource available",
SYNCHRONIZING: "Synchronizing to cluster in progress", SYNCHRONIZING: "Synchronizing to cluster in progress",
SYNC_REQUESTED: "Wait for download on cluster", SYNC_REQUESTED: "Synchronization to cluster requested",
LATEST: "Resource available (latest)", LATEST: "Resource available (latest)",
}; };
const emit = defineEmits<{ const emit = defineEmits<{
(e: "click-info", resourceVersion: ResourceVersionOut): void; (e: "click-info", resourceVersion: ResourceVersionOut): void;
(e: "click-update", resource: ResourceOut): void; (e: "click-update", resource: ResourceOut): void;
(e: "click-request-sync", resourceVersion: ResourceVersionOut): void;
}>(); }>();
const resourceVersionS3Ready = ref<Record<string, boolean>>({}); const resourceVersionS3Ready = ref<Record<string, boolean>>({});
...@@ -82,6 +83,10 @@ function requestReview(resourceVersion: ResourceVersionOut) { ...@@ -82,6 +83,10 @@ function requestReview(resourceVersion: ResourceVersionOut) {
onMounted(() => { onMounted(() => {
if (!props.loading) { if (!props.loading) {
new Tooltip("#resource-name-" + props.resource.resource_id);
if (props.resource.private) {
new Tooltip("#resource-private-icon-" + props.resource.resource_id);
}
for (const r of props.resource.versions) { for (const r of props.resource.versions) {
if (r.status == Status.RESOURCE_REQUESTED) { if (r.status == Status.RESOURCE_REQUESTED) {
checkS3Resource(r); checkS3Resource(r);
...@@ -110,8 +115,21 @@ onMounted(() => { ...@@ -110,8 +115,21 @@ onMounted(() => {
<div v-if="props.loading" class="placeholder-glow w-100"> <div v-if="props.loading" class="placeholder-glow w-100">
<span class="placeholder col-6"></span> <span class="placeholder col-6"></span>
</div> </div>
<div v-else> <div v-else class="d-inline-flex align-items-center text-truncate">
<span>{{ props.resource.name }}</span> <span
:id="'resource-name-' + props.resource.resource_id"
data-bs-toggle="tooltip"
:data-bs-title="props.resource.name"
>{{ props.resource.name }}</span
>
<font-awesome-icon
v-if="props.resource.private"
:id="'resource-private-icon-' + props.resource.resource_id"
icon="fa-solid fa-lock"
class="fs-5 ms-2 tooltip-private-repository"
data-bs-toggle="tooltip"
data-bs-title="Private resource"
/>
</div> </div>
<button <button
v-if="props.extended" v-if="props.extended"
...@@ -180,13 +198,27 @@ onMounted(() => { ...@@ -180,13 +198,27 @@ onMounted(() => {
}" }"
:data-bs-parent="'#accordion-' + props.resource.resource_id" :data-bs-parent="'#accordion-' + props.resource.resource_id"
> >
<div class="accordion-body"> <div class="accordion-body d-flex flex-column">
<div> <div>
Registered at: Registered at:
{{ {{
dayjs.unix(resourceVersion.created_at).format("DD MMM YYYY") dayjs.unix(resourceVersion.created_at).format("DD MMM YYYY")
}} }}
</div> </div>
<div
v-if="resourceVersion.status == Status.APPROVED"
class="d-grid gap-2"
>
<button
type="button"
class="btn btn-primary"
@click="emit('click-request-sync', resourceVersion)"
data-bs-toggle="modal"
data-bs-target="#request-synchronization-modal"
>
Request synchronization
</button>
</div>
<div <div
v-if=" v-if="
props.extended && props.extended &&
...@@ -231,7 +263,6 @@ onMounted(() => { ...@@ -231,7 +263,6 @@ onMounted(() => {
resourceVersion.status === Status.SYNCHRONIZED || resourceVersion.status === Status.SYNCHRONIZED ||
resourceVersion.status === Status.LATEST resourceVersion.status === Status.LATEST
" "
class="my-1"
> >
<label <label
:for=" :for="
...@@ -241,7 +272,7 @@ onMounted(() => { ...@@ -241,7 +272,7 @@ onMounted(() => {
class="form-label" class="form-label"
>Nextflow Access Path:</label >Nextflow Access Path:</label
> >
<div class="input-group fs-4 mb-3"> <div class="input-group fs-4">
<div <div
class="input-group-text hover-info" class="input-group-text hover-info"
:id=" :id="
...@@ -273,9 +304,8 @@ onMounted(() => { ...@@ -273,9 +304,8 @@ onMounted(() => {
<div <div
v-if=" v-if="
props.extended && props.extended &&
resourceVersion.status !== Status.S3_DELETED resourceVersion.status === Status.RESOURCE_REQUESTED
" "
class="my-1"
> >
<label <label
:for=" :for="
...@@ -339,4 +369,8 @@ onMounted(() => { ...@@ -339,4 +369,8 @@ onMounted(() => {
transform: translate(0, -5px); transform: translate(0, -5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
} }
.accordion-body > div:not(:last-child) {
margin-bottom: 0.5rem !important;
}
</style> </style>
...@@ -114,16 +114,16 @@ onMounted(() => { ...@@ -114,16 +114,16 @@ onMounted(() => {
<div v-if="props.loading" class="placeholder-glow w-100"> <div v-if="props.loading" class="placeholder-glow w-100">
<span class="placeholder col-6"></span> <span class="placeholder col-6"></span>
</div> </div>
<div v-else class="text-truncate"> <div v-else class="text-truncate d-inline-flex align-items-center">
<span>{{ props.workflow.name }}</span>
<font-awesome-icon <font-awesome-icon
v-if="props.workflow.private" v-if="props.workflow.private"
icon="fa-solid fa-lock" icon="fa-solid fa-lock"
class="fs-5 me-2 tooltip-private-repository" class="fs-5 ms-2 tooltip-private-repository"
:id="'tooltip-' + randomIDSuffix" :id="'tooltip-' + randomIDSuffix"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-title="Private Git Repository" data-bs-title="Private Git repository"
/> />
<span>{{ props.workflow.name }}</span>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button <button
......
...@@ -202,32 +202,37 @@ export const useResourceStore = defineStore({ ...@@ -202,32 +202,37 @@ export const useResourceStore = defineStore({
resourceVersion.resource_id, resourceVersion.resource_id,
resourceVersion.resource_version_id, resourceVersion.resource_version_id,
request, request,
).then((changedResourceVersion) => { )
if ( .then((changedResourceVersion) => {
this.ownResourceMapping[changedResourceVersion.resource_id] == const versionIndex = this.resourceMapping[
undefined changedResourceVersion.resource_id
) { ]?.versions?.findIndex(
this.fetchResource(resourceVersion.resource_id); (version) =>
version.resource_version_id ==
changedResourceVersion.resource_version_id,
);
if (versionIndex != undefined && versionIndex > -1) {
this.resourceMapping[changedResourceVersion.resource_id].versions[
versionIndex
] = changedResourceVersion;
}
return changedResourceVersion; return changedResourceVersion;
} })
const versionIndex = this.resourceMapping[ .then((changedResourceVersion) => {
changedResourceVersion.resource_id const versionIndex = this.ownResourceMapping[
].versions.findIndex(
(version) =>
version.resource_version_id ==
changedResourceVersion.resource_version_id,
);
if (versionIndex > -1) {
this.resourceMapping[changedResourceVersion.resource_id].versions[
versionIndex
] = changedResourceVersion;
} else {
this.resourceMapping[
changedResourceVersion.resource_id changedResourceVersion.resource_id
].versions.push(changedResourceVersion); ]?.versions?.findIndex(
} (version) =>
return changedResourceVersion; version.resource_version_id ==
}); changedResourceVersion.resource_version_id,
);
if (versionIndex != undefined && versionIndex > -1) {
this.ownResourceMapping[
changedResourceVersion.resource_id
].versions[versionIndex] = changedResourceVersion;
}
return changedResourceVersion;
});
}, },
requestReview( requestReview(
resourceVersion: ResourceVersionOut, resourceVersion: ResourceVersionOut,
......
...@@ -152,6 +152,7 @@ onMounted(() => { ...@@ -152,6 +152,7 @@ onMounted(() => {
modal-id="sync-request-reject-modal" modal-id="sync-request-reject-modal"
modal-label="Resource Synchronization Request Reject Modal" modal-label="Resource Synchronization Request Reject Modal"
:loading="resourceState.sendingRequest" :loading="resourceState.sendingRequest"
purpose="rejection"
@save="(reason) => rejectSyncRequest(reason, resourceState.rejectResource)" @save="(reason) => rejectSyncRequest(reason, resourceState.rejectResource)"
> >
<template #header> <template #header>
...@@ -194,7 +195,7 @@ onMounted(() => { ...@@ -194,7 +195,7 @@ onMounted(() => {
</div> </div>
<div v-else class="d-flex flex-column"> <div v-else class="d-flex flex-column">
<div <div
class="border p-2 pb-0 rounded mb-2 d-flex" class="border p-2 pb-0 rounded mb-2 d-flex hover-card"
v-for="request in resourceRepository.syncRequests" v-for="request in resourceRepository.syncRequests"
:key="request.resource_version_id" :key="request.resource_version_id"
> >
...@@ -263,4 +264,8 @@ onMounted(() => { ...@@ -263,4 +264,8 @@ onMounted(() => {
</div> </div>
</template> </template>
<style scoped></style> <style scoped>
.hover-card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
</style>
...@@ -5,27 +5,34 @@ import ResourceCard from "@/components/resources/ResourceCard.vue"; ...@@ -5,27 +5,34 @@ import ResourceCard from "@/components/resources/ResourceCard.vue";
import CardTransitionGroup from "@/components/transitions/CardTransitionGroup.vue"; import CardTransitionGroup from "@/components/transitions/CardTransitionGroup.vue";
import { useAuthStore } from "@/stores/users"; import { useAuthStore } from "@/stores/users";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import type { ResourceOut } from "@/client/resource"; import type { ResourceOut, ResourceVersionOut } from "@/client/resource";
import ReasonModal from "@/components/modals/ReasonModal.vue";
import { Modal, Toast } from "bootstrap";
import BootstrapToast from "@/components/BootstrapToast.vue";
const resourceRepository = useResourceStore(); const resourceRepository = useResourceStore();
const userRepository = useAuthStore(); const userRepository = useAuthStore();
let requestReasonModal: Modal | null = null;
let syncRequestSuccessToast: Toast | null = null;
const resourceState = reactive<{ const resourceState = reactive<{
loading: boolean; loading: boolean;
filterString: string; filterString: string;
sortDesc: boolean; sortDesc: boolean;
showPrivate: boolean;
syncResourceVersion?: ResourceVersionOut;
}>({ }>({
loading: true, loading: true,
filterString: "", filterString: "",
sortDesc: true, sortDesc: true,
showPrivate: false,
syncResourceVersion: undefined,
}); });
const publicResources = computed<ResourceOut[]>(() =>
resourceRepository.resources.filter((resource) => !resource.private),
);
const sortedResourcesByName = computed<ResourceOut[]>(() => { const sortedResourcesByName = computed<ResourceOut[]>(() => {
return [...publicResources.value].sort((a, b) => (a.name < b.name ? 1 : -1)); return [...resourceRepository.resources].sort((a, b) =>
a.name < b.name ? 1 : -1,
);
}); });
const sortedResources = computed<ResourceOut[]>(() => { const sortedResources = computed<ResourceOut[]>(() => {
...@@ -37,7 +44,7 @@ const sortedResources = computed<ResourceOut[]>(() => { ...@@ -37,7 +44,7 @@ const sortedResources = computed<ResourceOut[]>(() => {
const filteredSortedResources = computed<ResourceOut[]>(() => { const filteredSortedResources = computed<ResourceOut[]>(() => {
return sortedResources.value return sortedResources.value
.filter((resource) => !resource.private) .filter((resource) => !resource.private || resourceState.showPrivate)
.filter((resource) => { .filter((resource) => {
return resourceState.filterString.length > 0 return resourceState.filterString.length > 0
? resource.name.includes(resourceState.filterString) ? resource.name.includes(resourceState.filterString)
...@@ -45,7 +52,25 @@ const filteredSortedResources = computed<ResourceOut[]>(() => { ...@@ -45,7 +52,25 @@ const filteredSortedResources = computed<ResourceOut[]>(() => {
}); });
}); });
function requestResourceSync(
reason: string,
resourceVersion?: ResourceVersionOut,
) {
if (resourceVersion != undefined) {
resourceRepository
.requestSynchronization(resourceVersion, {
reason: reason,
})
.then(() => {
requestReasonModal?.hide();
syncRequestSuccessToast?.show();
});
}
}
onMounted(() => { onMounted(() => {
requestReasonModal = new Modal("#request-synchronization-modal");
syncRequestSuccessToast = new Toast("#request-sync-toast");
resourceRepository resourceRepository
.fetchPublicResources(() => { .fetchPublicResources(() => {
resourceState.loading = false; resourceState.loading = false;
...@@ -57,6 +82,20 @@ onMounted(() => { ...@@ -57,6 +82,20 @@ onMounted(() => {
</script> </script>
<template> <template>
<bootstrap-toast toast-id="request-sync-toast" color-class="success">
Requested resource synchronization
</bootstrap-toast>
<reason-modal
modal-id="request-synchronization-modal"
modal-label=""
:loading="false"
purpose="request"
@save="
(reason) => requestResourceSync(reason, resourceState.syncResourceVersion)
"
>
<template #header> Request resource synchronization</template>
</reason-modal>
<div class="row border-bottom mb-4"> <div class="row border-bottom mb-4">
<h2 class="mb-2">Available Resources</h2> <h2 class="mb-2">Available Resources</h2>
</div> </div>
...@@ -79,6 +118,17 @@ onMounted(() => { ...@@ -79,6 +118,17 @@ onMounted(() => {
/> />
</div> </div>
</div> </div>
<div class="form-check fs-5 ms-auto">
<label class="form-check-label" for="public-resources-checkbox">
Show only public resources
<input
class="form-check-input"
type="checkbox"
v-model="resourceState.showPrivate"
id="public-resources-checkbox"
/>
</label>
</div>
<font-awesome-icon <font-awesome-icon
:icon=" :icon="
resourceState.sortDesc resourceState.sortDesc
...@@ -86,18 +136,23 @@ onMounted(() => { ...@@ -86,18 +136,23 @@ onMounted(() => {
: 'fa-solid fa-arrow-up-wide-short' : 'fa-solid fa-arrow-up-wide-short'
" "
@click="resourceState.sortDesc = !resourceState.sortDesc" @click="resourceState.sortDesc = !resourceState.sortDesc"
class="fs-5 cursor-pointer ms-auto" class="fs-5 cursor-pointer ms-2"
/> />
</div> </div>
<div v-if="!resourceState.loading"> <div v-if="!resourceState.loading">
<div v-if="publicResources.length === 0" class="text-center fs-2 mt-5"> <div
v-if="resourceRepository.resources.length === 0"
class="text-center fs-2 mt-5"
>
<font-awesome-icon <font-awesome-icon
icon="fa-solid fa-x" icon="fa-solid fa-x"
class="my-5 fs-0" class="my-5 fs-0"
style="color: var(--bs-secondary)" style="color: var(--bs-secondary)"
/> />
<p> <p>
There are no public resources in the system. Please come again later. There are no resources
<span v-if="resourceState.showPrivate">public</span> in the system.
Please come again later.
</p> </p>
</div> </div>
<div <div
...@@ -125,6 +180,9 @@ onMounted(() => { ...@@ -125,6 +180,9 @@ onMounted(() => {
:resource="resource" :resource="resource"
:loading="false" :loading="false"
style="min-width: 47%; max-width: 48%" style="min-width: 47%; max-width: 48%"
@click-request-sync="
(version) => (resourceState.syncResourceVersion = version)
"
/> />
</CardTransitionGroup> </CardTransitionGroup>
</div> </div>
......
...@@ -9,17 +9,24 @@ import { useS3KeyStore } from "@/stores/s3keys"; ...@@ -9,17 +9,24 @@ import { useS3KeyStore } from "@/stores/s3keys";
import type { ResourceVersionOut, ResourceOut } from "@/client/resource"; import type { ResourceVersionOut, ResourceOut } from "@/client/resource";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import UpdateResourceModal from "@/components/resources/modals/UpdateResourceModal.vue"; import UpdateResourceModal from "@/components/resources/modals/UpdateResourceModal.vue";
import ReasonModal from "@/components/modals/ReasonModal.vue";
import BootstrapToast from "@/components/BootstrapToast.vue";
import { Modal, Toast } from "bootstrap";
const resourceRepository = useResourceStore(); const resourceRepository = useResourceStore();
const s3KeyRepository = useS3KeyStore(); const s3KeyRepository = useS3KeyStore();
let requestReasonModal: Modal | null = null;
let syncRequestSuccessToast: Toast | null = null;
const resourceState = reactive<{ const resourceState = reactive<{
loading: boolean; loading: boolean;
resourceVersionInfo?: ResourceVersionOut; resourceVersionInfo?: ResourceVersionOut;
updateResource: ResourceOut; updateResource: ResourceOut;
syncResourceVersion?: ResourceVersionOut;
}>({ }>({
loading: true, loading: true,
resourceVersionInfo: undefined, resourceVersionInfo: undefined,
syncResourceVersion: undefined,
updateResource: { updateResource: {
name: "", name: "",
description: "", description: "",
...@@ -38,8 +45,30 @@ function setResourceUpdate(resource: ResourceOut) { ...@@ -38,8 +45,30 @@ function setResourceUpdate(resource: ResourceOut) {
resourceState.updateResource = resource; resourceState.updateResource = resource;
} }
function setResourceSync(version: ResourceVersionOut) {
resourceState.syncResourceVersion = version;
}
function requestResourceSync(
reason: string,
resourceVersion?: ResourceVersionOut,
) {
if (resourceVersion != undefined) {
resourceRepository
.requestSynchronization(resourceVersion, {
reason: reason,
})
.then(() => {
requestReasonModal?.hide();
syncRequestSuccessToast?.show();
});
}
}
onMounted(() => { onMounted(() => {
let fetchedResources = false; let fetchedResources = false;
requestReasonModal = new Modal("#request-synchronization-modal");
syncRequestSuccessToast = new Toast("#request-sync-toast");
s3KeyRepository.fetchS3Keys(() => { s3KeyRepository.fetchS3Keys(() => {
if (!fetchedResources) { if (!fetchedResources) {
fetchedResources = true; fetchedResources = true;
...@@ -52,6 +81,20 @@ onMounted(() => { ...@@ -52,6 +81,20 @@ onMounted(() => {
</script> </script>
<template> <template>
<bootstrap-toast toast-id="request-sync-toast" color-class="success">
Requested resource synchronization
</bootstrap-toast>
<reason-modal
modal-id="request-synchronization-modal"
modal-label=""
:loading="false"
purpose="request"
@save="
(reason) => requestResourceSync(reason, resourceState.syncResourceVersion)
"
>
<template #header> Request resource synchronization</template>
</reason-modal>
<create-resource-modal modal-id="createResourceModal" /> <create-resource-modal modal-id="createResourceModal" />
<upload-resource-info-modal <upload-resource-info-modal
modal-id="uploadResourceInfoModal" modal-id="uploadResourceInfoModal"
...@@ -109,6 +152,7 @@ onMounted(() => { ...@@ -109,6 +152,7 @@ onMounted(() => {
extended extended
@click-info="setResourceVersionInfo" @click-info="setResourceVersionInfo"
@click-update="setResourceUpdate" @click-update="setResourceUpdate"
@click-request-sync="setResourceSync"
/> />
</CardTransitionGroup> </CardTransitionGroup>
</div> </div>
......
...@@ -110,6 +110,7 @@ onMounted(() => { ...@@ -110,6 +110,7 @@ onMounted(() => {
modal-id="review-reject-modal" modal-id="review-reject-modal"
modal-label="Resource Review Reject Modal" modal-label="Resource Review Reject Modal"
:loading="resourceState.sendingRequest" :loading="resourceState.sendingRequest"
purpose="rejection"
@save="(reason) => rejectReview(reason, resourceState.rejectResource)" @save="(reason) => rejectReview(reason, resourceState.rejectResource)"
> >
<template #header> <template #header>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment