feat: add vuelidate (#3438)

This commit is contained in:
Alexis Saettler
2020-01-18 12:42:01 +01:00
committed by GitHub
parent ff408805e7
commit 0a90d4bb84
23 changed files with 511 additions and 186 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ indent_size = 4
[*.blade.php] [*.blade.php]
indent_size = 2 indent_size = 2
[*.{js,vue}] [*.{js,vue,scss}]
indent_size = 2 indent_size = 2
[*.md] [*.md]
+1
View File
@@ -2,6 +2,7 @@
### New features: ### New features:
* Add vue data validations
* Add ability to edit activities * Add ability to edit activities
* Associate a photo to a gift * Associate a photo to a gift
+2 -1
View File
@@ -64,7 +64,8 @@
"vue-i18n": "^8.8", "vue-i18n": "^8.8",
"vue-js-toggle-button": "^1.2.2", "vue-js-toggle-button": "^1.2.2",
"vue-notification": "^1.3.6", "vue-notification": "^1.3.6",
"vue-select": "^3.0.2" "vue-select": "^3.0.2",
"vuelidate": "^0.7.4"
}, },
"snyk": true "snyk": true
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -1,9 +1,9 @@
{ {
"/js/manifest.js": "/js/manifest.js?id=7db827d654313dce4250", "/js/manifest.js": "/js/manifest.js?id=7db827d654313dce4250",
"/js/vendor.js": "/js/vendor.js?id=6ccef718ebd46e2c4f89", "/js/vendor.js": "/js/vendor.js?id=773b341f83f70ee67b38",
"/js/app.js": "/js/app.js?id=9ed7ed32ce507eff51e4", "/js/app.js": "/js/app.js?id=cb94afd4886fb56d9a38",
"/css/app-ltr.css": "/css/app-ltr.css?id=ff41d6c39e7fba5cf37e", "/css/app-ltr.css": "/css/app-ltr.css?id=543870c3275cb99afc76",
"/css/app-rtl.css": "/css/app-rtl.css?id=ee3f6d81111cdc99f35d", "/css/app-rtl.css": "/css/app-rtl.css?id=26aa1824e819b5990779",
"/css/stripe.css": "/css/stripe.css?id=76c70a7b11ae5f38a725", "/css/stripe.css": "/css/stripe.css?id=76c70a7b11ae5f38a725",
"/js/stripe.js": "/js/stripe.js?id=5e5e1d73338a536f7ce9", "/js/stripe.js": "/js/stripe.js?id=5e5e1d73338a536f7ce9",
"/js/u2f-api.js": "/js/u2f-api.js?id=1948d3efdfd801bed14a" "/js/u2f-api.js": "/js/u2f-api.js?id=1948d3efdfd801bed14a"
@@ -33,10 +33,11 @@
<form-input <form-input
:id="'age'" :id="'age'"
ref="age" ref="age"
:value="age" v-model="selectedAge"
:input-type="'number'" :input-type="'number'"
:width="50" :width="50"
:required="true" :required="true"
:validator="$v.selectedAge"
/> />
</div> </div>
</form-radio> </form-radio>
@@ -86,10 +87,12 @@
<form-date <form-date
:id="'birthdayDate'" :id="'birthdayDate'"
ref="birthday" ref="birthday"
:value="birthdate" v-model="selectedDate"
:show-calendar-on-focus="true" :show-calendar-on-focus="true"
:locale="locale" :locale="locale"
:label="$t('people.information_edit_birthdate_label')"
:class="[ dirltr ? 'fl' : 'fr' ]" :class="[ dirltr ? 'fl' : 'fr' ]"
:validator="$v.selectedDate"
/> />
</div> </div>
</form-radio> </form-radio>
@@ -112,8 +115,20 @@
</template> </template>
<script> <script>
import moment from 'moment';
import { validationMixin } from 'vuelidate';
import { required, numeric, helpers } from 'vuelidate/lib/validators';
const before = (param) =>
helpers.withParams(
{ type: 'before', date: param },
(value) => !helpers.req(value) || moment(value).isBefore(param)
);
export default { export default {
mixins: [validationMixin],
props: { props: {
value: { value: {
type: String, type: String,
@@ -121,15 +136,11 @@ export default {
}, },
days: { days: {
type: Array, type: Array,
default: function () { default: () => [],
return [];
}
}, },
months: { months: {
type: Array, type: Array,
default: function () { default: () => [],
return [];
}
}, },
day: { day: {
type: Number, type: Number,
@@ -158,12 +169,35 @@ export default {
selectedDate: null, selectedDate: null,
selectedOption: null, selectedOption: null,
selectedOptionSave: null, selectedOptionSave: null,
selectedAge: 0,
selectedMonth: 0, selectedMonth: 0,
selectedDay: 0, selectedDay: 0,
hasBirthdayReminder: false hasBirthdayReminder: false
}; };
}, },
validations() {
switch (this.selectedOption) {
case 'approximate':
return {
selectedAge: {
required,
numeric,
}
};
break;
case 'exact':
return {
selectedDate: {
required,
before: before(moment())
}
};
break;
}
return null;
},
computed: { computed: {
dirltr() { dirltr() {
return this.$root.htmldir == 'ltr'; return this.$root.htmldir == 'ltr';
@@ -173,9 +207,25 @@ export default {
} }
}, },
watch: {
birthdate(val) {
this.selectedDate = val;
},
value(val) {
this.selectedOption = val;
},
age(val) {
this.selectedAge = val;
},
},
mounted() { mounted() {
this.selectedDate = this.birthdate;
this.selectedOption = this.value != '' ? this.value : 'unknown'; this.selectedOption = this.value != '' ? this.value : 'unknown';
this.selectedOptionSave = this.selectedOption; this.selectedOptionSave = this.selectedOption;
this.selectedAge = this.age;
this.selectedMonth = this.month; this.selectedMonth = this.month;
this.selectedDay = this.day; this.selectedDay = this.day;
this.hasBirthdayReminder = this.reminder; this.hasBirthdayReminder = this.reminder;
@@ -6,7 +6,7 @@
<div class="pa4-ns ph3 pv2 bb b--gray-monica"> <div class="pa4-ns ph3 pv2 bb b--gray-monica">
<div class="mb3 mb0-ns"> <div class="mb3 mb0-ns">
<form-checkbox <form-checkbox
v-model="value" v-model.lazy="deceased"
:name="'is_deceased'" :name="'is_deceased'"
:value="true" :value="true"
:dclass="'flex mb2'" :dclass="'flex mb2'"
@@ -15,7 +15,7 @@
{{ $t('people.deceased_mark_person_deceased') }} {{ $t('people.deceased_mark_person_deceased') }}
</template> </template>
</form-checkbox> </form-checkbox>
<div v-show="value" :class="[ dirltr ? 'ml4' : 'mr4' ]"> <div v-show="deceased" :class="[ dirltr ? 'ml4' : 'mr4' ]">
<form-checkbox <form-checkbox
v-model.lazy="dateKnown" v-model.lazy="dateKnown"
:name="'is_deceased_date_known'" :name="'is_deceased_date_known'"
@@ -31,11 +31,13 @@
<form-date <form-date
:id="'deceased_date'" :id="'deceased_date'"
ref="deaceasedday" ref="deaceasedday"
v-model="date" v-model="selectedDate"
:label="$t('people.deceased_date_label')"
:show-calendar-on-focus="true" :show-calendar-on-focus="true"
:locale="locale" :locale="locale"
:validator="$v.selectedDate"
/> />
<div v-show="date != ''" class="mt2"> <div v-show="selectedDate != ''" class="mt2">
<form-checkbox <form-checkbox
:name="'add_reminder_deceased'" :name="'add_reminder_deceased'"
:value="true" :value="true"
@@ -51,8 +53,20 @@
</template> </template>
<script> <script>
import moment from 'moment';
import { validationMixin } from 'vuelidate';
import { required, numeric, helpers } from 'vuelidate/lib/validators';
const before = (param) =>
helpers.withParams(
{ type: 'before', date: param },
(value) => !helpers.req(value) || moment(value).isBefore(param)
);
export default { export default {
mixins: [validationMixin],
props: { props: {
value: { value: {
type: Boolean, type: Boolean,
@@ -70,10 +84,19 @@ export default {
data() { data() {
return { return {
deceased: false,
dateKnown: false, dateKnown: false,
selectedDate: null,
}; };
}, },
validations: {
selectedDate: {
required,
before: before(moment())
}
},
computed: { computed: {
dirltr() { dirltr() {
return this.$root.htmldir == 'ltr'; return this.$root.htmldir == 'ltr';
@@ -83,8 +106,20 @@ export default {
} }
}, },
watch: {
value(val) {
this.deceased = val;
},
date(val) {
this.selectedDate = val;
},
},
mounted() { mounted() {
this.deceased = this.value;
this.dateKnown = this.date != ''; this.dateKnown = this.date != '';
this.selectedDate = this.date;
}, },
methods: { methods: {
+54 -8
View File
@@ -1,5 +1,8 @@
<style scoped>
</style>
<template> <template>
<div> <div :class="{ 'form-group-error': validator && validator.$error }">
<datepicker <datepicker
ref="select" ref="select"
:ref-name="'select'" :ref-name="'select'"
@@ -8,15 +11,21 @@
:parse-typed-date="formatTypedValue" :parse-typed-date="formatTypedValue"
:language="locale" :language="locale"
:monday-first="mondayFirst" :monday-first="mondayFirst"
:input-class="'br2 f5 ba b--black-40 pa2 outline-0'" :input-class="inputClass"
:typeable="true" :typeable="true"
:clear-button="true" :clear-button="true"
:show-calendar-on-focus="showCalendarOnFocus" :show-calendar-on-focus="showCalendarOnFocus"
@input="$emit('input', exchangeValue($event))" @input="onInput($event)"
@selected="update" @selected="onSelected($event)"
@clearDate="update('')" @clearDate="onSelected('')"
/> />
<input :name="id" type="hidden" :value="exchange" /> <input :name="id" type="hidden" :value="exchange" />
<small v-if="validator && (validator.$error && validator.required !== undefined && !validator.required)" class="error">
{{ requiredMessage }}
</small>
<small v-if="validator && (validator.$error && validator.before !== undefined && !validator.before)" class="error">
{{ beforeMessage }}
</small>
</div> </div>
</template> </template>
@@ -44,6 +53,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
label: {
type: String,
default: '',
},
defaultDate: { defaultDate: {
type: String, type: String,
default: '', default: '',
@@ -55,7 +68,11 @@ export default {
showCalendarOnFocus: { showCalendarOnFocus: {
type: Boolean, type: Boolean,
default: false, default: false,
} },
validator: {
type: Object,
default: null,
},
}, },
data() { data() {
@@ -84,6 +101,25 @@ export default {
displayFormat() { displayFormat() {
return 'L'; return 'L';
}, },
inputClass() {
var c = ['br2 f5 ba b--black-40 pa2 outline-0'];
if (this.validator) {
c.push({ 'error': this.validator.$error });
}
return c;
},
requiredMessage() {
return this.$t('validation.vue.required', { field: this.label });
},
beforeMessage() {
return this.$t('validation.vue.max.numeric', {
field: this.label,
max: this.displayValue(this.validator.$params.before.date)
});
},
}, },
watch: { watch: {
@@ -130,7 +166,6 @@ export default {
} }
this.exchange = mdate.format(this.exchangeFormat); this.exchange = mdate.format(this.exchangeFormat);
} }
this.$emit('input', this.exchange);
}, },
updateExchange(date) { updateExchange(date) {
@@ -156,8 +191,19 @@ export default {
focus() { focus() {
this.$refs.select.$children[0].$refs.select.focus(); this.$refs.select.$children[0].$refs.select.focus();
} },
onInput(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('input', this.exchangeValue(event));
},
onSelected(event) {
this.update(event);
this.onInput(event);
}
} }
}; };
</script> </script>
@@ -1,21 +1,22 @@
<style scoped> <style scoped>
input { .input {
transition: all; transition: all;
transition-duration: 0.2s; transition-duration: 0.2s;
border: 1px solid #c4cdd5; border: 1px solid #c4cdd5;
} }
input:focus {
.input:focus {
border: 1px solid #5c6ac4; border: 1px solid #5c6ac4;
} }
</style> </style>
<template> <template>
<div> <div :class="{ 'form-group-error': validator && validator.$error }">
<label <label
v-if="title" v-if="title"
:for="realid" :for="realid"
class="mb2" class="mb2"
:class="{ b: required }" :class="{ b: required, error: validator && validator.$error }"
> >
{{ title }} {{ title }}
</label> </label>
@@ -31,9 +32,20 @@ input:focus {
:style="inputStyle" :style="inputStyle"
:value="value" :value="value"
:maxlength="maxlength" :maxlength="maxlength"
@input="event => { $emit('input', event.target.value) }" @input="onInput($event)"
@keyup.enter="event => { $emit('submit', event.target.value) }" @blur="onBlur($event)"
@change="onChange($event)"
@keyup.enter="onSubmit($event)"
/> />
<small v-if="validator && (validator.$error && validator.required !== undefined && !validator.required)" class="error">
{{ requiredMessage }}
</small>
<small v-if="validator && (validator.$error && validator.maxLength !== undefined && !validator.maxLength)" class="error">
{{ maxLengthMessage }}
</small>
<small v-if="validator && (validator.$error && validator.url !== undefined && !validator.url)" class="error">
{{ urlMessage }}
</small>
</div> </div>
</template> </template>
@@ -49,6 +61,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
label: {
type: String,
default: null,
},
id: { id: {
type: String, type: String,
default: '', default: '',
@@ -70,13 +86,17 @@ export default {
default: -1, default: -1,
}, },
iclass: { iclass: {
type: String, type: [String, Array],
default: '' default: ''
}, },
maxlength: { maxlength: {
type: Number, type: Number,
default: null, default: null,
}, },
validator: {
type: Object,
default: null,
},
}, },
computed: { computed: {
@@ -84,17 +104,72 @@ export default {
return this.id + this._uid; return this.id + this._uid;
}, },
inputClass() { inputClass() {
return this.iclass != '' ? this.iclass : 'br2 f5 w-100 ba b--black-40 pa2 outline-0'; var c = [this.iclass != '' ? this.iclass : 'br2 f5 w-100 ba b--black-40 pa2 outline-0'];
if (this.validator) {
c.push({ error: this.validator.$error });
}
c.push('input');
return c;
}, },
inputStyle() { inputStyle() {
return this.width >= 0 ? 'width:' + this.width + 'px' : ''; return this.width >= 0 ? 'width:' + this.width + 'px' : '';
} },
field() {
return this.label && this.label.length > 0 ? this.label : this.title;
},
requiredMessage() {
return this.$t('validation.vue.required', { field: this.field });
},
urlMessage() {
return this.$t('validation.vue.url', { field: this.field });
},
maxLengthMessage() {
var type = 'string';
switch (this.inputType) {
case 'number':
type = 'numeric';
break;
}
return this.$t('validation.vue.max.'.type, {
field: this.field,
max: this.validator ? this.validator.$params.maxLength.max : '',
});
},
}, },
methods: { methods: {
focus() { focus() {
this.$refs.input.focus(); this.$refs.input.focus();
}, },
onInput(event) {
if (this.validator && event.data !== undefined) {
this.validator.$reset();
}
this.$emit('input', event.target.value);
},
onSubmit(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('submit', event.target.value);
},
onBlur(event) {
if (this.validator && event.target.value !== '') {
this.validator.$touch();
}
this.$emit('blur', event.target.value);
},
onChange(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('change', event.target.value);
},
}, },
}; };
@@ -6,7 +6,7 @@
<div> <div>
<p-input <p-input
ref="input" ref="input"
v-model.lazy="modelValue" v-model.lazy="prop"
:name="name" :name="name"
:type="_type" :type="_type"
:class="inputClass" :class="inputClass"
@@ -14,7 +14,7 @@
:value="value" :value="value"
:disabled="disabled" :disabled="disabled"
:required="required" :required="required"
@change="event => { $emit('change', event) }" @change="$emit('change', $event)"
> >
<slot></slot> <slot></slot>
<slot slot="extra" name="inputextra"></slot> <slot slot="extra" name="inputextra"></slot>
@@ -82,6 +82,12 @@ export default {
} }
}, },
data() {
return {
prop: null
};
},
computed: { computed: {
_type() { _type() {
if (this.$options.input_type) { if (this.$options.input_type) {
@@ -97,6 +103,16 @@ export default {
}, },
}, },
watch: {
modelValue(val) {
this.prop = val;
},
},
mounted() {
this.prop = this.modelValue;
},
methods: { methods: {
select() { select() {
if (this.disabled) { if (this.disabled) {
@@ -1,22 +1,22 @@
<style scoped> <style scoped>
select { .select {
height: 34px; height: 34px;
transition: all; transition: all;
transition-duration: 0.2s; transition-duration: 0.2s;
border: 1px solid #c4cdd5; border: 1px solid #c4cdd5;
} }
select:focus { .select:focus {
border: 1px solid #5c6ac4; border: 1px solid #5c6ac4;
} }
</style> </style>
<template> <template>
<div> <div :class="{ 'form-group-error': validator && validator.$error }">
<label <label
v-if="title" v-if="title"
:for="realid" :for="realid"
class="mb2" class="mb2"
:class="{ b: required }" :class="{ b: required, error: validator && validator.$error }"
> >
{{ title }} {{ title }}
</label> </label>
@@ -27,7 +27,7 @@ select:focus {
:name="id" :name="id"
:required="required" :required="required"
:class="selectClass" :class="selectClass"
@input="event => { $emit('input', event.target.value) }" @input="onInput"
> >
<template v-if="Array.isArray(options)"> <template v-if="Array.isArray(options)">
<option <option
@@ -54,6 +54,9 @@ select:focus {
</optgroup> </optgroup>
</template> </template>
</select> </select>
<small v-if="validator && (validator.$error && validator.required !== undefined && !validator.required)" class="error">
{{ requiredMessage }}
</small>
</div> </div>
</template> </template>
@@ -73,6 +76,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
label: {
type: String,
default: null,
},
id: { id: {
type: String, type: String,
default: '', default: '',
@@ -89,6 +96,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
validator: {
type: Object,
default: null,
}
}, },
data() { data() {
@@ -102,8 +113,19 @@ export default {
return this.id + this._uid; return this.id + this._uid;
}, },
selectClass() { selectClass() {
return this.iclass != '' ? this.iclass : 'br2 f5 w-100 ba b--black-40 pa2 outline-0'; var c = [this.iclass != '' ? this.iclass : 'br2 f5 w-100 ba b--black-40 pa2 outline-0'];
} if (this.validator) {
c.push({ error: this.validator.$error });
}
c.push('select');
return c;
},
field() {
return this.label && this.label.length > 0 ? this.label : this.title;
},
requiredMessage() {
return this.$t('validation.vue.required', { field: this.field });
},
}, },
watch: { watch: {
@@ -129,7 +151,14 @@ export default {
focus() { focus() {
this.$refs.select.focus(); this.$refs.select.focus();
} },
onInput(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('input', event.target.value);
},
}, },
}; };
</script> </script>
+70 -123
View File
@@ -80,24 +80,26 @@
</div> </div>
<!-- Create Client Modal --> <!-- Create Client Modal -->
<sweet-modal ref="modalCreateClient" overlay-theme="dark" tabindex="-1" role="dialog" <sweet-modal ref="modalClient" overlay-theme="dark" tabindex="-1" role="dialog"
:title="$t('settings.api_oauth_create')" @open="_focusCreateInput" :title="form.id ? $t('settings.api_oauth_edit') : $t('settings.api_oauth_create')"
@open="_focusInput"
> >
<!-- Form Errors --> <!-- Form Errors -->
<error :errors="createForm.errors" /> <error :errors="form.errors" />
<!-- Create Client Form --> <!-- Create Client Form -->
<form class="form-horizontal" role="form"> <form ref="form" class="form-horizontal" role="form">
<!-- Name --> <!-- Name -->
<div class="form-group"> <div class="form-group">
<div class="col-md-auto"> <div class="col-md-auto">
<form-input <form-input
:id="'create-client-name'" :id="'client-name'"
ref="createClientName" ref="clientName"
v-model="createForm.name" v-model="form.name"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'" :iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true" :required="true"
:title="$t('settings.api_oauth_name')" :title="$t('settings.api_oauth_name')"
:validator="$v.form.name"
@submit="store" @submit="store"
/> />
@@ -111,11 +113,12 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-auto"> <div class="col-md-auto">
<form-input <form-input
:id="'create-redirect-url'" :id="'redirect-url'"
v-model="createForm.redirect" v-model="form.redirect"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'" :iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true" :required="true"
:title="$t('settings.api_oauth_redirecturl')" :title="$t('settings.api_oauth_redirecturl')"
:validator="$v.form.redirect"
@submit="store" @submit="store"
/> />
@@ -132,65 +135,7 @@
{{ $t('app.close') }} {{ $t('app.close') }}
</a> </a>
<a class="btn btn-primary" href="" @click.prevent="store"> <a class="btn btn-primary" href="" @click.prevent="store">
{{ $t('app.create') }} {{ form.id ? $t('app.save') : $t('app.create') }}
</a>
</div>
</sweet-modal>
<!-- Edit Client Modal -->
<sweet-modal ref="modalEditClient" overlay-theme="dark" tabindex="-1" role="dialog"
:title="$t('settings.api_oauth_edit')" @open="_focusEditInput"
>
<!-- Form Errors -->
<error :errors="editForm.errors" />
<!-- Edit Client Form -->
<form class="form-horizontal" role="form">
<!-- Name -->
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'edit-client-name'"
ref="editClientName"
v-model="editForm.name"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true"
:title="$t('settings.api_oauth_name')"
@submit="update"
/>
<small class="form-text text-muted">
{{ $t('settings.api_oauth_name_help') }}
</small>
</div>
</div>
<!-- Redirect URL -->
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'edit-redirect-url'"
v-model="editForm.redirect"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true"
:title="$t('settings.api_oauth_redirecturl')"
@submit="update"
/>
<small class="form-text text-muted">
{{ $t('settings.api_oauth_redirecturl_help') }}
</small>
</div>
</div>
</form>
<!-- Modal Actions -->
<div slot="button">
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.close') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="update">
{{ $t('app.save') }}
</a> </a>
</div> </div>
</sweet-modal> </sweet-modal>
@@ -200,6 +145,8 @@
<script> <script>
import Error from '../partials/Error.vue'; import Error from '../partials/Error.vue';
import { SweetModal } from 'sweet-modal-vue'; import { SweetModal } from 'sweet-modal-vue';
import { validationMixin } from 'vuelidate';
import { required, url } from 'vuelidate/lib/validators';
export default { export default {
@@ -208,17 +155,13 @@ export default {
Error Error
}, },
mixins: [validationMixin],
data() { data() {
return { return {
clients: [], clients: [],
createForm: { form: {
errors: [],
name: '',
redirect: ''
},
editForm: {
errors: [], errors: [],
name: '', name: '',
redirect: '' redirect: ''
@@ -226,6 +169,18 @@ export default {
}; };
}, },
validations: {
form: {
name: {
required,
},
redirect: {
required,
url,
}
}
},
computed: { computed: {
dirltr() { dirltr() {
return this.$root.htmldir == 'ltr'; return this.$root.htmldir == 'ltr';
@@ -244,26 +199,16 @@ export default {
/** /**
* Focus on modal open. * Focus on modal open.
*/ */
_focusCreateInput() { _focusInput() {
let vm = this; let vm = this;
setTimeout(function() { setTimeout(function() {
vm.$refs.createClientName.focus(); vm.$refs.clientName.focus();
}, 10); }, 10);
}, },
/** /**
* Focus on modal open. * Get all of the OAuth clients for the user.
*/ */
_focusEditInput() {
let vm = this;
setTimeout(function() {
vm.$refs.editClientName.focus();
}, 10);
},
/**
* Get all of the OAuth clients for the user.
*/
getClients() { getClients() {
axios.get('oauth/clients') axios.get('oauth/clients')
.then(response => { .then(response => {
@@ -272,46 +217,41 @@ export default {
}, },
/** /**
* Show the form for creating new clients. * Show the form for creating new clients.
*/ */
showCreateClientForm() { showCreateClientForm() {
this.$refs.modalCreateClient.open(); this.resetField();
this.$refs.modalClient.open();
}, },
/** /**
* Create a new OAuth client for the user. * Create a new OAuth client for the user.
*/ */
store() { store() {
this.persistClient( this.$v.$touch();
'post', 'oauth/clients',
this.createForm if (this.$v.$invalid) {
); return;
}
let method = this.form.id ? 'put' : 'post';
let url = this.form.id ? 'oauth/clients/' + this.form.id : 'oauth/clients';
this.persistClient(method, url, this.form);
}, },
/** /**
* Edit the given client. * Edit the given client.
*/ */
edit(client) { edit(client) {
this.editForm.id = client.id; this.form = Object.assign({errors:[]}, client);
this.editForm.name = client.name;
this.editForm.redirect = client.redirect;
this.$refs.modalEditClient.open(); this.$refs.modalClient.open();
}, },
/** /**
* Update the client being edited. * Persist the client to storage using the given form.
*/ */
update() {
this.persistClient(
'put', 'oauth/clients/' + this.editForm.id,
this.editForm
);
},
/**
* Persist the client to storage using the given form.
*/
persistClient(method, uri, form) { persistClient(method, uri, form) {
form.errors = []; form.errors = [];
@@ -319,10 +259,6 @@ export default {
.then(response => { .then(response => {
this.getClients(); this.getClients();
form.name = '';
form.redirect = '';
form.errors = [];
this.closeModal(); this.closeModal();
}) })
.catch(error => { .catch(error => {
@@ -335,8 +271,8 @@ export default {
}, },
/** /**
* Destroy the given client. * Destroy the given client.
*/ */
destroy(client) { destroy(client) {
axios.delete('oauth/clients/' + client.id) axios.delete('oauth/clients/' + client.id)
.then(response => { .then(response => {
@@ -345,8 +281,19 @@ export default {
}, },
closeModal() { closeModal() {
this.$refs.modalCreateClient.close(); this.resetField();
this.$refs.modalEditClient.close(); this.$v.$reset();
this.$refs.form.reset();
this.$refs.modalClient.close();
},
resetField() {
this.form = {
id: '',
errors:[],
name: '',
redirect: '',
};
}, },
/** /**
@@ -69,7 +69,7 @@
<error :errors="form.errors" /> <error :errors="form.errors" />
<!-- Create Token Form --> <!-- Create Token Form -->
<form class="form-horizontal" role="form" @submit.prevent="store"> <form ref="form" class="form-horizontal" role="form" @submit.prevent="store">
<!-- Name --> <!-- Name -->
<div class="col-md-auto"> <div class="col-md-auto">
<form-input <form-input
@@ -80,6 +80,7 @@
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'" :iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true" :required="true"
:title="$t('settings.api_token_name')" :title="$t('settings.api_token_name')"
:validator="$v.form.name"
/> />
</div> </div>
@@ -141,6 +142,8 @@
<script> <script>
import Error from '../partials/Error.vue'; import Error from '../partials/Error.vue';
import { SweetModal } from 'sweet-modal-vue'; import { SweetModal } from 'sweet-modal-vue';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
export default { export default {
@@ -149,6 +152,8 @@ export default {
Error Error
}, },
mixins: [validationMixin],
data() { data() {
return { return {
endpoint: '', endpoint: '',
@@ -165,6 +170,14 @@ export default {
}; };
}, },
validations: {
form: {
name: {
required,
}
}
},
computed: { computed: {
dirltr() { dirltr() {
return this.$root.htmldir == 'ltr'; return this.$root.htmldir == 'ltr';
@@ -207,6 +220,8 @@ export default {
* Close all modals. * Close all modals.
*/ */
closeModal() { closeModal() {
this.$refs.form.reset();
this.$v.$reset();
this.$refs.modalCreateToken.close(); this.$refs.modalCreateToken.close();
this.$refs.modalAccessToken.close(); this.$refs.modalAccessToken.close();
}, },
@@ -232,6 +247,12 @@ export default {
* Create a new personal access token. * Create a new personal access token.
*/ */
store() { store() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
this.accessToken = null; this.accessToken = null;
this.form.errors = []; this.form.errors = [];
+32 -8
View File
@@ -1,4 +1,15 @@
<style scoped> <style scoped>
.error {
animation-name: shakeError;
animation-fill-mode: forwards;
animation-duration: .6s;
animation-timing-function: ease-in-out;
}
.form-group-error {
display: block;
color:#f57f6c;
}
</style> </style>
<template> <template>
@@ -113,16 +124,19 @@
<div class="mb2"> <div class="mb2">
<toggle-button class="mr2" :sync="true" :labels="true" :value="stateInput" @change="stateInput = !stateInput" /> <toggle-button class="mr2" :sync="true" :labels="true" :value="stateInput" @change="stateInput = !stateInput" />
<div class="dib relative" style="top: -2px;"> <div class="dib relative" style="top: -2px;">
<stay-in-touch-label v-model="frequencyInput"> <stay-in-touch-label
v-model="frequencyInput"
:class="{ 'form-group-error': $v.frequencyInput.$error }"
>
<div class="dib"> <div class="dib">
<form-input <form-input
:id="'frequency'" :id="'frequency'"
v-model="frequencyInput" v-model.number="frequencyInput"
:value="frequencyInput"
:input-type="'number'" :input-type="'number'"
:width="60" :width="60"
:required="true" :required="true"
@input="stateInput = $event > 0" :validator="$v.frequencyInput"
@input="onInput($event)"
/> />
</div> </div>
</stay-in-touch-label> </stay-in-touch-label>
@@ -153,6 +167,8 @@
<script> <script>
import { SweetModal } from 'sweet-modal-vue'; import { SweetModal } from 'sweet-modal-vue';
import { ToggleButton } from 'vue-js-toggle-button'; import { ToggleButton } from 'vue-js-toggle-button';
import { validationMixin } from 'vuelidate';
import { required, numeric } from 'vuelidate/lib/validators';
let StayInTouchLabel = Vue.component('stay-in-touch-label', { let StayInTouchLabel = Vue.component('stay-in-touch-label', {
@@ -181,6 +197,8 @@ export default {
StayInTouchLabel, StayInTouchLabel,
}, },
mixins: [validationMixin],
props: { props: {
hash: { hash: {
type: String, type: String,
@@ -196,6 +214,13 @@ export default {
}, },
}, },
validations: {
frequencyInput: {
required,
numeric,
},
},
data() { data() {
return { return {
frequency: 0, frequency: 0,
@@ -250,10 +275,9 @@ export default {
return; return;
} }
// make sure we can't press update if the frequency is invalid this.$v.$touch();
// and if the feature is activated
if ((this.frequencyInput == '' || this.frequencyInput < 1) && this.stateInput) { if (this.$v.$invalid) {
this.errorMessage = this.$t('people.stay_in_touch_invalid');
return; return;
} }
@@ -1,7 +1,7 @@
<style scoped> <style scoped>
.photo { .photo {
height: 200px; height: 200px;
} }
</style> </style>
<template> <template>
@@ -58,6 +58,7 @@
:required="true" :required="true"
:class="'dtc pr2'" :class="'dtc pr2'"
:title="$t('people.gifts_add_gift_name')" :title="$t('people.gifts_add_gift_name')"
:validator="$v.newGift.name"
@submit="store" @submit="store"
/> />
</div> </div>
@@ -126,14 +127,16 @@
<form-checkbox <form-checkbox
v-model="hasRecipient" v-model="hasRecipient"
:name="'has_recipient'" :name="'has_recipient'"
@change="() => { this.$refs.recipient.focus() }" @change="(val) => { if (val) { $refs.recipient.focus() } }"
> >
{{ $t('people.gifts_add_someone', {name: ''}) }} {{ $t('people.gifts_add_someone', {name: ''}) }}
</form-checkbox> </form-checkbox>
<form-select <form-select
ref="recipient" ref="recipient"
v-model="newGift.recipient_id" v-model="newGift.recipient_id"
:label="$t('people.gifts_add_recipient_field')"
:options="familyContacts" :options="familyContacts"
:validator="$v.newGift.recipient_id"
@input="hasRecipient = true" @input="hasRecipient = true"
/> />
</div> </div>
@@ -205,6 +208,8 @@
import Error from '../../partials/Error.vue'; import Error from '../../partials/Error.vue';
import PhotoUpload from '../photo/PhotoUpload.vue'; import PhotoUpload from '../photo/PhotoUpload.vue';
import { validationMixin } from 'vuelidate';
import { required, maxLength } from 'vuelidate/lib/validators';
export default { export default {
components: { components: {
@@ -212,6 +217,8 @@ export default {
PhotoUpload PhotoUpload
}, },
mixins: [validationMixin],
props: { props: {
contactId: { contactId: {
type: Number, type: Number,
@@ -254,6 +261,27 @@ export default {
}; };
}, },
validations() {
var v = {
newGift: {
name: {
required,
maxLength: maxLength(255),
},
}
};
if (this.hasRecipient) {
v.newGift = Object.assign(v.newGift, {
recipient_id: {
required,
}
});
}
return v;
},
computed: { computed: {
locale() { locale() {
return this.$root.locale; return this.$root.locale;
@@ -307,6 +335,7 @@ export default {
this.displayUpload= this.gift ? this.gift.photos.length > 0 : false; this.displayUpload= this.gift ? this.gift.photos.length > 0 : false;
this.errors = []; this.errors = [];
this.$v.$reset();
}, },
close() { close() {
@@ -319,6 +348,12 @@ export default {
this.newGift.recipient_id = null; this.newGift.recipient_id = null;
} }
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
let method = this.gift ? 'put' : 'post'; let method = this.gift ? 'put' : 'post';
let url = this.gift ? 'api/gifts/'+this.gift.id : 'api/gifts'; let url = this.gift ? 'api/gifts/'+this.gift.id : 'api/gifts';
+3
View File
@@ -151,6 +151,7 @@ return [
'information_edit_probably' => 'This person is probably...', 'information_edit_probably' => 'This person is probably...',
'information_edit_not_year' => 'I know the day and month of the birthdate of this person, but not the year…', 'information_edit_not_year' => 'I know the day and month of the birthdate of this person, but not the year…',
'information_edit_exact' => 'I know the exact birthdate of this person...', 'information_edit_exact' => 'I know the exact birthdate of this person...',
'information_edit_birthdate_label' => 'Birthdate',
'information_no_work_defined' => 'No work information defined', 'information_no_work_defined' => 'No work information defined',
'information_work_at' => 'at :company', 'information_work_at' => 'at :company',
'work_add_cta' => 'Update work information', 'work_add_cta' => 'Update work information',
@@ -308,6 +309,7 @@ return [
'gifts_add_value' => 'Value (optional)', 'gifts_add_value' => 'Value (optional)',
'gifts_add_comment' => 'Comment (optional)', 'gifts_add_comment' => 'Comment (optional)',
'gifts_add_recipient' => 'Recipient (optional)', 'gifts_add_recipient' => 'Recipient (optional)',
'gifts_add_recipient_field' => 'Recipient',
'gifts_add_photo' => 'Photo (optional)', 'gifts_add_photo' => 'Photo (optional)',
'gifts_add_photo_title' => 'Add a photo for this gift', 'gifts_add_photo_title' => 'Add a photo for this gift',
'gifts_add_someone' => 'This gift is for someone in {name}s family in particular', 'gifts_add_someone' => 'This gift is for someone in {name}s family in particular',
@@ -366,6 +368,7 @@ return [
'deceased_know_date' => 'I know the date this person died', 'deceased_know_date' => 'I know the date this person died',
'deceased_add_reminder' => 'Add a reminder for this date', 'deceased_add_reminder' => 'Add a reminder for this date',
'deceased_label' => 'Deceased', 'deceased_label' => 'Deceased',
'deceased_date_label' => 'Deceased date',
'deceased_label_with_date' => 'Deceased on :date', 'deceased_label_with_date' => 'Deceased on :date',
'deceased_age' => 'Age at death', 'deceased_age' => 'Age at death',
+8
View File
@@ -120,4 +120,12 @@ return [
'attributes' => [], 'attributes' => [],
'vue' => [
'max' => [
'numeric' => '{field} may not be greater than {max}.',
'string' => '{field} may not be greater than {max} characters.',
],
'required' => '{field} is required.',
'url' => '{field} is not a valid URL.',
],
]; ];
+29
View File
@@ -594,3 +594,32 @@ footer {
svg { svg {
vertical-align: baseline !important; vertical-align: baseline !important;
} }
// Input error handle
.form-group-error {
animation-name: shake;
animation-fill-mode: forwards;
animation-duration: .6s;
animation-timing-function: ease-in-out;
z-index: 99;
}
.form-group-error .error {
border-color: #f79483 !important;
color:#f57f6c;
}
// Shake animation
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
15%, 45%, 75% {
transform: translateX(0.375rem);
}
30%, 60%, 90% {
transform: translateX(-0.375rem);
}
}
+5
View File
@@ -9673,6 +9673,11 @@ vue@^2.5.21:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
vuelidate@^0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.4.tgz#5a0e54be09ac0192f1aa3387d74b92e0945bf8aa"
integrity sha512-QHZWYOL325Zo+2K7VBNEJTZ496Kd8Z31p85aQJFldKudUUGBmgw4zu4ghl4CyqPwjRCmqZ9lDdx4FSdMnu4fGg==
watchpack@^1.6.0: watchpack@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"