diff options
| author | Thomas Hackl <hackl@data-quest.de> | 2025-01-27 15:52:02 +0000 |
|---|---|---|
| committer | Thomas Hackl <hackl@data-quest.de> | 2025-01-27 15:52:02 +0000 |
| commit | 9b2411a078a279cb4e24c0cc2ca78c032ad55e94 (patch) | |
| tree | 58a7cb651133c20280d1d7253276feb8a92e2e01 /resources | |
| parent | e4bf27a6e37efa3d7fd5decabacfd2dc94823aa6 (diff) | |
Resolve "Fehler bei der Anmeldeset-Verwaltung"
Closes #5086
Merge request studip/studip!3823
Diffstat (limited to 'resources')
16 files changed, 197 insertions, 264 deletions
diff --git a/resources/assets/javascripts/bootstrap/admission.js b/resources/assets/javascripts/bootstrap/admission.js index c04ff24..2fff6cf 100644 --- a/resources/assets/javascripts/bootstrap/admission.js +++ b/resources/assets/javascripts/bootstrap/admission.js @@ -22,32 +22,15 @@ STUDIP.ready(function () { components[ruleType] = result.default; STUDIP.Vue.load().then(({ createApp }) => { - createApp({ - el: container, - components: components - }); + createApp({components}).mount(container); }); }); } }); - $(document).on('change', 'tr.course input', function() { - STUDIP.Admission.toggleNotSavedAlert(); - }); - $('a.userlist-delete-user').on('click', function() { $(this).closest('tr').remove(); return false; }); - - $('#courseset-form .autosave').on('click', () => { - STUDIP.Admission.autosaveCourseset(); - }); - - STUDIP.ready(() => { - $('#toggle-date-link').on('click', () => { - $('#admissionrule-valid-date').toggleClass('hidden-js'); - }); - }); }); diff --git a/resources/assets/javascripts/entry-admission.js b/resources/assets/javascripts/entry-admission.js deleted file mode 100644 index 5454d08..0000000 --- a/resources/assets/javascripts/entry-admission.js +++ /dev/null @@ -1 +0,0 @@ -import "./bootstrap/admission.js" diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 2210471..db3d1f6 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -78,6 +78,7 @@ import "./bootstrap/oer.js" import "./bootstrap/courseware.js" import "./bootstrap/external_pages.js" import "./bootstrap/vips.js" +import "./bootstrap/admission.js" import "./mvv_course_wizard.js" import "./mvv.js" diff --git a/resources/assets/javascripts/lib/admission.js b/resources/assets/javascripts/lib/admission.js index 8c0413a..c735cb2 100644 --- a/resources/assets/javascripts/lib/admission.js +++ b/resources/assets/javascripts/lib/admission.js @@ -2,7 +2,6 @@ * Anmeldeverfahren und -sets * ------------------------------------------------------------------------ */ import { $gettext } from './gettext'; -import Dialog from './dialog.js'; const Admission = { @@ -46,143 +45,6 @@ const Admission = { return false; }, - configureRule: function(ruleType, targetUrl, ruleId) { - var urlparts = targetUrl.split('?'); - targetUrl = urlparts[0] + '/' + ruleType; - if (urlparts[1]) { - targetUrl += '?' + urlparts[1]; - } - - Dialog.fromURL(targetUrl, { - method: 'post', - size: 'auto', - title: $gettext('Anmelderegel konfigurieren'), - id: 'configurerule', - data: { ruleId: ruleId, rules: _.map($('#rules input[name="rules[]"]'), 'value') } - }); - - return false; - }, - - selectRuleType: function(source) { - Dialog.fromURL(source, { - title: $gettext('Anmelderegel konfigurieren'), - size: 'auto', - data: { rules: _.map($('#rules input[name="rules[]"]'), 'value') }, - method: 'post', - id: 'configurerule' - }); - return false; - }, - - saveRule: function(ruleId, targetId, targetUrl) { - if ($('#action').val() !== 'cancel') { - $.ajax({ - type: 'post', - url: targetUrl, - data: $('#ruleform').serialize(), - dataType: 'html', - success: function(data) { - if (data !== '') { - var result = ''; - if ($('#norules').length > 0) { - $('#norules').remove(); - $('#' + targetId).prepend('<div id="rulelist"></div>'); - } - result += data; - if ($('#rule_' + ruleId).length !== 0) { - $('#rule_' + ruleId).replaceWith(result); - } else { - $('#rulelist').append(result); - } - } - }, - error: function(jqXHR, textStatus, errorThrown) { - alert('Status: ' + textStatus + '\nError: ' + errorThrown); - } - }); - } - Admission.closeDialog('configurerule'); - Admission.toggleNotSavedAlert(); - return false; - }, - - removeRule: function(targetId, containerId) { - var parent = $('#' + targetId).parent(); - $('#' + targetId).remove(); - if (parent.children('div').length === 0) { - parent.remove(); - var norules = $gettext('Sie haben noch keine Anmelderegeln festgelegt.'); - $('#' + containerId).prepend('<span id="norules">' + '<i>' + norules + '</i></span>'); - } - Admission.toggleNotSavedAlert(); - }, - - toggleRuleDescription: function(targetId) { - $('#' + targetId).toggle(); - return false; - }, - - toggleDetails: function(arrowId, detailId) { - var oldSrc = $('#' + arrowId).attr('src'); - var newSrc = $('#' + arrowId).attr('rel'); - $('#' + arrowId).attr('src', newSrc); - $('#' + arrowId).attr('rel', oldSrc); - $('#' + detailId).slideToggle(); - return false; - }, - - /** - * - * @param String ruleId The rule to save. - * @param String errorTarget Target element ID where error messages will be - * shown. - * @param String validateUrl URL to call for validation. - * @param String savedTarget Target element ID where the saved rule will be - * displayed. - * @param String saveUrl URL to save the rule. - */ - checkAndSaveRule: function(ruleId, errorTarget, validateUrl, savedTarget, saveUrl) { - if (Admission.validateRuleConfig(errorTarget, validateUrl)) { - Admission.saveRule(ruleId, savedTarget, saveUrl); - Dialog.close({ id: 'configurerule' }); - } - return false; - }, - - validateRuleConfig: function(containerId, targetUrl) { - var valid = true; - var error = $.ajax({ - type: 'post', - async: false, - url: targetUrl, - data: $('#ruleform').serialize(), - dataType: 'html', - - error: function(jqXHR, textStatus, errorThrown) { - alert('Status: ' + textStatus + '\nError: ' + errorThrown); - } - }).responseText; - error = error.replace(/(\r\n|\n|\r)/gm, ''); - if ($.trim(error) != '') { - $('#' + containerId).html(error); - valid = false; - } - return valid; - }, - - removeUserFromUserlist: function(userId) { - var parent = $('#user_' + userId).parent(); - $('#user_' + userId).remove(); - if (parent.children('li').length === 0) { - var nousers = $gettext('Sie haben noch niemanden hinzugefügt.'); - $(parent) - .parent() - .append('<span id="nousers">' + '<i>' + nousers + '</i></span>'); - } - return false; - }, - updateInstitutes: function(elementId, instURL, courseURL, mode) { if (elementId !== '') { var query = ''; @@ -208,26 +70,6 @@ const Admission = { } }, - checkRuleActivation: function(target) { - var form = $('#' + target); - var globalActivation = form.find('input[name=enabled]'); - if (globalActivation.prop('checked')) { - $('#activation').show(); - if (form.find('input[name=activated]:checked').val() === 'studip') { - $('#institutes_activation').hide(); - } else { - $('#institutes_activation').show(); - } - } else { - $('#activation').hide(); - $('#institutes_activation').hide(); - } - }, - - closeDialog: function(elementId) { - $('#' + elementId).remove(); - }, - checkUncheckAll: function(inputName, mode) { switch (mode) { case 'check': @@ -251,20 +93,6 @@ const Admission = { toggleNotSavedAlert: function() { $('.hidden-alert').show(); - }, - - autosaveCourseset: function() { - $.post({ - url: $('#courseset-form').attr('action'), - data: $('#courseset-form').serialize() + '&submit=1', - dataType: 'html', - success: function() { - $('.hidden-alert').hide(); - }, - error: function(jqXHR, textStatus, errorThrown) { - alert('Status: ' + textStatus + '\nError: ' + errorThrown); - } - }); } }; diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js index d6483f3..f563b31 100644 --- a/resources/assets/javascripts/studip-ui.js +++ b/resources/assets/javascripts/studip-ui.js @@ -11,7 +11,9 @@ import RestrictedDatesHelper from './lib/RestrictedDatesHelper'; $.widget( "ui.dialog", $.ui.dialog, { _allowInteraction: function( event ) { - return hasParentWhich(isCKBodyWrapper)(event.target) || this._super( event ); + return hasParentWhich(isCKBodyWrapper)(event.target) + || event.target.closest('.studip-dialog') !== null + || this._super( event ); }, }); diff --git a/resources/assets/stylesheets/scss/admission.scss b/resources/assets/stylesheets/scss/admission.scss index d9d978c..50fce65 100644 --- a/resources/assets/stylesheets/scss/admission.scss +++ b/resources/assets/stylesheets/scss/admission.scss @@ -1,17 +1,3 @@ -#rulelist div.admissionrule { - display: list-item; - list-style-type: disc; - margin-left: 25px; -} - -#toggle-date-container { - margin-top: 10px; - - img, svg { - vertical-align: text-bottom; - } -} - .hover_box { div { display: inline; @@ -88,23 +74,6 @@ form.default { } } -#userlists { - div { - margin-bottom: 10px; - - a { - &.userlist-action { - margin-left: 2px; - margin-right: 2px; - } - - img { - vertical-align: bottom; - } - } - } -} - form { fieldset { section { diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue index 1a61513..9177ae5 100644 --- a/resources/vue/components/Quicksearch.vue +++ b/resources/vue/components/Quicksearch.vue @@ -32,7 +32,7 @@ <script> export default { name: 'quicksearch', - emits: ['update:modelValue'], + emits: ['update:modelValue', 'input'], props: { searchtype: { type: String, @@ -125,6 +125,7 @@ export default { this.results = []; this.$emit('update:modelValue', this.returnValue, this.inputValue); + this.$emit('input', this.returnValue, this.inputValue); if (!this.keepValue) { this.inputValue = ''; diff --git a/resources/vue/components/StudipDialog.vue b/resources/vue/components/StudipDialog.vue index ab42f48..94f95ed 100644 --- a/resources/vue/components/StudipDialog.vue +++ b/resources/vue/components/StudipDialog.vue @@ -1,7 +1,7 @@ <template> <Teleport to="body"> <focus-trap v-model="trap"> - <div class="studip-dialog" @keydown.esc="closeDialog"> + <div class="studip-dialog" @keydown.esc="closeDialog" :style="{zIndex: zIndex}"> <transition name="dialog-fade"> <div class="studip-dialog-backdrop" v-if="true"> <vue-resizeable @@ -170,6 +170,8 @@ export default { handlers: ["r", "rb", "b", "lb", "l", "lt", "t", "rt"], fit: false, footerHeight: 68, + + zIndex: null, }; }, computed: { @@ -279,6 +281,16 @@ export default { this.$refs.buttonB.focus(); }); } + }, + created() { + const maxZIndex = Array.from(document.querySelectorAll('.studip-dialog')).reduce( + (acc, el) => { + const style = getComputedStyle(el); + return Math.max(acc, Number.parseInt(style.zIndex, 10)); + }, + 1 + ); + this.zIndex = maxZIndex + 1; } }; </script> diff --git a/resources/vue/components/admission/AdmissionRuleConfig.vue b/resources/vue/components/admission/AdmissionRuleConfig.vue index 921a6c2..d6d6e65 100644 --- a/resources/vue/components/admission/AdmissionRuleConfig.vue +++ b/resources/vue/components/admission/AdmissionRuleConfig.vue @@ -1,5 +1,5 @@ <template> - <studip-dialog v-if="component !== null" + <studip-dialog v-if="useDialog && component !== null" :title="$gettext('Anmelderegel bearbeiten')" :close-text="$gettext('Abbrechen')" @close="cancel" @@ -25,6 +25,18 @@ </button> </template> </studip-dialog> + <div v-if="!useDialog && component !== null"> + <studip-message-box v-if="invalidData?.length" + type="error" + :details="invalidData" + :hide-close="true" + :hide-details="false" + :aria-description="errorText" + role="alert"> + {{ $gettext('Es sind ungültige Daten angegeben worden:') }} + </studip-message-box> + <component :is="component" v-bind="props" @submit="submit" @error="error"></component> + </div> </template> <script> @@ -32,7 +44,7 @@ import {shallowRef} from "vue"; export default { name: 'AdmissionRuleConfig', - emits: ['cancel', 'submit'], + emits: ['cancel', 'submit', 'error'], props: { type: { type: String, @@ -45,6 +57,10 @@ export default { assignedRuleTypes: { type: Array, default: () => [] + }, + useDialog: { + type: Boolean, + default: true } }, data() { diff --git a/resources/vue/components/admission/ConfigureCourseSet.vue b/resources/vue/components/admission/ConfigureCourseSet.vue index 226de37..8bd46f3 100644 --- a/resources/vue/components/admission/ConfigureCourseSet.vue +++ b/resources/vue/components/admission/ConfigureCourseSet.vue @@ -327,7 +327,7 @@ </button> <button class="button cancel" type="button" - data-dialog="close" + data-dialog-close @click.prevent="cancel" > {{ $gettext('Abbrechen') }} @@ -382,6 +382,10 @@ export default { myUserLists: { type: Array, default: () => [] + }, + instantCourseSetView: { + type: Boolean, + default: false } }, data() { @@ -413,7 +417,7 @@ export default { computed: { isStorable() { return this.name !== '' - && this.institutes.length > 0 + && (this.courseSetId !== '' || this.courseSetId === '' && this.institutes.length > 0) && this.rules.length > 0; }, hasConfigurableCourses() { @@ -498,7 +502,7 @@ export default { this.showRuleConfig = true; }, addRuleConfiguration(data) { - if (!this.ruleId) { + if (!this.ruleId || this.ruleId === data.type + '_') { STUDIP.jsonapi.withPromises().post( 'admission-rules/' + data.type, { @@ -566,7 +570,7 @@ export default { } }, addInstitute(returnValue, inputValue) { - if (!this.institutes.some(i => i.id === returnValue)) { + if (inputValue && !this.institutes.some(i => i.id === returnValue)) { this.institutes.push({ id: returnValue, name: inputValue }); } }, @@ -594,7 +598,11 @@ export default { { data: data } ).then(() => { this.$refs.courseSetForm.dataset.secure = 'false'; - window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + if (!this.instantCourseSetView) { + window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + } else { + window.location.reload(); + } }); } else { @@ -604,28 +612,31 @@ export default { { data: data} ).then(() => { this.$refs.courseSetForm.dataset.secure = 'false'; - window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + if (!this.instantCourseSetView) { + window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + } else { + window.location.reload(); + } }); } }, cancel() { - window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + if (!this.instantCourseSetView) { + window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + } }, - configureCourses() - { + configureCourses() { STUDIP.Dialog.fromURL( STUDIP.URLHelper.getURL('dispatch.php/admission/courseset/configure_courses/' + this.courseSetId) ); }, - getApplicants() - { + getApplicants() { STUDIP.Dialog.fromURL( STUDIP.URLHelper.getURL('dispatch.php/admission/courseset/applications_list/' + this.courseSetId) ); }, - messageApplicants() - { + messageApplicants() { STUDIP.Dialog.fromURL( STUDIP.URLHelper.getURL('dispatch.php/admission/courseset/applicants_message/' + this.courseSetId) ); diff --git a/resources/vue/components/admission/CourseMemberAdmission.vue b/resources/vue/components/admission/CourseMemberAdmission.vue index 9e689c5..3684eb3 100644 --- a/resources/vue/components/admission/CourseMemberAdmission.vue +++ b/resources/vue/components/admission/CourseMemberAdmission.vue @@ -24,8 +24,7 @@ <quicksearch v-if="courseSearch !== null" :searchtype="courseSearch" name="course" - :key="NaN" - @input="addCourse" + @update:model-value="addCourse" id="csearch" ref="courseSearch"></quicksearch> <ul v-if="courseList.length > 0"> @@ -89,7 +88,7 @@ export default { return this.invalidData.length === 0; }, }, - mounted() { + created() { // Get a new rule instance so we can use quicksearch. if (!this.id || this.id === '') { STUDIP.jsonapi.withPromises().post('admission-rules/CourseMemberAdmission', { @@ -97,7 +96,7 @@ export default { data: { attributes: { payload: { - mode: 0, + modus: 0, courses: [], message: '' } diff --git a/resources/vue/components/admission/InstantCourseSet.vue b/resources/vue/components/admission/InstantCourseSet.vue new file mode 100644 index 0000000..ec555c6 --- /dev/null +++ b/resources/vue/components/admission/InstantCourseSet.vue @@ -0,0 +1,102 @@ +<template> + <form v-if="!working" class="default"> + <section v-for="(type, index) in ruleTypes" :key="index"> + <admission-rule-config :type="type" + :use-dialog="false" + @submit="ruleData"></admission-rule-config> + </section> + <section> + <label class="caption"> + {{ $gettext("Name für diese Anmelderegel") }} + <input type="text" name="instant_course_set_name" size="70" v-model="name"> + </label> + </section> + <footer data-dialog-button> + <button class="button accept" @click.prevent="triggerRules"> + {{ $gettext('Speichern') }} + </button> + <button class="button cancel" data-dialog-close> + {{ $gettext('Abbrechen') }} + </button> + </footer> + </form> +</template> + +<script> +import AdmissionRuleConfig from './AdmissionRuleConfig'; + +export default { + name: 'InstantCourseSet', + components: { AdmissionRuleConfig }, + props: { + ruleTypes: { + type: Array, + required: true + }, + courseSetName: { + type: String, + required: true + }, + courseId: { + type: String, + required: true + } + }, + data() { + return { + name: this.courseSetName, + rules: [], + working: false + } + }, + methods: { + ruleData(data) { + this.working = true; + this.rules.push(data); + + // Check if all rultTypes have some data. If yes, the whole courseset can be stored. + let canStore = true; + for (let i = 0 ; i < this.ruleTypes.length ; i++) { + if (this.rules.filter(rule => rule.type === this.ruleTypes[i]).length === 0) { + canStore = false; + } + } + + if (canStore) { + this.store(); + } + }, + triggerRules() { + STUDIP.eventBus.emit('getRuleConfiguration'); + }, + store() { + const data = { + data: { + attributes: { + name: this.name, + private: true, + infotext: '', + institutes: [], + courses: [ this.courseId ], + rules: this.rules.map((rule) => { return { attributes: rule } } ), + userlists: [] + } + } + }; + + STUDIP.jsonapi.withPromises().post( + 'course-sets', + { data: data } + ).then(() => { + STUDIP.Report.success(this.$gettext('Die Zugangsberechtigungen wurden gespeichert.')); + window.location = STUDIP.URLHelper.getURL('dispatch.php/course/admission', {cid: this.courseId}); + }); + } + }, + created() { + for (let i = 0 ; i < this.ruleTypes.length ; i++) { + this.rules[this.ruleTypes[i]] = {}; + } + } +} +</script> diff --git a/resources/vue/components/admission/ParticipantRestrictedAdmission.vue b/resources/vue/components/admission/ParticipantRestrictedAdmission.vue index 9f75b9a..96b6cfb 100644 --- a/resources/vue/components/admission/ParticipantRestrictedAdmission.vue +++ b/resources/vue/components/admission/ParticipantRestrictedAdmission.vue @@ -1,4 +1,4 @@ -<template> + <template> <form class="default"> <section> <label> @@ -16,7 +16,7 @@ <section v-if="!fcfsAllowed || !fcfsEnabled"> <label> {{ $gettext('Zeitpunkt der automatischen Platzverteilung') }} - <datetimepicker v-if="loaded" :value="distributionTime" v-model="distributionTime"></datetimepicker> + <datetimepicker v-model="distributionTime"></datetimepicker> </label> </section> </form> @@ -24,17 +24,17 @@ <script> import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; -import Datetimepicker from '../Datetimepicker.vue'; +import datetimepicker from '../Datetimepicker.vue'; import StudipTooltipIcon from '../StudipTooltipIcon.vue'; export default { name: 'ParticipantRestrictedAdmission', - components: { StudipTooltipIcon, Datetimepicker }, + components: { StudipTooltipIcon, datetimepicker }, mixins: [AdmissionRuleMixin], props: { distribution: { type: Number, - default: Math.floor(new Date().getTime() / 1000 + 86400) + default: 0 }, fcfs: { type: Boolean, @@ -54,8 +54,7 @@ export default { messageText: this.message, fcfsAllowed: true, fcfsEnabled: this.distributionTime === 0, - distributionTime: this.distribution, - loaded: false + distributionTime: this.distribution !== 0 ? this.distribution : Math.floor(Date.now() / 1000 + 7 * 86400) } }, computed: { @@ -78,13 +77,23 @@ export default { ? data.attributes.payload['distribution-time'] : Math.floor(Date.now() / 1000 + 7 * 86400); this.fcfsEnabled = data.attributes.payload['distribution-time'] === 0; - this.loaded = true; - } - }, - created() { - if (!this.id) { - this.distributionTime = Math.floor(new Date().getTime() / 1000 + 86400); - this.loaded = true; + }, + validate() { + // Earliest possible date for seat distribution is 2 hours from now. + const earliest = new Date(); + earliest.setHours( earliest.getHours() + 2); + + if (!this.fcfsEnabled && this.distributionTime <= Math.floor(earliest.getTime() / 1000)) { + this.invalidData.push( + this.$gettext( + 'Geben Sie für die Platzverteilung ein Datum an, das weiter in der Zukunft liegt. ' + + 'Das frühestmögliche Datum ist %{earliest}.', + {earliest: earliest.toLocaleString('de-de')} + ) + ); + } + + return this.invalidData.length === 0; } } } diff --git a/resources/vue/components/admission/TimedAdmission.vue b/resources/vue/components/admission/TimedAdmission.vue index ebdc397..2437d7e 100644 --- a/resources/vue/components/admission/TimedAdmission.vue +++ b/resources/vue/components/admission/TimedAdmission.vue @@ -23,9 +23,10 @@ <script> import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; - +import datetimepicker from "../Datetimepicker.vue"; export default { name: 'TimedAdmission', + components: { datetimepicker }, mixins: [ AdmissionRuleMixin ], props: { start: { diff --git a/resources/vue/components/admission/ValidityTime.vue b/resources/vue/components/admission/ValidityTime.vue index 067567c..32efd04 100644 --- a/resources/vue/components/admission/ValidityTime.vue +++ b/resources/vue/components/admission/ValidityTime.vue @@ -16,24 +16,24 @@ <section v-if="configureTime" class="col-3"> <label> {{ $gettext('Diese Regel gilt von') }} - <datetimepicker :value="startTime"></datetimepicker> + <datetimepicker v-model="startTime"></datetimepicker> </label> </section> <section v-if="configureTime" class="col-3"> <label> {{ $gettext('bis') }} - <datetimepicker :value="endTime"></datetimepicker> + <datetimepicker v-model="endTime"></datetimepicker> </label> </section> </div> </template> <script> -import Datetimepicker from '../Datetimepicker.vue'; +import datetimepicker from '../Datetimepicker.vue'; export default { name: 'ValidityTime', - components: { Datetimepicker }, + components: { datetimepicker }, props: { start: { type: Number, diff --git a/resources/vue/mixins/AdmissionRuleMixin.js b/resources/vue/mixins/AdmissionRuleMixin.js index 0badb1a..80f6ec5 100644 --- a/resources/vue/mixins/AdmissionRuleMixin.js +++ b/resources/vue/mixins/AdmissionRuleMixin.js @@ -42,7 +42,7 @@ export const AdmissionRuleMixin = { } } }, - mounted() { + created() { if (this.id && this.id !== '' && !this.ruleData) { this.loadRuleData(); } |
