feat(list): add info dialoge to show list description (#2368)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2368 Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
parent
b24d5f2dce
commit
84260841be
9 changed files with 130 additions and 44 deletions
|
@ -65,6 +65,7 @@
|
||||||
"@cypress/vite-dev-server": "3.1.1",
|
"@cypress/vite-dev-server": "3.1.1",
|
||||||
"@cypress/vue": "4.2.0",
|
"@cypress/vue": "4.2.0",
|
||||||
"@faker-js/faker": "7.5.0",
|
"@faker-js/faker": "7.5.0",
|
||||||
|
"@types/dompurify": "^2.3.4",
|
||||||
"@types/flexsearch": "0.7.3",
|
"@types/flexsearch": "0.7.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.37.0",
|
"@typescript-eslint/eslint-plugin": "5.37.0",
|
||||||
"@typescript-eslint/parser": "5.37.0",
|
"@typescript-eslint/parser": "5.37.0",
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
|
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="info-button">
|
||||||
|
<icon icon="circle-info"/>
|
||||||
|
</BaseButton>
|
||||||
|
|
||||||
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
|
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -284,10 +288,21 @@ $hamburger-menu-icon-width: 28px;
|
||||||
|
|
||||||
:deep(.dropdown-trigger) {
|
:deep(.dropdown-trigger) {
|
||||||
color: var(--grey-400);
|
color: var(--grey-400);
|
||||||
margin-left: 1rem;
|
margin-left: .5rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-button {
|
||||||
|
text-align: center;
|
||||||
|
height: 1.25rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
width: 2rem;
|
||||||
|
margin-top: .25rem;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
color: var(--grey-400);
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -72,7 +72,7 @@ import {defineComponent} from 'vue'
|
||||||
import VueEasymde from './vue-easymde.vue'
|
import VueEasymde from './vue-easymde.vue'
|
||||||
import {marked} from 'marked'
|
import {marked} from 'marked'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import hljs from 'highlight.js/lib/common'
|
import {setupMarkdownRenderer} from '@/helpers/markdownRenderer'
|
||||||
|
|
||||||
import {createEasyMDEConfig} from './editorConfig'
|
import {createEasyMDEConfig} from './editorConfig'
|
||||||
|
|
||||||
|
@ -222,43 +222,7 @@ export default defineComponent({
|
||||||
return checkboxes[n]
|
return checkboxes[n]
|
||||||
},
|
},
|
||||||
renderPreview() {
|
renderPreview() {
|
||||||
const renderer = new marked.Renderer()
|
setupMarkdownRenderer(this.checkboxId)
|
||||||
const linkRenderer = renderer.link
|
|
||||||
|
|
||||||
let checkboxNum = -1
|
|
||||||
marked.use({
|
|
||||||
renderer: {
|
|
||||||
image: (src, title, text) => {
|
|
||||||
|
|
||||||
title = title ? ` title="${title}` : ''
|
|
||||||
|
|
||||||
// If the url starts with the api url, the image is likely an attachment and
|
|
||||||
// we'll need to download and parse it properly.
|
|
||||||
if (src.substr(0, window.API_URL.length + 7) === `${window.API_URL}/tasks/`) {
|
|
||||||
return `<img data-src="${src}" alt="${text}" ${title} class="attachment-image"/>`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<img src="${src}" alt="${text}" ${title}/>`
|
|
||||||
},
|
|
||||||
checkbox: (checked) => {
|
|
||||||
if (checked) {
|
|
||||||
checked = ' checked="checked"'
|
|
||||||
}
|
|
||||||
|
|
||||||
checkboxNum++
|
|
||||||
return `<input type="checkbox" data-checkbox-num="${checkboxNum}" ${checked} class="text-checkbox-${this.checkboxId}"/>`
|
|
||||||
},
|
|
||||||
link: (href, title, text) => {
|
|
||||||
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
|
|
||||||
const html = linkRenderer.call(renderer, href, title, text)
|
|
||||||
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
highlight: function (code, language) {
|
|
||||||
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
|
|
||||||
return hljs.highlight(code, {language: validLanguage}).value
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.preview = DOMPurify.sanitize(marked(this.text), {ADD_ATTR: ['target']})
|
this.preview = DOMPurify.sanitize(marked(this.text), {ADD_ATTR: ['target']})
|
||||||
|
|
||||||
|
|
44
src/helpers/markdownRenderer.ts
Normal file
44
src/helpers/markdownRenderer.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import {marked} from 'marked'
|
||||||
|
import hljs from 'highlight.js/lib/common'
|
||||||
|
|
||||||
|
export function setupMarkdownRenderer(checkboxId: string) {
|
||||||
|
const renderer = new marked.Renderer()
|
||||||
|
const linkRenderer = renderer.link
|
||||||
|
|
||||||
|
let checkboxNum = -1
|
||||||
|
marked.use({
|
||||||
|
renderer: {
|
||||||
|
image: (src, title, text) => {
|
||||||
|
|
||||||
|
title = title ? ` title="${title}` : ''
|
||||||
|
|
||||||
|
// If the url starts with the api url, the image is likely an attachment and
|
||||||
|
// we'll need to download and parse it properly.
|
||||||
|
if (src.substr(0, window.API_URL.length + 7) === `${window.API_URL}/tasks/`) {
|
||||||
|
return `<img data-src="${src}" alt="${text}" ${title} class="attachment-image"/>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<img src="${src}" alt="${text}" ${title}/>`
|
||||||
|
},
|
||||||
|
checkbox: (checked) => {
|
||||||
|
if (checked) {
|
||||||
|
checked = ' checked="checked"'
|
||||||
|
}
|
||||||
|
|
||||||
|
checkboxNum++
|
||||||
|
return `<input type="checkbox" data-checkbox-num="${checkboxNum}" ${checked} class="text-checkbox-${checkboxId}"/>`
|
||||||
|
},
|
||||||
|
link: (href, title, text) => {
|
||||||
|
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
|
||||||
|
const html = linkRenderer.call(renderer, href, title, text)
|
||||||
|
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
highlight: function (code, language) {
|
||||||
|
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
|
||||||
|
return hljs.highlight(code, {language: validLanguage}).value
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return renderer
|
||||||
|
}
|
|
@ -172,6 +172,7 @@
|
||||||
"search": "Type to search for a list…",
|
"search": "Type to search for a list…",
|
||||||
"searchSelect": "Click or press enter to select this list",
|
"searchSelect": "Click or press enter to select this list",
|
||||||
"shared": "Shared Lists",
|
"shared": "Shared Lists",
|
||||||
|
"noDescriptionAvailable": "No list description is available.",
|
||||||
"create": {
|
"create": {
|
||||||
"header": "New list",
|
"header": "New list",
|
||||||
"titlePlaceholder": "The list's title goes here…",
|
"titlePlaceholder": "The list's title goes here…",
|
||||||
|
|
10
src/icons.ts
10
src/icons.ts
|
@ -11,16 +11,17 @@ import {
|
||||||
faCheckDouble,
|
faCheckDouble,
|
||||||
faChessKnight,
|
faChessKnight,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faCircleInfo,
|
||||||
faCloudDownloadAlt,
|
faCloudDownloadAlt,
|
||||||
faCloudUploadAlt,
|
faCloudUploadAlt,
|
||||||
faCocktail,
|
faCocktail,
|
||||||
faCoffee,
|
faCoffee,
|
||||||
faCog,
|
faCog,
|
||||||
faEye,
|
|
||||||
faEyeSlash,
|
|
||||||
faEllipsisH,
|
faEllipsisH,
|
||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
faExclamation,
|
faExclamation,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
faFillDrip,
|
faFillDrip,
|
||||||
faFilter,
|
faFilter,
|
||||||
faForward,
|
faForward,
|
||||||
|
@ -82,6 +83,7 @@ library.add(faCheck)
|
||||||
library.add(faCheckDouble)
|
library.add(faCheckDouble)
|
||||||
library.add(faChessKnight)
|
library.add(faChessKnight)
|
||||||
library.add(faChevronDown)
|
library.add(faChevronDown)
|
||||||
|
library.add(faCircleInfo)
|
||||||
library.add(faClock)
|
library.add(faClock)
|
||||||
library.add(faCloudDownloadAlt)
|
library.add(faCloudDownloadAlt)
|
||||||
library.add(faCloudUploadAlt)
|
library.add(faCloudUploadAlt)
|
||||||
|
@ -89,11 +91,11 @@ library.add(faCocktail)
|
||||||
library.add(faCoffee)
|
library.add(faCoffee)
|
||||||
library.add(faCog)
|
library.add(faCog)
|
||||||
library.add(faComments)
|
library.add(faComments)
|
||||||
library.add(faEye)
|
|
||||||
library.add(faEyeSlash)
|
|
||||||
library.add(faEllipsisH)
|
library.add(faEllipsisH)
|
||||||
library.add(faEllipsisV)
|
library.add(faEllipsisV)
|
||||||
library.add(faExclamation)
|
library.add(faExclamation)
|
||||||
|
library.add(faEye)
|
||||||
|
library.add(faEyeSlash)
|
||||||
library.add(faFillDrip)
|
library.add(faFillDrip)
|
||||||
library.add(faFilter)
|
library.add(faFilter)
|
||||||
library.add(faForward)
|
library.add(faForward)
|
||||||
|
|
|
@ -35,6 +35,7 @@ import ListList from '../views/list/ListList.vue'
|
||||||
import ListGantt from '../views/list/ListGantt.vue'
|
import ListGantt from '../views/list/ListGantt.vue'
|
||||||
import ListTable from '../views/list/ListTable.vue'
|
import ListTable from '../views/list/ListTable.vue'
|
||||||
import ListKanban from '../views/list/ListKanban.vue'
|
import ListKanban from '../views/list/ListKanban.vue'
|
||||||
|
const ListInfo = () => import('../views/list/ListInfo.vue')
|
||||||
|
|
||||||
// List Settings
|
// List Settings
|
||||||
import ListSettingEdit from '../views/list/settings/edit.vue'
|
import ListSettingEdit from '../views/list/settings/edit.vue'
|
||||||
|
@ -336,6 +337,15 @@ const router = createRouter({
|
||||||
showAsModal: true,
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/lists/:listId/info',
|
||||||
|
name: 'list.info',
|
||||||
|
component: ListInfo,
|
||||||
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
|
},
|
||||||
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId',
|
path: '/lists/:listId',
|
||||||
name: 'list.index',
|
name: 'list.index',
|
||||||
|
|
42
src/views/list/ListInfo.vue
Normal file
42
src/views/list/ListInfo.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
@close="$router.back()"
|
||||||
|
>
|
||||||
|
<card
|
||||||
|
:title="list.title"
|
||||||
|
>
|
||||||
|
<div class="has-text-left" v-html="htmlDescription" v-if="htmlDescription !== ''"></div>
|
||||||
|
<p v-else class="is-italic">
|
||||||
|
{{ $t('list.noDescriptionAvailable') }}
|
||||||
|
</p>
|
||||||
|
</card>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed} from 'vue'
|
||||||
|
import {useStore} from '@/store'
|
||||||
|
import {setupMarkdownRenderer} from '@/helpers/markdownRenderer'
|
||||||
|
import {marked} from 'marked'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
import {createRandomID} from '@/helpers/randomId'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
listId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const list = computed(() => store.getters['lists/getListById'](props.listId))
|
||||||
|
const htmlDescription = computed(() => {
|
||||||
|
const description = list.value?.description || ''
|
||||||
|
if (description === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMarkdownRenderer(createRandomID())
|
||||||
|
return DOMPurify.sanitize(marked(description), {ADD_ATTR: ['target']})
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -2168,6 +2168,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/dompurify@^2.3.4":
|
||||||
|
version "2.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.4.tgz#94e997e30338ea24d4c8d08beca91ce4dd17a1b4"
|
||||||
|
integrity sha512-EXzDatIb5EspL2eb/xPGmaC8pePcTHrkDCONjeisusLFrVfl38Pjea/R0YJGu3k9ZQadSvMqW0WXPI2hEo2Ajg==
|
||||||
|
dependencies:
|
||||||
|
"@types/trusted-types" "*"
|
||||||
|
|
||||||
"@types/download@^8.0.0":
|
"@types/download@^8.0.0":
|
||||||
version "8.0.1"
|
version "8.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/download/-/download-8.0.1.tgz#9653e0deb52f1b47f659e8e8be1651c8515bc0a7"
|
resolved "https://registry.yarnpkg.com/@types/download/-/download-8.0.1.tgz#9653e0deb52f1b47f659e8e8be1651c8515bc0a7"
|
||||||
|
@ -2386,7 +2393,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "*"
|
"@types/estree" "*"
|
||||||
|
|
||||||
"@types/trusted-types@^2.0.2":
|
"@types/trusted-types@*", "@types/trusted-types@^2.0.2":
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
|
||||||
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
|
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
|
||||||
|
|
Loading…
Reference in a new issue