diff --git a/README.md b/README.md index d2023f3d5..0b5c68d5e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Eyra Mono -Primary collection of Eyra projects +Primary collection of Eyra projects ## Projects @@ -19,7 +19,7 @@ Project implementing a SaaS platform based on interlinked modules called Systems * Campaign * Assignment * Lab -* Survey +* Questionnaire * Pool * Data Donation * .. @@ -28,12 +28,12 @@ Project implementing a SaaS platform based on interlinked modules called Systems * Next -Primary bundle with all features available except Link specific features. +Primary bundle with all features available except Link specific features. Next is hosted on: https://eyra.co * Link -Secundary bundle with only Panl specific features. +Secundary bundle with only Panl specific features. Link is hosted on: https://researchpanl.eu ## Banking Proxy diff --git a/authorization_node.ex b/authorization_node.ex deleted file mode 100644 index cae855f16..000000000 --- a/authorization_node.ex +++ /dev/null @@ -1,2 +0,0 @@ -defmodule Core.Authorization.Node do -end diff --git a/core/.credo.exs b/core/.credo.exs index b01e52341..407c985c1 100644 --- a/core/.credo.exs +++ b/core/.credo.exs @@ -42,7 +42,7 @@ # If you don't want TODO comments to cause `mix credo` to fail, just # set this value to 0 (zero). # - {Credo.Check.Design.TagTODO, [exit_status: 2]}, + {Credo.Check.Design.TagTODO, [exit_status: 0]}, {Credo.Check.Design.TagFIXME, false}, # diff --git a/core/Makefile b/core/Makefile index b8cc455d9..3a4003f6e 100644 --- a/core/Makefile +++ b/core/Makefile @@ -23,6 +23,9 @@ gettext: build: build-digest build-app +build-assets: + @cd .assets && npm run build + build-digest: @mix phx.digest diff --git a/core/assets/css/app.css b/core/assets/css/app.css index 3d5a4809f..e78a1673e 100644 --- a/core/assets/css/app.css +++ b/core/assets/css/app.css @@ -127,37 +127,3 @@ src: url("../static/fonts/Finador-BlackOblique.woff2") format("woff2"), url("../static/fonts/Finador-BlackOblique.woff") format("woff"); } - -.data-donation-extraction-results tbody tr:nth-child(even) { - background-color: #fafafa; -} - -.data-donation-extraction-results thead { - background-color: #fafafa; -} - -.data-donation-extraction-results thead tr { - font-family: "Finador-Bold"; - font-size: 18px; -} - -.data-donation-extraction-results tbody tr { - font-family: "Finador-Regular"; - font-size: 14px; -} - -.data-donation-extraction-results thead tr td, -th { - padding-top: 9px; - padding-bottom: 9px; - padding-left: 10px; - padding-right: 10px; -} - -.data-donation-extraction-results tbody tr td, -th { - padding-top: 7px; - padding-bottom: 7px; - padding-left: 10px; - padding-right: 10px; -} diff --git a/core/assets/js/app.js b/core/assets/js/app.js index c66003216..7f85dc212 100644 --- a/core/assets/js/app.js +++ b/core/assets/js/app.js @@ -26,12 +26,11 @@ import "./100vh-fix"; import { ViewportResize } from "./viewport_resize"; import { SidePanel } from "./side_panel"; import { Toggle } from "./toggle"; +import { Cell } from "./cell"; import { LiveContent, LiveField } from "./live_content"; import { Tabbar, TabbarItem, TabbarFooterItem } from "./tabbar"; -import { PythonUploader } from "./python_uploader"; import { Clipboard } from "./clipboard"; -import { DataDonationHook } from "./data_donation_hook"; -import { Port } from "./port"; +import { FeldsparApp } from "./feldspar_app"; window.registerAPNSDeviceToken = registerAPNSDeviceToken; @@ -96,15 +95,14 @@ let Hooks = { ViewportResize, SidePanel, Toggle, + Cell, LiveContent, LiveField, Tabbar, TabbarItem, TabbarFooterItem, NativeWrapper, - PythonUploader, - DataDonationHook, - Port, + FeldsparApp, }; let liveSocket = new LiveSocket("/live", Socket, { diff --git a/core/assets/js/cell.js b/core/assets/js/cell.js new file mode 100644 index 000000000..532ae64ac --- /dev/null +++ b/core/assets/js/cell.js @@ -0,0 +1,82 @@ +const COLLAPSED = "collapsed"; +const EXPANDED = "expanded"; + +const COLLAPSED_VIEW = "cell-collapsed-view"; +const EXPANDED_VIEW = "cell-expanded-view"; + +const COLLAPSE_BUTTON = "cell-collapse-button"; +const EXPAND_BUTTON = "cell-expand-button"; + +export const Cell = { + mounted() { + this.collapseButton = this.el.getElementsByClassName(COLLAPSE_BUTTON)[0]; + this.collapsedView = this.el.getElementsByClassName(COLLAPSED_VIEW)[0]; + + this.expandButton = this.el.getElementsByClassName(EXPAND_BUTTON)[0]; + this.expandedView = this.el.getElementsByClassName(EXPANDED_VIEW)[0]; + + this.collapseButton.addEventListener("click", (event) => { + event.stopPropagation(); + this.updateStatus(COLLAPSED); + }); + + this.expandButton.addEventListener("click", (event) => { + event.stopPropagation(); + this.updateStatus(EXPANDED); + }); + + var initialStatus = this.el.dataset.initialStatus + ? this.el.dataset.initialTab + : COLLAPSED; + + var savedStatus = this.loadStatus(); + this.status = savedStatus ? savedStatus : initialStatus; + this.updateUI(); + }, + + updated() { + this.updateUI(); + }, + + loadStatus() { + const key = this.getStatusKey(); + const status = window.localStorage.getItem(key); + if (typeof status === "string") { + return status; + } + return undefined; + }, + + saveStatus() { + console.info("saveStatus ", this.status); + window.localStorage.setItem(this.getStatusKey(), this.status); + }, + + getStatusKey() { + return "cell://" + this.el.id + "/status"; + }, + + updateStatus(status) { + this.status = status; + this.saveStatus(); + this.updateUI(); + }, + + updateUI() { + if (this.status == EXPANDED) { + this.hide(this.collapsedView); + this.show(this.expandedView); + } else { + this.show(this.collapsedView); + this.hide(this.expandedView); + } + }, + hide(element) { + if (!element.classList.contains("hidden")) { + element.classList.add("hidden"); + } + }, + show(element) { + element.classList.remove("hidden"); + }, +}; diff --git a/core/assets/js/data_donation_assembly.js b/core/assets/js/data_donation_assembly.js deleted file mode 100644 index a0a99d718..000000000 --- a/core/assets/js/data_donation_assembly.js +++ /dev/null @@ -1,17 +0,0 @@ -import { VisualisationEngine } from "./visualisation_engine"; -import { VisualisationFactory } from "./visualisation_factory"; -import { ProcessingEngine } from "./processing_engine"; -import Worker from "./processing_worker.js"; - -export class DataDonationAssembly { - constructor() { - this.visualisationFactory = new VisualisationFactory(); - this.processingEngine = new ProcessingEngine(new Worker()); - this.visualisationEngine = new VisualisationEngine( - this.visualisationFactory, - this.processingEngine - ); - - this.processingEngine.eventListener = this.visualisationEngine.onEvent; - } -} diff --git a/core/assets/js/data_donation_hook.js b/core/assets/js/data_donation_hook.js deleted file mode 100644 index 91e8d1d5d..000000000 --- a/core/assets/js/data_donation_hook.js +++ /dev/null @@ -1,57 +0,0 @@ -import { Tabbar } from "./tabbar"; -import { DataDonationAssembly } from "./data_donation_assembly"; - -const assembly = new DataDonationAssembly(); - -export const DataDonationHook = { - mounted() { - console.log("[DataDonationHook] mounted"); - this.hideNextButton("execute"); - - const locale = this.el.dataset.locale; - const afterCompletionTab = this.el.dataset.afterCompletionTab; - - const hook = this; - const script = this.get_script(); - const prompt_element = this.get_prompt_element(); - const spinner_element = this.get_spinner_element(); - - assembly.visualisationEngine - .start(script, prompt_element, spinner_element, locale) - .then((result) => { - hook.el - .querySelector(".no-extraction-data-yet") - .classList.add("hidden"); - hook.el.querySelector(".donate-form").classList.remove("hidden"); - hook.el.querySelector(".extracted").innerHTML = result.html; - hook.el.querySelector("input[id='data']").value = result.data; - Tabbar.show("tab_" + afterCompletionTab, true); - }); - - assembly.processingEngine.start(); - }, - beforeUpdate() { - console.log("[DataDonationHook] beforeUpdate"); - }, - updated() { - console.log("[DataDonationHook] updated"); - }, - destroyed() { - assembly.visualisationEngine.terminate(); - }, - get_spinner_element() { - return this.el.querySelector("#spinner"); - }, - get_prompt_element() { - return this.el.querySelector("#prompt"); - }, - get_script_element() { - return this.el.getElementsByTagName("code")[0]; - }, - get_script() { - return this.get_script_element().innerText; - }, - hideNextButton(tabId) { - this.el.querySelector(`#tabbar-footer-item-${tabId}`).hidden = true; - }, -}; diff --git a/core/assets/js/feldspar_app.js b/core/assets/js/feldspar_app.js new file mode 100644 index 000000000..8cf14c428 --- /dev/null +++ b/core/assets/js/feldspar_app.js @@ -0,0 +1,30 @@ +export const FeldsparApp = { + mounted() { + console.log("FeldsparApp MOUNTED"); + + const iframe = this.getIframe(); + iframe.addEventListener("load", () => { + this.onFrameLoaded(); + }); + iframe.setAttribute("src", this.el.dataset.src); + }, + + getIframe() { + return this.el.querySelector("iframe"); + }, + + onFrameLoaded() { + console.log("Initializing iframe app"); + this.channel = new MessageChannel(); + this.channel.port1.onmessage = (e) => { + this.handleMessage(e); + }; + this.getIframe().contentWindow.postMessage("init", "*", [ + this.channel.port2, + ]); + }, + + handleMessage(e) { + this.pushEvent("app_event", e.data); + }, +}; diff --git a/core/assets/js/port.js b/core/assets/js/port.js deleted file mode 100644 index 4ef5ba4f9..000000000 --- a/core/assets/js/port.js +++ /dev/null @@ -1,42 +0,0 @@ -import Assembly from "port/dist/framework/assembly"; -import Worker from "port/dist/framework/processing/py_worker.js"; -import { isCommandSystemDonate } from "port/dist/framework/types/commands"; - -// Webpack will make sure the assets below can be served from root -import "port/dist/port-0.0.0-py3-none-any.whl"; - -// TODO: refactor import multiple css files. Temporary disable port css in the master branch -// import "port/dist/styles.css"; - -export const Port = { - mounted() { - const worker = new Worker(); - const container = document.getElementById(this.el.id); - const locale = this.el.dataset.locale; - // const participant = this.el.dataset.participant; - this.assembly = new Assembly(worker, this); - this.assembly.visualisationEngine.start(container, locale); - this.assembly.processingEngine.start(); - }, - - destroyed() { - this.assembly.visualisationEngine.destroyed(); - }, - - send(command) { - if (isCommandSystemDonate(command)) { - this.handleDonation(command); - } else { - console.log( - "[System] received unknown command: " + JSON.stringify(command) - ); - } - }, - - handleDonation(command) { - console.log( - `[System] received donation: key=${command.key}, payload=${command.json_string}` - ); - this.pushEvent("donate", command); - }, -}; diff --git a/core/assets/js/processing_engine.js b/core/assets/js/processing_engine.js deleted file mode 100755 index fa13f498a..000000000 --- a/core/assets/js/processing_engine.js +++ /dev/null @@ -1,41 +0,0 @@ -export class ProcessingEngine { - constructor(worker) { - this.eventListener = (event) => { - event_string = Object.stringify(event); - console.log( - "[ProcessingEngine] No event listener registered for event: ", - event_string - ); - }; - - this.worker = worker; - this.worker.onerror = console.log; - this.worker.onmessage = (event) => { - console.log( - "[ProcessingEngine] Received event from worker: ", - event.data.eventType - ); - this.eventListener(event); - }; - } - - start() { - this.worker.postMessage({ eventType: "initialise" }); - } - - loadScript(script) { - this.worker.postMessage({ eventType: "loadScript", script }); - } - - firstRunCycle() { - this.worker.postMessage({ eventType: "firstRunCycle" }); - } - - nextRunCycle(response) { - this.worker.postMessage({ eventType: "nextRunCycle", response }); - } - - terminate() { - this.worker.terminate(); - } -} diff --git a/core/assets/js/processing_worker.js b/core/assets/js/processing_worker.js deleted file mode 100755 index ca0df5d8a..000000000 --- a/core/assets/js/processing_worker.js +++ /dev/null @@ -1,124 +0,0 @@ -let pyScript = undefined; - -onmessage = (event) => { - const { eventType } = event.data; - switch (eventType) { - case "initialise": - initialise().then(() => { - self.postMessage({ eventType: "initialiseDone" }); - }); - break; - - case "loadScript": - loadScript(event.data.script); - self.postMessage({ eventType: "loadScriptDone" }); - break; - - case "firstRunCycle": - pyScript = self.pyodide.runPython(pyWorker()); - runCycle(null); - break; - - case "nextRunCycle": - const { response } = event.data; - unwrap(response).then((userInput) => { - runCycle(userInput); - }); - break; - - default: - console.log("[ProcessingWorker] Received unsupported event: ", eventType); - } -}; - -function runCycle(userInput) { - cmd = pyScript.send(userInput); - self.postMessage({ - eventType: "runCycleDone", - cmd: cmd.toJs({ - create_proxies: false, - dict_converter: Object.fromEntries, - }), - }); -} - -function unwrap(response) { - return new Promise((resolve) => { - switch (response.prompt.type) { - case "file": - copyFileToPyFS(response.userInput, resolve); - break; - - default: - resolve(response.userInput); - } - }); -} - -function copyFileToPyFS(file, resolve) { - const reader = file.stream().getReader(); - const pyFile = self.pyodide.FS.open(file.name, "w"); - - const writeToPyFS = ({ done, value }) => { - if (done) { - resolve(file.name); - } else { - self.pyodide.FS.write(pyFile, value, 0, value.length); - reader.read().then(writeToPyFS); - } - }; - reader.read().then(writeToPyFS); -} - -function initialise() { - importScripts("https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js"); - - return loadPyodide({ - indexURL: "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/", - }).then((pyodide) => { - self.pyodide = pyodide; - return self.pyodide.loadPackage(["micropip", "numpy", "pandas"]); - }); -} - -function loadScript(script) { - console.log("[ProcessingWorker] loadScript"); - self.pyodide.runPython(script); -} - -function pyWorker() { - return ` - from collections.abc import Generator - import json - import html - import pandas as pd - - class ScriptWrapper(Generator): - def __init__(self, script): - self.script = script - def send(self, data): - if data == None: - return self.script.send(None) - else: - response = self.script.send(data) - if response["cmd"] == "result": - response["result"] = self.translate_result(response["result"]) - return response - def throw(self, type=None, value=None, traceback=None): - raise StopIteration - def translate_result(self, result): - data_output = [] - html_output = [] - for data in result: - html_output.append(f"""

{html.escape(data["title"])}

""") - df = data["data_frame"] - html_output.append(df.to_html(classes=["data-donation-extraction-results"], justify="left")) - data_output.append({"id": data["id"], "data_frame": df.to_json()}) - return { - "html": "\\n".join(html_output), - "data": json.dumps(data_output), - } - script = process() - ScriptWrapper(script) - `; -} diff --git a/core/assets/js/python_uploader.js b/core/assets/js/python_uploader.js deleted file mode 100644 index 457708835..000000000 --- a/core/assets/js/python_uploader.js +++ /dev/null @@ -1,94 +0,0 @@ -import { Tabbar } from "./tabbar"; - -export const PythonUploader = { - nextButtonSelector: "#tabbar-footer-item-file_selection", - - destroyed() { - this.worker && this.worker.terminate(); - }, - mounted() { - console.log("PythonUploader mounted"); - const uploader = this; - - // First hide the next button (requires selected file) - this.el.querySelector(this.nextButtonSelector).hidden = true; - this.el - .querySelector(".extract-data-button") - .addEventListener("click", () => { - this.el.querySelector(".select-file").classList.add("hidden"); - this.el.querySelector(".extract-data").classList.add("hidden"); - this.el.querySelector(".data-extraction").classList.remove("hidden"); - const script = this.el.getElementsByTagName("code")[0].innerText; - uploader.process(script).then((result) => { - uploader.result = result; - uploader.el - .querySelector(".no-extraction-data-yet") - .classList.add("hidden"); - uploader.el.querySelector(".donate-form").classList.remove("hidden"); - uploader.el.querySelector(".extracted").innerHTML = result.html; - uploader.el.querySelector("input[id='data']").value = result.data; - Tabbar.show("tab_" + this.el.dataset.afterCompletionTab, true); - this.el.querySelector(".extract-data").classList.remove("hidden"); - this.el.querySelector(".data-extraction").classList.add("hidden"); - }); - }); - - // Hook up the process button to the worker - const FileInputFactory = this.el.querySelector("input[type=file]"); - FileInputFactory.addEventListener("change", () => { - this.el.querySelector(".select-file").classList.add("hidden"); - this.el.querySelector(".extract-data").classList.remove("hidden"); - const filenameInfo = this.el.querySelector(".selected-filename"); - filenameInfo.innerText = FileInputFactory.files[0].name; - filenameInfo.classList.remove("hidden"); - }); - - this.el.querySelector(".reset-button").addEventListener("click", () => { - // clear current selected file - const FileInputFactory = this.el.querySelector("input[type=file]"); - FileInputFactory.type = "text"; - FileInputFactory.type = "file"; - - // show select file panel - this.el.querySelector(".select-file").classList.remove("hidden"); - this.el.querySelector(".extract-data").classList.add("hidden"); - }); - }, - process(script) { - return new Promise((resolve) => { - // Initialize the Python worker - const worker = new Worker("/js/pyworker.js"); - worker.onerror = console.log; - worker.onmessage = (event) => { - const { eventType } = event.data; - if (eventType === "initialized") { - worker.postMessage({ eventType: "loadScript", script }); - this.sendDataToWorker(worker); - } else if (eventType === "result") { - worker.terminate(); - resolve(event.data.result); - } - }; - }); - }, - sendDataToWorker(worker) { - const FileInputFactory = this.el.querySelector("input[type=file]"); - const file = FileInputFactory.files[0]; - const filename = file.name; - const reader = file.stream().getReader(); - const sendToWorker = ({ done, value }) => { - if (done) { - worker.postMessage({ eventType: "processData" }); - return; - } - worker.postMessage({ eventType: "data", chunk: value }); - reader.read().then(sendToWorker); - }; - worker.postMessage({ - eventType: "initData", - filename: filename, - size: file.size, - }); - reader.read().then(sendToWorker); - }, -}; diff --git a/core/assets/js/pyworker.js b/core/assets/js/pyworker.js deleted file mode 100755 index 4a5f970ab..000000000 --- a/core/assets/js/pyworker.js +++ /dev/null @@ -1,80 +0,0 @@ -importScripts("https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js"); - -var data = undefined; - -loadPyodide({ indexURL: "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/" }) - .then((pyodide) => { - self.pyodide = pyodide; - return self.pyodide.loadPackage(["micropip", "numpy", "pandas"]); - }) - .then(() => { - self.postMessage({ eventType: "initialized" }); - }); - -let file = undefined; -var filename = undefined; - -onmessage = (event) => { - const { eventType } = event.data; - if (eventType === "loadScript") { - self.pyodide.runPython(event.data.script); - } else if (eventType === "initData") { - filename = event.data.filename; - file = self.pyodide.FS.open(filename, "w"); - } else if (eventType === "data") { - self.pyodide.FS.write(file, event.data.chunk, 0, event.data.chunk.length); - } else if (eventType === "processData") { - const result = self.pyodide.runPython(` - def _process_data(): - import json - import html - import pandas as pd - - result = process("${filename}") - - if not result: - data_frame = pd.DataFrame() - data_frame["Messages"] = pd.Series(["Unfortunately, no data could be extracted from the selected file."], name="Messages") - result = [{"id": "important_feedback", "title": "Important feedback", "data_frame": data_frame}] - - data_output = [] - html_output = [] - for data in result: - html_output.append(f"""

{html.escape(data['title'])}

""") - df = data['data_frame'] - html_output.append(df.to_html(classes=["data-donation-extraction-results"], justify="left")) - data_output.append({"id": data["id"], "data_frame": df.to_json()}) - - return { - "html": "\\n".join(html_output), - "data": json.dumps(data_output), - } - _process_data()`); - self.postMessage({ - eventType: "result", - result: result.toJs({ - create_proxies: false, - dict_converter: Object.fromEntries, - }), - }); - } else if (eventType === "run_cycle") { - var prompt = undefined; - - if (generator == undefined) { - generator = self.pyodide.runPython(` - return process() - `); - prompt = generator.__next__(); - } else { - prompt = generator.send(event.data); - } - - self.postMessage({ - eventType: "prompt", - result: prompt.toJs({ - create_proxies: false, - dict_converter: Object.fromEntries, - }), - }); - } -}; diff --git a/core/assets/js/side_panel.js b/core/assets/js/side_panel.js index e3f3a3a70..9cb5cac3a 100644 --- a/core/assets/js/side_panel.js +++ b/core/assets/js/side_panel.js @@ -1,54 +1,42 @@ -const maxBottomMargin = 62; +const maxBottomMargin = 63; export const SidePanel = { mounted() { + this.mainContent = document.getElementById("main-content"); this.parent = document.getElementById(this.el.dataset.parent); this.panel = this.el.getElementsByClassName("panel")[0]; - this.panel.style = `height: 0px;`; - this.make_absolute(); + this.panel.style = `position: fixed; height: 0px; top: 0px`; this.updateFrame(); - window.addEventListener("tab-activated", (event) => { + new ResizeObserver(() => { this.updateFrame(); - }); + }).observe(this.parent); - window.addEventListener("scroll", (event) => { + this.mainContent.addEventListener("scroll", (event) => { this.updateFrame(); }); window.addEventListener("resize", (event) => { this.updateFrame(); }); - }, - make_absolute() { - this.el.classList.remove("relative"); - this.el.classList.add("absolute"); + + window.addEventListener("tab-activated", (event) => { + this.updateFrame(); + }); }, updated() { - this.make_absolute(); this.updateFrame(); }, updateFrame() { - this.updateHeight(); - this.updatePosition(); - }, - updateHeight() { - const bottomMarginDelta = Math.min( - maxBottomMargin, - document.documentElement.scrollHeight - - window.scrollY - - window.innerHeight - ); - const bottomMargin = maxBottomMargin - bottomMarginDelta; + const bottomDistance = + this.mainContent.scrollHeight - + this.mainContent.scrollTop - + window.innerHeight; + const bottomMargin = + maxBottomMargin - Math.min(maxBottomMargin, bottomDistance); + const topMargin = Math.max(0, this.parent.getBoundingClientRect().top); + const height = window.innerHeight - (topMargin + bottomMargin); - const height = - window.innerHeight - - (this.parent.getBoundingClientRect().top + bottomMargin); - this.panel.style = `height: ${height}px;`; - }, - updatePosition() { - const top = - Math.max(0, this.parent.getBoundingClientRect().top) + window.scrollY; - this.el.style = `top: ${top}px; right: 0px`; + this.panel.style = `position: fixed; height: ${height}px; top: ${topMargin}px`; }, }; diff --git a/core/assets/js/tabbar.js b/core/assets/js/tabbar.js index 18af8d362..eff8758db 100644 --- a/core/assets/js/tabbar.js +++ b/core/assets/js/tabbar.js @@ -63,7 +63,7 @@ export const Tabbar = { var isVisible = tab.id === nextTabId; setVisible(tab, isVisible); if (isVisible) { - tab.dispatchEvent(new Event("tab-activated", { bubbles: true})); + tab.dispatchEvent(new Event("tab-activated", { bubbles: true })); } }); diff --git a/core/assets/package-lock.json b/core/assets/package-lock.json index 92c1440fd..284dd54a6 100644 --- a/core/assets/package-lock.json +++ b/core/assets/package-lock.json @@ -15,7 +15,6 @@ "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html", "phoenix_live_view": "file:../deps/phoenix_live_view", - "port": "github:eyra/port#v1.0.0", "stringify": "^5.2.0", "topbar": "^0.1.4", "workbox-precaching": "^6.1.5", @@ -54,16 +53,14 @@ } }, "../deps/phoenix": { - "version": "0.0.1" + "version": "1.7.2", + "license": "MIT" }, "../deps/phoenix_html": { - "version": "0.0.1" + "version": "3.3.1" }, "../deps/phoenix_live_view": { - "version": "0.0.1" - }, - "node_modules/@adobe/css-tools": { - "version": "4.0.1", + "version": "0.18.18", "license": "MIT" }, "node_modules/@alloc/quick-lru": { @@ -136,6 +133,7 @@ }, "node_modules/@babel/code-frame": { "version": "7.18.6", + "dev": true, "license": "MIT", "dependencies": { "@babel/highlight": "^7.18.6" @@ -498,6 +496,7 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.19.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -540,6 +539,7 @@ }, "node_modules/@babel/highlight": { "version": "7.18.6", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", @@ -1646,6 +1646,7 @@ }, "node_modules/@babel/runtime": { "version": "7.20.6", + "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.13.11" @@ -1701,9 +1702,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz", - "integrity": "sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.2.tgz", + "integrity": "sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==", "dev": true, "funding": [ { @@ -1719,26 +1720,32 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.1.1" + "@csstools/css-tokenizer": "^2.2.1" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz", - "integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.1.tgz", + "integrity": "sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "engines": { "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz", - "integrity": "sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.5.tgz", + "integrity": "sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ==", "dev": true, "funding": [ { @@ -1754,8 +1761,8 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.3.0", - "@csstools/css-tokenizer": "^2.1.1" + "@csstools/css-parser-algorithms": "^2.3.2", + "@csstools/css-tokenizer": "^2.2.1" } }, "node_modules/@csstools/selector-specificity": { @@ -1995,190 +2002,6 @@ "string.prototype.matchall": "^4.0.6" } }, - "node_modules/@testing-library/dom": { - "version": "8.19.0", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^5.0.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@testing-library/dom/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/@testing-library/dom/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/dom/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "5.16.5", - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.0.1", - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=8", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/@testing-library/jest-dom/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react": { - "version": "13.4.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/user-event": { - "version": "13.5.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@types/aria-query": { - "version": "4.2.2", - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.1.20", "dev": true, @@ -2229,33 +2052,26 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/jest": { - "version": "27.5.2", - "license": "MIT", - "dependencies": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.11", "dev": true, "license": "MIT" }, "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.3.tgz", + "integrity": "sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==", "dev": true }, "node_modules/@types/node": { "version": "16.18.6", + "dev": true, "license": "MIT" }, "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz", + "integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==", "dev": true }, "node_modules/@types/parse-json": { @@ -2263,31 +2079,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "license": "MIT" - }, "node_modules/@types/q": { "version": "1.5.4", "dev": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "18.0.26", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.0.9", - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/resolve": { "version": "1.17.1", "dev": true, @@ -2296,17 +2092,6 @@ "@types/node": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.2", - "license": "MIT" - }, - "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.5", - "license": "MIT", - "dependencies": { - "@types/jest": "*" - } - }, "node_modules/@types/trusted-types": { "version": "2.0.2", "dev": true, @@ -2563,13 +2348,16 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "3.2.1", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -2615,13 +2403,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/aria-query": { - "version": "5.1.3", - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, "node_modules/arr-diff": { "version": "4.0.0", "dev": true, @@ -2799,16 +2580,6 @@ "postcss": "^8.1.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/babel-loader": { "version": "8.3.0", "dev": true, @@ -3355,6 +3126,7 @@ }, "node_modules/call-bind": { "version": "1.0.2", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -3477,6 +3249,7 @@ }, "node_modules/chalk": { "version": "2.4.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -3719,6 +3492,7 @@ }, "node_modules/color-convert": { "version": "1.9.3", + "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -3726,6 +3500,7 @@ }, "node_modules/color-name": { "version": "1.1.3", + "dev": true, "license": "MIT" }, "node_modules/color-string": { @@ -4021,9 +3796,9 @@ } }, "node_modules/css-functions-list": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.1.0.tgz", - "integrity": "sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.0.tgz", + "integrity": "sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==", "dev": true, "engines": { "node": ">=12.22" @@ -4150,10 +3925,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css.escape": { - "version": "1.5.1", - "license": "MIT" - }, "node_modules/cssesc": { "version": "3.0.0", "dev": true, @@ -4398,10 +4169,6 @@ "node": ">=0.10.0" } }, - "node_modules/csstype": { - "version": "3.1.1", - "license": "MIT" - }, "node_modules/cyclist": { "version": "1.0.1", "dev": true, @@ -4468,34 +4235,6 @@ "version": "1.0.2", "license": "MIT" }, - "node_modules/deep-equal": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.2", - "get-intrinsic": "^1.1.3", - "is-arguments": "^1.1.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-equal/node_modules/isarray": { - "version": "2.0.5", - "license": "MIT" - }, "node_modules/deepmerge": { "version": "4.2.2", "dev": true, @@ -4506,6 +4245,7 @@ }, "node_modules/define-properties": { "version": "1.1.4", + "dev": true, "license": "MIT", "dependencies": { "has-property-descriptors": "^1.0.0", @@ -4595,13 +4335,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/diff-sequences": { - "version": "27.5.1", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/diffie-hellman": { "version": "5.0.3", "dev": true, @@ -4633,10 +4366,6 @@ "dev": true, "license": "MIT" }, - "node_modules/dom-accessibility-api": { - "version": "0.5.14", - "license": "MIT" - }, "node_modules/dom-serializer": { "version": "0.2.2", "dev": true, @@ -4846,27 +4575,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-get-iterator": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.0", - "has-symbols": "^1.0.1", - "is-arguments": "^1.1.0", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.5", - "isarray": "^2.0.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-get-iterator/node_modules/isarray": { - "version": "2.0.5", - "license": "MIT" - }, "node_modules/es-to-primitive": { "version": "1.2.1", "dev": true, @@ -4893,6 +4601,7 @@ }, "node_modules/escape-string-regexp": { "version": "1.0.5", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -5171,9 +4880,9 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", - "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5473,13 +5182,6 @@ "readable-stream": "^2.3.6" } }, - "node_modules/for-each": { - "version": "0.3.3", - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/for-in": { "version": "1.0.2", "dev": true, @@ -5580,6 +5282,7 @@ }, "node_modules/function-bind": { "version": "1.1.1", + "dev": true, "license": "MIT" }, "node_modules/function.prototype.name": { @@ -5601,6 +5304,7 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5624,6 +5328,7 @@ }, "node_modules/get-intrinsic": { "version": "1.1.3", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -5777,16 +5482,6 @@ "dev": true, "license": "MIT" }, - "node_modules/gopd": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.10", "dev": true, @@ -5833,6 +5528,7 @@ }, "node_modules/has": { "version": "1.0.3", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1" @@ -5843,6 +5539,7 @@ }, "node_modules/has-bigints": { "version": "1.0.2", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5850,6 +5547,7 @@ }, "node_modules/has-flag": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -5857,6 +5555,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.0", + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.1" @@ -5867,6 +5566,7 @@ }, "node_modules/has-symbols": { "version": "1.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5877,6 +5577,7 @@ }, "node_modules/has-tostringtag": { "version": "1.0.0", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -6215,6 +5916,7 @@ }, "node_modules/indent-string": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6300,20 +6002,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "dev": true, @@ -6321,6 +6009,7 @@ }, "node_modules/is-bigint": { "version": "1.0.4", + "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.1" @@ -6342,6 +6031,7 @@ }, "node_modules/is-boolean-object": { "version": "1.1.0", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.0" @@ -6360,6 +6050,7 @@ }, "node_modules/is-callable": { "version": "1.2.7", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6416,6 +6107,7 @@ }, "node_modules/is-date-object": { "version": "1.0.5", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -6492,13 +6184,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-map": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-module": { "version": "1.0.0", "dev": true, @@ -6528,6 +6213,7 @@ }, "node_modules/is-number-object": { "version": "1.0.4", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6576,6 +6262,7 @@ }, "node_modules/is-regex": { "version": "1.1.4", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -6593,13 +6280,6 @@ "dev": true, "license": "ISC" }, - "node_modules/is-set": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "dev": true, @@ -6624,6 +6304,7 @@ }, "node_modules/is-string": { "version": "1.0.7", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -6648,6 +6329,7 @@ }, "node_modules/is-symbol": { "version": "1.0.3", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.1" @@ -6659,30 +6341,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-weakref": { "version": "1.0.2", "dev": true, @@ -6694,17 +6352,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakset": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-windows": { "version": "1.0.2", "dev": true, @@ -6820,155 +6467,6 @@ "node": ">=8" } }, - "node_modules/jest-diff": { - "version": "27.5.1", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-get-type": { - "version": "27.5.1", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/jest-matcher-utils/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker": { "version": "25.5.0", "dev": true, @@ -7022,6 +6520,7 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -7114,9 +6613,9 @@ } }, "node_modules/known-css-properties": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.27.0.tgz", - "integrity": "sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.28.0.tgz", + "integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==", "dev": true }, "node_modules/last-call-webpack-plugin": { @@ -7224,32 +6723,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lottie-react": { - "version": "2.3.1", - "license": "MIT", - "dependencies": { - "lottie-web": "^5.9.4" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/lottie-web": { - "version": "5.10.0", - "license": "MIT" - }, "node_modules/lower-case": { "version": "1.1.4", "license": "MIT" @@ -7262,13 +6735,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lz-string": { - "version": "1.4.4", - "license": "WTFPL", - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.25.9", "dev": true, @@ -7396,49 +6862,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/meow/node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "dev": true, - "dependencies": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/meow/node_modules/yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", @@ -7503,7 +6926,9 @@ }, "node_modules/min-indent": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, "engines": { "node": ">=4" } @@ -7861,9 +7286,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -7984,27 +7409,15 @@ }, "node_modules/object-inspect": { "version": "1.12.2", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.5", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8023,6 +7436,7 @@ }, "node_modules/object.assign": { "version": "4.1.4", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -8358,23 +7772,6 @@ "node": ">=6" } }, - "node_modules/port": { - "version": "0.1.0", - "resolved": "git+ssh://git@github.com/eyra/port.git#4949d714e03ebe996c192450e86649a4203792ee", - "dependencies": { - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "@types/jest": "^27.5.2", - "@types/node": "^16.11.59", - "@types/react": "^18.0.21", - "@types/react-dom": "^18.0.6", - "lodash": "^4.17.21", - "lottie-react": "^2.3.1", - "typescript": "^4.4.3", - "web-vitals": "^2.1.4" - } - }, "node_modules/posix-character-classes": { "version": "0.1.1", "dev": true, @@ -8384,9 +7781,9 @@ } }, "node_modules/postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -10362,37 +9759,15 @@ "prettier": "^2.0.0" } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", + "node_modules/pretty-bytes": { + "version": "5.6.0", + "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/process": { @@ -10555,33 +9930,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/react": { - "version": "18.2.0", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "license": "MIT" - }, "node_modules/read-cache": { "version": "1.0.0", "dev": true, @@ -10747,14 +10095,31 @@ } }, "node_modules/redent": { - "version": "3.0.0", - "license": "MIT", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/redent/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/regenerate": { @@ -10775,6 +10140,7 @@ }, "node_modules/regenerator-runtime": { "version": "0.13.11", + "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { @@ -10799,6 +10165,7 @@ }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -11168,14 +10535,6 @@ "dev": true, "license": "ISC" }, - "node_modules/scheduler": { - "version": "0.23.0", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/schema-utils": { "version": "2.7.1", "dev": true, @@ -11277,6 +10636,7 @@ }, "node_modules/side-channel": { "version": "1.0.4", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.0", @@ -11594,9 +10954,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/split-string": { @@ -11827,13 +11187,18 @@ } }, "node_modules/strip-indent": { - "version": "3.0.0", - "license": "MIT", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, "dependencies": { - "min-indent": "^1.0.0" + "min-indent": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/style-search": { @@ -11904,22 +11269,22 @@ } }, "node_modules/stylelint": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.10.1.tgz", - "integrity": "sha512-CYkzYrCFfA/gnOR+u9kJ1PpzwG10WLVnoxHDuBA/JiwGqdM9+yx9+ou6SE/y9YHtfv1mcLo06fdadHTOx4gBZQ==", + "version": "15.10.3", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.10.3.tgz", + "integrity": "sha512-aBQMMxYvFzJJwkmg+BUUg3YfPyeuCuKo2f+LOw7yYbU8AZMblibwzp9OV4srHVeQldxvSFdz0/Xu8blq2AesiA==", "dev": true, "dependencies": { - "@csstools/css-parser-algorithms": "^2.3.0", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/media-query-list-parser": "^2.1.2", + "@csstools/css-parser-algorithms": "^2.3.1", + "@csstools/css-tokenizer": "^2.2.0", + "@csstools/media-query-list-parser": "^2.1.4", "@csstools/selector-specificity": "^3.0.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^8.2.0", - "css-functions-list": "^3.1.0", + "css-functions-list": "^3.2.0", "css-tree": "^2.3.1", "debug": "^4.3.4", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.1", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^6.0.1", "global-modules": "^2.0.0", @@ -11930,13 +11295,13 @@ "import-lazy": "^4.0.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.27.0", + "known-css-properties": "^0.28.0", "mathml-tag-names": "^2.1.3", "meow": "^10.1.5", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.4.24", + "postcss": "^8.4.27", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.0.13", @@ -12014,14 +11379,14 @@ } }, "node_modules/stylelint/node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "dependencies": { - "import-fresh": "^3.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", + "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "engines": { @@ -12029,6 +11394,14 @@ }, "funding": { "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/stylelint/node_modules/css-tree": { @@ -12207,9 +11580,9 @@ } }, "node_modules/stylelint/node_modules/signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "engines": { "node": ">=14" @@ -12239,6 +11612,21 @@ "node": ">=8.0" } }, + "node_modules/stylelint/node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/stylelint/node_modules/write-file-atomic": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", @@ -12319,6 +11707,7 @@ }, "node_modules/supports-color": { "version": "5.5.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -12960,6 +12349,7 @@ }, "node_modules/typescript": { "version": "4.9.3", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13408,10 +12798,6 @@ "node": ">=0.10" } }, - "node_modules/web-vitals": { - "version": "2.1.4", - "license": "Apache-2.0" - }, "node_modules/webpack": { "version": "4.41.5", "dev": true, @@ -13590,6 +12976,7 @@ }, "node_modules/which-boxed-primitive": { "version": "1.0.2", + "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.0.1", @@ -13602,42 +12989,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-collection": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-module": { "version": "2.0.0", "dev": true, "license": "ISC" }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/workbox-background-sync": { "version": "6.5.4", "dev": true, @@ -14170,9 +13526,6 @@ } }, "dependencies": { - "@adobe/css-tools": { - "version": "4.0.1" - }, "@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -14214,6 +13567,7 @@ }, "@babel/code-frame": { "version": "7.18.6", + "dev": true, "requires": { "@babel/highlight": "^7.18.6" } @@ -14451,7 +13805,8 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1" + "version": "7.19.1", + "dev": true }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -14478,6 +13833,7 @@ }, "@babel/highlight": { "version": "7.18.6", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -15120,6 +14476,7 @@ }, "@babel/runtime": { "version": "7.20.6", + "dev": true, "requires": { "regenerator-runtime": "^0.13.11" } @@ -15159,22 +14516,22 @@ } }, "@csstools/css-parser-algorithms": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz", - "integrity": "sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.2.tgz", + "integrity": "sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==", "dev": true, "requires": {} }, "@csstools/css-tokenizer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz", - "integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.1.tgz", + "integrity": "sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg==", "dev": true }, "@csstools/media-query-list-parser": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz", - "integrity": "sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.5.tgz", + "integrity": "sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ==", "dev": true, "requires": {} }, @@ -15331,116 +14688,6 @@ "string.prototype.matchall": "^4.0.6" } }, - "@testing-library/dom": { - "version": "8.19.0", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^5.0.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", - "pretty-format": "^27.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "has-flag": { - "version": "4.0.0" - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@testing-library/jest-dom": { - "version": "5.16.5", - "requires": { - "@adobe/css-tools": "^4.0.1", - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "has-flag": { - "version": "4.0.0" - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@testing-library/react": { - "version": "13.4.0", - "requires": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" - } - }, - "@testing-library/user-event": { - "version": "13.5.0", - "requires": { - "@babel/runtime": "^7.12.5" - } - }, - "@types/aria-query": { - "version": "4.2.2" - }, "@types/babel__core": { "version": "7.1.20", "dev": true, @@ -15486,57 +14733,34 @@ "version": "0.0.39", "dev": true }, - "@types/jest": { - "version": "27.5.2", - "requires": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" - } - }, "@types/json-schema": { "version": "7.0.11", "dev": true }, "@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.3.tgz", + "integrity": "sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==", "dev": true }, "@types/node": { - "version": "16.18.6" + "version": "16.18.6", + "dev": true }, "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz", + "integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==", "dev": true }, "@types/parse-json": { "version": "4.0.0", "dev": true }, - "@types/prop-types": { - "version": "15.7.5" - }, "@types/q": { "version": "1.5.4", "dev": true }, - "@types/react": { - "version": "18.0.26", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-dom": { - "version": "18.0.9", - "requires": { - "@types/react": "*" - } - }, "@types/resolve": { "version": "1.17.1", "dev": true, @@ -15544,15 +14768,6 @@ "@types/node": "*" } }, - "@types/scheduler": { - "version": "0.16.2" - }, - "@types/testing-library__jest-dom": { - "version": "5.14.5", - "requires": { - "@types/jest": "*" - } - }, "@types/trusted-types": { "version": "2.0.2", "dev": true @@ -15755,10 +14970,14 @@ "dev": true }, "ansi-regex": { - "version": "5.0.1" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "3.2.1", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -15794,12 +15013,6 @@ "sprintf-js": "~1.0.2" } }, - "aria-query": { - "version": "5.1.3", - "requires": { - "deep-equal": "^2.0.5" - } - }, "arr-diff": { "version": "4.0.0", "dev": true @@ -15909,9 +15122,6 @@ "postcss-value-parser": "^4.2.0" } }, - "available-typed-arrays": { - "version": "1.0.5" - }, "babel-loader": { "version": "8.3.0", "dev": true, @@ -16299,6 +15509,7 @@ }, "call-bind": { "version": "1.0.2", + "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -16375,6 +15586,7 @@ }, "chalk": { "version": "2.4.2", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -16539,12 +15751,14 @@ }, "color-convert": { "version": "1.9.3", + "dev": true, "requires": { "color-name": "1.1.3" } }, "color-name": { - "version": "1.1.3" + "version": "1.1.3", + "dev": true }, "color-string": { "version": "1.5.4", @@ -16764,9 +15978,9 @@ } }, "css-functions-list": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.1.0.tgz", - "integrity": "sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.0.tgz", + "integrity": "sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==", "dev": true }, "css-loader": { @@ -16846,9 +16060,6 @@ "version": "3.4.2", "dev": true }, - "css.escape": { - "version": "1.5.1" - }, "cssesc": { "version": "3.0.0", "dev": true @@ -17009,9 +16220,6 @@ } } }, - "csstype": { - "version": "3.1.1" - }, "cyclist": { "version": "1.0.1", "dev": true @@ -17052,37 +16260,13 @@ "deep-diff": { "version": "1.0.2" }, - "deep-equal": { - "version": "2.1.0", - "requires": { - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.2", - "get-intrinsic": "^1.1.3", - "is-arguments": "^1.1.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.8" - }, - "dependencies": { - "isarray": { - "version": "2.0.5" - } - } - }, "deepmerge": { "version": "4.2.2", "dev": true }, "define-properties": { "version": "1.1.4", + "dev": true, "requires": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -17141,9 +16325,6 @@ "version": "1.2.2", "dev": true }, - "diff-sequences": { - "version": "27.5.1" - }, "diffie-hellman": { "version": "5.0.3", "dev": true, @@ -17170,9 +16351,6 @@ "version": "1.1.3", "dev": true }, - "dom-accessibility-api": { - "version": "0.5.14" - }, "dom-serializer": { "version": "0.2.2", "dev": true, @@ -17332,24 +16510,6 @@ "unbox-primitive": "^1.0.2" } }, - "es-get-iterator": { - "version": "1.1.2", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.0", - "has-symbols": "^1.0.1", - "is-arguments": "^1.1.0", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.5", - "isarray": "^2.0.5" - }, - "dependencies": { - "isarray": { - "version": "2.0.5" - } - } - }, "es-to-primitive": { "version": "1.2.1", "dev": true, @@ -17364,7 +16524,8 @@ "dev": true }, "escape-string-regexp": { - "version": "1.0.5" + "version": "1.0.5", + "dev": true }, "eslint-scope": { "version": "4.0.3", @@ -17552,9 +16713,9 @@ "dev": true }, "fast-glob": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", - "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -17764,12 +16925,6 @@ "readable-stream": "^2.3.6" } }, - "for-each": { - "version": "0.3.3", - "requires": { - "is-callable": "^1.1.3" - } - }, "for-in": { "version": "1.0.2", "dev": true @@ -17834,7 +16989,8 @@ "optional": true }, "function-bind": { - "version": "1.1.1" + "version": "1.1.1", + "dev": true }, "function.prototype.name": { "version": "1.1.5", @@ -17847,7 +17003,8 @@ } }, "functions-have-names": { - "version": "1.2.3" + "version": "1.2.3", + "dev": true }, "gensync": { "version": "1.0.0-beta.2", @@ -17859,6 +17016,7 @@ }, "get-intrinsic": { "version": "1.1.3", + "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -17965,12 +17123,6 @@ "version": "0.1.4", "dev": true }, - "gopd": { - "version": "1.0.1", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, "graceful-fs": { "version": "4.2.10", "dev": true @@ -18005,27 +17157,33 @@ }, "has": { "version": "1.0.3", + "dev": true, "requires": { "function-bind": "^1.1.1" } }, "has-bigints": { - "version": "1.0.2" + "version": "1.0.2", + "dev": true }, "has-flag": { - "version": "3.0.0" + "version": "3.0.0", + "dev": true }, "has-property-descriptors": { "version": "1.0.0", + "dev": true, "requires": { "get-intrinsic": "^1.1.1" } }, "has-symbols": { - "version": "1.0.3" + "version": "1.0.3", + "dev": true }, "has-tostringtag": { "version": "1.0.0", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -18250,7 +17408,8 @@ "dev": true }, "indent-string": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true }, "indexes-of": { "version": "1.0.1", @@ -18309,19 +17468,13 @@ } } }, - "is-arguments": { - "version": "1.1.1", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, "is-arrayish": { "version": "0.2.1", "dev": true }, "is-bigint": { "version": "1.0.4", + "dev": true, "requires": { "has-bigints": "^1.0.1" } @@ -18335,6 +17488,7 @@ }, "is-boolean-object": { "version": "1.1.0", + "dev": true, "requires": { "call-bind": "^1.0.0" } @@ -18344,7 +17498,8 @@ "dev": true }, "is-callable": { - "version": "1.2.7" + "version": "1.2.7", + "dev": true }, "is-color-stop": { "version": "1.1.0", @@ -18383,6 +17538,7 @@ }, "is-date-object": { "version": "1.0.5", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -18427,9 +17583,6 @@ "is-extglob": "^2.1.1" } }, - "is-map": { - "version": "2.0.2" - }, "is-module": { "version": "1.0.0", "dev": true @@ -18455,7 +17608,8 @@ } }, "is-number-object": { - "version": "1.0.4" + "version": "1.0.4", + "dev": true }, "is-obj": { "version": "2.0.0", @@ -18474,6 +17628,7 @@ }, "is-regex": { "version": "1.1.4", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -18483,9 +17638,6 @@ "version": "1.1.0", "dev": true }, - "is-set": { - "version": "2.0.2" - }, "is-shared-array-buffer": { "version": "1.0.2", "dev": true, @@ -18499,6 +17651,7 @@ }, "is-string": { "version": "1.0.7", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -18512,23 +17665,11 @@ }, "is-symbol": { "version": "1.0.3", + "dev": true, "requires": { "has-symbols": "^1.0.1" } }, - "is-typed-array": { - "version": "1.1.10", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "is-weakmap": { - "version": "2.0.1" - }, "is-weakref": { "version": "1.0.2", "dev": true, @@ -18536,13 +17677,6 @@ "call-bind": "^1.0.2" } }, - "is-weakset": { - "version": "2.0.2", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, "is-windows": { "version": "1.0.2", "dev": true @@ -18612,93 +17746,6 @@ } } }, - "jest-diff": { - "version": "27.5.1", - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "has-flag": { - "version": "4.0.0" - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-get-type": { - "version": "27.5.1" - }, - "jest-matcher-utils": { - "version": "27.5.1", - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "has-flag": { - "version": "4.0.0" - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "jest-worker": { "version": "25.5.0", "dev": true, @@ -18737,7 +17784,8 @@ } }, "js-tokens": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true }, "js-yaml": { "version": "3.14.0", @@ -18792,9 +17840,9 @@ "dev": true }, "known-css-properties": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.27.0.tgz", - "integrity": "sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.28.0.tgz", + "integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==", "dev": true }, "last-call-webpack-plugin": { @@ -18874,22 +17922,6 @@ "version": "4.5.0", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "peer": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lottie-react": { - "version": "2.3.1", - "requires": { - "lottie-web": "^5.9.4" - } - }, - "lottie-web": { - "version": "5.10.0" - }, "lower-case": { "version": "1.1.4" }, @@ -18900,9 +17932,6 @@ "yallist": "^3.0.2" } }, - "lz-string": { - "version": "1.4.4" - }, "magic-string": { "version": "0.25.9", "dev": true, @@ -18990,31 +18019,6 @@ "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", "dev": true }, - "indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true - }, - "redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "dev": true, - "requires": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - } - }, - "strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dev": true, - "requires": { - "min-indent": "^1.0.1" - } - }, "yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", @@ -19065,7 +18069,10 @@ } }, "min-indent": { - "version": "1.0.1" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true }, "mini-css-extract-plugin": { "version": "0.9.0", @@ -19332,9 +18339,9 @@ } }, "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -19418,17 +18425,12 @@ "dev": true }, "object-inspect": { - "version": "1.12.2" - }, - "object-is": { - "version": "1.1.5", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } + "version": "1.12.2", + "dev": true }, "object-keys": { - "version": "1.1.1" + "version": "1.1.1", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -19439,6 +18441,7 @@ }, "object.assign": { "version": "4.1.4", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -19655,31 +18658,14 @@ "find-up": "^3.0.0" } }, - "port": { - "version": "git+ssh://git@github.com/eyra/port.git#4949d714e03ebe996c192450e86649a4203792ee", - "from": "port@github:eyra/port#v1.0.0", - "requires": { - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "@types/jest": "^27.5.2", - "@types/node": "^16.11.59", - "@types/react": "^18.0.21", - "@types/react-dom": "^18.0.6", - "lodash": "^4.17.21", - "lottie-react": "^2.3.1", - "typescript": "^4.4.3", - "web-vitals": "^2.1.4" - } - }, "posix-character-classes": { "version": "0.1.1", "dev": true }, "postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", @@ -20919,19 +19905,6 @@ "version": "5.6.0", "dev": true }, - "pretty-format": { - "version": "27.5.1", - "requires": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0" - } - } - }, "process": { "version": "0.11.10", "dev": true @@ -21042,24 +20015,6 @@ "safe-buffer": "^5.1.0" } }, - "react": { - "version": "18.2.0", - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-dom": { - "version": "18.2.0", - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - } - }, - "react-is": { - "version": "17.0.2" - }, "read-cache": { "version": "1.0.0", "dev": true, @@ -21176,10 +20131,21 @@ } }, "redent": { - "version": "3.0.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "dependencies": { + "indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true + } } }, "regenerate": { @@ -21194,7 +20160,8 @@ } }, "regenerator-runtime": { - "version": "0.13.11" + "version": "0.13.11", + "dev": true }, "regenerator-transform": { "version": "0.15.1", @@ -21213,6 +20180,7 @@ }, "regexp.prototype.flags": { "version": "1.4.3", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", @@ -21447,13 +20415,6 @@ "version": "1.2.4", "dev": true }, - "scheduler": { - "version": "0.23.0", - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, "schema-utils": { "version": "2.7.1", "dev": true, @@ -21522,6 +20483,7 @@ }, "side-channel": { "version": "1.0.4", + "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -21762,9 +20724,9 @@ } }, "spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "split-string": { @@ -21939,9 +20901,12 @@ "dev": true }, "strip-indent": { - "version": "3.0.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, "requires": { - "min-indent": "^1.0.0" + "min-indent": "^1.0.1" } }, "style-search": { @@ -21989,22 +20954,22 @@ } }, "stylelint": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.10.1.tgz", - "integrity": "sha512-CYkzYrCFfA/gnOR+u9kJ1PpzwG10WLVnoxHDuBA/JiwGqdM9+yx9+ou6SE/y9YHtfv1mcLo06fdadHTOx4gBZQ==", + "version": "15.10.3", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.10.3.tgz", + "integrity": "sha512-aBQMMxYvFzJJwkmg+BUUg3YfPyeuCuKo2f+LOw7yYbU8AZMblibwzp9OV4srHVeQldxvSFdz0/Xu8blq2AesiA==", "dev": true, "requires": { - "@csstools/css-parser-algorithms": "^2.3.0", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/media-query-list-parser": "^2.1.2", + "@csstools/css-parser-algorithms": "^2.3.1", + "@csstools/css-tokenizer": "^2.2.0", + "@csstools/media-query-list-parser": "^2.1.4", "@csstools/selector-specificity": "^3.0.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^8.2.0", - "css-functions-list": "^3.1.0", + "css-functions-list": "^3.2.0", "css-tree": "^2.3.1", "debug": "^4.3.4", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.1", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^6.0.1", "global-modules": "^2.0.0", @@ -22015,13 +20980,13 @@ "import-lazy": "^4.0.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.27.0", + "known-css-properties": "^0.28.0", "mathml-tag-names": "^2.1.3", "meow": "^10.1.5", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.4.24", + "postcss": "^8.4.27", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.0.13", @@ -22064,14 +21029,14 @@ } }, "cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "requires": { - "import-fresh": "^3.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", + "parse-json": "^5.2.0", "path-type": "^4.0.0" } }, @@ -22201,9 +21166,9 @@ "dev": true }, "signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true }, "slash": { @@ -22221,6 +21186,14 @@ "is-number": "^7.0.0" } }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "optional": true, + "peer": true + }, "write-file-atomic": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", @@ -22295,6 +21268,7 @@ }, "supports-color": { "version": "5.5.0", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -22743,7 +21717,8 @@ "dev": true }, "typescript": { - "version": "4.9.3" + "version": "4.9.3", + "dev": true }, "uglify-js": { "version": "3.0.28", @@ -23053,9 +22028,6 @@ } } }, - "web-vitals": { - "version": "2.1.4" - }, "webpack": { "version": "4.41.5", "dev": true, @@ -23183,6 +22155,7 @@ }, "which-boxed-primitive": { "version": "1.0.2", + "dev": true, "requires": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -23191,30 +22164,10 @@ "is-symbol": "^1.0.3" } }, - "which-collection": { - "version": "1.0.1", - "requires": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - } - }, "which-module": { "version": "2.0.0", "dev": true }, - "which-typed-array": { - "version": "1.1.9", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, "workbox-background-sync": { "version": "6.5.4", "dev": true, diff --git a/core/assets/package.json b/core/assets/package.json index 919ad46d7..094b39c6f 100644 --- a/core/assets/package.json +++ b/core/assets/package.json @@ -4,7 +4,8 @@ "license": "MIT", "scripts": { "deploy": "NODE_ENV=production webpack --mode production", - "watch": "webpack --mode development --watch" + "watch": "webpack --mode development --watch", + "build": "webpack --mode development" }, "dependencies": { "@ryangjchandler/spruce": "^2.7.1", @@ -16,7 +17,6 @@ "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html", "phoenix_live_view": "file:../deps/phoenix_live_view", - "port": "github:eyra/port#v1.0.0", "stringify": "^5.2.0", "topbar": "^0.1.4", "workbox-precaching": "^6.1.5", diff --git a/core/assets/static/images/icons/facebook.svg b/core/assets/static/images/icons/facebook.svg new file mode 100644 index 000000000..7d658d953 --- /dev/null +++ b/core/assets/static/images/icons/facebook.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/core/assets/static/images/icons/instagram.svg b/core/assets/static/images/icons/instagram.svg new file mode 100644 index 000000000..37751bca9 --- /dev/null +++ b/core/assets/static/images/icons/instagram.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/core/assets/static/images/icons/ready.svg b/core/assets/static/images/icons/ready.svg new file mode 100644 index 000000000..a5f0811bb --- /dev/null +++ b/core/assets/static/images/icons/ready.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/assets/static/landing_page/img/odissei.png b/core/assets/static/landing_page/img/odissei.png new file mode 100644 index 000000000..28ff16e69 Binary files /dev/null and b/core/assets/static/landing_page/img/odissei.png differ diff --git a/core/assets/static/landing_page/img/rug.png b/core/assets/static/landing_page/img/rug.png new file mode 100644 index 000000000..17f9ebe1b Binary files /dev/null and b/core/assets/static/landing_page/img/rug.png differ diff --git a/core/assets/tailwind.config.js b/core/assets/tailwind.config.js index d7deef92b..4217de3a0 100644 --- a/core/assets/tailwind.config.js +++ b/core/assets/tailwind.config.js @@ -50,7 +50,7 @@ module.exports = { "square-border-striped": "url('/images/square_border_striped.png')", }, spacing: { - "1px": "1px", + aap: "1px", "2px": "2px", "3px": "3px", "5px": "5px", @@ -75,7 +75,7 @@ module.exports = { "200px": "200px", "224px": "224px", "248px": "248px", - 15: "60x", + 15: "60px", 30: "120px", 34: "136px", 35: "140px", @@ -112,6 +112,7 @@ module.exports = { sheet: "760px", popup: "480px", "side-panel": "535px", + "left-column": "368px", "popup-sm": "520px", "popup-md": "730px", "popup-lg": "1228px", @@ -239,7 +240,7 @@ module.exports = { paddingBottom: "constant(safe-area-inset-bottom)", paddingBottom: "env(safe-area-inset-bottom)", }, - ".scrollbar-hide": { + ".scrollbar-hidden": { /* Firefox */ "scrollbar-width": "thin", diff --git a/core/assets/webpack.config.js b/core/assets/webpack.config.js index 1760ed79d..218cdb150 100644 --- a/core/assets/webpack.config.js +++ b/core/assets/webpack.config.js @@ -19,8 +19,6 @@ module.exports = (env, options) => { }, entry: { app: glob.sync("./vendor/**/*.js").concat(["./js/app.js"]), - pyworker: ["./js/pyworker.js"], - processing_worker: ["./js/processing_worker.js"], // 'sw': ['./js/sw.js'] }, output: { diff --git a/core/bundles/link/lib/console/page.ex b/core/bundles/link/lib/console/page.ex index 90d56a0a3..aab5dd85b 100644 --- a/core/bundles/link/lib/console/page.ex +++ b/core/bundles/link/lib/console/page.ex @@ -19,7 +19,7 @@ defmodule Link.Console.Page do alias Systems.NextAction def mount(_params, _session, %{assigns: %{current_user: user} = assigns} = socket) do - preload = Campaign.Model.preload_graph(:full) + preload = Campaign.Model.preload_graph(:down) next_best_action = NextAction.Public.next_best_action(user) diff --git a/core/bundles/link/lib/marketplace/page.ex b/core/bundles/link/lib/marketplace/page.ex index 124289048..3a262a700 100644 --- a/core/bundles/link/lib/marketplace/page.ex +++ b/core/bundles/link/lib/marketplace/page.ex @@ -19,7 +19,7 @@ defmodule Link.Marketplace.Page do next_best_action = NextAction.Public.next_best_action(user) user = socket.assigns[:current_user] - preload = Campaign.Model.preload_graph(:full) + preload = Campaign.Model.preload_graph(:down) subject_campaigns = Campaign.Public.list_subject_campaigns(user, preload: preload) excluded_campaigns = Campaign.Public.list_excluded_campaigns(subject_campaigns) diff --git a/core/bundles/link/seeds.exs b/core/bundles/link/seeds.exs index 14e2ce202..0ffd58e0a 100644 --- a/core/bundles/link/seeds.exs +++ b/core/bundles/link/seeds.exs @@ -15,11 +15,11 @@ student_count = 1500 researcher_count = 100 researchers_per_campaign = 5 lab_count = 200 -survey_count = 600 +alliance_count = 600 time_slots_per_lab = 20 seats_per_time_slot = 20 -survey_url = "https://vuamsterdam.eu.qualtrics.com/jfe/form/SV_4Po8iTxbvcxtuaW" +alliance_url = "https://vuamsterdam.eu.qualtrics.com/jfe/form/SV_4Po8iTxbvcxtuaW" images = [ "raw_url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1600614908054-57142d1eec2b%3Fixid%3DMnwyMTY0MzZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE2Mjc4MDc5MzY%26ixlib%3Drb-1.2.1&username=seonghojang95&name=Seongho+Jang&blur_hash=LKLy%5B%2AMd0L%3FG%3B0XSE2xDyC%25f%24zI%3B", @@ -184,9 +184,9 @@ researcher = {:ok, surveys} = Core.Repo.transaction(fn -> - for _ <- 1..survey_count do + for _ <- 1..alliance_count do %{ - type: :survey_tool, + type: :alliance_tool, promotion: %{ title: Faker.Lorem.sentence() <> " (survey)", subtitle: Faker.Lorem.sentence(), @@ -195,8 +195,8 @@ researcher = marks: ["vu"], plugin: "survey" }, - survey_tool: %{ - survey_url: Faker.Internet.url(), + alliance_tool: %{ + url: Faker.Internet.url(), # desktop_enabled: true, # phone_enabled: true, # tablet_enabled: true, @@ -250,7 +250,7 @@ Core.Repo.transaction( ) ) - if tool_type == :survey_tool do + if tool_type == :alliance_tool do participant_count = :random.uniform(tool.subject_count) for student <- Enum.take_random(students, participant_count) do diff --git a/core/bundles/next/bundle.ex b/core/bundles/next/bundle.ex index 85856f891..22dabc3a2 100644 --- a/core/bundles/next/bundle.ex +++ b/core/bundles/next/bundle.ex @@ -26,6 +26,7 @@ defmodule Next.Bundle do scope "/", Next do pipe_through([:browser, :require_authenticated_user]) live("/console", Console.Page) + live("/next", Console.Page) end end end diff --git a/core/bundles/next/lib/console/page.ex b/core/bundles/next/lib/console/page.ex index 23e7455c0..a9d36a1a4 100644 --- a/core/bundles/next/lib/console/page.ex +++ b/core/bundles/next/lib/console/page.ex @@ -21,7 +21,7 @@ defmodule Next.Console.Page do project_items = user - |> Project.Public.list_owned_projects(preload: Project.Model.preload_graph(:full)) + |> Project.Public.list_owned_projects(preload: Project.Model.preload_graph(:down)) |> Enum.map(&convert_to_vm(socket, &1)) content_items = spot_items ++ project_items diff --git a/core/bundles/next/lib/home/landing_page_html/show.html.eex b/core/bundles/next/lib/home/landing_page_html/show.html.eex index cf9333c86..80cbd7850 100644 --- a/core/bundles/next/lib/home/landing_page_html/show.html.eex +++ b/core/bundles/next/lib/home/landing_page_html/show.html.eex @@ -75,10 +75,10 @@

- Co-creating sustainable research infrastructure + Co-creating Sustainable Software-as-a-Service Solutions

- Co-creating sustainable research infrastructure + Co-creating Sustainable Software-as-a-Service Solutions

@@ -109,11 +109,14 @@

- Eyra Link + Link

- In collaboration with the VU, UvA and SURF, we are developing a sustainable online participant recruitment platform for social sciences and humanities research.

Funded by the PDI-SSH.
+ A participant recruitment software service for Panl, enabling Dutch people to participate in social sciences and humanities research, developed in collaboration with the VU, UvA and SURF.

Funded by the VU and PDI-SSH.

+
+ Read more +
vu uva @@ -135,11 +138,14 @@

- Eyra Port + Port

- In collaboration with the UU and UvA we are working on a tool that enables citizens to easily and safely donate their data from social media platforms like Instagram or Google for research.

Funded by the UU and PDI-SSH. + A data donation software service that enables people to donate their digital trace data (e.g. from Google or Instagram) for research, developed in collaboration with the UU and UvA (D3I Consortium).

Funded by the UU and PDI-SSH.

+
+ Read more +
uu uva @@ -160,11 +166,18 @@

- Eyra Next + Rank

- A sustainable science dissemination platform that empowers transparent scientific practice and enables everyone to monitor the state-of-the-art in science.

Funded by Eyra. -

eyra logo + A software service for benchmarking that supports the organisation of challenges for the direct comparison of various social sciences models, developed in collaboration with the RUG and ODISSEI.

Funded by the RUG and ODISSEI. +

+
+ Read more +
+
+ rug + odissei +
@@ -174,12 +187,49 @@ + + + + +

+ Team

@@ -279,7 +329,7 @@ Open Science

- ”We work towards a world in which the current state of human knowledge is available to all of humankind, so anyone can learn from and build upon previous knowledge.”
+ ”We work towards a world in which anyone can quickly gain insights from the current state of human knowledge, to contribute to the world in their own unique way.”

@@ -341,7 +391,7 @@ We are part of Apollo 14 

- Home for startups that innovate for a better world + Home for entrepreneurs who innovate for a better world

Find out more @@ -398,7 +448,7 @@
logo 200
diff --git a/core/bundles/next/lib/layouts/workspace/menu_builder.ex b/core/bundles/next/lib/layouts/workspace/menu_builder.ex index dd338bff5..def0c7b97 100644 --- a/core/bundles/next/lib/layouts/workspace/menu_builder.ex +++ b/core/bundles/next/lib/layouts/workspace/menu_builder.ex @@ -40,6 +40,6 @@ defmodule Next.Layouts.Workspace.MenuBuilder do def include_map(user), do: %{ console: Authorization.can_access?(user, Next.Console.Page), - projects: Systems.Admin.Public.admin?(user) + projects: Systems.Admin.Public.admin?(user) or user.researcher } end diff --git a/core/bundles/next/lib/menu/items.ex b/core/bundles/next/lib/menu/items.ex index a3a90896d..f8c410eae 100644 --- a/core/bundles/next/lib/menu/items.ex +++ b/core/bundles/next/lib/menu/items.ex @@ -7,7 +7,7 @@ defmodule Next.Menu.Items do @impl true def values() do %{ - next: %{action: %{type: :http_get, to: ~p"/console"}, title: "Next"}, + next: %{action: %{type: :http_get, to: ~p"/next"}, title: "Next"}, admin: %{ action: %{type: :redirect, to: ~p"/admin/config"}, title: dgettext("eyra-ui", "menu.item.admin") diff --git a/core/bundles/port/lib/console/page.ex b/core/bundles/port/lib/console/page.ex index b69191607..b96dc81cc 100644 --- a/core/bundles/port/lib/console/page.ex +++ b/core/bundles/port/lib/console/page.ex @@ -16,7 +16,7 @@ defmodule Port.Console.Page do } def mount(_params, _session, %{assigns: %{current_user: user}} = socket) do - preload = Project.Model.preload_graph(:full) + preload = Project.Model.preload_graph(:down) content_items = user diff --git a/core/config/config.exs b/core/config/config.exs index a9f87447a..da56ee0df 100644 --- a/core/config/config.exs +++ b/core/config/config.exs @@ -116,6 +116,11 @@ config :core, BankingClient, certfile: "../banking_proxy/certs/client_certificate.pem", keyfile: "../banking_proxy/certs/client_key.pem" +config :sentry, + included_environments: ~w(prod test), + environment_name: Mix.env(), + tags: %{app_version: System.get_env("VERSION", "dev")} + module = case Code.ensure_compiled(Bundle) do {:module, module} -> diff --git a/core/config/dev.exs b/core/config/dev.exs index dd7d81853..165355f35 100644 --- a/core/config/dev.exs +++ b/core/config/dev.exs @@ -23,7 +23,7 @@ config :core, Core.Repo, config :core, CoreWeb.Endpoint, reloadable_compilers: [:elixir], force_ssl: false, - debug_errors: false, + debug_errors: true, code_reloader: true, check_origin: false, live_reload: [ @@ -41,7 +41,7 @@ config :core, CoreWeb.Endpoint, "node_modules/webpack/bin/webpack.js", "--mode", "development", - "--watch-stdin", + "--watch", cd: Path.expand("../assets", __DIR__) ] ] @@ -74,7 +74,8 @@ config :core, :apns_backend, Core.APNS.LoggingBackend config :core, :static_path, File.cwd!() - |> Path.join("tmp") + |> Path.join("priv") + |> Path.join("static") |> Path.join("uploads") |> tap(&File.mkdir_p!/1) @@ -96,6 +97,15 @@ config :ex_aws, access_key_id: "my_access_key", secret_access_key: "a_super_secret" +config :core, :feldspar, + backend: Systems.Feldspar.LocalFS, + local_fs_root_path: + File.cwd!() + |> Path.join("priv") + |> Path.join("static") + |> Path.join("feldspar_apps") + |> tap(&File.mkdir_p!/1) + try do import_config "dev.secret.exs" rescue diff --git a/core/config/runtime.exs b/core/config/runtime.exs index 2170e3d79..12aa9a083 100644 --- a/core/config/runtime.exs +++ b/core/config/runtime.exs @@ -83,11 +83,17 @@ if config_env() == :prod do config :core, :azure_storage_backend, sas_token: sas_token end - config :core, Core.Repo, - username: System.get_env("DB_USER"), - password: System.get_env("DB_PASS"), - database: System.get_env("DB_NAME"), - hostname: System.get_env("DB_HOST") + database_url = System.get_env("DB_URL") + + if database_url do + config :core, Core.Repo, url: database_url + else + config :core, Core.Repo, + username: System.get_env("DB_USER"), + password: System.get_env("DB_PASS"), + database: System.get_env("DB_NAME"), + hostname: System.get_env("DB_HOST") + end config :core, GoogleSignIn, redirect_uri: "https://#{host}/google-sign-in/auth", @@ -117,4 +123,16 @@ if config_env() == :prod do private_key: System.get_env("WEB_PUSH_PRIVATE_KEY") config :logger, level: System.get_env("LOG_LEVEL", "info") |> String.to_existing_atom() + + config :sentry, + dsn: System.get_env("SENTRY_DSN"), + environment_name: System.get_env("RELEASE_ENV") || "prod" + + config :core, :feldspar, + backend: Systems.Feldspar.S3, + bucket: System.get_env("FELDSPAR_S3_BUCKET"), + prefix: System.get_env("FELDSPAR_S3_PREFIX", ""), + # The public URL must point to the root's (bucket) publicly accessible URL. + # It should have a policy that allows anonymous users to read all files. + public_url: System.get_env("FELDSPAR_S3_PUBLIC_URL") end diff --git a/core/config/test.exs b/core/config/test.exs index adf1a82d4..0d4123c34 100644 --- a/core/config/test.exs +++ b/core/config/test.exs @@ -1,5 +1,8 @@ import Config +# Print only errors during test +config :logger, level: :warn + # Setup for MinIO config :ex_aws, :s3, scheme: "http://", @@ -27,9 +30,6 @@ config :core, Core.Repo, pool_size: 10, queue_target: 5000 -# Print only warnings and errors during test -config :logger, level: :warn - # Reduce password hashing impact on test duration config :bcrypt_elixir, log_rounds: 4 @@ -54,3 +54,7 @@ config :core, Core.SurfConext, oidc_module: Core.SurfConext.FakeOIDC config :core, :bundle, :next config :core, :banking_backend, Systems.Banking.Dummy + +config :core, :feldspar, + backend: Systems.Feldspar.LocalFS, + local_fs_root_path: "/tmp" diff --git a/core/frameworks/concept/directable.ex b/core/frameworks/concept/directable.ex new file mode 100644 index 000000000..00b1f1a38 --- /dev/null +++ b/core/frameworks/concept/directable.ex @@ -0,0 +1,4 @@ +defprotocol Frameworks.Concept.Directable do + @spec director(t) :: atom() + def director(_t) +end diff --git a/core/systems/pool/_external.ex b/core/frameworks/concept/pool_director.ex similarity index 92% rename from core/systems/pool/_external.ex rename to core/frameworks/concept/pool_director.ex index 50af73205..f93159d76 100644 --- a/core/systems/pool/_external.ex +++ b/core/frameworks/concept/pool_director.ex @@ -1,4 +1,4 @@ -defmodule Systems.Pool.External do +defmodule Frameworks.Concept.PoolDirector do @type user :: map @type budget :: map @type submission :: map diff --git a/core/systems/presenter.ex b/core/frameworks/concept/presenter.ex similarity index 74% rename from core/systems/presenter.ex rename to core/frameworks/concept/presenter.ex index 064e7e7e9..6b48acd50 100644 --- a/core/systems/presenter.ex +++ b/core/frameworks/concept/presenter.ex @@ -1,4 +1,4 @@ -defmodule Systems.Presenter do +defmodule Frameworks.Concept.Presenter do @type model :: pos_integer() | map @type page :: atom() @type assigns :: map @@ -7,7 +7,7 @@ defmodule Systems.Presenter do defmacro __using__(_opts) do quote do - @behaviour Systems.Presenter + @behaviour Frameworks.Concept.Presenter alias Frameworks.Utility.ViewModelBuilder, as: Builder end diff --git a/core/frameworks/concept/promotable_director.ex b/core/frameworks/concept/promotable_director.ex new file mode 100644 index 000000000..129e2c912 --- /dev/null +++ b/core/frameworks/concept/promotable_director.ex @@ -0,0 +1,8 @@ +defmodule Frameworks.Promotable.Director do + @type promotable :: map() + @type error :: atom() + @type user :: map() + + @callback reward_value(promotable) :: integer() + @callback validate_open(promotable, user) :: :ok | {:error, error} +end diff --git a/core/frameworks/concept/system.ex b/core/frameworks/concept/system.ex new file mode 100644 index 000000000..3f6290b82 --- /dev/null +++ b/core/frameworks/concept/system.ex @@ -0,0 +1,15 @@ +defmodule Frameworks.Concept.System do + import Frameworks.Utility.Module + + def director(module) do + module + |> to_system() + |> get("Director") + end + + def presenter(module) do + module + |> to_system() + |> get("Presenter") + end +end diff --git a/core/frameworks/concept/tool_director.ex b/core/frameworks/concept/tool_director.ex new file mode 100644 index 000000000..19c092d2c --- /dev/null +++ b/core/frameworks/concept/tool_director.ex @@ -0,0 +1,15 @@ +defmodule Frameworks.Concept.ToolDirector do + @type user :: map() + @type tool :: map() + @type public_id :: binary() + @type subject :: {member, list(task)} + @type task :: map() + @type member :: map() + @type user_ref :: user | public_id + @type authorization_context :: nil | struct() + + @callback apply_member_and_activate_task(tool, user) :: task | nil + @callback search_subject(tool, user_ref) :: subject | nil + @callback assign_tester_role(tool, user) :: :ok | :error + @callback authorization_context(tool, user) :: authorization_context +end diff --git a/core/frameworks/concept/tool_model.ex b/core/frameworks/concept/tool_model.ex new file mode 100644 index 000000000..e5bdb55cb --- /dev/null +++ b/core/frameworks/concept/tool_model.ex @@ -0,0 +1,45 @@ +defprotocol Frameworks.Concept.ToolModel do + @spec key(t) :: atom() + def key(_t) + + @spec auth_tree(t) :: tuple() | list() | struct() | nil + def auth_tree(_t) + + @spec apply_label(t) :: binary() + def apply_label(_t) + + @spec open_label(t) :: binary() + def open_label(_t) + + @spec ready?(t) :: boolean() + def ready?(_t) + + @spec form(t) :: atom() + def form(_t) + + @spec launcher(t) :: %{url: binary()} | %{function: (map() -> any()), props: map()} + def launcher(_t) + + @spec task_labels(t) :: map() + def task_labels(_t) + + @spec attention_list_enabled?(t) :: boolean() + def attention_list_enabled?(_t) + + @spec group_enabled?(t) :: boolean() + def group_enabled?(_t) +end + +defimpl Frameworks.Concept.ToolModel, for: Ecto.Changeset do + alias Frameworks.Concept.ToolModel + def key(%{data: tool}), do: ToolModel.key(tool) + def auth_tree(%{data: tool}), do: ToolModel.auth_tree(tool) + def apply_label(%{data: tool}), do: ToolModel.apply_label(tool) + def open_label(%{data: tool}), do: ToolModel.open_label(tool) + def ready?(%{data: tool}), do: ToolModel.ready?(tool) + def form(%{data: tool}), do: ToolModel.form(tool) + def launcher(%{data: tool}), do: ToolModel.launcher(tool) + def task_labels(%{data: tool}), do: ToolModel.task_labels(tool) + def attention_list_enabled?(%{data: tool}), do: ToolModel.attention_list_enabled?(tool) + def group_enabled?(%{data: tool}), do: ToolModel.group_enabled?(tool) +end diff --git a/core/frameworks/green_light/live.ex b/core/frameworks/green_light/live.ex index 69b8b2a2d..7417d358b 100644 --- a/core/frameworks/green_light/live.ex +++ b/core/frameworks/green_light/live.ex @@ -6,7 +6,7 @@ defmodule Frameworks.GreenLight.Live do Phoenix.LiveView.unsigned_params() | :not_mounted_at_router, session :: map, socket :: Phoenix.Socket.t() - ) :: integer + ) :: integer | struct @optional_callbacks get_authorization_context: 3 defmacro __using__(auth_module) do diff --git a/core/frameworks/pixel/components/align.ex b/core/frameworks/pixel/components/align.ex index e06855f2d..88664b7b2 100644 --- a/core/frameworks/pixel/components/align.ex +++ b/core/frameworks/pixel/components/align.ex @@ -18,7 +18,7 @@ defmodule Frameworks.Pixel.Align do def vertical_center(assigns) do ~H""" -
+
<%= render_slot(@inner_block) %>
""" diff --git a/core/frameworks/pixel/components/button.ex b/core/frameworks/pixel/components/button.ex index af713a4e5..ea1f4e81d 100644 --- a/core/frameworks/pixel/components/button.ex +++ b/core/frameworks/pixel/components/button.ex @@ -32,6 +32,7 @@ defmodule Frameworks.Pixel.Button do defp action_function(type) do case type do + :fake -> &Action.fake/1 :toggle -> &Action.toggle/1 :click -> &Action.click/1 :redirect -> &Action.redirect/1 diff --git a/core/frameworks/pixel/components/button_action.ex b/core/frameworks/pixel/components/button_action.ex index dda6999fe..2df1e98f3 100644 --- a/core/frameworks/pixel/components/button_action.ex +++ b/core/frameworks/pixel/components/button_action.ex @@ -1,6 +1,16 @@ defmodule Frameworks.Pixel.Button.Action do use CoreWeb, :html + slot(:inner_block, required: true) + + def fake(assigns) do + ~H""" +
+ <%= render_slot(@inner_block) %> +
+ """ + end + attr(:code, :string, required: true) slot(:inner_block, required: true) diff --git a/core/frameworks/pixel/components/line.ex b/core/frameworks/pixel/components/line.ex index 8742b3b10..0e458da9a 100644 --- a/core/frameworks/pixel/components/line.ex +++ b/core/frameworks/pixel/components/line.ex @@ -4,9 +4,12 @@ defmodule Frameworks.Pixel.Line do """ use CoreWeb, :html + attr(:color, :string, default: "bg-grey4") + attr(:height, :string, default: "h-px") + def line(assigns) do ~H""" -
+
""" end end diff --git a/core/frameworks/pixel/components/side_panel.ex b/core/frameworks/pixel/components/side_panel.ex index e1bec621c..6e6020362 100644 --- a/core/frameworks/pixel/components/side_panel.ex +++ b/core/frameworks/pixel/components/side_panel.ex @@ -9,8 +9,8 @@ defmodule Frameworks.Pixel.SidePanel do def side_panel(assigns) do ~H""" -
-
+
+
<%= render_slot(@inner_block) %>
diff --git a/core/frameworks/pixel/components/square_container.ex b/core/frameworks/pixel/components/square_container.ex index 5a3450c67..0e844c3b5 100644 --- a/core/frameworks/pixel/components/square_container.ex +++ b/core/frameworks/pixel/components/square_container.ex @@ -8,7 +8,7 @@ defmodule Frameworks.Pixel.Square do def container(assigns) do ~H"""
-
+
<%= render_slot(@inner_block) %>
diff --git a/core/frameworks/pixel/components/text.ex b/core/frameworks/pixel/components/text.ex index 55292c8e1..e80f696a6 100644 --- a/core/frameworks/pixel/components/text.ex +++ b/core/frameworks/pixel/components/text.ex @@ -185,7 +185,7 @@ defmodule Frameworks.Pixel.Text do def title1(assigns) do ~H""" -
+
<%= render_slot(@inner_block) %>
""" diff --git a/core/frameworks/pixel/flash.ex b/core/frameworks/pixel/flash.ex index b4d63df92..58d733e56 100644 --- a/core/frameworks/pixel/flash.ex +++ b/core/frameworks/pixel/flash.ex @@ -70,7 +70,7 @@ defmodule Frameworks.Pixel.Flash do end def handle_info( - {:show_flash, %{type: type, message: message, auto_hide: auto_hide}}, + {:show_flash, %{type: type, message: message, auto_hide: auto_hide}} = params, socket ) do {:noreply, socket |> Flash.put(type, message, auto_hide)} diff --git a/core/frameworks/signal/_public.ex b/core/frameworks/signal/_public.ex index 21f746fd6..cafd1dcf6 100644 --- a/core/frameworks/signal/_public.ex +++ b/core/frameworks/signal/_public.ex @@ -1,11 +1,17 @@ defmodule Frameworks.Signal.Public do + require Logger + + import Frameworks.Utililty.PrettyPrint + @signal_handlers [ Core.Accounts.SignalHandlers, Core.Mailer.SignalHandlers, Core.WebPush.SignalHandlers, Core.APNS.SignalHandlers, Systems.Observatory.Switch, + Systems.Project.Switch, Systems.Assignment.Switch, + Systems.Workflow.Switch, Systems.Pool.Switch, Systems.Student.Switch, Systems.Campaign.Switch, @@ -13,8 +19,10 @@ defmodule Frameworks.Signal.Public do ] def dispatch(signal, message) do + Logger.warn("SIGNAL: " <> pretty_print(signal) <> " => " <> pretty_print(Map.keys(message))) + for handler <- signal_handlers() do - handler.dispatch(signal, message) + handler.intercept(signal, message) end :ok @@ -30,7 +38,7 @@ defmodule Frameworks.Signal.Public do It automatically merges the message with the multi changes. """ - def multi_dispatch(multi, signal, message) when is_map(message) do + def multi_dispatch(multi, signal, message \\ %{}) when is_map(message) do Ecto.Multi.run(multi, :dispatch_signal, fn _, updates -> :ok = dispatch(signal, Map.merge(updates, message)) {:ok, nil} diff --git a/core/frameworks/signal/handler.ex b/core/frameworks/signal/handler.ex index ea3824ca2..83ea60f8e 100644 --- a/core/frameworks/signal/handler.ex +++ b/core/frameworks/signal/handler.ex @@ -1,18 +1,24 @@ defmodule Frameworks.Signal.Handler do - @type signal :: atom | map + @type signal :: atom | map | {:atom, signal} @type message :: any - @callback dispatch(signal, message) :: any() + @callback intercept(signal, message) :: any() defmacro __using__(_opts) do quote do - @behaviour Frameworks.Signal.Handler - @before_compile Frameworks.Signal.Handler + alias Frameworks.Signal + + @behaviour Signal.Handler + @before_compile Signal.Handler + + defp dispatch!(signal, message) do + Signal.Public.dispatch!(signal, message) + end end end defmacro __before_compile__(_env) do quote do @impl true - def dispatch(_signal, _message), do: :ok + def intercept(_signal, _message), do: :ok end end end diff --git a/core/frameworks/utililty/ecto_helper.ex b/core/frameworks/utililty/ecto_helper.ex index 3089dfb04..96d826e20 100644 --- a/core/frameworks/utililty/ecto_helper.ex +++ b/core/frameworks/utililty/ecto_helper.ex @@ -2,6 +2,7 @@ defmodule Frameworks.Utility.EctoHelper do import Ecto.Query, only: [from: 2] alias Ecto.{Multi, Changeset} alias Core.Repo + alias Frameworks.Signal def upsert(%{data: %{id: id}} = changeset) when not is_nil(id) do Repo.update(changeset) @@ -11,6 +12,13 @@ defmodule Frameworks.Utility.EctoHelper do Repo.insert(changeset) end + def update_and_dispatch(changeset, key) do + Multi.new() + |> Repo.multi_update(key, changeset) + |> Signal.Public.multi_dispatch({key, :update_and_dispatch}, %{changeset: changeset}) + |> Repo.transaction() + end + def delete(multi, name, %table{id: id}) do delete(multi, name, table, id) end @@ -54,4 +62,25 @@ defmodule Frameworks.Utility.EctoHelper do changeset end end + + # Multi + + def run(multi, name, function) do + case :erlang.fun_info(function)[:arity] do + 1 -> + Multi.run(multi, name, fn _, args -> + function.(args) + end) + + 2 -> + Multi.run(multi, name, fn _, args -> + Multi.new() + |> function.(args) + |> Repo.transaction() + end) + + _ -> + multi + end + end end diff --git a/core/frameworks/utililty/legacy_routes_controller.ex b/core/frameworks/utililty/legacy_routes_controller.ex deleted file mode 100644 index 672dd322b..000000000 --- a/core/frameworks/utililty/legacy_routes_controller.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule Frameworks.Utility.LegacyRoutesController do - use CoreWeb, :controller - - alias Core.Accounts - - alias Systems.{ - Campaign, - Assignment - } - - # task/:type/:id/callback -> assignment/:id/callback - def task_callback(%{assigns: %{current_user: user}} = conn, %{"type" => "campaign", "id" => id}) do - %{promotable_assignment: %{crew: crew}} = - Campaign.Public.get!(id, promotable_assignment: [:crew]) - - case crew do - nil -> - redirect_to(conn, Accounts.start_page_path(user)) - - crew -> - # expect one assignment here - assignments = Assignment.Public.get_by_crew!(crew) - redirect_to_live(conn, Systems.Assignment.CallbackPage, assignments) - end - end - - defp redirect_to_live(conn, action, [model | _]), do: redirect_to_live(conn, action, model) - defp redirect_to_live(conn, action, %{id: id}), do: redirect_to_live(conn, action, id) - - defp redirect_to_live(conn, action, id), - do: redirect_to(conn, Routes.live_path(conn, action, id)) - - defp redirect_to(conn, path) do - conn - |> put_status(:moved_permanently) - |> redirect(to: path) - end -end diff --git a/core/frameworks/utililty/module.ex b/core/frameworks/utililty/module.ex new file mode 100644 index 000000000..7cff8a33d --- /dev/null +++ b/core/frameworks/utililty/module.ex @@ -0,0 +1,18 @@ +defmodule Frameworks.Utility.Module do + def get(system, name) when is_atom(system), + do: get(Atom.to_string(system), name) + + def get(system, name) when is_binary(system) do + system = Macro.camelize(system) + + "Elixir.Systems.#{system}.#{name}" + |> String.to_existing_atom() + end + + def to_system(module) when is_atom(module) do + to_system(String.split(Atom.to_string(module), ".")) + end + + def to_system(["Elixir", "Systems", system | _]), do: system + def to_system([system]) when is_binary(system), do: system +end diff --git a/core/frameworks/utililty/pretty_print.ex b/core/frameworks/utililty/pretty_print.ex new file mode 100644 index 000000000..d5051a8ff --- /dev/null +++ b/core/frameworks/utililty/pretty_print.ex @@ -0,0 +1,17 @@ +defmodule Frameworks.Utililty.PrettyPrint do + def pretty_print({left, right}) do + "{#{pretty_print(left)}, #{pretty_print(right)}}" + end + + def pretty_print(list) when is_list(list) do + "[#{Enum.map_join(list, ",", &pretty_print/1)}]" + end + + def pretty_print(value) when is_atom(value) do + ":#{value}" + end + + def pretty_print(value) do + "#{value}" + end +end diff --git a/core/lib/core/accounts.ex b/core/lib/core/accounts.ex index ab6898d05..bc7cc94f5 100644 --- a/core/lib/core/accounts.ex +++ b/core/lib/core/accounts.ex @@ -443,7 +443,7 @@ defmodule Core.Accounts do Multi.new() |> Multi.update(:profile, profile_changeset) |> Multi.update(:user, user_changeset) - |> Signal.Public.multi_dispatch(:user_profile_updated, %{ + |> Signal.Public.multi_dispatch({:user_profile, :updated}, %{ user_changeset: user_changeset, profile_changeset: profile_changeset }) diff --git a/core/lib/core/accounts/signal_handlers.ex b/core/lib/core/accounts/signal_handlers.ex index f27d77db3..04b73aeb4 100644 --- a/core/lib/core/accounts/signal_handlers.ex +++ b/core/lib/core/accounts/signal_handlers.ex @@ -11,7 +11,7 @@ defmodule Core.Accounts.SignalHandlers do alias Core.Accounts.NextActions.{CompleteProfile, PromotePushStudent, SelectStudyStudent} @impl true - def dispatch(:user_profile_updated, %{ + def intercept({:user_profile, :updated}, %{ user: user, user_changeset: user_changeset, profile_changeset: profile_changeset @@ -34,7 +34,7 @@ defmodule Core.Accounts.SignalHandlers do end @impl true - def dispatch(:features_updated, %{features: features, features_changeset: features_changeset}) do + def intercept(:features_updated, %{features: features, features_changeset: features_changeset}) do user = Accounts.get_user!(features.user_id) if user.student do @@ -52,7 +52,7 @@ defmodule Core.Accounts.SignalHandlers do end @impl true - def dispatch(:visited_pages_updated, %{user: user, visited_pages: visited_pages}) do + def intercept(:visited_pages_updated, %{user: user, visited_pages: visited_pages}) do visited_settings? = Enum.member?(visited_pages, "settings") if visited_settings? do @@ -61,7 +61,7 @@ defmodule Core.Accounts.SignalHandlers do end @impl true - def dispatch(:user_created, %{user: user}) do + def intercept({:user, :created}, %{user: user}) do if user.student do NextAction.Public.create_next_action(user, SelectStudyStudent) NextAction.Public.create_next_action(user, PromotePushStudent) diff --git a/core/lib/core/apns/signal_handlers.ex b/core/lib/core/apns/signal_handlers.ex index e6545ea6d..b4e330a3d 100644 --- a/core/lib/core/apns/signal_handlers.ex +++ b/core/lib/core/apns/signal_handlers.ex @@ -4,7 +4,7 @@ defmodule Core.APNS.SignalHandlers do import Core.APNS, only: [send_notification: 2] @impl true - def dispatch(:new_notification, %{box: box, data: %{title: title}}) do + def intercept(:new_notification, %{box: box, data: %{title: title}}) do for user <- Core.Authorization.users_with_role(box, :owner) do send_notification(user, title) end diff --git a/core/lib/core/authorization.ex b/core/lib/core/authorization.ex index 6083e2979..f4e341f57 100644 --- a/core/lib/core/authorization.ex +++ b/core/lib/core/authorization.ex @@ -15,6 +15,7 @@ defmodule Core.Authorization do require Logger import Ecto.Query + alias Ecto.Multi alias Core.Repo alias Frameworks.GreenLight @@ -24,7 +25,7 @@ defmodule Core.Authorization do # Models grant_access(Systems.Campaign.Model, [:visitor, :member]) - grant_access(Systems.Survey.ToolModel, [:owner, :coordinator, :participant]) + grant_access(Systems.Questionnaire.ToolModel, [:owner, :coordinator, :participant]) grant_access(Systems.Lab.ToolModel, [:owner, :coordinator, :participant]) grant_access(Systems.DataDonation.ToolModel, [:owner, :coordinator, :participant]) grant_access(Systems.Benchmark.SpotModel, [:owner]) @@ -42,8 +43,10 @@ defmodule Core.Authorization do grant_access(Systems.NextAction.OverviewPage, [:member]) grant_access(Systems.Campaign.OverviewPage, [:researcher]) grant_access(Systems.Campaign.ContentPage, [:owner]) + grant_access(Systems.Assignment.CrewPage, [:participant, :tester]) + grant_access(Systems.Assignment.ContentPage, [:owner]) grant_access(Systems.Assignment.LandingPage, [:participant]) - grant_access(Systems.Assignment.CallbackPage, [:participant, :tester]) + grant_access(Systems.Alliance.CallbackPage, [:owner]) grant_access(Systems.Lab.PublicPage, [:member]) grant_access(Systems.Promotion.LandingPage, [:visitor, :member, :owner]) grant_access(Systems.Pool.OverviewPage, [:researcher]) @@ -58,9 +61,10 @@ defmodule Core.Authorization do grant_access(Systems.DataDonation.OverviewPage, [:member]) grant_access(Systems.Project.OverviewPage, [:researcher]) grant_access(Systems.Project.NodePage, [:researcher, :owner]) - grant_access(Systems.Project.ItemContentPage, [:researcher, :owner]) + grant_access(Systems.Benchmark.ContentPage, [:researcher, :owner]) grant_access(Systems.Benchmark.ToolPage, [:owner]) grant_access(Systems.Benchmark.LeaderboardPage, [:visitor, :member]) + grant_access(Systems.Feldspar.AppPage, [:visitor, :member]) grant_access(CoreWeb.User.Signin, [:visitor]) grant_access(CoreWeb.User.Signup, [:visitor]) @@ -71,9 +75,9 @@ defmodule Core.Authorization do grant_access(CoreWeb.User.Profile, [:member]) grant_access(CoreWeb.User.Settings, [:member]) grant_access(CoreWeb.User.SecuritySettings, [:member]) - grant_access(CoreWeb.FakeSurvey, [:member]) + grant_access(CoreWeb.FakeQualtrics, [:member]) - grant_actions(CoreWeb.FakeSurveyController, %{ + grant_actions(CoreWeb.FakeAllianceController, %{ index: [:visitor, :member] }) @@ -83,31 +87,49 @@ defmodule Core.Authorization do def get_node!(id), do: Repo.get!(Core.Authorization.Node, id) - def make_node(), do: %Core.Authorization.Node{} + def prepare_node() do + %Core.Authorization.Node{} + end + + def prepare_node(nil) do + %Core.Authorization.Node{} + end - def make_node(%Core.Authorization.Node{} = parent) do - %Core.Authorization.Node{parent_id: parent.id} + def prepare_node(roles) when is_list(roles) do + %Core.Authorization.Node{role_assignments: roles} end - def make_node(nil), do: make_node() + def prepare_node(parent_id) when is_integer(parent_id) do + %Core.Authorization.Node{parent_id: parent_id} + end - def make_node(parent_id) when is_integer(parent_id) do + def prepare_node(%Core.Authorization.Node{id: parent_id}) do %Core.Authorization.Node{parent_id: parent_id} end - def make_node(parent) do - GreenLight.AuthorizationNode.id(parent) |> make_node + def prepare_node(principal, role) do + role = prepare_role(principal, role) + prepare_node([role]) + end + + def prepare_role(principal, role) when is_atom(role) do + principal_id = GreenLight.Principal.id(principal) + + %Core.Authorization.RoleAssignment{ + principal_id: principal_id, + role: :owner + } end def create_node(parent \\ nil) do - case make_node(parent) |> Core.Repo.insert() do + case prepare_node(parent) |> Core.Repo.insert() do {:ok, node} -> {:ok, node.id} error -> error end end def create_node!(parent \\ nil) do - case make_node(parent) |> Core.Repo.insert() do + case prepare_node(parent) |> Core.Repo.insert() do {:ok, node} -> node error -> error end @@ -175,8 +197,8 @@ defmodule Core.Authorization do node_id |> parent_node_query |> Core.Repo.all() end - def roles_intersect?(principal, node_id, roles) do - nodes_query = node_id |> parent_node_query + def roles_intersect?(principal, entity, roles) do + nodes_query = entity |> parent_node_query from(ra in Core.Authorization.RoleAssignment, where: @@ -257,4 +279,47 @@ defmodule Core.Authorization do |> get_parent_nodes() |> List.last() end + + def link(auth_tree) do + Multi.new() + |> link(auth_tree) + |> Repo.transaction() + end + + def link(multi, {parent, [h | t]}) do + multi + |> link({parent, h}) + |> link({parent, t}) + end + + def link( + multi, + {%Core.Authorization.Node{} = parent, {%Core.Authorization.Node{} = child, subtree}} + ) do + multi + |> link({parent, child}) + |> link({child, subtree}) + end + + def link(multi, {%Core.Authorization.Node{} = parent, %Core.Authorization.Node{} = child}) do + link(multi, parent, child) + end + + def link(multi, {_, _}), do: multi + + def link( + multi, + %Core.Authorization.Node{} = parent, + %Core.Authorization.Node{parent: %Ecto.Association.NotLoaded{}} = child + ) do + link(multi, parent, Repo.preload(child, :parent)) + end + + def link(multi, %Core.Authorization.Node{} = parent, %Core.Authorization.Node{} = child) do + changeset = + Core.Authorization.Node.change(child) + |> Ecto.Changeset.put_assoc(:parent, parent) + + Multi.update(multi, Ecto.UUID.generate(), changeset) + end end diff --git a/core/lib/core/authorization/node.ex b/core/lib/core/authorization/node.ex index 72e6bb353..1c2499841 100644 --- a/core/lib/core/authorization/node.ex +++ b/core/lib/core/authorization/node.ex @@ -5,12 +5,21 @@ defmodule Core.Authorization.Node do """ use Ecto.Schema + alias Ecto.Changeset + schema "authorization_nodes" do - belongs_to(:parent, Core.Authorization.Node) + belongs_to(:parent, __MODULE__) + has_many(:children, __MODULE__, foreign_key: :parent_id, references: :id) + has_many(:role_assignments, Core.Authorization.RoleAssignment) timestamps() end + def change(node) do + node + |> Changeset.cast(%{}, []) + end + def create() do %__MODULE__{} end @@ -21,9 +30,14 @@ defmodule Core.Authorization.Node do } end - def create(principal_id, role) do + def create([_ | _] = principal_ids, role) do + role_assignments = + Enum.map(principal_ids, &Core.Authorization.RoleAssignment.create(&1, role)) + %__MODULE__{ - role_assignments: [Core.Authorization.RoleAssignment.create(principal_id, role)] + role_assignments: role_assignments } end + + def create(principal_id, role), do: create([principal_id], role) end diff --git a/core/lib/core/enums/base.ex b/core/lib/core/enums/base.ex index f070d8c93..052b0db88 100644 --- a/core/lib/core/enums/base.ex +++ b/core/lib/core/enums/base.ex @@ -9,8 +9,22 @@ defmodule Core.Enums.Base do unquote(values) end + def contains(atom) when is_atom(atom) do + contains(Atom.to_string(atom)) + end + + def contains(binary) when is_binary(binary) do + values() + |> Enum.map(&Atom.to_string/1) + |> Enum.member?(binary) + end + def translate(value) do - Gettext.dgettext(CoreWeb.Gettext, "eyra-enums", "#{unquote(name)}.#{value}") + if contains(value) do + Gettext.dgettext(CoreWeb.Gettext, "eyra-enums", "#{unquote(name)}.#{value}") + else + value + end end def labels() do diff --git a/core/lib/core/factories.ex b/core/lib/core/factories.ex index 6f417dd9c..6387e396d 100644 --- a/core/lib/core/factories.ex +++ b/core/lib/core/factories.ex @@ -6,7 +6,6 @@ defmodule Core.Factories do alias Core.{ Authorization, - DataDonation, WebPush, Repo } @@ -20,11 +19,13 @@ defmodule Core.Factories do Campaign, Promotion, Assignment, + Workflow, Crew, Support, - Survey, + Alliance, + Feldspar, + Document, Lab, - DataDonation, Benchmark, Pool, Budget, @@ -142,8 +143,16 @@ defmodule Core.Factories do } end - def build(:survey_tool) do - build(:survey_tool, %{}) + def build(:alliance_tool) do + build(:alliance_tool, %{}) + end + + def build(:feldspar_tool) do + build(:feldspar_tool, %{}) + end + + def build(:document_tool) do + build(:document_tool, %{}) end def build(:lab_tool) do @@ -238,20 +247,36 @@ defmodule Core.Factories do build(:benchmark_tool, %{}) end - def build(:experiment) do - build(:experiment, %{}) + def build(:workflow) do + build(:workflow, %{}) + end + + def build(:workflow_item) do + build(:workflow_item, %{}) end def build(:assignment) do build(:assignment, %{}) end + def build(:assignment_info) do + build(:assignment_info, %{}) + end + def build(:project) do build(:project, %{name: Faker.Lorem.word()}) end def build(:project_node) do - build(:project_node, %{name: Faker.Lorem.word(), project_path: [1, 2]}) + build(:project_node, %{name: Faker.Lorem.word(), project_path: []}) + end + + def build(:project_item) do + build(:project_item, %{name: Faker.Lorem.word(), project_path: []}) + end + + def build(:tool_ref) do + build(:tool_ref, %{}) end def build(:auth_node, %{} = attributes) do @@ -345,8 +370,8 @@ defmodule Core.Factories do attributes } - {experiment, attributes} -> - {experiment, attributes} + {assignment, attributes} -> + {assignment, attributes} end %Campaign.Model{ @@ -361,7 +386,7 @@ defmodule Core.Factories do def build(:project, %{} = attributes) do {auth_node, attributes} = Map.pop(attributes, :auth_node, build(:auth_node)) - {node, attributes} = Map.pop(attributes, :budget, build(:project_node)) + {node, attributes} = Map.pop(attributes, :node, build(:project_node)) %Project.Model{ auth_node: auth_node, @@ -372,9 +397,41 @@ defmodule Core.Factories do def build(:project_node, %{} = attributes) do {auth_node, attributes} = Map.pop(attributes, :auth_node, build(:auth_node)) + {items, attributes} = Map.pop(attributes, :items, []) + {children, attributes} = Map.pop(attributes, :children, []) %Project.NodeModel{ - auth_node: auth_node + auth_node: auth_node, + items: items, + children: children + } + |> struct!(attributes) + end + + def build(:project_item, %{} = attributes) do + {tool_ref, attributes} = get_optional(:tool_ref, attributes) + {assignment, attributes} = get_optional(:assignment, attributes) + + %Project.ItemModel{ + tool_ref: tool_ref, + assignment: assignment + } + |> struct!(attributes) + end + + def build(:tool_ref, %{} = attributes) do + {alliance_tool, attributes} = get_optional(:alliance_tool, attributes) + {feldspar_tool, attributes} = get_optional(:feldspar_tool, attributes) + {document_tool, attributes} = get_optional(:document_tool, attributes) + {lab_tool, attributes} = get_optional(:lab_tool, attributes) + {benchmark_tool, attributes} = get_optional(:benchmark_tool, attributes) + + %Project.ToolRefModel{ + alliance_tool: alliance_tool, + document_tool: document_tool, + lab_tool: lab_tool, + feldspar_tool: feldspar_tool, + benchmark_tool: benchmark_tool } |> struct!(attributes) end @@ -385,51 +442,37 @@ defmodule Core.Factories do crew_auth_node = build(:auth_node, %{parent: auth_node}) {crew, attributes} = Map.pop(attributes, :crew, build(:crew, %{auth_node: crew_auth_node})) - - experiment_auth_node = build(:auth_node, %{parent: auth_node}) - - {experiment, attributes} = - case Map.pop(attributes, :experiment, nil) do - {nil, attributes} -> {build(:experiment, %{auth_node: experiment_auth_node}), attributes} - {experiment, attributes} -> {experiment, attributes} - end + {info, attributes} = Map.pop(attributes, :assignment_info, build(:assignment_info)) + {workflow, attributes} = Map.pop(attributes, :workflow, build(:workflow)) %Assignment.Model{ auth_node: auth_node, budget: budget, - assignable_experiment: experiment, + info: info, + workflow: workflow, crew: crew, excluded: [] } |> struct!(attributes) end - def build(:experiment, %{} = attributes) do - {auth_node, attributes} = Map.pop(attributes, :auth_node, build(:auth_node)) - - {lab_tool, attributes} = - if Map.has_key?(attributes, :lab_tool) do - Map.pop(attributes, :lab_tool) - else - {nil, attributes} - end + def build(:assignment_info, %{} = attributes) do + %Assignment.InfoModel{} + |> struct!(attributes) + end - {survey_tool, attributes} = - if lab_tool == nil do - tool_auth_node = build(:auth_node, %{parent: auth_node}) + def build(:workflow, %{} = attributes) do + %Workflow.Model{} + |> struct!(attributes) + end - case Map.pop(attributes, :survey_tool, nil) do - {nil, attributes} -> {build(:survey_tool, %{auth_node: tool_auth_node}), attributes} - {survey_tool, attributes} -> {survey_tool, attributes} - end - else - {nil, attributes} - end + def build(:workflow_item, %{} = attributes) do + {workflow, attributes} = Map.pop(attributes, :workflow, build(:workflow)) + {tool_ref, attributes} = Map.pop(attributes, :tool_ref, build(:tool_ref)) - %Assignment.ExperimentModel{ - auth_node: auth_node, - survey_tool: survey_tool, - lab_tool: lab_tool + %Workflow.ItemModel{ + workflow: workflow, + tool_ref: tool_ref } |> struct!(attributes) end @@ -455,11 +498,9 @@ defmodule Core.Factories do end def build(:crew_task, %{} = attributes) do - {member, attributes} = Map.pop(attributes, :member) {crew, _attributes} = Map.pop(attributes, :crew) %Crew.TaskModel{ - member: member, crew: crew } |> struct!(attributes) @@ -534,19 +575,28 @@ defmodule Core.Factories do |> struct!(attributes) end - def build(:data_donation_tool, %{} = attributes) do + def build(:feldspar_tool, %{} = attributes) do + {auth_node, attributes} = Map.pop(attributes, :auth_node, build(:auth_node)) + + %Feldspar.ToolModel{ + auth_node: auth_node + } + |> struct!(attributes) + end + + def build(:document_tool, %{} = attributes) do {auth_node, attributes} = Map.pop(attributes, :auth_node, build(:auth_node)) - %DataDonation.ToolModel{ + %Document.ToolModel{ auth_node: auth_node } |> struct!(attributes) end - def build(:survey_tool, %{} = attributes) do + def build(:alliance_tool, %{} = attributes) do {auth_node, attributes} = Map.pop(attributes, :auth_node, build(:auth_node)) - %Survey.ToolModel{ + %Alliance.ToolModel{ auth_node: auth_node } |> struct!(attributes) @@ -692,4 +742,12 @@ defmodule Core.Factories do defp random_identifier(type) when is_binary(type) do [type] ++ Faker.Lorem.words(3..5) end + + defp get_optional(key, attributes) do + if Map.has_key?(attributes, key) do + Map.pop(attributes, key) + else + {nil, attributes} + end + end end diff --git a/core/lib/core/release.ex b/core/lib/core/release.ex index 5e3521fe0..8feb9c2f0 100644 --- a/core/lib/core/release.ex +++ b/core/lib/core/release.ex @@ -2,16 +2,24 @@ defmodule Core.Release do @app :core def migrate do + load_app() + for repo <- repos() do {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) end end def rollback(repo, version) do + load_app() {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) end defp repos do Application.fetch_env!(@app, :ecto_repos) end + + defp load_app do + :ssl.start() + Application.load(@app) + end end diff --git a/core/lib/core/surfconext.ex b/core/lib/core/surfconext.ex index c47287d6d..6839d0703 100644 --- a/core/lib/core/surfconext.ex +++ b/core/lib/core/surfconext.ex @@ -68,7 +68,7 @@ defmodule Core.SurfConext do |> Core.SurfConext.User.register_changeset(attrs) |> Ecto.Changeset.put_assoc(:user, user) |> Repo.insert() do - Signal.Public.dispatch!(:user_created, %{user: surf_user.user}) + Signal.Public.dispatch!({:user, :created}, %{user: surf_user.user}) {:ok, surf_user} end end diff --git a/core/lib/core/web_push/signal_handlers.ex b/core/lib/core/web_push/signal_handlers.ex index 7c94d3e8e..38255623f 100644 --- a/core/lib/core/web_push/signal_handlers.ex +++ b/core/lib/core/web_push/signal_handlers.ex @@ -4,7 +4,7 @@ defmodule Core.WebPush.SignalHandlers do alias Core.WebPush @impl true - def dispatch(:new_notification, %{box: box, data: %{title: title}}) do + def intercept(:new_notification, %{box: box, data: %{title: title}}) do for user <- users(box) do :ok = WebPush.send(user, title) end diff --git a/core/lib/core_web.ex b/core/lib/core_web.ex index 2a52038a3..05123093a 100644 --- a/core/lib/core_web.ex +++ b/core/lib/core_web.ex @@ -19,7 +19,7 @@ defmodule CoreWeb do def static_paths, do: - ~w(css assets fonts images js favicon logo icon apple-touch-icon robots manifest sw privacy-statement.pdf landing_page port) + ~w(css assets fonts images js favicon logo icon apple-touch-icon robots manifest sw privacy-statement.pdf landing_page) def controller( opts \\ [formats: [:html, :json], layouts: [html: CoreWeb.Layouts], namespace: CoreWeb] @@ -87,7 +87,6 @@ defmodule CoreWeb do import Core.FeatureFlags use Frameworks.Pixel.Flash - use Systems.Observatory.Public import CoreWeb.UrlResolver, only: [url_resolver: 1] diff --git a/core/lib/core_web/controllers/fake_survey_controller.ex b/core/lib/core_web/controllers/fake_alliance_controller.ex similarity index 81% rename from core/lib/core_web/controllers/fake_survey_controller.ex rename to core/lib/core_web/controllers/fake_alliance_controller.ex index 18648ce8b..2d3b1a820 100644 --- a/core/lib/core_web/controllers/fake_survey_controller.ex +++ b/core/lib/core_web/controllers/fake_alliance_controller.ex @@ -1,4 +1,4 @@ -defmodule CoreWeb.FakeSurveyController do +defmodule CoreWeb.FakeAllianceController do use CoreWeb, :controller def index(conn, _params) do diff --git a/core/lib/core_web/controllers/fake_alliance_html.ex b/core/lib/core_web/controllers/fake_alliance_html.ex new file mode 100644 index 000000000..a8a526c40 --- /dev/null +++ b/core/lib/core_web/controllers/fake_alliance_html.ex @@ -0,0 +1,5 @@ +defmodule CoreWeb.FakeAllianceHTML do + use CoreWeb, :html + + embed_templates("fake_alliance_html/*") +end diff --git a/core/lib/core_web/controllers/fake_survey_html/index.html.eex b/core/lib/core_web/controllers/fake_alliance_html/index.html.eex similarity index 100% rename from core/lib/core_web/controllers/fake_survey_html/index.html.eex rename to core/lib/core_web/controllers/fake_alliance_html/index.html.eex diff --git a/core/lib/core_web/controllers/fake_survey_html.ex b/core/lib/core_web/controllers/fake_survey_html.ex deleted file mode 100644 index a0739651c..000000000 --- a/core/lib/core_web/controllers/fake_survey_html.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule CoreWeb.FakeSurveyHTML do - use CoreWeb, :html - - embed_templates("fake_survey_html/*") -end diff --git a/core/lib/core_web/controllers/layouts/error.html.heex b/core/lib/core_web/controllers/layouts/error.html.heex index b6dc33471..d9ca0ff32 100644 --- a/core/lib/core_web/controllers/layouts/error.html.heex +++ b/core/lib/core_web/controllers/layouts/error.html.heex @@ -15,7 +15,7 @@ /> - +
diff --git a/core/lib/core_web/controllers/layouts/live.html.leex b/core/lib/core_web/controllers/layouts/live.html.leex index 60ee3b91f..9edeac5e2 100644 --- a/core/lib/core_web/controllers/layouts/live.html.leex +++ b/core/lib/core_web/controllers/layouts/live.html.leex @@ -1,5 +1,37 @@
+
+
+
+
+
+ + + +
+
+
+
+
next_wide
+ Menu +

+ content.title +

\ No newline at end of file diff --git a/core/test/systems/crew/_public_test.exs b/core/test/systems/crew/_public_test.exs index 3c7569919..272baf5d3 100644 --- a/core/test/systems/crew/_public_test.exs +++ b/core/test/systems/crew/_public_test.exs @@ -2,13 +2,14 @@ defmodule Systems.Crew.PublicTest do use Core.DataCase alias Core.Authorization alias CoreWeb.UI.Timestamp + alias Frameworks.GreenLight describe "crews" do alias Systems.Crew test "list/0 returns all created crews with preloaded references" do - {:ok, crew1} = Crew.Public.create(Core.Authorization.make_node()) - {:ok, crew2} = Crew.Public.create(Core.Authorization.make_node()) + {:ok, crew1} = Crew.Public.prepare(Core.Authorization.prepare_node()) |> Core.Repo.insert() + {:ok, crew2} = Crew.Public.prepare(Core.Authorization.prepare_node()) |> Core.Repo.insert() list = Crew.Public.list() assert list |> Enum.find(&(&1.id == crew1.id)) assert list |> Enum.find(&(&1.id == crew2.id)) @@ -18,7 +19,7 @@ defmodule Systems.Crew.PublicTest do end test "get/1 returns crew with preloaded references" do - {:ok, crew} = Crew.Public.create(Core.Authorization.make_node()) + {:ok, crew} = Crew.Public.prepare(Core.Authorization.prepare_node()) |> Core.Repo.insert() crew = Crew.Public.get!(crew.id) assert crew.tasks == [] @@ -62,17 +63,25 @@ defmodule Systems.Crew.PublicTest do end test "apply_member/2 creates member + pending task" do - user = Factories.insert!(:member) + %{id: user_id} = user = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: %{id: member_id}, task: task}} = Crew.Public.apply_member(crew, user) + {:ok, %{member: %{user_id: ^user_id}, crew_task: task}} = + Crew.Public.apply_member(crew, user, ["task1"]) assert %{ status: :pending, expired: nil, started_at: nil, completed_at: nil, - member_id: ^member_id + auth_node: %Core.Authorization.Node{ + role_assignments: [ + %Core.Authorization.RoleAssignment{ + principal_id: ^user_id, + role: :owner + } + ] + } } = task end @@ -80,7 +89,7 @@ defmodule Systems.Crew.PublicTest do user = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: member}} = Crew.Public.apply_member(crew, user) + {:ok, %{member: member}} = Crew.Public.apply_member(crew, user, ["task1"]) assert member.crew_id == crew.id assert member.user_id == user.id @@ -95,11 +104,11 @@ defmodule Systems.Crew.PublicTest do user = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: member1}} = Crew.Public.apply_member(crew, user, expire_at(-1)) + {:ok, %{member: member1}} = Crew.Public.apply_member(crew, user, ["task1"], expire_at(-1)) Crew.Public.mark_expired() - {:ok, %{member: member2}} = Crew.Public.apply_member(crew, user, expire_at(1)) + {:ok, %{member: member2}} = Crew.Public.apply_member(crew, user, ["task1"], expire_at(1)) assert member1.id == member2.id assert %{expired: false} = member2 @@ -107,10 +116,12 @@ defmodule Systems.Crew.PublicTest do test "list_members/1 lists only members from that crew" do user = Factories.insert!(:member) + crew1 = Factories.insert!(:crew) crew2 = Factories.insert!(:crew) - {:ok, %{member: member1}} = Crew.Public.apply_member(crew1, user) - {:ok, %{member: member2}} = Crew.Public.apply_member(crew2, user) + + {:ok, %{member: member1}} = Crew.Public.apply_member(crew1, user, ["task1"]) + {:ok, %{member: member2}} = Crew.Public.apply_member(crew2, user, ["task2"]) list1 = Crew.Public.list_members(crew1) list2 = Crew.Public.list_members(crew2) @@ -128,7 +139,8 @@ defmodule Systems.Crew.PublicTest do user = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: member, task: task}} = Crew.Public.apply_member(crew, user, nil) + {:ok, %{member: member, crew_task: task}} = + Crew.Public.apply_member(crew, user, ["task1"], nil) assert Crew.Public.mark_expired() @@ -140,7 +152,8 @@ defmodule Systems.Crew.PublicTest do user = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: member, task: task}} = Crew.Public.apply_member(crew, user, expire_at(1)) + {:ok, %{member: member, crew_task: task}} = + Crew.Public.apply_member(crew, user, ["task1"], expire_at(1)) assert Crew.Public.mark_expired() @@ -152,7 +165,9 @@ defmodule Systems.Crew.PublicTest do user = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: member, task: task}} = Crew.Public.apply_member(crew, user, expire_at(-1)) + {:ok, %{member: member, crew_task: task}} = + Crew.Public.apply_member(crew, user, ["task1"], expire_at(-1)) + Crew.Public.lock_task(task) assert Crew.Public.mark_expired() @@ -167,7 +182,8 @@ defmodule Systems.Crew.PublicTest do user = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: member, task: task}} = Crew.Public.apply_member(crew, user, expire_at(-1)) + {:ok, %{member: member, crew_task: task}} = + Crew.Public.apply_member(crew, user, ["task1"], expire_at(-1)) assert Crew.Public.mark_expired() @@ -182,10 +198,11 @@ defmodule Systems.Crew.PublicTest do user2 = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: member1, task: task1}} = - Crew.Public.apply_member(crew, user1, expire_at(-1)) + {:ok, %{member: member1, crew_task: task1}} = + Crew.Public.apply_member(crew, user1, ["task1"], expire_at(-1)) - {:ok, %{member: member2, task: task2}} = Crew.Public.apply_member(crew, user2, expire_at(1)) + {:ok, %{member: member2, crew_task: task2}} = + Crew.Public.apply_member(crew, user2, ["task2"], expire_at(1)) assert Crew.Public.mark_expired() @@ -202,8 +219,8 @@ defmodule Systems.Crew.PublicTest do user = Factories.insert!(:member) crew = Factories.insert!(:crew) - {:ok, %{member: %{id: member_id} = member, task: task}} = - Crew.Public.apply_member(crew, user, expire_at(-1)) + {:ok, %{member: %{id: member_id} = member, crew_task: task}} = + Crew.Public.apply_member(crew, user, ["task1"], expire_at(-1)) assert Crew.Public.member?(crew, user) assert [%{id: ^member_id}] = Crew.Public.list_members(crew) @@ -223,82 +240,313 @@ defmodule Systems.Crew.PublicTest do alias Core.Factories test "create_task/2 returns valid task" do - user = Factories.insert!(:member) - crew = Factories.insert!(:crew) + %{id: user_id} = user = Factories.insert!(:member) + %{id: crew_id} = crew = Factories.insert!(:crew) member = Factories.insert!(:crew_member, %{crew: crew, user: user}) - {:ok, task} = Crew.Public.create_task(crew, member, nil) + {:ok, task} = Crew.Public.create_task(crew, member, ["task1"], nil) + + assert %{ + crew_id: ^crew_id, + auth_node: %Core.Authorization.Node{ + role_assignments: [ + %Core.Authorization.RoleAssignment{ + principal_id: ^user_id, + role: :owner + } + ] + } + } = task + end + + test "list_tasks/1 returns one task for the crew" do + %{id: crew_id} = crew = Factories.insert!(:crew) + + user1 = Factories.insert!(:member) + user2 = Factories.insert!(:member) - assert task.crew_id == crew.id - assert task.member_id == member.id + member1 = Factories.insert!(:crew_member, %{crew: crew, user: user1}) + member2 = Factories.insert!(:crew_member, %{crew: crew, user: user2}) - list = Crew.Public.list_members_without_task(crew) - assert is_nil(list |> Enum.find(&(&1.id == member.id))) + {:ok, %{auth_node_id: auth_node_id}} = + Crew.Public.create_task(crew, [member1, member2], ["task1"], nil) + + list = Crew.Public.list_tasks(crew) + + assert [ + %Systems.Crew.TaskModel{ + identifier: ["task1"], + status: :pending, + started_at: nil, + completed_at: nil, + accepted_at: nil, + rejected_at: nil, + expire_at: nil, + expired: false, + rejected_category: nil, + rejected_message: nil, + crew_id: ^crew_id, + auth_node_id: ^auth_node_id + } + ] = list end - test "list_tasks/2 returns all available tasks for the crew" do - user = Factories.insert!(:member) + test "list_tasks/1 returns 3 tasks for the crew, latest first" do crew = Factories.insert!(:crew) - member = Factories.insert!(:crew_member, %{crew: crew, user: user}) - {:ok, _task} = Crew.Public.create_task(crew, member, nil) + user1 = Factories.insert!(:member) + user2 = Factories.insert!(:member) + + member1 = Factories.insert!(:crew_member, %{crew: crew, user: user1}) + member2 = Factories.insert!(:crew_member, %{crew: crew, user: user2}) + + {:ok, _} = Crew.Public.create_task(crew, [member1], ["task1"], nil) + {:ok, _} = Crew.Public.create_task(crew, [member1, member2], ["task2"], nil) + {:ok, _} = Crew.Public.create_task(crew, [member2], ["task3"], nil) list = Crew.Public.list_tasks(crew) - assert list |> Enum.find(&(&1.member_id == member.id)) + + assert [ + %Systems.Crew.TaskModel{identifier: ["task3"]}, + %Systems.Crew.TaskModel{identifier: ["task2"]}, + %Systems.Crew.TaskModel{identifier: ["task1"]} + ] = list + end + + test "list_tasks_for_user/2 returns tasks for one member, latest first" do + crew = Factories.insert!(:crew) + + user1 = Factories.insert!(:member) + user2 = Factories.insert!(:member) + + member1 = Factories.insert!(:crew_member, %{crew: crew, user: user1}) + member2 = Factories.insert!(:crew_member, %{crew: crew, user: user2}) + + {:ok, _} = Crew.Public.create_task(crew, [member1], ["task1"], nil) + {:ok, _} = Crew.Public.create_task(crew, [member1, member2], ["task2"], nil) + {:ok, _} = Crew.Public.create_task(crew, [member2], ["task3"], nil) + + assert [ + %Systems.Crew.TaskModel{identifier: ["task2"]}, + %Systems.Crew.TaskModel{identifier: ["task1"]} + ] = Crew.Public.list_tasks_for_user(crew, member1) + + assert [ + %Systems.Crew.TaskModel{identifier: ["task3"]}, + %Systems.Crew.TaskModel{identifier: ["task2"]} + ] = Crew.Public.list_tasks_for_user(crew, member2) end test "count_tasks/2 returns correct nr of tasks in the crew" do user = Factories.insert!(:member) + auth_node = auth_node_with_owner(user) + crew = Factories.insert!(:crew) - member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + _member = Factories.insert!(:crew_member, %{crew: crew, user: user}) assert Crew.Public.count_tasks(crew, [:pending, :completed]) == 0 - _task = Factories.insert!(:crew_task, %{crew: crew, member: member, status: :pending}) + _task = + Factories.insert!(:crew_task, %{ + identifier: ["task1"], + crew: crew, + auth_node: auth_node, + status: :pending + }) assert Crew.Public.count_tasks(crew, [:pending]) == 1 assert Crew.Public.count_tasks(crew, [:completed]) == 0 - _task = Factories.insert!(:crew_task, %{crew: crew, member: member, status: :completed}) + _task = + Factories.insert!(:crew_task, %{ + identifier: ["task2"], + crew: crew, + auth_node: auth_node, + status: :completed + }) + assert Crew.Public.count_tasks(crew, [:pending]) == 1 assert Crew.Public.count_tasks(crew, [:completed]) == 1 end test "create_task/2 succeeds for member" do + %{id: user_id} = user = Factories.insert!(:member) + crew = Factories.insert!(:crew) + member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + + assert %{ + status: :pending, + auth_node: %{ + role_assignments: [%{principal_id: ^user_id}] + } + } = Crew.Public.create_task!(crew, member, ["task1"], nil) + end + + test "create_task/2 multiple tasks succeeds for member" do + %{id: user_id} = user = Factories.insert!(:member) + crew = Factories.insert!(:crew) + member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + + assert %{ + status: :pending, + auth_node: %{ + role_assignments: [%{principal_id: ^user_id}] + } + } = Crew.Public.create_task!(crew, member, ["task1"], nil) + + assert %{ + status: :pending, + auth_node: %{ + role_assignments: [%{principal_id: ^user_id}] + } + } = Crew.Public.create_task!(crew, member, ["task2"], nil) + + assert %{ + status: :pending, + auth_node: %{ + role_assignments: [%{principal_id: ^user_id}] + } + } = Crew.Public.create_task!(crew, member, ["task3"], nil) + end + + test "create_task/2 multiple tasks succeeds for multiple members" do + crew = Factories.insert!(:crew) + + user1 = Factories.insert!(:member) + user2 = Factories.insert!(:member) + + member1 = Factories.insert!(:crew_member, %{crew: crew, user: user1}) + member2 = Factories.insert!(:crew_member, %{crew: crew, user: user2}) + + assert {:ok, _} = Crew.Public.create_task(crew, member1, ["task1", "member1"], nil) + assert {:ok, _} = Crew.Public.create_task(crew, member1, ["task2", "member1"], nil) + assert {:ok, _} = Crew.Public.create_task(crew, member2, ["task1", "member2"], nil) + assert {:ok, _} = Crew.Public.create_task(crew, member2, ["task2", "member2"], nil) + end + + test "create_task/2 single task succeeds for team" do + %{id: crew_id} = crew = Factories.insert!(:crew) + + %{id: user_id1} = user1 = Factories.insert!(:member) + %{id: user_id2} = user2 = Factories.insert!(:member) + + member1 = Factories.insert!(:crew_member, %{crew: crew, user: user1}) + member2 = Factories.insert!(:crew_member, %{crew: crew, user: user2}) + + assert %{ + identifier: ["task1"], + status: :pending, + crew_id: ^crew_id, + auth_node: %Core.Authorization.Node{ + parent_id: nil, + role_assignments: [ + %Core.Authorization.RoleAssignment{ + principal_id: ^user_id1, + role: :owner + }, + %Core.Authorization.RoleAssignment{ + principal_id: ^user_id2, + role: :owner + } + ] + } + } = Crew.Public.create_task!(crew, [member1, member2], ["task1"], nil) + end + + test "create_task/2 multiple tasks succeeds for multiple teams" do + %{id: crew_id} = crew = Factories.insert!(:crew) + + %{id: user_id1} = user1 = Factories.insert!(:member) + %{id: user_id2} = user2 = Factories.insert!(:member) + %{id: user_id3} = user3 = Factories.insert!(:member) + + member1 = Factories.insert!(:crew_member, %{crew: crew, user: user1}) + member2 = Factories.insert!(:crew_member, %{crew: crew, user: user2}) + member3 = Factories.insert!(:crew_member, %{crew: crew, user: user3}) + + assert %{ + identifier: ["task1"], + status: :pending, + crew_id: ^crew_id, + auth_node: %Core.Authorization.Node{ + parent_id: nil, + role_assignments: [ + %Core.Authorization.RoleAssignment{ + principal_id: ^user_id1, + role: :owner + }, + %Core.Authorization.RoleAssignment{ + principal_id: ^user_id2, + role: :owner + } + ] + } + } = Crew.Public.create_task!(crew, [member1, member2], ["task1"], nil) + + assert %{ + identifier: ["task2"], + status: :pending, + crew_id: ^crew_id, + auth_node: %Core.Authorization.Node{ + parent_id: nil, + role_assignments: [ + %Core.Authorization.RoleAssignment{ + principal_id: ^user_id2, + role: :owner + }, + %Core.Authorization.RoleAssignment{ + principal_id: ^user_id3, + role: :owner + } + ] + } + } = Crew.Public.create_task!(crew, [member2, member3], ["task2"], nil) + end + + test "create_task/2 multiple tasks fails for one member: identifier must be unique" do user = Factories.insert!(:member) crew = Factories.insert!(:crew) member = Factories.insert!(:crew_member, %{crew: crew, user: user}) - assert %{status: :pending, member_id: member_id} = - Crew.Public.create_task!(crew, member, nil) + assert {:ok, _} = Crew.Public.create_task(crew, member, ["task1"], nil) - assert member_id == member.id + assert {:error, + %{ + errors: [identifier: {"has already been taken", _}] + }} = Crew.Public.create_task(crew, member, ["task1"], nil) end - test "setup_tasks_for_members/2 " do + test "create_task/2 multiple tasks fails for multiple members: identifier must be unique" do crew = Factories.insert!(:crew) + user1 = Factories.insert!(:member) user2 = Factories.insert!(:member) + member1 = Factories.insert!(:crew_member, %{crew: crew, user: user1}) member2 = Factories.insert!(:crew_member, %{crew: crew, user: user2}) - list = Crew.Public.setup_tasks_for_members!([member1, member2], crew) - assert list |> Enum.find(&(&1.member_id == member1.id)) - assert list |> Enum.find(&(&1.member_id == member2.id)) - assert Crew.Public.count_tasks(crew, [:pending]) == 2 + assert {:ok, _} = Crew.Public.create_task(crew, member1, ["task1"], nil) + + assert {:error, + %{ + errors: [identifier: {"has already been taken", _}] + }} = Crew.Public.create_task(crew, member2, ["task1"], nil) end test "activate_task/1 marks pending task completed" do user = Factories.insert!(:member) + auth_node = auth_node_with_owner(user) + crew = Factories.insert!(:crew) - member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + _member = Factories.insert!(:crew_member, %{crew: crew, user: user}) assert Crew.Public.count_tasks(crew, [:completed]) == 0 task = Factories.insert!(:crew_task, %{ + identifier: ["task1"], crew: crew, - member: member, + auth_node: auth_node, status: :pending }) @@ -309,41 +557,47 @@ defmodule Systems.Crew.PublicTest do test "activate_task/1 does not mark accepted task completed" do user = Factories.insert!(:member) + auth_node = auth_node_with_owner(user) + crew = Factories.insert!(:crew) - member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + _member = Factories.insert!(:crew_member, %{crew: crew, user: user}) assert Crew.Public.count_tasks(crew, [:completed]) == 0 task = Factories.insert!(:crew_task, %{ + identifier: ["task1"], crew: crew, - member: member, + auth_node: auth_node, status: :pending }) assert Crew.Public.count_tasks(crew, [:completed]) == 0 - {:ok, %{task: task}} = Crew.Public.accept_task(task) + {:ok, %{crew_task: task}} = Crew.Public.accept_task(task) assert %{status: :accepted} = Crew.Public.activate_task!(task) assert Crew.Public.count_tasks(crew, [:completed]) == 0 end test "activate_task/1 does not mark rejected task completed" do user = Factories.insert!(:member) + auth_node = auth_node_with_owner(user) + crew = Factories.insert!(:crew) - member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + _member = Factories.insert!(:crew_member, %{crew: crew, user: user}) assert Crew.Public.count_tasks(crew, [:completed]) == 0 task = Factories.insert!(:crew_task, %{ + identifier: ["task1"], crew: crew, - member: member, + auth_node: auth_node, status: :pending }) assert Crew.Public.count_tasks(crew, [:completed]) == 0 - {:ok, %{task: task}} = + {:ok, %{crew_task: task}} = Crew.Public.reject_task(task, %{ category: :attention_checks_failed, message: "rejection message" @@ -355,15 +609,18 @@ defmodule Systems.Crew.PublicTest do test "accept_task/1 marks task accepted" do user = Factories.insert!(:member) + auth_node = auth_node_with_owner(user) + crew = Factories.insert!(:crew) - member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + _member = Factories.insert!(:crew_member, %{crew: crew, user: user}) assert Crew.Public.count_tasks(crew, [:accepted]) == 0 task = Factories.insert!(:crew_task, %{ + identifier: ["task1"], crew: crew, - member: member, + auth_node: auth_node, status: :pending }) @@ -371,7 +628,7 @@ defmodule Systems.Crew.PublicTest do assert {:ok, %{ - task: %{ + crew_task: %{ status: :accepted, accepted_at: accepted_at } @@ -383,15 +640,18 @@ defmodule Systems.Crew.PublicTest do test "reject_task/1 marks task rejected" do user = Factories.insert!(:member) + auth_node = auth_node_with_owner(user) + crew = Factories.insert!(:crew) - member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + _member = Factories.insert!(:crew_member, %{crew: crew, user: user}) assert Crew.Public.count_tasks(crew, [:rejected]) == 0 task = Factories.insert!(:crew_task, %{ + identifier: ["task1"], crew: crew, - member: member, + auth_node: auth_node, status: :pending }) @@ -401,7 +661,7 @@ defmodule Systems.Crew.PublicTest do assert {:ok, %{ - task: %{ + crew_task: %{ status: :rejected, rejected_at: rejected_at, rejected_category: :attention_checks_failed, @@ -415,32 +675,40 @@ defmodule Systems.Crew.PublicTest do test "delete_task/1 " do user = Factories.insert!(:member) + auth_node = auth_node_with_owner(user) + crew = Factories.insert!(:crew) - member = Factories.insert!(:crew_member, %{crew: crew, user: user}) + _member = Factories.insert!(:crew_member, %{crew: crew, user: user}) assert Crew.Public.count_tasks(crew, [:pending]) == 0 task = Factories.insert!(:crew_task, %{ + identifier: ["task1"], crew: crew, - member: member, + auth_node: auth_node, status: :pending }) assert Crew.Public.count_tasks(crew, [:pending]) == 1 - list = Crew.Public.list_members_without_task(crew) - assert is_nil(list |> Enum.find(&(&1.id == member.id))) - Crew.Public.delete_task(task) assert Crew.Public.count_tasks(crew, [:pending]) == 0 - - list = Crew.Public.list_members_without_task(crew) - assert list |> Enum.find(&(&1.id == member.id)) end end defp expire_at(minutes) do Timestamp.naive_from_now(minutes) end + + defp auth_node_with_owner(user) do + Factories.insert!(:auth_node, %{ + role_assignments: [ + %{ + role: :owner, + principal_id: GreenLight.Principal.id(user) + } + ] + }) + end end diff --git a/core/test/systems/crew/factories.ex b/core/test/systems/crew/factories.ex new file mode 100644 index 000000000..017eb99bd --- /dev/null +++ b/core/test/systems/crew/factories.ex @@ -0,0 +1,29 @@ +defmodule Systems.Crew.Factories do + alias CoreWeb.UI.Timestamp + + def create_member(crew, user) do + Core.Factories.insert!(:crew_member, %{crew: crew, user: user}) + end + + def create_task(crew, member, [_ | _] = identifier, opts \\ []) do + {status, opts} = Keyword.pop(opts, :status, :pending) + {expired, opts} = Keyword.pop(opts, :expired, false) + {expire_at, opts} = Keyword.pop(opts, :expire_at, nil) + {minutes_ago, opts} = Keyword.pop(opts, :minutes_ago, 31) + + {updated_at, _opts} = + Keyword.pop(opts, :expired_at, Timestamp.naive_from_now(minutes_ago * -1)) + + auth_node = Core.Authorization.Node.create([member.user.id], :owner) + + Core.Factories.insert!(:crew_task, %{ + identifier: identifier, + crew: crew, + auth_node: auth_node, + status: status, + expired: expired, + expire_at: expire_at, + updated_at: updated_at + }) + end +end diff --git a/core/test/systems/feldspar/app_page_test.exs b/core/test/systems/feldspar/app_page_test.exs new file mode 100644 index 000000000..550e13f4c --- /dev/null +++ b/core/test/systems/feldspar/app_page_test.exs @@ -0,0 +1,21 @@ +defmodule Systems.Feldspar.AppPageTest do + use CoreWeb.ConnCase + import Phoenix.ConnTest + import Phoenix.LiveViewTest + + describe "render an app page" do + test "renders page with iframe", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/feldspar/apps/test") + assert html =~ " + Application.put_env(:core, :feldspar, conf) + end) + + folder_name = "temp_#{:crypto.strong_rand_bytes(16) |> Base.encode16()}" + + tmp_dir = + System.tmp_dir() + |> Path.join(folder_name) + + File.mkdir!(tmp_dir) + + on_exit(fn -> + File.rm_rf!(tmp_dir) + end) + + conf = + conf + |> Keyword.put(:backend, Systems.Feldspar.LocalFS) + |> Keyword.put(:local_fs_root_path, tmp_dir) + + Application.put_env( + :core, + :feldspar, + conf + ) + + {:ok, tmp_dir: tmp_dir, app_conf: conf} + end + + test "call with LocalFS backend serves static content", %{tmp_dir: tmp_dir} do + tmp_dir + |> Path.join("plug_test.txt") + |> File.write("hello world!") + + opts = Plug.init(at: "/web_apps") + conn = Plug.call(conn(:get, "/web_apps/plug_test.txt"), opts) + assert "hello world!" == conn.resp_body + end + + test "call with other backends doesn't serve static content", %{app_conf: conf} do + Application.put_env( + :core, + :feldspar, + Keyword.put(conf, :backend, Systems.Feldspar.FakeBackend) + ) + + opts = Plug.init(at: "/web_apps") + conn = Plug.call(conn(:get, "/web_apps/plug_test.txt"), opts) + assert nil == conn.resp_body + end +end diff --git a/core/test/systems/feldspar/s3_test.exs b/core/test/systems/feldspar/s3_test.exs new file mode 100644 index 000000000..34bcecaa3 --- /dev/null +++ b/core/test/systems/feldspar/s3_test.exs @@ -0,0 +1,77 @@ +defmodule Systems.Feldspar.S3Test do + use ExUnit.Case, async: true + import Mox + alias Systems.Feldspar.S3 + + setup :verify_on_exit! + + setup do + initial_config = Application.get_env(:core, :feldspar) + + Application.put_env(:core, :feldspar, + backend: Systems.Feldspar.S3, + bucket: "test-bucket", + public_url: "http://example.com", + s3_backend: MockAws + ) + + on_exit(fn -> + Application.put_env(:core, :feldspar, initial_config) + end) + + :ok + end + + describe "store/1" do + test "extracts zip and stores files on disk" do + expect(MockAws, :request!, fn args -> + assert %ExAws.Operation.S3{ + bucket: "test-bucket", + http_method: :put, + body: "Hello World!" + } = args + end) + + id = S3.store(Path.join(__DIR__, "hello.zip")) + assert is_binary(id) + refute id == "" + end + end + + describe "get_public_url/1" do + test "returns URL" do + id = Ecto.UUID.generate() + url = S3.get_public_url(id) + assert "http://example.com/#{id}/index.html" == url + end + end + + describe "remove/1" do + test "removes folder" do + id = Ecto.UUID.generate() + + expect(MockAws, :request!, 2, fn args -> + case args do + %{params: %{"list-type" => 2, "prefix" => ^id}} -> + %{ + body: %{ + contents: [ + %{key: "some-thing.html"} + ] + } + } + + %ExAws.Operation.S3DeleteAllObjects{ + bucket: "test-bucket", + objects: objects, + opts: [], + service: :s3 + } -> + assert objects == ["some-thing.html"] + end + end) + + assert :ok == S3.remove(id) + end + end +end diff --git a/core/test/systems/lab/_public_test.exs b/core/test/systems/lab/_public_test.exs index 5bb7f45cd..ef3f7af78 100644 --- a/core/test/systems/lab/_public_test.exs +++ b/core/test/systems/lab/_public_test.exs @@ -238,7 +238,7 @@ defmodule Systems.Lab.PublicTest do first_member = List.first(members) Lab.Public.cancel_reservation(lab_tool, first_member) - Lab.Public.get(id, time_slots: [:reservations]) + Lab.Public.get_tool!(id, time_slots: [:reservations]) assert [%{id: ^slot1_id}, %{id: ^slot2_id}] = Lab.Public.get_available_time_slots(id) end end diff --git a/core/test/systems/project/_assembly_test.exs b/core/test/systems/project/_assembly_test.exs new file mode 100644 index 000000000..38da768f3 --- /dev/null +++ b/core/test/systems/project/_assembly_test.exs @@ -0,0 +1,241 @@ +defmodule Systems.Project.AssemblyTest do + use Core.DataCase + + alias Systems.{ + Project + } + + test "create_item/3 create benchmark item" do + %{root: %{id: root_id} = root} = Factories.insert!(:project, %{name: "Project"}) + + item_name = "Item" + + {:ok, %{item: %{id: id}}} = Project.Assembly.create_item(:benchmark, item_name, root) + item = Project.Public.get_item!(id, Project.ItemModel.preload_graph(:down)) + + assert %Systems.Project.ItemModel{ + name: ^item_name, + project_path: [^root_id], + tool_ref: %Systems.Project.ToolRefModel{ + special: :benchmark, + alliance_tool: nil, + document_tool: nil, + lab_tool: nil, + feldspar_tool: nil, + benchmark_tool: %Systems.Benchmark.ToolModel{ + status: :concept, + title: nil, + expectations: nil, + data_set: nil, + template_repo: nil, + deadline: nil, + director: :project, + auth_node: %Core.Authorization.Node{}, + spots: [], + leaderboards: [] + } + }, + assignment: nil + } = item + end + + test "create_item/3 create data donation item" do + %{root: %{id: root_id} = root} = Factories.insert!(:project, %{name: "Project"}) + + item_name = "Item" + + {:ok, %{item: %{id: id}}} = Project.Assembly.create_item(:data_donation, item_name, root) + item = Project.Public.get_item!(id, Project.ItemModel.preload_graph(:down)) + + assert %{ + name: ^item_name, + project_path: [^root_id], + tool_ref: nil, + assignment: %Systems.Assignment.Model{ + info: %Systems.Assignment.InfoModel{}, + workflow: %Systems.Workflow.Model{ + type: :single_task, + items: [] + }, + crew: %Systems.Crew.Model{ + tasks: [], + members: [], + auth_node: %Core.Authorization.Node{} + }, + budget: nil, + auth_node: %Core.Authorization.Node{ + role_assignments: [] + }, + excluded: [], + director: nil + } + } = item + end + + test "update_path/1 succeeds with project and node depth of 1" do + item1 = + Project.Factories.build_tool() + |> Project.Factories.build_tool_ref() + |> Project.Factories.build_item() + + item2 = + Project.Factories.build_tool() + |> Project.Factories.build_tool_ref() + |> Project.Factories.build_item() + + %{id: project_id} = + project = + Project.Factories.build_node(items: [item1, item2]) + |> Project.Factories.build_project() + |> Repo.insert!() + + assert %Systems.Project.Model{ + root: %Systems.Project.NodeModel{ + id: root_id, + project_path: [], + items: [ + %Systems.Project.ItemModel{ + project_path: [] + }, + %Systems.Project.ItemModel{ + project_path: [] + } + ] + } + } = Repo.get!(Project.Model, project_id) |> Repo.preload(root: [:items]) + + {:ok, _} = + Ecto.Multi.new() + |> Project.Assembly.update_path(project) + |> Repo.transaction() + + assert %Systems.Project.Model{ + root: %Systems.Project.NodeModel{ + project_path: [^project_id], + items: [ + %Systems.Project.ItemModel{ + project_path: [^project_id, ^root_id] + }, + %Systems.Project.ItemModel{ + project_path: [^project_id, ^root_id] + } + ] + } + } = Repo.get!(Project.Model, project_id) |> Repo.preload(root: [:items]) + end + + test "update_path/1 succeeds with project and node depth of 2" do + item_a_1 = + Project.Factories.build_tool() + |> Project.Factories.build_tool_ref() + |> Project.Factories.build_item() + + item_a_2 = + Project.Factories.build_tool() + |> Project.Factories.build_tool_ref() + |> Project.Factories.build_item() + + item_b_1 = + Project.Factories.build_tool() + |> Project.Factories.build_tool_ref() + |> Project.Factories.build_item() + + item_b_2 = + Project.Factories.build_tool() + |> Project.Factories.build_tool_ref() + |> Project.Factories.build_item() + + node_a = Project.Factories.build_node(items: [item_a_1, item_a_2]) + node_b = Project.Factories.build_node(items: [item_b_1, item_b_2]) + + %{id: project_id} = + project = + Project.Factories.build_node(children: [node_a, node_b]) + |> Project.Factories.build_node() + |> Project.Factories.build_project() + |> Repo.insert!() + + assert %Systems.Project.Model{ + root: %Systems.Project.NodeModel{ + id: level1, + project_path: [], + children: [ + %Systems.Project.NodeModel{ + id: level2, + project_path: [], + children: [ + %Systems.Project.NodeModel{ + id: level_3_a, + project_path: [], + items: [ + %Systems.Project.ItemModel{ + project_path: [] + }, + %Systems.Project.ItemModel{ + project_path: [] + } + ] + }, + %Systems.Project.NodeModel{ + id: level_3_b, + project_path: [], + items: [ + %Systems.Project.ItemModel{ + project_path: [] + }, + %Systems.Project.ItemModel{ + project_path: [] + } + ] + } + ] + } + ] + } + } = + Repo.get!(Project.Model, project_id) + |> Repo.preload(root: [children: [children: [:items]]]) + + {:ok, _} = + Ecto.Multi.new() + |> Project.Assembly.update_path(project) + |> Repo.transaction() + + assert %Systems.Project.Model{ + root: %Systems.Project.NodeModel{ + project_path: [^project_id], + children: [ + %Systems.Project.NodeModel{ + project_path: [^project_id, ^level1], + children: [ + %Systems.Project.NodeModel{ + project_path: [^project_id, ^level1, ^level2], + items: [ + %Systems.Project.ItemModel{ + project_path: [^project_id, ^level1, ^level2, ^level_3_a] + }, + %Systems.Project.ItemModel{ + project_path: [^project_id, ^level1, ^level2, ^level_3_a] + } + ] + }, + %Systems.Project.NodeModel{ + project_path: [^project_id, ^level1, ^level2], + items: [ + %Systems.Project.ItemModel{ + project_path: [^project_id, ^level1, ^level2, ^level_3_b] + }, + %Systems.Project.ItemModel{ + project_path: [^project_id, ^level1, ^level2, ^level_3_b] + } + ] + } + ] + } + ] + } + } = + Repo.get!(Project.Model, project_id) + |> Repo.preload(root: [children: [children: [:items]]]) + end +end diff --git a/core/test/systems/project/assembly_test.exs b/core/test/systems/project/assembly_test.exs deleted file mode 100644 index b73a8a98d..000000000 --- a/core/test/systems/project/assembly_test.exs +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Systems.Project.AssemblyTest do - use Core.DataCase - - # alias Systems.{ - # Project - # } - - test "create_item/2" do - %{root: _root} = Factories.insert!(:project, %{name: "AAP"}) - end -end diff --git a/core/test/systems/project/factories.ex b/core/test/systems/project/factories.ex new file mode 100644 index 000000000..43702ea31 --- /dev/null +++ b/core/test/systems/project/factories.ex @@ -0,0 +1,69 @@ +defmodule Systems.Project.Factories do + alias Core.Factories + alias Systems.Project + alias Systems.Alliance + alias Systems.Assignment + + def build_project() do + Factories.build(:project, %{name: "project"}) + end + + def build_project(node) do + Factories.build(:project, %{name: "project", node: node}) + end + + def build_node(), do: build_node([]) + + def build_node(arguments) when is_list(arguments) do + {items, arguments} = Keyword.pop(arguments, :items, []) + {children, _arguments} = Keyword.pop(arguments, :children, []) + + Factories.build(:project_node, %{ + name: "project-node", + project_path: [], + items: items, + children: children + }) + end + + def build_node(_, arguments \\ []) + + def build_node(%Project.ItemModel{} = item, arguments) do + arguments = Keyword.update(arguments, :items, [item], &(&1 ++ [item])) + build_node(arguments) + end + + def build_node(%Project.NodeModel{} = child, arguments) do + arguments = Keyword.update(arguments, :children, [child], &(&1 ++ [child])) + build_node(arguments) + end + + def build_item(%Project.ToolRefModel{} = tool_ref) do + Factories.build(:project_item, %{name: "project-item", project_path: [], tool_ref: tool_ref}) + end + + def build_item(%Assignment.Model{} = assignment) do + Factories.build(:project_item, %{ + name: "project-item", + project_path: [], + assignment: assignment + }) + end + + def build_tool_ref(%Alliance.ToolModel{} = tool) do + Factories.build(:tool_ref, %{ + alliance_tool: tool + }) + end + + def build_tool() do + Factories.build(:alliance_tool, %{ + url: "https://eyra.co/alliance/123", + director: :assignment + }) + end + + def build_assignment() do + Factories.build(:assignment) + end +end diff --git a/core/test/systems/project/model_test.exs b/core/test/systems/project/model_test.exs new file mode 100644 index 000000000..31911214e --- /dev/null +++ b/core/test/systems/project/model_test.exs @@ -0,0 +1,91 @@ +defmodule Systems.Project.ModelTest do + use Core.DataCase + + alias Systems.{ + Project + } + + test "auth_tree/1 succeeds with tool_ref item" do + project = + Project.Factories.build_tool() + |> Project.Factories.build_tool_ref() + |> Project.Factories.build_item() + |> Project.Factories.build_node() + |> Project.Factories.build_project() + |> Repo.insert!() + + auth_tree = Project.Model.auth_tree(project) + + assert { + %Core.Authorization.Node{id: project_id}, + { + %Core.Authorization.Node{id: node_id}, + [ + %Core.Authorization.Node{id: tool_id} + ] + } + } = auth_tree + + assert %Core.Authorization.Node{ + id: ^tool_id, + parent_id: nil + } = Repo.get!(Core.Authorization.Node, tool_id) |> Repo.preload(parent: [:parent]) + + Core.Authorization.link(auth_tree) + + assert %Core.Authorization.Node{ + id: ^tool_id, + parent: %Core.Authorization.Node{ + id: ^node_id, + parent: %Core.Authorization.Node{ + id: ^project_id, + parent_id: nil + } + } + } = Repo.get!(Core.Authorization.Node, tool_id) |> Repo.preload(parent: [:parent]) + end + + test "auth_tree/1 succeeds with assignment item" do + project = + Project.Factories.build_assignment() + |> Project.Factories.build_item() + |> Project.Factories.build_node() + |> Project.Factories.build_project() + |> Repo.insert!() + + auth_tree = Project.Model.auth_tree(project) + + assert { + %Core.Authorization.Node{id: project_id}, + { + %Core.Authorization.Node{id: node_id}, + [ + %Core.Authorization.Node{id: assignment_id} + ] + } + } = auth_tree + + assert %Core.Authorization.Node{ + id: ^project_id, + children: [] + } = + Repo.get!(Core.Authorization.Node, project_id) |> Repo.preload(children: [:children]) + + Core.Authorization.link(auth_tree) + + assert %Core.Authorization.Node{ + id: ^project_id, + children: [ + %Core.Authorization.Node{ + id: ^node_id, + children: [ + %Core.Authorization.Node{ + id: ^assignment_id + } + ] + } + ] + } = + Repo.get!(Core.Authorization.Node, project_id) |> Repo.preload(children: [:children]) + end +end diff --git a/core/test/systems/promotion/landing_page_test.exs b/core/test/systems/promotion/landing_page_test.exs index 9390d5052..8f337ba1e 100644 --- a/core/test/systems/promotion/landing_page_test.exs +++ b/core/test/systems/promotion/landing_page_test.exs @@ -4,49 +4,40 @@ defmodule Systems.Promotion.LandingPageTest do import Phoenix.LiveViewTest alias Systems.{ + Assignment, Promotion, Crew, Budget } - describe "show landing page for: campaign -> assignment -> survey_tool" do + describe "show landing page for: campaign -> assignment -> alliance_tool" do setup [:login_as_member] setup do + campaign_auth_node = Factories.insert!(:auth_node) + promotion_auth_node = Factories.insert!(:auth_node, %{parent: campaign_auth_node}) + assignment_auth_node = Factories.insert!(:auth_node, %{parent: campaign_auth_node}) + tool_auth_node = Factories.insert!(:auth_node, %{parent: assignment_auth_node}) + currency = Budget.Factories.create_currency("test_1234", :legal, "ƒ", 2) budget = Budget.Factories.create_budget("test_1234", currency) pool = Factories.insert!(:pool, %{name: "test_1234", director: :citizen, currency: currency}) - survey_tool = - Factories.insert!( - :survey_tool, - %{ - survey_url: "https://eyra.co/fake_survey" - } - ) - - experiment = - Factories.insert!( - :experiment, - %{ - survey_tool: survey_tool, - subject_count: 10, - duration: "10", - language: "en", - devices: [:desktop] - } - ) + tool = Assignment.Factories.create_tool(tool_auth_node) + tool_ref = Assignment.Factories.create_tool_ref(tool) + workflow = Assignment.Factories.create_workflow() + _workflow_item = Assignment.Factories.create_workflow_item(workflow, tool_ref) + info = Assignment.Factories.create_info("10", 10) assignment = - Factories.insert!( - :assignment, - %{ - budget: budget, - experiment: experiment, - director: :campaign - } + Assignment.Factories.create_assignment( + info, + workflow, + assignment_auth_node, + budget, + :campaign ) promotion = @@ -62,7 +53,8 @@ defmodule Systems.Promotion.LandingPageTest do banner_subtitle: "Banner Subtitle", banner_photo_url: "https://eyra.co/image/1", banner_url: "https://eyra.co/member/1", - marks: ["vu"] + marks: ["vu"], + auth_node: promotion_auth_node } ) @@ -74,7 +66,8 @@ defmodule Systems.Promotion.LandingPageTest do assignment: assignment, promotion: promotion, authors: [author], - submissions: [submission] + submissions: [submission], + auth_node: campaign_auth_node }) %{promotion: promotion, assignment: assignment, submissions: [submission]} @@ -103,7 +96,7 @@ defmodule Systems.Promotion.LandingPageTest do test "One member applied", %{conn: conn, promotion: promotion, assignment: assignment} do user = Factories.insert!(:member) - {:ok, %{member: _member}} = Crew.Public.apply_member(assignment.crew, user) + {:ok, %{member: _member}} = Crew.Public.apply_member(assignment.crew, user, ["task1"]) {:ok, _view, html} = live(conn, Routes.live_path(conn, Promotion.LandingPage, promotion.id)) assert html =~ "Open voor deelname" diff --git a/core/test/systems/survey/_public_test.exs b/core/test/systems/survey/_public_test.exs deleted file mode 100644 index cac1e5b7f..000000000 --- a/core/test/systems/survey/_public_test.exs +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Systems.Survey.PublicTest do - use Core.DataCase - - alias Systems.{ - Survey - } - - describe "survey_tools" do - alias Core.Factories - - @update_attrs %{survey_url: "http://eyra.co/fake_survey"} - - test "list_survey_tools/0 returns all survey_tools" do - survey_tool = Factories.insert!(:survey_tool) - - assert Survey.Public.list_survey_tools() - |> Enum.map(& &1.id) - |> MapSet.new() - |> MapSet.member?(survey_tool.id) - end - - test "get_survey_tool!/1 returns the survey_tool with given id" do - survey_tool = Factories.insert!(:survey_tool) - assert Survey.Public.get_survey_tool!(survey_tool.id).id == survey_tool.id - end - - test "create_tool/1 with valid data creates a survey_tool" do - auth_node = Factories.insert!(:auth_node) - - assert {:ok, %Survey.ToolModel{} = _survey_tool} = Survey.Public.create_tool(%{}, auth_node) - end - - test "update_survey_tool/2 with valid data updates the survey_tool" do - survey_tool = Factories.insert!(:survey_tool) - - assert {:ok, %{tool: survey_tool}} = - Survey.Public.update_survey_tool(survey_tool, :auto_save, @update_attrs) - - assert survey_tool.survey_url == "http://eyra.co/fake_survey" - end - - test "delete_survey_tool/1 deletes the survey_tool" do - survey_tool = Factories.insert!(:survey_tool) - assert {:ok, %{}} = Survey.Public.delete_survey_tool(survey_tool) - assert_raise Ecto.NoResultsError, fn -> Survey.Public.get_survey_tool!(survey_tool.id) end - end - - test "change_survey_tool/1 returns a survey_tool changeset" do - survey_tool = Factories.insert!(:survey_tool) - assert %Ecto.Changeset{} = Survey.Public.change_survey_tool(survey_tool, :mount) - end - end -end diff --git a/core/test/systems/workflow/_public_test.exs b/core/test/systems/workflow/_public_test.exs new file mode 100644 index 000000000..32134034b --- /dev/null +++ b/core/test/systems/workflow/_public_test.exs @@ -0,0 +1,326 @@ +defmodule Systems.Workflow.PublicTest do + use Core.DataCase + + alias Ecto.Multi + + alias Systems.Workflow + alias Systems.Workflow.Factories + + test "rearrange/2 move last to first succeed" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + %{id: id_a} = item_a = Factories.create_item(workflow, tool_ref, 0) + %{id: id_b} = item_b = Factories.create_item(workflow, tool_ref, 1) + %{id: id_c} = item_c = Factories.create_item(workflow, tool_ref, 2) + %{id: id_d} = item_d = Factories.create_item(workflow, tool_ref, 3) + + result = + [item_a, item_b, item_c, item_d] + |> Workflow.Public.rearrange(3, 0) + + assert [ + ok: %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 0 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 1 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 2 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_c, + position: 3 + } + ] = result + end + + test "rearrange/2 move first to last succeed" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + %{id: id_a} = item_a = Factories.create_item(workflow, tool_ref, 0) + %{id: id_b} = item_b = Factories.create_item(workflow, tool_ref, 1) + %{id: id_c} = item_c = Factories.create_item(workflow, tool_ref, 2) + %{id: id_d} = item_d = Factories.create_item(workflow, tool_ref, 3) + + result = + [item_a, item_b, item_c, item_d] + |> Workflow.Public.rearrange(0, 3) + + assert [ + ok: %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 0 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_c, + position: 1 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 2 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 3 + } + ] = result + end + + test "update_position/2 move first to last succeed" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + %{id: id_a} = item_a = Factories.create_item(workflow, tool_ref, 0) + %{id: id_b} = Factories.create_item(workflow, tool_ref, 1) + %{id: id_c} = Factories.create_item(workflow, tool_ref, 2) + %{id: id_d} = Factories.create_item(workflow, tool_ref, 3) + + {:ok, result} = Workflow.Public.update_position(item_a, 3) + + assert %{ + validate_new_position: true, + validate_old_position: true, + items: [ + %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 0 + }, + %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 1 + }, + %Systems.Workflow.ItemModel{ + id: ^id_c, + position: 2 + }, + %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 3 + } + ], + order_and_update: [ + ok: %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 0 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_c, + position: 1 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 2 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 3 + } + ] + } = result + end + + test "update_position/2 move last to second succeed" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + %{id: id_a} = Factories.create_item(workflow, tool_ref, 0) + %{id: id_b} = Factories.create_item(workflow, tool_ref, 1) + %{id: id_c} = Factories.create_item(workflow, tool_ref, 2) + %{id: id_d} = item_d = Factories.create_item(workflow, tool_ref, 3) + + {:ok, result} = Workflow.Public.update_position(item_d, 1) + + assert %{ + validate_new_position: true, + validate_old_position: true, + items: [ + %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 0 + }, + %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 1 + }, + %Systems.Workflow.ItemModel{ + id: ^id_c, + position: 2 + }, + %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 3 + } + ], + order_and_update: [ + ok: %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 0 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 1 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 2 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_c, + position: 3 + } + ] + } = result + end + + test "update_position/2 move to same position succeeded" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + %{id: id_a} = Factories.create_item(workflow, tool_ref, 0) + %{id: id_b} = Factories.create_item(workflow, tool_ref, 1) + %{id: id_c} = Factories.create_item(workflow, tool_ref, 2) + %{id: id_d} = item_d = Factories.create_item(workflow, tool_ref, 3) + + {:ok, result} = Workflow.Public.update_position(item_d, 3) + + assert %{ + items: [ + %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 0 + }, + %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 1 + }, + %Systems.Workflow.ItemModel{ + id: ^id_c, + position: 2 + }, + %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 3 + } + ] + } = result + end + + test "update_position/2 out of upper bounds failure" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + Factories.create_item(workflow, tool_ref, 0) + Factories.create_item(workflow, tool_ref, 1) + Factories.create_item(workflow, tool_ref, 2) + item_d = Factories.create_item(workflow, tool_ref, 3) + + {:error, :validate_new_position, :out_of_bounds, _} = + Workflow.Public.update_position(item_d, 4) + end + + test "update_position/2 out of lower bounds failure" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + Factories.create_item(workflow, tool_ref, 0) + Factories.create_item(workflow, tool_ref, 1) + Factories.create_item(workflow, tool_ref, 2) + item_d = Factories.create_item(workflow, tool_ref, 3) + + {:error, :validate_new_position, :out_of_bounds, _} = + Workflow.Public.update_position(item_d, -1) + end + + test "update_position/2 position out of sync failure" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + Factories.create_item(workflow, tool_ref, 0) + Factories.create_item(workflow, tool_ref, 1) + item_c = Factories.create_item(workflow, tool_ref, 2) + item_d = Factories.create_item(workflow, tool_ref, 3) + + Multi.new() + |> Multi.update(:item_c, Workflow.ItemModel.changeset(item_c, %{position: 3})) + |> Multi.update(:item_d, Workflow.ItemModel.changeset(item_d, %{position: 2})) + |> Repo.transaction() + + {:error, :validate_old_position, :out_of_sync, _} = Workflow.Public.update_position(item_d, 1) + end + + test "update_position/2 item deleted underwater success" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + Factories.create_item(workflow, tool_ref, 0) + Factories.create_item(workflow, tool_ref, 1) + item_c = Factories.create_item(workflow, tool_ref, 2) + item_d = Factories.create_item(workflow, tool_ref, 3) + + Multi.new() + |> Multi.delete(:item_c, item_c) + |> Repo.transaction() + + {:error, :validate_old_position, :out_of_bounds, _} = + Workflow.Public.update_position(item_d, 0) + end + + test "delete/1 succeed" do + tool = Factories.create_tool() + tool_ref = Factories.create_tool_ref(tool) + workflow = Factories.create_workflow() + + %{id: id_a} = Factories.create_item(workflow, tool_ref, 0) + %{id: id_b} = Factories.create_item(workflow, tool_ref, 1) + item_c = Factories.create_item(workflow, tool_ref, 2) + %{id: id_d} = Factories.create_item(workflow, tool_ref, 3) + + {:ok, result} = Workflow.Public.delete(item_c) + + assert %{ + items: [ + %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 0 + }, + %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 1 + }, + %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 3 + } + ], + order_and_update: [ + ok: %Systems.Workflow.ItemModel{ + id: ^id_a, + position: 0 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_b, + position: 1 + }, + ok: %Systems.Workflow.ItemModel{ + id: ^id_d, + position: 2 + } + ] + } = result + end +end diff --git a/core/test/systems/workflow/factories.ex b/core/test/systems/workflow/factories.ex new file mode 100644 index 000000000..9932f58b6 --- /dev/null +++ b/core/test/systems/workflow/factories.ex @@ -0,0 +1,28 @@ +defmodule Systems.Workflow.Factories do + alias Core.Factories + alias Systems.Document + + def create_tool() do + Factories.insert!(:document_tool, %{ + director: :assignment + }) + end + + def create_tool_ref(%Document.ToolModel{} = tool) do + Factories.insert!(:tool_ref, %{ + document_tool: tool + }) + end + + def create_workflow(type \\ :single_task) do + Factories.insert!(:workflow, %{type: type}) + end + + def create_item(workflow, tool_ref, index) do + Factories.insert!(:workflow_item, %{ + workflow: workflow, + tool_ref: tool_ref, + position: index + }) + end +end diff --git a/core/test/test_helper.exs b/core/test/test_helper.exs index 3f9832da4..2b2934be9 100644 --- a/core/test/test_helper.exs +++ b/core/test/test_helper.exs @@ -1,6 +1,8 @@ ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(Core.Repo, :manual) +Mox.defmock(MockAws, for: ExAws.Behaviour) + Mox.defmock(Core.WebPush.MockBackend, for: Core.WebPush.Backend) Application.put_env(:core, :web_push_backend, Core.WebPush.MockBackend)