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]
indent_size = 2
[*.{js,vue}]
[*.{js,vue,scss}]
indent_size = 2
[*.md]
+1
View File
@@ -2,6 +2,7 @@
### New features:
* Add vue data validations
* Add ability to edit activities
* Associate a photo to a gift
+2 -1
View File
@@ -64,7 +64,8 @@
"vue-i18n": "^8.8",
"vue-js-toggle-button": "^1.2.2",
"vue-notification": "^1.3.6",
"vue-select": "^3.0.2"
"vue-select": "^3.0.2",
"vuelidate": "^0.7.4"
},
"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/vendor.js": "/js/vendor.js?id=6ccef718ebd46e2c4f89",
"/js/app.js": "/js/app.js?id=9ed7ed32ce507eff51e4",
"/css/app-ltr.css": "/css/app-ltr.css?id=ff41d6c39e7fba5cf37e",
"/css/app-rtl.css": "/css/app-rtl.css?id=ee3f6d81111cdc99f35d",
"/js/vendor.js": "/js/vendor.js?id=773b341f83f70ee67b38",
"/js/app.js": "/js/app.js?id=cb94afd4886fb56d9a38",
"/css/app-ltr.css": "/css/app-ltr.css?id=543870c3275cb99afc76",
"/css/app-rtl.css": "/css/app-rtl.css?id=26aa1824e819b5990779",
"/css/stripe.css": "/css/stripe.css?id=76c70a7b11ae5f38a725",
"/js/stripe.js": "/js/stripe.js?id=5e5e1d73338a536f7ce9",
"/js/u2f-api.js": "/js/u2f-api.js?id=1948d3efdfd801bed14a"
@@ -33,10 +33,11 @@
<form-input
:id="'age'"
ref="age"
:value="age"
v-model="selectedAge"
:input-type="'number'"
:width="50"
:required="true"
:validator="$v.selectedAge"
/>
</div>
</form-radio>
@@ -86,10 +87,12 @@
<form-date
:id="'birthdayDate'"
ref="birthday"
:value="birthdate"
v-model="selectedDate"
:show-calendar-on-focus="true"
:locale="locale"
:label="$t('people.information_edit_birthdate_label')"
:class="[ dirltr ? 'fl' : 'fr' ]"
:validator="$v.selectedDate"
/>
</div>
</form-radio>
@@ -112,8 +115,20 @@
</template>
<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 {
mixins: [validationMixin],
props: {
value: {
type: String,
@@ -121,15 +136,11 @@ export default {
},
days: {
type: Array,
default: function () {
return [];
}
default: () => [],
},
months: {
type: Array,
default: function () {
return [];
}
default: () => [],
},
day: {
type: Number,
@@ -158,12 +169,35 @@ export default {
selectedDate: null,
selectedOption: null,
selectedOptionSave: null,
selectedAge: 0,
selectedMonth: 0,
selectedDay: 0,
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: {
dirltr() {
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() {
this.selectedDate = this.birthdate;
this.selectedOption = this.value != '' ? this.value : 'unknown';
this.selectedOptionSave = this.selectedOption;
this.selectedAge = this.age;
this.selectedMonth = this.month;
this.selectedDay = this.day;
this.hasBirthdayReminder = this.reminder;
@@ -6,7 +6,7 @@
<div class="pa4-ns ph3 pv2 bb b--gray-monica">
<div class="mb3 mb0-ns">
<form-checkbox
v-model="value"
v-model.lazy="deceased"
:name="'is_deceased'"
:value="true"
:dclass="'flex mb2'"
@@ -15,7 +15,7 @@
{{ $t('people.deceased_mark_person_deceased') }}
</template>
</form-checkbox>
<div v-show="value" :class="[ dirltr ? 'ml4' : 'mr4' ]">
<div v-show="deceased" :class="[ dirltr ? 'ml4' : 'mr4' ]">
<form-checkbox
v-model.lazy="dateKnown"
:name="'is_deceased_date_known'"
@@ -31,11 +31,13 @@
<form-date
:id="'deceased_date'"
ref="deaceasedday"
v-model="date"
v-model="selectedDate"
:label="$t('people.deceased_date_label')"
:show-calendar-on-focus="true"
:locale="locale"
:validator="$v.selectedDate"
/>
<div v-show="date != ''" class="mt2">
<div v-show="selectedDate != ''" class="mt2">
<form-checkbox
:name="'add_reminder_deceased'"
:value="true"
@@ -51,8 +53,20 @@
</template>
<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 {
mixins: [validationMixin],
props: {
value: {
type: Boolean,
@@ -70,10 +84,19 @@ export default {
data() {
return {
deceased: false,
dateKnown: false,
selectedDate: null,
};
},
validations: {
selectedDate: {
required,
before: before(moment())
}
},
computed: {
dirltr() {
return this.$root.htmldir == 'ltr';
@@ -83,8 +106,20 @@ export default {
}
},
watch: {
value(val) {
this.deceased = val;
},
date(val) {
this.selectedDate = val;
},
},
mounted() {
this.deceased = this.value;
this.dateKnown = this.date != '';
this.selectedDate = this.date;
},
methods: {
+54 -8
View File
@@ -1,5 +1,8 @@
<style scoped>
</style>
<template>
<div>
<div :class="{ 'form-group-error': validator && validator.$error }">
<datepicker
ref="select"
:ref-name="'select'"
@@ -8,15 +11,21 @@
:parse-typed-date="formatTypedValue"
:language="locale"
:monday-first="mondayFirst"
:input-class="'br2 f5 ba b--black-40 pa2 outline-0'"
:input-class="inputClass"
:typeable="true"
:clear-button="true"
:show-calendar-on-focus="showCalendarOnFocus"
@input="$emit('input', exchangeValue($event))"
@selected="update"
@clearDate="update('')"
@input="onInput($event)"
@selected="onSelected($event)"
@clearDate="onSelected('')"
/>
<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>
</template>
@@ -44,6 +53,10 @@ export default {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
defaultDate: {
type: String,
default: '',
@@ -55,7 +68,11 @@ export default {
showCalendarOnFocus: {
type: Boolean,
default: false,
}
},
validator: {
type: Object,
default: null,
},
},
data() {
@@ -84,6 +101,25 @@ export default {
displayFormat() {
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: {
@@ -130,7 +166,6 @@ export default {
}
this.exchange = mdate.format(this.exchangeFormat);
}
this.$emit('input', this.exchange);
},
updateExchange(date) {
@@ -156,8 +191,19 @@ export default {
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>
@@ -1,21 +1,22 @@
<style scoped>
input {
.input {
transition: all;
transition-duration: 0.2s;
border: 1px solid #c4cdd5;
}
input:focus {
.input:focus {
border: 1px solid #5c6ac4;
}
</style>
<template>
<div>
<div :class="{ 'form-group-error': validator && validator.$error }">
<label
v-if="title"
:for="realid"
class="mb2"
:class="{ b: required }"
:class="{ b: required, error: validator && validator.$error }"
>
{{ title }}
</label>
@@ -31,9 +32,20 @@ input:focus {
:style="inputStyle"
:value="value"
:maxlength="maxlength"
@input="event => { $emit('input', event.target.value) }"
@keyup.enter="event => { $emit('submit', event.target.value) }"
@input="onInput($event)"
@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>
</template>
@@ -49,6 +61,10 @@ export default {
type: String,
default: '',
},
label: {
type: String,
default: null,
},
id: {
type: String,
default: '',
@@ -70,13 +86,17 @@ export default {
default: -1,
},
iclass: {
type: String,
type: [String, Array],
default: ''
},
maxlength: {
type: Number,
default: null,
},
validator: {
type: Object,
default: null,
},
},
computed: {
@@ -84,17 +104,72 @@ export default {
return this.id + this._uid;
},
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() {
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: {
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>
<p-input
ref="input"
v-model.lazy="modelValue"
v-model.lazy="prop"
:name="name"
:type="_type"
:class="inputClass"
@@ -14,7 +14,7 @@
:value="value"
:disabled="disabled"
:required="required"
@change="event => { $emit('change', event) }"
@change="$emit('change', $event)"
>
<slot></slot>
<slot slot="extra" name="inputextra"></slot>
@@ -82,6 +82,12 @@ export default {
}
},
data() {
return {
prop: null
};
},
computed: {
_type() {
if (this.$options.input_type) {
@@ -97,6 +103,16 @@ export default {
},
},
watch: {
modelValue(val) {
this.prop = val;
},
},
mounted() {
this.prop = this.modelValue;
},
methods: {
select() {
if (this.disabled) {
@@ -1,22 +1,22 @@
<style scoped>
select {
.select {
height: 34px;
transition: all;
transition-duration: 0.2s;
border: 1px solid #c4cdd5;
}
select:focus {
.select:focus {
border: 1px solid #5c6ac4;
}
</style>
<template>
<div>
<div :class="{ 'form-group-error': validator && validator.$error }">
<label
v-if="title"
:for="realid"
class="mb2"
:class="{ b: required }"
:class="{ b: required, error: validator && validator.$error }"
>
{{ title }}
</label>
@@ -27,7 +27,7 @@ select:focus {
:name="id"
:required="required"
:class="selectClass"
@input="event => { $emit('input', event.target.value) }"
@input="onInput"
>
<template v-if="Array.isArray(options)">
<option
@@ -54,6 +54,9 @@ select:focus {
</optgroup>
</template>
</select>
<small v-if="validator && (validator.$error && validator.required !== undefined && !validator.required)" class="error">
{{ requiredMessage }}
</small>
</div>
</template>
@@ -73,6 +76,10 @@ export default {
type: String,
default: '',
},
label: {
type: String,
default: null,
},
id: {
type: String,
default: '',
@@ -89,6 +96,10 @@ export default {
type: String,
default: '',
},
validator: {
type: Object,
default: null,
}
},
data() {
@@ -102,8 +113,19 @@ export default {
return this.id + this._uid;
},
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: {
@@ -129,7 +151,14 @@ export default {
focus() {
this.$refs.select.focus();
}
},
onInput(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('input', event.target.value);
},
},
};
</script>
+70 -123
View File
@@ -80,24 +80,26 @@
</div>
<!-- Create Client Modal -->
<sweet-modal ref="modalCreateClient" overlay-theme="dark" tabindex="-1" role="dialog"
:title="$t('settings.api_oauth_create')" @open="_focusCreateInput"
<sweet-modal ref="modalClient" overlay-theme="dark" tabindex="-1" role="dialog"
:title="form.id ? $t('settings.api_oauth_edit') : $t('settings.api_oauth_create')"
@open="_focusInput"
>
<!-- Form Errors -->
<error :errors="createForm.errors" />
<error :errors="form.errors" />
<!-- Create Client Form -->
<form class="form-horizontal" role="form">
<form ref="form" class="form-horizontal" role="form">
<!-- Name -->
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'create-client-name'"
ref="createClientName"
v-model="createForm.name"
:id="'client-name'"
ref="clientName"
v-model="form.name"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true"
:title="$t('settings.api_oauth_name')"
:validator="$v.form.name"
@submit="store"
/>
@@ -111,11 +113,12 @@
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'create-redirect-url'"
v-model="createForm.redirect"
:id="'redirect-url'"
v-model="form.redirect"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true"
:title="$t('settings.api_oauth_redirecturl')"
:validator="$v.form.redirect"
@submit="store"
/>
@@ -132,65 +135,7 @@
{{ $t('app.close') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="store">
{{ $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') }}
{{ form.id ? $t('app.save') : $t('app.create') }}
</a>
</div>
</sweet-modal>
@@ -200,6 +145,8 @@
<script>
import Error from '../partials/Error.vue';
import { SweetModal } from 'sweet-modal-vue';
import { validationMixin } from 'vuelidate';
import { required, url } from 'vuelidate/lib/validators';
export default {
@@ -208,17 +155,13 @@ export default {
Error
},
mixins: [validationMixin],
data() {
return {
clients: [],
createForm: {
errors: [],
name: '',
redirect: ''
},
editForm: {
form: {
errors: [],
name: '',
redirect: ''
@@ -226,6 +169,18 @@ export default {
};
},
validations: {
form: {
name: {
required,
},
redirect: {
required,
url,
}
}
},
computed: {
dirltr() {
return this.$root.htmldir == 'ltr';
@@ -244,26 +199,16 @@ export default {
/**
* Focus on modal open.
*/
_focusCreateInput() {
_focusInput() {
let vm = this;
setTimeout(function() {
vm.$refs.createClientName.focus();
vm.$refs.clientName.focus();
}, 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() {
axios.get('oauth/clients')
.then(response => {
@@ -272,46 +217,41 @@ export default {
},
/**
* Show the form for creating new clients.
*/
* Show the form for creating new clients.
*/
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() {
this.persistClient(
'post', 'oauth/clients',
this.createForm
);
this.$v.$touch();
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) {
this.editForm.id = client.id;
this.editForm.name = client.name;
this.editForm.redirect = client.redirect;
this.form = Object.assign({errors:[]}, client);
this.$refs.modalEditClient.open();
this.$refs.modalClient.open();
},
/**
* Update the client being edited.
*/
update() {
this.persistClient(
'put', 'oauth/clients/' + this.editForm.id,
this.editForm
);
},
/**
* Persist the client to storage using the given form.
*/
* Persist the client to storage using the given form.
*/
persistClient(method, uri, form) {
form.errors = [];
@@ -319,10 +259,6 @@ export default {
.then(response => {
this.getClients();
form.name = '';
form.redirect = '';
form.errors = [];
this.closeModal();
})
.catch(error => {
@@ -335,8 +271,8 @@ export default {
},
/**
* Destroy the given client.
*/
* Destroy the given client.
*/
destroy(client) {
axios.delete('oauth/clients/' + client.id)
.then(response => {
@@ -345,8 +281,19 @@ export default {
},
closeModal() {
this.$refs.modalCreateClient.close();
this.$refs.modalEditClient.close();
this.resetField();
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" />
<!-- Create Token Form -->
<form class="form-horizontal" role="form" @submit.prevent="store">
<form ref="form" class="form-horizontal" role="form" @submit.prevent="store">
<!-- Name -->
<div class="col-md-auto">
<form-input
@@ -80,6 +80,7 @@
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true"
:title="$t('settings.api_token_name')"
:validator="$v.form.name"
/>
</div>
@@ -141,6 +142,8 @@
<script>
import Error from '../partials/Error.vue';
import { SweetModal } from 'sweet-modal-vue';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
export default {
@@ -149,6 +152,8 @@ export default {
Error
},
mixins: [validationMixin],
data() {
return {
endpoint: '',
@@ -165,6 +170,14 @@ export default {
};
},
validations: {
form: {
name: {
required,
}
}
},
computed: {
dirltr() {
return this.$root.htmldir == 'ltr';
@@ -207,6 +220,8 @@ export default {
* Close all modals.
*/
closeModal() {
this.$refs.form.reset();
this.$v.$reset();
this.$refs.modalCreateToken.close();
this.$refs.modalAccessToken.close();
},
@@ -232,6 +247,12 @@ export default {
* Create a new personal access token.
*/
store() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
this.accessToken = null;
this.form.errors = [];
+32 -8
View File
@@ -1,4 +1,15 @@
<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>
<template>
@@ -113,16 +124,19 @@
<div class="mb2">
<toggle-button class="mr2" :sync="true" :labels="true" :value="stateInput" @change="stateInput = !stateInput" />
<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">
<form-input
:id="'frequency'"
v-model="frequencyInput"
:value="frequencyInput"
v-model.number="frequencyInput"
:input-type="'number'"
:width="60"
:required="true"
@input="stateInput = $event > 0"
:validator="$v.frequencyInput"
@input="onInput($event)"
/>
</div>
</stay-in-touch-label>
@@ -153,6 +167,8 @@
<script>
import { SweetModal } from 'sweet-modal-vue';
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', {
@@ -181,6 +197,8 @@ export default {
StayInTouchLabel,
},
mixins: [validationMixin],
props: {
hash: {
type: String,
@@ -196,6 +214,13 @@ export default {
},
},
validations: {
frequencyInput: {
required,
numeric,
},
},
data() {
return {
frequency: 0,
@@ -250,10 +275,9 @@ export default {
return;
}
// make sure we can't press update if the frequency is invalid
// and if the feature is activated
if ((this.frequencyInput == '' || this.frequencyInput < 1) && this.stateInput) {
this.errorMessage = this.$t('people.stay_in_touch_invalid');
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
@@ -1,7 +1,7 @@
<style scoped>
.photo {
height: 200px;
}
.photo {
height: 200px;
}
</style>
<template>
@@ -58,6 +58,7 @@
:required="true"
:class="'dtc pr2'"
:title="$t('people.gifts_add_gift_name')"
:validator="$v.newGift.name"
@submit="store"
/>
</div>
@@ -126,14 +127,16 @@
<form-checkbox
v-model="hasRecipient"
:name="'has_recipient'"
@change="() => { this.$refs.recipient.focus() }"
@change="(val) => { if (val) { $refs.recipient.focus() } }"
>
{{ $t('people.gifts_add_someone', {name: ''}) }}
</form-checkbox>
<form-select
ref="recipient"
v-model="newGift.recipient_id"
:label="$t('people.gifts_add_recipient_field')"
:options="familyContacts"
:validator="$v.newGift.recipient_id"
@input="hasRecipient = true"
/>
</div>
@@ -205,6 +208,8 @@
import Error from '../../partials/Error.vue';
import PhotoUpload from '../photo/PhotoUpload.vue';
import { validationMixin } from 'vuelidate';
import { required, maxLength } from 'vuelidate/lib/validators';
export default {
components: {
@@ -212,6 +217,8 @@ export default {
PhotoUpload
},
mixins: [validationMixin],
props: {
contactId: {
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: {
locale() {
return this.$root.locale;
@@ -307,6 +335,7 @@ export default {
this.displayUpload= this.gift ? this.gift.photos.length > 0 : false;
this.errors = [];
this.$v.$reset();
},
close() {
@@ -319,6 +348,12 @@ export default {
this.newGift.recipient_id = null;
}
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
let method = this.gift ? 'put' : 'post';
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_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_birthdate_label' => 'Birthdate',
'information_no_work_defined' => 'No work information defined',
'information_work_at' => 'at :company',
'work_add_cta' => 'Update work information',
@@ -308,6 +309,7 @@ return [
'gifts_add_value' => 'Value (optional)',
'gifts_add_comment' => 'Comment (optional)',
'gifts_add_recipient' => 'Recipient (optional)',
'gifts_add_recipient_field' => 'Recipient',
'gifts_add_photo' => 'Photo (optional)',
'gifts_add_photo_title' => 'Add a photo for this gift',
'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_add_reminder' => 'Add a reminder for this date',
'deceased_label' => 'Deceased',
'deceased_date_label' => 'Deceased date',
'deceased_label_with_date' => 'Deceased on :date',
'deceased_age' => 'Age at death',
+8
View File
@@ -120,4 +120,12 @@ return [
'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 {
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"
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:
version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"