implement new page modal, fix page rendering, improve anchor

This commit is contained in:
MiniDigger | Martin 2022-04-01 23:15:12 +02:00
parent e09caa8b8f
commit 20bd8e5300
11 changed files with 92 additions and 65 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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"));
}

View File

@ -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" />

View File

@ -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>

View File

@ -4,6 +4,7 @@ import typography from "windicss/plugin/typography";
export default defineConfig({
darkMode: "class",
safelist: "order-last",
attributify: true,
plugins: [typography()],
theme: {

View File

@ -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)