diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 654134e24..2be9576ed 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -8,11 +8,15 @@ module.exports = { '@vue/standard' ], parserOptions: { - parser: 'babel-eslint' + parser: '@babel/eslint-parser' }, rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'vue/valid-v-slot': ['error', { + allowModifiers: true + }], + 'vue/multi-word-component-names': 'off' }, overrides: [ { diff --git a/frontend/merge_translations.py b/frontend/merge_translations.py new file mode 100644 index 000000000..67f4646cd --- /dev/null +++ b/frontend/merge_translations.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +import polib + + +LOCALES = [ + "fr", +] + +OLD_TRANSLATION_MODULES = [ + "", + "admin", + "dnstools", + "ldapsync", + "limits", + "maillog", + "policyd", + "relaydomains", + "transport" +] + + +def merge_translations(): + for locale in LOCALES: + new_po = polib.pofile(f"locale/{locale}/LC_MESSAGES/app.po") + for module in OLD_TRANSLATION_MODULES: + prefix = f"../modoboa/{module}" if module else "../modoboa" + old_po = polib.pofile(f"{prefix}/locale/{locale}/LC_MESSAGES/django.po") + for entry in new_po: + old_entry = old_po.find(entry.msgid) + if old_entry: + entry.msgstr = old_entry.msgstr + new_po.save() + + +if __name__ == "__main__": + merge_translations() diff --git a/frontend/package.json b/frontend/package.json index f7c930ed5..34e576106 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,28 +25,29 @@ "vuex": "^3.4.0" }, "devDependencies": { - "@vue/cli-plugin-babel": "~4.5.0", - "@vue/cli-plugin-eslint": "~4.5.0", - "@vue/cli-plugin-router": "~4.5.0", - "@vue/cli-plugin-unit-mocha": "~4.5.0", - "@vue/cli-plugin-vuex": "~4.5.0", - "@vue/cli-service": "~4.5.0", - "@vue/eslint-config-standard": "^5.1.2", + "@babel/core": "^7.12.16", + "@babel/eslint-parser": "^7.12.16", + "@vue/cli-plugin-babel": "~5.0.8", + "@vue/cli-plugin-eslint": "~5.0.8", + "@vue/cli-plugin-router": "~5.0.8", + "@vue/cli-plugin-unit-mocha": "~5.0.8", + "@vue/cli-plugin-vuex": "~5.0.8", + "@vue/cli-service": "~5.0.8", + "@vue/eslint-config-standard": "^6.1.0", "@vue/test-utils": "^1.0.3", "axios": "^0.21.2", - "babel-eslint": "^10.1.0", "chai": "^4.1.2", "easygettext": "^2.15.0", - "eslint": "^6.7.2", - "eslint-plugin-import": "^2.20.2", + "eslint": "^7.32.0", + "eslint-plugin-import": "^2.25.3", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-promise": "^5.1.0", "eslint-plugin-standard": "^4.0.0", - "eslint-plugin-vue": "^6.2.2", + "eslint-plugin-vue": "^8.0.3", "sass": "~1.32.12", "sass-loader": "^8.0.2", "vue-cli-plugin-axios": "~0.0.4", - "vue-cli-plugin-vuetify": "~2.4.1", + "vue-cli-plugin-vuetify": "~2.5.8", "vue-template-compiler": "^2.6.11", "vuetify-loader": "^1.3.0" } diff --git a/frontend/src/components/domains/DNSDetail.vue b/frontend/src/components/domains/DNSDetail.vue index d1b10d4d8..2c391bf3c 100644 --- a/frontend/src/components/domains/DNSDetail.vue +++ b/frontend/src/components/domains/DNSDetail.vue @@ -1,5 +1,5 @@ @@ -138,7 +138,7 @@ import DomainDKIMKey from './DomainDKIMKey' import DomainDNSConfig from './DomainDNSConfig' export default { - props: ['domain'], + props: ['value'], components: { 'domain-dkim-key': DomainDKIMKey, 'domain-dns-config': DomainDNSConfig @@ -160,21 +160,25 @@ export default { const payload = { dkim_private_key_path: '' } - domains.patchDomain(this.domain.pk, payload).then(resp => { + domains.patchDomain(this.value.pk, payload).then(resp => { bus.$emit('notification', { msg: this.$gettext('A new key will be generated soon. Refresh the page in a moment to see it.'), type: 'success' }) - this.domain.dkim_private_key_path = '' + const domain = { ...this.value } + domain.dkim_private_key_path = '' + this.$emit('input', domain) }) } }, watch: { - domain: { + value: { handler: function (val) { - domains.getDomainDNSDetail(val.pk).then(resp => { - this.detail = resp.data - }) + if (val) { + domains.getDomainDNSDetail(val.pk).then(resp => { + this.detail = resp.data + }) + } }, immediate: true } diff --git a/frontend/src/components/domains/DomainCreationForm.vue b/frontend/src/components/domains/DomainCreationForm.vue index 18d8d54ef..0cc1a7c88 100644 --- a/frontend/src/components/domains/DomainCreationForm.vue +++ b/frontend/src/components/domains/DomainCreationForm.vue @@ -9,19 +9,19 @@ @create="submit" > @@ -37,7 +40,7 @@ export default { components: { ChoiceField }, - props: ['domain'], + props: ['value'], data () { return { domainTypes: [ @@ -51,7 +54,21 @@ export default { icon: 'mdi-earth', value: 'relaydomain' } - ] + ], + form: {} + } + }, + methods: { + update () { + this.$emit('input', this.form) + } + }, + watch: { + value: { + handler: function (newValue) { + this.form = { ...newValue } + }, + immediate: true } } } diff --git a/frontend/src/components/domains/DomainLimitationsForm.vue b/frontend/src/components/domains/DomainLimitationsForm.vue index 42d7c7848..b15004c47 100644 --- a/frontend/src/components/domains/DomainLimitationsForm.vue +++ b/frontend/src/components/domains/DomainLimitationsForm.vue @@ -8,10 +8,11 @@ :label="'Quota' | translate" :hint="'Quota shared between mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.' | translate" persistent-hint - v-model="domain.quota" + v-model="form.quota" :error-messages="errors" class="mb-4" outlined + @input="update" /> @@ -46,6 +49,21 @@ diff --git a/frontend/src/components/domains/DomainOptionsForm.vue b/frontend/src/components/domains/DomainOptionsForm.vue index 7129bd4fd..734249eb1 100644 --- a/frontend/src/components/domains/DomainOptionsForm.vue +++ b/frontend/src/components/domains/DomainOptionsForm.vue @@ -2,7 +2,7 @@ - - - @@ -45,26 +49,34 @@ import accounts from '@/api/accounts' export default { - props: ['domain'], + props: ['value'], data () { return { - createAdmin: false + createAdmin: false, + form: {} } }, methods: { updatePassword (value) { if (value) { accounts.getRandomPassword().then(resp => { - this.$set(this.domain.domain_admin, 'password', resp.data.password) + this.$set(this.form.domain_admin, 'password', resp.data.password) + this.update() }) } else { - delete this.domain.domain_admin.password + delete this.form.domain_admin.password + this.update() } }, - - update (val) { + updateCreateAdmin (val) { this.$emit('createAdmin', val) + }, + update () { + this.$emit('input', this.form) } + }, + mounted () { + this.form = { ...this.value } } } diff --git a/frontend/src/components/domains/DomainSummary.vue b/frontend/src/components/domains/DomainSummary.vue index 4b553badf..5b281dc8a 100644 --- a/frontend/src/components/domains/DomainSummary.vue +++ b/frontend/src/components/domains/DomainSummary.vue @@ -1,5 +1,5 @@ @@ -53,17 +55,18 @@ import transports from '@/api/transports' export default { props: { - domain: Object + value: Object }, data () { return { backends: [], + form: {}, service: null } }, methods: { resetForm (value) { - this.$set(this.domain, 'transport', { service: value.name, settings: {} }) + this.$set(this.form, 'transport', { service: value.name, settings: {} }) for (const setting of value.settings) { if (setting.default) { let defaultValue = setting.default @@ -76,7 +79,8 @@ export default { defaultValue = true } } - this.$set(this.domain.transport.settings, `${value.name}_${setting.name}`, defaultValue) + this.$set(this.form.transport.settings, `${value.name}_${setting.name}`, defaultValue) + this.update() } } }, @@ -87,13 +91,17 @@ export default { data.transport.settings[fullName] = parseInt(data.transport.settings[fullName]) } } + }, + update () { + this.$emit('input', this.form) } }, mounted () { + this.form = { ...this.value } transports.getAll().then(resp => { this.backends = resp.data - if (this.domain) { - this.service = this.backends.find(backend => backend.name === this.domain.transport.service) + if (this.value) { + this.service = this.backends.find(backend => backend.name === this.form.transport.service) } }) }, diff --git a/frontend/src/components/domains/TransportForm.vue b/frontend/src/components/domains/TransportForm.vue deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/components/identities/AccountAliasForm.vue b/frontend/src/components/identities/AccountAliasForm.vue index a77310ba2..83b758b69 100644 --- a/frontend/src/components/identities/AccountAliasForm.vue +++ b/frontend/src/components/identities/AccountAliasForm.vue @@ -15,11 +15,11 @@ /> {{ alias }} @@ -31,20 +31,21 @@ import accounts from '@/api/accounts' import EmailField from '@/components/tools/EmailField' export default { - props: ['account'], + props: ['value'], components: { EmailField }, data () { return { - currentAlias: '' + currentAlias: '', + aliases: [] } }, methods: { async addAlias () { try { await accounts.validate({ aliases: [this.currentAlias] }) - this.account.aliases.push(this.currentAlias) + this.aliases.push(this.currentAlias) this.$refs.aliasField.reset() } catch (error) { let errorMsg = null @@ -55,7 +56,17 @@ export default { } this.$refs.observer.setErrors({ alias: errorMsg }) } + }, + removeAlias (index) { + this.aliases.splice(index, 1) + this.update() + }, + update () { + this.$emit('input', this.aliases) } + }, + mounted () { + this.aliases = [...this.value] } } diff --git a/frontend/src/components/identities/AccountCreationForm.vue b/frontend/src/components/identities/AccountCreationForm.vue index e22fb23c4..0d70729be 100644 --- a/frontend/src/components/identities/AccountCreationForm.vue +++ b/frontend/src/components/identities/AccountCreationForm.vue @@ -9,16 +9,16 @@ @create="submit" > @@ -37,13 +40,20 @@ import { mapGetters } from 'vuex' export default { - props: ['account'], + props: ['value'], + data () { + return { + form: { + mailbox: {} + } + } + }, computed: { ...mapGetters({ getDomainByName: 'identities/getDomainByName' }), domainQuota () { - const email = this.account.mailbox.full_address + const email = this.form.mailbox.full_address if (email && email.indexOf('@') !== -1) { const domain = this.getDomainByName(email.split('@')[1]) if (domain) { @@ -64,11 +74,20 @@ export default { return result } }, + methods: { + update () { + this.$emit('input', this.form) + } + }, + mounted () { + this.form = { ...this.value } + }, watch: { - account: { - handler (val) { - if (val.role === 'SimpleUsers') { - this.$set(val.mailbox, 'full_address', val.username) + value: { + handler (newValue) { + this.form = { ...newValue } + if (this.form.role === 'SimpleUsers') { + this.$set(this.form.mailbox, 'full_address', this.form.username) } }, deep: true diff --git a/frontend/src/components/identities/AccountRoleForm.vue b/frontend/src/components/identities/AccountRoleForm.vue index bfb8cba8f..e640ff3aa 100644 --- a/frontend/src/components/identities/AccountRoleForm.vue +++ b/frontend/src/components/identities/AccountRoleForm.vue @@ -6,12 +6,14 @@ > - + @@ -32,7 +34,7 @@ export default { components: { ChoiceField }, - props: ['account'], + props: ['value', 'account'], computed: { roleLabel () { const role = this.accountRoles.find(role => role.value === this.account.role) diff --git a/frontend/src/components/identities/AliasCreationForm.vue b/frontend/src/components/identities/AliasCreationForm.vue index 9382790a5..64423f6ea 100644 --- a/frontend/src/components/identities/AliasCreationForm.vue +++ b/frontend/src/components/identities/AliasCreationForm.vue @@ -9,10 +9,10 @@ @create="submit" > diff --git a/frontend/src/components/identities/AliasForm.vue b/frontend/src/components/identities/AliasForm.vue index 38a279b1c..1cdb87fd1 100644 --- a/frontend/src/components/identities/AliasForm.vue +++ b/frontend/src/components/identities/AliasForm.vue @@ -32,7 +32,7 @@ - + @@ -65,7 +65,7 @@ - + diff --git a/frontend/src/components/identities/AliasGeneralForm.vue b/frontend/src/components/identities/AliasGeneralForm.vue index 92e750b78..9e7e3da76 100644 --- a/frontend/src/components/identities/AliasGeneralForm.vue +++ b/frontend/src/components/identities/AliasGeneralForm.vue @@ -1,12 +1,13 @@