mirror of
https://github.com/HangarMC/Hangar.git
synced 2024-12-21 06:51:19 +08:00
implement admin project approval page, visibility change modal
This commit is contained in:
parent
8aafe48269
commit
b1cb09299e
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
40
frontend-new/src/components/projects/AdminProjectList.vue
Normal file
40
frontend-new/src/components/projects/AdminProjectList.vue
Normal 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>
|
@ -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"
|
||||
|
40
frontend-new/src/components/ui/InputRadio.vue
Normal file
40
frontend-new/src/components/ui/InputRadio.vue
Normal 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>
|
23
frontend-new/src/components/ui/InputTextarea.vue
Normal file
23
frontend-new/src/components/ui/InputTextarea.vue
Normal 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>
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
2
frontend-new/src/types/generated/icons.d.ts
vendored
2
frontend-new/src/types/generated/icons.d.ts
vendored
@ -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"];
|
||||
|
Loading…
Reference in New Issue
Block a user