diff --git a/.github/workflows/featureDeploy.yml b/.github/workflows/featureDeploy.yml index acd59b32..7e77d25c 100644 --- a/.github/workflows/featureDeploy.yml +++ b/.github/workflows/featureDeploy.yml @@ -30,10 +30,10 @@ jobs: strategy: matrix: environment: ${{ fromJSON(needs.detect-environments.outputs.environments) }} - if: ${{ matrix.environment != 'production' }} + if: ${{ inputs.environment != 'production' }} uses: ./.github/workflows/deploy.yml with: - environment: '${{ matrix.environment }}' + environment: '${{ inputs.environment }}' secrets: inherit diff --git a/src/assets/js/application.js b/src/assets/js/application.js index 2a2e5d48..a207f019 100644 --- a/src/assets/js/application.js +++ b/src/assets/js/application.js @@ -3,8 +3,8 @@ as it will be loaded into the base nunjucks template. */ -import hideElementsWithJsHidden from './js-hidden.js' +import initiateJsHiddenChecks from './js-hidden.js' window.addEventListener('load', () => { - hideElementsWithJsHidden() + initiateJsHiddenChecks() }) diff --git a/src/assets/js/js-hidden.js b/src/assets/js/js-hidden.js index d099bc5e..280bad12 100644 --- a/src/assets/js/js-hidden.js +++ b/src/assets/js/js-hidden.js @@ -1,8 +1,43 @@ -const hideElementsWithJsHidden = () => { +/* globals MutationObserver, document */ + +/** + * Initiates checks for elements with the class 'js-hidden' and updates their display and visibility styles accordingly. + * + * When an element gains the 'js-hidden' class, its display and visibility styles are set to 'none' and 'hidden', respectively. + * When an element loses the 'js-hidden' class, its display and visibility styles are reset to their default values. + * + * This function also hides any elements that already have the 'js-hidden' class when it is called. + */ +const initiateJsHiddenChecks = () => { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + const target = mutation.target + const classList = target.classList + if (classList.contains('js-hidden')) { + // Class js-hidden was added + target.style.display = 'none' + target.style.visibility = 'hidden' + } else { + // Class js-hidden was removed + target.style.display = '' + target.style.visibility = '' + } + } + }) + }) + + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'], + subtree: true + }) + document.querySelectorAll('.js-hidden').forEach((el, i) => { - console.log(el) + console.log('Hiding element', el) el.style.display = 'none' + el.style.visibility = 'none' }) } -export default hideElementsWithJsHidden +export default initiateJsHiddenChecks diff --git a/src/assets/js/list-filter.js b/src/assets/js/list-filter.js new file mode 100644 index 00000000..02b0a2d9 --- /dev/null +++ b/src/assets/js/list-filter.js @@ -0,0 +1,127 @@ +/** + * This file is taken from https://github.com/alphagov/collections/blob/main/app/assets/javascripts/modules/list-filter.js + */ + +/* eslint-disable no-var */ +//= require govuk_publishing_components/vendor/polyfills/closest + +const keyPauseTime = 20 + +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {}; + +(function (Modules) { + function ListFilter ($module) { + this.$module = $module + this.filterTimeout = null + this.form = this.$module.querySelector('[data-filter="form"]') + this.searchResults = this.$module.querySelector('#search_results') + } + + ListFilter.prototype.init = function () { + this.$module.filterList = this.filterList.bind(this) + // Form should only appear if the JS is working + this.form.classList.add('filter-list__form--active') + this.results = document.createElement('div') + this.results.classList.add('filter-list__results', 'govuk-heading-m', 'js-search-results') + this.results.setAttribute('aria-live', 'polite') + this.results.innerHTML = this.countInitialItems() + ' results found' + this.searchResults.insertBefore(this.results, this.searchResults.firstChild) + + // We don't want the form to submit/refresh the page on enter key + this.form.onsubmit = function () { return false } + + this.form.addEventListener('keyup', function (e) { + var searchTerm = e.target.value + clearTimeout(this.filterTimeout) + this.filterTimeout = setTimeout(function () { + this.$module.filterList(searchTerm) + }.bind(this), keyPauseTime) + }.bind(this)) + } + + ListFilter.prototype.filterList = function (searchTerm) { + var itemsToFilter = this.$module.querySelectorAll('[data-filter="item"]') + var blocksToFilter = this.$module.querySelectorAll('[data-filter="block"]') + for (var i = 0; i <= itemsToFilter.length - 1; i++) { + var currentItem = itemsToFilter[i] + if (!this.matchSearchTerm(currentItem, searchTerm)) { + currentItem.classList.add('js-hidden') + } + } + this.updateItemCount(blocksToFilter) + } + + ListFilter.prototype.matchSearchTerm = function (item, term) { + var normaliseWhitespace = function (string) { + return string + .trim() // Removes spaces at beginning and end of string. + .replace(/\r?\n|\r/g, ' ') // Replaces line breaks with one space. + .replace(/\s+/g, ' ') // Squashes multiple spaces to one space. + } + + var searchTerms = item.getAttribute('data-filter-terms') || '' + var normalisedTerms = normaliseWhitespace(searchTerms) + + item.classList.remove('js-hidden') + + var searchTermRegexp = new RegExp(term.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') + return searchTermRegexp.exec(normalisedTerms) !== null + } + + ListFilter.prototype.countInitialItems = function () { + return this.$module.querySelectorAll('[data-filter="item"]').length + } + + ListFilter.prototype.updateItemCount = function (blocksToFilter) { + var totalMatchingItems = 0 + + for (var i = 0; i < blocksToFilter.length; i++) { + var block = blocksToFilter[i].closest('[data-filter="block"]') + block.classList.remove('js-hidden') + + var matchingItems = block.querySelectorAll('[data-filter="item"]') + var matchingItemCount = 0 + + var innerBlocks = block.querySelectorAll('[data-filter="inner-block"]') + for (var r = 0; r < innerBlocks.length; r++) { + innerBlocks[r].classList.add('js-hidden') + } + + for (var j = 0; j < matchingItems.length; j++) { + if (!matchingItems[j].classList.contains('js-hidden')) { + matchingItemCount++ + + if (matchingItems[j].closest('[data-filter="inner-block"]') !== null) { matchingItems[j].closest('[data-filter="inner-block"]').classList.remove('js-hidden') } + } + } + + var itemCount = block.querySelectorAll('[data-item-count="true"]') + var accessibleItemCount = block.querySelectorAll('.js-accessible-item-count') + + if (matchingItemCount === 0) { + block.classList.toggle('js-hidden') + } + + if (matchingItemCount > 0) { + for (var l = 0; l < itemCount.length; l++) { + itemCount[l].textContent = matchingItemCount + } + + for (var k = 0; k < accessibleItemCount.length; k++) { + accessibleItemCount[k].textContent = matchingItemCount + } + } + + totalMatchingItems += matchingItemCount + } + + var text = ' results found' + if (totalMatchingItems === 1) { + text = ' result found' + } + this.results.innerHTML = totalMatchingItems + text + } + + Modules.ListFilter = ListFilter +})(window.GOVUK.Modules) diff --git a/src/assets/scss/index.scss b/src/assets/scss/index.scss index 14c1bd62..bd86cf6d 100644 --- a/src/assets/scss/index.scss +++ b/src/assets/scss/index.scss @@ -101,4 +101,4 @@ $govuk-global-styles: true; code, code * { font-family: monospace; -} +} \ No newline at end of file diff --git a/src/controllers/OrganisationsController.js b/src/controllers/OrganisationsController.js index 00a0cfae..e3d7e517 100644 --- a/src/controllers/OrganisationsController.js +++ b/src/controllers/OrganisationsController.js @@ -1,3 +1,4 @@ +import datasette from '../services/datasette.js' import performanceDbApi from '../services/performanceDbApi.js' // Assume you have an API service module import logger from '../utils/logger.js' import { dataSubjects } from '../utils/utils.js' @@ -70,13 +71,40 @@ const organisationsController = { } }, + /** + * Handles the GET /organisations request + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ async getOrganisations (req, res, next) { - res.render('organisations/find.html') + try{ + const sql = 'select name, organisation from organisation' + const result = await datasette.runQuery(sql) + + const sortedResults = result.formattedData.sort((a, b) => { + return a.name.localeCompare(b.name) + }) + + const alphabetisedOrgs = sortedResults.reduce((acc, current) => { + const firstLetter = current.name.charAt(0).toUpperCase() + acc[firstLetter] = acc[firstLetter] || [] + acc[firstLetter].push(current) + return acc + }, {}) + + res.render('organisations/find.html', { alphabetisedOrgs }) + } catch (err) { + logger.warn(err) + next(err) + } }, async getGetStarted (req, res, next) { res.render('organisations/get-started.html') } + } export default organisationsController diff --git a/src/views/organisations/find.html b/src/views/organisations/find.html index c6d190d9..ffd58b8e 100644 --- a/src/views/organisations/find.html +++ b/src/views/organisations/find.html @@ -1,9 +1,9 @@ {% extends "layouts/main.html" %} -{% from "govuk/components/breadcrumbs/macro.njk" import govukBreadcrumbs %} -{% from "govuk/components/tag/macro.njk" import govukTag %} +{% set pageName = "Find your organisation" %} {% block beforeContent %} + {{ super() }} {% endblock %} @@ -12,15 +12,59 @@