Add markdown enhancements
This commit will bring: - Linking to other users with `@username` - Linking to tags with `#tag` - Opening external links in new tab (Fix #1647) - Single line breaks to avoid confusion for non-technical users (Fix #1377) - 😒 support... - Email encoding in markdown - Markdown editor now auto-resizes to accomodate content (Fix #1379) NOTE: This only works in very few places. We need to wait for #1835 to have those features available widely
This commit is contained in:
		
							parent
							
								
									8aa073b976
								
							
						
					
					
						commit
						f06c040b50
					
				| 
						 | 
					@ -32,6 +32,8 @@ tasks:
 | 
				
			||||||
      poetry run python manage.py gitpod dev
 | 
					      poetry run python manage.py gitpod dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - name: Frontend
 | 
					  - name: Frontend
 | 
				
			||||||
 | 
					    env:
 | 
				
			||||||
 | 
					      VUE_EDITOR: code
 | 
				
			||||||
    before: cd front
 | 
					    before: cd front
 | 
				
			||||||
    init: |
 | 
					    init: |
 | 
				
			||||||
      yarn install
 | 
					      yarn install
 | 
				
			||||||
| 
						 | 
					@ -42,6 +44,7 @@ tasks:
 | 
				
			||||||
    env:
 | 
					    env:
 | 
				
			||||||
      COMPOSE_FILE: /workspace/funkwhale/.gitpod/docker-compose.yml
 | 
					      COMPOSE_FILE: /workspace/funkwhale/.gitpod/docker-compose.yml
 | 
				
			||||||
      ENV_FILE: /workspace/funkwhale/.gitpod/.env
 | 
					      ENV_FILE: /workspace/funkwhale/.gitpod/.env
 | 
				
			||||||
 | 
					      VUE_EDITOR: code
 | 
				
			||||||
    command: |
 | 
					    command: |
 | 
				
			||||||
      clear
 | 
					      clear
 | 
				
			||||||
      echo ""
 | 
					      echo ""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,31 +18,35 @@
 | 
				
			||||||
    "postinstall": "yarn run fix-fomantic-css"
 | 
					    "postinstall": "yarn run fix-fomantic-css"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@tiptap/starter-kit": "^2.0.0-beta.191",
 | 
				
			||||||
 | 
					    "@tiptap/vue-3": "^2.0.0-beta.96",
 | 
				
			||||||
    "@vue/runtime-core": "3.2.37",
 | 
					    "@vue/runtime-core": "3.2.37",
 | 
				
			||||||
    "@vueuse/core": "8.9.4",
 | 
					    "@vueuse/core": "8.9.4",
 | 
				
			||||||
    "@vueuse/integrations": "8.9.4",
 | 
					    "@vueuse/integrations": "8.9.4",
 | 
				
			||||||
    "axios": "0.27.2",
 | 
					    "axios": "0.27.2",
 | 
				
			||||||
    "axios-auth-refresh": "3.3.1",
 | 
					    "axios-auth-refresh": "3.3.3",
 | 
				
			||||||
    "diff": "5.1.0",
 | 
					    "diff": "5.1.0",
 | 
				
			||||||
    "dompurify": "2.3.8",
 | 
					    "dompurify": "2.3.10",
 | 
				
			||||||
    "focus-trap": "6.9.4",
 | 
					    "focus-trap": "6.9.4",
 | 
				
			||||||
    "fomantic-ui-css": "2.8.8",
 | 
					    "fomantic-ui-css": "2.8.8",
 | 
				
			||||||
    "howler": "2.2.3",
 | 
					    "howler": "2.2.3",
 | 
				
			||||||
    "js-logger": "1.6.1",
 | 
					    "js-logger": "1.6.1",
 | 
				
			||||||
    "lodash-es": "4.17.21",
 | 
					    "lodash-es": "4.17.21",
 | 
				
			||||||
 | 
					    "mavon-editor": "^3.0.0-beta",
 | 
				
			||||||
    "moment": "2.29.4",
 | 
					    "moment": "2.29.4",
 | 
				
			||||||
    "qs": "6.11.0",
 | 
					    "qs": "6.11.0",
 | 
				
			||||||
    "register-service-worker": "1.7.2",
 | 
					    "register-service-worker": "1.7.2",
 | 
				
			||||||
    "sanitize-html": "2.7.1",
 | 
					    "sanitize-html": "2.7.1",
 | 
				
			||||||
    "sass": "1.53.0",
 | 
					    "sass": "1.54.0",
 | 
				
			||||||
    "showdown": "2.1.0",
 | 
					    "showdown": "2.1.0",
 | 
				
			||||||
    "text-clipper": "2.2.0",
 | 
					    "text-clipper": "2.2.0",
 | 
				
			||||||
 | 
					    "tiptap-markdown": "^0.5.0",
 | 
				
			||||||
    "transliteration": "2.3.5",
 | 
					    "transliteration": "2.3.5",
 | 
				
			||||||
    "vue": "3.2.37",
 | 
					    "vue": "3.2.37",
 | 
				
			||||||
    "vue-gettext": "2.1.12",
 | 
					    "vue-gettext": "2.1.12",
 | 
				
			||||||
    "vue-plyr": "7.0.0",
 | 
					    "vue-plyr": "7.0.0",
 | 
				
			||||||
    "vue-router": "4.1.2",
 | 
					    "vue-router": "4.1.2",
 | 
				
			||||||
    "vue-tsc": "0.38.9",
 | 
					    "vue-tsc": "0.39.0",
 | 
				
			||||||
    "vue-upload-component": "3.1.2",
 | 
					    "vue-upload-component": "3.1.2",
 | 
				
			||||||
    "vue-virtual-scroller": "^2.0.0-alpha.1",
 | 
					    "vue-virtual-scroller": "^2.0.0-alpha.1",
 | 
				
			||||||
    "vue3-gettext": "2.3.0",
 | 
					    "vue3-gettext": "2.3.0",
 | 
				
			||||||
| 
						 | 
					@ -66,7 +70,7 @@
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "5.30.7",
 | 
					    "@typescript-eslint/eslint-plugin": "5.30.7",
 | 
				
			||||||
    "@vitejs/plugin-vue": "3.0.1",
 | 
					    "@vitejs/plugin-vue": "3.0.1",
 | 
				
			||||||
    "@vue/compiler-sfc": "3.2.37",
 | 
					    "@vue/compiler-sfc": "3.2.37",
 | 
				
			||||||
    "@vue/eslint-config-standard": "7.0.0",
 | 
					    "@vue/eslint-config-standard": "8.0.0",
 | 
				
			||||||
    "@vue/eslint-config-typescript": "11.0.0",
 | 
					    "@vue/eslint-config-typescript": "11.0.0",
 | 
				
			||||||
    "@vue/test-utils": "2.0.2",
 | 
					    "@vue/test-utils": "2.0.2",
 | 
				
			||||||
    "@vue/tsconfig": "0.1.3",
 | 
					    "@vue/tsconfig": "0.1.3",
 | 
				
			||||||
| 
						 | 
					@ -79,14 +83,15 @@
 | 
				
			||||||
    "eslint-plugin-n": "15.2.4",
 | 
					    "eslint-plugin-n": "15.2.4",
 | 
				
			||||||
    "eslint-plugin-node": "11.1.0",
 | 
					    "eslint-plugin-node": "11.1.0",
 | 
				
			||||||
    "eslint-plugin-promise": "6.0.0",
 | 
					    "eslint-plugin-promise": "6.0.0",
 | 
				
			||||||
    "eslint-plugin-vue": "9.2.0",
 | 
					    "eslint-plugin-vue": "9.3.0",
 | 
				
			||||||
    "jest-cli": "28.1.3",
 | 
					    "jest-cli": "28.1.3",
 | 
				
			||||||
    "moxios": "0.4.0",
 | 
					    "moxios": "0.4.0",
 | 
				
			||||||
    "sinon": "14.0.0",
 | 
					    "sinon": "14.0.0",
 | 
				
			||||||
    "ts-jest": "28.0.7",
 | 
					    "ts-jest": "28.0.7",
 | 
				
			||||||
    "typescript": "4.7.4",
 | 
					    "typescript": "4.7.4",
 | 
				
			||||||
    "vite": "3.0.2",
 | 
					    "vite": "3.0.3",
 | 
				
			||||||
    "vite-plugin-pwa": "0.12.3",
 | 
					    "vite-plugin-pwa": "0.12.3",
 | 
				
			||||||
 | 
					    "vite-plugin-vue-inspector": "1.0.1",
 | 
				
			||||||
    "vue-jest": "3.0.7",
 | 
					    "vue-jest": "3.0.7",
 | 
				
			||||||
    "workbox-core": "6.5.3",
 | 
					    "workbox-core": "6.5.3",
 | 
				
			||||||
    "workbox-precaching": "6.5.3",
 | 
					    "workbox-precaching": "6.5.3",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,11 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { useStore } from '~/store'
 | 
					import { useStore } from '~/store'
 | 
				
			||||||
import { get } from 'lodash-es'
 | 
					import { get } from 'lodash-es'
 | 
				
			||||||
import showdown from 'showdown'
 | 
					import useMarkdown from '~/composables/useMarkdown'
 | 
				
			||||||
import { humanSize } from '~/utils/filters'
 | 
					import { humanSize } from '~/utils/filters'
 | 
				
			||||||
import { computed } from 'vue'
 | 
					import { computed } from 'vue'
 | 
				
			||||||
import { useGettext } from 'vue3-gettext'
 | 
					import { useGettext } from 'vue3-gettext'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const markdown = new showdown.Converter()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const store = useStore()
 | 
					const store = useStore()
 | 
				
			||||||
const nodeinfo = computed(() => store.state.instance.nodeinfo)
 | 
					const nodeinfo = computed(() => store.state.instance.nodeinfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,9 +16,9 @@ const labels = computed(() => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const podName = computed(() => get(nodeinfo.value, 'metadata.nodeName') || 'Funkwhale')
 | 
					const podName = computed(() => get(nodeinfo.value, 'metadata.nodeName') || 'Funkwhale')
 | 
				
			||||||
const banner = computed(() => get(nodeinfo.value, 'metadata.banner'))
 | 
					const banner = computed(() => get(nodeinfo.value, 'metadata.banner'))
 | 
				
			||||||
const longDescription = computed(() => get(nodeinfo.value, 'metadata.longDescription'))
 | 
					const longDescription = useMarkdown(() => get(nodeinfo.value, 'metadata.longDescription'))
 | 
				
			||||||
const rules = computed(() => get(nodeinfo.value, 'metadata.rules'))
 | 
					const rules = useMarkdown(() => get(nodeinfo.value, 'metadata.rules'))
 | 
				
			||||||
const terms = computed(() => get(nodeinfo.value, 'metadata.terms'))
 | 
					const terms = useMarkdown(() => get(nodeinfo.value, 'metadata.terms'))
 | 
				
			||||||
const contactEmail = computed(() => get(nodeinfo.value, 'metadata.contactEmail'))
 | 
					const contactEmail = computed(() => get(nodeinfo.value, 'metadata.contactEmail'))
 | 
				
			||||||
const anonymousCanListen = computed(() => get(nodeinfo.value, 'metadata.library.anonymousCanListen'))
 | 
					const anonymousCanListen = computed(() => get(nodeinfo.value, 'metadata.library.anonymousCanListen'))
 | 
				
			||||||
const allowListEnabled = computed(() => get(nodeinfo.value, 'metadata.allowList.enabled'))
 | 
					const allowListEnabled = computed(() => get(nodeinfo.value, 'metadata.allowList.enabled'))
 | 
				
			||||||
| 
						 | 
					@ -140,7 +138,7 @@ const headerStyle = computed(() => {
 | 
				
			||||||
              </h2>
 | 
					              </h2>
 | 
				
			||||||
              <sanitized-html
 | 
					              <sanitized-html
 | 
				
			||||||
                v-if="longDescription"
 | 
					                v-if="longDescription"
 | 
				
			||||||
                :html="markdown.makeHtml(longDescription)"
 | 
					                :html="longDescription"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
              <p v-else>
 | 
					              <p v-else>
 | 
				
			||||||
                <translate translate-context="Content/About/Paragraph">
 | 
					                <translate translate-context="Content/About/Paragraph">
 | 
				
			||||||
| 
						 | 
					@ -158,7 +156,7 @@ const headerStyle = computed(() => {
 | 
				
			||||||
              </h3>
 | 
					              </h3>
 | 
				
			||||||
              <sanitized-html
 | 
					              <sanitized-html
 | 
				
			||||||
                v-if="rules"
 | 
					                v-if="rules"
 | 
				
			||||||
                :html="markdown.makeHtml(rules)"
 | 
					                :html="rules"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
              <p v-else>
 | 
					              <p v-else>
 | 
				
			||||||
                <translate translate-context="Content/About/Paragraph">
 | 
					                <translate translate-context="Content/About/Paragraph">
 | 
				
			||||||
| 
						 | 
					@ -176,7 +174,7 @@ const headerStyle = computed(() => {
 | 
				
			||||||
              </h3>
 | 
					              </h3>
 | 
				
			||||||
              <sanitized-html
 | 
					              <sanitized-html
 | 
				
			||||||
                v-if="terms"
 | 
					                v-if="terms"
 | 
				
			||||||
                :html="markdown.makeHtml(terms)"
 | 
					                :html="terms"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
              <p v-else>
 | 
					              <p v-else>
 | 
				
			||||||
                <translate translate-context="Content/About/Paragraph">
 | 
					                <translate translate-context="Content/About/Paragraph">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,10 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { get } from 'lodash-es'
 | 
					import { get } from 'lodash-es'
 | 
				
			||||||
import showdown from 'showdown'
 | 
					 | 
				
			||||||
import AlbumWidget from '~/components/audio/album/Widget.vue'
 | 
					import AlbumWidget from '~/components/audio/album/Widget.vue'
 | 
				
			||||||
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
 | 
					import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
 | 
				
			||||||
import LoginForm from '~/components/auth/LoginForm.vue'
 | 
					import LoginForm from '~/components/auth/LoginForm.vue'
 | 
				
			||||||
import SignupForm from '~/components/auth/SignupForm.vue'
 | 
					import SignupForm from '~/components/auth/SignupForm.vue'
 | 
				
			||||||
 | 
					import useMarkdown from '~/composables/useMarkdown'
 | 
				
			||||||
import { humanSize } from '~/utils/filters'
 | 
					import { humanSize } from '~/utils/filters'
 | 
				
			||||||
import { useStore } from '~/store'
 | 
					import { useStore } from '~/store'
 | 
				
			||||||
import { computed } from 'vue'
 | 
					import { computed } from 'vue'
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,6 @@ import { whenever } from '@vueuse/core'
 | 
				
			||||||
import { useGettext } from 'vue3-gettext'
 | 
					import { useGettext } from 'vue3-gettext'
 | 
				
			||||||
import { useRouter } from 'vue-router'
 | 
					import { useRouter } from 'vue-router'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const markdown = new showdown.Converter()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { $pgettext } = useGettext()
 | 
					const { $pgettext } = useGettext()
 | 
				
			||||||
const labels = computed(() => ({
 | 
					const labels = computed(() => ({
 | 
				
			||||||
  title: $pgettext('Head/Home/Title', 'Home')
 | 
					  title: $pgettext('Head/Home/Title', 'Home')
 | 
				
			||||||
| 
						 | 
					@ -25,7 +23,7 @@ const nodeinfo = computed(() => store.state.instance.nodeinfo)
 | 
				
			||||||
const podName = computed(() => get(nodeinfo.value, 'metadata.nodeName') || 'Funkwhale')
 | 
					const podName = computed(() => get(nodeinfo.value, 'metadata.nodeName') || 'Funkwhale')
 | 
				
			||||||
const banner = computed(() => get(nodeinfo.value, 'metadata.banner'))
 | 
					const banner = computed(() => get(nodeinfo.value, 'metadata.banner'))
 | 
				
			||||||
const shortDescription = computed(() => get(nodeinfo.value, 'metadata.shortDescription'))
 | 
					const shortDescription = computed(() => get(nodeinfo.value, 'metadata.shortDescription'))
 | 
				
			||||||
const longDescription = computed(() => get(nodeinfo.value, 'metadata.longDescription'))
 | 
					const longDescription = useMarkdown(() => get(nodeinfo.value, 'metadata.longDescription'))
 | 
				
			||||||
const rules = computed(() => get(nodeinfo.value, 'metadata.rules'))
 | 
					const rules = computed(() => get(nodeinfo.value, 'metadata.rules'))
 | 
				
			||||||
const contactEmail = computed(() => get(nodeinfo.value, 'metadata.contactEmail'))
 | 
					const contactEmail = computed(() => get(nodeinfo.value, 'metadata.contactEmail'))
 | 
				
			||||||
const anonymousCanListen = computed(() => get(nodeinfo.value, 'metadata.library.anonymousCanListen'))
 | 
					const anonymousCanListen = computed(() => get(nodeinfo.value, 'metadata.library.anonymousCanListen'))
 | 
				
			||||||
| 
						 | 
					@ -111,7 +109,7 @@ whenever(() => store.state.auth.authenticated, () => {
 | 
				
			||||||
                  <sanitized-html
 | 
					                  <sanitized-html
 | 
				
			||||||
                    v-if="longDescription"
 | 
					                    v-if="longDescription"
 | 
				
			||||||
                    id="renderedDescription"
 | 
					                    id="renderedDescription"
 | 
				
			||||||
                    :html="markdown.makeHtml(longDescription)"
 | 
					                    :html="longDescription"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                  <div
 | 
					                  <div
 | 
				
			||||||
                    v-if="longDescription"
 | 
					                    v-if="longDescription"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,13 @@ const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
  tag: 'div'
 | 
					  tag: 'div'
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DOMPurify.addHook('afterSanitizeAttributes', (node) => {
 | 
				
			||||||
 | 
					  // set all elements owning target to target=_blank
 | 
				
			||||||
 | 
					  if ('target' in node) {
 | 
				
			||||||
 | 
					    node.setAttribute('target', '_blank')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const html = computed(() => DOMPurify.sanitize(props.html))
 | 
					const html = computed(() => DOMPurify.sanitize(props.html))
 | 
				
			||||||
const root = () => h(props.tag, { innerHTML: html.value })
 | 
					const root = () => h(props.tag, { innerHTML: html.value })
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,7 +46,7 @@ const fetchData = async (url = props.url) => {
 | 
				
			||||||
    count.value = response.data.count
 | 
					    count.value = response.data.count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const newObjects = !props.isActivity
 | 
					    const newObjects = !props.isActivity
 | 
				
			||||||
      ? response.data.results.map((track: Track) => { track })
 | 
					      ? response.data.results.map((track: Track) => ({ track }))
 | 
				
			||||||
      : response.data.results
 | 
					      : response.data.results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    objects.push(...newObjects)
 | 
					    objects.push(...newObjects)
 | 
				
			||||||
| 
						 | 
					@ -62,7 +62,7 @@ fetchData()
 | 
				
			||||||
const emit = defineEmits(['count'])
 | 
					const emit = defineEmits(['count'])
 | 
				
			||||||
watch(count, (to) => emit('count', to))
 | 
					watch(count, (to) => emit('count', to))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (props.websocketHandlers.includes('Listen')) {
 | 
					watch(() => props.websocketHandlers.includes('Listen'), (to) => {
 | 
				
			||||||
  useWebSocketHandler('Listen', (event) => {
 | 
					  useWebSocketHandler('Listen', (event) => {
 | 
				
			||||||
    // TODO (wvffle): Add reactivity to recently listened / favorited / added (#1316, #1534)
 | 
					    // TODO (wvffle): Add reactivity to recently listened / favorited / added (#1316, #1534)
 | 
				
			||||||
    // count.value += 1
 | 
					    // count.value += 1
 | 
				
			||||||
| 
						 | 
					@ -70,7 +70,7 @@ if (props.websocketHandlers.includes('Listen')) {
 | 
				
			||||||
    // objects.unshift(event as Listening)
 | 
					    // objects.unshift(event as Listening)
 | 
				
			||||||
    // objects.pop()
 | 
					    // objects.pop()
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					})
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,57 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { Library, Plugin, BackendError } from '~/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					import { clone } from 'lodash-es'
 | 
				
			||||||
 | 
					import useMarkdown, { useMarkdownRaw } from '~/composables/useMarkdown'
 | 
				
			||||||
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  plugin: Plugin
 | 
				
			||||||
 | 
					  libraries: Library[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<Props>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const description = useMarkdown(() => props.plugin.description ?? '')
 | 
				
			||||||
 | 
					const enabled = ref(props.plugin.enabled)
 | 
				
			||||||
 | 
					const values = clone(props.plugin.values ?? {})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const errors = ref([] as string[])
 | 
				
			||||||
 | 
					const isLoading = ref(false)
 | 
				
			||||||
 | 
					const submit = async () => {
 | 
				
			||||||
 | 
					  isLoading.value = true
 | 
				
			||||||
 | 
					  errors.value = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    await axios.post(`plugins/${props.plugin.name}/${enabled.value ? 'enable' : 'disable'}`)
 | 
				
			||||||
 | 
					    await axios.post(`plugins/${props.plugin.name}`, values)
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    errors.value = (error as BackendError).backendErrors
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isLoading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const scan = async () => {
 | 
				
			||||||
 | 
					  isLoading.value = true
 | 
				
			||||||
 | 
					  errors.value = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    await axios.post(`plugins/${props.plugin.name}/scan`, values)
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    errors.value = (error as BackendError).backendErrors
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isLoading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const submitAndScan = async () => {
 | 
				
			||||||
 | 
					  await submit()
 | 
				
			||||||
 | 
					  await scan()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <form
 | 
					  <form
 | 
				
			||||||
    :class="['ui segment form', {loading: isLoading}]"
 | 
					    :class="['ui segment form', {loading: isLoading}]"
 | 
				
			||||||
| 
						 | 
					@ -6,7 +60,7 @@
 | 
				
			||||||
    <h3>{{ plugin.label }}</h3>
 | 
					    <h3>{{ plugin.label }}</h3>
 | 
				
			||||||
    <sanitized-html
 | 
					    <sanitized-html
 | 
				
			||||||
      v-if="plugin.description"
 | 
					      v-if="plugin.description"
 | 
				
			||||||
      :html="markdown.makeHtml(plugin.description)"
 | 
					      :html="description"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <template v-if="plugin.homepage">
 | 
					    <template v-if="plugin.homepage">
 | 
				
			||||||
      <div class="ui small hidden divider" />
 | 
					      <div class="ui small hidden divider" />
 | 
				
			||||||
| 
						 | 
					@ -74,8 +128,8 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <template v-if="plugin.conf?.length > 0">
 | 
					    <template v-if="plugin.conf?.length > 0">
 | 
				
			||||||
      <template
 | 
					      <template
 | 
				
			||||||
        v-for="(field, key) in plugin.conf"
 | 
					        v-for="field in plugin.conf"
 | 
				
			||||||
        :key="key"
 | 
					        :key="field.name"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          v-if="field.type === 'text'"
 | 
					          v-if="field.type === 'text'"
 | 
				
			||||||
| 
						 | 
					@ -89,7 +143,7 @@
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
          <sanitized-html
 | 
					          <sanitized-html
 | 
				
			||||||
            v-if="field.help"
 | 
					            v-if="field.help"
 | 
				
			||||||
            :html="markdown.makeHtml(field.help)"
 | 
					            :html="useMarkdownRaw(field.help)"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
| 
						 | 
					@ -105,7 +159,7 @@
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <sanitized-html
 | 
					          <sanitized-html
 | 
				
			||||||
            v-if="field.help"
 | 
					            v-if="field.help"
 | 
				
			||||||
            :html="markdown.makeHtml(field.help)"
 | 
					            :html="useMarkdownRaw(field.help)"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
| 
						 | 
					@ -120,7 +174,7 @@
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
          <sanitized-html
 | 
					          <sanitized-html
 | 
				
			||||||
            v-if="field.help"
 | 
					            v-if="field.help"
 | 
				
			||||||
            :html="markdown.makeHtml(field.help)"
 | 
					            :html="useMarkdownRaw(field.help)"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
| 
						 | 
					@ -135,7 +189,7 @@
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
          <sanitized-html
 | 
					          <sanitized-html
 | 
				
			||||||
            v-if="field.help"
 | 
					            v-if="field.help"
 | 
				
			||||||
            :html="markdown.makeHtml(field.help)"
 | 
					            :html="useMarkdownRaw(field.help)"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
| 
						 | 
					@ -150,7 +204,6 @@
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
    <button
 | 
					    <button
 | 
				
			||||||
      v-if="plugin.source"
 | 
					      v-if="plugin.source"
 | 
				
			||||||
      type="scan"
 | 
					 | 
				
			||||||
      :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
 | 
					      :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
 | 
				
			||||||
      @click.prevent="submitAndScan"
 | 
					      @click.prevent="submitAndScan"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
| 
						 | 
					@ -161,54 +214,3 @@
 | 
				
			||||||
    <div class="ui clearing hidden divider" />
 | 
					    <div class="ui clearing hidden divider" />
 | 
				
			||||||
  </form>
 | 
					  </form>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import axios from 'axios'
 | 
					 | 
				
			||||||
import { clone } from 'lodash-es'
 | 
					 | 
				
			||||||
import showdown from 'showdown'
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    plugin: { type: Object, required: true },
 | 
					 | 
				
			||||||
    libraries: { type: Array, required: true }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      markdown: new showdown.Converter(),
 | 
					 | 
				
			||||||
      isLoading: false,
 | 
					 | 
				
			||||||
      enabled: this.plugin.enabled,
 | 
					 | 
				
			||||||
      values: clone(this.plugin.values || {}),
 | 
					 | 
				
			||||||
      errors: []
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    async submit () {
 | 
					 | 
				
			||||||
      this.isLoading = true
 | 
					 | 
				
			||||||
      this.errors = []
 | 
					 | 
				
			||||||
      const url = `plugins/${this.plugin.name}`
 | 
					 | 
				
			||||||
      const enableUrl = this.enabled ? `${url}/enable` : `${url}/disable`
 | 
					 | 
				
			||||||
      await axios.post(enableUrl)
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await axios.post(url, this.values)
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        this.errors = e.backendErrors
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.isLoading = false
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    async scan () {
 | 
					 | 
				
			||||||
      this.isLoading = true
 | 
					 | 
				
			||||||
      this.errors = []
 | 
					 | 
				
			||||||
      const url = `plugins/${this.plugin.name}/scan`
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await axios.post(url, this.values)
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        this.errors = e.backendErrors
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.isLoading = false
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    async submitAndScan () {
 | 
					 | 
				
			||||||
      await this.submit()
 | 
					 | 
				
			||||||
      await this.scan()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,82 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					import { useVModel, watchDebounced, useTextareaAutosize, syncRef } from '@vueuse/core'
 | 
				
			||||||
 | 
					import { ref, computed, watchEffect, onMounted, nextTick } from 'vue'
 | 
				
			||||||
 | 
					import { useGettext } from 'vue3-gettext'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Emits {
 | 
				
			||||||
 | 
					  (e: 'update:modelValue', value: string): void
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  modelValue: string
 | 
				
			||||||
 | 
					  placeholder?: string
 | 
				
			||||||
 | 
					  autofocus?: boolean
 | 
				
			||||||
 | 
					  permissive?: boolean
 | 
				
			||||||
 | 
					  required?: boolean
 | 
				
			||||||
 | 
					  charLimit?: number
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<Emits>()
 | 
				
			||||||
 | 
					const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
 | 
					  placeholder: undefined,
 | 
				
			||||||
 | 
					  autofocus: false,
 | 
				
			||||||
 | 
					  charLimit: 5000,
 | 
				
			||||||
 | 
					  permissive: false,
 | 
				
			||||||
 | 
					  required: false
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { $pgettext } = useGettext()
 | 
				
			||||||
 | 
					const { textarea, input } = useTextareaAutosize()
 | 
				
			||||||
 | 
					const value = useVModel(props, 'modelValue', emit)
 | 
				
			||||||
 | 
					syncRef(value, input)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isPreviewing = ref(false)
 | 
				
			||||||
 | 
					const preview = ref()
 | 
				
			||||||
 | 
					const isLoadingPreview = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const labels = computed(() => ({
 | 
				
			||||||
 | 
					  placeholder: props.placeholder ?? $pgettext('*/Form/Placeholder', 'Write a few words here…')
 | 
				
			||||||
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const remainingChars = computed(() => props.charLimit - props.modelValue.length)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loadPreview = async () => {
 | 
				
			||||||
 | 
					  isLoadingPreview.value = true
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await axios.post('text-preview/', { text: value.value, permissive: props.permissive })
 | 
				
			||||||
 | 
					    preview.value = response.data.rendered
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error(error)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  isLoadingPreview.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watchDebounced(value, async () => {
 | 
				
			||||||
 | 
					  await loadPreview()
 | 
				
			||||||
 | 
					}, { immediate: true, debounce: 500 })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watchEffect(async () => {
 | 
				
			||||||
 | 
					  if (isPreviewing.value) {
 | 
				
			||||||
 | 
					    if (value.value && !preview.value && !isLoadingPreview.value) {
 | 
				
			||||||
 | 
					      await loadPreview()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await nextTick()
 | 
				
			||||||
 | 
					  textarea.value.focus()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(async () => {
 | 
				
			||||||
 | 
					  if (props.autofocus) {
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    textarea.value.focus()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="content-form ui segments">
 | 
					  <div class="content-form ui segments">
 | 
				
			||||||
    <div class="ui segment">
 | 
					    <div class="ui segment">
 | 
				
			||||||
| 
						 | 
					@ -31,7 +110,7 @@
 | 
				
			||||||
            <div class="line" />
 | 
					            <div class="line" />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <p v-else-if="preview === null">
 | 
					        <p v-else-if="!preview">
 | 
				
			||||||
          <translate translate-context="*/Form/Paragraph">
 | 
					          <translate translate-context="*/Form/Paragraph">
 | 
				
			||||||
            Nothing to preview.
 | 
					            Nothing to preview.
 | 
				
			||||||
          </translate>
 | 
					          </translate>
 | 
				
			||||||
| 
						 | 
					@ -44,13 +123,10 @@
 | 
				
			||||||
      <template v-else>
 | 
					      <template v-else>
 | 
				
			||||||
        <div class="ui transparent input">
 | 
					        <div class="ui transparent input">
 | 
				
			||||||
          <textarea
 | 
					          <textarea
 | 
				
			||||||
            :id="fieldId"
 | 
					 | 
				
			||||||
            ref="textarea"
 | 
					            ref="textarea"
 | 
				
			||||||
            v-model="newValue"
 | 
					            v-model="value"
 | 
				
			||||||
            :name="fieldId"
 | 
					            :required="required"
 | 
				
			||||||
            :rows="rows"
 | 
					            :placeholder="labels.placeholder"
 | 
				
			||||||
            :required="required || null"
 | 
					 | 
				
			||||||
            :placeholder="placeholder || labels.placeholder"
 | 
					 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="ui very small hidden divider" />
 | 
					        <div class="ui very small hidden divider" />
 | 
				
			||||||
| 
						 | 
					@ -71,82 +147,3 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import axios from 'axios'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    modelValue: { type: String, default: '' },
 | 
					 | 
				
			||||||
    fieldId: { type: String, default: 'change-content' },
 | 
					 | 
				
			||||||
    placeholder: { type: String, default: null },
 | 
					 | 
				
			||||||
    autofocus: { type: Boolean, default: false },
 | 
					 | 
				
			||||||
    charLimit: { type: Number, default: 5000, required: false },
 | 
					 | 
				
			||||||
    rows: { type: Number, default: 5, required: false },
 | 
					 | 
				
			||||||
    permissive: { type: Boolean, default: false },
 | 
					 | 
				
			||||||
    required: { type: Boolean, default: false }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      isPreviewing: false,
 | 
					 | 
				
			||||||
      preview: null,
 | 
					 | 
				
			||||||
      newValue: this.modelValue,
 | 
					 | 
				
			||||||
      isLoadingPreview: false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    labels () {
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        placeholder: this.$pgettext('*/Form/Placeholder', 'Write a few words here…')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    remainingChars () {
 | 
					 | 
				
			||||||
      return this.charLimit - (this.modelValue || '').length
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  watch: {
 | 
					 | 
				
			||||||
    newValue (v) {
 | 
					 | 
				
			||||||
      this.preview = null
 | 
					 | 
				
			||||||
      this.$emit('update:modelValue', v)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    modelValue: {
 | 
					 | 
				
			||||||
      async handler (v) {
 | 
					 | 
				
			||||||
        this.preview = null
 | 
					 | 
				
			||||||
        this.newValue = v
 | 
					 | 
				
			||||||
        if (this.isPreviewing) {
 | 
					 | 
				
			||||||
          await this.loadPreview()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      immediate: true
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    async isPreviewing (v) {
 | 
					 | 
				
			||||||
      if (v && !!this.modelValue && this.preview === null && !this.isLoadingPreview) {
 | 
					 | 
				
			||||||
        await this.loadPreview()
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (!v) {
 | 
					 | 
				
			||||||
        await this.$nextTick()
 | 
					 | 
				
			||||||
        this.$refs.textarea.focus()
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  mounted () {
 | 
					 | 
				
			||||||
    if (this.autofocus) {
 | 
					 | 
				
			||||||
      this.$nextTick(() => {
 | 
					 | 
				
			||||||
        this.$refs.textarea.focus()
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    async loadPreview () {
 | 
					 | 
				
			||||||
      this.isLoadingPreview = true
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const response = await axios.post('text-preview/', { text: this.newValue, permissive: this.permissive })
 | 
					 | 
				
			||||||
        this.preview = response.data.rendered
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.error(error)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.isLoadingPreview = false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,17 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { InstancePolicy } from '~/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import useMarkdown from '~/composables/useMarkdown'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  object: InstancePolicy
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<Props>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const summary = useMarkdown(() => props.object.summary)
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div>
 | 
					  <div>
 | 
				
			||||||
    <slot />
 | 
					    <slot />
 | 
				
			||||||
| 
						 | 
					@ -64,10 +78,10 @@
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div v-if="markdown && object.summary">
 | 
					    <div v-if="summary">
 | 
				
			||||||
      <div class="ui hidden divider" />
 | 
					      <div class="ui hidden divider" />
 | 
				
			||||||
      <p><strong><translate translate-context="Content/Moderation/*/Noun">Reason</translate></strong></p>
 | 
					      <p><strong><translate translate-context="Content/Moderation/*/Noun">Reason</translate></strong></p>
 | 
				
			||||||
      <sanitized-html :html="markdown.makeHtml(object.summary)" />
 | 
					      <sanitized-html :html="summary" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="ui hidden divider" />
 | 
					    <div class="ui hidden divider" />
 | 
				
			||||||
    <button
 | 
					    <button
 | 
				
			||||||
| 
						 | 
					@ -81,21 +95,3 @@
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import showdown from 'showdown'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    object: { type: Object, default: null }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      markdown: null
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  created () {
 | 
					 | 
				
			||||||
    this.markdown = showdown.Converter({ simplifiedAutoLink: true, openLinksInNewWindow: true })
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,50 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { Note, BackendError } from '~/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					import { ref, computed } from 'vue'
 | 
				
			||||||
 | 
					import { useGettext } from 'vue3-gettext'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Emits {
 | 
				
			||||||
 | 
					  (e: 'created', note: Note): void
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  target: Note
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<Emits>()
 | 
				
			||||||
 | 
					const props = defineProps<Props>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { $pgettext } = useGettext()
 | 
				
			||||||
 | 
					const labels = computed(() => ({
 | 
				
			||||||
 | 
					  summaryPlaceholder: $pgettext('Content/Moderation/Placeholder', 'Describe what actions have been taken, or any other related updates…')
 | 
				
			||||||
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const summary = ref('')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isLoading = ref(false)
 | 
				
			||||||
 | 
					const errors = ref([] as string[])
 | 
				
			||||||
 | 
					const submit = async () => {
 | 
				
			||||||
 | 
					  isLoading.value = true
 | 
				
			||||||
 | 
					  errors.value = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await axios.post('manage/moderation/notes/', {
 | 
				
			||||||
 | 
					      target: props.target,
 | 
				
			||||||
 | 
					      summary: summary.value
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    emit('created', response.data)
 | 
				
			||||||
 | 
					    summary.value = ''
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    errors.value = (error as BackendError).backendErrors
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isLoading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <form
 | 
					  <form
 | 
				
			||||||
    class="ui form"
 | 
					    class="ui form"
 | 
				
			||||||
| 
						 | 
					@ -34,7 +81,7 @@
 | 
				
			||||||
    <button
 | 
					    <button
 | 
				
			||||||
      :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
 | 
					      :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
 | 
				
			||||||
      type="submit"
 | 
					      type="submit"
 | 
				
			||||||
      :disabled="isLoading || null"
 | 
					      :disabled="isLoading"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <translate translate-context="Content/Moderation/Button.Label/Verb">
 | 
					      <translate translate-context="Content/Moderation/Button.Label/Verb">
 | 
				
			||||||
        Add note
 | 
					        Add note
 | 
				
			||||||
| 
						 | 
					@ -42,48 +89,3 @@
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
  </form>
 | 
					  </form>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import axios from 'axios'
 | 
					 | 
				
			||||||
import showdown from 'showdown'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    target: { type: Object, required: true }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      markdown: new showdown.Converter(),
 | 
					 | 
				
			||||||
      isLoading: false,
 | 
					 | 
				
			||||||
      summary: '',
 | 
					 | 
				
			||||||
      errors: []
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    labels () {
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        summaryPlaceholder: this.$pgettext('Content/Moderation/Placeholder', 'Describe what actions have been taken, or any other related updates…')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    submit () {
 | 
					 | 
				
			||||||
      const self = this
 | 
					 | 
				
			||||||
      this.isLoading = true
 | 
					 | 
				
			||||||
      const payload = {
 | 
					 | 
				
			||||||
        target: this.target,
 | 
					 | 
				
			||||||
        summary: this.summary
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.errors = []
 | 
					 | 
				
			||||||
      axios.post('manage/moderation/notes/', payload).then((response) => {
 | 
					 | 
				
			||||||
        self.$emit('created', response.data)
 | 
					 | 
				
			||||||
        self.summary = ''
 | 
					 | 
				
			||||||
        self.isLoading = false
 | 
					 | 
				
			||||||
      }, error => {
 | 
					 | 
				
			||||||
        self.errors = error.backendErrors
 | 
					 | 
				
			||||||
        self.isLoading = false
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
import type { Note } from '~/types'
 | 
					import type { Note } from '~/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import axios from 'axios'
 | 
					import axios from 'axios'
 | 
				
			||||||
import showdown from 'showdown'
 | 
					import { useMarkdownRaw } from '~/composables/useMarkdown'
 | 
				
			||||||
import { ref } from 'vue'
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
| 
						 | 
					@ -11,8 +11,6 @@ interface Props {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineProps<Props>()
 | 
					defineProps<Props>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const markdown = new showdown.Converter()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const emit = defineEmits(['deleted'])
 | 
					const emit = defineEmits(['deleted'])
 | 
				
			||||||
const isLoading = ref(false)
 | 
					const isLoading = ref(false)
 | 
				
			||||||
const remove = async (note: Note) => {
 | 
					const remove = async (note: Note) => {
 | 
				
			||||||
| 
						 | 
					@ -51,7 +49,7 @@ const remove = async (note: Note) => {
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="extra text">
 | 
					        <div class="extra text">
 | 
				
			||||||
          <expandable-div :content="note.summary">
 | 
					          <expandable-div :content="note.summary">
 | 
				
			||||||
            <sanitized-html :html="markdown.makeHtml(note.summary)" />
 | 
					            <sanitized-html :html="useMarkdownRaw(note.summary ?? '')" />
 | 
				
			||||||
          </expandable-div>
 | 
					          </expandable-div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="meta">
 | 
					        <div class="meta">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,138 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { Report } from '~/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					import useReportConfigs from '~/composables/moderation/useReportConfigs'
 | 
				
			||||||
 | 
					import useMarkdown from '~/composables/useMarkdown'
 | 
				
			||||||
 | 
					import { ref, computed, reactive } from 'vue'
 | 
				
			||||||
 | 
					import { useGettext } from 'vue3-gettext'
 | 
				
			||||||
 | 
					import { useStore } from '~/store'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import NoteForm from '~/components/manage/moderation/NoteForm.vue'
 | 
				
			||||||
 | 
					import NotesThread from '~/components/manage/moderation/NotesThread.vue'
 | 
				
			||||||
 | 
					import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
 | 
				
			||||||
 | 
					import InstancePolicyModal from '~/components/manage/moderation/InstancePolicyModal.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Emits {
 | 
				
			||||||
 | 
					  (e: 'updated', updating: { type: string }): void
 | 
				
			||||||
 | 
					  (e: 'handled', isHandled: boolean): void
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  initObj: Report
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<Emits>()
 | 
				
			||||||
 | 
					const props = defineProps<Props>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const configs = useReportConfigs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const obj = ref(props.initObj)
 | 
				
			||||||
 | 
					const summary = useMarkdown(() => obj.value.summary ?? '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const target = computed(() => obj.value.target
 | 
				
			||||||
 | 
					  ? obj.value.target
 | 
				
			||||||
 | 
					  : obj.value.target_state._target
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const targetFields = computed(() => {
 | 
				
			||||||
 | 
					  if (!target.value) {
 | 
				
			||||||
 | 
					    return []
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const payload = obj.value.target_state
 | 
				
			||||||
 | 
					  const fields = configs[target.value.type].moderatedFields
 | 
				
			||||||
 | 
					  return fields.map((fieldConfig) => {
 | 
				
			||||||
 | 
					    const getValueRepr = fieldConfig.getValueRepr ?? (i => i)
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: fieldConfig.id,
 | 
				
			||||||
 | 
					      label: fieldConfig.label,
 | 
				
			||||||
 | 
					      value: payload[fieldConfig.id],
 | 
				
			||||||
 | 
					      repr: getValueRepr(payload[fieldConfig.id]) ?? ''
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { $pgettext } = useGettext()
 | 
				
			||||||
 | 
					const actions = computed(() => {
 | 
				
			||||||
 | 
					  if (!target.value) {
 | 
				
			||||||
 | 
					    return []
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const typeConfig = configs[target.value.type]
 | 
				
			||||||
 | 
					  const deleteUrl = typeConfig.getDeleteUrl?.(target.value)
 | 
				
			||||||
 | 
					  return deleteUrl
 | 
				
			||||||
 | 
					    ? [{
 | 
				
			||||||
 | 
					        label: $pgettext('Content/Moderation/Button/Verb', 'Delete reported object'),
 | 
				
			||||||
 | 
					        modalHeader: $pgettext('Content/Moderation/Popup/Header', 'Delete reported object?'),
 | 
				
			||||||
 | 
					        modalContent: $pgettext('Content/Moderation/Popup,Paragraph', 'This will delete the object associated with this report and mark the report as resolved. The deletion is irreversible.'),
 | 
				
			||||||
 | 
					        modalConfirmLabel: $pgettext('*/*/*/Verb', 'Delete'),
 | 
				
			||||||
 | 
					        icon: 'x',
 | 
				
			||||||
 | 
					        iconColor: 'danger',
 | 
				
			||||||
 | 
					        show: (obj: Report) => { return !!obj.target },
 | 
				
			||||||
 | 
					        dangerous: true,
 | 
				
			||||||
 | 
					        handler: async () => {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            await axios.delete(deleteUrl)
 | 
				
			||||||
 | 
					            console.log('Target deleted')
 | 
				
			||||||
 | 
					            obj.value.target = undefined
 | 
				
			||||||
 | 
					            resolveReport(true)
 | 
				
			||||||
 | 
					          } catch (error) {
 | 
				
			||||||
 | 
					            console.log('Error while deleting target', error)
 | 
				
			||||||
 | 
					            // TODO (wvffle): Handle error
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }]
 | 
				
			||||||
 | 
					    : []
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isLoading = ref(false)
 | 
				
			||||||
 | 
					const updating = reactive({ type: false })
 | 
				
			||||||
 | 
					const update = async (type: string) => {
 | 
				
			||||||
 | 
					  isLoading.value = true
 | 
				
			||||||
 | 
					  updating.type = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    await axios.patch(`manage/moderation/reports/${obj.value.uuid}/`, { type })
 | 
				
			||||||
 | 
					    emit('updated', { type })
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    // TODO (wvffle): Handle error
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updating.type = false
 | 
				
			||||||
 | 
					  isLoading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const store = useStore()
 | 
				
			||||||
 | 
					const isCollapsed = ref(false)
 | 
				
			||||||
 | 
					const resolveReport = async (isHandled: boolean) => {
 | 
				
			||||||
 | 
					  isLoading.value = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    await axios.patch(`manage/moderation/reports/${obj.value.uuid}/`, { is_handled: isHandled })
 | 
				
			||||||
 | 
					    emit('handled', isHandled)
 | 
				
			||||||
 | 
					    obj.value.is_handled = isHandled
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isHandled) {
 | 
				
			||||||
 | 
					      isCollapsed.value = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    store.commit('ui/incrementNotifications', {
 | 
				
			||||||
 | 
					      type: 'pendingReviewReports',
 | 
				
			||||||
 | 
					      count: isHandled ? -1 : 1
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    // TODO (wvffle): Handle error
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isLoading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleRemovedNote = (uuid: string) => {
 | 
				
			||||||
 | 
					  obj.value.notes = obj.value.notes.filter((note) => note.uuid !== uuid)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="ui fluid report card">
 | 
					  <div class="ui fluid report card">
 | 
				
			||||||
    <div class="content">
 | 
					    <div class="content">
 | 
				
			||||||
| 
						 | 
					@ -48,7 +183,7 @@
 | 
				
			||||||
                  <td>
 | 
					                  <td>
 | 
				
			||||||
                    <report-category-dropdown
 | 
					                    <report-category-dropdown
 | 
				
			||||||
                      v-model="obj.type"
 | 
					                      v-model="obj.type"
 | 
				
			||||||
                      @update:model-value="update({ type: $event })"
 | 
					                      @update:model-value="update($event)"
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                       
 | 
					                       
 | 
				
			||||||
                      <action-feedback :is-loading="updating.type" />
 | 
					                      <action-feedback :is-loading="updating.type" />
 | 
				
			||||||
| 
						 | 
					@ -163,11 +298,11 @@
 | 
				
			||||||
            </translate>
 | 
					            </translate>
 | 
				
			||||||
          </h3>
 | 
					          </h3>
 | 
				
			||||||
          <expandable-div
 | 
					          <expandable-div
 | 
				
			||||||
            v-if="obj.summary"
 | 
					            v-if="summary"
 | 
				
			||||||
            class="summary"
 | 
					            class="summary"
 | 
				
			||||||
            :content="obj.summary"
 | 
					            :content="obj.summary"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <sanitized-html :html="markdown.makeHtml(obj.summary)" />
 | 
					            <sanitized-html :html="summary" />
 | 
				
			||||||
          </expandable-div>
 | 
					          </expandable-div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <aside class="column">
 | 
					        <aside class="column">
 | 
				
			||||||
| 
						 | 
					@ -342,7 +477,7 @@
 | 
				
			||||||
            <button
 | 
					            <button
 | 
				
			||||||
              v-if="obj.is_handled === false"
 | 
					              v-if="obj.is_handled === false"
 | 
				
			||||||
              :class="['ui', {loading: isLoading}, 'button']"
 | 
					              :class="['ui', {loading: isLoading}, 'button']"
 | 
				
			||||||
              @click="resolve(true)"
 | 
					              @click="resolveReport(true)"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <i class="success check icon" /> 
 | 
					              <i class="success check icon" /> 
 | 
				
			||||||
              <translate translate-context="Content/*/Button.Label/Verb">
 | 
					              <translate translate-context="Content/*/Button.Label/Verb">
 | 
				
			||||||
| 
						 | 
					@ -352,7 +487,7 @@
 | 
				
			||||||
            <button
 | 
					            <button
 | 
				
			||||||
              v-if="obj.is_handled === true"
 | 
					              v-if="obj.is_handled === true"
 | 
				
			||||||
              :class="['ui', {loading: isLoading}, 'button']"
 | 
					              :class="['ui', {loading: isLoading}, 'button']"
 | 
				
			||||||
              @click="resolve(false)"
 | 
					              @click="resolveReport(false)"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <i class="warning redo icon" /> 
 | 
					              <i class="warning redo icon" /> 
 | 
				
			||||||
              <translate translate-context="Content/*/Button.Label">
 | 
					              <translate translate-context="Content/*/Button.Label">
 | 
				
			||||||
| 
						 | 
					@ -389,174 +524,3 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import axios from 'axios'
 | 
					 | 
				
			||||||
import NoteForm from '~/components/manage/moderation/NoteForm.vue'
 | 
					 | 
				
			||||||
import NotesThread from '~/components/manage/moderation/NotesThread.vue'
 | 
					 | 
				
			||||||
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
 | 
					 | 
				
			||||||
import InstancePolicyModal from '~/components/manage/moderation/InstancePolicyModal.vue'
 | 
					 | 
				
			||||||
import useReportConfigs from '~/composables/moderation/useReportConfigs.ts'
 | 
					 | 
				
			||||||
import { setUpdate } from '~/utils'
 | 
					 | 
				
			||||||
import showdown from 'showdown'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function castValue (value) {
 | 
					 | 
				
			||||||
  if (value === null || value === undefined) {
 | 
					 | 
				
			||||||
    return ''
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return String(value)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  components: {
 | 
					 | 
				
			||||||
    NoteForm,
 | 
					 | 
				
			||||||
    NotesThread,
 | 
					 | 
				
			||||||
    ReportCategoryDropdown,
 | 
					 | 
				
			||||||
    InstancePolicyModal
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    initObj: { type: Object, required: true },
 | 
					 | 
				
			||||||
    currentState: { type: String, required: false, default: '' }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  setup () {
 | 
					 | 
				
			||||||
    return { configs: useReportConfigs() }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      obj: this.initObj,
 | 
					 | 
				
			||||||
      markdown: new showdown.Converter(),
 | 
					 | 
				
			||||||
      isLoading: false,
 | 
					 | 
				
			||||||
      isCollapsed: false,
 | 
					 | 
				
			||||||
      updating: {
 | 
					 | 
				
			||||||
        type: false
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    previousState () {
 | 
					 | 
				
			||||||
      if (this.obj.is_applied) {
 | 
					 | 
				
			||||||
        // mutation was applied, we use the previous state that is stored
 | 
					 | 
				
			||||||
        // on the mutation itself
 | 
					 | 
				
			||||||
        return this.obj.previous_state
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      // mutation is not applied yet, so we use the current state that was
 | 
					 | 
				
			||||||
      // passed to the component, if any
 | 
					 | 
				
			||||||
      return this.currentState
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    detailUrl () {
 | 
					 | 
				
			||||||
      if (!this.target) {
 | 
					 | 
				
			||||||
        return ''
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      let namespace
 | 
					 | 
				
			||||||
      const id = this.target.id
 | 
					 | 
				
			||||||
      if (this.target.type === 'track') {
 | 
					 | 
				
			||||||
        namespace = 'library.tracks.edit.detail'
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (this.target.type === 'album') {
 | 
					 | 
				
			||||||
        namespace = 'library.albums.edit.detail'
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (this.target.type === 'artist') {
 | 
					 | 
				
			||||||
        namespace = 'library.artists.edit.detail'
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return this.$router.resolve({ name: namespace, params: { id, editId: this.obj.uuid } }).href
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    targetFields () {
 | 
					 | 
				
			||||||
      if (!this.target) {
 | 
					 | 
				
			||||||
        return []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const payload = this.obj.target_state
 | 
					 | 
				
			||||||
      const fields = this.configs[this.target.type].moderatedFields
 | 
					 | 
				
			||||||
      return fields.map((fieldConfig) => {
 | 
					 | 
				
			||||||
        const getValueRepr = fieldConfig.getValueRepr ?? (i => i)
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
          id: fieldConfig.id,
 | 
					 | 
				
			||||||
          label: fieldConfig.label,
 | 
					 | 
				
			||||||
          value: payload[fieldConfig.id],
 | 
					 | 
				
			||||||
          repr: castValue(getValueRepr(payload[fieldConfig.id]))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    target () {
 | 
					 | 
				
			||||||
      if (this.obj.target) {
 | 
					 | 
				
			||||||
        return this.obj.target
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        return this.obj.target_state._target
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    actions () {
 | 
					 | 
				
			||||||
      if (!this.target) {
 | 
					 | 
				
			||||||
        return []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const self = this
 | 
					 | 
				
			||||||
      const actions = []
 | 
					 | 
				
			||||||
      const typeConfig = this.configs[this.target.type]
 | 
					 | 
				
			||||||
      if (typeConfig.getDeleteUrl) {
 | 
					 | 
				
			||||||
        const deleteUrl = typeConfig.getDeleteUrl(this.target)
 | 
					 | 
				
			||||||
        actions.push({
 | 
					 | 
				
			||||||
          label: this.$pgettext('Content/Moderation/Button/Verb', 'Delete reported object'),
 | 
					 | 
				
			||||||
          modalHeader: this.$pgettext('Content/Moderation/Popup/Header', 'Delete reported object?'),
 | 
					 | 
				
			||||||
          modalContent: this.$pgettext('Content/Moderation/Popup,Paragraph', 'This will delete the object associated with this report and mark the report as resolved. The deletion is irreversible.'),
 | 
					 | 
				
			||||||
          modalConfirmLabel: this.$pgettext('*/*/*/Verb', 'Delete'),
 | 
					 | 
				
			||||||
          icon: 'x',
 | 
					 | 
				
			||||||
          iconColor: 'danger',
 | 
					 | 
				
			||||||
          show: (obj) => { return !!obj.target },
 | 
					 | 
				
			||||||
          dangerous: true,
 | 
					 | 
				
			||||||
          handler: () => {
 | 
					 | 
				
			||||||
            axios.delete(deleteUrl).then((response) => {
 | 
					 | 
				
			||||||
              console.log('Target deleted')
 | 
					 | 
				
			||||||
              self.obj.target = null
 | 
					 | 
				
			||||||
              self.resolve(true)
 | 
					 | 
				
			||||||
            }, () => {
 | 
					 | 
				
			||||||
              console.log('Error while deleting target')
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return actions
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    update (payload) {
 | 
					 | 
				
			||||||
      const url = `manage/moderation/reports/${this.obj.uuid}/`
 | 
					 | 
				
			||||||
      const self = this
 | 
					 | 
				
			||||||
      this.isLoading = true
 | 
					 | 
				
			||||||
      setUpdate(payload, this.updating, true)
 | 
					 | 
				
			||||||
      axios.patch(url, payload).then((response) => {
 | 
					 | 
				
			||||||
        self.$emit('updated', payload)
 | 
					 | 
				
			||||||
        Object.assign(self.obj, payload)
 | 
					 | 
				
			||||||
        self.isLoading = false
 | 
					 | 
				
			||||||
        setUpdate(payload, self.updating, false)
 | 
					 | 
				
			||||||
      }, () => {
 | 
					 | 
				
			||||||
        self.isLoading = false
 | 
					 | 
				
			||||||
        setUpdate(payload, self.updating, false)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    resolve (v) {
 | 
					 | 
				
			||||||
      const url = `manage/moderation/reports/${this.obj.uuid}/`
 | 
					 | 
				
			||||||
      const self = this
 | 
					 | 
				
			||||||
      this.isLoading = true
 | 
					 | 
				
			||||||
      axios.patch(url, { is_handled: v }).then((response) => {
 | 
					 | 
				
			||||||
        self.$emit('handled', v)
 | 
					 | 
				
			||||||
        self.isLoading = false
 | 
					 | 
				
			||||||
        self.obj.is_handled = v
 | 
					 | 
				
			||||||
        let increment
 | 
					 | 
				
			||||||
        if (v) {
 | 
					 | 
				
			||||||
          self.isCollapsed = true
 | 
					 | 
				
			||||||
          increment = -1
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          increment = 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        self.$store.commit('ui/incrementNotifications', { count: increment, type: 'pendingReviewReports' })
 | 
					 | 
				
			||||||
      }, () => {
 | 
					 | 
				
			||||||
        self.isLoading = false
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    handleRemovedNote (uuid) {
 | 
					 | 
				
			||||||
      this.obj.notes = this.obj.notes.filter((note) => {
 | 
					 | 
				
			||||||
        return note.uuid !== uuid
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,66 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { UserRequest, UserRequestStatus } from '~/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					import { useStore } from '~/store'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import NoteForm from '~/components/manage/moderation/NoteForm.vue'
 | 
				
			||||||
 | 
					import NotesThread from '~/components/manage/moderation/NotesThread.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Emits {
 | 
				
			||||||
 | 
					  (e: 'handled', status: UserRequestStatus): void
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  initObj: UserRequest
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<Emits>()
 | 
				
			||||||
 | 
					const props = defineProps<Props>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const store = useStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const obj = ref(props.initObj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isCollapsed = ref(false)
 | 
				
			||||||
 | 
					const isLoading = ref(false)
 | 
				
			||||||
 | 
					const approve = async (isApproved: boolean) => {
 | 
				
			||||||
 | 
					  isLoading.value = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const status = isApproved
 | 
				
			||||||
 | 
					      ? 'approved'
 | 
				
			||||||
 | 
					      : 'refused'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await axios.patch(`manage/moderation/requests/${obj.value.uuid}/`, {
 | 
				
			||||||
 | 
					      status
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    emit('handled', status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isApproved) {
 | 
				
			||||||
 | 
					      isCollapsed.value = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    store.commit('ui/incrementNotifications', {
 | 
				
			||||||
 | 
					      type: 'pendingReviewRequests',
 | 
				
			||||||
 | 
					      count: -1
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    // TODO (wvffle): Handle error
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isLoading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleRemovedNote = (uuid: string) => {
 | 
				
			||||||
 | 
					  obj.value.notes = obj.value.notes.filter((note) => note.uuid !== uuid)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isArray = Array.isArray
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="ui fluid user-request card">
 | 
					  <div class="ui fluid user-request card">
 | 
				
			||||||
    <div class="content">
 | 
					    <div class="content">
 | 
				
			||||||
| 
						 | 
					@ -157,12 +220,12 @@
 | 
				
			||||||
          <template v-if="obj.metadata">
 | 
					          <template v-if="obj.metadata">
 | 
				
			||||||
            <div class="ui hidden divider" />
 | 
					            <div class="ui hidden divider" />
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              v-for="k in Object.keys(obj.metadata)"
 | 
					              v-for="(value, key) in obj.metadata"
 | 
				
			||||||
              :key="k"
 | 
					              :key="key"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <h4>{{ k }}</h4>
 | 
					              <h4>{{ key }}</h4>
 | 
				
			||||||
              <p v-if="obj.metadata[k] && obj.metadata[k].length">
 | 
					              <p v-if="isArray(value)">
 | 
				
			||||||
                {{ obj.metadata[k] }}
 | 
					                {{ value }}
 | 
				
			||||||
              </p>
 | 
					              </p>
 | 
				
			||||||
              <translate
 | 
					              <translate
 | 
				
			||||||
                v-else
 | 
					                v-else
 | 
				
			||||||
| 
						 | 
					@ -222,52 +285,3 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import axios from 'axios'
 | 
					 | 
				
			||||||
import NoteForm from '~/components/manage/moderation/NoteForm.vue'
 | 
					 | 
				
			||||||
import NotesThread from '~/components/manage/moderation/NotesThread.vue'
 | 
					 | 
				
			||||||
import showdown from 'showdown'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  components: {
 | 
					 | 
				
			||||||
    NoteForm,
 | 
					 | 
				
			||||||
    NotesThread
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    initObj: { type: Object, required: true }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      markdown: new showdown.Converter(),
 | 
					 | 
				
			||||||
      isLoading: false,
 | 
					 | 
				
			||||||
      isCollapsed: false,
 | 
					 | 
				
			||||||
      obj: this.initObj
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    approve (v) {
 | 
					 | 
				
			||||||
      const url = `manage/moderation/requests/${this.obj.uuid}/`
 | 
					 | 
				
			||||||
      const self = this
 | 
					 | 
				
			||||||
      const newStatus = v ? 'approved' : 'refused'
 | 
					 | 
				
			||||||
      this.isLoading = true
 | 
					 | 
				
			||||||
      axios.patch(url, { status: newStatus }).then((response) => {
 | 
					 | 
				
			||||||
        self.$emit('handled', newStatus)
 | 
					 | 
				
			||||||
        self.isLoading = false
 | 
					 | 
				
			||||||
        self.obj.status = newStatus
 | 
					 | 
				
			||||||
        if (v) {
 | 
					 | 
				
			||||||
          self.isCollapsed = true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        self.$store.commit('ui/incrementNotifications', { count: -1, type: 'pendingReviewRequests' })
 | 
					 | 
				
			||||||
      }, () => {
 | 
					 | 
				
			||||||
        self.isLoading = false
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    handleRemovedNote (uuid) {
 | 
					 | 
				
			||||||
      this.obj.notes = this.obj.notes.filter((note) => {
 | 
					 | 
				
			||||||
        return note.uuid !== uuid
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					import type { EntityObjectType } from '~/types'
 | 
				
			||||||
import type { RouteLocationRaw } from 'vue-router'
 | 
					import type { RouteLocationRaw } from 'vue-router'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { gettext } from '~/init/locale'
 | 
					import { gettext } from '~/init/locale'
 | 
				
			||||||
| 
						 | 
					@ -19,7 +20,6 @@ export interface Entity {
 | 
				
			||||||
  moderatedFields: ModeratedField[]
 | 
					  moderatedFields: ModeratedField[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type EntityObjectType = 'artist' | 'album' | 'track' | 'library' | 'playlist' | 'account' | 'channel'
 | 
					 | 
				
			||||||
type Configs = Record<EntityObjectType, Entity>
 | 
					type Configs = Record<EntityObjectType, Entity>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { $pgettext } = gettext
 | 
					const { $pgettext } = gettext
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					import type { MaybeComputedRef } from '@vueuse/core'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { resolveUnref } from '@vueuse/core'
 | 
				
			||||||
 | 
					import { computed } from 'vue'
 | 
				
			||||||
 | 
					import showdown from 'showdown'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					showdown.extension('openExternalInNewTab', {
 | 
				
			||||||
 | 
					  type: 'output',
 | 
				
			||||||
 | 
					  regex: /<a.+?href.+">/g,
 | 
				
			||||||
 | 
					  replace (text: string) {
 | 
				
			||||||
 | 
					    const matches = text.match(/href="(.+)">/) ?? []
 | 
				
			||||||
 | 
					    const url = matches[1] ?? './'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ((!url.startsWith('http://') && !url.startsWith('https://')) || url.startsWith('mailto:')) {
 | 
				
			||||||
 | 
					      return text
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { hostname } = new URL(url)
 | 
				
			||||||
 | 
					    return hostname !== location.hostname
 | 
				
			||||||
 | 
					      ? text.replace(matches[0], `href="${url}" target="_blank" rel="noopener noreferrer">`)
 | 
				
			||||||
 | 
					      : text
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					showdown.extension('linkifyTags', {
 | 
				
			||||||
 | 
					  type: 'language',
 | 
				
			||||||
 | 
					  regex: /#[^\W]+/g,
 | 
				
			||||||
 | 
					  replace (text: string) {
 | 
				
			||||||
 | 
					    return `<a href="/library/tags/${text.slice(1)}">${text}</a>`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const markdown = new showdown.Converter({
 | 
				
			||||||
 | 
					  extensions: ['openExternalInNewTab', 'linkifyTags'],
 | 
				
			||||||
 | 
					  ghMentions: true,
 | 
				
			||||||
 | 
					  ghMentionsLink: '/@{u}',
 | 
				
			||||||
 | 
					  simplifiedAutoLink: true,
 | 
				
			||||||
 | 
					  openLinksInNewWindow: false,
 | 
				
			||||||
 | 
					  simpleLineBreaks: true,
 | 
				
			||||||
 | 
					  strikethrough: true,
 | 
				
			||||||
 | 
					  tables: true,
 | 
				
			||||||
 | 
					  tasklists: true,
 | 
				
			||||||
 | 
					  underline: true,
 | 
				
			||||||
 | 
					  noHeaderId: true,
 | 
				
			||||||
 | 
					  headerLevelStart: 3,
 | 
				
			||||||
 | 
					  literalMidWordUnderscores: true,
 | 
				
			||||||
 | 
					  excludeTrailingPunctuationFromURLs: true,
 | 
				
			||||||
 | 
					  encodeEmails: true,
 | 
				
			||||||
 | 
					  emoji: true
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useMarkdownRaw = (md: string) => markdown.makeHtml(md)
 | 
				
			||||||
 | 
					export const useMarkdownComputed = (md: MaybeComputedRef<string>) => computed(() => useMarkdownRaw(resolveUnref(md)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default useMarkdownComputed
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,29 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.content-form {
 | 
					.content-form {
 | 
				
			||||||
  background: var(--input-background);
 | 
					  background: var(--input-background);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .segment {
 | 
					  .segment {
 | 
				
			||||||
    background: none;
 | 
					    background: none;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .segment:first-child {
 | 
					  .segment:first-child {
 | 
				
			||||||
    min-height: 15em;
 | 
					    min-height: 15em;
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-template-rows: auto 1fr auto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    textarea {
 | 
				
			||||||
 | 
					      height: 100%;
 | 
				
			||||||
 | 
					      resize: none;
 | 
				
			||||||
 | 
					      overflow-y: hidden;
 | 
				
			||||||
 | 
					      max-height: none;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .ui.secondary.menu {
 | 
					  .ui.secondary.menu {
 | 
				
			||||||
    background: none;
 | 
					    background: none;
 | 
				
			||||||
    margin-top: -0.5em;
 | 
					    margin-top: -0.5em;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .input {
 | 
					  .input {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -242,6 +242,10 @@ export interface PendingReviewRequestsWSEvent {
 | 
				
			||||||
  pending_count: number
 | 
					  pending_count: number
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface InboxItemAddedWSEvent {
 | 
				
			||||||
 | 
					  item: Notification
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ListenWsEventObject {
 | 
					export interface ListenWsEventObject {
 | 
				
			||||||
  local_id: string
 | 
					  local_id: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -256,7 +260,7 @@ export interface ListenWSEvent {
 | 
				
			||||||
//   type: 'Listen'
 | 
					//   type: 'Listen'
 | 
				
			||||||
// }
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type WebSocketEvent = PendingReviewEditsWSEvent | PendingReviewReportsWSEvent | PendingReviewRequestsWSEvent | ListenWSEvent
 | 
					export type WebSocketEvent = PendingReviewEditsWSEvent | PendingReviewReportsWSEvent | PendingReviewRequestsWSEvent | ListenWSEvent | InboxItemAddedWSEvent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FS Browser
 | 
					// FS Browser
 | 
				
			||||||
export interface FSEntry {
 | 
					export interface FSEntry {
 | 
				
			||||||
| 
						 | 
					@ -374,7 +378,96 @@ export interface SettingsDataEntry {
 | 
				
			||||||
// Note stuff
 | 
					// Note stuff
 | 
				
			||||||
export interface Note {
 | 
					export interface Note {
 | 
				
			||||||
  uuid: string
 | 
					  uuid: string
 | 
				
			||||||
  author: Actor // TODO (wvffle): Check if is valid
 | 
					  type: 'request' | 'report'
 | 
				
			||||||
  summary: string
 | 
					  author?: Actor // TODO (wvffle): Check if is valid
 | 
				
			||||||
  creation_date: string
 | 
					  summary?: string
 | 
				
			||||||
 | 
					  creation_date?: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Instance policy stuff
 | 
				
			||||||
 | 
					export interface InstancePolicy {
 | 
				
			||||||
 | 
					  id: number
 | 
				
			||||||
 | 
					  uuid: string
 | 
				
			||||||
 | 
					  creation_date: string
 | 
				
			||||||
 | 
					  actor: Actor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  summary: string
 | 
				
			||||||
 | 
					  is_active: boolean
 | 
				
			||||||
 | 
					  block_all: boolean
 | 
				
			||||||
 | 
					  silence_activity: boolean
 | 
				
			||||||
 | 
					  silence_notifications: boolean
 | 
				
			||||||
 | 
					  reject_media: boolean
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Plugin stuff
 | 
				
			||||||
 | 
					export interface Plugin {
 | 
				
			||||||
 | 
					  name: string
 | 
				
			||||||
 | 
					  label: string
 | 
				
			||||||
 | 
					  homepage?: string
 | 
				
			||||||
 | 
					  enabled: boolean
 | 
				
			||||||
 | 
					  description?: string
 | 
				
			||||||
 | 
					  source?: string
 | 
				
			||||||
 | 
					  values?: Record<string, string>
 | 
				
			||||||
 | 
					  conf?: {
 | 
				
			||||||
 | 
					    name: string
 | 
				
			||||||
 | 
					    label: string
 | 
				
			||||||
 | 
					    type: 'text' | 'long_text' | 'url' | 'password'
 | 
				
			||||||
 | 
					    help?: string
 | 
				
			||||||
 | 
					  }[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Report stuff
 | 
				
			||||||
 | 
					export type EntityObjectType = 'artist' | 'album' | 'track' | 'library' | 'playlist' | 'account' | 'channel'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ReportTarget {
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					  type: EntityObjectType
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Report {
 | 
				
			||||||
 | 
					  uuid: string
 | 
				
			||||||
 | 
					  summary?: string
 | 
				
			||||||
 | 
					  is_applied: boolean
 | 
				
			||||||
 | 
					  is_handled: boolean
 | 
				
			||||||
 | 
					  previous_state: string
 | 
				
			||||||
 | 
					  notes: Note[]
 | 
				
			||||||
 | 
					  type: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  assigned_to?: Actor
 | 
				
			||||||
 | 
					  submitter?: Actor
 | 
				
			||||||
 | 
					  submitter_email?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  target_owner?: Actor
 | 
				
			||||||
 | 
					  target?: ReportTarget
 | 
				
			||||||
 | 
					  target_state: {
 | 
				
			||||||
 | 
					    _target: ReportTarget
 | 
				
			||||||
 | 
					    domain: string
 | 
				
			||||||
 | 
					    [k: string]: unknown
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  creation_date: string
 | 
				
			||||||
 | 
					  handled_date: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// User request stuff
 | 
				
			||||||
 | 
					export type UserRequestStatus = 'approved' | 'refused' | 'pending'
 | 
				
			||||||
 | 
					export interface UserRequest {
 | 
				
			||||||
 | 
					  uuid: string
 | 
				
			||||||
 | 
					  notes: Note[]
 | 
				
			||||||
 | 
					  status: UserRequestStatus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  assigned_to?: Actor
 | 
				
			||||||
 | 
					  submitter?: Actor
 | 
				
			||||||
 | 
					  submitter_email?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  creation_date: string
 | 
				
			||||||
 | 
					  handled_date: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  metadata: object
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Notification stuff
 | 
				
			||||||
 | 
					export interface Notification {
 | 
				
			||||||
 | 
					  id: number
 | 
				
			||||||
 | 
					  is_read: boolean
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,12 +4,6 @@ import type { Router } from 'vue-router'
 | 
				
			||||||
import type { APIErrorResponse } from '~/types'
 | 
					import type { APIErrorResponse } from '~/types'
 | 
				
			||||||
import type { RootState } from '~/store'
 | 
					import type { RootState } from '~/store'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function setUpdate (obj: object, statuses: Record<string, unknown>, value: unknown) {
 | 
					 | 
				
			||||||
  for (const key of Object.keys(obj)) {
 | 
					 | 
				
			||||||
    statuses[key] = value
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function parseAPIErrors (responseData: APIErrorResponse, parentField?: string): string[] {
 | 
					export function parseAPIErrors (responseData: APIErrorResponse, parentField?: string): string[] {
 | 
				
			||||||
  const errors = []
 | 
					  const errors = []
 | 
				
			||||||
  for (const [field, value] of Object.entries(responseData)) {
 | 
					  for (const [field, value] of Object.entries(responseData)) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,92 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { Notification, InboxItemAddedWSEvent } from '~/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					import moment from 'moment'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ref, reactive, computed, watch, markRaw } from 'vue'
 | 
				
			||||||
 | 
					import { useGettext } from 'vue3-gettext'
 | 
				
			||||||
 | 
					import { useStore } from '~/store'
 | 
				
			||||||
 | 
					import useMarkdown from '~/composables/useMarkdown'
 | 
				
			||||||
 | 
					import useWebSocketHandler from '~/composables/useWebSocketHandler'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import NotificationRow from '~/components/notifications/NotificationRow.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const store = useStore()
 | 
				
			||||||
 | 
					const supportMessage = useMarkdown(() => store.state.instance.settings.instance.support_message.value)
 | 
				
			||||||
 | 
					const { $pgettext } = useGettext()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const additionalNotifications = computed(() => store.getters['ui/additionalNotifications'])
 | 
				
			||||||
 | 
					const showInstanceSupportMessage = computed(() => store.getters['ui/showInstanceSupportMessage'])
 | 
				
			||||||
 | 
					const showFunkwhaleSupportMessage = computed(() => store.getters['ui/showFunkwhaleSupportMessage'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const labels = computed(() => ({
 | 
				
			||||||
 | 
					  title: $pgettext('*/Notifications/*', 'Notifications')
 | 
				
			||||||
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const filters = reactive({
 | 
				
			||||||
 | 
					  is_read: false
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isLoading = ref(false)
 | 
				
			||||||
 | 
					const notifications = reactive({ count: 0, results: [] as Notification[] })
 | 
				
			||||||
 | 
					const fetchData = async () => {
 | 
				
			||||||
 | 
					  isLoading.value = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await axios.get('federation/inbox/', { params: filters })
 | 
				
			||||||
 | 
					    notifications.count = response.data.count
 | 
				
			||||||
 | 
					    notifications.results = response.data.results.map(markRaw)
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    // TODO (wvffle): Handle error
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isLoading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(filters, fetchData, { immediate: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					useWebSocketHandler('inbox.item_added', (event) => {
 | 
				
			||||||
 | 
					  notifications.count += 1
 | 
				
			||||||
 | 
					  notifications.results.unshift(markRaw((event as InboxItemAddedWSEvent).item))
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const instanceSupportMessageDelay = ref(60)
 | 
				
			||||||
 | 
					const funkwhaleSupportMessageDelay = ref(60)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const setDisplayDate = async (field: string, days: number) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await axios.patch(`users/${store.state.auth.username}/`, {
 | 
				
			||||||
 | 
					      [field]: days
 | 
				
			||||||
 | 
					        ? moment().add({ days })
 | 
				
			||||||
 | 
					        : undefined
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    store.commit('auth/profilePartialUpdate', response.data)
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    // TODO (wvffle): Handle error
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const markAllAsRead = async () => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    await axios.post('federation/inbox/action/', {
 | 
				
			||||||
 | 
					      action: 'read',
 | 
				
			||||||
 | 
					      objects: 'all',
 | 
				
			||||||
 | 
					      filters: {
 | 
				
			||||||
 | 
					        is_read: false,
 | 
				
			||||||
 | 
					        before: notifications.results[0]?.id
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    store.commit('ui/notifications', { type: 'inbox', count: 0 })
 | 
				
			||||||
 | 
					    notifications.results = notifications.results.map(notification => ({ ...notification, is_read: true }))
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    // TODO (wvffle): Handle error
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <main
 | 
					  <main
 | 
				
			||||||
    v-title="labels.title"
 | 
					    v-title="labels.title"
 | 
				
			||||||
| 
						 | 
					@ -25,7 +114,7 @@
 | 
				
			||||||
                    Support this Funkwhale pod
 | 
					                    Support this Funkwhale pod
 | 
				
			||||||
                  </translate>
 | 
					                  </translate>
 | 
				
			||||||
                </h4>
 | 
					                </h4>
 | 
				
			||||||
                <sanitized-html :html="markdown.makeHtml($store.state.instance.settings.instance.support_message.value)" />
 | 
					                <sanitized-html :html="supportMessage" />
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="ui bottom attached segment">
 | 
					              <div class="ui bottom attached segment">
 | 
				
			||||||
                <form
 | 
					                <form
 | 
				
			||||||
| 
						 | 
					@ -210,104 +299,3 @@
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
  </main>
 | 
					  </main>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import { mapState, mapGetters } from 'vuex'
 | 
					 | 
				
			||||||
import axios from 'axios'
 | 
					 | 
				
			||||||
import showdown from 'showdown'
 | 
					 | 
				
			||||||
import moment from 'moment'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import NotificationRow from '~/components/notifications/NotificationRow.vue'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  components: {
 | 
					 | 
				
			||||||
    NotificationRow
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      isLoading: false,
 | 
					 | 
				
			||||||
      markdown: new showdown.Converter(),
 | 
					 | 
				
			||||||
      notifications: { count: 0, results: [] },
 | 
					 | 
				
			||||||
      instanceSupportMessageDelay: 60,
 | 
					 | 
				
			||||||
      funkwhaleSupportMessageDelay: 60,
 | 
					 | 
				
			||||||
      filters: {
 | 
					 | 
				
			||||||
        is_read: false
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    ...mapGetters({
 | 
					 | 
				
			||||||
      additionalNotifications: 'ui/additionalNotifications',
 | 
					 | 
				
			||||||
      showInstanceSupportMessage: 'ui/showInstanceSupportMessage',
 | 
					 | 
				
			||||||
      showFunkwhaleSupportMessage: 'ui/showFunkwhaleSupportMessage'
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    labels () {
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        title: this.$pgettext('*/Notifications/*', 'Notifications')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  watch: {
 | 
					 | 
				
			||||||
    'filters.is_read' () {
 | 
					 | 
				
			||||||
      this.fetch(this.filters)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  created () {
 | 
					 | 
				
			||||||
    this.fetch(this.filters)
 | 
					 | 
				
			||||||
    this.$store.commit('ui/addWebsocketEventHandler', {
 | 
					 | 
				
			||||||
      eventName: 'inbox.item_added',
 | 
					 | 
				
			||||||
      id: 'notificationPage',
 | 
					 | 
				
			||||||
      handler: this.handleNewNotification
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  unmounted () {
 | 
					 | 
				
			||||||
    this.$store.commit('ui/removeWebsocketEventHandler', {
 | 
					 | 
				
			||||||
      eventName: 'inbox.item_added',
 | 
					 | 
				
			||||||
      id: 'notificationPage'
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    handleNewNotification (event) {
 | 
					 | 
				
			||||||
      this.notifications.count += 1
 | 
					 | 
				
			||||||
      this.notifications.results.unshift(event.item)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    setDisplayDate (field, days) {
 | 
					 | 
				
			||||||
      const payload = {}
 | 
					 | 
				
			||||||
      let newDisplayDate
 | 
					 | 
				
			||||||
      if (days) {
 | 
					 | 
				
			||||||
        newDisplayDate = moment().add({ days })
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        newDisplayDate = null
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      payload[field] = newDisplayDate
 | 
					 | 
				
			||||||
      axios.patch(`users/${this.$store.state.auth.username}/`, payload).then((response) => {
 | 
					 | 
				
			||||||
        this.$store.commit('auth/profilePartialUpdate', response.data)
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    fetch (params) {
 | 
					 | 
				
			||||||
      this.isLoading = true
 | 
					 | 
				
			||||||
      axios.get('federation/inbox/', { params }).then(response => {
 | 
					 | 
				
			||||||
        this.isLoading = false
 | 
					 | 
				
			||||||
        this.notifications = response.data
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    markAllAsRead () {
 | 
					 | 
				
			||||||
      const before = this.notifications.results[0].id
 | 
					 | 
				
			||||||
      const payload = {
 | 
					 | 
				
			||||||
        action: 'read',
 | 
					 | 
				
			||||||
        objects: 'all',
 | 
					 | 
				
			||||||
        filters: {
 | 
					 | 
				
			||||||
          is_read: false,
 | 
					 | 
				
			||||||
          before
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      axios.post('federation/inbox/action/', payload).then(response => {
 | 
					 | 
				
			||||||
        this.$store.commit('ui/notifications', { type: 'inbox', count: 0 })
 | 
					 | 
				
			||||||
        this.notifications.results.forEach(n => {
 | 
					 | 
				
			||||||
          n.is_read = true
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <template v-if="object">
 | 
					    <template v-if="object">
 | 
				
			||||||
      <div class="ui vertical stripe segment">
 | 
					      <div class="ui vertical stripe segment">
 | 
				
			||||||
        <report-card :obj="object" />
 | 
					        <report-card :init-obj="object" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
  </main>
 | 
					  </main>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,6 @@
 | 
				
			||||||
// import type { HmrOptions } from 'vite'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { defineConfig } from 'vite'
 | 
					import { defineConfig } from 'vite'
 | 
				
			||||||
import Vue from '@vitejs/plugin-vue'
 | 
					import Vue from '@vitejs/plugin-vue'
 | 
				
			||||||
 | 
					import Inspector from 'vite-plugin-vue-inspector'
 | 
				
			||||||
import { VitePWA } from 'vite-plugin-pwa'
 | 
					import { VitePWA } from 'vite-plugin-pwa'
 | 
				
			||||||
import { resolve } from 'path'
 | 
					import { resolve } from 'path'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,16 +21,21 @@ export default defineConfig(() => ({
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // https://github.com/webfansplz/vite-plugin-vue-inspector
 | 
				
			||||||
 | 
					    Inspector({
 | 
				
			||||||
 | 
					      toggleComboKey: 'alt-shift-d'
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://github.com/antfu/vite-plugin-pwa
 | 
					    // https://github.com/antfu/vite-plugin-pwa
 | 
				
			||||||
    VitePWA({
 | 
					    VitePWA({
 | 
				
			||||||
      strategies: 'injectManifest',
 | 
					      strategies: 'injectManifest',
 | 
				
			||||||
      srcDir: 'src',
 | 
					      srcDir: 'src',
 | 
				
			||||||
      filename: 'serviceWorker.ts',
 | 
					      filename: 'serviceWorker.ts',
 | 
				
			||||||
 | 
					      manifestFilename: 'manifest.json',
 | 
				
			||||||
      devOptions: {
 | 
					      devOptions: {
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
        type: 'module',
 | 
					        type: 'module',
 | 
				
			||||||
        navigateFallback: 'index.html',
 | 
					        navigateFallback: 'index.html'
 | 
				
			||||||
        webManifestUrl: '/front/manifest.json'
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										900
									
								
								front/yarn.lock
								
								
								
								
							
							
						
						
									
										900
									
								
								front/yarn.lock
								
								
								
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue