PageSourceSearch

https://platform.isms.online/assets/controllers/ticket_queues/filters_controller-1cd0e3fc.js

js isms.online collected 2026-09-24 15:36:08 UTC 9,804 bytes, 359 lines download raw bytes

1import {Controller} from "@hotwired/stimulus"
2import {
3  applyCategoryFilterState,
4  applyDateFilterState,
5  bindCategoryFilterEvents,
6  clearCategoryFilterState,
7  clearDateFilterSelection,
8  filterIsSet,
9  initDatepickers,
10  isDateField,
11  parseSearchParams,
12  saveSetFiltersToDB,
13  readDateFilterValue,
14  readCategoryFilterState,
15  syncDateFilterSelection
16} from "helpers/ticket_queues/filters"
17
18const DEFAULTS = {
19  responsibleUserId: "",
20  creatorId: "",
21  statusId: "",
22  categoryIds: [],
23  categoryGroups: [],
24  categoryFilterType: "any_of",
25  searchTerm: "",
26  from: "",
27  to: "",
28  overdue: "0"
29}
30
31const FILTERS = [
32  "responsibleUserId",
33  "creatorId",
34  "statusId",
35  "searchTerm",
36  "overdue"
37]
38
39const DATE_FILTERS = ["from", "to"]
40const ARRAY_FILTERS = ["categoryIds", "categoryGroups"]
41
42export default class extends Controller {
43  static targets = [
44    "tokenList",
45    "clearAllButton",
46    "filterPanel",
47    "filterPanelToggle",
48    ...FILTERS,
49    ...DATE_FILTERS,
50    "categoryIds",
51    "viewButton"
52  ]
53
54  static values = {
55    defaultViewType: String,
56    initialFilters: Object,
57    queueId: Number
58  }
59
60  connect() {
61    this.unbindCategoryEvents = bindCategoryFilterEvents(this.element, (categoryState) => {
62      this.selectedFilters = {
63        ...this.selectedFilters,
64        ...categoryState
65      }
66      this.#updateFiltersInUI()
67      this.#emitFiltersChanged()
68    })
69    initDatepickers({
70      fromField: this.fromTarget,
71      toField: this.toTarget,
72      onChange: (field) => this.#applyDateSelection(field)
73    })
74
75    this.selectedFilters = this.#getInitialFilterState()
76    this.#updateFiltersInUI({ onPageLoad: true })
77    this.#emitFiltersChanged({ onPageLoad: true })
78  }
79
80  disconnect() {
81    this.unbindCategoryEvents?.()
82  }
83
84  apply(event) {
85    if (isDateField(event.target)) {
86      syncDateFilterSelection(event.target)
87    }
88
89    this.#updateFilters()
90  }
91
92  submitSearch(event) {
93    event.preventDefault()
94    this.#updateFilters()
95  }
96
97  clearSearch(event) {
98    event.preventDefault()
99    this.searchTermTarget.value = ""
100    this.#updateFilters()
101  }
102
103  clearAll(event) {
104    event.preventDefault()
105    clearCategoryFilterState(this.element)
106    DATE_FILTERS.forEach((filterKey) => {
107      clearDateFilterSelection(this.#targetForFilterKey(filterKey))
108    })
109    FILTERS.forEach((filterKey) => {
110      this.#targetForFilterKey(filterKey).value = DEFAULTS[filterKey]
111    })
112
113    this.#updateFilters({ forceReload: false })
114  }
115
116  clearField(event) {
117    event.preventDefault()
118    this.#clearFilter(event.params.filterKey)
119  }
120
121  clearToken(event) {
122    event.preventDefault()
123    this.#clearFilter(event.params.filterKey)
124  }
125
126  toggleView(event) {
127    event.preventDefault()
128
129    this.selectedFilters = {
130      ...this.selectedFilters,
131      viewType: event.currentTarget.dataset.viewType
132    }
133    this.#updateFiltersInUI()
134    this.#emitFiltersChanged({ forceReload: true })
135  }
136  #applyDateSelection(field) {
137    syncDateFilterSelection(field)
138    this.#updateFilters()
139  }
140
141  #getInitialFilterState() {
142    const urlFilters = parseSearchParams(new URL(window.location).searchParams, {
143      allowedKeys: [...Object.keys(DEFAULTS), "viewType", "unfiltered"],
144      keyAliases: {
145        filter_responsibleUserId: "responsibleUserId",
146        filter_creatorId: "creatorId",
147        filter_statusId: "statusId",
148        ticket_queues_filter_by_category: "categoryIds",
149        search_term: "searchTerm",
150      },
151      arrayKeys: ["categoryIds"]
152    })
153    const savedFilters = this.#standardizeFilterValues(this.initialFiltersValue || {})
154    const defaultViewType = savedFilters.viewType || this.defaultViewTypeValue || "columns"
155
156    if (urlFilters.unfiltered) {
157      return {
158        ...this.#defaultSelectedFilters(),
159        viewType: defaultViewType
160      }
161    }
162
163    const selectedFilters = this.#selectedFiltersFromSavedAndUrlFilters(savedFilters, urlFilters)
164
165    return {
166      ...selectedFilters,
167      viewType: selectedFilters.viewType || defaultViewType
168    }
169  }
170
171  #updateFilters({ forceReload = false, onPageLoad = false } = {}) {
172    this.selectedFilters = this.#getCurrentlySelectedFilters()
173    this.#updateFiltersInUI({ onPageLoad })
174    this.#emitFiltersChanged({ forceReload, onPageLoad })
175  }
176
177  #selectedFiltersFromSavedAndUrlFilters(savedFilters, urlFilters) {
178    return Object.keys(urlFilters).some((key) => key !== "unfiltered")
179      ? {
180        ...savedFilters,
181        ...urlFilters
182      }
183      : savedFilters
184  }
185
186  #getCurrentlySelectedFilters() {
187    const targetValueFilters = FILTERS.reduce((filters, filterKey) => {
188      filters[filterKey] = this.#targetForFilterKey(filterKey).value
189      return filters
190    }, {})
191    const dateFilters = DATE_FILTERS.reduce((filters, filterKey) => {
192      filters[filterKey] = readDateFilterValue(this.#targetForFilterKey(filterKey))
193      return filters
194    }, {})
195
196    return {
197      ...this.#defaultSelectedFilters(),
198      ...targetValueFilters,
199      ...dateFilters,
200      viewType: this.selectedFilters.viewType || this.defaultViewTypeValue || "columns",
201      ...readCategoryFilterState(this.element)
202    }
203  }
204
205  #updateFiltersInUI({ onPageLoad = false } = {}) {
206    FILTERS.forEach((filterKey) => {
207      this.#targetForFilterKey(filterKey).value = this.selectedFilters[filterKey] || DEFAULTS[filterKey]
208    })
209    DATE_FILTERS.forEach((filterKey) => {
210      applyDateFilterState(this.#targetForFilterKey(filterKey), this.selectedFilters[filterKey])
211    })
212    applyCategoryFilterState(this.element, this.selectedFilters)
213    this.#toggleViewButtons()
214    this.#updateSetFiltersUI({ onPageLoad })
215  }
216
217  #emitFiltersChanged({ forceReload = false, onPageLoad = false } = {}) {
218    saveSetFiltersToDB(this.queueIdValue, this.#filtersForUserViewSettings())
219    document.dispatchEvent(new CustomEvent("ticket-queues:filters-changed", {
220      detail: {
221        forceReload,
222        onPageLoad,
223        selectedFilters: { ...this.selectedFilters }
224      }
225    }))
226  }
227
228  #clearFilter(filterKey) {
229    if (!filterKey) return
230
231    if (filterKey === "categoryIds") {
232      clearCategoryFilterState(this.element)
233      this.#updateFilters()
234      return
235    }
236
237    if (filterKey === "from" || filterKey === "to") {
238      clearDateFilterSelection(this[`${filterKey}Target`])
239      this.#updateFilters()
240      return
241    }
242
243    if (filterKey === "searchTerm") {
244      this.searchTermTarget.value = ""
245    } else {
246      const target = this[`${filterKey}Target`]
247      if (!target) return
248      target.value = DEFAULTS[filterKey] || ""
249    }
250
251    this.#updateFilters()
252  }
253
254  #toggleViewButtons() {
255    this.viewButtonTargets.forEach((button) => {
256      const isActive = button.dataset.viewType === this.selectedFilters.viewType
257      button.classList.toggle("active", isActive)
258      button.setAttribute("aria-current", isActive ? "page" : "false")
259    })
260  }
261
262  #updateSetFiltersUI({ onPageLoad = false } = {}) {
263    const setFilters = this.#getActiveFilters()
264    const activeCount = Object.keys(setFilters).length
265
266    this.#renderSetFilterTokens(setFilters)
267    this.clearAllButtonTargets.forEach((button) => {
268      button.style.display = activeCount > 1 ? "" : "none"
269    })
270
271    if (onPageLoad && activeCount > 0) {
272      this.#openTogglePanel()
273    }
274  }
275
276  #openTogglePanel() {
277    const panelIsHidden = this.filterPanelTarget.ariaHidden === "true" || this.filterPanelTarget.style.display === "none"
278    if (!panelIsHidden) return
279
280    this.filterPanelToggleTarget.click()
281  }
282
283  #renderSetFilterTokens(setFilters) {
284    this.tokenListTarget.replaceChildren()
285
286    const template = _.template(document.getElementById("filter_token_template").innerHTML)
287
288    Object.entries(setFilters).forEach(([name, value]) => {
289      this.tokenListTarget.insertAdjacentHTML("beforeend", template({
290        label: this.#labelFor(name),
291        name,
292        value
293      }))
294    })
295  }
296
297  #getActiveFilters() {
298    const filtersWithoutClearButtons = ["categoryFilterType", "categoryGroups", "viewType"]
299
300    return Object.entries(this.selectedFilters).reduce((filters, [key, value]) => {
301      if (filtersWithoutClearButtons.includes(key) || !filterIsSet(value)) return filters
302      filters[key] = value
303      return filters
304    }, {})
305  }
306
307  #labelFor(filterKey) {
308    const labelTarget = filterKey === "categoryIds" && this.hasCategoryIdsTarget
309      ? this.categoryIdsTarget
310      : this.#targetForFilterKey(filterKey)
311
312    return labelTarget?.dataset.filterLabel || filterKey
313  }
314
315  #defaultSelectedFilters() {
316    return {
317      ...DEFAULTS,
318      viewType: this.defaultViewTypeValue || "columns"
319    }
320  }
321  #standardizeFilterValues(filters = {}) {
322    const storedFilters = this.#defaultSelectedFilters()
323    const storedStringFilters = [...FILTERS, ...DATE_FILTERS, "categoryFilterType", "viewType"]
324
325    storedStringFilters.forEach((filterKey) => {
326      storedFilters[filterKey] = this.#stringFilterValue(filters[filterKey], storedFilters[filterKey])
327    })
328    ARRAY_FILTERS.forEach((filterKey) => {
329      storedFilters[filterKey] = this.#arrayFilterValue(filters[filterKey])
330    })
331
332    return storedFilters
333  }
334
335  #filtersForUserViewSettings() {
336    return Object.keys({
337      ...DEFAULTS,
338      viewType: this.defaultViewTypeValue || "columns"
339    }).reduce((filters, key) => {
340      filters[key] = this.selectedFilters[key]
341      return filters
342    }, {})
343  }
344
345  #stringFilterValue(value, fallback = "") {
346    return value == null ? fallback : value.toString()
347  }
348
349  #arrayFilterValue(value) {
350    if (Array.isArray(value)) return value.map((item) => item.toString())
351    if (value == null || value === "") return []
352
353    return value.toString().split(",").filter(Boolean)
354  }
355
356  #targetForFilterKey(filterKey) {
357    return this[`${filterKey}Target`]
358  }
359}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.