diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f742526c..5e9f80e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.22.4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.3...v25.22.4) (2021-10-26) + + +### Bug Fixes + +* **routes:** make access to workflows free ([29c54db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/29c54db06f01659a3a6419009964a85cd11d5441)) + +## [25.22.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.2...v25.22.3) (2021-10-21) + + +### Bug Fixes + +* **navigation:** always link workflows nav to instances ([adf9709](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/adf9709567d9a320f2c17d3c5dde940c2f9d8862)) + ## [25.22.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.1...v25.22.2) (2021-10-13) ## [25.22.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.0...v25.22.1) (2021-10-02) diff --git a/frontend/src/lib/tooltips/frontend-tooltips.js b/frontend/src/lib/tooltips/frontend-tooltips.js new file mode 100644 index 000000000..75535ddae --- /dev/null +++ b/frontend/src/lib/tooltips/frontend-tooltips.js @@ -0,0 +1,22 @@ +export class FrontendTooltips { + + static addToolTip(element, text) { + let tooltipWrap = document.createElement('span'); + tooltipWrap.className = 'tooltip'; + + let tooltipContent = document.createElement('span'); + tooltipContent.className = 'tooltip__content'; + tooltipContent.appendChild(document.createTextNode(text)); + tooltipWrap.append(tooltipContent); + + let tooltipHandle = document.createElement('span'); + tooltipHandle.className = 'tooltip__handle'; + let icon = document.createElement('i'); + icon.classList.add('fas'); + icon.classList.add('fa-question-circle'); + tooltipHandle.append(icon); + tooltipWrap.append(tooltipHandle); + + element.append(tooltipWrap); + } +} \ No newline at end of file diff --git a/frontend/src/messages.js b/frontend/src/messages.js new file mode 100644 index 000000000..1f1ce3574 --- /dev/null +++ b/frontend/src/messages.js @@ -0,0 +1,17 @@ +export class Translations { + static translations = { + 'checkrangeTooltip' : { + 'de' : 'Shift-Klick, um mehrere Zellen zu markieren.', + 'en' : 'Shift-click to mark multiple cells.', + }, + }; + + static getTranslation(key, language) { + let json = Translations.translations[key]; + if(language === 'en') { + return json.en; + } else { + return json.de; + } + } +}; \ No newline at end of file diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index 79558a75f..29e33cff8 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -21,6 +21,8 @@ export class CheckAll { _tableIndices; + _lastCheckedCell = null; + constructor(element, app) { if (!element) { throw new Error('Check All utility cannot be setup without an element!'); @@ -41,7 +43,9 @@ export class CheckAll { if (DEBUG_MODE > 0) console.log(this._columns); - this._findCheckboxColumns().forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId], this._eventManager))); + let checkboxColumns = this._findCheckboxColumns(); + + checkboxColumns.forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId]))); // mark initialized this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS); @@ -116,6 +120,7 @@ class CheckAllColumn { this._checkAllCheckbox = document.createElement('input'); this._checkAllCheckbox.setAttribute('type', 'checkbox'); this._checkAllCheckbox.setAttribute('id', this._checkboxId); + th.insertBefore(this._checkAllCheckbox, th.firstChild); // set up new checkbox diff --git a/frontend/src/utils/inputs/checkrange.js b/frontend/src/utils/inputs/checkrange.js new file mode 100644 index 000000000..ee6441b95 --- /dev/null +++ b/frontend/src/utils/inputs/checkrange.js @@ -0,0 +1,125 @@ +import { Utility } from '../../core/utility'; +import { TableIndices } from '../../lib/table/table'; +import { FrontendTooltips } from '../../lib/tooltips/frontend-tooltips'; +import { Translations } from '../../messages'; + + +const CHECKRANGE_INITIALIZED_CLASS = 'checkrange--initialized'; +const CHECKBOX_SELECTOR = '[type="checkbox"]'; + + +@Utility({ + selector: 'table:not([uw-no-check-all])', + }) +export class CheckRange { + _lastCheckedCell = null; + _element; + _tableIndices + _columns = new Array(); + + constructor(element) { + if(!element) { + throw new Error('Check Range Utility cannot be setup without an element'); + } + + this._element = element; + + if (this._element.classList.contains(CHECKRANGE_INITIALIZED_CLASS)) + return false; + + this._tableIndices = new TableIndices(this._element); + + this._gatherColumns(); + + let checkboxColumns = this._findCheckboxColumns(); + + checkboxColumns.forEach(columnId => this._setUpShiftClickOnColumn(columnId)); + + this._element.classList.add(CHECKRANGE_INITIALIZED_CLASS); + } + + _setUpShiftClickOnColumn(columnId) { + if (!this._columns || columnId < 0 || columnId >= this._columns.length) return; + let column = this._columns[columnId]; + let language = document.documentElement.lang; + let toolTipMessage = Translations.getTranslation('checkrangeTooltip', language); + FrontendTooltips.addToolTip(column[0], toolTipMessage); + column.forEach(el => el.addEventListener('click', (ev) => { + + if(ev.shiftKey && this.lastCheckedCell !== null) { + let lastClickedIndex = this._tableIndices.rowIndex(this._lastCheckedCell); + let currentCellIndex = this._tableIndices.rowIndex(el); + let cell = this._columns[columnId][currentCellIndex]; + if(currentCellIndex > lastClickedIndex) + this._handleCellsInBetween(cell, lastClickedIndex, currentCellIndex, columnId); + else + this._handleCellsInBetween(cell, currentCellIndex, lastClickedIndex, columnId); + } else { + this._lastCheckedCell = el; + } + })); + } + + _handleCellsInBetween(cell, firstRowIndex, lastRowIndex, columnId) { + if(this._isChecked(cell)) { + this._uncheckMultipleCells(firstRowIndex, lastRowIndex, columnId); + } else { + this._checkMultipleCells(firstRowIndex, lastRowIndex, columnId); + } + } + + _checkMultipleCells(firstRowIndex, lastRowIndex, columnId) { + for(let i=firstRowIndex; i<=lastRowIndex; i++) { + let cell = this._columns[columnId][i]; + if (cell.tagName !== 'TH') { + cell.querySelector(CHECKBOX_SELECTOR).checked = true; + } + } + } + + _uncheckMultipleCells(firstRowIndex, lastRowIndex, columnId) { + for(let i=firstRowIndex; i<=lastRowIndex; i++) { + let cell = this._columns[columnId][i]; + if (cell.tagName !== 'TH') { + cell.querySelector(CHECKBOX_SELECTOR).checked = false; + } + } + } + + _isChecked(cell) { + return cell.querySelector(CHECKBOX_SELECTOR).checked; + } + + + _gatherColumns() { + for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) { + for (const colIndex of Array(this._tableIndices.maxCol + 1).keys()) { + + const cell = this._tableIndices.getCell(rowIndex, colIndex); + + if (!cell) + continue; + + if (!this._columns[colIndex]) + this._columns[colIndex] = new Array(); + + this._columns[colIndex][rowIndex] = cell; + } + } + } + + _findCheckboxColumns() { + let checkboxColumnIds = new Array(); + this._columns.forEach((col, i) => { + if (this._isCheckboxColumn(col)) { + checkboxColumnIds.push(i); + } + }); + return checkboxColumnIds; + } + + _isCheckboxColumn(col) { + return col.every(cell => cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR)) + && col.some(cell => cell.querySelector(CHECKBOX_SELECTOR)); + } +} \ No newline at end of file diff --git a/frontend/src/utils/inputs/checkrange.md b/frontend/src/utils/inputs/checkrange.md new file mode 100644 index 000000000..894a3bd4f --- /dev/null +++ b/frontend/src/utils/inputs/checkrange.md @@ -0,0 +1,5 @@ +# Checkrange Utility +Is set on the table header of a specific row. Remembers the last checked checkbox. When the users shift-clicks another checkbox in the same row, all checkboxes in between are also checked. + +# Attribute: table:not([uw-no-check-all] +(will be setup on all tables which use the util check-all) \ No newline at end of file diff --git a/frontend/src/utils/inputs/inputs.js b/frontend/src/utils/inputs/inputs.js index a072c2196..13d241895 100644 --- a/frontend/src/utils/inputs/inputs.js +++ b/frontend/src/utils/inputs/inputs.js @@ -2,6 +2,7 @@ import { Checkbox } from './checkbox'; import { FileInput } from './file-input'; import { FileMaxSize } from './file-max-size'; import { Password } from './password'; +import { CheckRange } from './checkrange'; import './inputs.sass'; import './radio-group.sass'; @@ -11,4 +12,5 @@ export const InputUtils = [ FileInput, FileMaxSize, Password, + CheckRange, ]; diff --git a/package-lock.json b/package-lock.json index 38fe5251b..3113011ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.2", + "version": "25.22.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1902d7a63..ae47ea5a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.2", + "version": "25.22.4", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 32949f723..43bc3cbc1 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.22.2 +version: 25.22.4 dependencies: - base - yesod diff --git a/routes b/routes index c7299e84c..8051d646f 100644 --- a/routes +++ b/routes @@ -78,7 +78,7 @@ /global-workflows/instances/#WorkflowInstanceName GlobalWorkflowInstanceR: /edit GWIEditR GET POST /delete GWIDeleteR GET POST - /workflows GWIWorkflowsR GET !¬empty + /workflows GWIWorkflowsR GET !free /initiate GWIInitiateR GET POST !workflow /update GWIUpdateR POST /global-workflows GlobalWorkflowWorkflowListR GET !free @@ -145,7 +145,7 @@ /workflows/instances/#WorkflowInstanceName SchoolWorkflowInstanceR: /edit SWIEditR GET POST /delete SWIDeleteR GET POST - /workflows SWIWorkflowsR GET !¬empty + /workflows SWIWorkflowsR GET !free /initiate SWIInitiateR GET POST !workflow /update SWIUpdateR POST /workflows SchoolWorkflowWorkflowListR GET !free diff --git a/src/Foundation/Navigation.hs b/src/Foundation/Navigation.hs index 28303797b..a1133b8e3 100644 --- a/src/Foundation/Navigation.hs +++ b/src/Foundation/Navigation.hs @@ -573,8 +573,8 @@ navLinkAccess NavLink{..} = case navAccess' of defaultLinks :: ( MonadHandler m , HandlerSite m ~ UniWorX - , MonadThrow m - , WithRunDB SqlReadBackend (HandlerFor UniWorX) m + -- , MonadThrow m + -- , WithRunDB SqlReadBackend (HandlerFor UniWorX) m , BearerAuthSite UniWorX ) => m [Nav] defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header. @@ -761,12 +761,14 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the , do guardVolatile clusterVolatileWorkflowsEnabled - authCtx <- getAuthContext - (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,) - <$> haveTopWorkflowInstances - <*> haveTopWorkflowWorkflows + -- authCtx <- getAuthContext + -- (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,) + -- <$> haveTopWorkflowInstances + -- <*> haveTopWorkflowWorkflows - if | haveInstances -> return NavHeader + mUserId <- maybeAuthId + -- if | haveInstances -> return NavHeader + if | isJust mUserId -> return NavHeader { navHeaderRole = NavHeaderPrimary , navIcon = IconMenuWorkflows , navLink = NavLink @@ -778,18 +780,18 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the , navForceActive = False } } - | haveWorkflows -> return NavHeader - { navHeaderRole = NavHeaderPrimary - , navIcon = IconMenuWorkflows - , navLink = NavLink - { navLabel = MsgMenuTopWorkflowWorkflowListHeader - , navRoute = TopWorkflowWorkflowListR - , navAccess' = NavAccessTrue - , navType = NavTypeLink { navModal = False } - , navQuick' = mempty - , navForceActive = False - } - } + -- | haveWorkflows -> return NavHeader + -- { navHeaderRole = NavHeaderPrimary + -- , navIcon = IconMenuWorkflows + -- , navLink = NavLink + -- { navLabel = MsgMenuTopWorkflowWorkflowListHeader + -- , navRoute = TopWorkflowWorkflowListR + -- , navAccess' = NavAccessTrue + -- , navType = NavTypeLink { navModal = False } + -- , navQuick' = mempty + -- , navForceActive = False + -- } + -- } | otherwise -> mzero , return NavHeaderContainer { navHeaderRole = NavHeaderPrimary @@ -2730,34 +2732,35 @@ haveWorkflowWorkflows rScope = hoist liftHandler . withReaderT (projectBackend @ lift $ anyM roles evalRole -haveTopWorkflowInstances, haveTopWorkflowWorkflows +-- haveTopWorkflowInstances, +haveTopWorkflowWorkflows :: ( MonadHandler m, HandlerSite m ~ UniWorX , BackendCompatible SqlReadBackend backend , BearerAuthSite UniWorX ) => ReaderT backend m Bool -haveTopWorkflowInstances = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do - roles <- memcachedBy @(Set ((RouteWorkflowScope, WorkflowInstanceName), WorkflowRole UserId)) (Just $ Right diffDay) NavCacheHaveTopWorkflowInstancesRoles $ do - let - getInstances = E.selectSource . E.from $ \workflowInstance -> do - E.where_ . isTopWorkflowScopeSql $ workflowInstance E.^. WorkflowInstanceScope - return workflowInstance - instanceRoles (Entity _ WorkflowInstance{..}) = do - rScope <- toRouteWorkflowScope $ _DBWorkflowScope # workflowInstanceScope - wiGraph <- lift $ getSharedIdWorkflowGraph workflowInstanceGraph - return . Set.mapMonotonic ((rScope, workflowInstanceName), ) . fold $ do - WGN{..} <- wiGraph ^.. _wgNodes . folded - WorkflowGraphEdgeInitial{..} <- wgnEdges ^.. folded - return wgeActors - runConduit $ transPipe lift getInstances .| C.foldMapM instanceRoles - - let - evalRole :: _ -> ReaderT SqlReadBackend (HandlerFor UniWorX) Bool - evalRole ((rScope, win), role) = do - let route = _WorkflowScopeRoute # (rScope, WorkflowInstanceR win WIInitiateR) - is _Authorized <$> hasWorkflowRole Nothing role route False - - lift $ anyM roles evalRole +-- haveTopWorkflowInstances = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do +-- roles <- memcachedBy @(Set ((RouteWorkflowScope, WorkflowInstanceName), WorkflowRole UserId)) (Just $ Right diffDay) NavCacheHaveTopWorkflowInstancesRoles $ do +-- let +-- getInstances = E.selectSource . E.from $ \workflowInstance -> do +-- E.where_ . isTopWorkflowScopeSql $ workflowInstance E.^. WorkflowInstanceScope +-- return workflowInstance +-- instanceRoles (Entity _ WorkflowInstance{..}) = do +-- rScope <- toRouteWorkflowScope $ _DBWorkflowScope # workflowInstanceScope +-- wiGraph <- lift $ getSharedIdWorkflowGraph workflowInstanceGraph +-- return . Set.mapMonotonic ((rScope, workflowInstanceName), ) . fold $ do +-- WGN{..} <- wiGraph ^.. _wgNodes . folded +-- WorkflowGraphEdgeInitial{..} <- wgnEdges ^.. folded +-- return wgeActors +-- runConduit $ transPipe lift getInstances .| C.foldMapM instanceRoles +-- +-- let +-- evalRole :: _ -> ReaderT SqlReadBackend (HandlerFor UniWorX) Bool +-- evalRole ((rScope, win), role) = do +-- let route = _WorkflowScopeRoute # (rScope, WorkflowInstanceR win WIInitiateR) +-- is _Authorized <$> hasWorkflowRole Nothing role route False +-- +-- lift $ anyM roles evalRole haveTopWorkflowWorkflows = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do roles <- memcachedBy (Just $ Right diffDay) NavCacheHaveTopWorkflowWorkflowsRoles $ do let