mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-30 14:30:08 +08:00
implement project channels page
This commit is contained in:
parent
ae4db0f972
commit
40909d1482
@ -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
|
||||
|
@ -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;
|
||||
}>();
|
||||
|
||||
|
@ -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')"
|
||||
>
|
||||
|
30
frontend-new/src/components/modals/ChannelModal.vue
Normal file
30
frontend-new/src/components/modals/ChannelModal.vue
Normal 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>
|
@ -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);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
8
frontend-new/src/types/generated/icons.d.ts
vendored
8
frontend-new/src/types/generated/icons.d.ts
vendored
@ -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"];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user