Bug 10190: (QA follow-up) UI/UX improvements and fixes

- Refactor (prepation): Extract form sub-components for better maintainability
  - Template stucture improvements (eg. consistent fieldset use)
  - Ensure triggerNumber is always cast to integer from query params
  - Add/Edit trigger form UI/UX improvements/fixes:
      - Restore add/edit button functionality on trigger tables in the form
      - Update Context section title to dynamically change depending on whether editing or displaying
      - Display trigger context as informational when not in confirmContext mode
      - Hide required labels and validation on view-only context fields
      - Always display trigger context at the top of the form
      - Add renewal/autorenewal summary (renewals allowed, automatic renewal) to circulation rules summary panel
      - Sort notice letter dropdowns by name rather than code
      - Update Circulation rules summary title to be more descriptive
  - Scroll behaviour improvements:
      - Extract scrollToElementById to store with polling and null guard
      - Block page scroll when modal is open
      - Disambiguate duplicate trigger-table element ids (trigger-table-main
        vs trigger-table-form) to fix unpredictable scroll-to behaviour

Assisted-by: Claude Sonnet 4.6 (Anthropic)

Experimenting a little here - not quite certain where we draw the line for substantial use so I thought I would add a description in case it's useful, especially since the AI code guidelines are quite fresh:

AI was used primarily to help parse longer Vue templates (CirculationTriggersFormAdd.vue) and help isolate elements for extractions as well as highlight template formatting inconsistencies and propose improvements. The refactoring itself was achieved starting from AI-proposed changes which were then thoroughly re-worked manually towards the code changes submitted.
I also trialled using it (currently experimenting with this) for minor coding tasks where there is a clear precedent (for example, to add renewal/autorenewal information and do so in a way that follows exactly what we had written).
In those cases, each code change made by AI was the outcome of multiple iterations with direct feedback (which it did require a fair amount of) from me to ensure the output matched what I would have written myself.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Chloe Zermatten 2026-03-26 13:49:10 +00:00 committed by Pedro Amorim
parent 8f6ff0a73d
commit 0fa640b387
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
13 changed files with 1046 additions and 869 deletions

View File

@ -0,0 +1,74 @@
<template>
<fieldset class="rows" id="trigger-edit-form-context">
<legend>
{{ $__("Circulation rules for this context (summarised)") }}
</legend>
<div class="page-section bg-info">
<table>
<thead>
<th>
{{ $__("Loan period") }}
</th>
<th>
{{ $__("Unit") }}
</th>
<th>
{{ $__("Decreased loan period for high holds") }}
</th>
<th>
{{ $__("Fine amount") }}
</th>
<th>
{{ $__("Fine charging interval") }}
</th>
<th>
{{ $__("Renewals allowed") }}
</th>
<th>
{{ $__("Automatic renewal") }}
</th>
</thead>
<tbody>
<tr>
<td>
{{ ruleSetInfo.issuelength }}
</td>
<td>
{{ ruleSetInfo.lengthunit }}
</td>
<td>
{{ ruleSetInfo.decreaseloanholds }}
</td>
<td>
{{ ruleSetInfo.fine }}
</td>
<td>
{{ ruleSetInfo.chargeperiod }}
</td>
<td>
{{ ruleSetInfo.renewalsallowed }}
</td>
<td>
{{
ruleSetInfo.auto_renew ? $__("Yes") : $__("No")
}}
</td>
</tr>
</tbody>
</table>
</div>
</fieldset>
</template>
<script>
export default {
props: ["ruleSetInfo"],
};
</script>
<style scoped>
table {
margin: auto;
width: fit-content;
}
</style>

View File

@ -17,399 +17,76 @@
></router-link>
</div>
<div class="modal-body">
<div class="page-section bg-info" v-if="ruleSetInitialized">
<h2>{{ $__("Circulation context") }}</h2>
<TriggerContext :ruleSetInfo="ruleSetInfo" />
<div class="page-section">
<fieldset class="rows" v-if="alertMessage">
<div class="alert alert-warning">{{ alertMessage }}</div>
</fieldset>
</div>
<div class="page-section">
<ConfirmContext
:context="context"
:contextInitialized="contextInitialized"
:editMode="editMode"
:canManageAnyLibrary="canManageAnyLibrary"
:libraries="libraries"
:logged_in_library_id="logged_in_library_id"
:patronCategories="patronCategories"
:itemTypes="itemTypes"
/>
</div>
<div class="page-section" v-if="ruleSetInitialized">
<CirculationRulesSummary :ruleSetInfo="ruleSetInfo" />
</div>
<div v-else-if="editMode !== 'confirmContext'">
<p>{{ $__("Loading rule set information...") }}</p>
</div>
<fieldset class="rows" v-if="contextInitialized">
<legend>{{ $__("Confirm trigger context") }}</legend>
<ol id="confirm-context-list">
<li>
<label for="library_id" class="required"
>{{ $__("Library") }}:</label
>
<v-select
id="library_id"
v-model="context.library_id"
label="name"
:reduce="lib => lib.library_id"
:options="
canManageAnyLibrary
? libraries
: libraries.filter(
lib =>
lib.library_id ===
logged_in_library_id
)
"
:disabled="
editMode !== 'confirmContext' ||
!canManageAnyLibrary
"
>
<template #search="{ attributes, events }">
<input
:required="!context.library_id"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $__("Required") }}</span>
</li>
<li>
<label for="patron_category_id" class="required"
>{{ $__("Patron category") }}:</label
>
<v-select
id="patron_category_id"
v-model="context.patron_category_id"
label="name"
:reduce="cat => cat.patron_category_id"
:options="patronCategories"
:disabled="editMode !== 'confirmContext'"
>
<template #search="{ attributes, events }">
<input
:required="!context.patron_category_id"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $__("Required") }}</span>
</li>
<li>
<label for="item_type_id" class="required"
>{{ $__("Item type") }}:</label
>
<v-select
id="item_type_id"
v-model="context.item_type_id"
label="description"
:reduce="type => type.item_type_id"
:options="itemTypes"
:disabled="editMode !== 'confirmContext'"
>
<template #search="{ attributes, events }">
<input
:required="!context.item_type_id"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span class="required">{{ $__("Required") }}</span>
</li>
</ol>
<div v-if="editMode === 'confirmContext'">
<router-link
:to="{
name: 'CirculationTriggersSelectOrAdd',
query: context,
}"
class="btn btn-default btn-xs float-end"
><i class="fa-solid fa-pencil"></i>
{{ $__("Confirm context") }}</router-link
>
</div>
<div
class="page-section"
v-if="ruleSetInitialized && editMode !== 'confirmContext'"
>
<TriggersTable
:triggerNumber="triggerNumber"
:modal="true"
:actions="true"
:ruleSets="effectiveTriggerFilteredRuleSets"
:ruleSetBeingEdited="currentRuleSet"
:triggerBeingEdited="triggerBeingEdited"
/>
</div>
</fieldset>
<div v-else>
<p>{{ $__("Loading context...") }}</p>
</div>
<fieldset class="rows" v-if="alertMessage">
<div class="alert alert-warning">{{ alertMessage }}</div>
</fieldset>
<fieldset
class="rows"
v-if="
(ruleSetInitialized && editMode === 'edit') ||
editMode === 'add'
"
id="trigger-edit-form-general-section"
<div
class="page-section"
v-if="ruleSetInitialized && editMode !== 'confirmContext'"
>
<legend v-if="editMode === 'add'">
{{ $__("Add new trigger") }}
{{ " " + triggerNumber }}
</legend>
<legend v-else>
{{ $__("Edit trigger") }} {{ " " + triggerNumber }}
</legend>
<div class="page-section bg-info">
<p>
{{
$__(
"NOTE: Delay for a given trigger can be pushed forward or backwards only within the bounds of what its two neighbouring triggers allows."
)
}}
</p>
</div>
<ol>
<li>
<label for="overdue_delay">{{ $__("Delay") }}: </label>
<div class="numeric-input-wrapper">
<div class="input-with-clear">
<input
@input="setAllowSubmission"
id="overdue_delay"
v-model="
ruleSetToSubmit[
`overdue_${triggerNumber}_delay`
]
"
type="number"
:placeholder="
fallbackRuleSet?.[
`overdue_${triggerNumber}_delay`
]
"
:min="minDelay"
:max="maxDelay"
class="numeric-input"
/>
<button
v-if="
ruleSetToSubmit[
`overdue_${triggerNumber}_delay`
] !== null &&
ruleSetToSubmit[
`overdue_${triggerNumber}_delay`
] !== undefined
"
type="button"
class="clear-btn"
@click="handleSetDelayToNull"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="10"
height="10"
>
<path
d="M6.895455 5l2.842897-2.842898c.348864-.348863.348864-.914488 0-1.263636L9.106534.261648c-.348864-.348864-.914489-.348864-1.263636 0L5 3.104545 2.157102.261648c-.348863-.348864-.914488-.348864-1.263636 0L.261648.893466c-.348864.348864-.348864.914489 0 1.263636L3.104545 5 .261648 7.842898c-.348864.348863-.348864.914488 0 1.263636l.631818.631818c.348864.348864.914773.348864 1.263636 0L5 6.895455l2.842898 2.842897c.348863.348864.914772.348864 1.263636 0l.631818-.631818c.348864-.348864.348864-.914489 0-1.263636L6.895455 5z"
></path>
</svg>
</button>
<div class="chevron-buttons">
<button
type="button"
class="increment-btn"
@click="incrementDelay"
>
▴
</button>
<button
type="button"
class="decrement-btn"
@click="decrementDelay"
>
▾
</button>
</div>
</div>
</div>
</li>
<li>
<label for="restricts"
>{{ $__("Restricts checkouts") }}:</label
>
<div>
<input
@click="setAllowSubmission"
type="radio"
id="restricts-yes"
v-model="
ruleSetToSubmit[
`overdue_${triggerNumber}_restrict`
]
"
:value="1"
/>
{{ $__("Yes") }}
<input
@click="setAllowSubmission"
type="radio"
id="restricts-no"
v-model="
ruleSetToSubmit[
`overdue_${triggerNumber}_restrict`
]
"
:value="0"
/>
{{ $__("No") }}
<input
@click="setAllowSubmission"
type="radio"
id="restricts-fallback"
v-model="
ruleSetToSubmit[
`overdue_${triggerNumber}_restrict`
]
"
:value="null"
/>
{{ $__("Fallback to default") }}
<span
v-if="
fallbackRuleSet?.[
`overdue_${triggerNumber}_restrict`
] !== null
"
>
({{
fallbackRuleSet?.[
`overdue_${triggerNumber}_restrict`
] === "1"
? $__("Yes")
: $__("No")
}})
</span>
</div>
</li>
</ol>
</fieldset>
<div v-else-if="editMode === 'add' || editMode === 'edit'">
<p>{{ $__("Loading circulation rules...") }}</p>
<TriggersTable
:triggerNumber="triggerNumber"
:modal="true"
:actions="true"
:ruleSets="effectiveTriggerFilteredRuleSets"
:ruleSetBeingEdited="currentRuleSet"
:triggerBeingEdited="triggerBeingEdited"
/>
</div>
<fieldset
class="rows"
v-if="
(ruleSetInitialized && editMode === 'edit') ||
editMode === 'add'
"
id="trigger-edit-form-notice-section"
<div
class="page-section"
v-if="editMode === 'add' || editMode === 'edit'"
>
<legend v-if="ruleSetInfo.triggerCount < triggerNumber">
{{ $__("Notice for trigger") }}
{{ " " + triggerNumber }}
</legend>
<legend v-else>
{{ $__("Edit notice for trigger") }}
{{ " " + triggerNumber }}
</legend>
<ol>
<li>
<label for="letter_code">{{ $__("Letter") }}:</label>
<v-select
id="letter_code"
v-model="
ruleSetToSubmit[
`overdue_${triggerNumber}_notice`
]
"
label="name"
:reduce="type => type.code"
:options="filteredLetters"
@update:modelValue="setAllowSubmission"
>
<template #search="{ attributes, events }">
<input
class="vs__search"
v-bind="attributes"
v-on="events"
:placeholder="
ruleSetToSubmit[
`overdue_${triggerNumber}_notice`
] === null ||
ruleSetToSubmit[
`overdue_${triggerNumber}_notice`
] === undefined
? letters.find(
letter =>
letter.code ===
fallbackRuleSet?.[
`overdue_${triggerNumber}_notice`
]
)?.name ||
fallbackRuleSet?.[
`overdue_${triggerNumber}_notice`
]
: ''
"
/>
</template>
</v-select>
</li>
<li
v-if="
ruleSetToSubmit[
`overdue_${triggerNumber}_notice`
] !== '' &&
ruleSetToSubmit[
`overdue_${triggerNumber}_notice`
] !== null &&
ruleSetToSubmit[
`overdue_${triggerNumber}_notice`
] !== undefined
"
>
<label for="mtt">{{ $__("Transport type(s)") }}:</label>
<v-select
id="mtt"
v-model="
ruleSetToSubmit[`overdue_${triggerNumber}_mtt`]
"
label="name"
:reduce="type => type.code"
:options="transportTypes"
multiple
:required="true"
@update:modelValue="setAllowSubmission"
>
<template #search="{ attributes, events }">
<input
class="vs__search"
v-bind="attributes"
v-on="events"
:placeholder="
ruleSetToSubmit[
`overdue_${triggerNumber}_mtt`
] === null ||
ruleSetToSubmit[
`overdue_${triggerNumber}_mtt`
] === undefined ||
ruleSetToSubmit[
`overdue_${triggerNumber}_mtt`
].length === 0
? fallbackRuleSet?.[
`overdue_${triggerNumber}_mtt`
]
: ''
"
:required="
!ruleSetToSubmit[
`overdue_${triggerNumber}_mtt`
]?.length
"
/>
</template>
</v-select>
<span class="required">{{ $__("Required") }}</span>
</li>
</ol>
</fieldset>
<div v-else-if="editMode === 'add' || editMode === 'edit'">
<p>{{ $__("Loading circulation rules...") }}</p>
<EditActions
:ruleSetInitialized="ruleSetInitialized"
:editMode="editMode"
:triggerNumber="triggerNumber"
:ruleSetToSubmit="ruleSetToSubmit"
:fallbackRuleSet="fallbackRuleSet"
:minDelay="minDelay"
:maxDelay="maxDelay"
:setAllowSubmission="setAllowSubmission"
:handleSetDelayToNull="handleSetDelayToNull"
:incrementDelay="incrementDelay"
:decrementDelay="decrementDelay"
/>
</div>
<div
class="page-section"
v-if="editMode === 'add' || editMode === 'edit'"
>
<EditNotice
:ruleSetInitialized="ruleSetInitialized"
:editMode="editMode"
:triggerNumber="triggerNumber"
:ruleSetToSubmit="ruleSetToSubmit"
:fallbackRuleSet="fallbackRuleSet"
:ruleSetInfo="ruleSetInfo"
:filteredLetters="filteredLetters"
:letters="letters"
:transportTypes="transportTypes"
:setAllowSubmission="setAllowSubmission"
/>
</div>
</div>
<div class="modal-footer">
@ -432,7 +109,10 @@ import TriggersTable from "./TriggersTable.vue";
import { inject } from "vue";
import { storeToRefs } from "pinia";
import ButtonSubmit from "../../ButtonSubmit.vue";
import TriggerContext from "./TriggerContext.vue";
import CirculationRulesSummary from "./CirculationRulesSummary.vue";
import ConfirmContext from "./ConfirmContext.vue";
import EditActions from "./EditActions.vue";
import EditNotice from "./EditNotice.vue";
import { isEqual, cloneDeep } from "lodash";
export default {
@ -445,6 +125,7 @@ export default {
setEffectiveTriggerFilteredRuleSet,
updateCircRuleSets,
hasConflict,
scrollToElementById,
} = circRulesStore;
const {
letters,
@ -474,6 +155,7 @@ export default {
storeInitialized,
canManageAnyLibrary,
logged_in_library_id,
scrollToElementById,
};
},
data() {
@ -524,12 +206,16 @@ export default {
next();
return;
}
next(vm => vm.initializeComponent());
next(vm => vm.initializeComponent(to));
},
beforeRouteUpdate(to, from, next) {
next();
this.initializeComponent(to);
},
methods: {
initializeComponent() {
const { query } = this.$route;
this.setEditMode();
initializeComponent(route) {
const { query } = route ?? this.$route;
this.setEditMode(route);
this.setContext(query);
this.contextInitialized = true;
this.setTriggerNumber(query.triggerNumber, this.context.library_id);
@ -688,9 +374,10 @@ export default {
this.context.item_type_id = query.item_type_id ?? "*";
this.context.patron_category_id = query.patron_category_id ?? "*";
},
setEditMode() {
this.editMode = this.$route.path.substring(
this.$route.path.lastIndexOf("/") + 1
setEditMode(route) {
const resolvedRoute = route ?? this.$route;
this.editMode = resolvedRoute.path.substring(
resolvedRoute.path.lastIndexOf("/") + 1
);
},
setRuleSetInfo() {
@ -700,6 +387,8 @@ export default {
fine: this.currentRuleSet.fine,
chargeperiod: this.currentRuleSet.chargeperiod,
lengthunit: this.currentRuleSet.lengthunit,
renewalsallowed: this.currentRuleSet.renewalsallowed,
auto_renew: this.currentRuleSet.auto_renew,
triggerCount: this.triggerCounts[this.context.library_id],
};
},
@ -715,7 +404,7 @@ export default {
setTriggerNumber(triggerNumber, library_id) {
this.triggerNumber =
this.editMode === "edit"
? triggerNumber
? parseInt(triggerNumber)
: (this.triggerCounts[library_id] ?? 0) + 1;
},
setFallbackRuleSet() {
@ -891,20 +580,6 @@ export default {
this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] = null;
this.setAllowSubmission();
},
async scrollToTriggerEditForm() {
let count = 0;
// ensures that the relevant section is loaded before we attempt to scroll it into view
while (
!document.getElementById("trigger-edit-form-general-section") &&
count < 8
) {
await new Promise(resolve => setTimeout(resolve, 250));
count++;
}
document
.getElementById("trigger-edit-form-general-section")
.scrollIntoView({ behavior: "smooth" });
},
},
watch: {
$route: {
@ -915,7 +590,7 @@ export default {
!oldVal.fullPath.includes("add") &&
!oldVal.fullPath.includes("edit") &&
oldVal.query.triggerNumber &&
newVal.query.triggerNumber !== oldVal.query.triggerNumber
newVal.query.triggerNumber != oldVal.query.triggerNumber
) {
this.$router.go(0);
}
@ -924,11 +599,18 @@ export default {
async editMode(newValue) {
if (newValue === "add" || newValue === "edit") {
await this.$nextTick();
await this.scrollToTriggerEditForm();
await this.scrollToElementById("trigger-table-form");
}
},
},
components: { TriggersTable, ButtonSubmit, TriggerContext },
components: {
TriggersTable,
ButtonSubmit,
CirculationRulesSummary,
ConfirmContext,
EditActions,
EditNotice,
},
};
</script>
@ -937,103 +619,6 @@ export default {
max-height: 90vh;
}
form li {
display: flex;
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
@ -1057,36 +642,4 @@ input[type="number"] {
.modal-body {
min-height: 280px;
}
#confirm-context-list {
display: flex;
gap: 20px;
height: fit-content;
}
#confirm-context-list li {
display: flex;
flex-direction: column;
width: 320px;
}
#confirm-context-list .v-select {
width: 100%;
}
#confirm-context-list label {
width: 100%;
text-align: left;
padding: 0 0 4px 10px;
}
#confirm-context-list span {
width: 100%;
align-content: right;
padding-top: 4px;
}
#confirm-context-list :deep(.v-select ul) {
max-height: 120px;
}
</style>

View File

@ -267,7 +267,6 @@
<script>
import ButtonSubmit from "../../ButtonSubmit.vue";
import CirculationTriggersForm from "./CirculationTriggersForm.vue";
import TriggerContext from "./TriggerContext.vue";
import { inject } from "vue";
import { storeToRefs } from "pinia";
@ -356,10 +355,10 @@ export default {
this.library_id = query.library_id ?? "*";
this.item_type_id = query.item_type_id ?? "*";
this.patron_category_id = query.patron_category_id ?? "*";
this.triggerNumber = query.triggerNumber;
this.triggerNumber = parseInt(query.triggerNumber);
},
},
components: { ButtonSubmit, TriggerContext, CirculationTriggersForm },
components: { ButtonSubmit, CirculationTriggersForm },
};
</script>

View File

@ -41,7 +41,6 @@
</template>
<script>
import TriggersTable from "./TriggersTable.vue";
import TriggerContext from "./TriggerContext.vue";
import CirculationTriggersForm from "./CirculationTriggersForm.vue";
import { inject } from "vue";
import { storeToRefs } from "pinia";
@ -134,10 +133,10 @@ export default {
},
setContext(query) {
this.library_id = query.library_id ?? "*";
this.triggerNumber = query.triggerNumber;
this.triggerNumber = parseInt(query.triggerNumber);
},
},
components: { TriggerContext, TriggersTable, CirculationTriggersForm },
components: { TriggersTable, CirculationTriggersForm },
};
</script>

View File

@ -256,8 +256,11 @@
</div>
</div>
</div>
<div v-if="filtersInitialized && ruleSetInitialized">
<div id="circ_triggers_tabs" class="toptabs numbered">
<div
v-if="filtersInitialized && ruleSetInitialized"
id="circ-triggers-content"
>
<div id="circ-triggers-tabs" class="toptabs numbered">
<ul class="nav nav-tabs" role="tablist">
<li
v-for="number in triggerCounts[currentLibraryId]"
@ -355,6 +358,7 @@ export default {
setAllExhaustiveEffectiveRuleSets,
isLastTrigger,
getLibrariesWithRules,
scrollToElementById,
} = circRulesStore;
const {
currentLibraryId,
@ -395,6 +399,7 @@ export default {
from_branch,
canManageAnyLibrary,
logged_in_library_id,
scrollToElementById,
};
},
data() {
@ -497,6 +502,9 @@ export default {
immediate: true,
handler: function (newVal, oldVal) {
this.showModal = newVal.meta && newVal.meta.showModal;
const overflow = this.showModal ? "hidden" : "";
const body = document.querySelector("body");
body.style.overflow = overflow;
},
},
"$route.query": {
@ -517,6 +525,7 @@ export default {
}
await this.$nextTick();
await this.filterRuleSetsbySearchParam();
await this.scrollToElementById("circ-triggers-content");
}
this.filtersInitialized = true;
},

View File

@ -0,0 +1,179 @@
<template>
<div v-if="!contextInitialized">
<p>{{ $__("Loading context...") }}</p>
</div>
<fieldset v-else class="rows">
<legend>
{{
isConfirming
? $__("Confirm trigger context")
: $__("Selected trigger context")
}}
</legend>
<div
:class="{
'bg-info': !isConfirming,
'page-section': true,
}"
>
<ol id="confirm-context-list">
<li>
<label for="library_id" :class="{ required: isConfirming }"
>{{ $__("Library") }}:</label
>
<v-select
id="library_id"
v-model="context.library_id"
label="name"
:reduce="lib => lib.library_id"
:options="
canManageAnyLibrary
? libraries
: libraries.filter(
lib =>
lib.library_id ===
logged_in_library_id
)
"
:disabled="!isConfirming || !canManageAnyLibrary"
>
<template #search="{ attributes, events }">
<input
:required="isConfirming && !context.library_id"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span v-if="isConfirming" class="required">{{
$__("Required")
}}</span>
</li>
<li>
<label
for="patron_category_id"
:class="{ required: isConfirming }"
>{{ $__("Patron category") }}:</label
>
<v-select
id="patron_category_id"
v-model="context.patron_category_id"
label="name"
:reduce="cat => cat.patron_category_id"
:options="patronCategories"
:disabled="!isConfirming"
>
<template #search="{ attributes, events }">
<input
:required="
isConfirming && !context.patron_category_id
"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span v-if="isConfirming" class="required">{{
$__("Required")
}}</span>
</li>
<li>
<label
for="item_type_id"
:class="{ required: isConfirming }"
>{{ $__("Item type") }}:</label
>
<v-select
id="item_type_id"
v-model="context.item_type_id"
label="description"
:reduce="type => type.item_type_id"
:options="itemTypes"
:disabled="!isConfirming"
>
<template #search="{ attributes, events }">
<input
:required="
isConfirming && !context.item_type_id
"
class="vs__search"
v-bind="attributes"
v-on="events"
/>
</template>
</v-select>
<span v-if="isConfirming" class="required">{{
$__("Required")
}}</span>
</li>
</ol>
</div>
<div v-if="isConfirming">
<router-link
:to="{
name: 'CirculationTriggersSelectOrAdd',
query: context,
}"
class="btn btn-default btn-xs float-end"
><i class="fa-solid fa-pencil"></i>
{{ $__("Confirm context") }}</router-link
>
</div>
</fieldset>
</template>
<script>
export default {
props: {
context: { type: Object, required: true },
contextInitialized: { type: Boolean, required: true },
editMode: { type: [String, Boolean], required: true },
canManageAnyLibrary: { type: Boolean, required: true },
libraries: { type: Array, required: true },
logged_in_library_id: { type: [String, null], default: null },
patronCategories: { type: Array, required: true },
itemTypes: { type: Array, required: true },
},
computed: {
isConfirming() {
return this.editMode === "confirmContext";
},
},
};
</script>
<style scoped>
#confirm-context-list {
display: flex;
gap: 20px;
height: fit-content;
}
#confirm-context-list li {
display: flex;
flex-direction: column;
width: 320px;
}
#confirm-context-list .v-select {
width: 100%;
}
#confirm-context-list label {
width: 100%;
text-align: left;
padding: 0 0 4px 10px;
}
#confirm-context-list span {
width: 100%;
align-content: right;
padding-top: 4px;
}
#confirm-context-list :deep(.v-select ul) {
max-height: 120px;
}
</style>

View File

@ -0,0 +1,257 @@
<template>
<fieldset
class="rows"
v-if="(ruleSetInitialized && editMode === 'edit') || editMode === 'add'"
id="trigger-edit-form-general-section"
>
<legend v-if="editMode === 'add'">
{{ $__("Add new trigger") }}
{{ " " + triggerNumber }}
</legend>
<legend v-else>
{{ $__("Edit trigger") }} {{ " " + triggerNumber }}
</legend>
<div class="page-section bg-info">
<p>
{{
$__(
"NOTE: Delay for a given trigger can be pushed forward or backwards only within the bounds of what its two neighbouring triggers allows."
)
}}
</p>
</div>
<ol>
<li>
<label for="overdue_delay">{{ $__("Delay") }}: </label>
<div class="numeric-input-wrapper">
<div class="input-with-clear">
<input
@input="setAllowSubmission"
id="overdue_delay"
v-model="
ruleSetToSubmit[
`overdue_${triggerNumber}_delay`
]
"
type="number"
:placeholder="
fallbackRuleSet?.[
`overdue_${triggerNumber}_delay`
]
"
:min="minDelay"
:max="maxDelay"
class="numeric-input"
/>
<button
v-if="
ruleSetToSubmit[
`overdue_${triggerNumber}_delay`
] !== null &&
ruleSetToSubmit[
`overdue_${triggerNumber}_delay`
] !== undefined
"
type="button"
class="clear-btn"
@click="handleSetDelayToNull"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="10"
height="10"
>
<path
d="M6.895455 5l2.842897-2.842898c.348864-.348863.348864-.914488 0-1.263636L9.106534.261648c-.348864-.348864-.914489-.348864-1.263636 0L5 3.104545 2.157102.261648c-.348863-.348864-.914488-.348864-1.263636 0L.261648.893466c-.348864.348864-.348864.914489 0 1.263636L3.104545 5 .261648 7.842898c-.348864.348863-.348864.914488 0 1.263636l.631818.631818c.348864.348864.914773.348864 1.263636 0L5 6.895455l2.842898 2.842897c.348863.348864.914772.348864 1.263636 0l.631818-.631818c.348864-.348864.348864-.914489 0-1.263636L6.895455 5z"
></path>
</svg>
</button>
<div class="chevron-buttons">
<button
type="button"
class="increment-btn"
@click="incrementDelay"
>
▴
</button>
<button
type="button"
class="decrement-btn"
@click="decrementDelay"
>
▾
</button>
</div>
</div>
</div>
</li>
<li>
<label for="restricts">{{ $__("Restricts checkouts") }}:</label>
<div>
<input
@click="setAllowSubmission"
type="radio"
id="restricts-yes"
v-model="
ruleSetToSubmit[`overdue_${triggerNumber}_restrict`]
"
:value="1"
/>
{{ $__("Yes") }}
<input
@click="setAllowSubmission"
type="radio"
id="restricts-no"
v-model="
ruleSetToSubmit[`overdue_${triggerNumber}_restrict`]
"
:value="0"
/>
{{ $__("No") }}
<input
@click="setAllowSubmission"
type="radio"
id="restricts-fallback"
v-model="
ruleSetToSubmit[`overdue_${triggerNumber}_restrict`]
"
:value="null"
/>
{{ $__("Fallback to default") }}
<span
v-if="
fallbackRuleSet?.[
`overdue_${triggerNumber}_restrict`
] !== null
"
>
({{
fallbackRuleSet?.[
`overdue_${triggerNumber}_restrict`
] === "1"
? $__("Yes")
: $__("No")
}})
</span>
</div>
</li>
</ol>
</fieldset>
<div v-else-if="editMode === 'add' || editMode === 'edit'">
<p>{{ $__("Loading circulation rules...") }}</p>
</div>
</template>
<script>
export default {
props: {
ruleSetInitialized: { type: Boolean, required: true },
editMode: { type: [String, Boolean], required: true },
triggerNumber: { type: Number, required: true },
ruleSetToSubmit: { type: Object, default: null },
fallbackRuleSet: { type: Object, default: null },
minDelay: { type: Number, required: true },
maxDelay: { type: Number, required: true },
setAllowSubmission: { type: Function, required: true },
handleSetDelayToNull: { type: Function, required: true },
incrementDelay: { type: Function, required: true },
decrementDelay: { type: Function, required: true },
},
};
</script>
<style scoped>
li {
display: flex;
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;
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;
fill: var(--vs-controls-color);
background-color: transparent;
border: 0;
font-size: 1.2em;
color: #333;
cursor: pointer;
z-index: 2;
}
.button:active:hover,
.clear-btn:active:hover {
background-color: #d4d4d4;
border-color: #8c8c8c;
}
.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;
}
.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;
}
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;
}
.numeric-input:focus,
.numeric-input:hover {
border-color: #007bff;
outline: none;
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<fieldset
class="rows"
v-if="(ruleSetInitialized && editMode === 'edit') || editMode === 'add'"
id="trigger-edit-form-notice-section"
>
<legend v-if="ruleSetInfo.triggerCount < triggerNumber">
{{ $__("Notice for trigger") }}
{{ " " + triggerNumber }}
</legend>
<legend v-else>
{{ $__("Edit notice for trigger") }}
{{ " " + triggerNumber }}
</legend>
<ol>
<li>
<label for="letter_code">{{ $__("Letter") }}:</label>
<v-select
id="letter_code"
v-model="ruleSetToSubmit[`overdue_${triggerNumber}_notice`]"
label="name"
:reduce="type => type.code"
:options="filteredLetters"
@update:modelValue="setAllowSubmission"
>
<template #search="{ attributes, events }">
<input
class="vs__search"
v-bind="attributes"
v-on="events"
:placeholder="
ruleSetToSubmit[
`overdue_${triggerNumber}_notice`
] === null ||
ruleSetToSubmit[
`overdue_${triggerNumber}_notice`
] === undefined
? letters.find(
letter =>
letter.code ===
fallbackRuleSet?.[
`overdue_${triggerNumber}_notice`
]
)?.name ||
fallbackRuleSet?.[
`overdue_${triggerNumber}_notice`
]
: ''
"
/>
</template>
</v-select>
</li>
<li
v-if="
ruleSetToSubmit[`overdue_${triggerNumber}_notice`] !== '' &&
ruleSetToSubmit[`overdue_${triggerNumber}_notice`] !==
null &&
ruleSetToSubmit[`overdue_${triggerNumber}_notice`] !==
undefined
"
>
<label for="mtt">{{ $__("Transport type(s)") }}:</label>
<v-select
id="mtt"
v-model="ruleSetToSubmit[`overdue_${triggerNumber}_mtt`]"
label="name"
:reduce="type => type.code"
:options="transportTypes"
multiple
:required="true"
@update:modelValue="setAllowSubmission"
>
<template #search="{ attributes, events }">
<input
class="vs__search"
v-bind="attributes"
v-on="events"
:placeholder="
ruleSetToSubmit[
`overdue_${triggerNumber}_mtt`
] === null ||
ruleSetToSubmit[
`overdue_${triggerNumber}_mtt`
] === undefined ||
ruleSetToSubmit[`overdue_${triggerNumber}_mtt`]
.length === 0
? fallbackRuleSet?.[
`overdue_${triggerNumber}_mtt`
]
: ''
"
:required="
!ruleSetToSubmit[`overdue_${triggerNumber}_mtt`]
?.length
"
/>
</template>
</v-select>
<span class="required">{{ $__("Required") }}</span>
</li>
</ol>
</fieldset>
<div v-else-if="editMode === 'add' || editMode === 'edit'">
<p>{{ $__("Loading circulation rules...") }}</p>
</div>
</template>
<script>
export default {
props: {
ruleSetInitialized: { type: Boolean, required: true },
editMode: { type: [String, Boolean], required: true },
triggerNumber: { type: Number, required: true },
ruleSetToSubmit: { type: Object, default: null },
fallbackRuleSet: { type: Object, default: null },
ruleSetInfo: { type: Object, required: true },
filteredLetters: { type: Array, required: true },
letters: { type: Array, required: true },
transportTypes: { type: Array, required: true },
setAllowSubmission: { type: Function, required: true },
},
};
</script>

View File

@ -30,6 +30,7 @@ export default {
setup() {
const circRulesStore = inject("circRulesStore");
circRulesStore.init(default_view, logged_in_library_id);
letters.sort(circRulesStore.compareLetterNames);
letters.unshift({
name: "No letter",
code: "",

View File

@ -1,55 +0,0 @@
<template>
<div class="page-section">
<table>
<thead>
<th>
{{ $__("Loan period") }}
</th>
<th>
{{ $__("Unit") }}
</th>
<th>
{{ $__("Decreased loan period for high holds") }}
</th>
<th>
{{ $__("Fine amount") }}
</th>
<th>
{{ $__("Fine charging interval") }}
</th>
</thead>
<tbody>
<tr>
<td>
{{ ruleSetInfo.issuelength }}
</td>
<td>
{{ ruleSetInfo.lengthunit }}
</td>
<td>
{{ ruleSetInfo.decreaseloanholds }}
</td>
<td>
{{ ruleSetInfo.fine }}
</td>
<td>
{{ ruleSetInfo.chargeperiod }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
props: ["ruleSetInfo"],
};
</script>
<style scoped>
table {
margin: auto;
width: fit-content;
}
</style>

View File

@ -1,284 +1,295 @@
<template>
<slot></slot>
<table :class="{ centered: modal }">
<thead>
<th v-if="!modal" class="trigger_context">
{{ $__("Library") }}
</th>
<th v-if="!modal" class="trigger_context">
{{ $__("Patron category") }}
</th>
<th v-if="!modal" class="trigger_context border_right">
{{ $__("Item type") }}
</th>
<th v-if="modal">
{{ $__("Trigger") }}
</th>
<th>
{{ $__("Delay") }}
</th>
<th>
{{ $__("Notice") }}
</th>
<th>
{{ $__("Email") }}
</th>
<th>
{{ $__("Print") }}
</th>
<th>
{{ $__("SMS") }}
</th>
<th>
{{ $__("Restricts checkouts") }}
</th>
<th v-if="actions">
{{ $__("Actions") }}
</th>
</thead>
<tbody>
<template
v-for="(ruleSet, i) in ruleSets"
:key="'ruleSet' + i"
:class="{
selected_rule_set:
modal && i + 1 === parseInt(activeTriggerBeingEdited),
}"
>
<tr
v-if="
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_has_rules`
]
"
<fieldset
class="rows"
:id="modal ? 'trigger-table-form' : 'trigger-table-main'"
>
<legend>
{{ $__("Existing overdues triggers for this context") }}
</legend>
<table>
<thead>
<th v-if="!modal" class="trigger_context">
{{ $__("Library") }}
</th>
<th v-if="!modal" class="trigger_context">
{{ $__("Patron category") }}
</th>
<th v-if="!modal" class="trigger_context border_right">
{{ $__("Item type") }}
</th>
<th v-if="modal">
{{ $__("Trigger") }}
</th>
<th>
{{ $__("Delay") }}
</th>
<th>
{{ $__("Notice") }}
</th>
<th>
{{ $__("Email") }}
</th>
<th>
{{ $__("Print") }}
</th>
<th>
{{ $__("SMS") }}
</th>
<th>
{{ $__("Restricts checkouts") }}
</th>
<th v-if="actions">
{{ $__("Actions") }}
</th>
</thead>
<tbody>
<template
v-for="(ruleSet, i) in ruleSets"
:key="'ruleSet' + i"
:class="{
selected_rule_set:
modal &&
i + 1 === parseInt(activeTriggerBeingEdited),
}"
>
<td v-if="!modal" class="trigger_context">
{{
handleContext(
ruleSet.context.library_id,
libraries,
"library_id"
)
}}
</td>
<td v-if="!modal" class="trigger_context">
{{
handleContext(
ruleSet.context.patron_category_id,
patronCategories,
"patron_category_id"
)
}}
</td>
<td v-if="!modal" class="trigger_context border_right">
{{
handleContext(
ruleSet.context.item_type_id,
itemTypes,
"item_type_id",
"description"
)
}}
</td>
<td v-if="modal">
{{ i + 1 }}
</td>
<!-- Delay -->
<td>
<span
:class="{
fallback:
<tr
v-if="
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_has_rules`
]
"
>
<td v-if="!modal" class="trigger_context">
{{
handleContext(
ruleSet.context.library_id,
libraries,
"library_id"
)
}}
</td>
<td v-if="!modal" class="trigger_context">
{{
handleContext(
ruleSet.context.patron_category_id,
patronCategories,
"patron_category_id"
)
}}
</td>
<td v-if="!modal" class="trigger_context border_right">
{{
handleContext(
ruleSet.context.item_type_id,
itemTypes,
"item_type_id",
"description"
)
}}
</td>
<td v-if="modal">
{{ i + 1 }}
</td>
<!-- Delay -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_delay`
].isFallback,
}"
>
{{
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_delay`
].isFallback,
}"
>
{{
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_delay`
].value
}}
</span>
</td>
<!-- Notice -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].isFallback,
}"
>
{{
handleNotice(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].value
)
}}
</span>
</td>
<!-- Email -->
<td>
<span
:class="{
fallback:
}}
</span>
</td>
<!-- Notice -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].isFallback,
}"
>
{{
handleNotice(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].value
)
}}
</span>
</td>
<!-- Email -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].isFallback,
}"
>
{{
handleTransport(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].value,
"email",
!ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].value
)
}}
</span>
</td>
<!-- Print -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].isFallback,
}"
>
{{
handleTransport(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].value,
"print",
!ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].value
)
}}
</span>
</td>
<!-- SMS -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].isFallback,
}"
>
{{
handleTransport(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].value,
"sms",
!ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].value
)
}}
</span>
</td>
<!-- Restricts Checkouts -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_restrict`
].isFallback,
}"
>
{{
handleRestrictions(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_restrict`
].value
)
}}
</span>
</td>
<td class="actions" v-if="actions">
<router-link
:to="{
name: 'CirculationTriggersFormEdit',
query: {
library_id: ruleSet.context.library_id,
item_type_id:
ruleSet.context.item_type_id,
patron_category_id:
ruleSet.context.patron_category_id,
triggerNumber: modal
? i + 1
: triggerNumber,
},
}"
class="btn btn-default btn-xs"
><i class="fa-solid fa-pencil"></i>
{{ $__("Edit") }}</router-link
>
<router-link
v-if="
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].isFallback,
}"
>
{{
handleTransport(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].value,
"email",
!ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].value
)
}}
</span>
</td>
<!-- Print -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].isFallback,
}"
>
{{
handleTransport(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].value,
"print",
!ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].value
)
}}
</span>
</td>
<!-- SMS -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].isFallback,
}"
>
{{
handleTransport(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_mtt`
].value,
"sms",
!ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_notice`
].value
)
}}
</span>
</td>
<!-- Restricts Checkouts -->
<td>
<span
:class="{
fallback:
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_restrict`
].isFallback,
}"
>
{{
handleRestrictions(
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_restrict`
].value
)
}}
</span>
</td>
<td class="actions" v-if="actions">
`overdue_${modal ? i + 1 : triggerNumber}_has_rules`
].value &&
!isOnlyRuleSetForTrigger(triggerNumber)
"
:to="{
name: 'CirculationTriggersFormConfirmReset',
query: {
library_id: ruleSet.context.library_id,
item_type_id:
ruleSet.context.item_type_id,
patron_category_id:
ruleSet.context.patron_category_id,
triggerNumber: triggerNumber,
},
}"
class="btn btn-default btn-xs"
><i class="fa-solid fa-eraser"></i>
{{ $__("Reset") }}</router-link
>
</td>
</tr>
</template>
<tr v-if="modal">
<td colspan="7"></td>
<td class="actions">
<router-link
:to="{
name: 'CirculationTriggersFormEdit',
name: 'CirculationTriggersFormAdd',
query: {
library_id: ruleSet.context.library_id,
item_type_id: ruleSet.context.item_type_id,
library_id:
activeRuleSetBeingEdited.context
.library_id,
item_type_id:
activeRuleSetBeingEdited.context
.item_type_id,
patron_category_id:
ruleSet.context.patron_category_id,
triggerNumber: modal
? i + 1
: triggerNumber,
activeRuleSetBeingEdited.context
.patron_category_id,
triggerNumber:
triggerCounts[
activeRuleSetBeingEdited.context
.library_id
] + 1,
},
}"
class="btn btn-default btn-xs"
><i class="fa-solid fa-pencil"></i>
{{ $__("Edit") }}</router-link
>
<router-link
v-if="
ruleSet[
`overdue_${modal ? i + 1 : triggerNumber}_has_rules`
].value &&
!isOnlyRuleSetForTrigger(triggerNumber)
"
:to="{
name: 'CirculationTriggersFormConfirmReset',
query: {
library_id: ruleSet.context.library_id,
item_type_id: ruleSet.context.item_type_id,
patron_category_id:
ruleSet.context.patron_category_id,
triggerNumber: triggerNumber,
},
}"
class="btn btn-default btn-xs"
><i class="fa-solid fa-eraser"></i>
{{ $__("Reset") }}</router-link
{{ $__("Add") }}</router-link
>
</td>
</tr>
</template>
<tr v-if="modal">
<td colspan="7"></td>
<td class="actions">
<router-link
:to="{
name: 'CirculationTriggersFormAdd',
query: {
library_id:
activeRuleSetBeingEdited.context.library_id,
item_type_id:
activeRuleSetBeingEdited.context
.item_type_id,
patron_category_id:
activeRuleSetBeingEdited.context
.patron_category_id,
triggerNumber:
triggerCounts[
activeRuleSetBeingEdited.context
.library_id
] + 1,
},
}"
class="btn btn-default btn-xs"
><i class="fa-solid fa-pencil"></i>
{{ $__("Add") }}</router-link
>
</td>
</tr>
</tbody>
</table>
</tbody>
</table>
</fieldset>
</template>
<script>

View File

@ -75,6 +75,15 @@ export const useCircRulesStore = defineStore("circRules", () => {
await this.getConfigurationOptions();
},
// utilities
compareLetterNames(a, b) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
},
formatTriggerSpecificRuleSetForDisplay(
context,
triggerNumber,
@ -160,6 +169,23 @@ export const useCircRulesStore = defineStore("circRules", () => {
this.triggerCounts[this.currentLibraryId]
);
},
async scrollToElementById(id) {
let count = 0;
// ensures that the relevant section is loaded before we attempt to scroll it into view
while (
!document.getElementById(id) &&
count < 8
) {
await new Promise(resolve => setTimeout(resolve, 250));
count++;
}
const element = document.getElementById(id);
if (!element) {
// handle loading the page if the element is not at all present
return;
}
element.scrollIntoView({ behavior: "smooth" });
},
// services
formatMttForDisplay(rawMtt) {
return rawMtt?.split(",");

View File

@ -192,7 +192,7 @@ describe("Circulation Triggers - Initial Load", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("#circ_triggers_tabs").should("exist");
cy.get("#circ-triggers-tabs").should("exist");
cy.get(".nav-link").contains("Trigger 1").should("exist");
cy.get(".nav-link").contains("Trigger 2").should("exist");
});