@samitouri / QOSami-HFS / commits / 60c1ab61

admin/home: display #N as link in updates' text

Massimo Melina committed Jul 22, 2023 at 13:38 UTC 60c1ab61eedaf75f3e5320ce9370ea902319a112
2 files changed +29 -23
admin/src/HomePage.ts
+9 -3
@@ -16,8 +16,8 @@ import {
16 wikiLink,
17 with_
18 } from './misc'
19 -import { BrowserUpdated as UpdateIcon, CheckCircle, Error, Info, Launch, Warning } from '@mui/icons-material'
20 -import md from './md'
19 +import { BrowserUpdated as UpdateIcon, CheckCircle, Error, Info, Launch, OpenInNew, Warning } from '@mui/icons-material'
20 +import md, { replaceStringToReact } from './md'
21 import { state, useSnapState } from './state'
22 import { alertDialog, confirmDialog, toast } from './dialog'
23 import { isCertError, isKeyError, makeCertAndSave } from './OptionsPage'
@@ -122,11 +122,17 @@ export default function HomePage() {
122 ...!x.isNewer && { color: 'warning', variant: 'outlined' },
123 onClick: () => update(x.tag_name)
124 }, prefix("Install ", x.name, x.isNewer ? '' : " (older)")),
125 - h(Box, { mt: 1 }, md(x.body, { linkTarget: '_blank' }))
125 + h(Box, { mt: 1 }, md(x.body, { onText, linkTarget: '_blank' }))
126 )),
127 )),
128 ))
129 )
130 +
131 + function onText(s: string) {
132 + return replaceStringToReact(s, /(?<=^|\W)#(\d+)\b/g, m => // link issues
133 + h(Link, { href: REPO_URL + 'issues/' + m[1], target: '_blank' },
134 + h(OpenInNew, { sx: { verticalAlign: 'bottom' } }) ))
135 + }
136 }
137
138 async function update(tag?: string) {
admin/src/md.ts
+20 -20
@@ -1,34 +1,34 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 -import { createElement as h, Fragment } from 'react'
3 +import { createElement as h, Fragment, ReactNode } from 'react'
4 import { Link } from '@mui/material'
5
6 +const TAGS = {
7 + '`': 'code',
8 + '*': 'b',
9 + '/': 'i',
10 + '_': 'u',
11 +}
12 // markdown inspired syntax to transform text into react elements: * for bold, / for italic, _ for underline, ` for code
7 -export default function md(text: string | TemplateStringsArray, { linkTarget='' }={}) {
13 +type OnText = (s: string) => ReactNode
14 +export default function md(text: string | TemplateStringsArray, { linkTarget='', onText=(x=>x) as OnText }={}) {
15 if (typeof text !== 'string')
16 text = text[0]
10 - const re = /([`*/_])(.+)\1|(\n)|\[(.+)\]\((.+)\)/g
17 + return replaceStringToReact(text, /([`*/_])(.+)\1|(\n)|\[(.+)\]\((.+)\)/g, m =>
18 + m[4] ? h(Link, { href: m[5], target: linkTarget }, onText(m[4]))
19 + : m[3] ? h('br')
20 + : h((TAGS as any)[ m[1] ], {}, onText(m[2])),
21 + onText)
22 +}
23 +
24 +export function replaceStringToReact(text: string, re: RegExp, cb: (match: RegExpExecArray) => ReactNode, onText=(x=>x) as OnText ) {
25 const res = []
26 let last = 0
27 let match
28 while (match = re.exec(text)) { //eslint-disable-line no-cond-assign
15 - res.push( text.slice(last, match.index) )
16 - if (match[4])
17 - res.push(h(Link, { href: match[5], target: linkTarget }, match[4]))
18 - else if (match[3])
19 - res.push(h('br'))
20 - else {
21 - const tag = ({
22 - '`': 'code',
23 - '*': 'b',
24 - '/': 'i',
25 - '_': 'u',
26 - })[ match[1] ]
27 - if (!tag)
28 - throw Error("should never happen")
29 - res.push( h(tag,{}, match[2]) )
30 - }
29 + res.push(onText(text.slice(last, match.index)))
30 + res.push(cb(match))
31 last = match.index + match[0].length
32 }
33 - return h(Fragment, {}, ...res, text.slice(last, Infinity))
33 + return h(Fragment, {}, ...res, onText(text.slice(last, Infinity)))
34 }