diff --git a/frontend/src/app.js b/frontend/src/app.js index acbc4702a..6aa438405 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -4,9 +4,6 @@ import { I18n } from './services/i18n/i18n'; import { UtilRegistry } from './services/util-registry/util-registry'; import { isValidUtility } from './core/utility'; -// load window.fetch polyfill -import 'whatwg-fetch'; - import './app.scss'; export class App { diff --git a/frontend/src/app.spec.js b/frontend/src/app.spec.js index 247be9f00..e682f0e28 100644 --- a/frontend/src/app.spec.js +++ b/frontend/src/app.spec.js @@ -1,3 +1,5 @@ +/* eslint-env node */ + import { App } from './app'; import { Utility } from './core/utility'; @@ -13,52 +15,50 @@ const TEST_UTILS = [ ]; describe('App', () => { - let app; - beforeEach(() => { - app = new App(); + global.App = new App(); }); it('should create', () => { - expect(app).toBeTruthy(); + expect(global.App).toBeTruthy(); }); it('should setup all utlites when page is done loading', () => { - spyOn(app.utilRegistry, 'setupAll'); + spyOn(global.App.utilRegistry, 'setupAll'); document.dispatchEvent(new Event('DOMContentLoaded')); - expect(app.utilRegistry.setupAll).toHaveBeenCalled(); + expect(global.App.utilRegistry.setupAll).toHaveBeenCalled(); }); describe('provides services', () => { it('HttpClient as httpClient', () => { - expect(app.httpClient).toBeTruthy(); + expect(global.App.httpClient).toBeTruthy(); }); it('HtmlHelpers as htmlHelpers', () => { - expect(app.htmlHelpers).toBeTruthy(); + expect(global.App.htmlHelpers).toBeTruthy(); }); it('I18n as i18n', () => { - expect(app.i18n).toBeTruthy(); + expect(global.App.i18n).toBeTruthy(); }); it('UtilRegistry as utilRegistry', () => { - expect(app.utilRegistry).toBeTruthy(); + expect(global.App.utilRegistry).toBeTruthy(); }); }); describe('registerUtilities()', () => { it('should register the given utilities', () => { - spyOn(app.utilRegistry, 'register'); - app.registerUtilities(TEST_UTILS); - expect(app.utilRegistry.register.calls.count()).toBe(TEST_UTILS.length); - expect(app.utilRegistry.register.calls.argsFor(0)).toEqual([TEST_UTILS[0]]); - expect(app.utilRegistry.register.calls.argsFor(1)).toEqual([TEST_UTILS[1]]); + spyOn(global.App.utilRegistry, 'register'); + global.App.registerUtilities(TEST_UTILS); + expect(global.App.utilRegistry.register.calls.count()).toBe(TEST_UTILS.length); + expect(global.App.utilRegistry.register.calls.argsFor(0)).toEqual([TEST_UTILS[0]]); + expect(global.App.utilRegistry.register.calls.argsFor(1)).toEqual([TEST_UTILS[1]]); }); it('should throw an error if not passed an array of utilities', () => { expect(() => { - app.registerUtilities({}); + global.App.registerUtilities({}); }).toThrow(); }); }); diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js new file mode 100644 index 000000000..9b6202329 --- /dev/null +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -0,0 +1,223 @@ +/* global global:writable */ + +import * as semver from 'semver'; + +export const LOCATION = { + LOCAL: 'local', + WINDOW: 'window', +}; + +const LOCATION_SHADOWING = [ LOCATION.WINDOW, LOCATION.LOCAL ]; + +export class StorageManager { + + namespace; + version; + _options; + _global; + + constructor(namespace, version, options) { + this.namespace = namespace; + this.version = semver.valid(version); + + if (!namespace) { + throw new Error('Cannot setup StorageManager without namespace'); + } + if (!this.version) { + throw new Error('Cannot setup StorageManager without valid semver version'); + } + + if (options !== undefined) { + this._options = options; + } + + if (global !== undefined) + this._global = global; + else if (window !== undefined) + this._global = window; + else + throw new Error('Cannot setup StorageManager without window or global'); + } + + save(key, value, options=this._options) { + if (!key) { + throw new Error('StorageManager.save called with invalid key'); + } + + if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { + throw new Error('StorageManager.save called with unsupported location option'); + } + + const location = options && options.location !== undefined ? options.location : LOCATION_SHADOWING[0]; + + switch (location) { + case LOCATION.LOCAL: { + this._saveToLocalStorage({ ...this._getFromLocalStorage(), [key]: value }); + break; + } + case LOCATION.WINDOW: { + this._saveToWindow({ ...this._getFromLocalStorage(), [key]: value }); + break; + } + default: + console.error('StorageManager.save cannot save item with unsupported location'); + } + } + + load(key, options=this._options) { + if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { + throw new Error('StorageManager.load called with unsupported location option'); + } + + let locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING; + + while (locations.length > 0) { + const location = locations.shift(); + let val; + + switch (location) { + case LOCATION.LOCAL: { + val = this._getFromLocalStorage()[key]; + break; + } + case LOCATION.WINDOW: { + val = this._getFromWindow()[key]; + break; + } + default: + console.error('StorageManager.load cannot load item with unsupported location'); + } + + if (val !== undefined || locations.length === 0) { + return val; + } + } + } + + remove(key, options=this._options) { + if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { + throw new Error('StorageManager.load called with unsupported location option'); + } + + const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING; + + for (const location of locations) { + switch (location) { + case LOCATION.LOCAL: { + let val = this._getFromLocalStorage(); + + delete val[key]; + + return this._saveToLocalStorage(val); + } + case LOCATION.WINDOW: { + let val = this._getFromWindow(); + + delete val[key]; + + return this._saveToWindow(val); + } + default: + console.error('StorageManager.load cannot load item with unsupported location'); + } + } + } + + clear(options) { + if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { + throw new Error('StorageManager.clear called with unsupported location option'); + } + + const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING; + + for (const location of locations) { + switch (location) { + case LOCATION.LOCAL: + return this._clearLocalStorage(); + case LOCATION.WINDOW: + return this._clearWindow(); + default: + console.error('StorageManager.clear cannot clear with unsupported location'); + } + } + + } + + + _getFromLocalStorage() { + let state; + + try { + state = JSON.parse(window.localStorage.getItem(this.namespace)); + } catch { + state = null; + } + + if (state === null || !state.version || !semver.satisfies(this.version, `^${state.version}`)) { + // remove item from localStorage if it stores an invalid state + this._clearLocalStorage(); + return {}; + } + + if ('state' in state) + return state.state; + else { + delete state.version; + return state; + } + } + + _saveToLocalStorage(state) { + if (!state) + return this._clearLocalStorage(); + + let versionedState; + + if ('version' in state || 'state' in state) { + versionedState = { version: this.version, state: state }; + } else { + versionedState = { version: this.version, ...state }; + } + + window.localStorage.setItem(this.namespace, JSON.stringify(versionedState)); + } + + _clearLocalStorage() { + window.localStorage.removeItem(this.namespace); + } + + + _getFromWindow() { + if (!this._global || !this._global.App) + return {}; + + if (!this._global.App.Storage) + this._global.App.Storage = {}; + + return this._global.App.Storage[this.namespace] || {}; + } + + _saveToWindow(value) { + if (!this._global || !this._global.App) { + throw new Error('StorageManager._saveToWindow called when window.App is not available'); + } + + if (!value) + return this._clearWindow(); + + if (!this._global.App.Storage) + this._global.App.Storage = {}; + + this._global.App.Storage[this.namespace] = value; + } + + _clearWindow() { + if (!this._global || !this._global.App) { + throw new Error('StorageManager._saveToWindow called when window.App is not available'); + } + + if (this._global.App.Storage) { + delete this._global.App.Storage[this.namespace]; + } + } +} diff --git a/frontend/src/polyfill.js b/frontend/src/polyfill.js new file mode 100644 index 000000000..27333c1a0 --- /dev/null +++ b/frontend/src/polyfill.js @@ -0,0 +1,4 @@ +import 'whatwg-fetch'; + +import { ResizeObserver as Polyfill } from '@juggle/resize-observer'; +window.ResizeObserver = window.ResizeObserver || Polyfill; diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index b8330aa4b..80dadca3a 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import { Datepicker } from '../form/datepicker'; import { HttpClient } from '../../services/http-client/http-client'; import * as debounce from 'lodash.debounce'; @@ -40,6 +41,8 @@ export class AsyncTable { }; _ignoreRequest = false; + _storageManager = new StorageManager(ASYNC_TABLE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.WINDOW }); + constructor(element, app) { if (!element) { throw new Error('Async Table utility cannot be setup without an element!'); @@ -81,10 +84,10 @@ export class AsyncTable { this._setupPageSizeSelect(); this._setupTableFilter(); - this._processLocalStorage(); + this._processStorage(); // clear currentTableUrl from previous requests - setLocalStorageParameter('currentTableUrl', null); + this._storageManager.remove('currentTableUrl'); // mark initialized this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS); @@ -100,7 +103,7 @@ export class AsyncTable { this._ths.forEach((th) => { th.clickHandler = (event) => { - setLocalStorageParameter('horizPos', (this._scrollTable || {}).scrollLeft); + this._storageManager.save('horizPos', (this._scrollTable || {}).scrollLeft); this._linkClickHandler(event); }; th.element.addEventListener('click', th.clickHandler); @@ -122,7 +125,7 @@ export class AsyncTable { left: this._scrollTable.offsetLeft || 0, behavior: 'smooth', }; - setLocalStorageParameter('scrollTo', scrollTo); + this._storageManager.save('scrollTo', scrollTo); } this._linkClickHandler(event); }; @@ -225,7 +228,7 @@ export class AsyncTable { const prefix = findCssIdPrefix(focusedInput.id); const focusId = focusedInput.id.replace(prefix, ''); callback = function(wrapper) { - const idPrefix = getLocalStorageParameter('cssIdPrefix'); + const idPrefix = this._storageManager.load('cssIdPrefix'); const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId); if (toBeFocused) { toBeFocused.focus(); @@ -238,7 +241,7 @@ export class AsyncTable { } _serializeTableFilterToURL(tableFilterForm) { - const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href); + const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href); // create new FormData and format any date values const formData = Datepicker.unformatAll(this._massInputForm, new FormData(tableFilterForm)); @@ -254,18 +257,18 @@ export class AsyncTable { return url; } - _processLocalStorage() { - const scrollTo = getLocalStorageParameter('scrollTo'); + _processStorage() { + const scrollTo = this._storageManager.load('scrollTo'); if (scrollTo && this._scrollTable) { window.scrollTo(scrollTo); } - setLocalStorageParameter('scrollTo', null); + this._storageManager.remove('scrollTo'); - const horizPos = getLocalStorageParameter('horizPos'); + const horizPos = this._storageManager.load('horizPos'); if (horizPos && this._scrollTable) { this._scrollTable.scrollLeft = horizPos; } - setLocalStorageParameter('horizPos', null); + this._storageManager.remove('horizPos'); } _removeListeners() { @@ -300,7 +303,7 @@ export class AsyncTable { } _changePagesizeHandler = () => { - const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href); + const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href); // create new FormData and format any date values const formData = Datepicker.unformatAll(this._pagesizeForm, new FormData(this._pagesizeForm)); @@ -336,7 +339,7 @@ export class AsyncTable { return false; } - setLocalStorageParameter('currentTableUrl', url.href); + this._storageManager.save('currentTableUrl', url.href); // reset table this._removeListeners(); this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); @@ -346,9 +349,9 @@ export class AsyncTable { this._app.utilRegistry.setupAll(this._element); if (callback && typeof callback === 'function') { - setLocalStorageParameter('cssIdPrefix', response.idPrefix); + this._storageManager.save('cssIdPrefix', response.idPrefix); callback(this._element); - setLocalStorageParameter('cssIdPrefix', ''); + this._storageManager.remove('cssIdPrefix'); } }).catch((err) => console.error(err) ).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS)); @@ -365,17 +368,3 @@ function findCssIdPrefix(id) { } return ''; } - -function setLocalStorageParameter(key, value) { - const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {}; - if (value !== null) { - currentLSState[key] = value; - } else { - delete currentLSState[key]; - } - window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState)); -} -function getLocalStorageParameter(key) { - const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {}; - return currentLSState[key]; -} diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js new file mode 100644 index 000000000..acedc2375 --- /dev/null +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -0,0 +1,312 @@ +import { Utility } from '../../core/utility'; +import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; +import './hide-columns.scss'; + +const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; +const TABLE_HEADER_IDENT = 'uw-hide-column-header'; + +const TABLE_UTILS_ATTR = 'table-utils'; +const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; + +const TABLE_HIDER_CLASS = 'table-hider'; +const TABLE_HIDER_VISIBLE_CLASS = 'table-hider--visible'; + +const TABLE_PILL_CLASS = 'table-pill'; + +const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell'; +const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan'; + +@Utility({ + selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`, +}) +export class HideColumns { + + _storageManager = new StorageManager('HIDE_COLUMNS', '1.0.0', { location: LOCATION.LOCAL }); + + _element; + _elementWrapper; + _tableUtilContainer; + + _autoHide; + + headerToHider = new Map(); + hiderToHeader = new Map(); + + addHeaderHider(th, hider) { + this.headerToHider.set(th, hider); + this.hiderToHeader.set(hider, th); + } + + constructor(element) { + this._autoHide = this._storageManager.load('autoHide', {}) || false; + + if (!element) { + throw new Error('Hide Columns utility cannot be setup without an element!'); + } + + // do not provide hide-column ability in tables inside modals or async forms with response + if (element.closest('[uw-modal], .async-form__response')) { + return false; + } + + this._element = element; + + const hideColumnsContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}]`); + if (!hideColumnsContainer) { + throw new Error('Hide Columns utility needs to be setup on a table inside a hide columns container!'); + } + this._elementWrapper = hideColumnsContainer; + + // get or create table utils container + this._tableUtilContainer = hideColumnsContainer.querySelector(TABLE_UTILS_CONTAINER_SELECTOR); + if (!this._tableUtilContainer) { + this._tableUtilContainer = document.createElement('div'); + this._tableUtilContainer.setAttribute(TABLE_UTILS_ATTR, ''); + const tableContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}] > *`); + hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer); + } + + this._element.querySelectorAll('th').forEach(th => this.setupHideButton(th)); + } + + setupHideButton(th) { + const preHidden = this.isHiddenColumn(th); + + const hider = document.createElement('span'); + + const hiderIcon = document.createElement('i'); + hiderIcon.classList.add('fas', 'fa-fw'); + hider.appendChild(hiderIcon); + + const hiderContent = document.createElement('span'); + hiderContent.classList.add('table-hider__label'); + hiderContent.innerHTML = th.innerText; + hider.appendChild(hiderContent); + + this.addHeaderHider(th, hider); + + th.addEventListener('mouseover', () => { + hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); + }); + th.addEventListener('mouseout', () => { + if (hider.classList.contains(TABLE_HIDER_CLASS)) { + hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); + } + }); + + hider.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + this.switchColumnDisplay(th, hider); + // this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); + }); + + hider.addEventListener('mouseover', () => { + hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); + const currentlyHidden = this.isHiddenColumn(th); + this.updateHiderIcon(hider, !currentlyHidden); + }); + hider.addEventListener('mouseout', () => { + if (hider.classList.contains(TABLE_HIDER_CLASS)) { + hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); + } + const currentlyHidden = this.isHiddenColumn(th); + this.updateHiderIcon(hider, currentlyHidden); + }); + + new ResizeObserver(() => { this.repositionHider(hider); }).observe(th); + + // reposition hider on each window resize event + // window.addEventListener('resize', () => this.repositionHider(hider)); + + this.updateColumnDisplay(this.colIndex(th), preHidden); + this.updateHider(hider, preHidden); + + if (preHidden) { + this._tableUtilContainer.appendChild(hider); + } else { + this.hideHiderBehindHeader(hider); + } + } + + switchColumnDisplay(th, hider) { + const hidden = !this.isHiddenColumn(th); + const originalColspan = Math.max(1, th.getAttribute(CELL_ORIGINAL_COLSPAN)) || 1; + const colspan = Math.max(1, th.colSpan) || 1; + const columnIndex = this.colIndex(th); + + for (var i = 0; i < Math.max(colspan, originalColspan); i++) { + this.updateColumnDisplay(columnIndex + i, hidden); + } + this.updateHider(hider, hidden); + + // persist new hidden setting for column + if ((hidden && this.isEmptyColumn(columnIndex) && this._autoHide) || (!hidden && (!this.isEmptyColumn(columnIndex) || !this._autoHide))) { + this._storageManager.remove(this.getStorageKey(th)); + } else { + this._storageManager.save(this.getStorageKey(th), hidden); + } + } + + updateColumnDisplay(columnIndex, hidden) { + this._element.getElementsByTagName('tr').forEach(row => { + const cell = this.getCol(row, columnIndex); + + if (cell) { + const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN); + const colspan = Math.max(1, cell.colSpan) || 1; + + if (hidden) { + if (colspan > 1) { + if (!originalColspan) { + cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan); + } + cell.colSpan--; + } else { + cell.classList.add(CELL_HIDDEN_CLASS); + } + } else { + if (cell.classList.contains(CELL_HIDDEN_CLASS)) { + cell.classList.remove(CELL_HIDDEN_CLASS); + } else if (originalColspan && colspan < originalColspan) { + cell.colSpan++; + } + } + } + }); + } + + updateHider(hider, hidden) { + if (hidden) { + hider.classList.remove(TABLE_HIDER_CLASS); + hider.classList.add(TABLE_PILL_CLASS); + this._tableUtilContainer.appendChild(hider); + } else { + hider.classList.remove(TABLE_PILL_CLASS); + hider.classList.add(TABLE_HIDER_CLASS); + this.hideHiderBehindHeader(hider); + } + this.updateHiderIcon(hider, hidden); + } + + updateHiderIcon(hider, hidden) { + hider.getElementsByClassName('fas').forEach(hiderIcon => { + hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash'); + hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye'); + }); + } + + hideHiderBehindHeader(hider) { + if (!this.hiderToHeader.get(hider).contains(hider)) { + this.hiderToHeader.get(hider).appendChild(hider); + } + + this.repositionHider(hider); + + // remove visible class if necessary + hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); + } + + repositionHider(hider) { + const thR = this.hiderToHeader.get(hider).getBoundingClientRect(), + hR = hider.getBoundingClientRect(); + + hider.style.left = (thR.width/2 - hR.width/2) + 'px'; + hider.style.top = thR.height + 'px'; + } + + getStorageKey(th) { + // get handler name + const handlerIdent = document.querySelector('[uw-handler]').getAttribute('uw-handler'); + + // get hide-columns container ident (if not present, use table index in document as fallback) + let tIdent = this._elementWrapper.getAttribute(HIDE_COLUMNS_CONTAINER_IDENT); + if (!tIdent) { + const tablesInDocument = document.getElementsByTagName('TABLE'); + for (let i = 0; i < tablesInDocument.length; i++) { + if (tablesInDocument[i] === this._element) { + tIdent = i; + break; + } + } + } + + // check for unique table header ident from backend (if not present, use cell index as fallback) + let thIdent = th.getAttribute(TABLE_HEADER_IDENT); + if (!thIdent) { + thIdent = this.colIndex(th); + } + + return `${handlerIdent}__${tIdent}__${thIdent}`; + } + + isEmptyColumn(columnIndex) { + for (let row of this._element.getElementsByTagName('tr')) { + const cell = this.getCol(row, columnIndex); + if (cell.matches('th')) + continue; + if (cell.querySelector('.table__td-content')) { + for (let child of cell.children) { + if (!isEmptyElement(child)) + return false; + } + return true; + } else { + return isEmptyElement(cell); + } + } + } + + isHiddenColumn(th) { + const hidden = this._storageManager.load(this.getStorageKey(th)), + emptyColumn = this.isEmptyColumn(this.colIndex(th)); + return hidden === true || hidden === undefined && emptyColumn && this._autoHide; + } + + colSpan(cell) { + if (!cell) + return 1; + + const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN); + const colspan = Math.max(1, cell.colSpan) || 1; + + return originalColspan ? Math.max(colspan, originalColspan) : colspan; + } + + colIndex(cell) { + if (!cell) + return 0; + + const rowParent = cell.closest('tr'); + + if (!rowParent) + return 0; + + var i = 0; + for (const sibling of Array.from(rowParent.cells).slice(0, cell.cellIndex)) { + i += this.colSpan(sibling); + } + + return i; + } + + getCol(row, columnIndex) { + var c = 0; + + for (const cell of row.cells) { + c += cell ? this.colSpan(cell) : 1; + + if (columnIndex < c) + return cell; + } + } +} + +function isEmptyElement(element) { + for (let child of element.childNodes) { + if (child.nodeName !== '#comment') + return false; + } + return true; +} + diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss new file mode 100644 index 000000000..59aab069d --- /dev/null +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -0,0 +1,71 @@ +.table-hider { + background-color: #fff; + color: var(--color-link); + padding: 10px; + cursor: pointer; + box-shadow: 0 0 2px 0 rgba(0,0,0,0.6); + position: absolute; + overflow: hidden; + transition: transform .2s ease; + transform: scaleY(0); + transform-origin: top; + + &:hover { + background-color: var(--color-grey-light); + } + + .table-hider__label { + display: none; + } + + &.table-hider--visible { + transform: scaleY(.4); + transition: none; /* TODO find better way to prevent transition on icons */ + + .fas { + transform: scaleY(2.5); + } + + &:hover { + transform: scaleY(1); + + .fas { + transform: scaleY(1); + } + } + } +} + +[table-utils] { + max-width: 85vw; + margin-bottom: 10px; + min-height: 0; + line-height: 1.4; + + .table-pill { + background-color: var(--color-dark); + float: left; + color: #fff; + padding: 10px; + border-radius: 20px / 50%; + margin-right: 20px; + margin-bottom: 10px; + cursor: pointer; + + .table-hider__label { + font-size: 16px; + font-weight: bold; + margin-left: 5px; + } + } + + &:after { + content: ""; + display: block; + clear: both; + } +} + +.hide-columns--hidden-cell { + display: none; +} diff --git a/frontend/src/utils/show-hide/show-hide.js b/frontend/src/utils/show-hide/show-hide.js index 0780a465d..aa1f4f792 100644 --- a/frontend/src/utils/show-hide/show-hide.js +++ b/frontend/src/utils/show-hide/show-hide.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import './show-hide.scss'; const SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE'; @@ -15,6 +16,8 @@ export class ShowHide { _showHideId; _element; + _storageManager = new StorageManager(SHOW_HIDE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.LOCAL }); + constructor(element) { if (!element) { throw new Error('ShowHide utility cannot be setup without an element!'); @@ -41,9 +44,9 @@ export class ShowHide { } if (this._showHideId) { - let localStorageCollapsed = this._getLocalStorage()[this._showHideId]; - if (typeof localStorageCollapsed !== 'undefined') { - collapsed = localStorageCollapsed; + let storageCollapsed = this._storageManager.load(this._showHideId); + if (typeof storageCollapsed !== 'undefined') { + collapsed = storageCollapsed; } } this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed); @@ -70,18 +73,7 @@ export class ShowHide { const newState = this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS); if (this._showHideId) { - this._setLocalStorage(this._showHideId, newState); + this._storageManager.save(this._showHideId, newState); } } - - // maybe move these to a LocalStorageHelper? - _setLocalStorage(id, state) { - const lsData = this._getLocalStorage(); - lsData[id] = state; - window.localStorage.setItem(SHOW_HIDE_LOCAL_STORAGE_KEY, JSON.stringify(lsData)); - } - - _getLocalStorage() { - return JSON.parse(window.localStorage.getItem(SHOW_HIDE_LOCAL_STORAGE_KEY)) || {}; - } } diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index b539edb44..00f3e5ec0 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -11,6 +11,7 @@ import { Modal } from './modal/modal'; import { Tooltip } from './tooltips/tooltips'; import { CourseTeaser } from './course-teaser/course-teaser'; import { NavbarUtils } from './navbar/navbar'; +import { HideColumns } from './hide-columns/hide-columns'; export const Utils = [ Alerts, @@ -27,4 +28,5 @@ export const Utils = [ Tooltip, CourseTeaser, ...NavbarUtils, + HideColumns, ]; diff --git a/package-lock.json b/package-lock.json index a03239ca5..a1bd61eda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,6 +178,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -1907,6 +1913,14 @@ "@babel/helper-plugin-utils": "^7.0.0", "resolve": "^1.8.1", "semver": "^5.5.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "@babel/plugin-transform-shorthand-properties": { @@ -2035,6 +2049,12 @@ "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -2541,6 +2561,11 @@ "integrity": "sha512-EKKR4p0higjsIPKjSSkGqtweUwo/GgR/zKL4rCwzF5Z/BZ/ebJZaS8ZjGE7YUNEN63SYk2WhpJVI+l9dwfU7RQ==", "dev": true }, + "@juggle/resize-observer": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-2.5.0.tgz", + "integrity": "sha512-Nmkeaj5LalJeciRVEqi9Uxi61r0LvGc2yhUCykhXuft9fMyb/6VkZbwJ+UmUl8bk2k6qhwd1qJw6S2YJ0joXlA==" + }, "@marionebl/sander": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@marionebl/sander/-/sander-0.6.1.tgz", @@ -6277,6 +6302,14 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "crypto-browserify": { @@ -6989,6 +7022,12 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -8608,6 +8647,12 @@ "strip-indent": "^2.0.0" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10348,6 +10393,14 @@ "requires": { "pify": "^4.0.1", "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "mamacro": { @@ -10941,6 +10994,14 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "normalize-path": { @@ -14352,6 +14413,12 @@ "os-tmpdir": "^1.0.0" } }, + "outdent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.7.0.tgz", + "integrity": "sha512-Ue462G+UIFoyQmOzapGIKWS3d/9NHeD/018WGEDZIhN2/VaQpVXbofMcZX0socv1fw4/tmEn7Vd3McOdPZfKzQ==", + "dev": true + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -15468,9 +15535,9 @@ } }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, "semver-compare": { @@ -17555,6 +17622,15 @@ "tapable": "^1.0.0" } }, + "webpack-plugin-hash-output": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/webpack-plugin-hash-output/-/webpack-plugin-hash-output-3.2.1.tgz", + "integrity": "sha512-Iu4Sox3/bdiqd6TdYwZAExuH+XNbnJStPrwh6yhzOflwc/hZUP9MdiZDbFwTXrmm9ZwoXNUmvn7C0Qj4qRez2A==", + "dev": true, + "requires": { + "outdent": "^0.7.0" + } + }, "webpack-sources": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", diff --git a/package.json b/package.json index 1c4af85d9..3361f6443 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "", "license": "ISC", "scripts": { - "start": "run-p frontend:build:watch yesod:start", + "start": "npm-run-all frontend:build --parallel \"frontend:build:watch\" \"yesod:start\"", "test": "run-s frontend:test yesod:test", "lint": "run-s frontend:lint yesod:lint", "build": "run-s frontend:build yesod:build", @@ -90,6 +90,7 @@ "null-loader": "^2.0.0", "postcss-loader": "^3.0.0", "sass-loader": "^7.3.1", + "semver": "^6.3.0", "standard-version": "^6.0.1", "style-loader": "^0.23.1", "terser-webpack-plugin": "^2.2.3", @@ -97,10 +98,12 @@ "typeface-source-sans-pro": "0.0.75", "webpack": "^4.41.2", "webpack-cli": "^3.3.10", - "webpack-manifest-plugin": "^2.2.0" + "webpack-manifest-plugin": "^2.2.0", + "webpack-plugin-hash-output": "^3.2.1" }, "dependencies": { "@babel/runtime": "^7.7.6", + "@juggle/resize-observer": "^2.5.0", "core-js": "^3.4.8", "moment": "^2.24.0", "npm": "^6.13.3", diff --git a/records.json b/records.json deleted file mode 100644 index c3e4f6812..000000000 --- a/records.json +++ /dev/null @@ -1,544 +0,0 @@ -{ - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/typeface-roboto/index.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/typeface-source-sans-pro/index.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/@fortawesome/fontawesome-pro/css/all.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/async-form/async-form.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/asidenav/asidenav.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/show-hide/show-hide.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/inputs/inputs.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/inputs/radio.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/form/form.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/alerts/alerts.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/tooltips/tooltips.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/course-teaser/course-teaser.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/modal/modal.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/navbar/navbar.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/async-table/async-table.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/async-table/async-table-filter.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/mass-input/mass-input.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/inputs/file-input.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/inputs/checkbox.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!frontend/src/utils/form/datepicker.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - }, - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!node_modules/typeface-source-sans-pro/index.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!node_modules/typeface-roboto/index.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!node_modules/@fortawesome/fontawesome-pro/css/all.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/asidenav/asidenav.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-form/async-form.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/show-hide/show-hide.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/form/form.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/inputs.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/radio.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/tooltips/tooltips.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/course-teaser/course-teaser.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/modal/modal.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-table/async-table.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-table/async-table-filter.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/mass-input/mass-input.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/alerts/alerts.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/navbar/navbar.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!frontend/src/utils/form/datepicker.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/file-input.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/checkbox.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/app.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ] -} \ No newline at end of file diff --git a/src/Foundation.hs b/src/Foundation.hs index 3014250a6..1e7d8db89 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -71,6 +71,7 @@ import Handler.Utils.SchoolLdap import Handler.Utils.ExamOffice.Exam import Handler.Utils.ExamOffice.Course import Handler.Utils.Profile +import Handler.Utils.Routes import Utils.Form import Utils.Sheet import Utils.SystemMessage @@ -96,86 +97,12 @@ import qualified Ldap.Client as Ldap import UnliftIO.Pool - --- This is where we define all of the routes in our application. For a full --- explanation of the syntax, please see: --- http://www.yesodweb.com/book/routing-and-handlers --- --- Note that this is really half the story; in Application.hs, mkYesodDispatch --- generates the rest of the code. Please see the following documentation --- for an explanation for this split: --- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules --- --- This function also generates the following type synonyms: --- type Handler x = HandlerT UniWorX IO x --- type Widget = WidgetT UniWorX IO () -mkYesodData "UniWorX" uniworxRoutes - -deriving instance Generic CourseR -deriving instance Generic SheetR -deriving instance Generic SubmissionR -deriving instance Generic MaterialR -deriving instance Generic TutorialR -deriving instance Generic ExamR -deriving instance Generic CourseApplicationR -deriving instance Generic AllocationR -deriving instance Generic SchoolR -deriving instance Generic ExamOfficeR -deriving instance Generic CourseNewsR -deriving instance Generic CourseEventR -deriving instance Generic (Route UniWorX) - -data RouteChildren -type instance Children RouteChildren a = ChildrenRouteChildren a -type family ChildrenRouteChildren a where - ChildrenRouteChildren (Route EmbeddedStatic) = '[] - ChildrenRouteChildren (Route Auth) = '[] - ChildrenRouteChildren UUID = '[] - ChildrenRouteChildren (Key a) = '[] - ChildrenRouteChildren (CI a) = '[] - - ChildrenRouteChildren a = Children ChGeneric a - -- | Convenient Type Synonyms: type DB = YesodDB UniWorX type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, Widget) type MsgRenderer = MsgRendererS UniWorX -- see Utils type MailM a = MailT (HandlerFor UniWorX) a --- Pattern Synonyms for convenience -pattern CSheetR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetR -> Route UniWorX -pattern CSheetR tid ssh csh shn ptn - = CourseR tid ssh csh (SheetR shn ptn) - -pattern CMaterialR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> MaterialR -> Route UniWorX -pattern CMaterialR tid ssh csh mnm ptn - = CourseR tid ssh csh (MaterialR mnm ptn) - -pattern CTutorialR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> TutorialR -> Route UniWorX -pattern CTutorialR tid ssh csh tnm ptn - = CourseR tid ssh csh (TutorialR tnm ptn) - -pattern CExamR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> ExamR -> Route UniWorX -pattern CExamR tid ssh csh tnm ptn - = CourseR tid ssh csh (ExamR tnm ptn) - -pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionR -> Route UniWorX -pattern CSubmissionR tid ssh csh shn cid ptn - = CSheetR tid ssh csh shn (SubmissionR cid ptn) - -pattern CApplicationR :: TermId -> SchoolId -> CourseShorthand -> CryptoFileNameCourseApplication -> CourseApplicationR -> Route UniWorX -pattern CApplicationR tid ssh csh appId ptn - = CourseR tid ssh csh (CourseApplicationR appId ptn) - -pattern CNewsR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseNews -> CourseNewsR -> Route UniWorX -pattern CNewsR tid ssh csh nId ptn - = CourseR tid ssh csh (CourseNewsR nId ptn) - -pattern CEventR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseEvent -> CourseEventR -> Route UniWorX -pattern CEventR tid ssh csh nId ptn - = CourseR tid ssh csh (CourseEventR nId ptn) - - -- Requires `rendeRoute`, thus cannot currently be moved to Foundation.I18n instance RenderMessage UniWorX (UnsupportedAuthPredicate AuthTag (Route UniWorX)) where renderMessage f ls (UnsupportedAuthPredicate tag route) = mr . MsgUnsupportedAuthPredicate (mr tag) $ Text.intercalate "/" pieces @@ -1486,6 +1413,7 @@ siteLayout' headingOverride widget = do primaryLanguage <- unsafeHead . Text.splitOn "-" <$> selectLanguage appLanguages mcurrentRoute <- getCurrentRoute + let currentHandler = classifyHandler <$> mcurrentRoute -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. let diff --git a/src/Foundation/Routes.hs b/src/Foundation/Routes.hs index 614bdea6d..0e83a0734 100644 --- a/src/Foundation/Routes.hs +++ b/src/Foundation/Routes.hs @@ -1,10 +1,84 @@ +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + module Foundation.Routes - ( uniworxRoutes + ( module Foundation.Routes.Definitions + , module Foundation.Routes ) where -import ClassyPrelude.Yesod -import Yesod.Routes.TH.Types (ResourceTree) +import Import.NoFoundation +import Foundation.Type +import Foundation.Routes.Definitions -uniworxRoutes :: [ResourceTree String] -uniworxRoutes = $(parseRoutesFile "routes") +-- This is where we define all of the routes in our application. For a full +-- explanation of the syntax, please see: +-- http://www.yesodweb.com/book/routing-and-handlers +-- +-- Note that this is really half the story; in Application.hs, mkYesodDispatch +-- generates the rest of the code. Please see the following documentation +-- for an explanation for this split: +-- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules +-- +-- This function also generates the following type synonyms: +-- type Handler x = HandlerT UniWorX IO x +-- type Widget = WidgetT UniWorX IO () +mkYesodData "UniWorX" uniworxRoutes + +deriving instance Generic CourseR +deriving instance Generic SheetR +deriving instance Generic SubmissionR +deriving instance Generic MaterialR +deriving instance Generic TutorialR +deriving instance Generic ExamR +deriving instance Generic CourseApplicationR +deriving instance Generic AllocationR +deriving instance Generic SchoolR +deriving instance Generic ExamOfficeR +deriving instance Generic CourseNewsR +deriving instance Generic CourseEventR +deriving instance Generic (Route UniWorX) + +data RouteChildren +type instance Children RouteChildren a = ChildrenRouteChildren a +type family ChildrenRouteChildren a where + ChildrenRouteChildren (Route EmbeddedStatic) = '[] + ChildrenRouteChildren (Route Auth) = '[] + ChildrenRouteChildren UUID = '[] + ChildrenRouteChildren (Key a) = '[] + ChildrenRouteChildren (CI a) = '[] + + ChildrenRouteChildren a = Children ChGeneric a + +-- Pattern Synonyms for convenience +pattern CSheetR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetR -> Route UniWorX +pattern CSheetR tid ssh csh shn ptn + = CourseR tid ssh csh (SheetR shn ptn) + +pattern CMaterialR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> MaterialR -> Route UniWorX +pattern CMaterialR tid ssh csh mnm ptn + = CourseR tid ssh csh (MaterialR mnm ptn) + +pattern CTutorialR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> TutorialR -> Route UniWorX +pattern CTutorialR tid ssh csh tnm ptn + = CourseR tid ssh csh (TutorialR tnm ptn) + +pattern CExamR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> ExamR -> Route UniWorX +pattern CExamR tid ssh csh tnm ptn + = CourseR tid ssh csh (ExamR tnm ptn) + +pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionR -> Route UniWorX +pattern CSubmissionR tid ssh csh shn cid ptn + = CSheetR tid ssh csh shn (SubmissionR cid ptn) + +pattern CApplicationR :: TermId -> SchoolId -> CourseShorthand -> CryptoFileNameCourseApplication -> CourseApplicationR -> Route UniWorX +pattern CApplicationR tid ssh csh appId ptn + = CourseR tid ssh csh (CourseApplicationR appId ptn) + +pattern CNewsR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseNews -> CourseNewsR -> Route UniWorX +pattern CNewsR tid ssh csh nId ptn + = CourseR tid ssh csh (CourseNewsR nId ptn) + +pattern CEventR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseEvent -> CourseEventR -> Route UniWorX +pattern CEventR tid ssh csh nId ptn + = CourseR tid ssh csh (CourseEventR nId ptn) diff --git a/src/Foundation/Routes/Definitions.hs b/src/Foundation/Routes/Definitions.hs new file mode 100644 index 000000000..4908a25ee --- /dev/null +++ b/src/Foundation/Routes/Definitions.hs @@ -0,0 +1,10 @@ +module Foundation.Routes.Definitions + ( uniworxRoutes + ) where + +import ClassyPrelude.Yesod +import Yesod.Routes.TH.Types (ResourceTree) + + +uniworxRoutes :: [ResourceTree String] +uniworxRoutes = $(parseRoutesFile "routes") diff --git a/src/Handler/Allocation/List.hs b/src/Handler/Allocation/List.hs index 9d52233ed..081734394 100644 --- a/src/Handler/Allocation/List.hs +++ b/src/Handler/Allocation/List.hs @@ -89,7 +89,7 @@ getAllocationListR = do dbtSorting = mconcat [ sortTerm $ queryAllocation . to (E.^. AllocationTerm) - , sortSchool $ queryAllocation . to (E.^. AllocationSchool) + , sortSchoolShort $ queryAllocation . to (E.^. AllocationSchool) , sortAllocationName $ queryAllocation . to (E.^. AllocationName) , singletonMap "available" . SortColumn $ view queryAvailable , if @@ -124,7 +124,7 @@ getAllocationListR = do psValidator :: PSValidator _ _ psValidator = def - & defaultSorting [SortDescBy "term", SortAscBy "school", SortAscBy "allocation"] + & defaultSorting [SortDescBy "term", SortAscBy "school-short", SortAscBy "allocation"] table <- runDB $ dbTableWidget' psValidator DBTable{..} diff --git a/src/Handler/Utils/Routes.hs b/src/Handler/Utils/Routes.hs index 52a93dfed..345718bcb 100644 --- a/src/Handler/Utils/Routes.hs +++ b/src/Handler/Utils/Routes.hs @@ -2,7 +2,9 @@ module Handler.Utils.Routes ( classifyHandler ) where -import Import +import Import.NoFoundation +import Foundation.Routes +import Foundation.Type import Utils.TH.Routes diff --git a/stack.yaml.lock b/stack.yaml.lock index 3758cb266..3a7017a21 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -138,12 +138,33 @@ packages: original: hackage: HaXml-1.25.5 - completed: - hackage: esqueleto-3.0.0@sha256:efd84fd11ceaf0ae4e1b0c6236122b1f213c2c6f2f4f58e30f03eddc2ec3f423,5248 + hackage: persistent-2.10.4@sha256:16c4c0823dd5e16bac4d607895ab0f4febd0626c020e5755ed1a52bf04068148,4738 pantry-tree: - size: 1127 - sha256: 74e43834d5cc468acc3cb6e8a81567ebfbb5350a3e07ae01dd7f30d6255274a1 + size: 2094 + sha256: b40d1783b539ddbbceaa827bf286d0b3bfcf76ca19e604c9d510b2a64008714e original: - hackage: esqueleto-3.0.0 + hackage: persistent-2.10.4 +- completed: + hackage: persistent-postgresql-2.10.1@sha256:ea53a0f1f4223b4884b5e19511325367879560d2432a02a976aa4da57c5fb760,2871 + pantry-tree: + size: 740 + sha256: 3cdbc757b1cebb65542fb919369be238b3f120adc45f023084a8b64c214d9675 + original: + hackage: persistent-postgresql-2.10.1 +- completed: + hackage: persistent-template-2.7.3@sha256:ac3e5e8c48e968b927bbf4e97162c52e7e417d69b05efeb1c581d7c682e043d2,2703 + pantry-tree: + size: 560 + sha256: fdfb2a721eb9c9831d7381d36bc52de0808a008ed3d553b6490080f337249684 + original: + hackage: persistent-template-2.7.3 +- completed: + hackage: esqueleto-3.2.3@sha256:5e1e0a8600e2744127ef4bb5956fa84ae6bc1fc337c7b8726fabb7ca53e2d9b3,5466 + pantry-tree: + size: 1461 + sha256: f6215274a43addd339f8bc89f1ca0e8fdfb08180b13d779ae8f7e360acc4c473 + original: + hackage: esqueleto-3.2.3 - completed: hackage: HaskellNet-SSL-0.3.4.1@sha256:3ca14dd69460a380cf69aed40654fb10c4c03e344632b6a9986568c87feda157,1843 pantry-tree: @@ -242,6 +263,20 @@ packages: sha256: 9ed161eadfda5b1eb36cfcf077146f7b66db1da69f1041fc720aea287ec021b0 original: hackage: generic-lens-1.2.0.0 +- completed: + hackage: prometheus-metrics-ghc-1.0.0@sha256:0f4ecbefa810bd847e66c498ab3387bf21e426525a7c9a94841973c582719ba3,1231 + pantry-tree: + size: 293 + sha256: 8a6d6ef3235ab980e867f64b712b5d38f1a84c3ac4920f5b4c3b3e63bcdf6ec9 + original: + hackage: prometheus-metrics-ghc-1.0.0 +- completed: + hackage: wai-middleware-prometheus-1.0.0@sha256:1625792914fb2139f005685be8ce519111451cfb854816e430fbf54af46238b4,1314 + pantry-tree: + size: 307 + sha256: 6d64803c639ed4c7204ea6fab0536b97d3ee16cdecb9b4a883cd8e56d3c61402 + original: + hackage: wai-middleware-prometheus-1.0.0 snapshots: - completed: size: 498180 diff --git a/templates/course-participants.hamlet b/templates/course-participants.hamlet index 37f9960b9..ff5847746 100644 --- a/templates/course-participants.hamlet +++ b/templates/course-participants.hamlet @@ -4,4 +4,4 @@ $# $# participantTable : widget table ^{participantTable} -_{MsgCourseMembersCountOf (fromIntegral numParticipants) (courseCapacity course)}. \ No newline at end of file +_{MsgCourseMembersCountOf (fromIntegral numParticipants) (courseCapacity course)}. diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet index cdcffaad5..22fe5fa2d 100644 --- a/templates/default-layout.hamlet +++ b/templates/default-layout.hamlet @@ -11,7 +11,7 @@ $if not isModal
-
+
$if not isModal diff --git a/templates/default-layout.lucius b/templates/default-layout.lucius index 11355082b..0f09cb225 100644 --- a/templates/default-layout.lucius +++ b/templates/default-layout.lucius @@ -329,8 +329,12 @@ input[type="button"].btn-info:hover, width: 100%; } -.table:only-child { - margin: 0; +.table:first-child { + margin-top: 0; +} + +.table:last-child { + margin-bottom: 0; } .table--striped { diff --git a/templates/table/cell/header.hamlet b/templates/table/cell/header.hamlet index ddea5960c..fca1c703c 100644 --- a/templates/table/cell/header.hamlet +++ b/templates/table/cell/header.hamlet @@ -1,5 +1,5 @@ $newline never - + $maybe flag <- sortableKey $case directions $of [SortAsc] diff --git a/templates/table/colonnade.hamlet b/templates/table/colonnade.hamlet index c756229c5..081195f60 100644 --- a/templates/table/colonnade.hamlet +++ b/templates/table/colonnade.hamlet @@ -1,4 +1,5 @@ $newline never +
$maybe wHeaders' <- wHeaders diff --git a/templates/table/layout-wrapper.hamlet b/templates/table/layout-wrapper.hamlet index 791ccd656..791694f14 100644 --- a/templates/table/layout-wrapper.hamlet +++ b/templates/table/layout-wrapper.hamlet @@ -1,3 +1,3 @@ $newline never -
+
^{table} diff --git a/webpack.config.js b/webpack.config.js index 3a93a5bbe..c16c23a9d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,9 +6,10 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); const yaml = require('js-yaml'); +const HashOutput = require('webpack-plugin-hash-output'); -var webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.'); - +const webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.'); +const packageVersion = require('./package.json').version; module.exports = { module: { @@ -71,15 +72,21 @@ module.exports = { }, entry: { - main: path.resolve(__dirname, 'frontend/src', 'main.js') + main: [ path.resolve(__dirname, 'frontend/src', 'polyfill.js'), + path.resolve(__dirname, 'frontend/src', 'main.js') + ] }, plugins: [ + new HashOutput({ + validateOutput: true, + validateOutputRegex: /static\/wp-[^\/]\// + }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // all options are optional - filename: '[contenthash].css', - chunkFilename: '[contenthash].css', + filename: '[chunkhash].css', + chunkFilename: '[chunkhash].css', ignoreOrder: false, // Enable to remove warnings about conflicting order }), new webpack.NamedChunksPlugin((chunk) => { @@ -101,12 +108,15 @@ module.exports = { new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new CopyPlugin([ { from: 'assets/lmu/sigillum.svg', to: path.resolve(__dirname, 'static', 'img/lmu/sigillum.svg') }, - ]) + ]), + new webpack.DefinePlugin({ + VERSION: JSON.stringify(packageVersion) + }) ], output: { - chunkFilename: '[contenthash].js', - filename: '[contenthash].js', + chunkFilename: '[chunkhash].js', + filename: '[chunkhash].js', path: path.resolve(__dirname, 'static', `wp-${webpackVersion}`), publicPath: `/static/res/wp-${webpackVersion}/`, hashFunction: 'shake256',