mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-24 14:24:47 +08:00
implement new page modal, fix page rendering, improve anchor
This commit is contained in:
parent
e09caa8b8f
commit
20bd8e5300
@ -141,10 +141,10 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
|
||||
- [x] design
|
||||
- [ ] qa
|
||||
* index
|
||||
- [ ] fetch
|
||||
- [ ] layout
|
||||
- [ ] functionality
|
||||
- [ ] design
|
||||
- [x] fetch
|
||||
- [x] layout
|
||||
- [ ] functionality (promoted versions)
|
||||
- [x] design
|
||||
- [ ] qa
|
||||
* notes
|
||||
- [x] fetch
|
||||
@ -174,7 +174,7 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
|
||||
- [...all]
|
||||
- [x] fetch
|
||||
- [x] layout
|
||||
- [ ] functionality (page editing, page creation, page deleting)
|
||||
- [ ] functionality (page editing, page deleting)
|
||||
- [x] design
|
||||
- [ ] qa
|
||||
* versions (empty)
|
||||
|
@ -1,8 +1,4 @@
|
||||
.markdown {
|
||||
//padding: 10px 20px 20px 20px;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
|
||||
table tr > th {
|
||||
padding: 5px;
|
||||
}
|
||||
@ -17,7 +13,7 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
img {
|
||||
@ -52,37 +48,12 @@
|
||||
.headeranchor {
|
||||
visibility: hidden;
|
||||
text-decoration: none;
|
||||
padding-top: 68px; // So links to header's show up below the toolbar
|
||||
}
|
||||
|
||||
.headeranchor:hover {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
.headeranchor {
|
||||
margin-left: -18px;
|
||||
|
||||
.v-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
.headeranchor {
|
||||
margin-left: -12px;
|
||||
|
||||
.v-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1:hover,
|
||||
h2:hover,
|
||||
h3:hover,
|
||||
@ -99,8 +70,4 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, ref, watch } from "vue";
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
@ -11,7 +11,8 @@ const props = defineProps<{
|
||||
raw: string;
|
||||
}>();
|
||||
|
||||
watch(props, fetch, { deep: true });
|
||||
const dum = computed(() => props);
|
||||
watch(dum, fetch, { deep: true });
|
||||
|
||||
const renderedMarkdown = ref<string>("");
|
||||
const loading = ref<boolean>(true);
|
||||
@ -83,7 +84,7 @@ function setupAdmonition() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prose max-w-full rounded p-4">
|
||||
<div class="prose max-w-full rounded p-4 markdown">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="!loading" v-bind="$attrs" v-html="renderedMarkdown" />
|
||||
<div v-else>Loading...</div>
|
||||
|
@ -6,6 +6,12 @@ import { useBackendDataStore } from "~/store/backendData";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
import { HangarProjectPage } from "hangar-internal";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import InputText from "~/components/ui/InputText.vue";
|
||||
import InputSelect, { Option } from "~/components/ui/InputSelect.vue";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import { AxiosError } from "axios";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: number;
|
||||
@ -14,40 +20,81 @@ const props = defineProps<{
|
||||
|
||||
const i18n = useI18n();
|
||||
const ctx = useContext();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const backendData = useBackendDataStore();
|
||||
|
||||
const pageRoots = computed(() => flatDeep(props.pages));
|
||||
const pageRoots = computed(() => flatDeep(props.pages, ""));
|
||||
const name = ref("");
|
||||
const nameErrorMessages = ref<string[]>([]);
|
||||
const parent = ref<number | null>(null);
|
||||
const validateLoading = ref<boolean>(false);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
watch(name, () => {
|
||||
// todo validate name
|
||||
watch(name, async () => {
|
||||
if (!name.value) return;
|
||||
validateLoading.value = true;
|
||||
nameErrorMessages.value = [];
|
||||
await useInternalApi("pages/checkName", true, "get", {
|
||||
projectId: props.projectId,
|
||||
name: name.value,
|
||||
parentId: parent.value,
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
if (!err.response?.data.isHangarApiException) {
|
||||
return handleRequestError(err, ctx, i18n);
|
||||
}
|
||||
nameErrorMessages.value.push(i18n.t(err.response.data.message));
|
||||
})
|
||||
.finally(() => {
|
||||
validateLoading.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
// TODO these should be sorted into somewhat of an order
|
||||
function flatDeep(pages: HangarProjectPage[]): HangarProjectPage[] {
|
||||
let ps: HangarProjectPage[] = [];
|
||||
function flatDeep(pages: HangarProjectPage[], prefix: string): Option[] {
|
||||
let ps: Option[] = [];
|
||||
for (const page of pages) {
|
||||
if (page.children.length > 0) {
|
||||
ps = [...ps, ...flatDeep(page.children)];
|
||||
ps = [...ps, ...flatDeep(page.children, prefix + "-")];
|
||||
}
|
||||
ps.push(page);
|
||||
ps.unshift({ value: page.id, text: prefix + page.name });
|
||||
}
|
||||
return ps;
|
||||
}
|
||||
|
||||
function createPage() {
|
||||
// todo create page
|
||||
async function createPage() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const slug = await useInternalApi<string>(`pages/create/${props.projectId}`, true, "post", {
|
||||
name: name.value,
|
||||
parentId: parent.value,
|
||||
});
|
||||
await router.push(`/${route.params.user}/${route.params.project}/pages/${slug}`);
|
||||
} catch (e) {
|
||||
handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="i18n.t('page.new.title')">
|
||||
<template #default="{ on }">
|
||||
<!-- todo new page modal -->
|
||||
|
||||
<Button class="mt-2" v-on="on">{{ i18n.t("general.close") }}</Button>
|
||||
<Button class="mt-2 ml-2" @click="createPage">{{ i18n.t("general.create") }}</Button>
|
||||
<div class="flex flex-col">
|
||||
<InputText
|
||||
v-model.trim="name"
|
||||
:label="i18n.t('page.new.name')"
|
||||
:error-messages="nameErrorMessages"
|
||||
counter
|
||||
:maxlength="backendData.validations.project.pageName.max"
|
||||
:minlength="backendData.validations.project.pageName.min"
|
||||
/>
|
||||
<InputSelect v-model="parent" :values="pageRoots" :label="i18n.t('page.new.parent')" />
|
||||
</div>
|
||||
<div>
|
||||
<Button class="mt-2" v-on="on">{{ i18n.t("general.close") }}</Button>
|
||||
<Button class="mt-2 ml-2" :disabled="validateLoading || loading" @click="createPage">{{ i18n.t("general.create") }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #activator="{ on }">
|
||||
<Button v-bind="$attrs" class="mr-1 h-[32px]" size="small" v-on="on">
|
||||
|
@ -29,7 +29,10 @@ const route = useRoute();
|
||||
|
||||
<TreeView :items="project.pages" item-key="slug" :open="open">
|
||||
<template #item="{ item }">
|
||||
<Link v-if="item.home" :to="`/${route.params.user}/${route.params.project}`" exact><IconMdiHome /> {{ item.name }}</Link>
|
||||
<Link v-if="item.home" :to="`/${route.params.user}/${route.params.project}`" exact class="inline-flex items-center">
|
||||
<IconMdiHome class="mr-1" />
|
||||
{{ item.name }}
|
||||
</Link>
|
||||
<Link v-else :to="`/${route.params.user}/${route.params.project}/pages/${item.slug}`" exact> {{ item.name }}</Link>
|
||||
</template>
|
||||
</TreeView>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { computed } from "vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: object | string | boolean | number): void;
|
||||
(e: "update:modelValue", value: object | string | boolean | number | null): void;
|
||||
}>();
|
||||
const internalVal = computed({
|
||||
get: () => props.modelValue,
|
||||
@ -15,15 +15,19 @@ export interface Option {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: object | string | boolean | number;
|
||||
modelValue: object | string | boolean | number | null;
|
||||
values: Option[];
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<!-- todo make fancy -->
|
||||
<template>
|
||||
<select v-model="internalVal" :disabled="disabled">
|
||||
<option v-for="val in values" :key="val.value" :value="val.value">{{ val.text }}</option>
|
||||
</select>
|
||||
<label>
|
||||
<template v-if="label">{{ label }}</template>
|
||||
<select v-model="internalVal" :disabled="disabled">
|
||||
<option v-for="val in values" :key="val.value" :value="val.value">{{ val.text }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</template>
|
||||
|
@ -15,7 +15,9 @@ export async function useProjectPage(
|
||||
i18n: Composer<unknown, unknown, unknown, VueMessageType>,
|
||||
project: HangarProject
|
||||
) {
|
||||
const page = await usePage(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, ctx, i18n));
|
||||
const page = await usePage(route.params.user as string, route.params.project as string, route.params.all as string).catch((e) =>
|
||||
handleRequestError(e, ctx, i18n)
|
||||
);
|
||||
if (!page) {
|
||||
await useRouter().push(useErrorRedirect(useRoute(), 404, "Not found"));
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ useHead(useSeo(props.project.name, props.project.description, route, projectIcon
|
||||
<ProjectInfo :project="project"></ProjectInfo>
|
||||
<Card>
|
||||
<template #header>{{ i18n.t("project.promotedVersions") }}</template>
|
||||
<!-- todo promoted versions go here -->
|
||||
<template #default>Promoted versions go here</template>
|
||||
</Card>
|
||||
<ProjectPageList :project="project" :open="open" />
|
||||
|
@ -91,6 +91,7 @@ async function updateInvite(invite: Invite, status: "accept" | "decline" | "unac
|
||||
delete invites.value[invite.type][invites.value[invite.type].indexOf(invite)];
|
||||
}
|
||||
notificationStore.success(i18n.t(`notifications.invite.msgs.${status}`, [invite.name]));
|
||||
await useRouter().go(0);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -4,6 +4,7 @@ import typography from "windicss/plugin/typography";
|
||||
|
||||
export default defineConfig({
|
||||
darkMode: "class",
|
||||
safelist: "order-last",
|
||||
attributify: true,
|
||||
plugins: [typography()],
|
||||
theme: {
|
||||
|
@ -47,8 +47,8 @@ public class MarkdownService {
|
||||
|
||||
options = new MutableDataSet()
|
||||
.set(HtmlRenderer.ESCAPE_HTML, true)
|
||||
.set(AnchorLinkExtension.ANCHORLINKS_TEXT_PREFIX, "<svg preserveAspectRatio=\"xMidYMid meet\" viewBox=\"0 0 24 24\" width=\"1.2em\" height=\"1.2em\"><path fill=\"currentColor\" d=\"M10.59 13.41c.41.39.41 1.03 0 1.42c-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0a5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24a2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24m2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0a5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24a2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24a.973.973 0 0 1 0-1.42Z\"></path></svg>")
|
||||
.set(AnchorLinkExtension.ANCHORLINKS_ANCHOR_CLASS, "headeranchor")
|
||||
.set(AnchorLinkExtension.ANCHORLINKS_TEXT_SUFFIX, "<svg class=\"ml-2 text-xl\" preserveAspectRatio=\"xMidYMid meet\" viewBox=\"0 0 24 24\" width=\"1.2em\" height=\"1.2em\"><path fill=\"currentColor\" d=\"M10.59 13.41c.41.39.41 1.03 0 1.42c-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0a5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24a2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24m2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0a5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24a2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24a.973.973 0 0 1 0-1.42Z\"></path></svg>")
|
||||
.set(AnchorLinkExtension.ANCHORLINKS_ANCHOR_CLASS, "headeranchor inline-flex items-center order-last")
|
||||
.set(AnchorLinkExtension.ANCHORLINKS_WRAP_TEXT, false)
|
||||
// GFM table compatibility
|
||||
.set(TablesExtension.COLUMN_SPANS, false)
|
||||
|
Loading…
Reference in New Issue
Block a user