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.