implement project channels page

This commit is contained in:
MiniDigger 2022-03-24 20:56:40 +01:00
parent ae4db0f972
commit 40909d1482
8 changed files with 177 additions and 13 deletions

View File

@ -128,10 +128,10 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
- [ ] design
- [ ] qa
* channels
- [ ] fetch
- [ ] layout
- [ ] functionality
- [ ] design
- [x] fetch
- [x] layout
- [ ] functionality (modal is missing)
- [x] design
- [ ] qa
* discuss
- [ ] fetch
@ -155,7 +155,7 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
- [x] fetch
- [x] layout
- [x] functionality
- [x] design
- [x] design
- [ ] qa
* settings
- [ ] fetch

View File

@ -3,10 +3,10 @@ import { Tag } from "hangar-api";
import { computed } from "vue";
const props = defineProps<{
name: string;
data: string;
color: { foreground?: string; background: string };
tag: Tag;
name?: string;
data?: string;
color?: { foreground?: string; background: string };
tag?: Tag;
shortForm?: boolean;
}>();

View File

@ -5,17 +5,20 @@ const emit = defineEmits<{
const props = withDefaults(
defineProps<{
small?: boolean;
disabled?: boolean;
}>(),
{
small: false,
disabled: false,
}
);
</script>
<template>
<button
:class="'rounded-md bg-gradient-to-r from-[#004ee9] to-[#367aff] text-white h-min ' + (small ? 'p-1' : 'p-3')"
hover="text-shadow-xl from-[#004ee9] to-[#6699ff]"
:class="'rounded-md text-white h-min ' + (small ? 'p-1' : 'p-3') + (disabled ? ' bg-gray-500' : ' bg-gradient-to-r from-[#004ee9] to-[#367aff]')"
:hover="disabled ? '' : 'text-shadow-xl from-[#004ee9] to-[#6699ff]'"
:disabled="disabled"
v-bind="$attrs"
@click="$emit('click')"
>

View File

@ -0,0 +1,30 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import Button from "~/components/design/Button.vue";
import Modal from "~/components/modals/Modal.vue";
import { ProjectChannel } from "hangar-internal";
const props = defineProps<{
projectId: number;
edit?: boolean;
channel?: ProjectChannel;
}>();
const emit = defineEmits<{
(e: "create", channel: ProjectChannel): void;
}>();
const i18n = useI18n();
</script>
<template>
<Modal :title="edit ? i18n.t('channel.modal.titleEdit') : i18n.t('channel.modal.titleNew')">
<template #default="{ on }">
<!-- todo implement channel modal -->
<Button class="mt-2" v-on="on">{{ i18n.t("general.close") }}</Button>
</template>
<template #activator="{ on }">
<slot name="activator" :on="on"></slot>
</template>
</Modal>
</template>

View File

@ -1,7 +1,7 @@
import { useApi, useInternalApi } from "~/composables/useApi";
import { PaginatedResult, Project, User } from "hangar-api";
import { useInitialState } from "~/composables/useInitialState";
import { Flag, HangarNotification, HangarProject, HealthReport, Invites, LoggedAction, Note, ReviewQueueEntry } from "hangar-internal";
import { Flag, HangarNotification, HangarProject, HealthReport, Invites, LoggedAction, Note, ProjectChannel, ReviewQueueEntry } from "hangar-internal";
export async function useProjects(pagination = { limit: 25, offset: 0 }, blocking = true) {
return useInitialState("useProjects", () => useApi<PaginatedResult<Project>>("projects", false, "get", pagination), blocking);
@ -51,6 +51,10 @@ export async function useProjectNotes(projectId: number, blocking = true) {
return useInitialState("useProjectNotes", () => useInternalApi<Note[]>("projects/notes/" + projectId, false), blocking);
}
export async function useProjectChannels(user: string, project: string, blocking = true) {
return useInitialState("useProjectChannels", () => useInternalApi<ProjectChannel[]>(`channels/${user}/${project}`, false), blocking);
}
export async function useHealthReport(blocking = true) {
return useInitialState("useHealthReport", () => useInternalApi<HealthReport>("admin/health", false), blocking);
}

View File

@ -1,5 +1,122 @@
<script lang="ts" setup>
import Card from "~/components/design/Card.vue";
import Link from "~/components/design/Link.vue";
import { User } from "hangar-api";
import { useI18n } from "vue-i18n";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import Alert from "~/components/design/Alert.vue";
import { useContext } from "vite-ssr/vue";
import { useProjectChannels } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { HangarProject, ProjectChannel } from "hangar-internal";
import { useInternalApi } from "~/composables/useApi";
import Table from "~/components/design/Table.vue";
import Tag from "~/components/Tag.vue";
import Button from "~/components/design/Button.vue";
import { useBackendDataStore } from "~/store/backendData";
import ChannelModal from "~/components/modals/ChannelModal.vue";
const props = defineProps<{
user: User;
project: HangarProject;
}>();
const i18n = useI18n();
const ctx = useContext();
const channels = await useProjectChannels(props.project.namespace.owner, props.project.namespace.slug).catch((e) => handleRequestError(e, ctx, i18n));
const validations = useBackendDataStore().validations;
async function refreshChannels() {
const newChannels = await useInternalApi<ProjectChannel[]>(`channels/${props.project.namespace.owner}/${props.project.namespace.slug}`, false).catch((e) =>
handleRequestError(e, ctx, i18n)
);
if (channels && newChannels) {
channels.value = newChannels;
}
}
async function deleteChannel(channel: ProjectChannel) {
await useInternalApi(`channels/${props.project.id}/delete/${channel.id}`, true, "post");
await refreshChannels();
}
async function addChannel(channel: ProjectChannel) {
if (!channel.id) return;
await useInternalApi(`channels/${props.project.id}/create`, true, "post", {
name: channel.name,
color: channel.color,
nonReviewed: channel.nonReviewed,
}).catch((e) => handleRequestError(e, ctx, i18n));
await refreshChannels();
}
async function editChannel(channel: ProjectChannel) {
if (!channel.id) return;
await useInternalApi(`channels/${props.project.id}/edit`, true, "post", {
id: channel.id,
name: channel.name,
color: channel.color,
nonReviewed: channel.nonReviewed,
}).catch((e) => handleRequestError(e, ctx, i18n));
await refreshChannels();
}
</script>
<template>
<h1>channels</h1>
<Card>
<template #header>{{ i18n.t("channel.manage.title") }}</template>
<p class="mb-2">{{ i18n.t("channel.manage.subtitle") }}</p>
<Table class="w-full">
<thead>
<tr>
<th><IconMdiTag />{{ i18n.t("channel.manage.channelName") }}</th>
<th><IconMdiFormatListNumbered />{{ i18n.t("channel.manage.versionCount") }}</th>
<th><IconMdiFileFind />{{ i18n.t("channel.manage.reviewed") }}</th>
<th><IconMdiPencil />{{ i18n.t("channel.manage.edit") }}</th>
<th v-if="channels.length !== 1"><IconMdiDelete />{{ i18n.t("channel.manage.trash") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="channel in channels" :key="channel.name">
<td><Tag :name="channel.name" :color="{ background: channel.color }" /></td>
<td>{{ channel.versionCount }}</td>
<td>
<IconMdiCheckboxBlankCircleOutline v-if="channel.nonReviewed" />
<IconMdiCheckCircle v-else />
</td>
<td>
<ChannelModal :project-id="props.project.id" edit :channel="channel" @create="editChannel">
<template #activator="{ on, attrs }">
<Button v-bind="attrs" :disabled="!channel.editable" v-on="on">
{{ i18n.t("channel.manage.editButton") }}
</Button>
</template>
</ChannelModal>
</td>
<td v-if="channels.length !== 1">
<Button v-if="channel.versionCount === 0" :disabled="!channel.editable" @click="deleteChannel(channel)">
{{ i18n.t("channel.manage.deleteButton") }}
</Button>
</td>
</tr>
</tbody>
</Table>
<ChannelModal :project-id="props.project.id" @create="addChannel">
<template #activator="{ on, attrs }">
<Button
v-if="channels.length < validations.project.maxChannelCount"
:disabled="channels.length >= validations.project.maxChannelCount"
class="mt-2"
v-bind="attrs"
v-on="on"
>
<IconMdiPlus />
{{ i18n.t("channel.manage.add") }}
</Button>
</template>
</ChannelModal>
</Card>
</template>
<route lang="yaml">

View File

@ -77,6 +77,8 @@ export const useBackendDataStore = defineStore("backendData", () => {
await fetchIfNeeded(async () => await useInternalApi<AnnouncementObject[]>("data/announcements", false), announcements);
await fetchIfNeeded(async () => await useInternalApi<IVisibility[]>("data/visibilities", false), visibilities);
await fetchIfNeeded(async () => await useInternalApi("data/validations", false), validations);
} catch (e) {
console.error("ERROR FETCHING BACKEND DATA");
console.error(e);

View File

@ -4,15 +4,23 @@
declare module "vue" {
export interface GlobalComponents {
IconMdiCheckboxBlankCircleOutline: typeof import("~icons/mdi/checkbox-blank-circle-outline")["default"];
IconMdiCheckCircle: typeof import("~icons/mdi/check-circle")["default"];
IconMdiClipboardOutline: typeof import("~icons/mdi/clipboard-outline")["default"];
IconMdiCloseCircle: typeof import("~icons/mdi/close-circle")["default"];
IconMdiDelete: typeof import("~icons/mdi/delete")["default"];
IconMdiEye: typeof import("~icons/mdi/eye")["default"];
IconMdiFileFind: typeof import("~icons/mdi/file-find")["default"];
IconMdiFormatListNumbered: typeof import("~icons/mdi/format-list-numbered")["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"];
IconMdiPencil: typeof import("~icons/mdi/pencil")["default"];
IconMdiPlay: typeof import("~icons/mdi/play")["default"];
IconMdiPlus: typeof import("~icons/mdi/plus")["default"];
IconMdiSortVariant: typeof import("~icons/mdi/sort-variant")["default"];
IconMdiTag: typeof import("~icons/mdi/tag")["default"];
IconMdiWeatherNight: typeof import("~icons/mdi/weather-night")["default"];
IconMdiWhiteBalanceSunny: typeof import("~icons/mdi/white-balance-sunny")["default"];
}