implement admin project approval page, visibility change modal

This commit is contained in:
MiniDigger 2022-03-22 20:38:55 +01:00
parent 8aafe48269
commit b1cb09299e
11 changed files with 209 additions and 12 deletions

View File

@ -217,7 +217,7 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
- flags
- [x] fetch
- [x] layout
- [x] functionality (cors error)
- [x] functionality
- [x] design
- [ ] qa
- health
@ -253,10 +253,10 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
- [ ] qa
- approval (empty)
- projects
- [ ] fetch
- [ ] layout
- [ ] functionality
- [ ] design
- [x] fetch
- [x] layout
- [x] functionality
- [x] design
- [ ] qa
- versions
- [ ] fetch

View File

@ -7,10 +7,10 @@ const classes = "color-primary font-bold hover:(underline)";
</script>
<template>
<router-link v-if="to" :to="to" :class="classes">
<router-link v-if="to" :to="to" :class="classes" v-bind="$attrs">
<slot></slot>
</router-link>
<a v-else :href="href" :class="classes">
<a v-else :href="href" :class="classes" v-bind="$attrs">
<slot></slot>
</a>
</template>

View File

@ -0,0 +1,60 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import Button from "~/components/design/Button.vue";
import Modal from "~/components/modals/Modal.vue";
import { Visibility } from "~/types/enums";
import InputRadio from "~/components/ui/InputRadio.vue";
import { useBackendDataStore } from "~/store/backendData";
import { computed, ref } from "vue";
import InputTextarea from "~/components/ui/InputTextarea.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
const props = defineProps<{
type: "project" | "version";
propVisibility: Visibility;
postUrl: string;
}>();
const i18n = useI18n();
const ctx = useContext();
const backendData = useBackendDataStore();
const visibility = ref<Visibility>();
const reason = ref<string>("");
const showTextarea = computed(() => currentIVis.value?.showModal && props.propVisibility !== visibility.value);
const currentIVis = computed(() => backendData.visibilities.find((v) => v.name === visibility.value));
async function submit(closeModal: () => void): Promise<void> {
await useInternalApi(props.postUrl, true, "post", {
visibility: visibility.value,
comment: currentIVis.value?.showModal ? reason.value : null,
}).catch((e) => handleRequestError(e, ctx, i18n));
reason.value = "";
// TODO success notification
// this.$util.success(i18n.t("visibility.modal.success", [this.type, i18n.t(currentIVis.value?.title)]));
// this.$nuxt.refresh();
closeModal();
}
</script>
<template>
<Modal :title="i18n.t('visibility.modal.title', [type])">
<template #default="{ on }">
<InputRadio v-for="vis in backendData.visibilities" :key="vis.name" v-model="visibility" :value="vis.name" :label="i18n.t(vis.title)" class="block" />
<InputTextarea v-if="showTextarea" v-model.trim="reason" rows="2" :label="i18n.t('visibility.modal.reason')" />
<Button class="mt-2" v-on="on">{{ i18n.t("general.close") }}</Button>
<Button class="mt-2 ml-2" @click="submit(on.click)">{{ i18n.t("general.submit") }}</Button>
</template>
<template #activator="{ on }">
<Button v-bind="$attrs" color="warning" class="mr-1" v-on="on">
<IconMdiEye />
{{ i18n.t("visibility.modal.activatorBtn") }}
</Button>
</template>
</Modal>
</template>

View File

@ -0,0 +1,40 @@
<script lang="ts" setup>
import { ProjectApproval } from "hangar-internal";
import Alert from "~/components/design/Alert.vue";
import { useI18n } from "vue-i18n";
import Markdown from "~/components/Markdown.vue";
import Link from "~/components/design/Link.vue";
import VisibilityChangerModal from "~/components/modals/VisibilityChangerModal.vue";
const i18n = useI18n();
const props = defineProps<{
projects: ProjectApproval[];
}>();
</script>
<template>
<ul v-if="projects.length">
<template v-for="project in projects" :key="project.projectId">
<hr class="mb-3" />
<li>
<div class="flex">
<div class="flex-grow">
{{ i18n.t("projectApproval.description", [project.changeRequester, `${project.namespace.owner}/${project.namespace.slug}`]) }}
<Link :to="`/${project.namespace.owner}/${project.namespace.slug}`" target="_blank">
<IconMdiOpenInNew />
</Link>
</div>
<div class="flex-shrink">
<VisibilityChangerModal :prop-visibility="project.visibility" small-btn type="project" :post-url="`projects/visibility/${project.projectId}`" />
</div>
<div class="basis-full">
<Markdown :raw="project.comment" class="mb-3" />
</div>
</div>
</li>
</template>
</ul>
<Alert v-else>
{{ i18n.t("projectApproval.noProjects") }}
</Alert>
</template>

View File

@ -4,7 +4,7 @@ import { computed } from "vue";
const emit = defineEmits<{
(e: "update:modelValue", value: boolean | boolean[]): void;
}>();
const value = computed({
const internalVal = computed({
get: () => props.modelValue,
set: (v) => emit("update:modelValue", v),
});
@ -17,7 +17,7 @@ const props = defineProps<{
<template>
<label class="group relative cursor-pointer pl-30px customCheckboxContainer w-max">
<template v-if="props.label">{{ props.label }}</template>
<input v-model="value" type="checkbox" class="hidden" v-bind="$attrs" />
<input v-model="internalVal" type="checkbox" class="hidden" v-bind="$attrs" />
<span
class="absolute top-5px left-0 h-15px w-15px rounded bg-gray-300"
after="absolute hidden content-DEFAULT top-1px left-5px border-solid w-6px h-12px border-r-3px border-b-3px border-white"

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { computed } from "vue";
const emit = defineEmits<{
(e: "update:modelValue", value?: string): void;
}>();
const internalVal = computed({
get: () => props.modelValue,
set: (v) => emit("update:modelValue", v),
});
const props = defineProps<{
modelValue?: string;
label?: string;
}>();
</script>
<template>
<label class="group relative cursor-pointer pl-30px customCheckboxContainer w-max">
<template v-if="props.label">{{ props.label }}</template>
<input v-model="internalVal" type="radio" class="hidden" v-bind="$attrs" />
<span
class="absolute top-5px left-0 h-15px w-15px rounded-full bg-gray-300"
after="absolute hidden content-DEFAULT top-1px left-5px border-solid w-6px h-12px border-r-3px border-b-3px border-white rounded-full"
group-hover="bg-gray-400"
/>
</label>
</template>
<style>
/*This is needed, because you cannot have more than one parent group in tailwind/windi*/
.customCheckboxContainer input:checked ~ span {
background-color: #004ee9 !important;
}
/*The tailwind/windi utility class rotate-45 is BROKEN*/
.customCheckboxContainer input:checked ~ span:after {
display: block;
transform: rotate(45deg);
}
</style>

View File

@ -0,0 +1,23 @@
<script lang="ts" setup>
import { computed } from "vue";
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
}>();
const value = computed({
get: () => props.modelValue,
set: (v) => emit("update:modelValue", v),
});
const props = defineProps<{
modelValue: string;
label?: string;
}>();
</script>
<template>
<!-- todo make fancy -->
<label class="block">
<span v-if="label" class="block">{{ label }}</span>
<textarea v-model="value" class="ml-2" v-bind="$attrs" />
</label>
</template>

View File

@ -1,5 +1,32 @@
<script lang="ts" setup>
import { ProjectApproval } from "hangar-internal";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import Card from "~/components/design/Card.vue";
import AdminProjectList from "~/components/projects/AdminProjectList.vue";
const ctx = useContext();
const i18n = useI18n();
const data = await useInternalApi<{ needsApproval: ProjectApproval[]; waitingProjects: ProjectApproval[] }>("admin/approval/projects").catch((e) =>
handleRequestError(e, ctx, i18n)
);
</script>
<template>
<h1>project approvals</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<template #header>{{ i18n.t("projectApproval.awaitingChanges") }}</template>
<AdminProjectList :projects="data.waitingProjects" />
</Card>
<Card>
<template #header>{{ i18n.t("projectApproval.needsApproval") }}</template>
<AdminProjectList :projects="data.needsApproval" />
</Card>
</div>
</template>
<route lang="yaml">

View File

@ -12,6 +12,7 @@ import PageTitle from "~/components/design/PageTitle.vue";
import Card from "~/components/design/Card.vue";
import Link from "~/components/design/Link.vue";
import Button from "~/components/design/Button.vue";
import VisibilityChangerModal from "~/components/modals/VisibilityChangerModal.vue";
const ctx = useContext();
const i18n = useI18n();
@ -61,7 +62,7 @@ function resolve(flag: Flag) {
</div>
<Link fix-href="$util.forumUrl(flag.reportedByName)">{{ i18n.t("flagReview.msgUser") }}</Link>
<Link fix-href="$util.forumUrl(flag.projectNamespace.owner)">{{ i18n.t("flagReview.msgProjectOwner") }}</Link>
<!-- todo modal for visibility change -->
<VisibilityChangerModal :prop-visibility="flag.projectVisibility" type="project" :post-url="`projects/visibility/${flag.projectId}`" />
<Button :disabled="loading[flag.id]" @click="resolve(flag)">{{ i18n.t("flagReview.markResolved") }}</Button>
</Card>
</template>

View File

@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { IPlatform, IProjectCategory, IPrompt } from "hangar-internal";
import { IPlatform, IProjectCategory, IPrompt, IVisibility } from "hangar-internal";
import { NamedPermission, Platform, ProjectCategory, Prompt } from "~/types/enums";
import { Announcement as AnnouncementObject, Announcement, IPermission } from "hangar-api";
@ -38,6 +38,7 @@ export const useBackendDataStore = defineStore("backendData", () => {
const validations = ref<Validations | null>(null);
const prompts = ref<Map<Prompt, IPrompt> | null>(null);
const announcements = ref<Announcement[]>([]);
const visibilities = ref<IVisibility[]>([]);
const licenses = ref<string[]>([]);
async function initBackendData() {
@ -74,6 +75,8 @@ export const useBackendDataStore = defineStore("backendData", () => {
await fetchIfNeeded(async () => await useInternalApi<string[]>("data/licenses", false), licenses);
await fetchIfNeeded(async () => await useInternalApi<AnnouncementObject[]>("data/announcements", false), announcements);
await fetchIfNeeded(async () => await useInternalApi<IVisibility[]>("data/visibilities", false), visibilities);
} catch (e) {
console.error("ERROR FETCHING BACKEND DATA");
console.error(e);
@ -91,6 +94,7 @@ export const useBackendDataStore = defineStore("backendData", () => {
prompts,
licenses,
announcements,
visibilities,
initBackendData,
visibleCategories,
visiblePlatforms,

View File

@ -6,7 +6,9 @@ declare module "vue" {
export interface GlobalComponents {
IconMdiClipboardOutline: typeof import("~icons/mdi/clipboard-outline")["default"];
IconMdiCloseCircle: typeof import("~icons/mdi/close-circle")["default"];
IconMdiEye: typeof import("~icons/mdi/eye")["default"];
IconMdiKeyOutline: typeof import("~icons/mdi/key-outline")["default"];
IconMdiListStatus: typeof import("~icons/mdi/list-status")["default"];
IconMdiMenu: typeof import("~icons/mdi/menu")["default"];
IconMdiOpenInNew: typeof import("~icons/mdi/open-in-new")["default"];
IconMdiSortVariant: typeof import("~icons/mdi/sort-variant")["default"];