From ff121427c6ef82903b7cf050bdf3ad65954499ee Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Mon, 9 Sep 2024 17:00:51 +0100 Subject: [PATCH] Bug 10190: Add custom input for delay This custom input allows for dynamic min/max rules and it updates the input styling to match other inputs on the page including a 'clear' button. --- .../CirculationTriggersFormAdd.vue | 419 ++++++++++++++---- .../CirculationTriggers/TriggersTable.vue | 192 +++++--- 2 files changed, 467 insertions(+), 144 deletions(-) diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue index c0dbce2d3cf..0ae22737919 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue @@ -12,8 +12,12 @@

{{ $__("Trigger context") }}

-

{{ $__("Existing rules") }}

-

{{ $__("Notice") }} {{ " " + newTriggerNumber - 1 }}

+

+ {{ $__("Existing rules") }} +

+

+ {{ $__("Notice") }} {{ " " + newTriggerNumber - 1 }} +

  • - +
    {{ $__("Yes") }} - + {{ $__("No") }} - + {{ $__("Fallback to default") }} - ({{ fallbackRule.restricts === 1 ? $__("Yes") : $__("No") }}) + ({{ + fallbackRule.restricts === 1 + ? $__("Yes") + : $__("No") + }})
  • @@ -268,6 +337,8 @@ export default { editMode: false, ruleBeingEdited: null, triggerBeingEdited: null, + minDelay: 0, + maxDelay: Infinity, } }, beforeRouteEnter(to, from, next) { @@ -286,12 +357,6 @@ export default { ) }) }, - computed: { - minDelay() { - const lastRule = this.circRules[this.newTriggerNumber - 2]; - return lastRule ? parseInt(lastRule[`overdue_${this.newTriggerNumber - 1}_delay`]) + 1 : 0; - } - }, methods: { async addCircRule(e) { e.preventDefault() @@ -299,17 +364,22 @@ export default { const context = { library_id: this.newRule.library_id || "*", item_type_id: this.newRule.item_type_id || "*", - patron_category_id: - this.newRule.patron_category_id || "*", + patron_category_id: this.newRule.patron_category_id || "*", } const circRule = { context, } - circRule[`overdue_${this.newTriggerNumber}_delay`] = this.newRule.delay - circRule[`overdue_${this.newTriggerNumber}_notice`] = this.newRule.notice - circRule[`overdue_${this.newTriggerNumber}_restrict`] = this.newRule.restrict - circRule[`overdue_${this.newTriggerNumber}_mtt`] = this.newRule.mtt && this.newRule.mtt.length ? this.newRule.mtt.join(",") : null + circRule[`overdue_${this.newTriggerNumber}_delay`] = + this.newRule.delay + circRule[`overdue_${this.newTriggerNumber}_notice`] = + this.newRule.notice + circRule[`overdue_${this.newTriggerNumber}_restrict`] = + this.newRule.restrict + circRule[`overdue_${this.newTriggerNumber}_mtt`] = + this.newRule.mtt && this.newRule.mtt.length + ? this.newRule.mtt.join(",") + : null const client = APIClient.circRule await client.circRules.update(circRule).then( @@ -367,11 +437,14 @@ export default { const client = APIClient.circRule await client.circRules.getAll({}, { effective: false }).then( rules => { - const { rulesPerTrigger } = this.splitCircRulesByTriggerNumber(rules) - this.circRules = rulesPerTrigger.length ? rulesPerTrigger : rules + const { rulesPerTrigger } = + this.splitCircRulesByTriggerNumber(rules) + this.circRules = rulesPerTrigger.length + ? rulesPerTrigger + : rules }, error => {} - ) + ) }, async handleContextChange() { await this.checkForExistingRules() @@ -411,7 +484,9 @@ export default { const numberOfTriggers = Object.keys(rules[0]).filter( key => regex.test(key) && rules[0][key] !== null ).length - const splitRules = this.filterCircRulesByContext(this.ruleBeingEdited) + const splitRules = this.filterCircRulesByContext( + this.ruleBeingEdited + ) this.newTriggerNumber = editMode ? routeParams.triggerNumber : numberOfTriggers + 1 @@ -429,31 +504,36 @@ export default { lengthunit: rules[0].lengthunit, numberOfTriggers: numberOfTriggers, } + + this.setMinDelay() + this.setMaxDelay() }, - error => { } + error => {} ) }, filterCircRulesByContext(effectiveRule) { - const context = effectiveRule.context; - + const context = effectiveRule.context + // Filter rules that match the context let contextRules = this.circRules.filter(rule => { return Object.keys(context).every(key => { - return context[key] === rule.context[key]; - }); - }); - + return context[key] === rule.context[key] + }) + }) + // Calculate the number of 'overdue_X_' triggers in the effectiveRule - const regex = /overdue_(\d+)_delay/g; + const regex = /overdue_(\d+)_delay/g const numberOfTriggers = Object.keys(effectiveRule).filter( key => regex.test(key) && effectiveRule[key] !== null - ).length; - + ).length + // Ensure there is one contextRule per 'X' from 1 to numberOfTriggers for (let i = 1; i <= numberOfTriggers; i++) { // Check if there's already a rule for overdue_X_ in contextRules - const matchingRule = contextRules.find(rule => rule[`overdue_${i}_delay`] !== undefined); - + const matchingRule = contextRules.find( + rule => rule[`overdue_${i}_delay`] !== undefined + ) + if (!matchingRule) { // Create a new rule with the same context and null overdue_X_* keys const placeholderRule = { @@ -461,36 +541,37 @@ export default { [`overdue_${i}_delay`]: null, [`overdue_${i}_notice`]: null, [`overdue_${i}_mtt`]: null, - [`overdue_${i}_restrict`]: null - }; - + [`overdue_${i}_restrict`]: null, + } + // Add the new rule to contextRules - contextRules.push(placeholderRule); + contextRules.push(placeholderRule) } } // Sort contextRules by the 'X' value in 'overdue_X_delay' contextRules.sort((a, b) => { const getX = rule => { - const match = Object.keys(rule).find(key => regex.test(key)); - return match ? parseInt(match.match(/\d+/)[0], 10) : 0; - }; - - return getX(a) - getX(b); - }); - - return contextRules; + const match = Object.keys(rule).find(key => regex.test(key)) + return match ? parseInt(match.match(/\d+/)[0], 10) : 0 + } + + return getX(a) - getX(b) + }) + + return contextRules }, findFallbackRule(currentContext, key) { - // Filter rules to only those with non-null values for the specified key and not the current context - const relevantRules = this.circRules.filter( - rule => { - return Object.keys(currentContext).some(key => currentContext[key] !== rule.context[key]) - && rule[key] !== null - && rule[key] !== undefined; - } - ); + const relevantRules = this.circRules.filter(rule => { + return ( + Object.keys(currentContext).some( + key => currentContext[key] !== rule.context[key] + ) && + rule[key] !== null && + rule[key] !== undefined + ) + }) // Function to calculate specificity score const getSpecificityScore = ruleContext => { @@ -503,7 +584,7 @@ export default { if ( ruleContext.patron_category_id !== "*" && ruleContext.patron_category_id === - currentContext.patron_category_id + currentContext.patron_category_id ) score += 2 if ( @@ -524,7 +605,7 @@ export default { // If no rule found, return null if (sortedRules.length === 0) { - return null + return null } // Get the value from the most specific rule @@ -543,27 +624,93 @@ export default { ? context.patron_category_id : rules[triggerNumber - 1].context.patron_category_id || "*", - delay: rules[triggerNumber - 1] ? rules[triggerNumber - 1][ - `overdue_${triggerNumber}_delay` - ] : null, - notice: rules[triggerNumber - 1] ? rules[triggerNumber - 1][ - `overdue_${triggerNumber}_notice` - ] : null, - mtt: rules[triggerNumber - 1] ? rules[triggerNumber - 1][`overdue_${triggerNumber}_mtt`] + delay: rules[triggerNumber - 1] + ? rules[triggerNumber - 1][`overdue_${triggerNumber}_delay`] + : null, + notice: rules[triggerNumber - 1] + ? rules[triggerNumber - 1][ + `overdue_${triggerNumber}_notice` + ] + : null, + mtt: rules[triggerNumber - 1] + ? rules[triggerNumber - 1][`overdue_${triggerNumber}_mtt`] + ? rules[triggerNumber - 1][ + `overdue_${triggerNumber}_mtt` + ].split(",") + : [] + : null, + restrict: rules[triggerNumber - 1] ? rules[triggerNumber - 1][ - `overdue_${triggerNumber}_mtt` - ].split(",") - : [] : null, - restrict: rules[triggerNumber - 1] ? - rules[triggerNumber - 1][ - `overdue_${triggerNumber}_restrict` - ] : null, + `overdue_${triggerNumber}_restrict` + ] + : null, } this.fallbackRule = { - delay: this.findFallbackRule(context, `overdue_${triggerNumber}_delay`), - notice: this.findFallbackRule(context, `overdue_${triggerNumber}_notice`), - mtt: this.findFallbackRule(context, `overdue_${triggerNumber}_mtt`), - restrict: this.findFallbackRule(context, `overdue_${triggerNumber}_restrict`) + delay: this.findFallbackRule( + context, + `overdue_${triggerNumber}_delay` + ), + notice: this.findFallbackRule( + context, + `overdue_${triggerNumber}_notice` + ), + mtt: this.findFallbackRule( + context, + `overdue_${triggerNumber}_mtt` + ), + restrict: this.findFallbackRule( + context, + `overdue_${triggerNumber}_restrict` + ), + } + }, + setMinDelay() { + const priorTriggerNumber = parseInt(this.newTriggerNumber) - 1 + this.minDelay = this.ruleBeingEdited[ + `overdue_${priorTriggerNumber}_delay` + ] + ? parseInt( + this.ruleBeingEdited[ + `overdue_${priorTriggerNumber}_delay` + ] + ) + 1 + : 0 + }, + setMaxDelay() { + const nextTriggerNumber = parseInt(this.newTriggerNumber) + 1 + this.maxDelay = this.ruleBeingEdited[ + `overdue_${nextTriggerNumber}_delay` + ] + ? parseInt( + this.ruleBeingEdited[`overdue_${nextTriggerNumber}_delay`] + ) - 1 + : Infinity + }, + incrementDelay() { + // Check for minDelay and maxDelay + const min = this.minDelay !== undefined ? this.minDelay : 1 + const max = this.maxDelay !== undefined ? this.maxDelay : Infinity + + // Set to minDelay if it's null or undefined + if ( + this.newRule.delay === undefined || + this.newRule.delay === null + ) { + this.newRule.delay = min + } + + // Increment within the valid range + else { + this.newRule["delay"] = Math.min(this.newRule.delay + 1, max) + } + }, + decrementDelay() { + // Check for minDelay + const min = this.minDelay !== undefined ? this.minDelay : 1 + + // Decrement only if greater than minDelay + if (this.newRule.delay > min) { + this.newRule.delay-- } }, }, @@ -598,6 +745,98 @@ form li { align-items: center; } +.numeric-input-wrapper { + position: relative; + display: inline-block; + width: 30%; +} + +.input-with-clear { + position: relative; + display: flex; + align-items: center; + width: 100%; +} + +.numeric-input { + padding-right: 40px; /* Adjust to leave space for clear button */ + padding-left: 0.25em; + padding-top: 2px; + padding-bottom: 2px; + width: 100%; + border-radius: 4px; + border: 1px solid #ccc; + font-size: 16px; + box-sizing: border-box; + transition: border-color 0.2s ease; +} + +.clear-btn { + position: absolute; + right: 22px; /* Adjust positioning */ + fill: var(--vs-controls-color); + background-color: transparent; + border: 0; + font-size: 1.2em; + color: #333; + cursor: pointer; + z-index: 2; /* Ensure it is above the input */ +} + +.button:active:hover, +.clear-btn:active:hover { + background-color: #d4d4d4; + border-color: #8c8c8c; +} + +/* Chevron buttons container */ +.chevron-buttons { + display: flex; + flex-direction: column; + position: absolute; + right: 0px; + top: 0; + bottom: 0; + width: 16px; + padding: 0px 5px 0px 2px; + justify-content: center; + z-index: 2; +} + +/* Chevron button styles */ +.increment-btn, +.decrement-btn { + background-color: transparent; + border: 0px solid #ccc; + font-size: 10px; + padding: 0px; + cursor: pointer; + color: rgba(60, 60, 60, 0.5); + border-radius: 2px; +} + +.increment-btn:hover, +.decrement-btn:hover { + background-color: #ddd; +} + +/* Hide the native increment/decrement buttons */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; /* For Firefox */ +} + +.numeric-input:focus, +.numeric-input:hover { + border-color: #007bff; /* Match focus color of v-select */ + outline: none; +} + .dialog.alert fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(.bg-success):not(.bg-primary):not(.action), .dialog.error diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue index 590fe682763..06d540e93b9 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue @@ -36,9 +36,9 @@ {{ - findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_delay`).value + + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_delay` + ).value + " " + $__("days") }} @@ -85,10 +95,24 @@ - {{ handleNotice(findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_notice`).value) }} + {{ + handleNotice( + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_notice` + ).value + ) + }} @@ -96,15 +120,31 @@ {{ - findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_notice`).value !== '' ? - handleTransport( - findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_mtt`).value, - "email" - ) : '' + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_notice` + ).value !== "" + ? handleTransport( + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_mtt` + ).value, + "email" + ) + : "" }} @@ -113,15 +153,31 @@ {{ - findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_notice`).value !== '' ? - handleTransport( - findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_mtt`).value, - "print" - ) : '' + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_notice` + ).value !== "" + ? handleTransport( + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_mtt` + ).value, + "print" + ) + : "" }} @@ -130,15 +186,31 @@ {{ - findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_notice`).value !== '' ? - handleTransport( - findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_mtt`).value, - "sms" - ) : '' + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_notice` + ).value !== "" + ? handleTransport( + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_mtt` + ).value, + "sms" + ) + : "" }} @@ -147,12 +219,22 @@ {{ handleRestrictions( - findEffectiveRule(rule, `overdue_${modal ? i + 1 : triggerNumber}_restrict`).value + findEffectiveRule( + rule, + `overdue_${ + modal ? i + 1 : triggerNumber + }_restrict` + ).value ) }} @@ -167,7 +249,9 @@ item_type_id: rule.context.item_type_id, patron_category_id: rule.context.patron_category_id, - triggerNumber: modal ? i + 1 : triggerNumber, + triggerNumber: modal + ? i + 1 + : triggerNumber, }, }" class="btn btn-default btn-xs" @@ -203,32 +287,34 @@ export default { ? value.includes(type) ? this.$__("Yes") : this.$__("No") - : '' + : "" }, handleRestrictions(value) { return value === "1" ? this.$__("Yes") : this.$__("No") }, filterCircRulesByContext(effectiveRule) { - const context = effectiveRule.context; - + const context = effectiveRule.context + // Filter rules that match the context let contextRules = this.circRules.filter(rule => { return Object.keys(context).every(key => { - return context[key] === rule.context[key]; - }); - }); - + return context[key] === rule.context[key] + }) + }) + // Calculate the number of 'overdue_X_' triggers in the effectiveRule - const regex = /overdue_(\d+)_delay/g; + const regex = /overdue_(\d+)_delay/g const numberOfTriggers = Object.keys(effectiveRule).filter( key => regex.test(key) && effectiveRule[key] !== null - ).length; - + ).length + // Ensure there is one contextRule per 'X' from 1 to numberOfTriggers for (let i = 1; i <= numberOfTriggers; i++) { // Check if there's already a rule for overdue_X_ in contextRules - const matchingRule = contextRules.find(rule => rule[`overdue_${i}_delay`] !== undefined); - + const matchingRule = contextRules.find( + rule => rule[`overdue_${i}_delay`] !== undefined + ) + if (!matchingRule) { // Create a new rule with the same context and null overdue_X_* keys const newRule = { @@ -236,25 +322,25 @@ export default { [`overdue_${i}_delay`]: null, [`overdue_${i}_notice`]: null, [`overdue_${i}_mtt`]: null, - [`overdue_${i}_restrict`]: null - }; - + [`overdue_${i}_restrict`]: null, + } + // Add the new rule to contextRules - contextRules.push(newRule); + contextRules.push(newRule) } } // Sort contextRules by the 'X' value in 'overdue_X_delay' contextRules.sort((a, b) => { const getX = rule => { - const match = Object.keys(rule).find(key => regex.test(key)); - return match ? parseInt(match.match(/\d+/)[0], 10) : 0; - }; - - return getX(a) - getX(b); - }); + const match = Object.keys(rule).find(key => regex.test(key)) + return match ? parseInt(match.match(/\d+/)[0], 10) : 0 + } + + return getX(a) - getX(b) + }) - return contextRules; + return contextRules }, filterCircRulesByTabNumber(number) { return this.circRules.filter( @@ -268,9 +354,7 @@ export default { }, handleNotice(notice) { const letter = this.letters.find(letter => letter.code === notice) - return letter - ? letter.name - : notice + return letter ? letter.name : notice }, findEffectiveRule(ruleSet, key) { // Check if the current rule's value for the key is null -- 2.47.0