mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
commit
f624963458
22 changed files with 533 additions and 504 deletions
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<abstract-form
|
<abstract-form
|
||||||
v-bind="{ id: panel.id + '-form', validation, serverError: panel.serverError }"
|
v-bind="{ id: panel.id + '-form', validation, serverError: panel.serverError }"
|
||||||
@submit.prevent.stop="$emit('submit', panel.id)"
|
@submit.prevent.stop="onApply"
|
||||||
>
|
>
|
||||||
<slot name="tab-top" />
|
<slot name="tab-top" />
|
||||||
|
|
||||||
|
@ -12,18 +12,22 @@
|
||||||
<slot name="tab-before" />
|
<slot name="tab-before" />
|
||||||
|
|
||||||
<template v-for="section in panel.sections">
|
<template v-for="section in panel.sections">
|
||||||
<div v-if="isVisible(section.visible, section)" :key="section.id" class="mb-5">
|
<component
|
||||||
|
v-if="section.visible" :is="section.name ? 'section' : 'div'"
|
||||||
|
:key="section.id" class="mb-5"
|
||||||
|
>
|
||||||
<b-card-title v-if="section.name" title-tag="h3">
|
<b-card-title v-if="section.name" title-tag="h3">
|
||||||
{{ section.name }} <small v-if="section.help">{{ section.help }}</small>
|
{{ section.name }} <small v-if="section.help">{{ section.help }}</small>
|
||||||
</b-card-title>
|
</b-card-title>
|
||||||
|
|
||||||
<template v-for="(field, fname) in section.fields">
|
<template v-for="(field, fname) in section.fields">
|
||||||
<form-field
|
<component
|
||||||
v-if="isVisible(field.visible, field)" :key="fname"
|
v-if="field.visible" :is="field.is" v-bind="field.props"
|
||||||
v-model="forms[panel.id][fname]" v-bind="field" :validation="validation[fname]"
|
v-model="forms[panel.id][fname]" :validation="validation[fname]" :key="fname"
|
||||||
|
@action.stop="onAction(section.id, fname, section.fields)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<slot name="tab-after" />
|
<slot name="tab-after" />
|
||||||
|
@ -31,7 +35,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { configPanelsFieldIsVisible } from '@/helpers/yunohostArguments'
|
import { filterObject } from '@/helpers/commons'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -55,8 +59,25 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
isVisible (expression, field) {
|
onApply () {
|
||||||
return configPanelsFieldIsVisible(expression, field, this.forms)
|
const panelId = this.panel.id
|
||||||
|
|
||||||
|
this.$emit('submit', {
|
||||||
|
id: panelId,
|
||||||
|
form: this.forms[panelId]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction (sectionId, actionId, actionFields) {
|
||||||
|
const panelId = this.panel.id
|
||||||
|
const actionFieldsKeys = Object.keys(actionFields)
|
||||||
|
|
||||||
|
this.$emit('submit', {
|
||||||
|
id: panelId,
|
||||||
|
form: filterObject(this.forms[panelId], ([key]) => actionFieldsKeys.includes(key)),
|
||||||
|
action: [panelId, sectionId, actionId].join('.'),
|
||||||
|
name: actionId
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<routable-tabs
|
<routable-tabs
|
||||||
:routes="routes_"
|
:routes="routes_"
|
||||||
v-bind="{ panels, forms, v: $v }"
|
v-bind="{ panels, forms, v: $v, ...$attrs }"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -49,10 +49,3 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.card-title {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
border-bottom: solid 1px #aaa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -28,14 +28,17 @@
|
||||||
<!-- Render description -->
|
<!-- Render description -->
|
||||||
<template v-if="description || link">
|
<template v-if="description || link">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<b-link v-if="link" :to="link" :href="link.href"
|
<b-link
|
||||||
class="ml-auto"
|
v-if="link"
|
||||||
|
:to="link" :href="link.href" class="ml-auto"
|
||||||
>
|
>
|
||||||
{{ link.text }}
|
{{ link.text }}
|
||||||
</b-link>
|
</b-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<vue-showdown :markdown="description" flavor="github" v-if="description"
|
<vue-showdown
|
||||||
|
v-if="description"
|
||||||
|
:markdown="description" flavor="github"
|
||||||
:class="{ ['alert p-1 px-2 alert-' + descriptionVariant]: descriptionVariant }"
|
:class="{ ['alert p-1 px-2 alert-' + descriptionVariant]: descriptionVariant }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -76,8 +79,8 @@ export default {
|
||||||
if ('label' in attrs) {
|
if ('label' in attrs) {
|
||||||
const defaultAttrs = {
|
const defaultAttrs = {
|
||||||
'label-cols-md': 4,
|
'label-cols-md': 4,
|
||||||
'label-cols-lg': 2,
|
'label-cols-lg': 3,
|
||||||
'label-class': 'font-weight-bold'
|
'label-class': ['font-weight-bold', 'py-0']
|
||||||
}
|
}
|
||||||
if (!('label-cols' in attrs)) {
|
if (!('label-cols' in attrs)) {
|
||||||
for (const attr in defaultAttrs) {
|
for (const attr in defaultAttrs) {
|
||||||
|
|
65
app/src/components/globals/ReadOnlyField.vue
Normal file
65
app/src/components/globals/ReadOnlyField.vue
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<b-row no-gutters class="description-row">
|
||||||
|
<b-col v-bind="cols_" class="font-weight-bold">
|
||||||
|
{{ label }}
|
||||||
|
</b-col>
|
||||||
|
|
||||||
|
<!-- FIXME not sure about rendering html -->
|
||||||
|
<b-col v-html="text" />
|
||||||
|
</b-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ReadOnlyField',
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
label: { type: String, required: true },
|
||||||
|
component: { type: String, default: 'InputItem' },
|
||||||
|
value: { type: null, default: null },
|
||||||
|
cols: { type: Object, default: () => ({ md: 4, lg: 3 }) }
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
cols_ () {
|
||||||
|
return Object.assign({ md: 4, lg: 3 }, this.cols)
|
||||||
|
},
|
||||||
|
|
||||||
|
text () {
|
||||||
|
return this.parseValue(this.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseValue (value) {
|
||||||
|
const item = this.component
|
||||||
|
if (item === 'FileItem') value = value.file ? value.file.name : null
|
||||||
|
if (item === 'CheckboxItem') value = this.$i18n.t(value ? 'yes' : 'no')
|
||||||
|
if (item === 'TextAreaItem') value = value.replaceAll('\n', '<br>')
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value = value.length ? value.join(this.$i18n.t('words.separator')) : null
|
||||||
|
}
|
||||||
|
if ([null, undefined, ''].includes(this.value)) value = this.$i18n.t('words.none')
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.description-row {
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
flex-direction: column;
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: $border-width solid $card-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
40
app/src/components/globals/formItems/ButtonItem.vue
Normal file
40
app/src/components/globals/formItems/ButtonItem.vue
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<b-button
|
||||||
|
:id="id"
|
||||||
|
:variant="type"
|
||||||
|
@click="$emit('action', $event)"
|
||||||
|
:disabled="!enabled"
|
||||||
|
class="d-block mb-3"
|
||||||
|
>
|
||||||
|
<icon :iname="icon_" class="mr-2" />
|
||||||
|
<span v-html="label" />
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ButtonItem',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
label: { type: String, default: null },
|
||||||
|
id: { type: String, default: null },
|
||||||
|
type: { type: String, default: 'success' },
|
||||||
|
icon: { type: String, default: null },
|
||||||
|
enabled: { type: [Boolean, String], default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
icon_ () {
|
||||||
|
const icons = {
|
||||||
|
success: 'thumbs-up',
|
||||||
|
info: 'info',
|
||||||
|
warning: 'exclamation',
|
||||||
|
danger: 'times'
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.icon || icons[this.type]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
16
app/src/components/globals/formItems/DisplayTextItem.vue
Normal file
16
app/src/components/globals/formItems/DisplayTextItem.vue
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p v-text="label" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DisplayTextItem',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
id: { type: String, default: null },
|
||||||
|
label: { type: String, default: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,19 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<b-button-group class="w-100">
|
<b-button-group class="w-100">
|
||||||
<b-button @click="clearFiles" variant="danger" v-if="!this.required && this.value !== null && !this.value._removed">
|
<b-button
|
||||||
|
v-if="!this.required && this.value.file !== null"
|
||||||
|
@click="clearFiles" variant="danger"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ $t('delete') }}</span>
|
||||||
<icon iname="trash" />
|
<icon iname="trash" />
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|
||||||
<b-form-file
|
<b-form-file
|
||||||
v-model="file"
|
:value="value.file"
|
||||||
ref="input-file"
|
ref="input-file"
|
||||||
:id="id"
|
:id="id"
|
||||||
v-on="$listeners"
|
|
||||||
:required="required"
|
:required="required"
|
||||||
:placeholder="_placeholder"
|
:placeholder="_placeholder"
|
||||||
:accept="accept"
|
:accept="accept"
|
||||||
:drop-placeholder="dropPlaceholder"
|
:drop-placeholder="dropPlaceholder"
|
||||||
:state="state"
|
:state="state"
|
||||||
:browse-text="$t('words.browse')"
|
:browse-text="$t('words.browse')"
|
||||||
|
@input="onInput"
|
||||||
@blur="$parent.$emit('touch', name)"
|
@blur="$parent.$emit('touch', name)"
|
||||||
@focusout.native="$parent.$emit('touch', name)"
|
@focusout.native="$parent.$emit('touch', name)"
|
||||||
/>
|
/>
|
||||||
|
@ -21,18 +26,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { getFileContent } from '@/helpers/commons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileItem',
|
name: 'FileItem',
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
file: this.value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
value: { type: [File, null], default: null },
|
value: { type: Object, default: () => ({ file: null }) },
|
||||||
placeholder: { type: String, default: 'Choose a file or drop it here...' },
|
placeholder: { type: String, default: 'Choose a file or drop it here...' },
|
||||||
dropPlaceholder: { type: String, default: null },
|
dropPlaceholder: { type: String, default: null },
|
||||||
accept: { type: String, default: null },
|
accept: { type: String, default: null },
|
||||||
|
@ -43,22 +44,35 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
_placeholder: function () {
|
_placeholder: function () {
|
||||||
return (this.value === null) ? this.placeholder : this.value.name
|
return this.value.file === null ? this.placeholder : this.value.file.name
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
clearFiles () {
|
onInput (file) {
|
||||||
const f = new File([''], this.placeholder)
|
const value = {
|
||||||
f._removed = true
|
file,
|
||||||
if (this.value && this.value.currentfile) {
|
content: '',
|
||||||
this.$refs['input-file'].reset()
|
current: false,
|
||||||
this.$emit('input', f)
|
removed: false
|
||||||
} else {
|
|
||||||
this.$refs['input-file'].setFiles([f])
|
|
||||||
this.file = f
|
|
||||||
this.$emit('input', f)
|
|
||||||
}
|
}
|
||||||
|
// Update the value with the new File and an empty content for now
|
||||||
|
this.$emit('input', value)
|
||||||
|
|
||||||
|
// Asynchronously load the File content and update the value again
|
||||||
|
getFileContent(file).then(content => {
|
||||||
|
this.$emit('input', { ...value, content })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFiles () {
|
||||||
|
this.$refs['input-file'].reset()
|
||||||
|
this.$emit('input', {
|
||||||
|
file: null,
|
||||||
|
content: '',
|
||||||
|
current: false,
|
||||||
|
removed: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<b-input
|
<b-input
|
||||||
:value="value"
|
:value="value"
|
||||||
:id="id"
|
:id="id"
|
||||||
v-on="$listeners"
|
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:type="type"
|
:type="type"
|
||||||
:state="state"
|
:state="state"
|
||||||
|
@ -12,6 +11,7 @@
|
||||||
:step="step"
|
:step="step"
|
||||||
:trim="trim"
|
:trim="trim"
|
||||||
:autocomplete="autocomplete_"
|
:autocomplete="autocomplete_"
|
||||||
|
v-on="$listeners"
|
||||||
@blur="$parent.$emit('touch', name)"
|
@blur="$parent.$emit('touch', name)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -21,11 +21,6 @@
|
||||||
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 },
|
||||||
|
@ -40,6 +35,12 @@ export default {
|
||||||
autocomplete: { type: String, default: null },
|
autocomplete: { type: String, default: null },
|
||||||
pattern: { type: Object, default: null },
|
pattern: { type: Object, default: null },
|
||||||
name: { type: String, default: null }
|
name: { type: String, default: null }
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
autocomplete_: (this.autocomplete) ? this.autocomplete : (this.type === 'password') ? 'new-password' : null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,4 +12,3 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<b-alert class="d-flex" :variant="type" show>
|
<b-alert class="d-flex flex-column flex-md-row align-items-center" :variant="type" show>
|
||||||
<icon :iname="icon_" class="mr-1 mt-1" />
|
<icon :iname="icon_" class="mr-md-3 mb-md-0 mb-2" :variant="type" />
|
||||||
<vue-showdown :markdown="label" flavor="github"
|
|
||||||
|
<vue-showdown
|
||||||
|
:markdown="label" flavor="github"
|
||||||
tag="span" class="markdown"
|
tag="span" class="markdown"
|
||||||
/>
|
/>
|
||||||
</b-alert>
|
</b-alert>
|
||||||
|
@ -11,29 +13,23 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'ReadOnlyAlertItem',
|
name: 'ReadOnlyAlertItem',
|
||||||
|
|
||||||
data () {
|
|
||||||
const icons = {
|
|
||||||
success: 'thumbs-up',
|
|
||||||
info: 'info-circle',
|
|
||||||
warning: 'warning',
|
|
||||||
danger: 'times'
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
icon_: (this.icon) ? this.icon : icons[this.type]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
label: { type: String, default: null },
|
label: { type: String, default: null },
|
||||||
type: { type: String, default: null },
|
type: { type: String, default: null },
|
||||||
icon: { type: String, default: null }
|
icon: { type: String, default: null }
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
icon_ () {
|
||||||
|
const icons = {
|
||||||
|
success: 'thumbs-up',
|
||||||
|
info: 'info',
|
||||||
|
warning: 'exclamation',
|
||||||
|
danger: 'times'
|
||||||
|
}
|
||||||
|
return this.icon || icons[this.type]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.alert p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<b-form-textarea
|
<b-form-textarea
|
||||||
v-model="value"
|
:value="value"
|
||||||
:id="id"
|
:id="id"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:required="required"
|
:required="required"
|
||||||
|
|
|
@ -63,6 +63,19 @@ export function flattenObjectLiteral (obj, flattened = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an new Object filtered with passed filter function.
|
||||||
|
* Each entry `[key, value]` will be forwarded to the `filter` function.
|
||||||
|
*
|
||||||
|
* @param {Object} obj - object to filter.
|
||||||
|
* @param {Function} filter - the filter function to call for each entry.
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
export function filterObject (obj, filter) {
|
||||||
|
return Object.fromEntries(Object.entries(obj).filter((...args) => filter(...args)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an new array containing items that are in first array but not in the other.
|
* Returns an new array containing items that are in first array but not in the other.
|
||||||
*
|
*
|
||||||
|
@ -100,3 +113,26 @@ export function escapeHtml (unsafe) {
|
||||||
export function randint (min, max) {
|
export function randint (min, max) {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a File content.
|
||||||
|
*
|
||||||
|
* @param {File} file
|
||||||
|
* @param {Object} [extraParams] - Optionnal params
|
||||||
|
* @param {Boolean} [extraParams.base64] - returns a base64 representation of the file.
|
||||||
|
* @return {Promise<String>}
|
||||||
|
*/
|
||||||
|
export function getFileContent (file, { base64 = false } = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.onload = () => resolve(reader.result)
|
||||||
|
|
||||||
|
if (base64) {
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
} else {
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,12 @@ import i18n from '@/i18n'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import evaluate from 'simple-evaluate'
|
import evaluate from 'simple-evaluate'
|
||||||
import * as validators from '@/helpers/validators'
|
import * as validators from '@/helpers/validators'
|
||||||
import { isObjectLiteral, isEmptyValue, flattenObjectLiteral } from '@/helpers/commons'
|
import {
|
||||||
|
isObjectLiteral,
|
||||||
|
isEmptyValue,
|
||||||
|
flattenObjectLiteral,
|
||||||
|
getFileContent
|
||||||
|
} from '@/helpers/commons'
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,6 +54,49 @@ export function adressToFormValue (address) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate config panel string expression that can contain regular expressions.
|
||||||
|
* Expression are evaluated with the config panel form as context.
|
||||||
|
*
|
||||||
|
* @param {String} expression - A String to evaluate.
|
||||||
|
* @param {Object} forms - A nested form used in config panels.
|
||||||
|
* @return {Boolean} - expression evaluation result.
|
||||||
|
*/
|
||||||
|
export function evaluateExpression (expression, forms) {
|
||||||
|
if (!expression) return true
|
||||||
|
if (expression === '"false"') return false
|
||||||
|
|
||||||
|
const context = Object.values(forms).reduce((ctx, args) => {
|
||||||
|
Object.entries(args).forEach(([name, value]) => {
|
||||||
|
ctx[name] = isObjectLiteral(value) && 'file' in value ? value.content : value
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// Allow to use match(var,regexp) function
|
||||||
|
const matchRe = new RegExp('match\\(\\s*(\\w+)\\s*,\\s*"([^"]+)"\\s*\\)', 'g')
|
||||||
|
for (const matched of expression.matchAll(matchRe)) {
|
||||||
|
const [fullMatch, varMatch, regExpMatch] = matched
|
||||||
|
const varName = varMatch + '__re' + matched.index
|
||||||
|
context[varName] = new RegExp(regExpMatch, 'm').test(context[varMatch])
|
||||||
|
expression = expression.replace(fullMatch, varName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return !!evaluate(context, expression)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a property to an Object that will dynamically returns a expression evaluation result.
|
||||||
|
function addEvaluationGetter (prop, obj, expr, ctx) {
|
||||||
|
Object.defineProperty(obj, prop, {
|
||||||
|
get: () => evaluateExpression(expr, ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format app install, actions and config panel argument into a data structure that
|
* Format app install, actions and config panel argument into a data structure that
|
||||||
* will be automaticly transformed into a component on screen.
|
* will be automaticly transformed into a component on screen.
|
||||||
|
@ -62,22 +110,21 @@ export function formatYunoHostArgument (arg) {
|
||||||
const error = { message: null }
|
const error = { message: null }
|
||||||
arg.ask = formatI18nField(arg.ask)
|
arg.ask = formatI18nField(arg.ask)
|
||||||
const field = {
|
const field = {
|
||||||
component: undefined,
|
is: arg.readonly ? 'ReadOnlyField' : 'FormField',
|
||||||
|
visible: [undefined, true, '"true"'].includes(arg.visible),
|
||||||
|
props: {
|
||||||
label: arg.ask,
|
label: arg.ask,
|
||||||
|
component: undefined,
|
||||||
props: {}
|
props: {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const defaultProps = ['id:name', 'placeholder:example']
|
const defaultProps = ['id:name', 'placeholder:example']
|
||||||
const components = [
|
const components = [
|
||||||
{
|
{
|
||||||
types: [undefined, 'string', 'path'],
|
types: ['string', 'path'],
|
||||||
name: 'InputItem',
|
name: 'InputItem',
|
||||||
props: defaultProps.concat(['autocomplete', 'trim', 'choices']),
|
props: defaultProps.concat(['autocomplete', 'trim', 'choices'])
|
||||||
callback: function () {
|
|
||||||
if (arg.choices && Object.keys(arg.choices).length) {
|
|
||||||
arg.type = 'select'
|
|
||||||
this.name = 'SelectItem'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['email', 'url', 'date', 'time', 'color'],
|
types: ['email', 'url', 'date', 'time', 'color'],
|
||||||
|
@ -115,8 +162,8 @@ export function formatYunoHostArgument (arg) {
|
||||||
name: 'SelectItem',
|
name: 'SelectItem',
|
||||||
props: ['id:name', 'choices'],
|
props: ['id:name', 'choices'],
|
||||||
callback: function () {
|
callback: function () {
|
||||||
if ((arg.type !== 'select')) {
|
if (arg.type !== 'select') {
|
||||||
field.link = { name: arg.type + '-list', text: i18n.t(`manage_${arg.type}s`) }
|
field.props.link = { name: arg.type + '-list', text: i18n.t(`manage_${arg.type}s`) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -125,9 +172,12 @@ export function formatYunoHostArgument (arg) {
|
||||||
name: 'FileItem',
|
name: 'FileItem',
|
||||||
props: defaultProps.concat(['accept']),
|
props: defaultProps.concat(['accept']),
|
||||||
callback: function () {
|
callback: function () {
|
||||||
if (value) {
|
value = {
|
||||||
value = new File([''], value)
|
// in case of already defined file, we receive only the file path (not the actual file)
|
||||||
value.currentfile = true
|
file: value ? new File([''], value) : null,
|
||||||
|
content: '',
|
||||||
|
current: !!value,
|
||||||
|
removed: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -141,11 +191,13 @@ export function formatYunoHostArgument (arg) {
|
||||||
name: 'TagsItem',
|
name: 'TagsItem',
|
||||||
props: defaultProps.concat(['limit', 'placeholder', 'options:choices', 'tagIcon:icon']),
|
props: defaultProps.concat(['limit', 'placeholder', 'options:choices', 'tagIcon:icon']),
|
||||||
callback: function () {
|
callback: function () {
|
||||||
if (arg.choices) {
|
if (arg.choices && arg.choices.length) {
|
||||||
this.name = 'TagsSelectizeItem'
|
this.name = 'TagsSelectizeItem'
|
||||||
field.props.auto = true
|
Object.assign(field.props.props, {
|
||||||
field.props.itemsName = ''
|
auto: true,
|
||||||
field.props.label = arg.placeholder
|
itemsName: '',
|
||||||
|
label: arg.placeholder
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
value = value.split(',')
|
value = value.split(',')
|
||||||
|
@ -170,53 +222,67 @@ export function formatYunoHostArgument (arg) {
|
||||||
types: ['alert'],
|
types: ['alert'],
|
||||||
name: 'ReadOnlyAlertItem',
|
name: 'ReadOnlyAlertItem',
|
||||||
props: ['type:style', 'label:ask', 'icon'],
|
props: ['type:style', 'label:ask', 'icon'],
|
||||||
readonly: true
|
renderSelf: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['markdown', 'display_text'],
|
types: ['markdown'],
|
||||||
name: 'MarkdownItem',
|
name: 'MarkdownItem',
|
||||||
props: ['label:ask'],
|
props: ['label:ask'],
|
||||||
readonly: true
|
renderSelf: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['display_text'],
|
||||||
|
name: 'DisplayTextItem',
|
||||||
|
props: ['label:ask'],
|
||||||
|
renderSelf: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ['button'],
|
||||||
|
name: 'ButtonItem',
|
||||||
|
props: ['type:style', 'label:ask', 'icon', 'enabled'],
|
||||||
|
renderSelf: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// Default type management if no one is filled
|
// Default type management if no one is filled
|
||||||
if (arg.type === undefined) {
|
if (arg.type === undefined) {
|
||||||
arg.type = (arg.choices === undefined) ? 'string' : 'select'
|
arg.type = arg.choices && arg.choices.length ? 'select' : 'string'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search the component bind to the type
|
// Search the component bind to the type
|
||||||
const component = components.find(element => element.types.includes(arg.type))
|
const component = components.find(element => element.types.includes(arg.type))
|
||||||
if (component === undefined) throw new TypeError('Unknown type: ' + arg.type)
|
if (component === undefined) throw new TypeError('Unknown type: ' + arg.type)
|
||||||
|
|
||||||
// Callback use for specific behaviour
|
// Callback use for specific behaviour
|
||||||
if (component.callback) component.callback()
|
if (component.callback) component.callback()
|
||||||
field.component = component.name
|
field.props.component = component.name
|
||||||
// Affect properties to the field Item
|
// Affect properties to the field Item
|
||||||
for (let prop of component.props) {
|
for (let prop of component.props) {
|
||||||
prop = prop.split(':')
|
prop = prop.split(':')
|
||||||
const propName = prop[0]
|
const propName = prop[0]
|
||||||
const argName = prop.slice(-1)[0]
|
const argName = prop.slice(-1)[0]
|
||||||
if (argName in arg) {
|
if (argName in arg) {
|
||||||
field.props[propName] = arg[argName]
|
field.props.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)
|
||||||
else if (field.component !== 'CheckboxItem' && arg.optional !== true) {
|
if (!component.renderSelf && arg.type !== 'boolean' && arg.optional !== true) {
|
||||||
validation.required = validators.required
|
validation.required = validators.required
|
||||||
}
|
}
|
||||||
if (arg.pattern && arg.type !== 'tags') {
|
if (arg.pattern && arg.type !== 'tags') {
|
||||||
validation.pattern = validators.helpers.regex(formatI18nField(arg.pattern.error), new RegExp(arg.pattern.regexp))
|
validation.pattern = validators.helpers.regex(formatI18nField(arg.pattern.error), new RegExp(arg.pattern.regexp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!component.renderSelf && !arg.readonly) {
|
||||||
|
// Bind a validation with what the server may respond
|
||||||
validation.remote = validators.helpers.withParams(error, (v) => {
|
validation.remote = validators.helpers.withParams(error, (v) => {
|
||||||
const result = !error.message
|
const result = !error.message
|
||||||
error.message = null
|
error.message = null
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// field.props['title'] = field.pattern.error
|
|
||||||
// Default value if still `null`
|
// Default value if still `null`
|
||||||
if (value === null && arg.current_value) {
|
if (value === null && arg.current_value) {
|
||||||
value = arg.current_value
|
value = arg.current_value
|
||||||
|
@ -227,18 +293,17 @@ export function formatYunoHostArgument (arg) {
|
||||||
|
|
||||||
// Help message
|
// Help message
|
||||||
if (arg.help) {
|
if (arg.help) {
|
||||||
field.description = formatI18nField(arg.help)
|
field.props.description = formatI18nField(arg.help)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help message
|
// Help message
|
||||||
if (arg.helpLink) {
|
if (arg.helpLink) {
|
||||||
field.link = { href: arg.helpLink.href, text: i18n.t(arg.helpLink.text) }
|
field.props.link = { href: arg.helpLink.href, text: i18n.t(arg.helpLink.text) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.visible) {
|
if (component.renderSelf) {
|
||||||
field.visible = arg.visible
|
field.is = field.props.component
|
||||||
// Temporary value to wait visible expression to be evaluated
|
field.props = field.props.props
|
||||||
field.isVisible = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -256,30 +321,29 @@ export function formatYunoHostArgument (arg) {
|
||||||
* as v-model values, fields that can be passed to a FormField component and validations.
|
* as v-model values, fields that can be passed to a FormField component and validations.
|
||||||
*
|
*
|
||||||
* @param {Array} args - a yunohost arg array written by a packager.
|
* @param {Array} args - a yunohost arg array written by a packager.
|
||||||
* @param {String} name - (temp) an app name to build a label field in case of manifest install args
|
* @param {Object|null} forms - nested form used as the expression evualuations context.
|
||||||
* @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, forms) {
|
||||||
const form = {}
|
const form = {}
|
||||||
const fields = {}
|
const fields = {}
|
||||||
const validations = {}
|
const validations = {}
|
||||||
const errors = {}
|
const errors = {}
|
||||||
|
|
||||||
// FIXME yunohost should add the label field by default
|
|
||||||
if (name) {
|
|
||||||
args.unshift({
|
|
||||||
ask: i18n.t('label_for_manifestname', { name }),
|
|
||||||
default: name,
|
|
||||||
name: 'label'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
const { value, field, validation, error } = formatYunoHostArgument(arg)
|
const { value, field, validation, error } = 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
|
errors[arg.name] = error
|
||||||
|
|
||||||
|
if ('visible' in arg && ![false, '"false"'].includes(arg.visible)) {
|
||||||
|
addEvaluationGetter('visible', field, arg.visible, forms)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('enabled' in arg) {
|
||||||
|
addEvaluationGetter('enabled', field.props, arg.enabled, forms)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { form, fields, validations, errors }
|
return { form, fields, validations, errors }
|
||||||
|
@ -303,11 +367,24 @@ export function formatYunoHostConfigPanels (data) {
|
||||||
if (name) panel.name = formatI18nField(name)
|
if (name) panel.name = formatI18nField(name)
|
||||||
if (help) panel.help = formatI18nField(help)
|
if (help) panel.help = formatI18nField(help)
|
||||||
|
|
||||||
for (const { id: sectionId, name, help, visible, options } of sections) {
|
for (const _section of sections) {
|
||||||
const section = { id: sectionId, visible, isVisible: false }
|
const section = {
|
||||||
if (help) section.help = formatI18nField(help)
|
id: _section.id,
|
||||||
if (name) section.name = formatI18nField(name)
|
isActionSection: _section.is_action_section,
|
||||||
const { form, fields, validations, errors } = formatYunoHostArguments(options)
|
visible: [undefined, true, '"true"'].includes(_section.visible)
|
||||||
|
}
|
||||||
|
if (_section.help) section.help = formatI18nField(_section.help)
|
||||||
|
if (_section.name) section.name = formatI18nField(_section.name)
|
||||||
|
if (_section.visible && ![false, '"false"'].includes(_section.visible)) {
|
||||||
|
addEvaluationGetter('visible', section, _section.visible, result.forms)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
fields,
|
||||||
|
validations,
|
||||||
|
errors
|
||||||
|
} = formatYunoHostArguments(_section.options, result.forms)
|
||||||
// Merge all sections forms to the panel to get a unique form
|
// Merge all sections forms to the panel to get a unique form
|
||||||
Object.assign(result.forms[panelId], form)
|
Object.assign(result.forms[panelId], form)
|
||||||
Object.assign(result.validations[panelId], validations)
|
Object.assign(result.validations[panelId], validations)
|
||||||
|
@ -323,80 +400,65 @@ export function formatYunoHostConfigPanels (data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function configPanelsFieldIsVisible (expression, field, forms) {
|
|
||||||
if (!expression || !field) return true
|
|
||||||
const context = {}
|
|
||||||
|
|
||||||
const promises = []
|
|
||||||
for (const args of Object.values(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
|
|
||||||
Promise.all(promises).then(() => {
|
|
||||||
for (const matched of expression.matchAll(matchRe)) {
|
|
||||||
i++
|
|
||||||
const varName = matched[1] + '__re' + i.toString()
|
|
||||||
context[varName] = new RegExp(matched[2], 'm').test(context[matched[1]])
|
|
||||||
expression = expression.replace(matched[0], varName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
field.isVisible = evaluate(context, expression)
|
|
||||||
} catch {
|
|
||||||
field.isVisible = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return field.isVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
* Parse a front-end value to its API equivalent. This function returns a Promise or an
|
||||||
* Convert Boolean to (1|0) and concatenate adresses.
|
* Object `{ key: Promise }` if `key` is supplied. When parsing a form, all those
|
||||||
|
* objects must be merged to define the final sent form.
|
||||||
|
*
|
||||||
|
* Convert Boolean to '1' (true) or '0' (false),
|
||||||
|
* Concatenate two parts adresses (subdomain or email for example) into a single string,
|
||||||
|
* Convert File to its Base64 representation or set its value to '' to ask for a removal.
|
||||||
*
|
*
|
||||||
* @param {*} value
|
* @param {*} value
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
export function formatFormDataValue (value) {
|
export function formatFormDataValue (value, key = null) {
|
||||||
if (typeof value === 'boolean') {
|
if (Array.isArray(value)) {
|
||||||
return value ? 1 : 0
|
return Promise.all(
|
||||||
} else if (isObjectLiteral(value) && 'separator' in value) {
|
value.map(value_ => formatFormDataValue(value_))
|
||||||
return Object.values(value).join('')
|
).then(resolvedValues => ({ [key]: resolvedValues }))
|
||||||
}
|
}
|
||||||
return value
|
|
||||||
|
let result = value
|
||||||
|
if (typeof value === 'boolean') result = value ? 1 : 0
|
||||||
|
if (isObjectLiteral(value) && 'file' in value) {
|
||||||
|
// File has to be deleted
|
||||||
|
if (value.removed) result = ''
|
||||||
|
// File has not changed (will not be sent)
|
||||||
|
else if (value.current || value.file === null) result = null
|
||||||
|
else {
|
||||||
|
return getFileContent(value.file, { base64: true }).then(content => {
|
||||||
|
return {
|
||||||
|
[key]: content.replace(/data:[^;]*;base64,/, ''),
|
||||||
|
[key + '[name]']: value.file.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (isObjectLiteral(value) && 'separator' in value) {
|
||||||
|
result = Object.values(value).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a resolved Promise for non async values
|
||||||
|
return Promise.resolve(key ? { [key]: result } : result)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convinient helper to properly parse a front-end form to its API equivalent.
|
||||||
|
* This parse each values asynchronously, allow to inject keys into the final form and
|
||||||
|
* make sure every async values resolves before resolving itself.
|
||||||
|
*
|
||||||
|
* @param {Object} formData
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
function formatFormDataValues (formData) {
|
||||||
|
const promisedValues = Object.entries(formData).map(([key, value]) => {
|
||||||
|
return formatFormDataValue(value, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.all(promisedValues).then(resolvedValues => {
|
||||||
|
return resolvedValues.reduce((form, obj) => ({ ...form, ...obj }), {})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -412,38 +474,28 @@ export function formatFormDataValue (value) {
|
||||||
*/
|
*/
|
||||||
export async function formatFormData (
|
export async function formatFormData (
|
||||||
formData,
|
formData,
|
||||||
{ extract = null, flatten = false, removeEmpty = true, removeNull = false, multipart = true } = {}
|
{ extract = null, flatten = false, removeEmpty = true, removeNull = false } = {}
|
||||||
) {
|
) {
|
||||||
const output = {
|
const output = {
|
||||||
data: {},
|
data: {},
|
||||||
extracted: {}
|
extracted: {}
|
||||||
}
|
}
|
||||||
const promises = []
|
|
||||||
for (const key in formData) {
|
|
||||||
const type = extract && extract.includes(key) ? 'extracted' : 'data'
|
|
||||||
const value = Array.isArray(formData[key])
|
|
||||||
? formData[key].map(item => formatFormDataValue(item))
|
|
||||||
: formatFormDataValue(formData[key])
|
|
||||||
|
|
||||||
|
const values = await formatFormDataValues(formData)
|
||||||
|
for (const key in values) {
|
||||||
|
const type = extract && extract.includes(key) ? 'extracted' : 'data'
|
||||||
|
const value = values[key]
|
||||||
if (removeEmpty && isEmptyValue(value)) {
|
if (removeEmpty && isEmptyValue(value)) {
|
||||||
continue
|
continue
|
||||||
} else if (removeNull && (value === null || value === undefined)) {
|
} else if (removeNull && [null, undefined].includes(value)) {
|
||||||
continue
|
continue
|
||||||
} else if (value instanceof File && !multipart) {
|
|
||||||
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 {
|
||||||
output[type][key] = value
|
output[type][key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (promises.length) await Promise.all(promises)
|
|
||||||
const { data, extracted } = output
|
const { data, extracted } = output
|
||||||
return extract ? { data, ...extracted } : data
|
return extract ? { data, ...extracted } : data
|
||||||
}
|
}
|
||||||
|
|
|
@ -378,13 +378,14 @@
|
||||||
"human_routes": {
|
"human_routes": {
|
||||||
"adminpw": "Change admin password",
|
"adminpw": "Change admin password",
|
||||||
"apps": {
|
"apps": {
|
||||||
|
"action_config": "Run action '{action}' of app '{name}' configuration",
|
||||||
"change_label": "Change label of '{prevName}' for '{nextName}'",
|
"change_label": "Change label of '{prevName}' for '{nextName}'",
|
||||||
"change_url": "Change access URL of '{name}'",
|
"change_url": "Change access URL of '{name}'",
|
||||||
"install": "Install app '{name}'",
|
"install": "Install app '{name}'",
|
||||||
"set_default": "Redirect '{domain}' domain root to '{name}'",
|
"set_default": "Redirect '{domain}' domain root to '{name}'",
|
||||||
"perform_action": "Perform action '{action}' of app '{name}'",
|
"perform_action": "Perform action '{action}' of app '{name}'",
|
||||||
"uninstall": "Uninstall app '{name}'",
|
"uninstall": "Uninstall app '{name}'",
|
||||||
"update_config": "Update app '{name}' configuration"
|
"update_config": "Update panel '{id}' of app '{name}' configuration"
|
||||||
},
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
"create": "Create a backup",
|
"create": "Create a backup",
|
||||||
|
@ -406,13 +407,11 @@
|
||||||
"domains": {
|
"domains": {
|
||||||
"add": "Add domain '{name}'",
|
"add": "Add domain '{name}'",
|
||||||
"delete": "Delete domain '{name}'",
|
"delete": "Delete domain '{name}'",
|
||||||
"install_LE": "Install certificate for '{name}'",
|
"cert_install": "Install certificate for '{name}'",
|
||||||
"manual_renew_LE": "Renew certificate for '{name}'",
|
"cert_renew": "Renew certificate for '{name}'",
|
||||||
"push_dns_changes": "Push DNS records to registrar for '{name}'",
|
"push_dns_changes": "Push DNS records to registrar for '{name}'",
|
||||||
"regen_selfsigned": "Renew self-signed certificate for '{name}'",
|
|
||||||
"revert_to_selfsigned": "Revert to self-signed certificate for '{name}'",
|
|
||||||
"set_default": "Set '{name}' as default domain",
|
"set_default": "Set '{name}' as default domain",
|
||||||
"update_config": "Update '{name}' configuration"
|
"update_config": "Update panel '{id}' of domain '{name}' configuration"
|
||||||
},
|
},
|
||||||
"firewall": {
|
"firewall": {
|
||||||
"ports": "{action} port {port} ({protocol}, {connection})",
|
"ports": "{action} port {port} ({protocol}, {connection})",
|
||||||
|
@ -548,37 +547,16 @@
|
||||||
"words": {
|
"words": {
|
||||||
"browse": "Browse",
|
"browse": "Browse",
|
||||||
"collapse": "Collapse",
|
"collapse": "Collapse",
|
||||||
"default": "Default"
|
"default": "Default",
|
||||||
|
"none": "None",
|
||||||
|
"separator": ", "
|
||||||
},
|
},
|
||||||
"wrong_password": "Wrong password",
|
"wrong_password": "Wrong password",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"yunohost_admin": "YunoHost Admin",
|
"yunohost_admin": "YunoHost Admin",
|
||||||
"certificate_alert_not_valid": "CRITICAL: Current certificate is not valid! HTTPS won't work at all!",
|
|
||||||
"certificate_alert_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!",
|
|
||||||
"certificate_alert_letsencrypt_about_to_expire": "Current certificate is about to expire. It should soon be renewed automatically.",
|
|
||||||
"certificate_alert_about_to_expire": "WARNING: Current certificate is about to expire! It will NOT be renewed automatically!",
|
|
||||||
"certificate_alert_good": "Okay, current certificate looks good!",
|
|
||||||
"certificate_alert_great": "Great! You're using a valid Let's Encrypt certificate!",
|
|
||||||
"certificate_alert_unknown": "Unknown status",
|
|
||||||
"certificate_manage": "Manage SSL certificate",
|
"certificate_manage": "Manage SSL certificate",
|
||||||
"ssl_certificate": "SSL certificate",
|
"ssl_certificate": "SSL certificate",
|
||||||
"confirm_cert_install_LE": "Are you sure you want to install a Let's Encrypt certificate for this domain?",
|
|
||||||
"confirm_cert_regen_selfsigned": "Are you sure you want to regenerate a self-signed certificate for this domain?",
|
|
||||||
"confirm_cert_manual_renew_LE": "Are you sure you want to manually renew the Let's Encrypt certificate for this domain now?",
|
|
||||||
"confirm_cert_revert_to_selfsigned": "Are you sure you want to revert this domain to a self-signed certificate?",
|
|
||||||
"certificate": "Certificate",
|
"certificate": "Certificate",
|
||||||
"certificate_status": "Certificate status",
|
|
||||||
"certificate_authority": "Certification authority",
|
|
||||||
"validity": "Validity",
|
|
||||||
"domain_is_eligible_for_ACME": "This domain seems correctly configured to install a Let's Encrypt certificate!",
|
|
||||||
"domain_not_eligible_for_ACME": "This domain doesn't seem ready for a Let's Encrypt certificate. Please check your DNS configuration and HTTP server reachability. The 'DNS records' and 'Web' section in <a href='#/diagnosis'>the diagnosis page</a> can help you understand what is misconfigured.",
|
|
||||||
"install_letsencrypt_cert": "Install a Let's Encrypt certificate",
|
|
||||||
"manually_renew_letsencrypt_message": "Certificate will be automatically renewed during the last 15 days of validity. You can manually renew it if you want to. (Not recommended).",
|
|
||||||
"manually_renew_letsencrypt": "Manually renew now",
|
|
||||||
"regenerate_selfsigned_cert_message": "If you want, you can regenerate the self-signed certificate.",
|
|
||||||
"regenerate_selfsigned_cert": "Regenerate self-signed certificate",
|
|
||||||
"revert_to_selfsigned_cert_message": "If you really want to, you can reinstall a self-signed certificate. (Not recommended)",
|
|
||||||
"revert_to_selfsigned_cert": "Revert to a self-signed certificate",
|
|
||||||
"purge_user_data_checkbox": "Purge {name}'s data? (This will remove the content of its home and mail directories.)",
|
"purge_user_data_checkbox": "Purge {name}'s data? (This will remove the content of its home and mail directories.)",
|
||||||
"purge_user_data_warning": "Purging user's data is not reversible. Be sure you know what you're doing!"
|
"purge_user_data_warning": "Purging user's data is not reversible. Be sure you know what you're doing!"
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,16 +179,6 @@ const routes = [
|
||||||
breadcrumb: ['domain-list', 'domain-info', 'domain-dns']
|
breadcrumb: ['domain-list', 'domain-info', 'domain-dns']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'domain-cert',
|
|
||||||
path: '/domains/:name/cert-management',
|
|
||||||
component: () => import(/* webpackChunkName: "views/domain/cert" */ '@/views/domain/DomainCert'),
|
|
||||||
props: true,
|
|
||||||
meta: {
|
|
||||||
args: { trad: 'certificate' },
|
|
||||||
breadcrumb: ['domain-list', 'domain-info', 'domain-cert']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ───────╮
|
/* ───────╮
|
||||||
│ APPS │
|
│ APPS │
|
||||||
|
|
|
@ -102,6 +102,11 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3.card-title {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border-bottom: solid 1px #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
// collapse icon
|
// collapse icon
|
||||||
.not-collapsed > .icon {
|
.not-collapsed > .icon {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
|
@ -165,6 +170,10 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background: ghostwhite;
|
background: ghostwhite;
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,12 +126,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit () {
|
async onSubmit () {
|
||||||
const domainType = this.selected
|
const domainType = this.selected
|
||||||
this.$emit('submit', {
|
const domain = await formatFormDataValue(this.form[domainType])
|
||||||
domain: formatFormDataValue(this.form[domainType]),
|
this.$emit('submit', { domain, domainType })
|
||||||
domainType
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
:queries="queries" @queries-response="onQueriesResponse"
|
:queries="queries" @queries-response="onQueriesResponse"
|
||||||
ref="view" skeleton="card-form-skeleton"
|
ref="view" skeleton="card-form-skeleton"
|
||||||
>
|
>
|
||||||
<config-panels v-if="config.panels" v-bind="config" @submit="applyConfig" />
|
<config-panels
|
||||||
|
v-if="config.panels" v-bind="config"
|
||||||
|
@submit="onConfigSubmit"
|
||||||
|
/>
|
||||||
|
|
||||||
<b-alert v-else-if="config.panels === null" variant="warning">
|
<b-alert v-else-if="config.panels === null" variant="warning">
|
||||||
<icon iname="exclamation-triangle" /> {{ $t('app_config_panel_no_panel') }}
|
<icon iname="exclamation-triangle" /> {{ $t('app_config_panel_no_panel') }}
|
||||||
|
@ -34,7 +37,7 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['GET', `apps/${this.id}/config-panel?full`]
|
['GET', `apps/${this.id}/config?full`]
|
||||||
],
|
],
|
||||||
config: {}
|
config: {}
|
||||||
}
|
}
|
||||||
|
@ -49,23 +52,22 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async applyConfig (id_) {
|
async onConfigSubmit ({ id, form, action, name }) {
|
||||||
const formatedData = await formatFormData(
|
const args = await formatFormData(form, { removeEmpty: false, removeNull: true })
|
||||||
this.config.forms[id_],
|
|
||||||
{ removeEmpty: false, removeNull: true, multipart: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
api.put(
|
api.put(
|
||||||
`apps/${this.id}/config`,
|
action
|
||||||
{ key: id_, args: objectToParams(formatedData) },
|
? `apps/${this.id}/actions/${action}`
|
||||||
{ key: 'apps.update_config', name: this.id }
|
: `apps/${this.id}/config/${id}`,
|
||||||
).then(response => {
|
{ args: objectToParams(args) },
|
||||||
|
{ key: `apps.${action ? 'action' : 'update'}_config`, id, name: this.id }
|
||||||
|
).then(() => {
|
||||||
this.$refs.view.fetchQueries({ triggerLoading: true })
|
this.$refs.view.fetchQueries({ triggerLoading: true })
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
const panel = this.config.panels.find(({ id }) => id_ === id)
|
const panel = this.config.panels.find(panel => panel.id === id)
|
||||||
if (err.data.name) {
|
if (err.data.name) {
|
||||||
this.config.errors[id_][err.data.name].message = err.message
|
this.config.errors[id][err.data.name].message = err.message
|
||||||
} else this.$set(panel, 'serverError', err.message)
|
} else this.$set(panel, 'serverError', err.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,9 @@
|
||||||
@submit.prevent="performInstall"
|
@submit.prevent="performInstall"
|
||||||
>
|
>
|
||||||
<template v-for="(field, fname) in fields">
|
<template v-for="(field, fname) in fields">
|
||||||
<form-field
|
<component
|
||||||
v-if="isVisible(field.visible, field)"
|
v-if="field.visible" :is="field.is" v-bind="field.props"
|
||||||
:key="fname" label-cols="0"
|
v-model="form[fname]" :validation="$v.form[fname]" :key="fname"
|
||||||
v-bind="field" v-model="form[fname]" :validation="$v.form[fname]"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</card-form>
|
</card-form>
|
||||||
|
@ -47,10 +46,13 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { validationMixin } from 'vuelidate'
|
import { validationMixin } from 'vuelidate'
|
||||||
import evaluate from 'simple-evaluate'
|
|
||||||
|
|
||||||
import api, { objectToParams } from '@/api'
|
import api, { objectToParams } from '@/api'
|
||||||
import { formatYunoHostArguments, formatI18nField, formatFormData, pFileReader } from '@/helpers/yunohostArguments'
|
import {
|
||||||
|
formatYunoHostArguments,
|
||||||
|
formatI18nField,
|
||||||
|
formatFormData
|
||||||
|
} from '@/helpers/yunohostArguments'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppInstall',
|
name: 'AppInstall',
|
||||||
|
@ -92,10 +94,19 @@ 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, errors } = formatYunoHostArguments(
|
// FIXME yunohost should add the label field by default
|
||||||
manifest.arguments.install,
|
manifest.arguments.install.unshift({
|
||||||
manifest.name
|
ask: this.$t('label_for_manifestname', { name: manifest.name }),
|
||||||
)
|
default: manifest.name,
|
||||||
|
name: 'label'
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
fields,
|
||||||
|
validations,
|
||||||
|
errors
|
||||||
|
} = formatYunoHostArguments(manifest.arguments.install)
|
||||||
|
|
||||||
this.fields = fields
|
this.fields = fields
|
||||||
this.form = form
|
this.form = form
|
||||||
|
@ -103,41 +114,6 @@ export default {
|
||||||
this.errors = errors
|
this.errors = errors
|
||||||
},
|
},
|
||||||
|
|
||||||
isVisible (expression, field) {
|
|
||||||
if (!expression || !field) return true
|
|
||||||
const context = {}
|
|
||||||
|
|
||||||
const promises = []
|
|
||||||
for (const shortname in this.form) {
|
|
||||||
if (this.form[shortname] instanceof File) {
|
|
||||||
if (expression.includes(shortname)) {
|
|
||||||
promises.push(pFileReader(this.form[shortname], context, shortname, false))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context[shortname] = this.form[shortname]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Allow to use match(var,regexp) function
|
|
||||||
const matchRe = new RegExp('match\\(\\s*(\\w+)\\s*,\\s*"([^"]+)"\\s*\\)', 'g')
|
|
||||||
let i = 0
|
|
||||||
Promise.all(promises).then(() => {
|
|
||||||
for (const matched of expression.matchAll(matchRe)) {
|
|
||||||
i++
|
|
||||||
const varName = matched[1] + '__re' + i.toString()
|
|
||||||
context[varName] = new RegExp(matched[2], 'm').test(context[matched[1]])
|
|
||||||
expression = expression.replace(matched[0], varName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
field.isVisible = evaluate(context, expression)
|
|
||||||
} catch (error) {
|
|
||||||
field.isVisible = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// This value should be updated magically when vuejs will detect isVisible changed
|
|
||||||
return field.isVisible
|
|
||||||
},
|
|
||||||
|
|
||||||
async performInstall () {
|
async performInstall () {
|
||||||
if ('path' in this.form && this.form.path === '/') {
|
if ('path' in this.form && this.form.path === '/') {
|
||||||
const confirmed = await this.$askConfirmation(
|
const confirmed = await this.$askConfirmation(
|
||||||
|
@ -148,7 +124,7 @@ export default {
|
||||||
|
|
||||||
const { data: args, label } = await formatFormData(
|
const { data: args, label } = await formatFormData(
|
||||||
this.form,
|
this.form,
|
||||||
{ extract: ['label'], removeEmpty: false, removeNull: true, multipart: false }
|
{ extract: ['label'], removeEmpty: false, removeNull: true }
|
||||||
)
|
)
|
||||||
const data = { app: this.id, label, args: Object.entries(args).length ? objectToParams(args) : undefined }
|
const data = { app: this.id, label, args: Object.entries(args).length ? objectToParams(args) : undefined }
|
||||||
|
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
<template>
|
|
||||||
<view-base :queries="queries" @queries-response="onQueriesResponse" ref="view">
|
|
||||||
<card v-if="cert" :title="$t('certificate_status')" icon="lock">
|
|
||||||
<p :class="'alert alert-' + cert.alert.type">
|
|
||||||
<icon :iname="cert.alert.icon" /> {{ $t('certificate_alert_' + cert.alert.trad) }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<b-row no-gutters class="row-line">
|
|
||||||
<b-col md="4" xl="2">
|
|
||||||
<strong v-t="'certificate_authority'" />
|
|
||||||
</b-col>
|
|
||||||
<b-col>{{ cert.type }} ({{ name }})</b-col>
|
|
||||||
</b-row>
|
|
||||||
|
|
||||||
<b-row no-gutters class="row-line">
|
|
||||||
<b-col md="4" xl="2">
|
|
||||||
<strong v-t="'validity'" />
|
|
||||||
</b-col>
|
|
||||||
<b-col>{{ $tc('day_validity', cert.validity) }}</b-col>
|
|
||||||
</b-row>
|
|
||||||
</card>
|
|
||||||
|
|
||||||
<card v-if="cert" :title="$t('operations')" icon="wrench">
|
|
||||||
<!-- CERT INSTALL LETSENCRYPT -->
|
|
||||||
<template v-if="actionsEnabled.installLetsencrypt">
|
|
||||||
<p>
|
|
||||||
<icon :iname="cert.acmeEligible ? 'check' : 'meh-o'" /> <span v-html="$t(`domain_${cert.acmeEligible ? 'is' : 'not'}_eligible_for_ACME`)" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<b-button @click="callAction('install_LE')" variant="success" :disabled="!cert.acmeEligible">
|
|
||||||
<icon iname="star" /> {{ $t('install_letsencrypt_cert') }}
|
|
||||||
</b-button>
|
|
||||||
<hr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- CERT RENEW LETS-ENCRYPT -->
|
|
||||||
<template v-if="actionsEnabled.manualRenewLetsencrypt">
|
|
||||||
<p v-t="'manually_renew_letsencrypt_message'" />
|
|
||||||
|
|
||||||
<b-button @click="callAction('manual_renew_LE')" variant="warning">
|
|
||||||
<icon iname="refresh" /> {{ $t('manually_renew_letsencrypt') }}
|
|
||||||
</b-button>
|
|
||||||
<hr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- CERT REGEN SELF-SIGNED -->
|
|
||||||
<template v-if="actionsEnabled.regenSelfsigned">
|
|
||||||
<p v-t="'regenerate_selfsigned_cert_message'" />
|
|
||||||
|
|
||||||
<b-button @click="callAction('regen_selfsigned')" variant="warning">
|
|
||||||
<icon iname="refresh" /> {{ $t('regenerate_selfsigned_cert') }}
|
|
||||||
</b-button>
|
|
||||||
<hr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- CERT REPLACE WITH SELF-SIGNED -->
|
|
||||||
<template v-if="actionsEnabled.replaceWithSelfsigned">
|
|
||||||
<p v-t="'revert_to_selfsigned_cert_message'" />
|
|
||||||
|
|
||||||
<b-button @click="callAction('revert_to_selfsigned')" variant="danger">
|
|
||||||
<icon iname="exclamation-triangle" /> {{ $t('revert_to_selfsigned_cert') }}
|
|
||||||
</b-button>
|
|
||||||
<hr>
|
|
||||||
</template>
|
|
||||||
</card>
|
|
||||||
|
|
||||||
<template #skeleton>
|
|
||||||
<card-info-skeleton :item-count="2" />
|
|
||||||
<card-buttons-skeleton :item-count="2" />
|
|
||||||
</template>
|
|
||||||
</view-base>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'DomainCert',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
name: { type: String, required: true }
|
|
||||||
},
|
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
queries: [
|
|
||||||
['GET', `domains/${this.name}/cert?full`]
|
|
||||||
],
|
|
||||||
cert: undefined,
|
|
||||||
actionsEnabled: undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
formatCertAlert (code, type) {
|
|
||||||
switch (code) {
|
|
||||||
case 'critical': return { type: 'danger', trad: 'not_valid', icon: 'exclamation-circle' }
|
|
||||||
case 'warning': return { type: 'warning', trad: 'selfsigned', icon: 'exclamation-triangle' }
|
|
||||||
case 'attention':
|
|
||||||
if (type === 'lets-encrypt') {
|
|
||||||
return { type: 'warning', trad: 'letsencrypt_about_to_expire', icon: 'clock-o' }
|
|
||||||
} else {
|
|
||||||
return { type: 'danger', trad: 'about_to_expire', icon: 'clock-o' }
|
|
||||||
}
|
|
||||||
case 'good': return { type: 'success', trad: 'good', icon: 'check-circle' }
|
|
||||||
case 'great': return { type: 'success', trad: 'great', icon: 'thumbs-up' }
|
|
||||||
default: return { type: 'warning', trad: 'unknown', icon: 'question' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onQueriesResponse (data) {
|
|
||||||
const certData = data.certificates[this.name]
|
|
||||||
|
|
||||||
const cert = {
|
|
||||||
type: certData.CA_type.verbose,
|
|
||||||
name: certData.CA_name,
|
|
||||||
validity: certData.validity,
|
|
||||||
acmeEligible: certData.ACME_eligible,
|
|
||||||
alert: this.formatCertAlert(certData.summary.code, certData.CA_type.verbose)
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionsEnabled = {
|
|
||||||
installLetsencrypt: false,
|
|
||||||
manualRenewLetsencrypt: false,
|
|
||||||
regenSelfsigned: false,
|
|
||||||
replaceWithSelfsigned: false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (certData.CA_type.code) {
|
|
||||||
case 'self-signed':
|
|
||||||
actionsEnabled.installLetsencrypt = true
|
|
||||||
actionsEnabled.regenSelfsigned = true
|
|
||||||
break
|
|
||||||
case 'lets-encrypt':
|
|
||||||
actionsEnabled.manualRenewLetsencrypt = true
|
|
||||||
actionsEnabled.replaceWithSelfsigned = true
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
actionsEnabled.replaceWithSelfsigned = true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cert = cert
|
|
||||||
this.actionsEnabled = actionsEnabled
|
|
||||||
},
|
|
||||||
|
|
||||||
async callAction (action) {
|
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t(`confirm_cert_${action}`))
|
|
||||||
if (!confirmed) return
|
|
||||||
|
|
||||||
let uri = `domains/${this.name}/cert`
|
|
||||||
if (action === 'regen_selfsigned') uri += '?self_signed'
|
|
||||||
else if (action === 'manual_renew_LE') uri += '?force'
|
|
||||||
else if (action === 'revert_to_selfsigned') uri += '?self_signed&force'
|
|
||||||
api.put(
|
|
||||||
uri, {}, { key: 'domains.' + action, name: this.name }
|
|
||||||
).then(this.$refs.view.fetchQueries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -3,7 +3,10 @@
|
||||||
:queries="queries" @queries-response="onQueriesResponse"
|
:queries="queries" @queries-response="onQueriesResponse"
|
||||||
ref="view" skeleton="card-form-skeleton"
|
ref="view" skeleton="card-form-skeleton"
|
||||||
>
|
>
|
||||||
<config-panels v-if="config.panels" v-bind="config" @submit="applyConfig" />
|
<config-panels
|
||||||
|
v-if="config.panels" v-bind="config"
|
||||||
|
@submit="onConfigSubmit"
|
||||||
|
/>
|
||||||
</view-base>
|
</view-base>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -41,23 +44,27 @@ export default {
|
||||||
this.config = formatYunoHostConfigPanels(config)
|
this.config = formatYunoHostConfigPanels(config)
|
||||||
},
|
},
|
||||||
|
|
||||||
async applyConfig (id_) {
|
async onConfigSubmit ({ id, form, action, name }) {
|
||||||
const formatedData = await formatFormData(
|
const args = await formatFormData(form, { removeEmpty: false, removeNull: true })
|
||||||
this.config.forms[id_],
|
const call = action
|
||||||
{ removeEmpty: false, removeNull: true, multipart: false }
|
? api.put(
|
||||||
|
`domain/${this.name}/actions/${action}`,
|
||||||
|
{ args: objectToParams(args) },
|
||||||
|
{ key: 'domains.' + name, name: this.name }
|
||||||
|
)
|
||||||
|
: api.put(
|
||||||
|
`domains/${this.name}/config/${id}`,
|
||||||
|
{ args: objectToParams(args) },
|
||||||
|
{ key: 'domains.update_config', id, name: this.name }
|
||||||
)
|
)
|
||||||
|
|
||||||
api.put(
|
call.then(() => {
|
||||||
`domains/${this.name}/config`,
|
|
||||||
{ key: id_, args: objectToParams(formatedData) },
|
|
||||||
{ key: 'domains.update_config', name: this.name }
|
|
||||||
).then(() => {
|
|
||||||
this.$refs.view.fetchQueries({ triggerLoading: true })
|
this.$refs.view.fetchQueries({ triggerLoading: true })
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
const panel = this.config.panels.find(({ id }) => id_ === id)
|
const panel = this.config.panels.find(panel => panel.id === id)
|
||||||
if (err.data.name) {
|
if (err.data.name) {
|
||||||
this.config.errors[id_][err.data.name].message = err.message
|
this.config.errors[id][err.data.name].message = err.message
|
||||||
} else this.$set(panel, 'serverError', err.message)
|
} else this.$set(panel, 'serverError', err.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,13 +32,6 @@
|
||||||
</b-button>
|
</b-button>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<!-- SSL CERTIFICATE -->
|
|
||||||
<p>{{ $t('certificate_manage') }}</p>
|
|
||||||
<b-button variant="outline-dark" :to="{ name: 'domain-cert', param: { name } }">
|
|
||||||
<icon iname="lock" /> {{ $t('ssl_certificate') }}
|
|
||||||
</b-button>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<!-- DELETE -->
|
<!-- DELETE -->
|
||||||
<p>{{ $t('domain_delete_longdesc') }}</p>
|
<p>{{ $t('domain_delete_longdesc') }}</p>
|
||||||
<p
|
<p
|
||||||
|
|
Loading…
Add table
Reference in a new issue