validation fixes, etc...

remotes/origin/hasaccess
Sanasol 8 years ago
parent 8cd3c102f7
commit e1d33dbfee
  1. 338
      web/src/components/CourseRedactor.vue
  2. 4
      web/src/js/modules/api.js
  3. 9
      web/src/js/modules/notification.js
  4. 36
      web/src/sass/_common.sass

@ -1,6 +1,6 @@
<template>
<div id="lilcity__course-redactor" v-on:course_publish="onCoursePublish" v-on:course_preview="onCoursePreview">
<div v-if="!courseLoading">
<div v-if="!courseLoading && !mounting">
<form v-if="viewSection !== 'lessons-edit'" @submit.prevent="onSubmit">
<div class="info">
<div class="info__section" :style="coverBackgroundStyle">
@ -60,15 +60,10 @@
<div class="info__field field field_info"
v-bind:class="{ error: ($v.course.duration.$dirty || showErrors) && $v.course.duration.$invalid }">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model.number="course.duration"
<div class="field__wrap field__wrap__appended">
<input type="text" class="field__input field__input__appended" v-model.number="course.duration"
@input="$v.course.duration.$touch()">
</div>
</div>
<div class="info__field field field_info">
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model.number="displayPrice" :disabled="!course.is_paid">
<button disabled class="field__append">{{pluralize(course.duration, ['день', 'дня', 'дней'])}}</button>
</div>
</div>
</div>
@ -85,14 +80,16 @@
<div class="field__wrap field__wrap--additional">{{ courseFullUrl }}</div>
</div>
<div v-if="live" class="info__field field">
<div v-if="live" class="info__field field"
v-bind:class="{ error: ($v.course.stream.$dirty || showErrors) && $v.course.stream.$invalid }">
<div class="field__label field__label_gray">ССЫЛКА НА VIMEO</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model="course.stream">
</div>
</div>
<div v-if="live" class="info__field field">
<div v-if="live" class="info__field field"
v-bind:class="{ error: ($v.course.date.$dirty || showErrors) && $v.course.date.$invalid }">
<div class="field__label">ДАТА</div>
<div class="field__wrap">
<lil-select :value.sync="course.date" :options="scheduleOptions" placeholder="Выберите дату"/>
@ -102,20 +99,28 @@
<div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ДОСТУП</div>
<div class="field__wrap">
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="true" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Платный</span>
</label>
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="false" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Бесплатный</span>
</label>
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="true" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Платный</span>
</label>
</div>
</div>
<label v-if="!live" class="info__switch switch switch_lg">
<input type="checkbox" class="switch__input" v-model="course.is_featured">
<span class="switch__content">Выделить</span>
</label>
<div v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
<div class="field__wrap field__wrap__appended field__wrap__100px">
<input type="text" class="field__input field__input__appended" v-model.number="displayPrice">
<button disabled class="field__append">руб.</button>
</div>
</div>
<!--<label v-if="!live" class="info__switch switch switch_lg">-->
<!--<input type="checkbox" class="switch__input" v-model="course.is_featured">-->
<!--<span class="switch__content">Выделить</span>-->
<!--</label>-->
</div>
<div v-if="!live" class="info__fieldset">
<div class="info__field field">
@ -250,7 +255,7 @@
<div v-else>
<div class="section">
<div class="section__center center">
Загрузка...
<h1>Загрузка...</h1>
</div>
</div>
</div>
@ -271,12 +276,13 @@
import {api} from "../js/modules/api";
import BlockAdd from "./blocks/BlockAdd";
import $ from 'jquery';
import {required, minValue, numeric } from 'vuelidate/lib/validators'
import {required, minValue, numeric, url } from 'vuelidate/lib/validators'
import slugify from 'slugify';
import Draggable from 'vuedraggable';
import {showNotification} from "../js/modules/notification";
import createHistory from "history/createBrowserHistory";
import moment from 'moment'
import _ from 'lodash'
const history = createHistory();
@ -285,6 +291,7 @@
props: ["authorName", "authorPicture", "accessToken", "courseId", "live"],
data() {
return {
mounting: false,
viewSection: 'course',
me: null,
users: null,
@ -373,6 +380,14 @@
'Суббота',
'Воскресенье',
],
fields: {
title: "Заголовок",
short_description: "Краткое описание",
stream: "Ссылка на Vimeo",
date: "Дата",
duration: "Продолжительность",
category: "Категория",
},
showErrors: false,
savingTimeout: null,
savingDebounceTimeout: null,
@ -381,25 +396,47 @@
courseSyncHook: false, // Если true, то watch не будет отправлять курс на обновление
}
},
validations: {
course: {
title: {
required
},
short_description: {
required
},
duration: {
required,
numeric,
minValue: minValue(1)
},
category: {
required,
numeric,
minValue: minValue(1)
}
},
validations() {
if(this.live) {
return {
course: {
title: {
required
},
short_description: {
required
},
stream: {
required,
// url
},
date: {
required
},
},
};
} else {
return {
course: {
title: {
required
},
short_description: {
required
},
duration: {
required,
numeric,
minValue: minValue(1)
},
category: {
required,
numeric,
minValue: minValue(1)
}
},
};
}
},
methods: {
onCoverImageSelected(event) {
@ -489,6 +526,7 @@
window.scrollTo(0, 0);
},
onSubmit() {
console.log('submit');
this.courseSaving = true;
api.saveCourse(this.course, this.accessToken)
.then((response) => {
@ -534,14 +572,17 @@
});
},
loadCourseDraft() {
console.log('loadCourseDraft');
if(this.live) { return; }
this.courseLoading = true;
let response = api.getCourseDraft(this.accessToken);
response
.then((response) => {
this.courseLoading = false;
this.course = api.convertCourseJson(response.data);
this.course.live = this.live;
this.courseLoading = false;
this.lessons = response.data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
});
@ -554,6 +595,7 @@
return response;
},
loadCourse() {
console.log('loadCourse');
this.courseLoading = true;
let request = null;
if(this.live) {
@ -563,9 +605,18 @@
}
request
.then((response) => {
this.courseLoading = false;
this.course = api.convertCourseJson(response.data);
this.course.live = this.live;
if (this.course.date) {
this.course.date = _.find(this.scheduleOptions, (item) => {
return item.value == this.course.date;
})
}
this.$nextTick(() => {
this.courseLoading = false;
});
this.lessons = response.data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
});
@ -581,54 +632,84 @@
},
onCoursePublish() {
if(this.validate()) {
const publishButton = $('#course-redactor__publish-button');
publishButton.attr('disabled', 'disabled');
api.publishCourse(this.course.id, this.accessToken)
.then((response) => {
if(this.live) {
window.location = '/school/lessons';
} else {
window.location = '/course/on-moderation';
}
})
.catch(() => {
publishButton.removeAttr('disabled');
});
}
},
validate(silent) {
console.log('valadte', arguments);
this.showErrors = true;
if (this.$v.$invalid) {
showNotification("error", "Заполните все необходимые поля");
return;
if(!silent) {
for(let i in this.$v.course) {
if(this.$v.course[i].$invalid) {
showNotification("error", "Ошибка валидации поля "+this.fields[i]);
}
}
}
// showNotification("error", "Заполните все необходимые поля");
return false;
}
const publishButton = $('#course-redactor__publish-button');
publishButton.attr('disabled', 'disabled');
api.publishCourse(this.course.id, this.accessToken)
.then((response) => {
window.location = '/course/on-moderation';
})
.catch(() => {
publishButton.removeAttr('disabled');
});
return true;
},
onCoursePreview() {
let url;
if (!this.course.url) {
url = `/course/${this.course.id}`;
} else {
if(this.course.id) {
let url;
if(this.live) {
url = `/school/lessons/${this.course.id}`;
} else {
if (this.course.url) {
url = `/course/${this.course.url}`;
}
url = `/course/${this.course.id}`;
}
let newTab = window.open(url, '_blank');
newTab.focus();
} else {
showNotification("error", "Пока еще нечего посмотреть, давайте что-нибудь напишем");
}
url = `/course/${this.course.url}`;
let newTab = window.open(url, '_blank');
newTab.focus();
},
saveCourseDraft: function (newValue, oldValue) {
// if (!oldValue.id) {
// return;
// }
if(this.live) {
if(!this.course.date || this.course.short_description == '' || this.course.title == '') {
console.log('live valiedation error');
return;
}
} else {
if(this.course.short_description == '' || this.course.title == '') {
console.log('course validation error');
return;
}
}
// if(this.live) {
// if(!this.course.date || this.course.short_description == '' || this.course.title == '') {
// console.log('live valiedation error');
// return;
// }
// } else {
// if(this.course.short_description == '' || this.course.title == '') {
// console.log('course validation error');
// return;
// }
// }
if (this.savingDebounceTimeout) {
clearTimeout(this.savingDebounceTimeout);
}
this.courseSyncHook = false;
this.savingDebounceTimeout = setTimeout(() => {
if(!this.validate(true)) {
return;
}
this.courseSaving = true;
clearTimeout(this.savingTimeout);
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ...';
@ -659,10 +740,20 @@
if (!contentElement.data.id) {
this.$set(this.course.content[index].data, 'id', remoteUUIDMapper[contentElement.uuid])
}
})
});
if (courseData.url) {
this.slugChanged = true;
}
if(courseData.id) {
this.course.id = courseData.id;
}
if(courseData.date) {
this.course.date = _.find(this.scheduleOptions, function(item){
return item.value == courseData.date;
});
}
this.$nextTick(() => {
this.courseSyncHook = false;
});
@ -675,7 +766,15 @@
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
alert('Произошло что-то страшное: '+err.toString());
// alert('Произошло что-то страшное: '+err.toString());
console.log(err.response.data);
if(err.response.data) {
for(let i in err.response.data) {
showNotification("error", this.fields[i]+": "+err.response.data[i].join(', '));
}
} else {
showNotification("error", "Ошибка "+err.toString());
}
});
}, 500);
},
@ -694,16 +793,24 @@
this.currentLesson = this.lessons.find((i)=>{return i.id === lessonId});
this.viewSection = 'lessons-edit';
}
},
pluralize(count, words) {
var cases = [2, 0, 1, 1, 1, 2];
return words[ (count % 100 > 4 && count % 100 < 20) ? 2 : cases[ Math.min(count % 10, 5)] ];
}
},
mounted() {
this.mounting = true;
moment.locale('ru');
this.course.live = this.live;
// Listen for changes to the current location.
this.unlisten = history.listen(this.updateViewSection);
api.getCategories(this.accessToken)
.then((response) => {
let promises = [];
let cats = api.getCategories(this.accessToken);
promises.push(cats);
cats.then((response) => {
if (response.data) {
this.categoryOptions = response.data.results.map((category) => {
return {
@ -715,25 +822,30 @@
this.updateCategory();
});
api.getSchedule(this.accessToken)
.then((response) => {
if (response.data) {
this.scheduleOptions = response.data.results.map((schedule) => {
var now = new Date();
now.setDate(now.getDate() + (schedule.weekday+(7-now.getDay())) % 7);
if(this.live) {
let schedule = api.getSchedule(this.accessToken);
promises.push(schedule);
return {
title: `${schedule.title} (${this.weekdays[schedule.weekday]}, ${moment(now).format("D MMM")})`,
value: moment(now).format('YYYY-MM-DD')
}
});
}
this.updateCategory();
});
schedule.then((response) => {
if (response.data) {
this.scheduleOptions = response.data.results.map((schedule) => {
var now = new Date();
now.setDate(now.getDate() + (schedule.weekday + (7 - now.getDay())) % 7);
return {
title: `${schedule.title} (${this.weekdays[schedule.weekday]}, ${moment(now).format("D MMM")})`,
value: moment(now).format('YYYY-MM-DD')
}
});
}
this.updateCategory();
});
}
let user = api.getCurrentUser(this.accessToken);
promises.push(user);
api.getCurrentUser(this.accessToken)
.then((response) => {
user.then((response) => {
if (response.data) {
this.me = response.data;
@ -753,15 +865,34 @@
}
});
if (this.courseId) {
this.loadCourse().then(()=>{this.updateViewSection(window.location, 'load')}).catch(()=>{
this.updateViewSection(window.location, 'load err')
})
} else {
this.loadCourseDraft().then(()=>{this.updateViewSection(window.location, 'load draft')}).catch(()=>{
this.updateViewSection(window.location, 'load draft err')
});
}
// if (this.courseId) {
// this.loadCourse().then(()=>{this.updateViewSection(window.location, 'load')}).catch(()=>{
// this.updateViewSection(window.location, 'load err')
// })
// } else {
// this.loadCourseDraft().then(()=>{this.updateViewSection(window.location, 'load draft')}).catch(()=>{
// this.updateViewSection(window.location, 'load draft err')
// });
// }
console.log('wait promises');
Promise.all(promises.map(p => p.catch(e => e))).then(()=>{
console.log('promises end');
this.mounting = false;
let load;
if (this.courseId) {
load = this.loadCourse()
} else {
load = this.loadCourseDraft();
}
load.then(()=>{
this.updateViewSection(window.location, 'load '+this.courseId)
})
.catch(()=>{
this.updateViewSection(window.location, 'load err '+this.courseId)
})
});
console.log('mounted end');
// this.updateViewSection(window.location);
@ -826,14 +957,17 @@
watch: {
'course': {
handler: function (newValue, oldValue) {
console.log('watch', newValue, oldValue);
console.log('watch', JSON.stringify(newValue), JSON.stringify(oldValue));
// Если курс загрузился и есть ID - делаем кнопки превью и публикации активными
console.log('newValue.id', newValue.id);
if (newValue.id) {
console.log('newValue.id disabled remove', newValue.id);
$('#course-redactor__preview-button').removeAttr('disabled');
$('#course-redactor__publish-button').removeAttr('disabled');
}
console.log('courseSyncHook', this.courseSyncHook);
if (this.courseSyncHook) {
if (this.courseSyncHook || this.courseLoading) {
console.log('abort save draft', this.courseSyncHook, this.courseLoading);
return;
}
this.saveCourseDraft(newValue, oldValue);

@ -257,7 +257,7 @@ export const api = {
},
convertCourseJson: (courseJSON) => {
let isDeferred = false;
let deferredDate = '';
let deferredDate = false;
let deferredTime = '';
if (courseJSON.deferred_start_at) {
let deferredDateTime = moment(courseJSON.deferred_start_at);
@ -275,7 +275,7 @@ export const api = {
price: parseFloat(courseJSON.price),
is_paid: parseFloat(courseJSON.price) > 0,
is_deferred: isDeferred,
date: deferredDate,
date: deferredDate || courseJSON.date,
time: deferredTime ? {title: deferredTime, value: deferredTime} : null,
duration: courseJSON.duration,
is_featured: courseJSON.is_featured,

@ -3,8 +3,17 @@ import '../../sass/components/notification.scss';
export function showNotification(style, text) {
let htmlNode = document.createElement('div');
let htmlElement = $(htmlNode).addClass('notification').addClass(`notification--${style}`).text(text).appendTo($('body'));
if($('body').find('.notification').length) {
var outerHeight = 0;
$('body').find('.notification').each(function() {
outerHeight += $(this).outerHeight()+10;
});
htmlElement.css('top', outerHeight+'px');
}
setTimeout(() => {
htmlElement.fadeOut(400, () => {
htmlElement.remove();

@ -2060,14 +2060,14 @@ a.grey-link
&.active
z-index: 99
&.active &__head
border-color: $cl
border-color: #686868
&:after
transform: rotate(180deg)
&.active &__drop
opacity: 1
visibility: visible
&.selected &__head
color: $cl
color: #686868
.error.info__field--light
.select
@ -2078,15 +2078,15 @@ a.grey-link
.info__field--light
.select
.select__head
color: #fff
color: #525252
font-size: 15px
&:after
border-color: #fff transparent transparent transparent
border-color: #525252 transparent transparent transparent
&.selected
.select__head
color: #fff
color: #525252
&:after
border-color: #fff transparent transparent transparent
border-color: #525252 transparent transparent transparent
.field
margin-bottom: 20px
@ -2102,6 +2102,12 @@ a.grey-link
margin-left: auto
&__wrap
position: relative
&__appended
display: flex
&__100px
width: 100px
&__append
text-transform: uppercase
&__input,
&__textarea
width: 100%
@ -2125,6 +2131,8 @@ a.grey-link
&_bg
height: 50px
padding: 0 20px
&__appended
flex: 1;
&__textarea
display: block
height: 36px
@ -2156,10 +2164,10 @@ a.grey-link
color: $gray
&_info &__input,
&_info &__textarea
border-color: rgba($border,.2)
color: white
border-color: rgba(#525252,.2)
color: #525252
&:focus
border-color: white
border-color: #686868
&.error &__input,
&.error &__textarea
border-color: $pink
@ -3215,7 +3223,7 @@ a.grey-link
left: 0
width: 100%
height: 100%
background-image: linear-gradient(0deg, rgba(0,0,0,0.24) 0%, rgba(0,0,0,0.64) 100%)
background-image: linear-gradient(135deg, rgb(255, 226, 235) 0%, rgb(216, 245, 246) 100%)
z-index: -2
&__sidebar
flex: 0 0 calc(50% - 169px)
@ -3249,11 +3257,11 @@ a.grey-link
letter-spacing: 1px
&__value
font-size: 18px
color: white
color: #525252
&__upload
margin-left: auto
font-size: 16px
color: rgba(white,.7)
color: #525252
&__foot
display: flex
margin-top: auto
@ -3275,7 +3283,7 @@ a.grey-link
display: flex
flex-direction: column
&__wrap &__field
margin-bottom: 30px
margin-bottom: 20px
&:last-child
margin: 0
&__fieldset
@ -3283,6 +3291,8 @@ a.grey-link
margin-bottom: 50px
&:last-child
margin-top: auto
&:only-child
margin-top: 0px
.add
display: flex

Loading…
Cancel
Save