feat: ability to pin custom pages to the help menu
This commit is contained in:
parent
3272b908c6
commit
e7e6ae69e0
11 changed files with 138 additions and 11 deletions
|
@ -4,6 +4,11 @@ Breaking changes are indicated by the :warning: icon.
|
||||||
|
|
||||||
## v1.0.5 (unreleased)
|
## v1.0.5 (unreleased)
|
||||||
|
|
||||||
|
### dev21
|
||||||
|
|
||||||
|
- `admin/update-meta` can now take `moreUrls` parameter, and response of `admin/meta` now includes `moreUrls`
|
||||||
|
- These URLs are used for the help menu ([related merge request](https://git.joinfirefish.org/firefish/firefish/-/merge_requests/10640))
|
||||||
|
|
||||||
### dev18
|
### dev18
|
||||||
|
|
||||||
- :warning: response of `meta` no longer includes the following:
|
- :warning: response of `meta` no longer includes the following:
|
||||||
|
|
|
@ -2161,3 +2161,5 @@ _iconSets:
|
||||||
regular: "Regular"
|
regular: "Regular"
|
||||||
fill: "Filled"
|
fill: "Filled"
|
||||||
duotone: "Duotone"
|
duotone: "Duotone"
|
||||||
|
moreUrls: "Pinned pages"
|
||||||
|
moreUrlsDescription: "Enter the pages you want to pin to the help menu in the lower left corner using this notation:\n\"Display name\": https://example.com/"
|
||||||
|
|
|
@ -2003,3 +2003,5 @@ _iconSets:
|
||||||
regular: "標準"
|
regular: "標準"
|
||||||
fill: "塗りつぶし"
|
fill: "塗りつぶし"
|
||||||
duotone: "2色"
|
duotone: "2色"
|
||||||
|
moreUrls: "固定するページ"
|
||||||
|
moreUrlsDescription: "左下のヘルプメニューに固定したいページを以下の形式で、改行区切りで入力してください:\n\"表示名\": https://example.com/"
|
||||||
|
|
13
packages/backend/migration/1699305365258-more-urls.js
Normal file
13
packages/backend/migration/1699305365258-more-urls.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export class MoreUrls1699305365258 {
|
||||||
|
name = "MoreUrls1699305365258";
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ADD "moreUrls" jsonb NOT NULL DEFAULT '[]'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "moreUrls"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,8 @@ pub struct Model {
|
||||||
pub pinned_users: StringVec,
|
pub pinned_users: StringVec,
|
||||||
#[sea_orm(column_name = "ToSUrl")]
|
#[sea_orm(column_name = "ToSUrl")]
|
||||||
pub to_s_url: Option<String>,
|
pub to_s_url: Option<String>,
|
||||||
|
#[sea_orm(column_name = "moreUrls", column_type = "JsonBinary")]
|
||||||
|
pub more_urls: Json,
|
||||||
#[sea_orm(column_name = "repositoryUrl")]
|
#[sea_orm(column_name = "repositoryUrl")]
|
||||||
pub repository_url: String,
|
pub repository_url: String,
|
||||||
#[sea_orm(column_name = "feedbackUrl")]
|
#[sea_orm(column_name = "feedbackUrl")]
|
||||||
|
|
|
@ -383,6 +383,12 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public ToSUrl: string | null;
|
public ToSUrl: string | null;
|
||||||
|
|
||||||
|
@Column("jsonb", {
|
||||||
|
default: [],
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
public moreUrls: [string, string][];
|
||||||
|
|
||||||
@Column("varchar", {
|
@Column("varchar", {
|
||||||
length: 512,
|
length: 512,
|
||||||
default: "https://git.joinfirefish.org/firefish/firefish",
|
default: "https://git.joinfirefish.org/firefish/firefish",
|
||||||
|
|
|
@ -472,6 +472,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
description: instance.description,
|
description: instance.description,
|
||||||
langs: instance.langs,
|
langs: instance.langs,
|
||||||
tosUrl: instance.ToSUrl,
|
tosUrl: instance.ToSUrl,
|
||||||
|
moreUrls: instance.moreUrls,
|
||||||
repositoryUrl: instance.repositoryUrl,
|
repositoryUrl: instance.repositoryUrl,
|
||||||
feedbackUrl: instance.feedbackUrl,
|
feedbackUrl: instance.feedbackUrl,
|
||||||
disableRegistration: instance.disableRegistration,
|
disableRegistration: instance.disableRegistration,
|
||||||
|
|
|
@ -143,6 +143,17 @@ export const paramDef = {
|
||||||
swPublicKey: { type: "string", nullable: true },
|
swPublicKey: { type: "string", nullable: true },
|
||||||
swPrivateKey: { type: "string", nullable: true },
|
swPrivateKey: { type: "string", nullable: true },
|
||||||
tosUrl: { type: "string", nullable: true },
|
tosUrl: { type: "string", nullable: true },
|
||||||
|
moreUrls: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
url: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
repositoryUrl: { type: "string" },
|
repositoryUrl: { type: "string" },
|
||||||
feedbackUrl: { type: "string" },
|
feedbackUrl: { type: "string" },
|
||||||
useObjectStorage: { type: "boolean" },
|
useObjectStorage: { type: "boolean" },
|
||||||
|
@ -174,6 +185,18 @@ export const paramDef = {
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
function isValidHttpUrl(src: string) {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(src);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
|
}
|
||||||
|
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const set = {} as Partial<Meta>;
|
const set = {} as Partial<Meta>;
|
||||||
|
|
||||||
|
@ -434,6 +457,14 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.ToSUrl = ps.tosUrl;
|
set.ToSUrl = ps.tosUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.moreUrls !== undefined) {
|
||||||
|
const areUrlsVaild = ps.moreUrls.every(
|
||||||
|
(obj: { name: string; url: string }) => isValidHttpUrl(String(obj.url)),
|
||||||
|
);
|
||||||
|
if (!areUrlsVaild) throw new Error("invalid URL");
|
||||||
|
set.moreUrls = ps.moreUrls;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.repositoryUrl !== undefined) {
|
if (ps.repositoryUrl !== undefined) {
|
||||||
set.repositoryUrl = ps.repositoryUrl;
|
set.repositoryUrl = ps.repositoryUrl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,11 @@ export const meta = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
},
|
},
|
||||||
|
moreUrls: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
repositoryUrl: {
|
repositoryUrl: {
|
||||||
type: "string",
|
type: "string",
|
||||||
optional: false,
|
optional: false,
|
||||||
|
@ -416,6 +421,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
description: instance.description,
|
description: instance.description,
|
||||||
langs: instance.langs,
|
langs: instance.langs,
|
||||||
tosUrl: instance.ToSUrl,
|
tosUrl: instance.ToSUrl,
|
||||||
|
moreUrls: instance.moreUrls,
|
||||||
repositoryUrl: instance.repositoryUrl,
|
repositoryUrl: instance.repositoryUrl,
|
||||||
feedbackUrl: instance.feedbackUrl,
|
feedbackUrl: instance.feedbackUrl,
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,18 @@
|
||||||
|
|
||||||
<FormInput v-model="tosUrl" class="_formBlock">
|
<FormInput v-model="tosUrl" class="_formBlock">
|
||||||
<template #prefix
|
<template #prefix
|
||||||
><i :class="icon('ph-link-simple')"></i
|
><i :class="icon('ph-scroll')"></i
|
||||||
></template>
|
></template>
|
||||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
|
|
||||||
|
<FormTextarea v-model="moreUrls" class="_formBlock">
|
||||||
|
<template #label>{{ i18n.ts.moreUrls }}</template>
|
||||||
|
<template #caption>{{
|
||||||
|
i18n.ts.moreUrlsDescription
|
||||||
|
}}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
<FormSplit :min-width="300">
|
<FormSplit :min-width="300">
|
||||||
<FormInput
|
<FormInput
|
||||||
v-model="maintainerName"
|
v-model="maintainerName"
|
||||||
|
@ -446,6 +453,7 @@ import icon from "@/scripts/icon";
|
||||||
const name = ref<string | null>(null);
|
const name = ref<string | null>(null);
|
||||||
const description = ref<string | null>(null);
|
const description = ref<string | null>(null);
|
||||||
const tosUrl = ref<string | null>(null);
|
const tosUrl = ref<string | null>(null);
|
||||||
|
const moreUrls = ref<string | null>(null);
|
||||||
const maintainerName = ref<string | null>(null);
|
const maintainerName = ref<string | null>(null);
|
||||||
const maintainerEmail = ref<string | null>(null);
|
const maintainerEmail = ref<string | null>(null);
|
||||||
const donationLink = ref<string | null>(null);
|
const donationLink = ref<string | null>(null);
|
||||||
|
@ -480,12 +488,44 @@ const defaultReactionCustom = ref("");
|
||||||
const enableServerMachineStats = ref(false);
|
const enableServerMachineStats = ref(false);
|
||||||
const enableIdenticonGeneration = ref(false);
|
const enableIdenticonGeneration = ref(false);
|
||||||
|
|
||||||
|
function isValidHttpUrl(src: string) {
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(src);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMoreUrls(src: string): { name: string; url: string }[] {
|
||||||
|
const toReturn: { name: string; url: string }[] = [];
|
||||||
|
const pattern = /"(.+)"\s*:\s*(http.+)/;
|
||||||
|
src.trim()
|
||||||
|
.split("\n")
|
||||||
|
.forEach((line) => {
|
||||||
|
const match = pattern.exec(line);
|
||||||
|
if (match != null && isValidHttpUrl(match[2]))
|
||||||
|
toReturn.push({ name: match[1], url: match[2] });
|
||||||
|
else console.error(`invalid syntax or invalid URL: ${line}`);
|
||||||
|
});
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyMoreUrls(src: { name: string; url: string }[]): string {
|
||||||
|
let toReturn = "";
|
||||||
|
for (const { name, url } of src)
|
||||||
|
toReturn = toReturn.concat(`"${name}": ${url}`, "\n");
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await os.api("admin/meta");
|
const meta = await os.api("admin/meta");
|
||||||
if (!meta) throw new Error("No meta");
|
if (!meta) throw new Error("No meta");
|
||||||
name.value = meta.name;
|
name.value = meta.name;
|
||||||
description.value = meta.description;
|
description.value = meta.description;
|
||||||
tosUrl.value = meta.tosUrl;
|
tosUrl.value = meta.tosUrl;
|
||||||
|
moreUrls.value = stringifyMoreUrls(meta.moreUrls);
|
||||||
iconUrl.value = meta.iconUrl;
|
iconUrl.value = meta.iconUrl;
|
||||||
bannerUrl.value = meta.bannerUrl;
|
bannerUrl.value = meta.bannerUrl;
|
||||||
logoImageUrl.value = meta.logoImageUrl;
|
logoImageUrl.value = meta.logoImageUrl;
|
||||||
|
@ -535,6 +575,7 @@ function save() {
|
||||||
name: name.value,
|
name: name.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
tosUrl: tosUrl.value,
|
tosUrl: tosUrl.value,
|
||||||
|
moreUrls: parseMoreUrls(moreUrls.value ?? ""),
|
||||||
iconUrl: iconUrl.value,
|
iconUrl: iconUrl.value,
|
||||||
bannerUrl: bannerUrl.value,
|
bannerUrl: bannerUrl.value,
|
||||||
logoImageUrl: logoImageUrl.value,
|
logoImageUrl: logoImageUrl.value,
|
||||||
|
|
|
@ -5,6 +5,31 @@ import { host } from "@/config";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import type { MenuItem } from "@/types/menu";
|
||||||
|
|
||||||
|
const instanceSpecificItems: MenuItem[] = [];
|
||||||
|
|
||||||
|
if (instance.tosUrl != null) {
|
||||||
|
instanceSpecificItems.push({
|
||||||
|
type: "button",
|
||||||
|
text: i18n.ts.tos,
|
||||||
|
icon: `${icon("ph-scroll")}`,
|
||||||
|
action: () => {
|
||||||
|
window.open(instance.tosUrl, "_blank");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { name, url } of instance.moreUrls) {
|
||||||
|
instanceSpecificItems.push({
|
||||||
|
type: "button",
|
||||||
|
text: name,
|
||||||
|
icon: `${icon("ph-link-simple")}`,
|
||||||
|
action: () => {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function openHelpMenu_(ev: MouseEvent) {
|
export function openHelpMenu_(ev: MouseEvent) {
|
||||||
os.popupMenu(
|
os.popupMenu(
|
||||||
|
@ -25,16 +50,9 @@ export function openHelpMenu_(ev: MouseEvent) {
|
||||||
icon: `${icon("ph-lightbulb")}`,
|
icon: `${icon("ph-lightbulb")}`,
|
||||||
to: "/about-firefish",
|
to: "/about-firefish",
|
||||||
},
|
},
|
||||||
instance.tosUrl
|
...(instanceSpecificItems.length >= 2 ? [null] : []),
|
||||||
? {
|
...instanceSpecificItems,
|
||||||
type: "button",
|
null,
|
||||||
text: i18n.ts.tos,
|
|
||||||
icon: `${icon("ph-scroll")}`,
|
|
||||||
action: () => {
|
|
||||||
window.open(instance.tosUrl, "_blank");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
text: i18n.ts.apps,
|
text: i18n.ts.apps,
|
||||||
|
|
Loading…
Reference in a new issue