mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
Merge pull request #366 from YunoHost/config-panel
New config panel with file and other nice formitem
This commit is contained in:
commit
da87972ca4
23 changed files with 2779 additions and 2166 deletions
4188
app/package-lock.json
generated
4188
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -18,10 +18,12 @@
|
||||||
"firacode": "^5.2.0",
|
"firacode": "^5.2.0",
|
||||||
"fontsource-firago": "^3.1.5",
|
"fontsource-firago": "^3.1.5",
|
||||||
"fork-awesome": "^1.1.7",
|
"fork-awesome": "^1.1.7",
|
||||||
|
"simple-evaluate": "^1.4.3",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-i18n": "^8.24.1",
|
"vue-i18n": "^8.24.1",
|
||||||
"vue-router": "^3.5.1",
|
"vue-router": "^3.5.1",
|
||||||
"vuelidate": "^0.7.6",
|
"vuelidate": "^0.7.6",
|
||||||
|
"vue-showdown": "^2.4.1",
|
||||||
"vuex": "^3.6.2"
|
"vuex": "^3.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -49,6 +49,7 @@ class APIBadRequestError extends APIError {
|
||||||
super(method, response, errorData)
|
super(method, response, errorData)
|
||||||
this.name = 'APIBadRequestError'
|
this.name = 'APIBadRequestError'
|
||||||
this.key = errorData.error_key
|
this.key = errorData.error_key
|
||||||
|
this.data = errorData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
|
|
||||||
<slot name="server-error">
|
<slot name="server-error">
|
||||||
<b-alert
|
<b-alert
|
||||||
variant="danger" class="my-3"
|
variant="danger" class="my-3" icon="ban"
|
||||||
:show="serverError !== ''" v-html="serverError"
|
:show="errorFeedback !== ''" v-html="errorFeedback"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</b-form>
|
</b-form>
|
||||||
|
@ -46,7 +46,13 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
disabled () {
|
disabled () {
|
||||||
return this.validation ? this.validation.$invalid : false
|
return false // this.validation ? this.validation.$invalid : false
|
||||||
|
},
|
||||||
|
errorFeedback () {
|
||||||
|
if (this.serverError) return this.serverError
|
||||||
|
else if (this.validation && this.validation.$anyError) {
|
||||||
|
return this.$i18n.t('form_errors.invalid_form')
|
||||||
|
} else return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -26,17 +26,16 @@
|
||||||
|
|
||||||
<template #description>
|
<template #description>
|
||||||
<!-- Render description -->
|
<!-- Render description -->
|
||||||
<template v-if="description || example || link">
|
<template v-if="description || link">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<span v-if="example">{{ $t('form_input_example', { example }) }}</span>
|
<b-link v-if="link" :to="link" :href="link.href"
|
||||||
|
class="ml-auto"
|
||||||
<b-link v-if="link" :to="link" class="ml-auto">
|
>
|
||||||
{{ link.text }}
|
{{ link.text }}
|
||||||
</b-link>
|
</b-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<vue-showdown :markdown="description" flavor="github" v-if="description"
|
||||||
v-if="description" v-html="description"
|
|
||||||
:class="{ ['alert p-1 px-2 alert-' + descriptionVariant]: descriptionVariant }"
|
:class="{ ['alert p-1 px-2 alert-' + descriptionVariant]: descriptionVariant }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -57,7 +56,6 @@ export default {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
description: { type: String, default: null },
|
description: { type: String, default: null },
|
||||||
descriptionVariant: { type: String, default: null },
|
descriptionVariant: { type: String, default: null },
|
||||||
example: { type: String, default: null },
|
|
||||||
link: { type: Object, default: null },
|
link: { type: Object, default: null },
|
||||||
// Rendered field component props
|
// Rendered field component props
|
||||||
component: { type: String, default: 'InputItem' },
|
component: { type: String, default: 'InputItem' },
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
|
<b-button-group class="w-100">
|
||||||
|
<b-button @click="clearFiles" variant="danger" v-if="!this.required && this.value !== null && !this.value._removed">
|
||||||
|
<icon iname="trash" />
|
||||||
|
</b-button>
|
||||||
<b-form-file
|
<b-form-file
|
||||||
:id="id"
|
|
||||||
v-model="file"
|
v-model="file"
|
||||||
|
ref="input-file"
|
||||||
|
:id="id"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
:placeholder="placeholder"
|
:required="required"
|
||||||
|
:placeholder="_placeholder"
|
||||||
|
:accept="accept"
|
||||||
:drop-placeholder="dropPlaceholder"
|
:drop-placeholder="dropPlaceholder"
|
||||||
:state="state"
|
:state="state"
|
||||||
:required="required"
|
|
||||||
:browse-text="$t('words.browse')"
|
:browse-text="$t('words.browse')"
|
||||||
|
@blur="$parent.$emit('touch', name)"
|
||||||
@focusout.native="$parent.$emit('touch', name)"
|
@focusout.native="$parent.$emit('touch', name)"
|
||||||
/>
|
/>
|
||||||
|
</b-button-group>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -17,17 +25,41 @@ export default {
|
||||||
name: 'FileItem',
|
name: 'FileItem',
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return { file: null }
|
return {
|
||||||
|
file: this.value
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
placeholder: { type: String, default: null },
|
value: { type: [File, null], default: null },
|
||||||
|
placeholder: { type: String, default: 'Choose a file or drop it here...' },
|
||||||
dropPlaceholder: { type: String, default: null },
|
dropPlaceholder: { type: String, default: null },
|
||||||
required: { type: Boolean, default: false },
|
accept: { type: String, default: null },
|
||||||
state: { type: Boolean, default: null },
|
state: { type: Boolean, default: null },
|
||||||
name: { type: String, default: null },
|
required: { type: Boolean, default: false },
|
||||||
accept: { type: String, default: null }
|
name: { type: String, default: null }
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
_placeholder: function () {
|
||||||
|
return (this.value === null) ? this.placeholder : this.value.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
clearFiles () {
|
||||||
|
const f = new File([''], this.placeholder)
|
||||||
|
f._removed = true
|
||||||
|
if (this.value && this.value.currentfile) {
|
||||||
|
this.$refs['input-file'].reset()
|
||||||
|
this.$emit('input', f)
|
||||||
|
} else {
|
||||||
|
this.$refs['input-file'].setFiles([f])
|
||||||
|
this.file = f
|
||||||
|
this.$emit('input', f)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,14 +7,25 @@
|
||||||
:type="type"
|
:type="type"
|
||||||
:state="state"
|
:state="state"
|
||||||
:required="required"
|
:required="required"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
|
:trim="trim"
|
||||||
|
:autocomplete="autocomplete_"
|
||||||
@blur="$parent.$emit('touch', name)"
|
@blur="$parent.$emit('touch', name)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InputItem',
|
name: 'InputItem',
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
autocomplete_: (this.autocomplete) ? this.autocomplete : (this.type === 'password') ? 'new-password' : null
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
value: { type: [String, Number], default: null },
|
value: { type: [String, Number], default: null },
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
|
@ -22,6 +33,12 @@ export default {
|
||||||
type: { type: String, default: 'text' },
|
type: { type: String, default: 'text' },
|
||||||
required: { type: Boolean, default: false },
|
required: { type: Boolean, default: false },
|
||||||
state: { type: Boolean, default: null },
|
state: { type: Boolean, default: null },
|
||||||
|
min: { type: Number, default: null },
|
||||||
|
max: { type: Number, default: null },
|
||||||
|
step: { type: Number, default: null },
|
||||||
|
trim: { type: Boolean, default: true },
|
||||||
|
autocomplete: { type: String, default: null },
|
||||||
|
pattern: { type: Object, default: null },
|
||||||
name: { type: String, default: null }
|
name: { type: String, default: null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
app/src/components/globals/formItems/MarkdownItem.vue
Normal file
15
app/src/components/globals/formItems/MarkdownItem.vue
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<vue-showdown :markdown="label" flavor="github" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'MarkdownItem',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
id: { type: String, default: null },
|
||||||
|
label: { type: String, default: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
42
app/src/components/globals/formItems/ReadOnlyAlertItem.vue
Normal file
42
app/src/components/globals/formItems/ReadOnlyAlertItem.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<b-alert :variant="type" show>
|
||||||
|
<icon :iname="icon_" />
|
||||||
|
<vue-showdown :markdown="label" flavor="github"
|
||||||
|
tag="span" class="markdown"
|
||||||
|
/>
|
||||||
|
</b-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ReadOnlyAlertItem',
|
||||||
|
|
||||||
|
data () {
|
||||||
|
const icons = {
|
||||||
|
success: 'thumbs-up',
|
||||||
|
info: 'info',
|
||||||
|
warning: 'warning',
|
||||||
|
danger: 'ban'
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
icon_: (this.icon) ? this.icon : icons[this.type]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
id: { type: String, default: null },
|
||||||
|
label: { type: String, default: null },
|
||||||
|
type: { type: String, default: null },
|
||||||
|
icon: { type: String, default: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.icon + span.markdown > *:first-child {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.alert p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
36
app/src/components/globals/formItems/TagsItem.vue
Normal file
36
app/src/components/globals/formItems/TagsItem.vue
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<b-form-tags
|
||||||
|
v-model="tags"
|
||||||
|
:id="id"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:required="required"
|
||||||
|
separator=" ,;"
|
||||||
|
:limit="limit"
|
||||||
|
remove-on-delete
|
||||||
|
:state="state"
|
||||||
|
v-on="$listeners"
|
||||||
|
@blur="$parent.$emit('touch', name)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TagsItem',
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
tags: this.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: { type: Array, default: null },
|
||||||
|
id: { type: String, default: null },
|
||||||
|
placeholder: { type: String, default: null },
|
||||||
|
limit: { type: Number, default: null },
|
||||||
|
required: { type: Boolean, default: false },
|
||||||
|
state: { type: Boolean, default: null },
|
||||||
|
name: { type: String, default: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -67,12 +67,15 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'TagsSelectize',
|
name: 'TagsSelectizeItem',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: { type: Array, required: true },
|
value: { type: Array, required: true },
|
||||||
options: { type: Array, required: true },
|
options: { type: Array, required: true },
|
||||||
id: { type: String, required: true },
|
id: { type: String, required: true },
|
||||||
|
placeholder: { type: String, default: null },
|
||||||
|
limit: { type: Number, default: null },
|
||||||
|
name: { type: String, default: null },
|
||||||
itemsName: { type: String, required: true },
|
itemsName: { type: String, required: true },
|
||||||
disabledItems: { type: Array, default: () => ([]) },
|
disabledItems: { type: Array, default: () => ([]) },
|
||||||
// By default `addTag` and `removeTag` have to be executed manually by listening to 'tag-update'.
|
// By default `addTag` and `removeTag` have to be executed manually by listening to 'tag-update'.
|
28
app/src/components/globals/formItems/TextAreaItem.vue
Normal file
28
app/src/components/globals/formItems/TextAreaItem.vue
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<b-form-textarea
|
||||||
|
v-model="value"
|
||||||
|
:id="id"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:required="required"
|
||||||
|
:state="state"
|
||||||
|
rows="4"
|
||||||
|
v-on="$listeners"
|
||||||
|
@blur="$parent.$emit('touch', name)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TextAreaItem',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: { type: String, default: null },
|
||||||
|
id: { type: String, default: null },
|
||||||
|
placeholder: { type: String, default: null },
|
||||||
|
type: { type: String, default: 'text' },
|
||||||
|
required: { type: Boolean, default: false },
|
||||||
|
state: { type: Boolean, default: null },
|
||||||
|
name: { type: String, default: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -67,7 +67,6 @@ const unique = items => item => helpers.withParams(
|
||||||
item => items ? !helpers.req(item) || !items.includes(item) : true
|
item => items ? !helpers.req(item) || !items.includes(item) : true
|
||||||
)(item)
|
)(item)
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
alphalownum_,
|
alphalownum_,
|
||||||
domain,
|
domain,
|
||||||
|
|
|
@ -56,85 +56,201 @@ export function adressToFormValue (address) {
|
||||||
* @return {Object} an formated argument containing formItem props, validation and base value.
|
* @return {Object} an formated argument containing formItem props, validation and base value.
|
||||||
*/
|
*/
|
||||||
export function formatYunoHostArgument (arg) {
|
export function formatYunoHostArgument (arg) {
|
||||||
let value = null
|
let value = (arg.value !== undefined) ? arg.value : (arg.current_value !== undefined) ? arg.current_value : null
|
||||||
const validation = {}
|
const validation = {}
|
||||||
|
const error = { message: null }
|
||||||
|
arg.ask = formatI18nField(arg.ask)
|
||||||
const field = {
|
const field = {
|
||||||
component: undefined,
|
component: undefined,
|
||||||
label: formatI18nField(arg.ask),
|
label: arg.ask,
|
||||||
props: {}
|
props: {}
|
||||||
}
|
}
|
||||||
|
const defaultProps = ['id:name', 'placeholder:example']
|
||||||
if (arg.type === 'boolean') {
|
const components = [
|
||||||
field.id = arg.name
|
{
|
||||||
} else {
|
types: [undefined, 'string', 'path'],
|
||||||
field.props.id = arg.name
|
name: 'InputItem',
|
||||||
|
props: defaultProps.concat(['autocomplete', 'trim'])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['email', 'url', 'date', 'time', 'color'],
|
||||||
|
name: 'InputItem',
|
||||||
|
props: defaultProps.concat(['type', 'trim'])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['password'],
|
||||||
|
name: 'InputItem',
|
||||||
|
props: defaultProps.concat(['type', 'autocomplete', 'trim']),
|
||||||
|
callback: function () {
|
||||||
|
if (!arg.help) {
|
||||||
|
arg.help = 'good_practices_about_admin_password'
|
||||||
}
|
}
|
||||||
|
arg.example = '••••••••••••'
|
||||||
// Some apps has an argument type `string` as type but expect a select since it has `choices`
|
|
||||||
if (arg.choices !== undefined) {
|
|
||||||
field.component = 'SelectItem'
|
|
||||||
field.props.choices = arg.choices
|
|
||||||
// Input
|
|
||||||
} else if ([undefined, 'string', 'number', 'password', 'email'].includes(arg.type)) {
|
|
||||||
field.component = 'InputItem'
|
|
||||||
if (![undefined, 'string'].includes(arg.type)) {
|
|
||||||
field.props.type = arg.type
|
|
||||||
if (arg.type === 'password') {
|
|
||||||
field.description = i18n.t('good_practices_about_admin_password')
|
|
||||||
field.placeholder = '••••••••'
|
|
||||||
validation.passwordLenght = validators.minLength(8)
|
validation.passwordLenght = validators.minLength(8)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['number', 'range'],
|
||||||
|
name: 'InputItem',
|
||||||
|
props: defaultProps.concat(['type', 'min', 'max', 'step']),
|
||||||
|
callback: function () {
|
||||||
|
if (!isNaN(parseInt(arg.min))) {
|
||||||
|
validation.minValue = validators.minValue(parseInt(arg.min))
|
||||||
}
|
}
|
||||||
// Checkbox
|
if (!isNaN(parseInt(arg.max))) {
|
||||||
} else if (arg.type === 'boolean') {
|
validation.maxValue = validators.maxValue(parseInt(arg.max))
|
||||||
field.component = 'CheckboxItem'
|
|
||||||
if (typeof arg.default === 'number') {
|
|
||||||
value = arg.default === 1
|
|
||||||
} else {
|
|
||||||
value = arg.default || false
|
|
||||||
}
|
}
|
||||||
// Special (store related)
|
validation.numValue = validators.helpers.regex('Please provide an integer', new RegExp('^-?[0-9]+$'))
|
||||||
} else if (['user', 'domain'].includes(arg.type)) {
|
}
|
||||||
field.component = 'SelectItem'
|
},
|
||||||
|
{
|
||||||
|
types: ['select'],
|
||||||
|
name: 'SelectItem',
|
||||||
|
props: ['id:name', 'choices']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['user', 'domain'],
|
||||||
|
name: 'SelectItem',
|
||||||
|
props: ['id:name', 'choices'],
|
||||||
|
callback: function () {
|
||||||
field.link = { name: arg.type + '-list', text: i18n.t(`manage_${arg.type}s`) }
|
field.link = { name: arg.type + '-list', text: i18n.t(`manage_${arg.type}s`) }
|
||||||
field.props.choices = store.getters[arg.type + 'sAsChoices']
|
field.props.choices = store.getters[arg.type + 'sAsChoices']
|
||||||
|
if (value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (arg.type === 'domain') {
|
if (arg.type === 'domain') {
|
||||||
value = store.getters.mainDomain
|
value = store.getters.mainDomain
|
||||||
} else {
|
} else {
|
||||||
value = field.props.choices.length ? field.props.choices[0].value : null
|
value = field.props.choices.length ? field.props.choices[0].value : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown from the specs, try to display it as an input[text]
|
|
||||||
// FIXME throw an error instead ?
|
|
||||||
} else {
|
|
||||||
field.component = 'InputItem'
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['file'],
|
||||||
|
name: 'FileItem',
|
||||||
|
props: defaultProps.concat(['accept']),
|
||||||
|
callback: function () {
|
||||||
|
if (value) {
|
||||||
|
value = new File([''], value)
|
||||||
|
value.currentfile = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['text'],
|
||||||
|
name: 'TextAreaItem',
|
||||||
|
props: defaultProps
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['tags'],
|
||||||
|
name: 'TagsItem',
|
||||||
|
props: defaultProps.concat(['limit', 'placeholder', 'options:choices', 'tagIcon:icon']),
|
||||||
|
callback: function () {
|
||||||
|
if (arg.choices) {
|
||||||
|
this.name = 'TagsSelectizeItem'
|
||||||
|
field.props.auto = true
|
||||||
|
field.props.itemsName = ''
|
||||||
|
field.props.label = arg.placeholder
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.split(',')
|
||||||
|
} else if (!value) {
|
||||||
|
value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['boolean'],
|
||||||
|
name: 'CheckboxItem',
|
||||||
|
props: ['id:name', 'choices'],
|
||||||
|
callback: function () {
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
value = ['1', 'yes', 'y', 'true'].includes(String(value).toLowerCase())
|
||||||
|
} else if (arg.default !== null && arg.default !== undefined) {
|
||||||
|
value = ['1', 'yes', 'y', 'true'].includes(String(arg.default).toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['alert'],
|
||||||
|
name: 'ReadOnlyAlertItem',
|
||||||
|
props: ['type:style', 'label:ask', 'icon'],
|
||||||
|
readonly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['markdown', 'display_text'],
|
||||||
|
name: 'MarkdownItem',
|
||||||
|
props: ['label:ask'],
|
||||||
|
readonly: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Default type management if no one is filled
|
||||||
|
if (arg.type === undefined) {
|
||||||
|
arg.type = (arg.choices === undefined) ? 'string' : 'select'
|
||||||
|
}
|
||||||
|
// Search the component bind to the type
|
||||||
|
const component = components.find(element => element.types.includes(arg.type))
|
||||||
|
if (component === undefined) throw new TypeError('Unknown type: ' + arg.type)
|
||||||
|
// Callback use for specific behaviour
|
||||||
|
if (component.callback) component.callback()
|
||||||
|
field.component = component.name
|
||||||
|
// Affect properties to the field Item
|
||||||
|
for (let prop of component.props) {
|
||||||
|
prop = prop.split(':')
|
||||||
|
const propName = prop[0]
|
||||||
|
const argName = prop.slice(-1)[0]
|
||||||
|
if (argName in arg) {
|
||||||
|
field.props[propName] = arg[argName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We don't want to display a label html item as this kind or field contains
|
||||||
|
// already the text to display
|
||||||
|
if (component.readonly) delete field.label
|
||||||
// Required (no need for checkbox its value can't be null)
|
// Required (no need for checkbox its value can't be null)
|
||||||
if (field.component !== 'CheckboxItem' && arg.optional !== true) {
|
else if (field.component !== 'CheckboxItem' && arg.optional !== true) {
|
||||||
validation.required = validators.required
|
validation.required = validators.required
|
||||||
}
|
}
|
||||||
|
if (arg.pattern) {
|
||||||
|
// validation.pattern = validators.helpers.withMessage(arg.pattern.error,
|
||||||
|
validation.pattern = validators.helpers.regex(arg.pattern.error, new RegExp(arg.pattern.regexp))
|
||||||
|
}
|
||||||
|
validation.remote = validators.helpers.withParams(error, (v) => {
|
||||||
|
const result = !error.message
|
||||||
|
error.message = null
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// field.props['title'] = field.pattern.error
|
||||||
// Default value if still `null`
|
// Default value if still `null`
|
||||||
|
if (value === null && arg.current_value) {
|
||||||
|
value = arg.current_value
|
||||||
|
}
|
||||||
if (value === null && arg.default) {
|
if (value === null && arg.default) {
|
||||||
value = arg.default
|
value = arg.default
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help message
|
// Help message
|
||||||
if (arg.help) {
|
if (arg.help) {
|
||||||
field.description = formatI18nField(arg.help)
|
field.description = formatI18nField(arg.help)
|
||||||
}
|
}
|
||||||
// Example
|
|
||||||
if (arg.example) {
|
// Help message
|
||||||
field.example = arg.example
|
if (arg.helpLink) {
|
||||||
if (field.component === 'InputItem') {
|
field.link = { href: arg.helpLink.href, text: i18n.t(arg.helpLink.text) }
|
||||||
field.props.placeholder = field.example
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg.visible) {
|
||||||
|
field.visible = arg.visible
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
field,
|
field,
|
||||||
// Return null instead of empty object if there's no validation
|
// Return null instead of empty object if there's no validation
|
||||||
validation: Object.keys(validation).length === 0 ? null : validation
|
validation: Object.keys(validation).length === 0 ? null : validation,
|
||||||
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,10 +264,10 @@ export function formatYunoHostArgument (arg) {
|
||||||
* @return {Object} an object containing all parsed values to be used in vue views.
|
* @return {Object} an object containing all parsed values to be used in vue views.
|
||||||
*/
|
*/
|
||||||
export function formatYunoHostArguments (args, name = null) {
|
export function formatYunoHostArguments (args, name = null) {
|
||||||
let disclaimer = null
|
|
||||||
const form = {}
|
const form = {}
|
||||||
const fields = {}
|
const fields = {}
|
||||||
const validations = {}
|
const validations = {}
|
||||||
|
const errors = {}
|
||||||
|
|
||||||
// FIXME yunohost should add the label field by default
|
// FIXME yunohost should add the label field by default
|
||||||
if (name) {
|
if (name) {
|
||||||
|
@ -163,20 +279,35 @@ export function formatYunoHostArguments (args, name = null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
if (arg.type === 'display_text') {
|
const { value, field, validation, error } = formatYunoHostArgument(arg)
|
||||||
disclaimer = formatI18nField(arg.ask)
|
|
||||||
} else {
|
|
||||||
const { value, field, validation } = formatYunoHostArgument(arg)
|
|
||||||
fields[arg.name] = field
|
fields[arg.name] = field
|
||||||
form[arg.name] = value
|
form[arg.name] = value
|
||||||
if (validation) validations[arg.name] = validation
|
if (validation) validations[arg.name] = validation
|
||||||
}
|
errors[arg.name] = error
|
||||||
}
|
}
|
||||||
|
|
||||||
return { form, fields, validations, disclaimer }
|
return { form, fields, validations, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pFileReader (file, output, key, base64 = true) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fr = new FileReader()
|
||||||
|
fr.onerror = reject
|
||||||
|
fr.onload = () => {
|
||||||
|
output[key] = fr.result
|
||||||
|
if (base64) {
|
||||||
|
output[key] = fr.result.replace(/data:[^;]*;base64,/, '')
|
||||||
|
}
|
||||||
|
output[key + '[name]'] = file.name
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
if (base64) {
|
||||||
|
fr.readAsDataURL(file)
|
||||||
|
} else {
|
||||||
|
fr.readAsText(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Format helper for a form value.
|
* Format helper for a form value.
|
||||||
* Convert Boolean to (1|0) and concatenate adresses.
|
* Convert Boolean to (1|0) and concatenate adresses.
|
||||||
|
@ -206,12 +337,13 @@ export function formatFormDataValue (value) {
|
||||||
*/
|
*/
|
||||||
export function formatFormData (
|
export function formatFormData (
|
||||||
formData,
|
formData,
|
||||||
{ extract = null, flatten = false, removeEmpty = true } = {}
|
{ extract = null, flatten = false, removeEmpty = true, removeNull = false, promise = false } = {}
|
||||||
) {
|
) {
|
||||||
const output = {
|
const output = {
|
||||||
data: {},
|
data: {},
|
||||||
extracted: {}
|
extracted: {}
|
||||||
}
|
}
|
||||||
|
const promises = []
|
||||||
for (const key in formData) {
|
for (const key in formData) {
|
||||||
const type = extract && extract.includes(key) ? 'extracted' : 'data'
|
const type = extract && extract.includes(key) ? 'extracted' : 'data'
|
||||||
const value = Array.isArray(formData[key])
|
const value = Array.isArray(formData[key])
|
||||||
|
@ -220,6 +352,16 @@ export function formatFormData (
|
||||||
|
|
||||||
if (removeEmpty && isEmptyValue(value)) {
|
if (removeEmpty && isEmptyValue(value)) {
|
||||||
continue
|
continue
|
||||||
|
} else if (removeNull && (value === null || value === undefined)) {
|
||||||
|
continue
|
||||||
|
} else if (value instanceof File) {
|
||||||
|
if (value.currentfile) {
|
||||||
|
continue
|
||||||
|
} else if (value._removed) {
|
||||||
|
output[type][key] = ''
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
promises.push(pFileReader(value, output[type], key))
|
||||||
} else if (flatten && isObjectLiteral(value)) {
|
} else if (flatten && isObjectLiteral(value)) {
|
||||||
flattenObjectLiteral(value, output[type])
|
flattenObjectLiteral(value, output[type])
|
||||||
} else {
|
} else {
|
||||||
|
@ -227,5 +369,13 @@ export function formatFormData (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { data, extracted } = output
|
const { data, extracted } = output
|
||||||
|
if (promises.length > 0 || promise) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Promise.all(promises).then((value) => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
return extract ? { data, ...extracted } : data
|
return extract ? { data, ...extracted } : data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,11 +176,15 @@
|
||||||
"githubLink": "URL must be a valid GitHub link to a repository",
|
"githubLink": "URL must be a valid GitHub link to a repository",
|
||||||
"name": "Names may not includes special characters except <code> ,.'-</code>",
|
"name": "Names may not includes special characters except <code> ,.'-</code>",
|
||||||
"minValue": "Value must be a number equal or greater than {min}.",
|
"minValue": "Value must be a number equal or greater than {min}.",
|
||||||
|
"maxValue": "Value must be a number equal or lesser than {max}.",
|
||||||
"notInUsers": "The user '{value}' already exists.",
|
"notInUsers": "The user '{value}' already exists.",
|
||||||
"number": "Value must be a number.",
|
"number": "Value must be a number.",
|
||||||
"passwordLenght": "Password must be at least 8 characters long.",
|
"passwordLenght": "Password must be at least 8 characters long.",
|
||||||
"passwordMatch": "Passwords don't match.",
|
"passwordMatch": "Passwords don't match.",
|
||||||
"required": "Field is required."
|
"required": "Field is required.",
|
||||||
|
"remote": "{message}",
|
||||||
|
"pattern": "{type}",
|
||||||
|
"invalid_form": "The form contains some errors."
|
||||||
},
|
},
|
||||||
"form_input_example": "Example: {example}",
|
"form_input_example": "Example: {example}",
|
||||||
"from_to": "from {0} to {1}",
|
"from_to": "from {0} to {1}",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import BootstrapVue from 'bootstrap-vue'
|
import BootstrapVue from 'bootstrap-vue'
|
||||||
|
import VueShowdown from 'vue-showdown'
|
||||||
|
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
@ -11,7 +12,6 @@ import { registerGlobalErrorHandlers } from './api'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
|
||||||
// Styles are imported in `src/App.vue` <style>
|
// Styles are imported in `src/App.vue` <style>
|
||||||
Vue.use(BootstrapVue, {
|
Vue.use(BootstrapVue, {
|
||||||
BSkeleton: { animation: 'none' },
|
BSkeleton: { animation: 'none' },
|
||||||
|
@ -24,6 +24,11 @@ Vue.use(BootstrapVue, {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Vue.use(VueShowdown, {
|
||||||
|
options: {
|
||||||
|
emoji: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Ugly wrapper for `$bvModal.msgBoxConfirm` to set default i18n button titles
|
// Ugly wrapper for `$bvModal.msgBoxConfirm` to set default i18n button titles
|
||||||
// FIXME find or wait for a better way
|
// FIXME find or wait for a better way
|
||||||
|
@ -46,13 +51,14 @@ requireComponent.keys().forEach((fileName) => {
|
||||||
Vue.component(component.name, component)
|
Vue.component(component.name, component)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
registerGlobalErrorHandlers()
|
registerGlobalErrorHandlers()
|
||||||
|
|
||||||
|
|
||||||
new Vue({
|
const app = new Vue({
|
||||||
i18n,
|
i18n,
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
render: h => h(App)
|
render: h => h(App)
|
||||||
}).$mount('#app')
|
})
|
||||||
|
|
||||||
|
app.$mount('#app')
|
||||||
|
|
|
@ -15,14 +15,6 @@
|
||||||
:validation="$v.actions[i]" :id="action.id + '-form'" :server-error="action.serverError"
|
:validation="$v.actions[i]" :id="action.id + '-form'" :server-error="action.serverError"
|
||||||
@submit.prevent="performAction(action)" :submit-text="$t('perform')"
|
@submit.prevent="performAction(action)" :submit-text="$t('perform')"
|
||||||
>
|
>
|
||||||
<template #disclaimer>
|
|
||||||
<div
|
|
||||||
v-if="action.formDisclaimer"
|
|
||||||
class="alert alert-info" v-html="action.formDisclaimer"
|
|
||||||
/>
|
|
||||||
<b-card-text v-if="action.description" v-html="action.description" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<form-field
|
<form-field
|
||||||
v-for="(field, fname) in action.fields" :key="fname" label-cols="0"
|
v-for="(field, fname) in action.fields" :key="fname" label-cols="0"
|
||||||
v-bind="field" v-model="action.form[fname]" :validation="$v.actions[i][fname]"
|
v-bind="field" v-model="action.form[fname]" :validation="$v.actions[i][fname]"
|
||||||
|
@ -85,11 +77,10 @@ export default {
|
||||||
const action = { name, id, serverError: '' }
|
const action = { name, id, serverError: '' }
|
||||||
if (description) action.description = formatI18nField(description)
|
if (description) action.description = formatI18nField(description)
|
||||||
if (arguments_ && arguments_.length) {
|
if (arguments_ && arguments_.length) {
|
||||||
const { form, fields, validations, disclaimer } = formatYunoHostArguments(arguments_)
|
const { form, fields, validations } = formatYunoHostArguments(arguments_)
|
||||||
action.form = form
|
action.form = form
|
||||||
action.fields = fields
|
action.fields = fields
|
||||||
if (validations) action.validations = validations
|
if (validations) action.validations = validations
|
||||||
if (disclaimer) action.formDisclaimer = disclaimer
|
|
||||||
}
|
}
|
||||||
return action
|
return action
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,30 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<view-base :queries="queries" @queries-response="onQueriesResponse" skeleton="card-form-skeleton">
|
<view-base :queries="queries" @queries-response="onQueriesResponse" skeleton="card-form-skeleton">
|
||||||
<template v-if="panels" #default>
|
<template v-if="panels" #default>
|
||||||
<b-alert variant="warning" class="mb-4">
|
<b-tabs pills card vertical>
|
||||||
<icon iname="exclamation-triangle" /> {{ $t('experimental_warning') }}
|
<b-tab v-for="{ name, id: id_, sections, help, serverError } in panels"
|
||||||
</b-alert>
|
:key="id_"
|
||||||
|
:title="name"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<icon iname="wrench" /> {{ name }}
|
||||||
|
</template>
|
||||||
<card-form
|
<card-form
|
||||||
v-for="{ name, id: id_, sections, help, serverError } in panels" :key="id_"
|
:key="id_"
|
||||||
:title="name" icon="wrench" title-tag="h4"
|
:title="name" icon="wrench" title-tag="h2"
|
||||||
:validation="$v.forms[id_]" :id="id_ + '-form'" :server-error="serverError"
|
:validation="$v.forms[id_]" :id="id_ + '-form'" :server-error="serverError"
|
||||||
collapsable
|
|
||||||
@submit.prevent="applyConfig(id_)"
|
@submit.prevent="applyConfig(id_)"
|
||||||
>
|
>
|
||||||
<template v-if="help" #disclaimer>
|
<template v-if="help" #disclaimer>
|
||||||
<div class="alert alert-info" v-html="help" />
|
<div class="alert alert-info" v-html="help" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-for="section in sections" :key="section.id" class="mb-5">
|
<template v-for="section in sections">
|
||||||
<b-card-title>{{ section.name }} <small v-if="section.help">{{ section.help }}</small></b-card-title>
|
<div :key="section.id" class="mb-5" v-if="isVisible(section.visible)">
|
||||||
|
<b-card-title v-if="section.name" title-tag="h3">
|
||||||
<form-field
|
{{ section.name }} <small v-if="section.help">{{ section.help }}</small>
|
||||||
v-for="(field, fname) in section.fields" :key="fname" label-cols="0"
|
</b-card-title>
|
||||||
v-bind="field" v-model="forms[id_][fname]" :validation="$v.forms[id_][fname]"
|
<template v-for="(field, fname) in section.fields">
|
||||||
|
<form-field :key="fname" v-model="forms[id_][fname]"
|
||||||
|
:validation="$v.forms[id_][fname]"
|
||||||
|
v-if="isVisible(field.visible)" v-bind="field"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</card-form>
|
</card-form>
|
||||||
|
</b-tab>
|
||||||
|
</b-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- if no config panel -->
|
<!-- if no config panel -->
|
||||||
|
@ -36,10 +46,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { validationMixin } from 'vuelidate'
|
import { validationMixin } from 'vuelidate'
|
||||||
|
import evaluate from 'simple-evaluate'
|
||||||
|
|
||||||
// FIXME needs test and rework
|
// FIXME needs test and rework
|
||||||
import api, { objectToParams } from '@/api'
|
import api, { objectToParams } from '@/api'
|
||||||
import { formatI18nField, formatYunoHostArguments, formatFormData } from '@/helpers/yunohostArguments'
|
import { formatI18nField, formatYunoHostArguments, formatFormData, pFileReader } from '@/helpers/yunohostArguments'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppConfigPanel',
|
name: 'AppConfigPanel',
|
||||||
|
@ -53,13 +64,14 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['GET', `apps/${this.id}/config-panel`],
|
['GET', `apps/${this.id}/config-panel?full`],
|
||||||
['GET', { uri: 'domains' }],
|
['GET', { uri: 'domains' }],
|
||||||
['GET', { uri: 'domains/main', storeKey: 'main_domain' }],
|
['GET', { uri: 'domains/main', storeKey: 'main_domain' }],
|
||||||
['GET', { uri: 'users' }]
|
['GET', { uri: 'users' }]
|
||||||
],
|
],
|
||||||
panels: undefined,
|
panels: undefined,
|
||||||
forms: undefined,
|
forms: undefined,
|
||||||
|
errors: undefined,
|
||||||
validations: null
|
validations: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -69,27 +81,71 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
isVisible (expression) {
|
||||||
|
if (!expression) return true
|
||||||
|
const context = {}
|
||||||
|
|
||||||
|
const promises = []
|
||||||
|
for (const args of Object.values(this.forms)) {
|
||||||
|
for (const shortname in args) {
|
||||||
|
if (args[shortname] instanceof File) {
|
||||||
|
if (expression.includes(shortname)) {
|
||||||
|
promises.push(pFileReader(args[shortname], context, shortname, false))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context[shortname] = args[shortname]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Allow to use match(var,regexp) function
|
||||||
|
const matchRe = new RegExp('match\\(\\s*(\\w+)\\s*,\\s*"([^"]+)"\\s*\\)', 'g')
|
||||||
|
let i = 0
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
i = 2
|
||||||
|
resolve(false)
|
||||||
|
Promise.all(promises).then((value) => {
|
||||||
|
for (const matched of expression.matchAll(matchRe)) {
|
||||||
|
i++
|
||||||
|
const varName = matched[1] + '__re' + i.toString()
|
||||||
|
context[varName] = new RegExp(matched[2]).test(context[matched[1]])
|
||||||
|
expression = expression.replace(matched[0], varName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resolve(evaluate(context, expression))
|
||||||
|
} catch (error) {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
onQueriesResponse (data) {
|
onQueriesResponse (data) {
|
||||||
if (!data.config_panel || data.config_panel.length === 0) {
|
if (!data.panels || data.panels.length === 0) {
|
||||||
this.panels = null
|
this.panels = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const forms = {}
|
const forms = {}
|
||||||
const validations_ = {}
|
const validations_ = {}
|
||||||
|
const errors_ = {}
|
||||||
const panels_ = []
|
const panels_ = []
|
||||||
for (const { id, name, help, sections } of data.config_panel.panel) {
|
for (const { id, name, help, sections } of data.panels) {
|
||||||
const panel_ = { id, name, sections: [] }
|
const panel_ = { id, sections: [] }
|
||||||
|
if (name) panel_.name = formatI18nField(name)
|
||||||
if (help) panel_.help = formatI18nField(help)
|
if (help) panel_.help = formatI18nField(help)
|
||||||
forms[id] = {}
|
forms[id] = {}
|
||||||
validations_[id] = {}
|
validations_[id] = {}
|
||||||
for (const { name, help, options } of sections) {
|
errors_[id] = {}
|
||||||
const section_ = { name }
|
for (const { id_, name, help, visible, options } of sections) {
|
||||||
|
const section_ = { id: id_, visible }
|
||||||
if (help) section_.help = formatI18nField(help)
|
if (help) section_.help = formatI18nField(help)
|
||||||
const { form, fields, validations } = formatYunoHostArguments(options)
|
if (name) section_.name = formatI18nField(name)
|
||||||
|
const { form, fields, validations, errors } = formatYunoHostArguments(options)
|
||||||
Object.assign(forms[id], form)
|
Object.assign(forms[id], form)
|
||||||
Object.assign(validations_[id], validations)
|
Object.assign(validations_[id], validations)
|
||||||
panel_.sections.push({ name, fields })
|
Object.assign(errors_[id], errors)
|
||||||
|
section_.fields = fields
|
||||||
|
panel_.sections.push(section_)
|
||||||
}
|
}
|
||||||
panels_.push(panel_)
|
panels_.push(panel_)
|
||||||
}
|
}
|
||||||
|
@ -97,23 +153,34 @@ export default {
|
||||||
this.forms = forms
|
this.forms = forms
|
||||||
this.validations = { forms: validations_ }
|
this.validations = { forms: validations_ }
|
||||||
this.panels = panels_
|
this.panels = panels_
|
||||||
|
this.errors = errors_
|
||||||
},
|
},
|
||||||
|
|
||||||
applyConfig (id_) {
|
applyConfig (id_) {
|
||||||
const args = objectToParams(formatFormData(this.forms[id_]))
|
formatFormData(this.forms[id_], { promise: true, removeEmpty: false, removeNull: true }).then((formatedData) => {
|
||||||
|
const args = objectToParams(formatedData)
|
||||||
|
|
||||||
api.put(
|
api.put(
|
||||||
`apps/${this.id}/config`, { args }, { key: 'apps.update_config', name: this.id }
|
`apps/${this.id}/config`, { key: id_, args }, { key: 'apps.update_config', name: this.id }
|
||||||
).then(response => {
|
).then(response => {
|
||||||
// FIXME what should be done ?
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
console.log('SUCCESS', response)
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
const panel = this.panels.find(({ id }) => id_ === id)
|
const panel = this.panels.find(({ id }) => id_ === id)
|
||||||
this.$set(panel, 'serverError', err.message)
|
if (err.data.name) {
|
||||||
|
this.errors[id_][err.data.name].message = err.message
|
||||||
|
} else this.$set(panel, 'serverError', err.message)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
h3.card-title {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border-bottom: solid 1px #aaa;
|
||||||
|
}
|
||||||
|
.form-control::placeholder, .form-file-text {
|
||||||
|
color: #6d7780;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -23,10 +23,6 @@
|
||||||
:validation="$v" :server-error="serverError"
|
:validation="$v" :server-error="serverError"
|
||||||
@submit.prevent="performInstall"
|
@submit.prevent="performInstall"
|
||||||
>
|
>
|
||||||
<template v-if="formDisclaimer" #disclaimer>
|
|
||||||
<div class="alert alert-info" v-html="formDisclaimer" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<form-field
|
<form-field
|
||||||
v-for="(field, fname) in fields" :key="fname" label-cols="0"
|
v-for="(field, fname) in fields" :key="fname" label-cols="0"
|
||||||
v-bind="field" v-model="form[fname]" :validation="$v.form[fname]"
|
v-bind="field" v-model="form[fname]" :validation="$v.form[fname]"
|
||||||
|
@ -75,6 +71,7 @@ export default {
|
||||||
form: undefined,
|
form: undefined,
|
||||||
fields: undefined,
|
fields: undefined,
|
||||||
validations: null,
|
validations: null,
|
||||||
|
errors: undefined,
|
||||||
serverError: ''
|
serverError: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -94,15 +91,15 @@ export default {
|
||||||
manifest.multi_instance = this.$i18n.t(manifest.multi_instance ? 'yes' : 'no')
|
manifest.multi_instance = this.$i18n.t(manifest.multi_instance ? 'yes' : 'no')
|
||||||
this.infos = Object.fromEntries(infosKeys.map(key => [key, manifest[key]]))
|
this.infos = Object.fromEntries(infosKeys.map(key => [key, manifest[key]]))
|
||||||
|
|
||||||
const { form, fields, validations, disclaimer } = formatYunoHostArguments(
|
const { form, fields, validations, errors } = formatYunoHostArguments(
|
||||||
manifest.arguments.install,
|
manifest.arguments.install,
|
||||||
manifest.name
|
manifest.name
|
||||||
)
|
)
|
||||||
|
|
||||||
this.formDisclaimer = disclaimer
|
|
||||||
this.fields = fields
|
this.fields = fields
|
||||||
this.form = form
|
this.form = form
|
||||||
this.validations = { form: validations }
|
this.validations = { form: validations }
|
||||||
|
this.errors = errors
|
||||||
},
|
},
|
||||||
|
|
||||||
async performInstall () {
|
async performInstall () {
|
||||||
|
@ -120,7 +117,9 @@ export default {
|
||||||
this.$router.push({ name: 'app-list' })
|
this.$router.push({ name: 'app-list' })
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
this.serverError = err.message
|
if (err.data.name) {
|
||||||
|
this.errors[err.data.name].message = err.message
|
||||||
|
} else this.serverError = err.message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<tags-selectize
|
<tags-selectize-item
|
||||||
v-model="group.members" :options="usersOptions"
|
v-model="group.members" :options="usersOptions"
|
||||||
:id="groupName + '-users'" :label="$t('group_add_member')"
|
:id="groupName + '-users'" :label="$t('group_add_member')"
|
||||||
tag-icon="user" items-name="users"
|
tag-icon="user" items-name="users"
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
<strong>{{ $t('permissions') }}</strong>
|
<strong>{{ $t('permissions') }}</strong>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col>
|
<b-col>
|
||||||
<tags-selectize
|
<tags-selectize-item
|
||||||
v-model="group.permissions" :options="permissionsOptions"
|
v-model="group.permissions" :options="permissionsOptions"
|
||||||
:id="groupName + '-perms'" :label="$t('group_add_permission')"
|
:id="groupName + '-perms'" :label="$t('group_add_permission')"
|
||||||
tag-icon="key-modern" items-name="permissions"
|
tag-icon="key-modern" items-name="permissions"
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
</b-col>
|
</b-col>
|
||||||
|
|
||||||
<b-col>
|
<b-col>
|
||||||
<tags-selectize
|
<tags-selectize-item
|
||||||
v-model="userGroups[userName].permissions" :options="permissionsOptions"
|
v-model="userGroups[userName].permissions" :options="permissionsOptions"
|
||||||
:id="userName + '-perms'" :label="$t('group_add_permission')"
|
:id="userName + '-perms'" :label="$t('group_add_permission')"
|
||||||
tag-icon="key-modern" items-name="permissions"
|
tag-icon="key-modern" items-name="permissions"
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
<hr :key="index">
|
<hr :key="index">
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<tags-selectize
|
<tags-selectize-item
|
||||||
v-model="activeUserGroups" :options="usersOptions"
|
v-model="activeUserGroups" :options="usersOptions"
|
||||||
id="user-groups" :label="$t('group_add_member')"
|
id="user-groups" :label="$t('group_add_member')"
|
||||||
no-tags items-name="users"
|
no-tags items-name="users"
|
||||||
|
@ -109,7 +109,7 @@ import Vue from 'vue'
|
||||||
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { isEmptyValue } from '@/helpers/commons'
|
import { isEmptyValue } from '@/helpers/commons'
|
||||||
import TagsSelectize from '@/components/TagsSelectize'
|
import TagsSelectizeItem from '@/components/globals/formItems/TagsSelectizeItem'
|
||||||
|
|
||||||
// TODO add global search with type (search by: group, user, permission)
|
// TODO add global search with type (search by: group, user, permission)
|
||||||
// TODO add vuex store update on inputs ?
|
// TODO add vuex store update on inputs ?
|
||||||
|
@ -117,7 +117,7 @@ export default {
|
||||||
name: 'GroupList',
|
name: 'GroupList',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
TagsSelectize
|
TagsSelectizeItem
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
|
|
|
@ -205,12 +205,12 @@ export default {
|
||||||
label: this.$i18n.t('password'),
|
label: this.$i18n.t('password'),
|
||||||
description: this.$i18n.t('good_practices_about_user_password'),
|
description: this.$i18n.t('good_practices_about_user_password'),
|
||||||
descriptionVariant: 'warning',
|
descriptionVariant: 'warning',
|
||||||
props: { id: 'change_password', type: 'password', placeholder: '••••••••', autocomplete: "new-password" }
|
props: { id: 'change_password', type: 'password', placeholder: '••••••••', autocomplete: 'new-password' }
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmation: {
|
confirmation: {
|
||||||
label: this.$i18n.t('password_confirmation'),
|
label: this.$i18n.t('password_confirmation'),
|
||||||
props: { id: 'confirmation', type: 'password', placeholder: '••••••••', autocomplete: "new-password" }
|
props: { id: 'confirmation', type: 'password', placeholder: '••••••••', autocomplete: 'new-password' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
<icon iname="download" /> {{ $t('users_export') }}
|
<icon iname="download" /> {{ $t('users_export') }}
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<b-list-group>
|
<b-list-group>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const webpack = require('webpack')
|
const webpack = require('webpack')
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
const dateFnsLocales = [
|
const dateFnsLocales = [
|
||||||
'ar',
|
'ar',
|
||||||
|
@ -58,17 +59,20 @@ module.exports = {
|
||||||
},
|
},
|
||||||
publicPath: '/yunohost/admin',
|
publicPath: '/yunohost/admin',
|
||||||
devServer: {
|
devServer: {
|
||||||
|
public: fs.readFileSync('/etc/yunohost/current_host', 'utf8'),
|
||||||
https: false,
|
https: false,
|
||||||
disableHostCheck: true,
|
disableHostCheck: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'^/yunohost': {
|
'^/yunohost': {
|
||||||
target: `http://${process.env.VUE_APP_IP}`,
|
target: `http://${process.env.VUE_APP_IP}`,
|
||||||
ws: true,
|
ws: true,
|
||||||
logLevel: 'debug'
|
logLevel: 'info'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watchOptions: {
|
watchOptions: {
|
||||||
ignored: /node_modules/
|
ignored: /node_modules/,
|
||||||
}
|
aggregateTimeout: 300,
|
||||||
|
poll: 1000
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue