/*eslint no-var: "error"*/ /*eslint no-console: "off"*/ /*eslint no-unused-vars: warn */ /*eslint max-len: ["error", { "code": 160 }] */ /*eslint-disable no-trailing-spaces */ /*eslint-env es6*/ // Put this file in path/to/plugin/amd/src import {SimpleLine} from "./simpleline"; import {call} from 'core/ajax'; import notification from 'core/notification'; import {debounce} from './debounce'; import {get_strings} from 'core/str'; import {load_stringkeys, load_strings} from './string-helper'; import {objCopy,transportItem} from './studyplan-processor'; import Debugger from './debugger'; import {download,upload} from './downloader'; const STUDYPLAN_EDITOR_FIELDS = ['name','shortname','description','context_id', 'slots','startdate','enddate','aggregation','aggregation_config']; export default { STUDYPLAN_EDITOR_FIELDS: STUDYPLAN_EDITOR_FIELDS, // make copy available in plugin install(Vue/*,options*/){ let debug = new Debugger("treestudyplan-editor"); debug.enable(); /************************************ * * * Treestudyplan Editor components * * * ************************************/ /** * Check if element is visible * @param {Object} elem The element to check * @returns {boolean} True if visible */ function isVisible(elem){ return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); } // Create new eventbus for interaction between item components const ItemEventBus = new Vue(); let string_keys = load_stringkeys({ conditions: [ { value: null, textkey: 'condition_default'}, { value: 'ALL', textkey: 'condition_all'}, { value: '67', textkey: 'condition_67'}, { value: '50', textkey: 'condition_50'}, { value: 'ANY', textkey: 'condition_any'}, ], }); let strings = load_strings({ studyplan_text: { studyline_editmode: 'studyline_editmode', editmode_modules_hidden:'editmode_modules_hidden', studyline_add: 'studyline_add', add$core: 'add$core', edit$core: 'edit$core', studyline_name: 'studyline_name', studyline_name_ph: 'studyline_name_ph', studyline_shortname: 'studyline_shortname', studyline_shortname_ph: 'studyline_shortname_ph', studyline_color: 'studyline_color', associations: 'associations', associated_cohorts: 'associated_cohorts', associated_users: 'associated_users', studyline_edit: 'studyline_edit', studyplan_name: 'studyplan_name', studyplan_name_ph: 'studyplan_name_ph', studyplan_shortname: 'studyplan_shortname', studyplan_shortname_ph: 'studyplan_shortname_ph', studyplan_description: 'studyplan_description', studyplan_description_ph: 'studyplan_description_ph', studyplan_slots: 'studyplan_slots', studyplan_startdate: 'studyplan_startdate', studyplan_enddate: 'studyplan_enddate', }, studyplan_advanced: { advanced_tools: 'advanced_tools', confirm_cancel: 'confirm_cancel', confirm_ok: 'confirm_ok', success$core: 'success$core', error$core: 'failed$core', advanced_converted: 'advanced_converted', advanced_skipped: 'advanced_skipped', advanced_failed: 'advanced_failed', advanced_locked: 'advanced_locked', advanced_multiple: 'advanced_multiple', advanced_error: 'advanced_error', advanced_tools_heading: 'advanced_tools_heading', advanced_warning_title: 'advanced_warning_title', advanced_warning: 'advanced_warning', advanced_pick_scale: 'advanced_pick_scale', advanced_course_manipulation_title: 'advanced_course_manipulation_title', advanced_force_scale_title: 'advanced_force_scale_title', advanced_force_scale_desc: 'advanced_force_scale_desc', advanced_force_scale_button: 'advanced_force_scale_button', advanced_disable_autoenddate_title: 'advanced_disable_autoenddate_title', advanced_disable_autoenddate_desc: 'advanced_disable_autoenddate_desc', advanced_disable_autoenddate_button: 'advanced_disable_autoenddate_button', advanced_confirm_header: 'advanced_confirm_header', advanced_force_scale_confirm: 'advanced_force_scale_confirm', advanced_import: 'advanced_import', advanced_export: 'advanced_export', advanced_export_csv: 'advanced_export_csv', advanced_import_from_file: 'advanced_import_from_file', advanced_purge: "advanced_purge", advanced_purge_expl: "advanced_purge_expl", advanced_cascade_cohortsync_title: "advanced_cascade_cohortsync_title", advanced_cascade_cohortsync_desc: "advanced_cascade_cohortsync_desc", advanced_cascade_cohortsync: "advanced_cascade_cohortsync", }, studyplan_edit: { studyplan_edit: 'studyplan_edit', studyplan_name: 'studyplan_name', studyplan_name_ph: 'studyplan_name_ph', studyplan_shortname: 'studyplan_shortname', studyplan_shortname_ph: 'studyplan_shortname_ph', studyplan_description: 'studyplan_description', studyplan_description_ph: 'studyplan_description_ph', studyplan_context: 'studyplan_context', studyplan_slots: 'studyplan_slots', studyplan_startdate: 'studyplan_startdate', studyplan_enddate: 'studyplan_enddate', choose_aggregation_style: 'choose_aggregation_style', setting_bistate_thresh_excellent: 'setting_bistate_thresh_excellent', settingdesc_bistate_thresh_excellent: 'settingdesc_bistate_thresh_excellent', setting_bistate_thresh_good: 'setting_bistate_thresh_good', settingdesc_bistate_thresh_good: 'settingdesc_bistate_thresh_good', setting_bistate_thresh_completed: 'setting_bistate_thresh_completed', settingdesc_bistate_thresh_completed: 'settingdesc_bistate_thresh_completed', setting_bistate_support_failed: 'setting_bistate_support_failed', settingdesc_bistate_support_failed: 'settingdesc_bistate_support_failed', setting_bistate_thresh_progress: 'setting_bistate_thresh_progress', settingdesc_bistate_thresh_progress: 'settingdesc_bistate_thresh_progress', setting_bistate_accept_pending_submitted: 'setting_bistate_accept_pending_submitted', settingdesc_bistate_accept_pending_submitted: 'settingdesc_bistate_accept_pending_submitted', }, studyplan_associate: { associations: 'associations', associated_cohorts: 'associated_cohorts', associated_users: 'associated_users', associate_cohorts: 'associate_cohorts', associate_users: 'associate_users', add_association: 'add_association', delete_association: 'delete_association', associations_empty: 'associations_empty', associations_search: 'associations_search', cohorts: 'cohorts', users: 'users', selected: 'selected', name: 'name', context: 'context', }, item_text: { select_conditions: "select_conditions", item_configuration: "item_configuration", }, item_course_text: { select_conditions: "select_conditions", select_grades: "select_grades", coursetiming_past: "coursetiming_past", coursetiming_present: "coursetiming_present", coursetiming_future: "coursetiming_future", grade_include: "grade_include", grade_require: "grade_require", }, invalid: { error: 'error', }, completion: { completion_completed: "completion_completed", completion_incomplete: "completion_incomplete", aggregation_all: "aggregation_all", aggregation_any: "aggregation_any", aggregation_overall_all: "aggregation_overall_all", aggregation_overall_any: "aggregation_overall_any", completion_not_configured: "completion_not_configured", configure_completion: "configure_completion", }, badge: { share_badge: "share_badge", dateissued: "dateissued", dateexpire: "dateexpire", badgeinfo: "badgeinfo", }, }); /* * T-STUDYPLAN-ADVANCED */ Vue.component('t-studyplan-advanced', { props: { value: { type: Object, default(){ return null;}, }, }, data() { return { force_scales: { selected_scale: null, result: [], }, text: strings.studyplan_advanced, }; }, created() { }, mounted() { }, updated() { }, computed: { scales(){ return [{ id: null, disabled: true, name: this.text.advanced_pick_scale, }].concat(this.value.advanced.force_scales.scales); }, }, methods: { disable_autoenddate(){ const self=this; call([{ methodname: 'local_treestudyplan_disable_autoenddate', args: { studyplan_id: this.value.id, } }])[0].done(function(response){ self.$bvModal.msgBoxConfirm((response.success?self.text.success$core:self.text.error$core) + "\n" + response.msg); }).fail(notification.exception); }, force_scales_start(){ // set confirmation box const self=this; this.$bvModal.msgBoxConfirm(this.text.advanced_force_scale_confirm,{ title: this.text.advanced_force_scale_confirm, okVariant: 'danger', okTitle: this.text.confirm_ok, cancelTitle: this.text.confirm_cancel, }).then( value => { if(value == true){ call([{ methodname: 'local_treestudyplan_force_studyplan_scale', args: { studyplan_id: this.value.id, scale_id: this.force_scales.selected_scale, } }])[0].done(function(response){ self.force_scales.result = response; }).fail(notification.exception); } }); }, export_plan(format){ const self = this; if(format == undefined || !["json","csv"].includes(format)){ format = "json"; } call([{ methodname: 'local_treestudyplan_export_plan', args: { studyplan_id: this.value.id, format: format, }, }])[0].done(function(response){ download(self.value.shortname+"."+format,response.content,response.format); }).fail(notification.exception); }, import_studylines(){ //const self = this; upload((filename,content)=>{ call([{ methodname: 'local_treestudyplan_import_studylines', args: { studyplan_id: this.value.id, content: content, format: "application/json", }, }])[0].done(function(response){ if(response.success){ location.reload(); } else { debug.error("Import failed: ",response.msg); } }).fail(notification.exception); }, "application/json"); }, purge_studyline(){ call([{ methodname: 'local_treestudyplan_delete_studyplan', args: { id: this.value.id, force: true, }, }])[0].done(function(response){ if(response.success){ location.reload(); } else { debug.error("Could not delete plan: ",response.msg); } }).fail(notification.exception); }, cascade_cohortsync(){ const self = this; call([{ methodname: 'local_treestudyplan_cascade_cohortsync', args: { studyplan_id: this.value.id, }, }])[0].done(function(response){ self.$bvModal.msgBoxOk(response.success?self.text.success$core:self.text.error$core, { title: self.text.advanced_cascade_cohortsync}); }).fail(notification.exception); }, modal_close(){ this.force_scales.result = []; } }, template: ` {{text.advanced_tools}} {{ text.advanced_warning}}

{{ text.advanced_cascade_cohortsync_title}}

{{ text.advanced_cascade_cohortsync_desc}}
{{ text.advanced_cascade_cohortsync}}

{{ text.advanced_force_scale_title}}

{{ text.advanced_force_scale_desc}}
{{ text.advanced_force_scale_button}}
  • {{c.course.fullname}}
    • {{g.name}} {{text.advanced_converted}}{{text.advanced_skipped}} {{text.advanced_error}}

{{ text.advanced_disable_autoenddate_title}}

{{ text.advanced_disable_autoenddate_desc}}
{{ text.advanced_disable_autoenddate_button}}
{{ text.advanced_export}} {{ text.advanced_import}} {{ text.advanced_export_csv}}

{{text.advanced_purge_expl}}

{{ text.advanced_purge}}

` }); /* * T-STUDYPLAN-EDIT */ Vue.component('t-studyplan-edit', { props: { 'value' :{ type: Object, default(){ return null;}, }, 'mode' :{ type: String, default() { return "edit";}, }, 'type' :{ type: String, default() { return "link";}, }, 'variant' : { type: String, default() { return "";}, } }, data() { return { show: false, config: { userfields: [ { key: "selected",}, { key: "firstname", "sortable": true,}, { key: "lastname", "sortable": true,}, ], cohortfields:[ { key: "selected",}, { key: "name", "sortable": true,}, { key: "context", "sortable": true,}, ] }, editdata: { name: '', shortname: '', description: '', context_id: 1, slots : 4, startdate: (new Date()).getFullYear() + '-08-01', enddate: ((new Date()).getFullYear()+1) + '-08-01', aggregation: 'bistate', aggregation_config: '', }, aggregation_parsed: { }, aggregators: [], categories: [ { context_id: 1, category: { path: "System"}}], // overwritten during load... text: strings.studyplan_edit, }; }, created() { // retrieve aggregator info const self = this; call([{ methodname: 'local_treestudyplan_list_aggregators', args: [], }])[0].done(function(response){ self.aggregators = response; for(const ix in self.aggregators){ const ag = self.aggregators[ix]; try{ if(ag.defaultconfig && ag.defaultconfig.length > 0){ self.aggregation_parsed[ag.id] = JSON.parse(ag.defaultconfig); } } catch(e){ debug.warn(e); } } }).fail(notification.exception); call([{ methodname: 'local_treestudyplan_list_accessible_categories', args: {operation: "edit",} }])[0].done(function(response){ for(const ix in response){ const cat = response[ix]; cat.category.pathname = cat.category.path.join(" / "); } self.categories = response; }).fail(notification.exception); }, mounted() { }, updated() { }, computed: { }, methods: { editPlanStart(){ if(this.mode != 'create'){ objCopy(this.editdata,this.value,STUDYPLAN_EDITOR_FIELDS); } // decode the aggregation config data that is stored if(this.editdata.aggregation_config && this.editdata.aggregation_config.length > 0){ try{ this.aggregation_parsed[this.editdata.aggregation] = JSON.parse(this.editdata.aggregation_config); } catch(e){ debug.warn(e); } } this.show = true; }, editPlanFinish(){ const self = this; let args = { }; let method = 'local_treestudyplan_edit_studyplan'; if(this.mode == 'create'){ method = 'local_treestudyplan_add_studyplan'; } else { args['id'] = this.value.id; } // store the configuration for this aggregation type if it is relevant if(this.aggregation_parsed[this.editdata.aggregation]){ this.editdata.aggregation_config = JSON.stringify(this.aggregation_parsed[this.editdata.aggregation]); } objCopy(args,this.editdata,STUDYPLAN_EDITOR_FIELDS); call([{ methodname: method, args: args }])[0].done(function(response){ if(self.mode == 'create'){ self.$emit("created", response); // And reset the edit fields to default self.editdata = { name: '', shortname: '', description: '', context_id: 1, slots : 4, startdate: (new Date()).getFullYear() + '-08-01', enddate: ((new Date()).getFullYear()+1) + '-08-01', aggregation: 'bistate', aggregation_config: '', }; } else { // determine if the plan moved context... const moved_from = self.value.context_id; const moved_to = response.context_id; const moved = (moved_from != moved_to); objCopy(self.value,response,STUDYPLAN_EDITOR_FIELDS); self.$emit('input',self.value); if(moved){ self.$emit('moved',self.value,moved_from, moved_to); } } }).fail(notification.exception); }, numberFilter(value){ return value; } } , template: ` {{ text.studyplan_name}} {{ text.studyplan_shortname}} {{ text.studyplan_description}} {{ text.studyplan_context}} {{ text.studyplan_slots}} {{ text.studyplan_startdate}} {{ text.studyplan_enddate}} {{ text.choose_aggregation_style}} ` }); /* * T-STUDYPLAN-ASSOCIATE */ Vue.component('t-studyplan-associate', { props: ['value',], data() { return { show: false, config: { userfields: [ { key: "selected",}, { key: "firstname", "sortable": true,}, { key: "lastname", "sortable": true,}, ], cohortfields:[ { key: "selected",}, { key: "name", "sortable": true,}, { key: "context", "sortable": true,}, ] }, association: { cohorts: [], users: [], }, loading: { cohorts: false, users: false, }, search: {users: [], cohorts:[]}, selected: { search: {users: [] , cohorts:[]}, associated: {users: [] , cohorts:[]} }, text: strings.studyplan_associate, }; }, created() { }, mounted() { }, updated() { }, methods: { showModal(){ this.show = true; this.loadAssociations(); }, cohortOptionModel(c){ return { value: c.id, text: c.name + ' (' + c.context.path.join(' / ') + ')', }; }, userOptionModel(u){ return { value: u.id, text: u.firstname + ' ' + u.lastname, }; }, loadAssociations(){ const self = this; self.loading.cohorts = true; self.loading.users = true; call([{ methodname: 'local_treestudyplan_associated_users', args: { studyplan_id: self.value.id,} }])[0].done(function(response){ self.association.users = response.map(self.userOptionModel); self.loading.users = false; }).fail(notification.exception); call([{ methodname: 'local_treestudyplan_associated_cohorts', args: { studyplan_id: self.value.id,} }])[0].done(function(response){ self.association.cohorts = response.map(self.cohortOptionModel); self.loading.cohorts = false; }).fail(notification.exception); }, searchCohorts(searchtext){ const self = this; if(searchtext.length > 0) { call([{ methodname: 'local_treestudyplan_list_cohort', args: { like: searchtext, exclude_id: self.value.id} }])[0].done(function(response){ self.search.cohorts = response.map(self.cohortOptionModel); }).fail(notification.exception); } else { self.search.cohorts = []; } }, cohortAssociate(){ const self = this; let requests = []; const associated = self.association.cohorts; const search = self.search.cohorts; const searchselected = self.selected.search.cohorts; for(const i in searchselected){ const r = searchselected[i]; requests.push({ methodname: 'local_treestudyplan_connect_cohort', args: {studyplan_id: self.value.id, cohort_id: r}, fail: notification.exception, done: function(response){ if(response.success){ transportItem(associated,search,r); } } }); } call(requests); }, cohortDisassociate(){ const self = this; let requests = []; const associatedselected = self.selected.associated.cohorts; const associated = self.association.cohorts; const search = self.search.cohorts; for(const i in associatedselected){ const r = associatedselected[i]; requests.push({ methodname: 'local_treestudyplan_disconnect_cohort', args: {studyplan_id: self.value.id, cohort_id: r}, fail: notification.exception, done: function(response){ if(response.success){ transportItem(search,associated,r); } } }); } call(requests); }, searchUsers(searchtext){ const self = this; if(searchtext.length > 0) { call([{ methodname: 'local_treestudyplan_find_user', args: { like: searchtext, exclude_id: self.value.id} }])[0].done(function(response){ self.search.users = response.map(self.userOptionModel); }).fail(notification.exception); } else { self.search.users = []; } }, userAssociate(){ const self = this; let requests = []; const associated = self.association.users; const search = self.search.users; const searchselected = self.selected.search.users; for(const i in searchselected){ const r = searchselected[i]; requests.push({ methodname: 'local_treestudyplan_connect_user', args: {studyplan_id: self.value.id, user_id: r}, fail: notification.exception, done: function(response){ if(response.success){ transportItem(associated,search,r); } } }); } call(requests); }, userDisassociate(){ const self = this; let requests = []; const associated = self.association.users; const associatedselected = self.selected.associated.users; const search = self.search.users; for(const i in associatedselected){ const r = associatedselected[i]; requests.push({ methodname: 'local_treestudyplan_disconnect_user', args: {studyplan_id: self.value.id, user_id: r}, fail: notification.exception, done: function(response){ if(response.success){ transportItem(search,associated,r); } } }); } call(requests); }, } , template: ` {{text.associated_cohorts}} {{text.associate_cohorts}}  {{text.delete_association}}  {{text.add_association}} {{text.associated_users}} {{text.associate_users}}  {{text.delete_association}}  {{text.add_association}} ` }); /* * T-STUDYPLAN */ Vue.component('t-studyplan', { props: ['value', 'index'], data() { return { config: { userfields: [ { key: "selected",}, { key: "firstname", "sortable": true,}, { key: "lastname", "sortable": true,}, ], cohortfields:[ { key: "selected",}, { key: "name", "sortable": true,}, { key: "context", "sortable": true,}, ] }, create: { studyline: { 'name': '', 'shortname': '', 'color': '#DDDDDD', }, }, edit: { studyline: { editmode: false, data: { name: '', shortname: '', color: '#DDDDDD', }, original: {}, }, studyplan: { data: { name: '', shortname: '', description: '', slots : 4, startdate: '2020-08-01', enddate: '', aggregation: '', aggregation_config: '', aggregation_info: { useRequiredGrades: true, useItemCondition: false, }, }, original: {}, } }, text: strings.studyplan_text, }; }, created() { }, mounted() { if(this.value.studylines.length == 0){ // start in editmode if studylines are empty this.edit.studyline.editmode = true; } this.$root.$emit('redrawLines'); }, updated() { console.info("UPDATED Studyplan"); this.$root.$emit('redrawLines'); ItemEventBus.$emit('redrawLines'); }, computed: { columns() { return 1+ (this.value.slots * 2); }, columns_stylerule() { // Uses css variables, so width for slots and filters can be configured in css let s = "grid-template-columns: var(--studyplan-filter-width)"; // use css variable here for(let i=0; i maxLayer){ maxLayer = item.layer; } } for(const ix in line.slots[i].filters){ const item = line.slots[i].filters[ix]; if(item.layer > maxLayer){ maxLayer = item.layer; } } } return maxLayer+1; }, slotsempty(slots) { if(Array.isArray(slots)){ let count = 0; for(let i = 0; i < slots.length; i++) { if(Array.isArray(slots[i].competencies)){ count += slots[i].competencies.length; } if(Array.isArray(slots[i].filters)){ count += slots[i].filters.length; } } return (count == 0); } else { return false; } }, movedStudyplan(plan,from,to) { this.$emit('moved',plan,from,to); // Throw the event up.... }, addStudyLine(studyplan,newlineinfo) { call([{ methodname: 'local_treestudyplan_add_studyline', args: { 'studyplan_id': studyplan.id, 'name': newlineinfo.name, 'shortname': newlineinfo.shortname, 'color': newlineinfo.color, 'sequence': studyplan.studylines.length, } }])[0].done(function(response){ debug.info("New studyline:",response); studyplan.studylines.push(response); newlineinfo.name = ''; newlineinfo.shortname = ''; newlineinfo.color = "#dddddd"; }).fail(notification.exception); }, editLineStart(line) { Object.assign(this.edit.studyline.data,line); this.edit.studyline.original = line; this.$bvModal.show('modal-edit-studyline-'+this.value.id); }, editLineFinish() { let editedline = this.edit.studyline.data; let originalline = this.edit.studyline.original; debug.info('Edit Line',this.edit.studyline); call([{ methodname: 'local_treestudyplan_edit_studyline', args: { 'id': editedline.id, 'name': editedline.name, 'shortname': editedline.shortname, 'color': editedline.color,} }])[0].done(function(response){ debug.info('Edit response:', response); originalline['name'] = response['name']; originalline['shortname'] = response['shortname']; originalline['color'] = response['color']; }).fail(notification.exception); }, deleteLine(studyplan,line) { debug.info('Delete Line',line); const self=this; get_strings([ {key: 'studyline_confirm_remove', param: line.name, component: 'local_treestudyplan' }, {key: 'delete', component: 'core' }, ]).then(function(s){ self.$bvModal.msgBoxConfirm(s[0], { okTitle: s[1], okVariant: 'danger', }).then(function(modalresponse){ if(modalresponse){ call([{ methodname: 'local_treestudyplan_delete_studyline', args: { 'id': line.id, } }])[0].done(function(response){ debug.info('Delete response:', response); if(response.success == true){ let index = studyplan.studylines.indexOf(line); studyplan.studylines.splice(index, 1); } }).fail(notification.exception); } }); }); }, reorderLines(event,lines){ debug.info("Reorder lines",event,lines); // apply reordering event.apply(lines); // send the new sequence to the server let sequence = []; for(let idx in lines) { sequence.push({'id': lines[idx].id,'sequence': idx}); } call([{ methodname: 'local_treestudyplan_reorder_studylines', args: { 'sequence': sequence } }])[0].done(function(response){ debug.info('Reorder response:', response); }).fail(notification.exception); }, deletePlan(studyplan){ const self=this; debug.info('Delete studyplan:', studyplan); get_strings([ {key: 'studyplan_confirm_remove', param: studyplan.name, component: 'local_treestudyplan' }, {key: 'delete', component: 'core' }, ]).then(function(s){ self.$bvModal.msgBoxConfirm(s[0], { okTitle: s[1], okVariant: 'danger', }).then(function(modalresponse){ if(modalresponse){ call([{ methodname: 'local_treestudyplan_delete_studyplan', args: { 'id': studyplan.id, } }])[0].done(function(response){ debug.info('Delete response:', response); if(response.success == true){ self.$root.$emit("studyplanRemoved",studyplan); } }).fail(notification.exception); } }); }); }, deleteStudyItem(event){ debug.info('Delete studyitem:', event); //const self = this; let item = event.data; call([{ methodname: 'local_treestudyplan_delete_studyitem', args: { 'id': item.id, } }])[0].done(function(response){ debug.info('Delete response:', response); if(response.success == true){ event.source.$emit('cut',event); } }).fail(notification.exception); }, } , template: `
{{ text.studyline_editmode }} {{text.edit$core}} {{text.associations}}
{{text.studyline_name}} {{text.studyline_shortname}} {{text.studyline_color}} {{ text.studyline_name}} {{ text.studyline_shortname}} {{ text.studyline_color}}
` }); /* * T-STUDYLINE-HEADER */ Vue.component('t-studyline-heading', { props: { value : { type: Object, default: function(){ return {};}, }, numlines: { type: Number, default: 1, }, layers: { type: Number, default: 1, }, }, data() { return { layerHeights: {} }; }, created() { // Listener for the signal that a new connection was made and needs to be drawn // Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines ItemEventBus.$on('lineHeightChange', this.onLineHeightChange); }, computed: { }, methods: { onLineHeightChange(lineid,layerid,newheight){ // All layers for this line have the first slot send an update message on layer height change. // When one of those updates is received, record the height and recalculate the total height of the // header if(lineid == this.value.id){ const items = document.querySelectorAll( `.t-studyline-slot-0[data-studyline='${this.value.id}']`); // determine the height of all the lines and add them up. let heightSum = 0; items.forEach((el) => { // getBoundingClientRect() Gets the actual fractional height instead of rounded to integer pixels const r = el.getBoundingClientRect(); const height = r.height; heightSum += height; }); const heightStyle=`${heightSum}px`; this.$refs.mainEl.style.height = heightStyle; } } }, template: `
{{ value.shortname }}
`, }); /* * T-STUDYLINE (Used only for study line edit mode) */ Vue.component('t-studyline-edit', { props: { value : { type: Object, default: function(){ return {};}, }, numlines: { type: Number, default: 1, }, /* :color='value.color' :name='value.name' :code='value.shortname' :sequence='value.sequence' */ }, data() { return { }; }, computed: { deletable() { // Check if all the slots are empty const slots = this.value.slots; if(Array.isArray(slots)){ let count = 0; for(let i = 0; i < slots.length; i++) { if(Array.isArray(slots[i].competencies)){ count += slots[i].competencies.length; } if(Array.isArray(slots[i].filters)){ count += slots[i].filters.length; } } return (count == 0); } else { return false; } } }, methods: { onEdit() { this.$emit('edit',this.value); }, onDelete() { this.$emit('delete',this.value); }, }, template: `
{{ value.shortname }}
`, }); /* * During a redisign it was decided to have the studyline still get the entire array as a value, * even though it only shows one drop slot for the layer it is in. This is to make repainting easier, * since we modify the array for the layer we handle */ Vue.component('t-studyline-slot', { props: { type : { type: String, default: 'gradable', }, slotindex : { type: Number, default: '', }, line : { type: Object, default(){ return null;}, }, layer : { type: Number, }, value: { type: Array, // dict with layer as index default(){ return [];}, }, plan: { type: Object, default(){ return null;}, }, }, mounted() { const self=this; if(self.type == "gradable" && self.slotindex == 1){ self.resizeListener = new ResizeObserver(() => { if(self.$refs.sizeElement){ const height = self.$refs.sizeElement.getBoundingClientRect().height; ItemEventBus.$emit('lineHeightChange', self.line.id, self.layer, height); } }).observe(self.$refs.sizeElement); } }, unmounted() { if(this.resizeListener) { this.resizeListener.disconnect(); } }, computed: { item(){ for(const itm of this.value){ if(itm.layer == this.layer){ return itm; } } return null; }, listtype() { return this.type; }, dragacceptlist(){ if(this.type == "gradable"){ return ["course", "gradable-item"]; } else { return ["filter", "filter-item"]; } }, courseHoverDummy(){ return {course: this.hover.component}; } }, data() { return { resizeListener: null, hover: { component:null, type: null, } }; }, methods: { dragacceptitem(){ if(this.type == "gradable"){ return ["gradable-item"]; } else { return ["filter-item"]; } }, dragacceptcomponent(){ if(this.type == "gradable"){ return ["course",]; } else { return ["filter",]; } }, onDrop(event) { this.hover.component = null; this.hover.type = null; const self = this; if(self.dragacceptitem().includes(event.type)) { let item = event.data; // Perform layer update - set this slot and layer here self.relocateStudyItem(item).done(()=>{ item.layer = this.layer; item.slot = this.slotindex; self.value.push(item); self.$emit("input",self.value); }); } else if(self.dragacceptcomponent().includes(event.type) ){ if(event.type == "course"){ call([{ methodname: 'local_treestudyplan_add_studyitem', args: { "line_id": self.line.id, "slot" : self.slotindex, "layer" : self.layer, "type": 'course', "details": { "competency_id": null, 'conditions':'', 'course_id':event.data.id, 'badge_id':null, 'continuation_id':null, } } }])[0].done((response) => { console.info('Add item response:', response); let item = response; self.relocateStudyItem(item).done(()=>{ self.value.push(item); self.$emit("input",self.value); }); }).fail(notification.exception); } else if(event.type == "filter") { call([{ methodname: 'local_treestudyplan_add_studyitem', args: { "line_id": self.line.id, "slot" : self.slotindex, "type": event.data.type, "details":{ "badge_id": event.data.badge?event.data.badge.id:undefined, } } }])[0].done((response) => { console.info('Add item response:', response); let item = response; self.relocateStudyItem(item).done(()=>{ self.value.push(item); self.$emit("input",self.value); }); }).fail(notification.exception); } } }, onCut(event) { const self=this; let id = event.data.id; for(let i = 0; i < self.value.length; i++){ if(self.value[i].id == id){ self.value.splice(i, 1); i--; break; // just remove one } } // Do something to signal that this item has been removed this.$emit("input",this.value); }, relocateStudyItem(item){ const iteminfo = {'id': item.id, 'layer': this.layer, 'slot': this.slotindex, 'line_id': this.line.id}; return call([{ methodname: 'local_treestudyplan_reorder_studyitems', args: { 'items': [iteminfo] } // function was designed to relocate multiple items at once, hence the array }])[0].fail(notification.exception); }, onDragEnter(event){ this.hover.component = event.data; this.hover.type = event.type; }, onDragLeave(){ this.hover.component = null; this.hover.type = null; }, }, template: `
`, }); Vue.component('t-item', { props: { 'value' :{ type: Object, default(){ return null;}, }, 'dummy' :{ type: Boolean, default() { return false;}, }, 'plan': { type: Object, default() { return null;}, }, }, data() { return { dragLine: null, dragEventListener: null, deleteMode: false, condition_options: string_keys.conditions, text: strings.item_text, showContext: false, lines: [], }; }, methods: { dragStart(event){ // Add line between start point and drag image this.deleteMode = false; let start = document.getElementById('studyitem-'+this.value.id); let dragelement= document.getElementById('t-item-cdrag-'+this.value.id); dragelement.style.position = 'fixed'; dragelement.style.left = event.position.x+'px'; dragelement.style.top = event.position.y+'px'; this.dragLine = new SimpleLine(start,dragelement,{ color: "#777" }); // Add separate event listener to reposition mouse move document.addEventListener("mousemove",this.onMouseMove); }, dragEnd(){ if(this.dragLine !== null) { this.dragLine.remove(); } let dragelement = document.getElementById('t-item-cdrag-'+this.value.id); dragelement.style.removeProperty('left'); dragelement.style.removeProperty('top'); dragelement.style.removeProperty('position'); document.removeEventListener("mousemove",this.onMouseMove); }, onMouseMove: function(event){ let dragelement = document.getElementById('t-item-cdrag-'+this.value.id); dragelement.style.position = 'fixed'; dragelement.style.left = event.clientX+'px'; dragelement.style.top = event.clientY+'px'; // line will follow automatically }, onDrop(event){ let from_id = event.data.id; let to_id = this.value.id; this.redrawLines(); call([{ methodname: 'local_treestudyplan_connect_studyitems', args: { 'from_id': from_id, 'to_id': to_id } }])[0].done((result)=>{ console.info("Drop result",result); let conn = {'id': result.id, 'from_id': result.from_id, 'to_id': result.to_id}; ItemEventBus.$emit("createdConnection",conn); this.value.connections.in.push(conn); }).fail(notification.exception); }, redrawLine(conn){ const lineColor = "var(--success)"; const start = document.getElementById(`studyitem-${conn.from_id}`); const end = document.getElementById(`studyitem-${conn.to_id}`); // delete old line if(this.lines[conn.to_id]){ this.lines[conn.to_id].remove(); delete this.lines[conn.to_id]; } // create a new line if the start and finish items are visible if(start !== null && end !== null && isVisible(start) && isVisible(end)){ this.lines[conn.to_id] = new SimpleLine( start,end,{color: lineColor,} ); } }, deleteLine(conn){ const self = this; // console.info("Delete Line",conn); call([{ methodname: 'local_treestudyplan_disconnect_studyitems', args: { 'from_id': conn.from_id, 'to_id': conn.to_id } }])[0].done((result)=>{ if(result.success){ this.removeLine(conn); // send disconnect event on message bus, so the connection on the other end can delete it too ItemEventBus.$emit("connectionDisconnected",conn); // Remove connection from our outgoing list let index = self.value.connections.out.indexOf(conn); self.value.connections.out.splice(index, 1); } }).fail(notification.exception); }, highlight(conn){ if(this.lines[conn.to_id]){ this.lines[conn.to_id].setConfig({color:"var(--danger)",}); } }, normalize(conn){ if(this.lines[conn.to_id]){ this.lines[conn.to_id].setConfig({color:"var(--success)",}); } }, updateItem() { call([{ methodname: 'local_treestudyplan_edit_studyitem', args: { 'id': this.value.id, 'conditions': this.value.conditions, 'continuation_id': this.value.continuation_id,} }])[0].fail(notification.exception); }, doShowContext(event) { if(this.hasContext){ this.showContext=true; event.preventDefault(); } }, redrawLines(){ for(let i in this.value.connections.out){ let conn = this.value.connections.out[i]; // console.info('Connection out', conn); this.redrawLine(conn); } }, // EVENT LISTENERS onCreatedConnection(conn){ if(conn.from_id == this.value.id){ // console.info("incomingConnection",conn); this.value.connections.out.push(conn); this.redrawLine(conn); } }, // Listener for the signal that a connection was removed by the outgoing item onRemovedConnection(conn){ for(let i in this.value.connections.in){ let c_in = this.value.connections.in[i]; if(conn.id == c_in.id){ // console.info("Deleting incoming connection",conn); self.value.connections.out.splice(i, 1); } } }, // Listener for reposition events // When an item in the list is repositioned, all lines need to be redrawn onRePositioned(){ for(let i in this.value.connections.out){ let conn = this.value.connections.out[i]; this.redrawLine(conn); } }, // When an item is disPositioned - (temporarily) removed from the list, // all connections need to be deleted. onDisPositioned(re_id){ for(let i in this.value.connections.out){ let conn = this.value.connections.out[i]; if(conn.to_id == re_id){ this.removeLine(conn); } } }, // When an item is deleted // all connections to/from that item need to be cleaned up onItemDeleted(item_id){ const self = this; for(const i in this.value.connections.out){ let conn = this.value.connections.out[i]; if(conn.to_id == item_id){ self.removeLine(conn); self.value.connections.out.splice(i, 1); } } for(const i in this.value.connections.in){ let conn = this.value.connections.in[i]; if(conn.from_id == item_id){ self.value.connections.out.splice(i, 1); } } }, onRedrawLines(){ this.redrawLines(); }, removeLine(conn){ if(this.lines[conn.to_id]){ this.lines[conn.to_id].remove(); delete this.lines[conn.to_id]; } }, }, computed: { hasConnectionsOut() { return !(["finish",].includes(this.value.type)); }, hasConnectionsIn() { return !(["start",].includes(this.value.type)); }, hasContext() { return ['junction','finish'].includes(this.value.type); } }, created(){ // Add event listeners on the message bus // But only if not in "dummy" mode - mode which is used for droplist placeholders // Since an item is "fully made" with all references, not specifying dummy mode really messes things up if(!this.dummy){ // Listener for the signal that a new connection was made and needs to be drawn // Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines ItemEventBus.$on('createdConnection', this.onCreatedConnection); // Listener for the signal that a connection was removed by the outgoing item ItemEventBus.$on('removedConnection', this.onRemovedConnection); // Listener for reposition events // When an item in the list is repositioned, all lines need to be redrawn ItemEventBus.$on('rePositioned', this.onRePositioned); // When an item is disPositioned - (temporarily) removed from the list, // all connections need to be deleted. ItemEventBus.$on('disPositioned', this.onDisPositioned); // When an item is deleted // all connections to/from that item need to be cleaned up ItemEventBus.$on('itemDeleted', this.onItemDeleted); ItemEventBus.$on('redrawLines', this.onRedrawLines); } }, mounted(){ // Initialize connection lines when mounting // But only if not in "dummy" mode - mode which is used for droplist placeholders // Since an item is "fully made" with all references, not specifying dummy mode really messes things up if(!this.dummy) { // console.info('Mounted', this); this.redrawLines(); setTimeout(()=>{ ItemEventBus.$emit("rePositioned",this.value.id); },10); } }, beforeDestroy(){ if(!this.dummy) { for(let i in this.value.connections.out){ let conn = this.value.connections.out[i]; this.removeLine(conn); } ItemEventBus.$emit("disPositioned",this.value.id); // Remove event listeners ItemEventBus.$off('createdConnection', this.onCreatedConnection); ItemEventBus.$off('removedConnection', this.onRemovedConnection); ItemEventBus.$off('rePositioned', this.onRePositioned); ItemEventBus.$off('disPositioned', this.onDisPositioned); ItemEventBus.$off('itemDeleted', this.onItemDeleted); ItemEventBus.$off('redrawLines', this.onRedrawLines); } }, beforeUpdate(){ }, updated(){ if(!this.dummy) { this.redrawLines(); } }, template: `
`, }); Vue.component('t-item-invalid', { props: { 'value' :{ type: Object, default: function(){ return null;}, }, }, data() { return { text: strings.invalid, }; }, methods: { }, template: ` {{text.error}} `, }); Vue.component('t-item-competency', { props: { 'value' :{ type: Object, default: function(){ return null;}, }, }, data() { return { dragLine: null, }; }, methods: { }, template: ` {{ value.competency.shortname }}
  • {{g.shortname}}
`, }); Vue.component('t-item-course', { props: { 'value' :{ type: Object, default(){ return null;}, }, 'plan' :{ type: Object, default(){ return null;}, }, }, data() { return { condition_options: string_keys.conditions, text: strings.item_course_text, }; }, computed: { useItemConditions() { if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useItemConditions !== undefined){ return this.plan.aggregation_info.useItemConditions; } else { return false; } }, configurationState(){ if(this.hasGrades() || this.hasCompletions()) { return "t-configured-ok"; } else { return "t-configured-alert"; } }, configurationIcon(){ if(this.hasGrades() || this.hasCompletions()) { return "check"; } else { return "exclamation-circle"; } } }, methods: { hasGrades() { if(this.value.course.grades && this.value.course.grades.length > 0){ for(const g of this.value.course.grades){ if(g.selected){ return true; } } } return false; }, hasCompletions() { if(this.value.course.completion && this.value.course.completion.conditions) { for(const cgroup of this.value.course.completion.conditions){ if(cgroup.items && cgroup.items.length > 0){ return true; } } } return false; }, includeChanged(newValue,g){ call([{ methodname: 'local_treestudyplan_include_grade', args: { 'grade_id': g.id, 'item_id': this.value.id, 'include': newValue, 'required': g.required, } }])[0].fail(notification.exception); }, requiredChanged(newValue,g){ call([{ methodname: 'local_treestudyplan_include_grade', args: { 'grade_id': g.id, 'item_id': this.value.id, 'include': g.selected, 'required': newValue, } }])[0].fail(notification.exception); }, updateConditions() { call([{ methodname: 'local_treestudyplan_edit_studyitem', args: { 'id': this.value.id, 'conditions': this.value.conditions, } }])[0].fail(notification.exception); }, }, created() { }, template: ` {{ value.course.displayname }} `, }); Vue.component('t-item-course-grades', { props: { 'value' :{ type: Object, default(){ return null;}, }, 'plan' :{ type: Object, default(){ return null;}, }, }, data() { return { condition_options: string_keys.conditions, text: strings.item_course_text, }; }, computed: { useRequiredGrades() { if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){ return this.plan.aggregation_info.useRequiredGrades; } else { return false; } }, selectedgrades(){ let list = []; for(let ix in this.value.course.grades){ let g = this.value.course.grades[ix]; if(g.selected){ list.push(g); } } return list; }, }, methods: { includeChanged(newValue,g){ call([{ methodname: 'local_treestudyplan_include_grade', args: { 'grade_id': g.id, 'item_id': this.value.id, 'include': newValue, 'required': g.required, } }])[0].fail(notification.exception); }, requiredChanged(newValue,g){ call([{ methodname: 'local_treestudyplan_include_grade', args: { 'grade_id': g.id, 'item_id': this.value.id, 'include': g.selected, 'required': newValue, } }])[0].fail(notification.exception); }, }, created() { }, template: `
  • {{text.grade_include}}{{text.grade_require}}
  • {{g.name}}
`, }); Vue.component('t-item-course-completion',{ props: { value : { type: Object, default: function(){ return {};}, }, guestmode: { type: Boolean, default: false, }, course: { type: Object, default: function(){ return {};}, }, }, data() { return { text: strings.completion, }; }, created(){ const self = this; // Get text strings for condition settings let stringkeys = []; for(const key in this.text){ stringkeys.push({ key: key, component: 'local_treestudyplan'}); } get_strings(stringkeys).then(function(strings){ let i = 0; for(const key in self.text){ self.text[key] = strings[i]; i++; } }); }, computed: { hasCompletions() { if(this.value.conditions) { for(const cgroup of this.value.conditions){ if(cgroup.items && cgroup.items.length > 0){ return true; } } } return false; }, }, methods: { completion_icon(completion) { switch(completion){ case "progress": return "exclamation-circle"; case "complete": return "check-circle"; case "complete-pass": return "check-circle"; case "complete-fail": return "times-circle"; default: // case "incomplete" return "circle-o"; } }, completion_tag(cgroup){ return cgroup.completion?'completed':'incomplete'; } }, template: `
{{ text.aggregation_overall_all}}{{ text.aggregation_overall_any}}
{{text.completion_not_configured}}!
{{text.configure_completion}}
`, }); /************************************ * * * Competency map Vue components * * * ************************************/ Vue.component('t-competency-heading', { props: { value : { type: Object, }, }, data() { return { }; }, computed: { inuse() { return (this.value.inuse !== undefined && !!this.value.inuse); } }, methods: { onCut(){ // console.info('cutevent-competency',event); this.value.inuse=true; this.$emit('input',this.value); }, }, template: ` {{ value.shortname }} {{ value.shortname }} {{ value.shortname }} `, }); Vue.component('t-competency-display', { props: { value : { type: Object, default: function(){ return {};}, }, }, data() { return { dragLine: null, }; }, methods: { }, computed: { haschildren() { return this.value.children && this.value.children.length > 0; }, }, template: `
  • `, }); Vue.component('t-competency-list', { props: { value : { type: Array, default: function(){ return [];}, }, }, data() { return { }; }, methods: { }, template: ` `, }); Vue.component('t-item-junction',{ props: { value : { type: Object, default: function(){ return {};}, }, }, data() { return { condition_options: string_keys.conditions, }; }, methods: { }, template: `
    `, }); Vue.component('t-item-finish',{ props: { value : { type: Object, default: function(){ return {};}, }, }, data() { return { }; }, methods: { }, template: `
    `, }); Vue.component('t-item-start',{ props: { value : { type: Object, default: function(){ return {};}, }, }, data() { return { }; }, created(){ }, methods: { }, template: `
    `, }); Vue.component('t-item-badge',{ props: { value : { type: Object, default: function(){ return { badge: {}};}, }, }, data() { return { txt: strings, }; }, methods: { }, template: `
    {{value.badge.name}}

    {{value.badge.description}}

    {{ txt.badge.badgeinfo }}

    `, }); Vue.component('t-coursecat-list',{ props: { value : { type: Array, default: function(){ return {};}, }, }, data() { return { }; }, methods: { }, template: ` `, }); Vue.component('t-coursecat-list-item',{ props: { value : { type: Object, default: function(){ return {};}, }, }, data() { return { loading: false, }; }, computed: { showSpinner() { return this.canLoadMore(); }, hasDetails() { return (this.value.haschildren || this.value.hascourses); } }, methods: { canLoadMore() { return (this.value.haschildren && (!this.value.children || this.value.children.length == 0)) || (this.value.hascourses && (!this.value.courses || this.value.courses.length == 0)); }, onShowDetails(){ const self = this; if(this.canLoadMore()) { call([{ methodname: 'local_treestudyplan_get_category', args: { "id": this.value.id} }])[0].done(function(response){ debug.info("Course info:",response); self.$emit('input', response); }).fail(notification.exception); } } }, template: `
  • {{ value.category.name }} {{ value.category.name }}
  • `, }); Vue.component('t-course-list',{ props: { value : { type: Array, default: function(){ return {};}, }, }, data() { return { }; }, methods: { }, template: ` `, }); }, };