Migrate a couple of components to new v-model and cleanup linting stuff
This commit is contained in:
parent
16d437be62
commit
2f80e0935f
|
@ -34,7 +34,8 @@ module.exports = {
|
|||
// and gettext for vue 2
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
|
||||
// TODO (wvffle): Enable typescript rules later
|
||||
// TODO (wvffle): Enable these rules later
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"serve": "vite preview",
|
||||
"test:unit": "jest",
|
||||
"lint": "eslint --ext .ts,.js,.vue src",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"fix-fomantic-css": "scripts/fix-fomantic-css.sh",
|
||||
"i18n-compile": "scripts/i18n-compile.sh",
|
||||
"i18n-extract": "scripts/i18n-extract.sh",
|
||||
|
@ -40,6 +41,7 @@
|
|||
"vue-gettext": "2.1.12",
|
||||
"vue-plyr": "7.0.0",
|
||||
"vue-router": "4.0.14",
|
||||
"vue-tsc": "0.34.7",
|
||||
"vue-upload-component": "2.8.22",
|
||||
"vue3-gettext": "2.2.0-alpha.1",
|
||||
"vue3-lazyload": "0.2.5-beta",
|
||||
|
@ -49,9 +51,6 @@
|
|||
"vuex-router-sync": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.12",
|
||||
"@babel/plugin-transform-runtime": "7.17.12",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/jquery": "3.5.14",
|
||||
"@types/lodash-es": "4.17.6",
|
||||
|
@ -62,9 +61,7 @@
|
|||
"@vue/eslint-config-standard": "6.1.0",
|
||||
"@vue/eslint-config-typescript": "10.0.0",
|
||||
"@vue/test-utils": "1.3.0",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-jest": "27.5.1",
|
||||
"autoprefixer": "10.4.4",
|
||||
"chai": "4.3.6",
|
||||
"easygettext": "2.17.0",
|
||||
"eslint": "8.11.0",
|
||||
|
@ -74,7 +71,6 @@
|
|||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.0.0",
|
||||
"eslint-plugin-vue": "7.20.0",
|
||||
"glob-all": "3.3.0",
|
||||
"jest-cli": "27.5.1",
|
||||
"moxios": "0.4.0",
|
||||
"sinon": "13.0.2",
|
||||
|
@ -91,27 +87,6 @@
|
|||
"resolutions": {
|
||||
"vue-plyr/plyr": "3.6.12"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"IE >= 11",
|
||||
"Firefox >= 52",
|
||||
"ChromeAndroid >= 70",
|
||||
"Chrome >= 49",
|
||||
"Safari >= 9",
|
||||
"Edge >= 16",
|
||||
"Opera >= 57",
|
||||
"OperaMini >= 57",
|
||||
"Samsung >= 7",
|
||||
"FirefoxAndroid >= 63",
|
||||
"UCAndroid >= 11",
|
||||
"iOS >= 9",
|
||||
"Android >= 4",
|
||||
"not dead"
|
||||
],
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
|
|
|
@ -154,8 +154,8 @@
|
|||
</template>
|
||||
<user-modal
|
||||
v-model:show="showUserModal"
|
||||
@showThemeModalEvent="showThemeModal=true"
|
||||
@showLanguageModalEvent="showLanguageModal=true"
|
||||
@show-theme-modal-event="showThemeModal=true"
|
||||
@show-language-modal-event="showLanguageModal=true"
|
||||
/>
|
||||
<modal
|
||||
ref="languageModal"
|
||||
|
|
|
@ -56,9 +56,8 @@
|
|||
/>
|
||||
<signup-form-builder
|
||||
v-else-if="setting.fieldType === 'formBuilder'"
|
||||
:value="values[setting.identifier]"
|
||||
v-model="values[setting.identifier]"
|
||||
:signup-approval-enabled="values.moderation__signup_approval_enabled"
|
||||
@input="set(setting.identifier, $event)"
|
||||
/>
|
||||
<input
|
||||
v-else-if="setting.field.widget.class === 'PasswordInput'"
|
||||
|
@ -238,12 +237,6 @@ export default {
|
|||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
set (key, value) {
|
||||
// otherwise reactivity doesn't trigger :/
|
||||
this.values = cloneDeep(this.values)
|
||||
// TODO (wvffle): Replace $set and $delete with reactive()
|
||||
this.$set(this.values, key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,62 @@
|
|||
<script setup lang="ts">
|
||||
import SignupForm from '~/components/auth/SignupForm.vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { Form } from '~/types'
|
||||
import { arrayMove } from '~/utils'
|
||||
|
||||
interface Props {
|
||||
modelValue: Form
|
||||
signupApprovalEnabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
signupApprovalEnabled: false
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const value = useVModel(props, 'modelValue', emit, { deep: true })
|
||||
|
||||
const maxFields = ref(10)
|
||||
const isPreviewing = ref(false)
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
delete: $pgettext('*/*/*', 'Delete'),
|
||||
up: $pgettext('*/*/*', 'Move up'),
|
||||
down: $pgettext('*/*/*', 'Move down')
|
||||
}))
|
||||
|
||||
if (!value.value?.fields) {
|
||||
value.value = {
|
||||
help_text: {
|
||||
text: '',
|
||||
content_type: 'text/markdown'
|
||||
},
|
||||
fields: []
|
||||
}
|
||||
}
|
||||
|
||||
const addField = () => {
|
||||
value.value.fields.push({
|
||||
label: $pgettext('*/*/Form-builder', 'Additional field') + ' ' + (value.value.fields.length + 1),
|
||||
required: true,
|
||||
input_type: 'short_text'
|
||||
})
|
||||
}
|
||||
|
||||
const remove = (idx: number) => {
|
||||
value.value.fields.splice(idx, 1)
|
||||
}
|
||||
|
||||
const move = (idx: number, increment: number) => {
|
||||
if (idx + increment >= value.value.fields.length) return
|
||||
if (idx === 0 && increment < 0) return
|
||||
arrayMove(value.value.fields, idx, idx + increment)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="ui top attached tabular menu">
|
||||
|
@ -23,7 +82,7 @@
|
|||
class="ui bottom attached segment"
|
||||
>
|
||||
<signup-form
|
||||
:customization="local"
|
||||
:customization="value"
|
||||
:signup-approval-enabled="signupApprovalEnabled"
|
||||
:fetch-description-html="true"
|
||||
/>
|
||||
|
@ -43,10 +102,9 @@
|
|||
</translate>
|
||||
</p>
|
||||
<content-form
|
||||
v-model="value.help_text.text"
|
||||
field-id="help-text"
|
||||
:permissive="true"
|
||||
:value="(local.help_text || {}).text"
|
||||
@input="update('help_text.text', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -58,7 +116,7 @@
|
|||
Additional form fields to be displayed in the form. Only shown if manual sign-up validation is enabled.
|
||||
</translate>
|
||||
</p>
|
||||
<table v-if="local.fields.length > 0">
|
||||
<table v-if="value.fields.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
|
@ -80,8 +138,9 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- TODO (wvffle): Add random _id as :key -->
|
||||
<tr
|
||||
v-for="(field, idx) in local.fields"
|
||||
v-for="(field, idx) in value.fields"
|
||||
:key="idx"
|
||||
>
|
||||
<td>
|
||||
|
@ -128,10 +187,10 @@
|
|||
@click="move(idx, -1)"
|
||||
/>
|
||||
<i
|
||||
:disabled="idx >= local.fields.length - 1 || null"
|
||||
:disabled="idx >= value.fields.length - 1 || null"
|
||||
role="button"
|
||||
:title="labels.down"
|
||||
:class="['down', 'arrow', {disabled: idx >= local.fields.length - 1}, 'icon']"
|
||||
:class="['down', 'arrow', { disabled: idx >= value.fields.length - 1 }, 'icon']"
|
||||
@click="move(idx, 1)"
|
||||
/>
|
||||
<i
|
||||
|
@ -146,7 +205,7 @@
|
|||
</table>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="local.fields.length < maxFields"
|
||||
v-if="value.fields.length < maxFields"
|
||||
class="ui basic button"
|
||||
@click.stop.prevent="addField"
|
||||
>
|
||||
|
@ -159,90 +218,3 @@
|
|||
<div class="ui hidden divider" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { cloneDeep, tap, set } from 'lodash-es'
|
||||
|
||||
import SignupForm from '~/components/auth/SignupForm.vue'
|
||||
|
||||
function arrayMove (arr, oldIndex, newIndex) {
|
||||
if (newIndex >= arr.length) {
|
||||
let k = newIndex - arr.length + 1
|
||||
while (k--) {
|
||||
arr.push(undefined)
|
||||
}
|
||||
}
|
||||
arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0])
|
||||
return arr
|
||||
}
|
||||
|
||||
// v-model with objects is complex, cf
|
||||
// https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
|
||||
export default {
|
||||
components: {
|
||||
SignupForm
|
||||
},
|
||||
props: {
|
||||
value: { type: Object, required: true },
|
||||
signupApprovalEnabled: { type: Boolean }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
maxFields: 10,
|
||||
isPreviewing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
delete: this.$pgettext('*/*/*', 'Delete'),
|
||||
up: this.$pgettext('*/*/*', 'Move up'),
|
||||
down: this.$pgettext('*/*/*', 'Move down')
|
||||
}
|
||||
},
|
||||
local () {
|
||||
return (this.value && this.value.fields) ? this.value : { help_text: { text: null, content_type: 'text/markdown' }, fields: [] }
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$emit('input', this.local)
|
||||
},
|
||||
methods: {
|
||||
addField () {
|
||||
const newValue = tap(cloneDeep(this.local), v => v.fields.push({
|
||||
label: this.$pgettext('*/*/Form-builder', 'Additional field') + ' ' + (this.local.fields.length + 1),
|
||||
required: true,
|
||||
input_type: 'short_text'
|
||||
}))
|
||||
this.$emit('input', newValue)
|
||||
},
|
||||
remove (idx) {
|
||||
this.$emit('input', tap(cloneDeep(this.local), v => v.fields.splice(idx, 1)))
|
||||
},
|
||||
move (idx, incr) {
|
||||
if (idx === 0 && incr < 0) {
|
||||
return
|
||||
}
|
||||
if (idx + incr >= this.local.fields.length) {
|
||||
return
|
||||
}
|
||||
const newFields = arrayMove(cloneDeep(this.local).fields, idx, idx + incr)
|
||||
this.update('fields', newFields)
|
||||
},
|
||||
update (key, value) {
|
||||
if (key === 'help_text.text') {
|
||||
key = 'help_text'
|
||||
if (!value || value.length === 0) {
|
||||
value = null
|
||||
} else {
|
||||
value = {
|
||||
text: value,
|
||||
content_type: 'text/markdown'
|
||||
}
|
||||
}
|
||||
}
|
||||
this.$emit('input', tap(cloneDeep(this.local), v => set(v, key, value)))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -170,7 +170,7 @@ export default {
|
|||
defaultInvitation: { type: String, required: false, default: null },
|
||||
next: { type: String, default: '/' },
|
||||
buttonClasses: { type: String, default: 'success' },
|
||||
customization: { type: Object, default: null },
|
||||
customization: { type: Object, default: null }, // ts type: Form
|
||||
fetchDescriptionHtml: { type: Boolean, default: false },
|
||||
signupApprovalEnabled: { type: Boolean, default: null, required: false }
|
||||
},
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const value = useVModel(props, 'modelValue', emit)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
role="button"
|
||||
class="collapse link"
|
||||
@click.prevent="$emit('input', !value)"
|
||||
@click.prevent="value = !value"
|
||||
>
|
||||
<translate
|
||||
v-if="isCollapsed"
|
||||
key="1"
|
||||
v-if="value"
|
||||
translate-context="*/*/Button,Label"
|
||||
>Expand</translate>
|
||||
>
|
||||
Expand
|
||||
</translate>
|
||||
<translate
|
||||
v-else
|
||||
key="2"
|
||||
translate-context="*/*/Button,Label"
|
||||
>Collapse</translate>
|
||||
<i :class="[{down: !isCollapsed}, {right: isCollapsed}, 'angle', 'icon']" />
|
||||
>
|
||||
Collapse
|
||||
</translate>
|
||||
<i :class="[{ down: !value, right: value }, 'angle', 'icon']" />
|
||||
</a>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: { type: Boolean, required: true }
|
||||
},
|
||||
computed: {
|
||||
isCollapsed () {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -124,9 +124,8 @@ export default {
|
|||
await this.loadPreview()
|
||||
}
|
||||
if (!v) {
|
||||
this.$nextTick(() => {
|
||||
await this.$nextTick()
|
||||
this.$refs.textarea.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,3 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import { useClipboard, useVModel } from '@vueuse/core'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
buttonClasses?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
buttonClasses: 'accent',
|
||||
id: 'copy-input'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const value = useVModel(props, 'modelValue', emit)
|
||||
|
||||
const { copy, isSupported: canCopy, copied } = useClipboard({ source: value, copiedDuring: 5000 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui fluid action input component-copy-input">
|
||||
<p
|
||||
|
@ -10,15 +30,15 @@
|
|||
</p>
|
||||
<input
|
||||
:id="id"
|
||||
ref="input"
|
||||
v-model="value"
|
||||
:name="id"
|
||||
:value="value"
|
||||
type="text"
|
||||
readonly
|
||||
>
|
||||
<button
|
||||
:class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']"
|
||||
@click="copy"
|
||||
:disabled="!canCopy || undefined"
|
||||
@click="copy()"
|
||||
>
|
||||
<i class="copy icon" />
|
||||
<translate translate-context="*/*/Button.Label/Short, Verb">
|
||||
|
@ -27,32 +47,3 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: { type: String, required: true },
|
||||
buttonClasses: { type: String, default: 'accent' },
|
||||
id: { type: String, default: 'copy-input' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
copied: false,
|
||||
timeout: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copy () {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
this.$refs.input.select()
|
||||
document.execCommand('Copy')
|
||||
const self = this
|
||||
self.copied = true
|
||||
this.timeout = setTimeout(() => {
|
||||
self.copied = false
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -23,7 +23,7 @@ onMounted(() => {
|
|||
...props.message
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
$('body').toast(params)
|
||||
$('.ui.toast.visible').last().attr('role', 'alert')
|
||||
})
|
||||
|
|
|
@ -1,12 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useClipboard, useVModel } from '@vueuse/core'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
defaultShow?: boolean
|
||||
copyButton?: boolean
|
||||
fieldId: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
defaultShow: false,
|
||||
copyButton: false
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const value = useVModel(props, 'modelValue', emit)
|
||||
|
||||
const showPassword = ref(props.defaultShow)
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
title: $pgettext('Content/Settings/Button.Tooltip/Verb', 'Show/hide password'),
|
||||
copy: $pgettext('*/*/Button.Label/Short, Verb', 'Copy')
|
||||
}))
|
||||
|
||||
const passwordInputType = computed(() => showPassword.value ? 'text' : 'password')
|
||||
|
||||
const store = useStore()
|
||||
const { isSupported: canCopy, copy } = useClipboard({ source: value })
|
||||
const copyPassword = () => {
|
||||
copy()
|
||||
store.commit('ui/addMessage', {
|
||||
content: $pgettext('Content/*/Paragraph', 'Text copied to clipboard!'),
|
||||
date: new Date()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui fluid action input">
|
||||
<input
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
required
|
||||
name="password"
|
||||
:type="passwordInputType"
|
||||
:value="value"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -17,7 +58,7 @@
|
|||
<i class="eye icon" />
|
||||
</button>
|
||||
<button
|
||||
v-if="copyButton"
|
||||
v-if="copyButton && canCopy"
|
||||
type="button"
|
||||
class="ui icon button"
|
||||
:title="labels.copy"
|
||||
|
@ -27,62 +68,3 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: { type: String, required: true },
|
||||
defaultShow: { type: Boolean, default: false },
|
||||
copyButton: { type: Boolean, default: false },
|
||||
fieldId: { type: String, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showPassword: this.defaultShow || false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext(
|
||||
'Content/Settings/Button.Tooltip/Verb',
|
||||
'Show/hide password'
|
||||
),
|
||||
copy: this.$pgettext('*/*/Button.Label/Short, Verb', 'Copy')
|
||||
}
|
||||
},
|
||||
passwordInputType () {
|
||||
if (this.showPassword) {
|
||||
return 'text'
|
||||
}
|
||||
return 'password'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyPassword () {
|
||||
try {
|
||||
this._copyStringToClipboard(this.value)
|
||||
this.$store.commit('ui/addMessage', {
|
||||
content: this.$pgettext(
|
||||
'Content/*/Paragraph',
|
||||
'Text copied to clipboard!'
|
||||
),
|
||||
date: new Date()
|
||||
})
|
||||
} catch ($e) {
|
||||
console.error('Cannot copy', $e)
|
||||
}
|
||||
},
|
||||
_copyStringToClipboard (str) {
|
||||
// cf https://techoverflow.net/2018/03/30/copying-strings-to-the-clipboard-using-pure-javascript/
|
||||
const el = document.createElement('textarea')
|
||||
el.value = str
|
||||
el.setAttribute('readonly', '')
|
||||
el.style = { position: 'absolute', left: '-9999px' }
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
</a>
|
||||
<modal
|
||||
v-if="checkResult"
|
||||
v-model::show="showCandidadesModal"
|
||||
v-model:show="showCandidadesModal"
|
||||
>
|
||||
<h4 class="header">
|
||||
<translate translate-context="Popup/Radio/Title/Noun">
|
||||
|
|
|
@ -47,8 +47,8 @@
|
|||
</td>
|
||||
<td>
|
||||
<report-category-dropdown
|
||||
:model-value="obj.type"
|
||||
@update:modelValue="update({type: $event})"
|
||||
v-model="obj.type"
|
||||
@update:model-value="update({ type: $event })"
|
||||
>
|
||||
 
|
||||
<action-feedback :is-loading="updating.type" />
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
v-html="notificationData.message"
|
||||
/>
|
||||
</router-link>
|
||||
<template
|
||||
<div
|
||||
v-else
|
||||
v-html="notificationData.message"
|
||||
/>
|
||||
|
|
|
@ -27,6 +27,7 @@ const show = useVModel(props, 'show', emit)
|
|||
|
||||
const control = ref()
|
||||
const initModal = () => {
|
||||
// @ts-expect-error
|
||||
control.value = $(modal.value).modal({
|
||||
duration: 100,
|
||||
onApprove: () => emit('approved'),
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import EmbedFrame from './EmbedFrame.vue'
|
||||
import VuePlyr from 'vue-plyr'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
// @ts-expect-error
|
||||
import VuePlyr from 'vue-plyr'
|
||||
|
||||
const app = createApp(EmbedFrame)
|
||||
app.use(VuePlyr)
|
||||
app.mount('#app')
|
||||
|
|
|
@ -7,7 +7,7 @@ export const install: InitModule = ({ app, store }) => {
|
|||
})
|
||||
|
||||
app.directive('dropdown', function (el, binding) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
jQuery(el).dropdown({
|
||||
selectOnKeydown: false,
|
||||
action (text: string, value: string, $el: JQuery<HTMLElement>) {
|
||||
|
@ -15,7 +15,7 @@ export const install: InitModule = ({ app, store }) => {
|
|||
// works as expected
|
||||
const button = $el[0]
|
||||
button.click()
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
jQuery(el).find('.ui.dropdown').dropdown('hide')
|
||||
},
|
||||
...(binding.value || {})
|
||||
|
|
|
@ -3,10 +3,12 @@ import store from '~/store'
|
|||
import { configureCompat, createApp, defineAsyncComponent, h } from 'vue'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import useTheme from '~/composables/useTheme'
|
||||
|
||||
// NOTE: Set the theme as fast as possible
|
||||
useTheme()
|
||||
|
||||
configureCompat({
|
||||
RENDER_FUNCTION: false,
|
||||
RENDER_FUNCTION: false
|
||||
// COMPONENT_V_MODEL: false
|
||||
})
|
||||
|
||||
|
@ -32,7 +34,7 @@ const app = createApp({
|
|||
app.use(router)
|
||||
app.use(store)
|
||||
|
||||
const modules: Promise<unknown>[] = []
|
||||
const modules: Array<Promise<unknown>> = []
|
||||
for (const module of Object.values(import.meta.globEager('./init/*.ts'))) {
|
||||
modules.push(module.install?.({
|
||||
app,
|
||||
|
@ -50,8 +52,10 @@ Promise.all(modules).finally(() => {
|
|||
// TODO (wvffle): Migrate to pinia
|
||||
// TODO (wvffle): Remove global Vue (Only vuex files affected)
|
||||
// TODO (wvffle): Remove shims-vue2.d.ts
|
||||
// TODO (wvffle): Replace $set and $delete with reactive()
|
||||
// TODO (wvffle): Check for mixin merging: https://v3-migration.vuejs.org/breaking-changes/data-option.html#mixin-merge-behavior-change=
|
||||
// TODO (wvffle): Use emits options: https://v3-migration.vuejs.org/breaking-changes/emits-option.html
|
||||
// TODO (wvffle): Migrate to new v-model: https://v3-migration.vuejs.org/breaking-changes/v-model.html
|
||||
// TODO (wvffle): Migrate to <script setup>
|
||||
// TODO (wvffle): Replace `from '(../)+` with `from '~/`
|
||||
// TODO (wvffle): Remove `allowJs` from tsconfig.json
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHistory, NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
|
||||
import store from '~/store'
|
||||
|
||||
function adminPermissions (to, from, next) {
|
||||
if (store.state.auth.authenticated === true && store.state.auth.availablePermissions.settings === true) {
|
||||
function adminPermissions (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
|
||||
if (store.state.auth.authenticated && store.state.auth.availablePermissions.settings) {
|
||||
next()
|
||||
} else {
|
||||
console.log('Not authenticated. Redirecting to library.')
|
||||
|
@ -10,8 +10,8 @@ function adminPermissions (to, from, next) {
|
|||
}
|
||||
}
|
||||
|
||||
function moderatorPermissions (to, from, next) {
|
||||
if (store.state.auth.authenticated === true && store.state.auth.availablePermissions.moderation === true) {
|
||||
function moderatorPermissions (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
|
||||
if (store.state.auth.authenticated && store.state.auth.availablePermissions.moderation) {
|
||||
next()
|
||||
} else {
|
||||
console.log('Not authenticated. Redirecting to library.')
|
||||
|
@ -19,8 +19,8 @@ function moderatorPermissions (to, from, next) {
|
|||
}
|
||||
}
|
||||
|
||||
function libraryPermissions (to, from, next) {
|
||||
if (store.state.auth.authenticated === true && store.state.auth.availablePermissions.library === true) {
|
||||
function libraryPermissions (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
|
||||
if (store.state.auth.authenticated && store.state.auth.availablePermissions.library) {
|
||||
next()
|
||||
} else {
|
||||
console.log('Not authenticated. Redirecting to library.')
|
||||
|
@ -126,7 +126,7 @@ export default createRouter({
|
|||
initialId: route.query.id,
|
||||
initialType: route.query.type || 'artists',
|
||||
initialQuery: route.query.q,
|
||||
initialPage: parseInt(route.query.page) || 1
|
||||
initialPage: parseInt(route.query.page as string) || 1
|
||||
})
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { createStore, Store } from 'vuex'
|
||||
import { createStore } from 'vuex'
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
|
||||
import favorites from './favorites'
|
||||
|
@ -13,7 +13,7 @@ import player from './player'
|
|||
import playlists from './playlists'
|
||||
import ui from './ui'
|
||||
|
||||
export default <Store<any>> createStore({
|
||||
export default createStore({
|
||||
modules: {
|
||||
ui,
|
||||
auth,
|
||||
|
@ -69,7 +69,7 @@ export default <Store<any>> createStore({
|
|||
return {
|
||||
queue: {
|
||||
currentIndex: state.queue.currentIndex,
|
||||
tracks: state.queue.tracks.map(track => {
|
||||
tracks: state.queue.tracks.map((track: any) => {
|
||||
// we keep only valuable fields to make the cache lighter and avoid
|
||||
// cyclic value serialization errors
|
||||
const artist = {
|
||||
|
@ -83,7 +83,8 @@ export default <Store<any>> createStore({
|
|||
mbid: track.mbid,
|
||||
uploads: track.uploads,
|
||||
listen_url: track.listen_url,
|
||||
artist: artist
|
||||
artist: artist,
|
||||
album: {}
|
||||
}
|
||||
if (track.album) {
|
||||
data.album = {
|
||||
|
|
|
@ -82,6 +82,23 @@ export interface FileSystem {
|
|||
content: FSEntry[]
|
||||
}
|
||||
|
||||
// Form stuff
|
||||
export interface FormHelpText {
|
||||
content_type: string
|
||||
text?: string
|
||||
}
|
||||
|
||||
export interface FormField {
|
||||
label: string
|
||||
input_type: 'short_text' | 'long_text'
|
||||
required: boolean
|
||||
}
|
||||
|
||||
export interface Form {
|
||||
fields: FormField[]
|
||||
help_text: FormHelpText
|
||||
}
|
||||
|
||||
// Yet uncategorized stuff
|
||||
export interface Actor {
|
||||
preferred_username: string
|
||||
|
|
|
@ -20,15 +20,15 @@ export function parseAPIErrors (responseData: APIErrorResponse, parentField?: st
|
|||
|
||||
const value = responseData[field]
|
||||
if (Array.isArray(value)) {
|
||||
const values = value as string[]
|
||||
const values = value
|
||||
errors.push(...values.map(err => {
|
||||
return err.toLocaleLowerCase().includes('this field ')
|
||||
? `${fieldName}: ${err}`
|
||||
: err
|
||||
}))
|
||||
} else if (value as APIErrorResponse) {
|
||||
} else if (value) {
|
||||
// nested errors
|
||||
const nestedErrors = parseAPIErrors(value as APIErrorResponse, fieldName)
|
||||
const nestedErrors = parseAPIErrors(value, fieldName)
|
||||
errors.push(...nestedErrors)
|
||||
}
|
||||
}
|
||||
|
@ -63,3 +63,12 @@ export function getDomain (url: string) {
|
|||
parser.href = url
|
||||
return parser.hostname
|
||||
}
|
||||
|
||||
export function arrayMove (arr: unknown[], oldIndex: number, newIndex: number) {
|
||||
if (newIndex >= arr.length) {
|
||||
arr.push(...Array(newIndex - arr.length + 1))
|
||||
}
|
||||
|
||||
arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0])
|
||||
return arr
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
:all="true"
|
||||
:label="true"
|
||||
:model-value="getTokenValue('category', '')"
|
||||
@update:modelValue="addSearchToken('category', $event)"
|
||||
@update:model-value="addSearchToken('category', $event)"
|
||||
/>
|
||||
<div class="field">
|
||||
<label for="reports-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
||||
|
|
|
@ -38,7 +38,7 @@ if (store.state.auth.authenticated) {
|
|||
Log in to your Funkwhale account
|
||||
</translate>
|
||||
</h2>
|
||||
<login-form :next="redirectTo" />
|
||||
<login-form :next="next" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "ESNext", "WebWorker"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"skipLibCheck": true,
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["dom", "esnext", "webworker"],
|
||||
|
||||
"allowJs": true,
|
||||
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"types": [
|
||||
"vite/client",
|
||||
"vue/ref-macros",
|
||||
"unplugin-vue2-script-setup/types",
|
||||
"vue-gettext/types"
|
||||
"vue-gettext/types",
|
||||
"vite-plugin-pwa/client"
|
||||
],
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
|
@ -29,5 +33,6 @@
|
|||
"vueCompilerOptions": {
|
||||
"experimentalCompatMode": 2
|
||||
},
|
||||
"include": ["src/*.d.ts", "src/**/*.ts", "src/**/*.vue"]
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
Loading…
Reference in New Issue