From e070c5f8980ea248dc268b8359d3ce830d571632 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Sat, 7 Aug 2021 16:58:17 +1000 Subject: [PATCH 01/17] New filter selector --- ui/src/package.json | 3 + ui/src/src/common/Utils.js | 2 +- ui/src/src/components/ui/Filter.js | 137 ++++++++++++++++++ ui/src/src/index.css | 6 + .../stories/components/ui/Filter.stories.js | 44 ++++++ ui/src/tailwind.config.js | 39 ++++- ui/src/yarn.lock | 15 ++ 7 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 ui/src/src/components/ui/Filter.js create mode 100644 ui/src/src/stories/components/ui/Filter.stories.js diff --git a/ui/src/package.json b/ui/src/package.json index 9a4d1447..7bb0bc93 100644 --- a/ui/src/package.json +++ b/ui/src/package.json @@ -6,10 +6,13 @@ "@fortawesome/fontawesome-svg-core": "^1.2.2", "@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/react-fontawesome": "^0.1.2", + "@headlessui/react": "^1.4.0", + "@heroicons/react": "^1.0.3", "@tinymce/tinymce-react": "^3.12.6", "bootstrap": "^4.1.2", "bootstrap-fileinput": "^5.2.2", "bootstrap-icons": "^1.5.0", + "clsx": "^1.1.1", "d3": "^5.9.2", "gijgo": "^1.9.13", "highcharts-react-official": "^2.1.3", diff --git a/ui/src/src/common/Utils.js b/ui/src/src/common/Utils.js index 97caab16..f8ac3201 100644 --- a/ui/src/src/common/Utils.js +++ b/ui/src/src/common/Utils.js @@ -1,5 +1,5 @@ /* eslint-disable eqeqeq */ -import Moment from "moment/min/moment.min.js"; +import Moment from "moment"; import * as UserSession from "../user/UserSession"; import $ from "jquery"; import qs from "qs"; diff --git a/ui/src/src/components/ui/Filter.js b/ui/src/src/components/ui/Filter.js new file mode 100644 index 00000000..a3af0976 --- /dev/null +++ b/ui/src/src/components/ui/Filter.js @@ -0,0 +1,137 @@ +import CheckIcon from "@heroicons/react/solid/CheckIcon"; +import SelectorIcon from "@heroicons/react/solid/SelectorIcon"; +import XCircleIcon from "@heroicons/react/solid/XCircleIcon"; +import { Listbox, Transition } from "@headlessui/react"; +import clsx from "clsx"; +import { useMemo } from "react"; + +const CustomListbox = ({ label, value, onChange, children, className }) => ( + + + {label} + + + + {children} + + +); + +const Placeholder = ({ children }) => {children}; + +const Option = ({ value, label, forceSelected }) => ( + + {({ active, selected }) => ( +
+ + {label} +
+ )} +
+); + +const SelectedValue = ({ value, label, onRemoveClick }) => ( + + {label} + + +); + +export const SelectedValues = ({ values, allValues, onRemoveClick }) => ( +
+ {values.map(value => ( + val.id === value).name} + onRemoveClick={onRemoveClick} + /> + ))} +
+); + +const Filter = ({ value, onChange, attributes, onRemoveClick }) => { + const selectedAttribute = useMemo( + () => attributes.find(attribute => attribute.id === value?.attribute), + [attributes, value], + ); + + const selectedValues = value?.values || []; + const handleAttributeChange = attribute => { + onChange({ attribute, values: [] }); + }; + const handleValueChange = newValue => { + const prev = value?.values ?? []; + onChange({ + ...value, + values: prev.includes(newValue) ? prev.filter(value => value !== newValue) : [...prev, newValue], + }); + }; + const handleRemoveValueClick = (e, removeValue) => { + e.stopPropagation(); + const prev = value?.values ?? []; + onChange({ + ...value, + values: prev.filter(value => value !== removeValue), + }); + }; + + return ( +
+ Select attribute} + > + {attributes.map(attribute => ( + + {selectedValues.length > 1 ? "IN" : "="} + + ) : ( + Select value + ) + } + > + {(selectedAttribute?.values ?? []).map(attributeValue => ( + + +
+ ); +}; +export default Filter; diff --git a/ui/src/src/index.css b/ui/src/src/index.css index b4cc7250..02a3b381 100644 --- a/ui/src/src/index.css +++ b/ui/src/src/index.css @@ -1,3 +1,9 @@ +@tailwind base; + +@tailwind components; + +@tailwind utilities; + body { margin: 0; padding: 0; diff --git a/ui/src/src/stories/components/ui/Filter.stories.js b/ui/src/src/stories/components/ui/Filter.stories.js new file mode 100644 index 00000000..723ff284 --- /dev/null +++ b/ui/src/src/stories/components/ui/Filter.stories.js @@ -0,0 +1,44 @@ +// Button.stories.js | Button.stories.jsx + +import React, { useState } from "react"; + +import Filter, { SelectedValues as SelectedValuesComponent } from "../../../components/ui/Filter"; + +export default { + component: Filter, + title: "components/ui/Filter", +}; + +const attributes = [ + { + id: "foo", + name: "foo", + values: [ + { id: "foo-value-1", name: "Foo value 1" }, + { id: "foo-value-2", name: "Foo value 2" }, + ], + }, + { + id: "bar", + name: "Bar", + values: [ + { id: "bar-value-1", name: "Bar value 1" }, + { id: "var-value-2", name: "Bar value 2" }, + ], + }, +]; +const allValues = attributes[0].values; + +export const Primary = () => { + const [state, setState] = useState({ attribute: undefined, values: [] }); + return ; +}; + +export const SelectedValues = () => { + const selectedValues = allValues.map(val => val.id); + return ( +
+ +
+ ); +}; diff --git a/ui/src/tailwind.config.js b/ui/src/tailwind.config.js index eb924bab..3caa47f4 100644 --- a/ui/src/tailwind.config.js +++ b/ui/src/tailwind.config.js @@ -2,7 +2,44 @@ module.exports = { purge: [], darkMode: false, // or 'media' or 'class' theme: { - extend: {}, + colors: { + text: { + DEFAULT: "#2E384D", + fade1: "#69707F", + fade2: "#8798AD", + fade3: "#B0BAC9", + }, + white: "#FFF", + primary: { + DEFAULT: "#2E5BFF", + }, + neutral: { + DEFAULT: "#2E384D", + fade1: "#69707F", + fade2: "#8798AD", + fade3: "#BFC5D2", + fade4: "#F4F6FC", + fade5: "#E0E7FF", + fade6: "#EEF3F5", + }, + success: "#33AC2E", + error: "#D63649", + warning: "#F7C137", + teal: "#00C1D4", + purple: "#8C54FF", + }, + fontSize: { + base: "15px", + sm: "12px", + lg: "28px", + xl: "34px", + xxl: "48px", + }, + extend: { + outline: { + focus: "3px solid #BFC5D2", + }, + }, }, variants: { extend: {}, diff --git a/ui/src/yarn.lock b/ui/src/yarn.lock index 78736f22..d531d670 100644 --- a/ui/src/yarn.lock +++ b/ui/src/yarn.lock @@ -1760,6 +1760,16 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@headlessui/react@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.4.0.tgz#c6d424d8ab10ac925e4423d7f3cbab02c30d736a" + integrity sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw== + +"@heroicons/react@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.3.tgz#4dae106c6019e3250e36401b55b599ad41a68273" + integrity sha512-wdzWbDiFKzeL6xFJsgY2PqvDkx4hFmQDpEFRVj872EA71XOjr8g0DQj5rHWm0y7LwfGOFL0eQmEiMbTyGNOnTQ== + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -5208,6 +5218,11 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clsx@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" From f0e31ab35175a86b189a2a2b12f6becf72951c77 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Sat, 7 Aug 2021 17:18:31 +1000 Subject: [PATCH 02/17] Transitions + better focus management --- ui/src/src/components/ui/Filter.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ui/src/src/components/ui/Filter.js b/ui/src/src/components/ui/Filter.js index a3af0976..38a20105 100644 --- a/ui/src/src/components/ui/Filter.js +++ b/ui/src/src/components/ui/Filter.js @@ -3,17 +3,25 @@ import SelectorIcon from "@heroicons/react/solid/SelectorIcon"; import XCircleIcon from "@heroicons/react/solid/XCircleIcon"; import { Listbox, Transition } from "@headlessui/react"; import clsx from "clsx"; -import { useMemo } from "react"; +import { useMemo, Fragment } from "react"; const CustomListbox = ({ label, value, onChange, children, className }) => ( - + {label} - - {children} - + + {children} + ); @@ -88,7 +96,7 @@ const Filter = ({ value, onChange, attributes, onRemoveClick }) => { }; return ( -
+
{ - {selectedValues.length > 1 ? "IN" : "="} + {selectedValues.length > 1 ? "in" : "="} { ))} - -); - -export const SelectedValues = ({ values, allValues, onRemoveClick }) => ( -
- {values.map(value => ( - val.id === value).name} - onRemoveClick={onRemoveClick} - /> - ))} -
-); - const Filter = ({ value, onChange, attributes, onRemoveClick }) => { const selectedAttribute = useMemo( () => attributes.find(attribute => attribute.id === value?.attribute), diff --git a/ui/src/src/components/ui/Filters.js b/ui/src/src/components/ui/Filters.js new file mode 100644 index 00000000..6b7b2214 --- /dev/null +++ b/ui/src/src/components/ui/Filters.js @@ -0,0 +1,31 @@ +import Filter from "./Filter"; +import PlusCircleIcon from "@heroicons/react/solid/PlusCircleIcon"; + +const Filters = ({ attributes, value, onChange }) => { + const handleFilterChange = index => newValue => onChange(value.map((filter, i) => (i === index ? newValue : value))); + const handleRemoveFilterClick = index => () => + onChange(value.map((filter, i) => (i === index ? undefined : filter)).filter(Boolean)); + const handleAddFilterClick = () => onChange([...value, { attribute: undefined, values: [] }]); + return ( +
+ {value.map((filter, index) => ( + + ))} + +
+ ); +}; + +export default Filters; diff --git a/ui/src/src/components/ui/SelectedValues.js b/ui/src/src/components/ui/SelectedValues.js new file mode 100644 index 00000000..345fb5fb --- /dev/null +++ b/ui/src/src/components/ui/SelectedValues.js @@ -0,0 +1,25 @@ +import XCircleIcon from "@heroicons/react/solid/XCircleIcon"; + +const SelectedValue = ({ value, label, onRemoveClick }) => ( + + {label} + + +); + +const SelectedValues = ({ values, allValues, onRemoveClick }) => ( +
+ {values.map(value => ( + val.id === value).name} + onRemoveClick={onRemoveClick} + /> + ))} +
+); + +export default SelectedValues; diff --git a/ui/src/src/stories/components/ui/Filter.stories.js b/ui/src/src/stories/components/ui/Filter.stories.js index 723ff284..22be2625 100644 --- a/ui/src/src/stories/components/ui/Filter.stories.js +++ b/ui/src/src/stories/components/ui/Filter.stories.js @@ -1,44 +1,15 @@ -// Button.stories.js | Button.stories.jsx - import React, { useState } from "react"; -import Filter, { SelectedValues as SelectedValuesComponent } from "../../../components/ui/Filter"; +import Filter from "../../../components/ui/Filter"; +import { attributes } from "./attributes"; export default { component: Filter, title: "components/ui/Filter", + args: { attributes }, }; -const attributes = [ - { - id: "foo", - name: "foo", - values: [ - { id: "foo-value-1", name: "Foo value 1" }, - { id: "foo-value-2", name: "Foo value 2" }, - ], - }, - { - id: "bar", - name: "Bar", - values: [ - { id: "bar-value-1", name: "Bar value 1" }, - { id: "var-value-2", name: "Bar value 2" }, - ], - }, -]; -const allValues = attributes[0].values; - -export const Primary = () => { +export const Default = args => { const [state, setState] = useState({ attribute: undefined, values: [] }); - return ; -}; - -export const SelectedValues = () => { - const selectedValues = allValues.map(val => val.id); - return ( -
- -
- ); + return ; }; diff --git a/ui/src/src/stories/components/ui/Filters.stories.js b/ui/src/src/stories/components/ui/Filters.stories.js new file mode 100644 index 00000000..b5045fb3 --- /dev/null +++ b/ui/src/src/stories/components/ui/Filters.stories.js @@ -0,0 +1,17 @@ +import React, { useState } from "react"; + +import Filters from "../../../components/ui/Filters"; +import { attributes } from "./attributes"; + +export default { + component: Filters, + title: "components/ui/Filters", + args: { + attributes, + }, +}; + +export const Default = args => { + const [state, setState] = useState([]); + return ; +}; diff --git a/ui/src/src/stories/components/ui/SelectedValues.stories.js b/ui/src/src/stories/components/ui/SelectedValues.stories.js new file mode 100644 index 00000000..dcc0ee4f --- /dev/null +++ b/ui/src/src/stories/components/ui/SelectedValues.stories.js @@ -0,0 +1,22 @@ +import React from "react"; +import SelectedValues from "../../../components/ui/SelectedValues"; +import { attributes } from "./attributes"; + +const allValues = attributes[0].values; + +export default { + component: SelectedValues, + title: "components/ui/SelectedValues", + args: { + values: allValues.map(val => val.id), + allValues, + }, +}; + +export const Default = args => { + return ( +
+ +
+ ); +}; diff --git a/ui/src/src/stories/components/ui/attributes.js b/ui/src/src/stories/components/ui/attributes.js new file mode 100644 index 00000000..c4f7bc8e --- /dev/null +++ b/ui/src/src/stories/components/ui/attributes.js @@ -0,0 +1,18 @@ +export const attributes = [ + { + id: "foo", + name: "foo", + values: [ + { id: "foo-value-1", name: "Foo value 1" }, + { id: "foo-value-2", name: "Foo value 2" }, + ], + }, + { + id: "bar", + name: "Bar", + values: [ + { id: "bar-value-1", name: "Bar value 1" }, + { id: "var-value-2", name: "Bar value 2" }, + ], + }, +]; From 3cb048ce3333dd07bf8b8112d80ed0d3a06166c7 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Sat, 7 Aug 2021 18:22:51 +1000 Subject: [PATCH 04/17] Minor aethetic adjustments --- ui/src/src/components/ui/SelectedValues.js | 2 +- ui/src/src/stories/components/ui/SelectedValues.stories.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/src/components/ui/SelectedValues.js b/ui/src/src/components/ui/SelectedValues.js index 345fb5fb..ab84f454 100644 --- a/ui/src/src/components/ui/SelectedValues.js +++ b/ui/src/src/components/ui/SelectedValues.js @@ -1,7 +1,7 @@ import XCircleIcon from "@heroicons/react/solid/XCircleIcon"; const SelectedValue = ({ value, label, onRemoveClick }) => ( - + {label} + ); diff --git a/ui/src/src/testcases/TestCasesFilter.js b/ui/src/src/testcases/TestCasesFilter.js index df6ee979..a24e55e4 100644 --- a/ui/src/src/testcases/TestCasesFilter.js +++ b/ui/src/src/testcases/TestCasesFilter.js @@ -5,12 +5,37 @@ import LaunchForm from "../launches/LaunchForm"; import { withRouter } from "react-router"; import Select from "react-select"; import qs from "qs"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faMinusCircle } from "@fortawesome/free-solid-svg-icons"; import $ from "jquery"; import * as Utils from "../common/Utils"; import Backend from "../services/backend"; +import Filters from "../components/ui/Filters"; +const mapOldFilterToNew = filter => ({ + values: filter.attrValues.map(({ value }) => value), + attribute: filter.id, +}); +const getAttributeName = (attributes, id) => + ( + attributes.find(function (attribute) { + return attribute.id === id; + }) || { attrValues: [] } + ).name; + +const mapOldAttributeToNew = attribute => ({ + id: attribute.id, + name: attribute.name, + values: attribute.attrValues?.length + ? attribute.attrValues + : attribute.values.map(value => ({ id: value.toLowerCase(), name: value })), +}); +const mapNewFilterToOld = attributes => filter => ({ + id: filter.attribute, + name: getAttributeName(attributes, filter.attribute), + attrValues: filter.values.map(value => ({ value })), + title: "Select an attribute", +}); +const mapNewFiltersToOld = (attributes, filters) => filters.map(mapNewFilterToOld(attributes)); +const mapAttributesToNewFormat = attributes => attributes.map(mapOldAttributeToNew); class TestCasesFilter extends Component { constructor(props) { super(props); @@ -45,7 +70,7 @@ class TestCasesFilter extends Component { this.changeFilterAttributeId = this.changeFilterAttributeId.bind(this); this.changeFilterAttributeValues = this.changeFilterAttributeValues.bind(this); this.handleFilter = this.handleFilter.bind(this); - this.getAttributeName = this.getAttributeName.bind(this); + this.createLaunchModal = this.createLaunchModal.bind(this); this.saveSuite = this.saveSuite.bind(this); this.showSuiteModal = this.showSuiteModal.bind(this); @@ -54,18 +79,13 @@ class TestCasesFilter extends Component { this.getProjectAttributesSelect = this.getProjectAttributesSelect.bind(this); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.projectAttributes) { this.state.projectAttributes = nextProps.projectAttributes; - this.state.testSuite.filter.filters.forEach( - function (filter) { - filter.name = this.getAttributeName(filter.id); - }.bind(this), - ); this.state.groupsToDisplay.forEach( function (groupToDisplay) { - groupToDisplay.label = this.getAttributeName(groupToDisplay.value); + groupToDisplay.label = getAttributeName(this.state.projectAttributes, groupToDisplay.value); }.bind(this), ); } @@ -82,7 +102,7 @@ class TestCasesFilter extends Component { this.state.testSuiteNameToDisplay = this.state.testSuite.name; this.state.groupsToDisplay = this.state.testSuite.filter.groups.map( function (attrId) { - return { value: attrId, label: this.getAttributeName(attrId) }; + return { value: attrId, label: getAttributeName(this.state.projectAttributes, attrId) }; }.bind(this), ); this.setState(this.state); @@ -99,7 +119,7 @@ class TestCasesFilter extends Component { this.state.testSuite.filter.groups = params.groups; this.state.groupsToDisplay = params.groups.map( function (attrId) { - return { value: attrId, label: this.getAttributeName(attrId) }; + return { value: attrId, label: getAttributeName(this.state.projectAttributes, attrId) }; }.bind(this), ); } @@ -122,7 +142,7 @@ class TestCasesFilter extends Component { this.state.testSuite.filter.filters.push({ id: key, attrValues: map[key].map(val => ({ value: val })), - title: this.getAttributeName(key), + title: getAttributeName(this.state.projectAttributes, key), }); }.bind(this), ); @@ -188,14 +208,6 @@ class TestCasesFilter extends Component { this.props.onFilter(this.state.testSuite.filter); } - getAttributeName(id) { - return ( - this.state.projectAttributes.find(function (attribute) { - return attribute.id === id; - }) || { attrValues: [] } - ).name; - } - createLaunchModal() { this.state.createdLaunch = { name: "", @@ -251,13 +263,24 @@ class TestCasesFilter extends Component { return projectAttributes; } + handleFilterChange = value => { + this.setState({ + ...this.state, + testSuite: { + ...this.state.testSuite, + filter: { ...this.state.filter, filters: mapNewFiltersToOld(this.state.projectAttributes, value) }, + }, + }); + }; render() { + const newFilters = this.state.testSuite.filter.filters.map(mapOldFilterToNew); + const newFormatAttributes = mapAttributesToNewFormat(this.state.projectAttributes); return (

{this.state.testSuiteNameToDisplay}

-
-
Grouping
+
+
Grouping
this.changeFilterAttributeId(i, e)} - options={this.getProjectAttributesSelect()} - /> - +); + +export default TextInput; diff --git a/ui/src/src/components/ui/focus.js b/ui/src/src/components/ui/focus.js new file mode 100644 index 00000000..afd4e19d --- /dev/null +++ b/ui/src/src/components/ui/focus.js @@ -0,0 +1,2 @@ +export const focusClasses = + "focus:transition-all focus:outline-none duration-100 focus:ring-offset-2 focus:ring focus:ring-neutral-fade5"; diff --git a/ui/src/src/components/ui/index.js b/ui/src/src/components/ui/index.js new file mode 100644 index 00000000..a1fa576d --- /dev/null +++ b/ui/src/src/components/ui/index.js @@ -0,0 +1,2 @@ +export { default as Listbox } from "./Listbox"; +export { default as Loading } from "./Loading"; diff --git a/ui/src/src/components/ui/input.js b/ui/src/src/components/ui/input.js new file mode 100644 index 00000000..f69482f7 --- /dev/null +++ b/ui/src/src/components/ui/input.js @@ -0,0 +1,14 @@ +import { focusClasses } from "./focus"; + +export const inputTextClasses = "text-neutral text-left text-base"; +export const inputLayoutClasses = "min-h-10 z-1"; +export const inputBackgroundClasses = "bg-neutral-fade5 bg-opacity-20"; +export const inputBorderClasses = "border border-neutral-fade5 rounded-md"; + +export const inputClasses = [ + focusClasses, + inputTextClasses, + inputLayoutClasses, + inputBackgroundClasses, + inputBorderClasses, +]; diff --git a/ui/src/src/domain/index.ts b/ui/src/src/domain/index.ts new file mode 100644 index 00000000..bad2a6bd --- /dev/null +++ b/ui/src/src/domain/index.ts @@ -0,0 +1,18 @@ +export interface WithID { + id: string; +} + +export interface AttributeValue extends WithID { + name: string; +} + +export interface Attribute extends WithID { + name: string; + values: AttributeValue[]; +} + +export interface TestCaseFilter {} +export interface Suite extends WithID { + name: string; + filter: TestCaseFilter; +} diff --git a/ui/src/src/index.css b/ui/src/src/index.css index 02a3b381..2a53bbf1 100644 --- a/ui/src/src/index.css +++ b/ui/src/src/index.css @@ -4,8 +4,16 @@ @tailwind utilities; +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@300;400;600;700&display=swap'); + body { margin: 0; padding: 0; - font-family: sans-serif; + font-family: 'Noto Sans', sans-serif; + font-size: 15px; +} + +html { + font-size: 15px; } + diff --git a/ui/src/src/react-app-env.d.ts b/ui/src/src/react-app-env.d.ts new file mode 100644 index 00000000..6431bc5f --- /dev/null +++ b/ui/src/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/src/src/services/backend.js b/ui/src/src/services/backend.js index d97a9d31..a268dc21 100644 --- a/ui/src/src/services/backend.js +++ b/ui/src/src/services/backend.js @@ -8,8 +8,8 @@ const getOptions = options => ({ const fetchInternal = (url, method, options) => fetch(getApiBaseUrl(url), { ...getOptions(options), method }).then(async response => { if (!response.ok) { - if (response.status === 401 && window.location.pathname !== "/auth" && window.location.pathname !== "/idpauth" && window.location.pathname !== "/login") { - window.location.href = "/auth?" + qs.stringify({ retpath: window.location.pathname }); + if (response.status === 401 && window.location.pathname !== "/login" && window.location.pathname !== "/idpauth" && window.location.pathname !== "/login") { + window.location.href = "/login?" + qs.stringify({ retpath: window.location.pathname }); } else { throw new Error(await response.text()); } diff --git a/ui/src/src/services/backend.ts b/ui/src/src/services/backend.ts new file mode 100644 index 00000000..c9e7b995 --- /dev/null +++ b/ui/src/src/services/backend.ts @@ -0,0 +1,76 @@ +import qs from "qs"; +import * as Utils from "../common/Utils"; +import { Attribute, Suite } from "../domain"; +export const getApiBaseUrl = (url: string) => (process.env.REACT_APP_BASE_API_URL || "/api/") + url; +const getOptions = (options?: RequestInit) => ({ + ...options, + credentials: "include" as RequestCredentials, +}); + +export type HttpMethod = "GET" | "PUT" | "DELETE" | "POST"; + +const fetchInternal = (url: string, method: HttpMethod, options?: RequestInit) => + fetch(getApiBaseUrl(url), { ...getOptions(options), method }) + .then(async response => { + if (!response.ok) { + if (response.status === 401 && window.location.pathname !== "/login") { + window.location.href = "/login?" + qs.stringify({ retpath: window.location.pathname }); + } else { + throw new Error(await response.text()); + } + } + return response; + }) + .catch(error => { + Utils.onErrorMessage("Couldn't fetch testsuite: ", error); + throw error; + }); +const fetchJSON = (url: string, method: HttpMethod, options?: RequestInit) => + fetchInternal(url, method, options).then(response => response.json()); + +const getMutationOptions = (body: TBody) => ({ + body: JSON.stringify(body), + headers: { "content-type": "application/json" }, +}); + +const backend = { + get: (url: string, options?: RequestInit) => fetchJSON(url, "GET", options) as Promise, + post: (url: string, body: TBody) => fetchJSON(url, "POST", getMutationOptions(body)), + delete: (url: string) => fetchInternal(url, "DELETE"), + put: (url: string, body: TBody) => fetchJSON(url, "PUT", getMutationOptions(body)), +}; + +export interface TestCasesFilter {} +const mapServerAttributesToClient = (response: Attribute[]) => { + const attributes = response.sort((a, b) => (a.name || "").localeCompare(b.name)); + attributes.unshift({ + id: "broken", + name: "Broken", + values: [ + { id: "true", name: "True" }, + { id: "false", name: "False" }, + ], + }); + return attributes; +}; + +export const backendService = { + project: (projectId: string) => ({ + testSuites: { + single: (testSuiteId: string) => ({ + get: () => backend.get(projectId + "/testsuite/" + testSuiteId), + }), + create: (suite: Suite) => backend.post(projectId + "/testsuite/", suite), + }, + attributes: { + list: () => backend.get(projectId + "/attribute").then(mapServerAttributesToClient), + }, + testcases: { + count: (filter: TestCasesFilter) => backend.get(projectId + "/testcase/count?" + qs.stringify(filter)), + tree: (filter: TestCasesFilter) => backend.get(projectId + "/testcase/tree?" + qs.stringify(filter)), + list: (filter: TestCasesFilter) => backend.get(projectId + "/testcase?" + qs.stringify(filter)), + }, + }), +}; + +export default backend; diff --git a/ui/src/src/services/navigation.ts b/ui/src/src/services/navigation.ts new file mode 100644 index 00000000..7b29d3a4 --- /dev/null +++ b/ui/src/src/services/navigation.ts @@ -0,0 +1,8 @@ +export const navigation = { + project: (projectID: string) => ({ + testCases: { + list: () => `/${projectID}/testcases`, + single: (testCaseID: string) => `/${projectID}/testcases/?testcase=${testCaseID}`, + }, + }), +}; diff --git a/ui/src/src/stories/components/suite/Form.stories.js b/ui/src/src/stories/components/suite/Form.stories.js new file mode 100644 index 00000000..9e1d7e15 --- /dev/null +++ b/ui/src/src/stories/components/suite/Form.stories.js @@ -0,0 +1,14 @@ +import React from "react"; +import SuiteDialog from "../../../components/suite/Dialog"; +import Form from "../../../components/suite/Form"; +export default { + component: Form, + title: "components/suite/Form", + args: { + initialValues: { name: "Test suite" }, + }, + argTypes: { onSubmit: { action: true }, onCancel: { action: true } }, +}; + +export const Default = args =>
; +export const Dialog = args => ; diff --git a/ui/src/src/stories/components/ui/Button.stories.js b/ui/src/src/stories/components/ui/Button.stories.js new file mode 100644 index 00000000..81fe8579 --- /dev/null +++ b/ui/src/src/stories/components/ui/Button.stories.js @@ -0,0 +1,17 @@ +import React from "react"; + +import Button from "../../../components/ui/Button"; + +export default { + component: Button, + title: "components/ui/Button", + args: { + children: "Click me", + }, + // parameters: { actions: { argTypesRegex: "^on.*" } }, + argTypes: { onClick: { action: true } }, +}; + +export const Default = args => - -
-
-
+
+
Filter
+ {projectAttributes?.length && ( + + )}
- ); - } -} + + + setShowSuiteDialog(false)} + /> + + ); +}; -export default withRouter(TestCasesFilter); +export default TestCasesFilter; diff --git a/ui/src/tailwind.config.js b/ui/src/tailwind.config.js index 3caa47f4..3d124528 100644 --- a/ui/src/tailwind.config.js +++ b/ui/src/tailwind.config.js @@ -1,17 +1,15 @@ +const color = require("color"); + module.exports = { + important: ".tailwind", purge: [], darkMode: false, // or 'media' or 'class' theme: { colors: { - text: { - DEFAULT: "#2E384D", - fade1: "#69707F", - fade2: "#8798AD", - fade3: "#B0BAC9", - }, white: "#FFF", primary: { DEFAULT: "#2E5BFF", + hover: color("#2E5BFF").darken(0.05).hex(), }, neutral: { DEFAULT: "#2E384D", @@ -28,6 +26,9 @@ module.exports = { teal: "#00C1D4", purple: "#8C54FF", }, + fontFamily: { + sans: ["Noto Sans", "sans-serif"], + }, fontSize: { base: "15px", sm: "12px", @@ -35,10 +36,18 @@ module.exports = { xl: "34px", xxl: "48px", }, + extend: { outline: { focus: "3px solid #BFC5D2", }, + minHeight: { + 10: "40px", + }, + spacing: { + 3: "1rem !important", + 5: "1.25rem !important", + }, }, }, variants: { diff --git a/ui/src/tsconfig.json b/ui/src/tsconfig.json new file mode 100644 index 00000000..e535b1a0 --- /dev/null +++ b/ui/src/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ESNEXT", + "module": "ESNext", + "jsx": "react-jsx", + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "allowSyntheticDefaultImports": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "noEmitHelpers": true, + "importHelpers": true, + }, + "include": [ + "**/*.ts*" + ] +} diff --git a/ui/src/webpack.config.js b/ui/src/webpack.config.js deleted file mode 100644 index 1281a45a..00000000 --- a/ui/src/webpack.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const webpack = require("webpack"); - -module.exports = { - plugins: [ - new webpack.ProvidePlugin({ - React: "react", - $: "jquery", - _: "lodash", - ReactDOM: "react-dom", - cssModule: "react-css-modules", - Promise: "bluebird", - }), - ], -}; diff --git a/ui/src/yarn.lock b/ui/src/yarn.lock index d531d670..70e6a4c9 100644 --- a/ui/src/yarn.lock +++ b/ui/src/yarn.lock @@ -1431,7 +1431,14 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.6": +"@babel/runtime@^7.10.5": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" + integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.6": version "7.14.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== @@ -2164,17 +2171,17 @@ prop-types "^15.7.2" react-is "^16.6.3" -"@storybook/addon-actions@6.3.4", "@storybook/addon-actions@^6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.3.4.tgz#3dfd9ee288ce3569c6aa53092f283a39460167eb" - integrity sha512-+fTTaWepSaMeMD96zsNap0AcFUu3xyGepIucNMvkpfJfQnyYzr7S0qyjpsW84DLKHPcjujNvOOi98y7cAKV6tw== - dependencies: - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/client-api" "6.3.4" - "@storybook/components" "6.3.4" - "@storybook/core-events" "6.3.4" - "@storybook/theming" "6.3.4" +"@storybook/addon-actions@6.3.6", "@storybook/addon-actions@^6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.3.6.tgz#691d61d6aca9c4b3edba50c531cbe4d4139ed451" + integrity sha512-1MBqCbFiupGEDyIXqFkzF4iR8AduuB7qSNduqtsFauvIkrG5bnlbg5JC7WjnixkCaaWlufgbpasEHioXO9EXGw== + dependencies: + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/client-api" "6.3.6" + "@storybook/components" "6.3.6" + "@storybook/core-events" "6.3.6" + "@storybook/theming" "6.3.6" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -2187,17 +2194,17 @@ util-deprecate "^1.0.2" uuid-browser "^3.1.0" -"@storybook/addon-backgrounds@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.3.4.tgz#2e506048ec5189d5ee204f6b8bc66d81fd26ad7c" - integrity sha512-Ck0AdZtAJl5sCA6qSqPUY2tEk4X2hmsiEfT/melDoSLE9s+D98CtWRfnajPtAypTlg+a81UjX0wEtWSXWKRjfg== - dependencies: - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/components" "6.3.4" - "@storybook/core-events" "6.3.4" - "@storybook/theming" "6.3.4" +"@storybook/addon-backgrounds@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.3.6.tgz#93128e6ebfcb953a83cc2165056dd5815d32cef2" + integrity sha512-1lBVAem2M+ggb1UNVgB7/56LaQAor9lI8q0xtQdAzAkt9K4RbbOsLGRhyUm3QH5OiB3qHHG5WQBujWUD6Qfy4g== + dependencies: + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/components" "6.3.6" + "@storybook/core-events" "6.3.6" + "@storybook/theming" "6.3.6" core-js "^3.8.2" global "^4.4.0" memoizerific "^1.11.3" @@ -2205,24 +2212,24 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-controls@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.3.4.tgz#7849905a0ec56fbea7608c15c08f872211b3a878" - integrity sha512-+MIKcWqsIF6vLoKxukK2m6ADYwyNHOmaMgnJSHKlcNKc7Qxv2w0FZJxIjjkWHXRBIC5MQXxv7/L4sY+EnPdjyg== - dependencies: - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/client-api" "6.3.4" - "@storybook/components" "6.3.4" - "@storybook/node-logger" "6.3.4" - "@storybook/theming" "6.3.4" +"@storybook/addon-controls@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.3.6.tgz#2f8071e5b521375aace60af96e33a19f016581c9" + integrity sha512-wTWmnZl2qEAUqgLh8a7TL5f6w37Q51lAoJNlwxFFBSKtGS7xFUnou4qTUArNy5iKu1cWoVvofJ9RnP1maGByYA== + dependencies: + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/client-api" "6.3.6" + "@storybook/components" "6.3.6" + "@storybook/node-logger" "6.3.6" + "@storybook/theming" "6.3.6" core-js "^3.8.2" ts-dedent "^2.0.0" -"@storybook/addon-docs@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.3.4.tgz#e6dd9026b28d2d007bda7dc3fbcf2d404982939a" - integrity sha512-KoC6GImsC5ZVvco228Jw3+1k/eGeDabjF3+V4In4rPWnOyv0qsFIlE7wcwOCRKcGrPfkBSCWYajH/h+rD0bzUw== +"@storybook/addon-docs@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.3.6.tgz#85b8a72b91f9c43edfaf21c416a9b01ad0e06ea4" + integrity sha512-/ZPB9u3lfc6ZUrgt9HENU1BxAHNfTbh9r2LictQ8o9gYE/BqvZutl2zqilTpVuutQtTgQ6JycVhxtpk9+TDcuA== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -2233,20 +2240,20 @@ "@mdx-js/loader" "^1.6.22" "@mdx-js/mdx" "^1.6.22" "@mdx-js/react" "^1.6.22" - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/builder-webpack4" "6.3.4" - "@storybook/client-api" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/components" "6.3.4" - "@storybook/core" "6.3.4" - "@storybook/core-events" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/builder-webpack4" "6.3.6" + "@storybook/client-api" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/components" "6.3.6" + "@storybook/core" "6.3.6" + "@storybook/core-events" "6.3.6" "@storybook/csf" "0.0.1" - "@storybook/csf-tools" "6.3.4" - "@storybook/node-logger" "6.3.4" - "@storybook/postinstall" "6.3.4" - "@storybook/source-loader" "6.3.4" - "@storybook/theming" "6.3.4" + "@storybook/csf-tools" "6.3.6" + "@storybook/node-logger" "6.3.6" + "@storybook/postinstall" "6.3.6" + "@storybook/source-loader" "6.3.6" + "@storybook/theming" "6.3.6" acorn "^7.4.1" acorn-jsx "^5.3.1" acorn-walk "^7.2.0" @@ -2269,36 +2276,36 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-essentials@^6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.3.4.tgz#2456c81c939b64918f16d851fd5e52234efa94c5" - integrity sha512-z06IhJaHkledTPc1TdkMikDwcJhPQZCEoQftSj0MwjafdjntYUbNYsGlD40Lo/wmEW4OtUiHOx/3TWaC1WiIbw== +"@storybook/addon-essentials@^6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.3.6.tgz#29f5249daee086fe2d14c899ae61712b8c8fbcbd" + integrity sha512-FUrpCeINaN4L9L81FswtQFEq2xLwj3W7EyhmqsZcYSr64nscpQyjlPVjs5zhrEanOGIf+4E+mBmWafxbYufXwQ== dependencies: - "@storybook/addon-actions" "6.3.4" - "@storybook/addon-backgrounds" "6.3.4" - "@storybook/addon-controls" "6.3.4" - "@storybook/addon-docs" "6.3.4" + "@storybook/addon-actions" "6.3.6" + "@storybook/addon-backgrounds" "6.3.6" + "@storybook/addon-controls" "6.3.6" + "@storybook/addon-docs" "6.3.6" "@storybook/addon-measure" "^2.0.0" - "@storybook/addon-toolbars" "6.3.4" - "@storybook/addon-viewport" "6.3.4" - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/node-logger" "6.3.4" + "@storybook/addon-toolbars" "6.3.6" + "@storybook/addon-viewport" "6.3.6" + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/node-logger" "6.3.6" core-js "^3.8.2" regenerator-runtime "^0.13.7" storybook-addon-outline "^1.4.1" ts-dedent "^2.0.0" -"@storybook/addon-links@^6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-6.3.4.tgz#2ec5b7532e67303b7ec693ecc22a6c4feac93cb3" - integrity sha512-qIey6kNcrg43vsmMmEGXGMbO1Wjqc6hbcMaLstUn3xN29vZnjUcv7PVPFlJsBBYIxW7gXBtRmqjdxN5UD0bvZA== +"@storybook/addon-links@^6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-6.3.6.tgz#dc410d5b4a0d222b6b8d0ef03da7a4c16919c092" + integrity sha512-PaeAJTjwtPlhrLZlaSQ1YIFA8V0C1yI0dc351lPbTiE7fJ7DwTE03K6xIF/jEdTo+xzhi2PM1Fgvi/SsSecI8w== dependencies: - "@storybook/addons" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/core-events" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/core-events" "6.3.6" "@storybook/csf" "0.0.1" - "@storybook/router" "6.3.4" + "@storybook/router" "6.3.6" "@types/qs" "^6.9.5" core-js "^3.8.2" global "^4.4.0" @@ -2323,37 +2330,52 @@ postcss-loader "^4.2.0" style-loader "^1.3.0" -"@storybook/addon-toolbars@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.3.4.tgz#fddf547ea447679b7c02ecb659f602809cec58d0" - integrity sha512-QN3EWTQzlUa3VQG9YIJu79Hi1O3Kpft4QRFZZpv7dmLT0Hi8jOe4tFoTrFD1VMJyjQJ45iFOhexu6V2HwonHAQ== +"@storybook/addon-toolbars@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.3.6.tgz#41f5f29988260d2aad9431b7a91f57e848c3e0bf" + integrity sha512-VpwkMtvT/4KNjqdO2SCkFw4koMgYN2k8hckbTGRzuUYYTHBvl9yK4q0A7RELEnkm/tsmDI1TjenV/MBifp2Aiw== dependencies: - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/client-api" "6.3.4" - "@storybook/components" "6.3.4" - "@storybook/theming" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/client-api" "6.3.6" + "@storybook/components" "6.3.6" + "@storybook/theming" "6.3.6" core-js "^3.8.2" regenerator-runtime "^0.13.7" -"@storybook/addon-viewport@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.3.4.tgz#9ea72c6fc37efde6d5a3f0fe0368b90c2572afd1" - integrity sha512-6h52s1F+MkIkdAuUgmDv4L9aYRMPB+8gWnPGbBAkm8PsHglddr1QkWZAG7yZJR1+mm4oM5oCyRMvZ0V/oKePhg== - dependencies: - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/components" "6.3.4" - "@storybook/core-events" "6.3.4" - "@storybook/theming" "6.3.4" +"@storybook/addon-viewport@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.3.6.tgz#9117316e918559d389a19571166579858b25b09b" + integrity sha512-Z5eztFFGd6vd+38sDurfTkIr9lY6EYWtMJzr5efedRZGg2IZLXZxQCoyjKEB29VB/IIjHEYHhHSh4SFsHT/m6g== + dependencies: + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/components" "6.3.6" + "@storybook/core-events" "6.3.6" + "@storybook/theming" "6.3.6" core-js "^3.8.2" global "^4.4.0" memoizerific "^1.11.3" prop-types "^15.7.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.3.4", "@storybook/addons@^6.3.0": +"@storybook/addons@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.3.6.tgz#330fd722bdae8abefeb029583e89e51e62c20b60" + integrity sha512-tVV0vqaEEN9Md4bgScwfrnZYkN8iKZarpkIOFheLev+PHjSp8lgWMK5SNWDlbBYqfQfzrz9xbs+F07bMjfx9jQ== + dependencies: + "@storybook/api" "6.3.6" + "@storybook/channels" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/core-events" "6.3.6" + "@storybook/router" "6.3.6" + "@storybook/theming" "6.3.6" + core-js "^3.8.2" + global "^4.4.0" + regenerator-runtime "^0.13.7" + +"@storybook/addons@^6.3.0": version "6.3.4" resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.3.4.tgz#016c5c3e36c78a320eb8b022cf7fe556d81577c2" integrity sha512-rf8K8X3JrB43gq5nw5SYgfucQkFg2QgUMWdByf7dQ4MyIl5zet+2MYiSXJ9lfbhGKJZ8orc81rmMtiocW4oBjg== @@ -2394,10 +2416,36 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-webpack4@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.3.4.tgz#f798d50312d7c70b1d3e9e65d2551d80be9e11dd" - integrity sha512-fDExaPYseRmb/vmxIj+DrbAvUg9reodN32Ssb2wTD4u+ZV5VZ/TDEgYuGqi/Hz/9CLAKtM4DEhMj/9TTILm/Rw== +"@storybook/api@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.3.6.tgz#b110688ae0a970c9443d47b87616a09456f3708e" + integrity sha512-F5VuR1FrEwD51OO/EDDAZXNfF5XmJedYHJLwwCB4az2ZMrzG45TxGRmiEohrSTO6wAHGkAvjlEoX5jWOCqQ4pw== + dependencies: + "@reach/router" "^1.3.4" + "@storybook/channels" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/core-events" "6.3.6" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.3.6" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.3.6" + "@types/reach__router" "^1.3.7" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + memoizerific "^1.11.3" + qs "^6.10.0" + regenerator-runtime "^0.13.7" + store2 "^2.12.0" + telejson "^5.3.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + +"@storybook/builder-webpack4@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.3.6.tgz#fe444abfc178e005ea077e2bcfd6ae7509522908" + integrity sha512-LhTPQQowS2t6BRnyfusWZLbhjjf54/HiQyovJTTDnqrCiO6QoCMbVnp79LeO1aSkpQCKoeqOZ7TzH87fCytnZA== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -2420,20 +2468,20 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/channel-postmessage" "6.3.4" - "@storybook/channels" "6.3.4" - "@storybook/client-api" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/components" "6.3.4" - "@storybook/core-common" "6.3.4" - "@storybook/core-events" "6.3.4" - "@storybook/node-logger" "6.3.4" - "@storybook/router" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/channel-postmessage" "6.3.6" + "@storybook/channels" "6.3.6" + "@storybook/client-api" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/components" "6.3.6" + "@storybook/core-common" "6.3.6" + "@storybook/core-events" "6.3.6" + "@storybook/node-logger" "6.3.6" + "@storybook/router" "6.3.6" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.3.4" - "@storybook/ui" "6.3.4" + "@storybook/theming" "6.3.6" + "@storybook/ui" "6.3.6" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" autoprefixer "^9.8.6" @@ -2470,14 +2518,14 @@ webpack-hot-middleware "^2.25.0" webpack-virtual-modules "^0.2.2" -"@storybook/channel-postmessage@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.3.4.tgz#1a0000aefc9494d5585a1d2c7bdb75f540965f70" - integrity sha512-UIHNrMD9ZaT249nkKXibqRjKEoqfeFJk5HKW2W17/Z/imVcKG9THBnRJ7cb+r7LqS8Yoh+Q87ycRqcPVLRJ/Xw== +"@storybook/channel-postmessage@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.3.6.tgz#f29c3678161462428e78c9cfed2da11ffca4acb0" + integrity sha512-GK7hXnaa+1pxEeMpREDzAZ3+2+k1KN1lbrZf+V7Kc1JZv1/Ji/vxk8AgxwiuzPAMx5J0yh/FduPscIPZ87Pibw== dependencies: - "@storybook/channels" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/core-events" "6.3.4" + "@storybook/channels" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/core-events" "6.3.6" core-js "^3.8.2" global "^4.4.0" qs "^6.10.0" @@ -2492,15 +2540,24 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/cli@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/cli/-/cli-6.3.4.tgz#0d8d675992c2ff9910a7e32fa43afa028cdf44da" - integrity sha512-lXODHx8tvhFWa5dCOzv7OQ5Oo19d6I1gdCw1KAFNSPc49xO6uKZGj9UT6gRGPj07dF2dlwnb+v4EyHEbxK+HuA== +"@storybook/channels@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.3.6.tgz#a258764ed78fd836ff90489ae74ac055312bf056" + integrity sha512-gCIQVr+dS/tg3AyCxIvkOXMVAs08BCIHXsaa2+XzmacnJBSP+CEHtI6IZ8WEv7tzZuXOiKLVg+wugeIh4j2I4g== + dependencies: + core-js "^3.8.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + +"@storybook/cli@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/cli/-/cli-6.3.6.tgz#8e06f32bed5dcf59c699a8739a4ce18abe126d35" + integrity sha512-6yCep4yQ1RJBYWZy2Sdf4vOxnE8gYhRjvtgdM7cviwnkvXWfKBRTIUAxgzV6ybR1nuEZ5mcWqcw4aPgGYEXUUA== dependencies: "@babel/core" "^7.12.10" "@babel/preset-env" "^7.12.11" - "@storybook/codemod" "6.3.4" - "@storybook/node-logger" "6.3.4" + "@storybook/codemod" "6.3.6" + "@storybook/node-logger" "6.3.6" "@storybook/semver" "^7.3.2" boxen "^4.2.0" chalk "^4.1.0" @@ -2524,16 +2581,16 @@ ts-dedent "^2.0.0" update-notifier "^5.0.1" -"@storybook/client-api@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.3.4.tgz#7dd6dda0126012ed37fa885642973cc75366b5a8" - integrity sha512-lOrfz8ic3+nHZzqIdNH2I7Q3Wp0kS/Ic0PD/3QKvI2f6iVIapIjjWW1xAuor80Dl7rMhOn8zxgXta+7G7Pn2yQ== +"@storybook/client-api@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.3.6.tgz#4826ce366ae109f608da6ade24b29efeb9b7f7dd" + integrity sha512-Q/bWuH691L6k7xkiKtBmZo8C+ijgmQ+vc2Fz8pzIRZuMV8ROL74qhrS4BMKV4LhiYm4f8todtWfaQPBjawZMIA== dependencies: - "@storybook/addons" "6.3.4" - "@storybook/channel-postmessage" "6.3.4" - "@storybook/channels" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/core-events" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/channel-postmessage" "6.3.6" + "@storybook/channels" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/core-events" "6.3.6" "@storybook/csf" "0.0.1" "@types/qs" "^6.9.5" "@types/webpack-env" "^1.16.0" @@ -2556,16 +2613,24 @@ core-js "^3.8.2" global "^4.4.0" -"@storybook/codemod@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/codemod/-/codemod-6.3.4.tgz#0c4361f1868fb64c57868939d478ef454d5269c5" - integrity sha512-zc99fxlYm/m7htkRCDUSerqvhZqEIfqxKrTokBOCfgyOAMiyQoewtvgAgDiu6CoTAhDjASJm5HCc+Qrn9/u8Rw== +"@storybook/client-logger@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.3.6.tgz#020ba518ab8286194ce103a6ff91767042e296c0" + integrity sha512-qpXQ52ylxPm7l3+WAteV42NmqWA+L1FaJhMOvm2gwl3PxRd2cNXn2BwEhw++eA6qmJH/7mfOKXG+K+QQwOTpRA== + dependencies: + core-js "^3.8.2" + global "^4.4.0" + +"@storybook/codemod@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/codemod/-/codemod-6.3.6.tgz#fa9712e1b516d77e836446250f6dce92fe863beb" + integrity sha512-pyNif3igrpJu0J7U0BptUCNpj+XAsnOJFhBUep7QtbYE3taIH2dGlpGXvyD5kN4zH4Mk/o2KdMp/OxETv1M4hA== dependencies: "@babel/types" "^7.12.11" "@mdx-js/mdx" "^1.6.22" "@storybook/csf" "0.0.1" - "@storybook/csf-tools" "6.3.4" - "@storybook/node-logger" "6.3.4" + "@storybook/csf-tools" "6.3.6" + "@storybook/node-logger" "6.3.6" core-js "^3.8.2" cross-spawn "^7.0.3" globby "^11.0.2" @@ -2575,7 +2640,37 @@ recast "^0.19.0" regenerator-runtime "^0.13.7" -"@storybook/components@6.3.4", "@storybook/components@^6.3.0": +"@storybook/components@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.3.6.tgz#bc2fa1dbe59f42b5b2aeb9f84424072835d4ce8b" + integrity sha512-aZkmtAY8b+LFXG6dVp6cTS6zGJuxkHRHcesRSWRQPxtgitaz1G58clRHxbKPRokfjPHNgYA3snogyeqxSA7YNQ== + dependencies: + "@popperjs/core" "^2.6.0" + "@storybook/client-logger" "6.3.6" + "@storybook/csf" "0.0.1" + "@storybook/theming" "6.3.6" + "@types/color-convert" "^2.0.0" + "@types/overlayscrollbars" "^1.12.0" + "@types/react-syntax-highlighter" "11.0.5" + color-convert "^2.0.1" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + markdown-to-jsx "^7.1.3" + memoizerific "^1.11.3" + overlayscrollbars "^1.13.1" + polished "^4.0.5" + prop-types "^15.7.2" + react-colorful "^5.1.2" + react-popper-tooltip "^3.1.1" + react-syntax-highlighter "^13.5.3" + react-textarea-autosize "^8.3.0" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + +"@storybook/components@^6.3.0": version "6.3.4" resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.3.4.tgz#c872ec267edf315eaada505be8595c70eb6db09b" integrity sha512-0hBKTkkQbW+daaA6nRedkviPr2bEzy1kwq0H5eaLKI1zYeXN3U5Z8fVhO137PPqH5LmLietrmTPkqiljUBk9ug== @@ -2605,18 +2700,18 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/core-client@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.3.4.tgz#ac75ede84f4ce87c7fc3b31a5d7f602fa712f845" - integrity sha512-DIxDWnSoUEwqyu01EygoAhkNwvEIIooFXZleTesZYkjAbEQJJ6KUmgehaVC9BKoTKYf2OQW6HdFkqrW034n93g== +"@storybook/core-client@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.3.6.tgz#7def721aa15d4faaff574780d30b92055db7261c" + integrity sha512-Bq86flEdXdMNbdHrGMNQ6OT1tcBQU8ym56d+nG46Ctjf5GN+Dl+rPtRWuu7cIZs10KgqJH+86DXp+tvpQIDidg== dependencies: - "@storybook/addons" "6.3.4" - "@storybook/channel-postmessage" "6.3.4" - "@storybook/client-api" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/core-events" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/channel-postmessage" "6.3.6" + "@storybook/client-api" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/core-events" "6.3.6" "@storybook/csf" "0.0.1" - "@storybook/ui" "6.3.4" + "@storybook/ui" "6.3.6" airbnb-js-shims "^2.2.1" ansi-to-html "^0.6.11" core-js "^3.8.2" @@ -2628,10 +2723,10 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/core-common@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.3.4.tgz#b51fe790d1c9e664ce4b70c685911c7951832476" - integrity sha512-RL+Q7RMbCQrqNMi+pKB1IdtoUnRY6PuEnabcbaUrQBkc+DyR6Q8iOz71NR3PxRZgVbHNwlN8QEroeF31PFzUEg== +"@storybook/core-common@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.3.6.tgz#da8eed703b609968e15177446f0f1609d1d6d0d0" + integrity sha512-nHolFOmTPymI50j180bCtcf1UJZ2eOnYaECRtHvVrCUod5KFF7wh2EHrgWoKqrKrsn84UOY/LkX2C2WkbYtWRg== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -2654,7 +2749,7 @@ "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" "@babel/register" "^7.12.1" - "@storybook/node-logger" "6.3.4" + "@storybook/node-logger" "6.3.6" "@storybook/semver" "^7.3.2" "@types/glob-base" "^0.3.0" "@types/micromatch" "^4.0.1" @@ -2689,17 +2784,24 @@ dependencies: core-js "^3.8.2" -"@storybook/core-server@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.3.4.tgz#03f931d938061b7b14c154167d8639ad36512e48" - integrity sha512-BIhTYoLbxd8RY1wVji3WbEaZ1LxhccZhF/q2jviXRCdeBQqFcFFvMjRixrGFnr8/FTh06pSSoD8XsIMyK5y9fA== - dependencies: - "@storybook/builder-webpack4" "6.3.4" - "@storybook/core-client" "6.3.4" - "@storybook/core-common" "6.3.4" - "@storybook/csf-tools" "6.3.4" - "@storybook/manager-webpack4" "6.3.4" - "@storybook/node-logger" "6.3.4" +"@storybook/core-events@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.3.6.tgz#c4a09e2c703170995604d63e46e45adc3c9cd759" + integrity sha512-Ut1dz96bJ939oSn5t1ckPXd3WcFejK96Sb3+R/z23vEHUWGBFtygGyw8r/SX/WNDVzGmQU8c+mzJJTZwCBJz8A== + dependencies: + core-js "^3.8.2" + +"@storybook/core-server@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.3.6.tgz#43c1415573c3b72ec6b9ae48d68e1bb446722f7c" + integrity sha512-47ZcfxYn7t891oAMG98iH1BQIgQT9Yk/2BBNVCWY43Ong+ME1xJ6j4C/jkRUOseP7URlfLUQsUYKAYJNVijDvg== + dependencies: + "@storybook/builder-webpack4" "6.3.6" + "@storybook/core-client" "6.3.6" + "@storybook/core-common" "6.3.6" + "@storybook/csf-tools" "6.3.6" + "@storybook/manager-webpack4" "6.3.6" + "@storybook/node-logger" "6.3.6" "@storybook/semver" "^7.3.2" "@types/node" "^14.0.10" "@types/node-fetch" "^2.5.7" @@ -2728,18 +2830,18 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/core@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.3.4.tgz#340c8201180004a067f9ac90f0451cdd4f98b6ad" - integrity sha512-OmzcTKYFLr9KaFnC0JF1Hb/zy7Y7HKrL5FUrcQdJz1td/8Kb6woH9CXAPhX39HUrTNNu69BdM1qQGbzd37j4dA== +"@storybook/core@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.3.6.tgz#604419d346433103675901b3736bfa1ed9bc534f" + integrity sha512-y71VvVEbqCpG28fDBnfNg3RnUPnicwFYq9yuoFVRF0LYcJCy5cYhkIfW3JG8mN2m0P+LzH80mt2Rj6xlSXrkdQ== dependencies: - "@storybook/core-client" "6.3.4" - "@storybook/core-server" "6.3.4" + "@storybook/core-client" "6.3.6" + "@storybook/core-server" "6.3.6" -"@storybook/csf-tools@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.3.4.tgz#5c5df4fcb2bbb71cd6c04b2936ff2d6501562c24" - integrity sha512-q7+owEWlQa7e/YOt8HXqOvNbtk28YqFOt5/RliXr8s6w7KY7PAlXWckdSBThVSHQnpbk6Fzb0RPqNjxM+qEXiw== +"@storybook/csf-tools@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.3.6.tgz#603d9e832f946998b75ff8368fe862375d6cb52c" + integrity sha512-MQevelkEUVNCSjKMXLNc/G8q/BB5babPnSeI0IcJq4k+kLUSHtviimLNpPowMgGJBPx/y9VihH8N7vdJUWVj9w== dependencies: "@babel/generator" "^7.12.11" "@babel/parser" "^7.12.11" @@ -2763,20 +2865,20 @@ dependencies: lodash "^4.17.15" -"@storybook/manager-webpack4@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.3.4.tgz#001635efa3ebcf7aefa330af69aebeb87b1388e2" - integrity sha512-iH6cXhHcHC2RZEhbhtUhvPzAz5zLZQsZo/l+iNqynNvpXcSxvRuh+dytGBy7Np8yzYeaIenU43nNXm85SewdlQ== +"@storybook/manager-webpack4@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.3.6.tgz#a5334aa7ae1e048bca8f4daf868925d7054fb715" + integrity sha512-qh/jV4b6mFRpRFfhk1JSyO2gKRz8PLPvDt2AD52/bTAtNRzypKoiWqyZNR2CJ9hgNQtDrk2CO3eKPrcdKYGizQ== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.3.4" - "@storybook/core-client" "6.3.4" - "@storybook/core-common" "6.3.4" - "@storybook/node-logger" "6.3.4" - "@storybook/theming" "6.3.4" - "@storybook/ui" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/core-client" "6.3.6" + "@storybook/core-common" "6.3.6" + "@storybook/node-logger" "6.3.6" + "@storybook/theming" "6.3.6" + "@storybook/ui" "6.3.6" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" babel-loader "^8.2.2" @@ -2806,7 +2908,18 @@ webpack-dev-middleware "^3.7.3" webpack-virtual-modules "^0.2.2" -"@storybook/node-logger@6.3.4", "@storybook/node-logger@^6.1.14", "@storybook/node-logger@^6.3.4": +"@storybook/node-logger@6.3.6", "@storybook/node-logger@^6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.3.6.tgz#10356608593440a8e3acf2aababef40333a3401b" + integrity sha512-XMDkMN7nVRojjiezrURlkI57+nz3OoH4UBV6qJZICKclxtdKAy0wwOlUSYEUq+axcJ4nvdfzPPoDfGoj37SW7A== + dependencies: + "@types/npmlog" "^4.1.2" + chalk "^4.1.0" + core-js "^3.8.2" + npmlog "^4.1.2" + pretty-hrtime "^1.0.3" + +"@storybook/node-logger@^6.1.14": version "6.3.4" resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.3.4.tgz#ab7bd9f78f9ff10c9d20439734de6882233a9a75" integrity sha512-z65CCCQPbZcHKnofley15+dl8UfrJOeCi7M9c7AJQ0aGh7z1ofySNjwrAf3SO1YMn4K4dkRPDFFq0SY3LA1DLw== @@ -2817,10 +2930,10 @@ npmlog "^4.1.2" pretty-hrtime "^1.0.3" -"@storybook/postinstall@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.3.4.tgz#134981ce120715b5914c04e91e162adc086c92ca" - integrity sha512-HGqpChxBiDH7iQ1KDKr1sWoSdwPrVYPAuhvxAPaQhmL159cGyrcJ1PSit9iQt2NMkm+hIfuTcBQOu6eiZw3iYg== +"@storybook/postinstall@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.3.6.tgz#fd79a6c109b38ced4b9b40db2d27b88ee184d449" + integrity sha512-90Izr8/GwLiXvdF2A3v1PCpWoxUBgqA0TrWGuiWXfJnfFRVlVrX9A/ClGUPSh80L3oE01E6raaOG4wW4JTRKfw== dependencies: core-js "^3.8.2" @@ -2837,18 +2950,18 @@ react-docgen-typescript "^2.0.0" tslib "^2.0.0" -"@storybook/react@^6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.3.4.tgz#b5677736cc90b75594190a2ca367b696d2c9bc7b" - integrity sha512-A4wycYqu/IcIaFb0cw2dPi8azhDj3SWZZModSRaQP3GkKi1LH5p14corP8RLKu2+dWC3FP4ZR6Yyaq/ymPGMzA== +"@storybook/react@^6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.3.6.tgz#593bc0743ad22ed5e6e072e6157c20c704864fc3" + integrity sha512-2c30XTe7WzKnvgHBGOp1dzBVlhcbc3oEX0SIeVE9ZYkLvRPF+J1jG948a26iqOCOgRAW13Bele37mG1gbl4tiw== dependencies: "@babel/preset-flow" "^7.12.1" "@babel/preset-react" "^7.12.10" "@pmmmwh/react-refresh-webpack-plugin" "^0.4.3" - "@storybook/addons" "6.3.4" - "@storybook/core" "6.3.4" - "@storybook/core-common" "6.3.4" - "@storybook/node-logger" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/core" "6.3.6" + "@storybook/core-common" "6.3.6" + "@storybook/node-logger" "6.3.6" "@storybook/react-docgen-typescript-plugin" "1.0.2-canary.253f8c1.0" "@storybook/semver" "^7.3.2" "@types/webpack-env" "^1.16.0" @@ -2882,6 +2995,22 @@ qs "^6.10.0" ts-dedent "^2.0.0" +"@storybook/router@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.3.6.tgz#cea20d64bae17397dc9e1689a656b80a98674c34" + integrity sha512-fQ1n7cm7lPFav7I+fStQciSVMlNdU+yLY6Fue252rpV5Q68bMTjwKpjO9P2/Y3CCj4QD3dPqwEkn4s0qUn5tNA== + dependencies: + "@reach/router" "^1.3.4" + "@storybook/client-logger" "6.3.6" + "@types/reach__router" "^1.3.7" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + memoizerific "^1.11.3" + qs "^6.10.0" + ts-dedent "^2.0.0" + "@storybook/semver@^7.3.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0" @@ -2890,13 +3019,13 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.3.4.tgz#a63af7122e7ea895f7756892b5b592855176cb15" - integrity sha512-6gHuWDJ5MLMSOO5X6R7CbKoZDY+P78vZsZBBY2TB+xIQ3pT9MaSl2aA7bxSO7JCSoFG0GiStHHyYx220nNgaWQ== +"@storybook/source-loader@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.3.6.tgz#2d3d01919baad7a40f67d1150c74e41dea5f1d4c" + integrity sha512-om3iS3a+D287FzBrbXB/IXB6Z5Ql2yc4dFKTy6FPe5v4N3U0p5puWOKUYWWbTX1JbcpRj0IXXo7952G68tcC1g== dependencies: - "@storybook/addons" "6.3.4" - "@storybook/client-logger" "6.3.4" + "@storybook/addons" "6.3.6" + "@storybook/client-logger" "6.3.6" "@storybook/csf" "0.0.1" core-js "^3.8.2" estraverse "^5.2.0" @@ -2924,21 +3053,39 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.3.4.tgz#348ea6eb2b0d5428ab88221485c531fb761ef27d" - integrity sha512-vO2365MEFpqc6Lvg8dK6qBnvcWjdIWi8imXGyWEPPhx27bdAiteCewtXTKX11VM2EmHkJvekbcqNhTIKYPPG7g== +"@storybook/theming@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.3.6.tgz#75624f6d4e01530b87afca3eab9996a16c0370ab" + integrity sha512-mPrQrMUREajNEWxzgR8t0YIZsI9avPv25VNA08fANnwVsc887p4OL5eCTL2dFIlD34YDzAwiyRKYoLj2vDW4nw== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.3.4" - "@storybook/api" "6.3.4" - "@storybook/channels" "6.3.4" - "@storybook/client-logger" "6.3.4" - "@storybook/components" "6.3.4" - "@storybook/core-events" "6.3.4" - "@storybook/router" "6.3.4" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.27" + "@storybook/client-logger" "6.3.6" + core-js "^3.8.2" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.27" + global "^4.4.0" + memoizerific "^1.11.3" + polished "^4.0.5" + resolve-from "^5.0.0" + ts-dedent "^2.0.0" + +"@storybook/ui@6.3.6": + version "6.3.6" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.3.6.tgz#a9ed8265e34bb8ef9f0dd08f40170b3dcf8a8931" + integrity sha512-S9FjISUiAmbBR7d6ubUEcELQdffDfRxerloxkXs5Ou7n8fEPqpgQB01Hw5MLRUwTEpxPzHn+xtIGYritAGxt/Q== + dependencies: + "@emotion/core" "^10.1.1" + "@storybook/addons" "6.3.6" + "@storybook/api" "6.3.6" + "@storybook/channels" "6.3.6" + "@storybook/client-logger" "6.3.6" + "@storybook/components" "6.3.6" + "@storybook/core-events" "6.3.6" + "@storybook/router" "6.3.6" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.3.4" + "@storybook/theming" "6.3.6" "@types/markdown-to-jsx" "^6.11.3" copy-to-clipboard "^3.3.1" core-js "^3.8.2" @@ -3237,6 +3384,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash@^4.14.165": + version "4.14.172" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a" + integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw== + "@types/markdown-to-jsx@^6.11.3": version "6.11.3" resolved "https://registry.yarnpkg.com/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz#cdd1619308fecbc8be7e6a26f3751260249b020e" @@ -5316,6 +5468,14 @@ color@^3.1.3: color-convert "^1.9.3" color-string "^1.6.0" +color@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67" + integrity sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA== + dependencies: + color-convert "^2.0.1" + color-string "^1.6.0" + colorette@^1.2.1, colorette@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" @@ -6312,6 +6472,11 @@ deep-object-diff@^1.1.0: resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -7775,6 +7940,19 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +formik@^2.2.9: + version "2.2.9" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0" + integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA== + dependencies: + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^1.10.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -8206,6 +8384,11 @@ gzip-size@5.1.1: duplexer "^0.1.1" pify "^4.0.1" +hamt_plus@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601" + integrity sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE= + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -8401,7 +8584,7 @@ highlight.js@^10.1.1, highlight.js@~10.7.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== -history@^4.7.2: +history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== @@ -8422,12 +8605,12 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: +hoist-non-react-statics@^2.3.1: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -10262,6 +10445,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.15, lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -10658,6 +10846,14 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +mini-create-react-context@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e" + integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== + dependencies: + "@babel/runtime" "^7.12.1" + tiny-warning "^1.0.3" + mini-css-extract-plugin@0.11.3: version "0.11.3" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz#15b0910a7f32e62ffde4a7430cfefbd700724ea6" @@ -10822,6 +11018,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== +nanoclone@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" + integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== + nanoid@^3.1.23: version "3.1.23" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" @@ -12590,6 +12791,11 @@ prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, object-assign "^4.1.1" react-is "^16.8.1" +property-expr@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910" + integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg== + property-information@^5.0.0, property-information@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" @@ -12842,11 +13048,6 @@ react-app-polyfill@^2.0.0: regenerator-runtime "^0.13.7" whatwg-fetch "^3.4.1" -react-axios@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/react-axios/-/react-axios-2.0.5.tgz#ad24cfc9927a03f607b530a02b86af3da62b997a" - integrity sha512-NGbOeflZm3Ry3ObiWcK7/ynBFmX0ThjnonTg0KA1AmAYgybERrF0inL5eNJdtyHjmbLXd2y27tf4r0FyHez+QA== - react-calendar@^3.0.0: version "3.4.0" resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-3.4.0.tgz#25396ca0992b3b2042abca334515f6ff4d9878bc" @@ -12958,6 +13159,11 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" @@ -13005,7 +13211,7 @@ react-inspector@^5.1.0: is-dom "^1.0.0" prop-types "^15.0.0" -react-is@^16.6.3, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: +react-is@^16.6.0, react-is@^16.6.3, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -13055,30 +13261,34 @@ react-refresh@^0.8.3: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== -react-router-dom@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" - integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== +react-router-dom@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" + integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== dependencies: - history "^4.7.2" - invariant "^2.2.4" + "@babel/runtime" "^7.1.2" + history "^4.9.0" loose-envify "^1.3.1" - prop-types "^15.6.1" - react-router "^4.3.1" - warning "^4.0.1" + prop-types "^15.6.2" + react-router "5.2.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" -react-router@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" - integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== +react-router@5.2.0, react-router@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" + integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== dependencies: - history "^4.7.2" - hoist-non-react-statics "^2.5.0" - invariant "^2.2.4" + "@babel/runtime" "^7.1.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" loose-envify "^1.3.1" + mini-create-react-context "^0.4.0" path-to-regexp "^1.7.0" - prop-types "^15.6.1" - warning "^4.0.1" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" react-scripts@^4.0.3: version "4.0.3" @@ -13328,6 +13538,13 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +recoil@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.4.0.tgz#2a933ba7c043cbf6f50f52da0828a879c7cd7d69" + integrity sha512-FZ2ljI4ldZU820V0APbKOtS4bPwPJHvpDBQEl+Cf47DMaM35wuLXl2u37E0TSgdvKAZBOUsIwcBnzE+ncODRxQ== + dependencies: + hamt_plus "1.0.2" + "recompose@0.27.1 - 0.30.0": version "0.30.0" resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0" @@ -14425,12 +14642,12 @@ storybook-addon-outline@^1.4.1: "@storybook/core-events" "^6.3.0" ts-dedent "^2.1.1" -storybook@^6.3.4: - version "6.3.4" - resolved "https://registry.yarnpkg.com/storybook/-/storybook-6.3.4.tgz#4787337d29fac414b658b2637d4bd210c62e2a9d" - integrity sha512-YiI6c+rntLR/GmHRhHERVlTKJF7spJGw4v5Us/+hViIihBX9EEq0ls5a0c8OadglBP+QpBmphG9UVso7CsH7QA== +storybook@^6.3.6: + version "6.3.6" + resolved "https://registry.yarnpkg.com/storybook/-/storybook-6.3.6.tgz#d48e2912aa7ae85dc0062aaf387c5130ce91a834" + integrity sha512-2y4HJu735S0ED1u1w0je5KVdTZDI6pmjxhsEErziz5uoDGItqzgc0wVVpFEpEuP8eQbKKfqG0G6UuqQeKip/Bg== dependencies: - "@storybook/cli" "6.3.4" + "@storybook/cli" "6.3.6" stream-browserify@^2.0.1: version "2.0.2" @@ -14992,7 +15209,7 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== -tiny-warning@^1.0.0: +tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -15071,6 +15288,11 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + touch@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" @@ -15139,7 +15361,7 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.8.1: +tslib@^1.10.0, tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -15149,6 +15371,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + tsutils@^3.17.1, tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -15697,7 +15924,7 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" -warning@^4.0.1, warning@^4.0.2, warning@^4.0.3: +warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== @@ -16351,6 +16578,19 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yup@^0.32.9: + version "0.32.9" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.9.tgz#9367bec6b1b0e39211ecbca598702e106019d872" + integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg== + dependencies: + "@babel/runtime" "^7.10.5" + "@types/lodash" "^4.14.165" + lodash "^4.17.20" + lodash-es "^4.17.15" + nanoclone "^0.2.1" + property-expr "^2.0.4" + toposort "^2.0.2" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From c299e6eb7f8517f38127841c146cebeb669198ec Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Sat, 18 Sep 2021 22:07:36 +1000 Subject: [PATCH 08/17] New filtering and tree --- .gitignore | 1 + ui/src/.storybook/main.js | 9 +- ui/src/package.json | 34 +- ui/src/src/App.css | 13 +- ui/src/src/common/Main.js | 11 +- ui/src/src/components/suite/Dialog.js | 11 - ui/src/src/components/suite/Dialog.tsx | 26 + .../components/suite/{Form.js => Form.tsx} | 23 +- ui/src/src/components/testcase/List.tsx | 6 + .../src/components/testcase/TestCaseGroup.tsx | 89 + .../components/testcase/TestCaseListItem.tsx | 55 + ui/src/src/components/ui/Button.js | 16 - ui/src/src/components/ui/Button.tsx | 37 + ui/src/src/components/ui/Checkbox.tsx | 36 + .../components/ui/{Dialog.js => Dialog.tsx} | 36 +- ui/src/src/components/ui/FieldError.js | 8 - ui/src/src/components/ui/FieldError.tsx | 11 + .../components/ui/{Filter.js => Filter.tsx} | 27 +- .../components/ui/{Filters.js => Filters.tsx} | 17 +- ui/src/src/components/ui/ListBox.js | 68 - ui/src/src/components/ui/ListBox.tsx | 92 + ui/src/src/components/ui/SelectedValues.tsx | 4 +- ui/src/src/components/ui/TextInput.js | 8 - ui/src/src/components/ui/TextInput.tsx | 79 + ui/src/src/components/ui/focus.js | 2 - ui/src/src/components/ui/focus.ts | 6 + .../src/components/ui/{index.js => index.ts} | 2 +- .../src/components/ui/{input.js => input.ts} | 4 +- ui/src/src/components/ui/typography.ts | 1 + ui/src/src/domain/index.ts | 98 +- ui/src/src/index.css | 6 + ui/src/src/launches/LaunchesWidget.js | 8 +- ui/src/src/lib/hooks.ts | 42 + ui/src/src/lib/listToLookup.ts | 11 + ui/src/src/services/backend.ts | 193 +- .../components/suite/SuiteHeader.stories.js | 13 + .../components/testcase/List.stories.tsx | 134 ++ .../{Button.stories.js => Button.stories.tsx} | 8 +- .../components/ui/Checkbox.stories.tsx | 14 + .../{Dialog.stories.js => Dialog.stories.tsx} | 6 +- .../{Filter.stories.js => Filter.stories.tsx} | 2 +- .../stories/components/ui/Filters.stories.js | 17 - .../stories/components/ui/Filters.stories.tsx | 19 + ...Listbox.stories.js => Listbox.stories.tsx} | 2 +- ....stories.js => SelectedValues.stories.tsx} | 11 +- ui/src/src/testcases/SuiteHeader.js | 39 + ui/src/src/testcases/TestCaseList.tsx | 42 + ui/src/src/testcases/TestCaseTree.tsx | 90 + ui/src/src/testcases/TestCases.js | 2 +- ui/src/src/testcases/TestCasesFilters.tsx | 74 + ui/src/src/testcases/TestCasesPanel.tsx | 36 + ui/src/src/testcases/TestCasesScreen.tsx | 102 + ui/src/src/testcases/hooks.ts | 70 + ui/src/src/testcases/testCasesScreen.data.ts | 79 + ui/src/src/testsuites/TestSuitesWidget.js | 6 +- ui/src/tailwind.config.js | 2 +- ui/src/yarn.lock | 2037 +++++++++++++---- 57 files changed, 3195 insertions(+), 700 deletions(-) delete mode 100644 ui/src/src/components/suite/Dialog.js create mode 100644 ui/src/src/components/suite/Dialog.tsx rename ui/src/src/components/suite/{Form.js => Form.tsx} (55%) create mode 100644 ui/src/src/components/testcase/List.tsx create mode 100644 ui/src/src/components/testcase/TestCaseGroup.tsx create mode 100644 ui/src/src/components/testcase/TestCaseListItem.tsx delete mode 100644 ui/src/src/components/ui/Button.js create mode 100644 ui/src/src/components/ui/Button.tsx create mode 100644 ui/src/src/components/ui/Checkbox.tsx rename ui/src/src/components/ui/{Dialog.js => Dialog.tsx} (61%) delete mode 100644 ui/src/src/components/ui/FieldError.js create mode 100644 ui/src/src/components/ui/FieldError.tsx rename ui/src/src/components/ui/{Filter.js => Filter.tsx} (78%) rename ui/src/src/components/ui/{Filters.js => Filters.tsx} (59%) delete mode 100644 ui/src/src/components/ui/ListBox.js create mode 100644 ui/src/src/components/ui/ListBox.tsx delete mode 100644 ui/src/src/components/ui/TextInput.js create mode 100644 ui/src/src/components/ui/TextInput.tsx delete mode 100644 ui/src/src/components/ui/focus.js create mode 100644 ui/src/src/components/ui/focus.ts rename ui/src/src/components/ui/{index.js => index.ts} (50%) rename ui/src/src/components/ui/{input.js => input.ts} (76%) create mode 100644 ui/src/src/components/ui/typography.ts create mode 100644 ui/src/src/lib/hooks.ts create mode 100644 ui/src/src/lib/listToLookup.ts create mode 100644 ui/src/src/stories/components/suite/SuiteHeader.stories.js create mode 100644 ui/src/src/stories/components/testcase/List.stories.tsx rename ui/src/src/stories/components/ui/{Button.stories.js => Button.stories.tsx} (57%) create mode 100644 ui/src/src/stories/components/ui/Checkbox.stories.tsx rename ui/src/src/stories/components/ui/{Dialog.stories.js => Dialog.stories.tsx} (97%) rename ui/src/src/stories/components/ui/{Filter.stories.js => Filter.stories.tsx} (90%) delete mode 100644 ui/src/src/stories/components/ui/Filters.stories.js create mode 100644 ui/src/src/stories/components/ui/Filters.stories.tsx rename ui/src/src/stories/components/ui/{Listbox.stories.js => Listbox.stories.tsx} (93%) rename ui/src/src/stories/components/ui/{SelectedValues.stories.js => SelectedValues.stories.tsx} (65%) create mode 100644 ui/src/src/testcases/SuiteHeader.js create mode 100644 ui/src/src/testcases/TestCaseList.tsx create mode 100644 ui/src/src/testcases/TestCaseTree.tsx create mode 100644 ui/src/src/testcases/TestCasesFilters.tsx create mode 100644 ui/src/src/testcases/TestCasesPanel.tsx create mode 100644 ui/src/src/testcases/TestCasesScreen.tsx create mode 100644 ui/src/src/testcases/hooks.ts create mode 100644 ui/src/src/testcases/testCasesScreen.data.ts diff --git a/.gitignore b/.gitignore index 36e70a8d..16e7b45b 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ fabric.properties .vscode +ui/src/yarn-error.log diff --git a/ui/src/.storybook/main.js b/ui/src/.storybook/main.js index 3296a74a..dee3b3ac 100644 --- a/ui/src/.storybook/main.js +++ b/ui/src/.storybook/main.js @@ -4,7 +4,14 @@ module.exports = { "../src/**/*.stories.@(js|jsx|ts|tsx)" ], "addons": [ - "@storybook/addon-postcss", + { + name: '@storybook/addon-postcss', + options: { + postcssLoaderOptions: { + implementation: require('postcss'), + }, + }, + }, "@storybook/addon-links", "@storybook/addon-essentials", ] diff --git a/ui/src/package.json b/ui/src/package.json index 27f8098c..8f9bdbb4 100644 --- a/ui/src/package.json +++ b/ui/src/package.json @@ -6,8 +6,8 @@ "@fortawesome/fontawesome-svg-core": "^1.2.2", "@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/react-fontawesome": "^0.1.2", - "@headlessui/react": "^1.4.0", - "@heroicons/react": "^1.0.3", + "@headlessui/react": "^1.4.1", + "@heroicons/react": "^1.0.4", "@tinymce/tinymce-react": "^3.12.6", "bootstrap": "^4.1.2", "bootstrap-fileinput": "^5.2.2", @@ -34,7 +34,7 @@ "react-scripts": "^4.0.3", "react-select": "^2.0.0", "react-spinners": "^0.5.4", - "recoil": "^0.4.0", + "recoil": "^0.4.1", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^0.88.1", "tslib": "^2.3.1", @@ -42,7 +42,8 @@ }, "scripts": { "start": "craco start", - "start:dev": "BASE_API_URL=http://localhost:81/api/ yarn start", + "start:dev": "REACT_APP_BASE_API_URL=http://localhost:81/api/ yarn start", + "start:server": "(cd ../../ && docker-compose -f docker-compose-api.yml up)", "build": "craco build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", @@ -99,24 +100,25 @@ "trailingComma": "all", "arrowParens": "avoid", "semi": true, - "jsxBracketSameLine": false, "endOfLine": "lf" }, "devDependencies": { - "@craco/craco": "^6.2.0", - "@storybook/addon-actions": "^6.3.6", - "@storybook/addon-essentials": "^6.3.6", - "@storybook/addon-links": "^6.3.6", + "@craco/craco": "^6.3.0", + "@storybook/addon-actions": "^6.3.8", + "@storybook/addon-essentials": "^6.3.8", + "@storybook/addon-links": "^6.3.8", "@storybook/addon-postcss": "^2.0.0", - "@storybook/node-logger": "^6.3.6", - "@storybook/react": "^6.3.6", + "@storybook/builder-webpack5": "^6.3.8", + "@storybook/manager-webpack5": "^6.3.8", + "@storybook/node-logger": "^6.3.8", + "@storybook/react": "^6.3.8", + "@types/react-router": "^5.1.16", "autoprefixer": "^9", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-prettier": "^3.4.0", - "postcss": "^7", - "prettier": "^2.3.2", - "storybook": "^6.3.6", + "eslint-plugin-prettier": "^4.0.0", + "prettier": "^2.4.0", + "storybook": "^6.3.8", "tailwindcss": "npm:@tailwindcss/postcss7-compat", - "typescript": "^4.3.3" + "typescript": "^4.4.3" } } diff --git a/ui/src/src/App.css b/ui/src/src/App.css index 09cffb39..cf640054 100644 --- a/ui/src/src/App.css +++ b/ui/src/src/App.css @@ -35,11 +35,23 @@ } .tree-side{ +<<<<<<< HEAD height: 100vh; overflow-y: scroll; +======= + height: 100%; + overflow-y: auto; +>>>>>>> 24554e4 (New filtering and tree) z-index: 0; } +.tailwind .list-group-item { + background: transparent; +} +.tailwind .list-group-item.active { + background: #2E5BFF; +} + .testcase-side{ height: 100vh; overflow-y: scroll; @@ -123,7 +135,6 @@ code { .bg-green { background-color: #135f38!important; - #background-color: #13515f!important; } .edit-icon-visible { diff --git a/ui/src/src/common/Main.js b/ui/src/src/common/Main.js index 703623d1..62f8a1f1 100644 --- a/ui/src/src/common/Main.js +++ b/ui/src/src/common/Main.js @@ -1,6 +1,6 @@ import React, { Component } from "react"; import { Switch, Route } from "react-router-dom"; -import TestCases from "../testcases/TestCases"; +import TestCasesScreen from "../testcases/TestCasesScreen"; import TestSuites from "../testsuites/TestSuites"; import Projects from "../projects/Projects"; import ProjectForm from "../projects/ProjectForm"; @@ -24,6 +24,7 @@ import Events from "../audit/Events"; import Redirect from "../common/Redirect"; import { Suspense } from "react"; import Loading from "../components/ui/Loading"; +import { RecoilRoot } from "recoil"; class Main extends Component { onProjectChange(project) { @@ -111,7 +112,13 @@ class Main extends Component { } + render={props => ( + + }> + + + + )} /> ( - - -
-
-); - -export default SuiteDialog; diff --git a/ui/src/src/components/suite/Dialog.tsx b/ui/src/src/components/suite/Dialog.tsx new file mode 100644 index 00000000..0f269c51 --- /dev/null +++ b/ui/src/src/components/suite/Dialog.tsx @@ -0,0 +1,26 @@ +import { useRef } from "react"; +import Dialog from "../ui/Dialog"; +import Form, { FormValues } from "./Form"; + +export type SuiteDialogProps = { + initialValues: FormValues; + onCancel: VoidFunction; + onSubmit: (values: FormValues) => void; +}; +const SuiteDialog = ({ initialValues, onCancel, onSubmit, ...other }: SuiteDialogProps) => { + const firstElementRef = useRef(null); + return ( + + + + + ); +}; + +export default SuiteDialog; diff --git a/ui/src/src/components/suite/Form.js b/ui/src/src/components/suite/Form.tsx similarity index 55% rename from ui/src/src/components/suite/Form.js rename to ui/src/src/components/suite/Form.tsx index 8bc3cc8a..605c92d6 100644 --- a/ui/src/src/components/suite/Form.js +++ b/ui/src/src/components/suite/Form.tsx @@ -3,32 +3,43 @@ import { OKCancelFooter } from "../ui/Dialog"; import TextInput from "../ui/TextInput"; import * as Yup from "yup"; import FieldError from "../ui/FieldError"; - +import clsx from "clsx"; +import { captionClasses } from "../ui/typography"; +import { FormHTMLAttributes, Ref } from "react"; const schema = Yup.object().shape({ name: Yup.string().required("Please give this suite a name"), }); -const Form = ({ initialValues, onSubmit, onCancel, ...other }) => ( +export type FormValues = { + name: string; +}; +export type FormProps = Omit, "onSubmit"> & { + initialValues: FormValues; + initialFocus: Ref; + onSubmit: (values: FormValues) => void; + onCancel: VoidFunction; +}; +const Form = ({ initialValues, initialFocus, onSubmit, onCancel, ...other }: FormProps) => ( {({ values, errors, handleSubmit, handleChange, handleBlur }) => ( -

Save a new test case

+

Save a new test case

Your new test suite requires a name. Please choose something meaningful and easy to remember.

-
diff --git a/ui/src/src/components/testcase/List.tsx b/ui/src/src/components/testcase/List.tsx new file mode 100644 index 00000000..ace291b2 --- /dev/null +++ b/ui/src/src/components/testcase/List.tsx @@ -0,0 +1,6 @@ +import { PropsWithChildren, FunctionComponent } from "react"; + +export type TestCaseListProps = PropsWithChildren<{}>; +const TestCaseList: FunctionComponent = ({ children }) =>
    {children}
; + +export default TestCaseList; diff --git a/ui/src/src/components/testcase/TestCaseGroup.tsx b/ui/src/src/components/testcase/TestCaseGroup.tsx new file mode 100644 index 00000000..bfed935e --- /dev/null +++ b/ui/src/src/components/testcase/TestCaseGroup.tsx @@ -0,0 +1,89 @@ +import clsx from "clsx"; +import { FunctionComponent, memo } from "react"; +import { GroupState, TestCaseGroup } from "../../domain"; +import Checkbox from "../ui/Checkbox"; +import TestCaseListItem from "./TestCaseListItem"; +import ChevronRightIcon from "@heroicons/react/solid/ChevronRightIcon"; +import { focusClasses } from "../ui/focus"; + +export type TestCaseGroupProps = { + testCaseGroup: TestCaseGroup; + getGroupState: (id: string) => GroupState; + isTestCaseSelected: (id: string) => boolean; + isGroupOpen: (id: string) => boolean; + selectedTestCase: string | undefined; + onTestCaseClick: (id: string) => void; + onTestCaseCheckboxClick: (id: string) => void; + onCheckboxClick: (id: string) => void; + onChevronClick: (id: string) => void; + level: number; + disabled?: boolean; +}; + +const TestCaseGroupComponent: FunctionComponent = memo( + ({ + isGroupOpen, + getGroupState, + isTestCaseSelected, + testCaseGroup, + onTestCaseClick, + onTestCaseCheckboxClick, + onCheckboxClick, + onChevronClick, + selectedTestCase, + level, + disabled, + }) => ( +
+
+ + onCheckboxClick(testCaseGroup.id)} + checked={getGroupState(testCaseGroup.id) === GroupState.Selected} + indeterminate={getGroupState(testCaseGroup.id) === GroupState.Indeterminate} + disabled={disabled} + /> + {testCaseGroup.title} +
+ {isGroupOpen(testCaseGroup.id) && ( + <> + {(testCaseGroup.children ?? []).map(childGroup => ( + + ))} + {testCaseGroup.testCases.map(testCase => ( + + ))} + + )} +
+ ), +); + +export default TestCaseGroupComponent; diff --git a/ui/src/src/components/testcase/TestCaseListItem.tsx b/ui/src/src/components/testcase/TestCaseListItem.tsx new file mode 100644 index 00000000..1312627b --- /dev/null +++ b/ui/src/src/components/testcase/TestCaseListItem.tsx @@ -0,0 +1,55 @@ +import { FunctionComponent, memo, MouseEvent, useRef } from "react"; +import { ExistingTestCase } from "../../domain"; +import clsx from "clsx"; +import Checkbox from "../ui/Checkbox"; + +export type TestCaseListItemProps = { + disabled?: boolean; + testCase: ExistingTestCase; + checked?: boolean; + selected?: boolean; + onClick?: (id: string) => void; + onCheckboxClick?: (id: string) => void; + level: number; +}; + +const TestCaseListItem: FunctionComponent = memo( + ({ testCase, checked, disabled, selected, onClick, onCheckboxClick, level }) => { + const checkboxRef = useRef(null); + const handleClick = (e: MouseEvent) => { + if (checkboxRef.current?.contains(e.target as HTMLElement) || checkboxRef.current === e.target) { + return; + } + onClick?.(testCase.id); + }; + + return ( +
  • + { + onCheckboxClick?.(testCase.id); + }} + className={clsx({ + "border-neutral-fade2": !selected, + "border-white": selected, + })} + iconClassName={clsx({ + "text-neutral-fade1": !selected, + })} + style={{ marginLeft: `${(level + 1) * 28}px` }} + /> +
    {testCase.name}
    +
  • + ); + }, +); + +export default TestCaseListItem; diff --git a/ui/src/src/components/ui/Button.js b/ui/src/src/components/ui/Button.js deleted file mode 100644 index fd9aa32c..00000000 --- a/ui/src/src/components/ui/Button.js +++ /dev/null @@ -1,16 +0,0 @@ -import clsx from "clsx"; -export const baseClasses = "duration-200 transition-colors font-semibold "; -const Button = ({ className, ...other }) => + ), +); + +export default Checkbox; diff --git a/ui/src/src/components/ui/Dialog.js b/ui/src/src/components/ui/Dialog.tsx similarity index 61% rename from ui/src/src/components/ui/Dialog.js rename to ui/src/src/components/ui/Dialog.tsx index d087001d..37d9c056 100644 --- a/ui/src/src/components/ui/Dialog.js +++ b/ui/src/src/components/ui/Dialog.tsx @@ -2,14 +2,23 @@ import { Dialog as HeadlessDialog } from "@headlessui/react"; import XCircleIcon from "@heroicons/react/outline/XCircleIcon"; import clsx from "clsx"; +import { ButtonHTMLAttributes, EventHandler, MouseEvent, PropsWithChildren } from "react"; import { useLayoutEffect } from "react"; import Button from "./Button"; +import { focusClasses } from "./focus"; -const Dialog = ({ children, className, onOverlayClick, ...other }) => { +export type DialogProps = PropsWithChildren<{ + className?: string; + onOverlayClick: EventHandler; + onClose: (b: boolean) => void; + open: boolean; +}>; + +const Dialog = ({ children, className, onOverlayClick, onClose, ...other }: DialogProps) => { // TODO: Remove this when we get rid of Bootstrap useLayoutEffect(() => { const root = document.getElementById("headlessui-portal-root"); - if (root) { + if (root && !root.className.includes("tailwind")) { root.className = root.className + " tailwind"; } }); @@ -17,6 +26,7 @@ const Dialog = ({ children, className, onOverlayClick, ...other }) => {
    { ); }; -const Close = ({ className, ...other }) => ( +const Close = ({ className, ...other }: ButtonHTMLAttributes) => ( ); +export type OKCancelFooterProps = { + className?: string; + OKText?: string; + OKType?: ButtonHTMLAttributes["type"]; + cancelType?: ButtonHTMLAttributes["type"]; + cancelText?: string; + onOKClick?: ButtonHTMLAttributes["onClick"]; + onCancelClick?: ButtonHTMLAttributes["onClick"]; +}; + export const OKCancelFooter = ({ className, - children, OKText = "OK", OKType = "submit", - cancelType = "cancel", + cancelType = "button", cancelText = "Cancel", onOKClick, onCancelClick, ...other -}) => ( +}: OKCancelFooterProps) => (
    - + {OKText} diff --git a/ui/src/src/components/ui/FieldError.js b/ui/src/src/components/ui/FieldError.js deleted file mode 100644 index dc026de1..00000000 --- a/ui/src/src/components/ui/FieldError.js +++ /dev/null @@ -1,8 +0,0 @@ -const FieldError = ({ children }) => - children ? ( -
    - {children} -
    - ) : null; - -export default FieldError; diff --git a/ui/src/src/components/ui/FieldError.tsx b/ui/src/src/components/ui/FieldError.tsx new file mode 100644 index 00000000..0f6dc20e --- /dev/null +++ b/ui/src/src/components/ui/FieldError.tsx @@ -0,0 +1,11 @@ +import { PropsWithChildren } from "react"; + +export type FieldErrorProps = PropsWithChildren<{}>; +const FieldError = ({ children }: FieldErrorProps) => + children ? ( +
    + {children} +
    + ) : null; + +export default FieldError; diff --git a/ui/src/src/components/ui/Filter.js b/ui/src/src/components/ui/Filter.tsx similarity index 78% rename from ui/src/src/components/ui/Filter.js rename to ui/src/src/components/ui/Filter.tsx index 0bda8205..59606e93 100644 --- a/ui/src/src/components/ui/Filter.js +++ b/ui/src/src/components/ui/Filter.tsx @@ -1,29 +1,40 @@ -import { useMemo } from "react"; +import { EventHandler, MouseEvent, useMemo } from "react"; import SelectedValues from "./SelectedValues"; import { BasicListbox as CustomListbox } from "./ListBox"; import XCircleIcon from "@heroicons/react/solid/XCircleIcon"; import { inputBackgroundClasses, inputBorderClasses, inputTextClasses } from "./input"; import clsx from "clsx"; import { focusClasses } from "./focus"; +import { ExistingAttribute, FakeAttribute } from "../../domain"; -const Filter = ({ value, onChange, attributes, onRemoveClick }) => { +export type FilterValue = { + attribute: string | undefined; + values: string[]; +}; +export type FilterProps = { + value: FilterValue; + onChange: (value: FilterValue) => void; + attributes: (ExistingAttribute | FakeAttribute)[]; + onRemoveClick: EventHandler; +}; +const Filter = ({ value, onChange, attributes, onRemoveClick }: FilterProps) => { const selectedAttribute = useMemo( () => attributes.find(attribute => attribute.id === value?.attribute), [attributes, value], ); const selectedValues = value?.values || []; - const handleAttributeChange = attribute => { + const handleAttributeChange = (attribute: string | undefined) => { onChange({ attribute, values: [] }); }; - const handleValueChange = newValue => { + const handleValueChange = (newValue: string) => { const prev = value?.values ?? []; onChange({ ...value, values: prev.includes(newValue) ? prev.filter(value => value !== newValue) : [...prev, newValue], }); }; - const handleRemoveValueClick = (e, removeValue) => { + const handleRemoveValueClick = (e: MouseEvent, removeValue: string) => { e.stopPropagation(); const prev = value?.values ?? []; onChange({ @@ -48,7 +59,7 @@ const Filter = ({ value, onChange, attributes, onRemoveClick }) => { onChange={handleAttributeChange} label={ value?.attribute ? ( - selectedAttribute.name + selectedAttribute?.name ) : ( Select attribute ) @@ -64,7 +75,7 @@ const Filter = ({ value, onChange, attributes, onRemoveClick }) => { { ) } > - {selectedAttribute?.values.length && + {!!selectedAttribute?.values.length && selectedAttribute?.values.map(attributeValue => ( { - const handleFilterChange = index => newValue => onChange(value.map((filter, i) => (i === index ? newValue : filter))); - const handleRemoveFilterClick = index => () => - onChange(value.map((filter, i) => (i === index ? undefined : filter)).filter(Boolean)); +export type FiltersProps = { + attributes: (ExistingAttribute | FakeAttribute)[]; + value: FilterValue[]; + onChange: (value: FilterValue[]) => void; +}; +const Filters = ({ attributes, value, onChange }: FiltersProps) => { + const handleFilterChange = (index: number) => (newValue: FilterValue) => + onChange(value.map((filter, i) => (i === index ? newValue : filter))); + const handleRemoveFilterClick = (index: number) => () => + onChange(value.map((filter, i) => (i === index ? undefined : filter)!).filter(Boolean)); const handleAddFilterClick = () => onChange([...value, { attribute: undefined, values: [] }]); return (
    diff --git a/ui/src/src/components/ui/ListBox.js b/ui/src/src/components/ui/ListBox.js deleted file mode 100644 index 473aa861..00000000 --- a/ui/src/src/components/ui/ListBox.js +++ /dev/null @@ -1,68 +0,0 @@ -import SelectorIcon from "@heroicons/react/solid/SelectorIcon"; -import { Listbox as HeadlessListBox, Transition } from "@headlessui/react"; -import clsx from "clsx"; -import CheckIcon from "@heroicons/react/solid/CheckIcon"; -import { inputLayoutClasses, inputTextClasses } from "./input"; -import { focusClasses } from "./focus"; - -const Placeholder = ({ children }) => {children}; - -export const BasicListbox = ({ label, value, onChange, children, className, buttonClassName }) => ( - - - {label} - - - - {!!children && ( - - {children} - - )} - - -); - -export const Option = ({ value, children, forceSelected }) => ( - - {({ active, selected }) => ( -
    - - {children} -
    - )} -
    -); - -BasicListbox.Option = Option; -BasicListbox.Placeholder = Placeholder; -const Listbox = ({ className, ...props }) => ( - -); - -Listbox.Option = Option; -Listbox.Placeholder = Placeholder; -export default Listbox; diff --git a/ui/src/src/components/ui/ListBox.tsx b/ui/src/src/components/ui/ListBox.tsx new file mode 100644 index 00000000..48b9dff5 --- /dev/null +++ b/ui/src/src/components/ui/ListBox.tsx @@ -0,0 +1,92 @@ +import SelectorIcon from "@heroicons/react/solid/SelectorIcon"; +import { Listbox as HeadlessListBox, Transition } from "@headlessui/react"; +import clsx from "clsx"; +import CheckIcon from "@heroicons/react/solid/CheckIcon"; +import { inputBorderClasses, inputLayoutClasses, inputTextClasses } from "./input"; +import { focusClasses } from "./focus"; +import { PropsWithChildren, ReactNode } from "react"; + +const Placeholder = ({ children }: PropsWithChildren<{}>) => {children}; + +export type BasicListboxProps = PropsWithChildren<{ + label?: ReactNode; + value: TValue; + onChange: (newValue: TValue) => void; + className?: string; + buttonClassName?: string; +}>; + +export function BasicListbox({ + label, + value, + onChange, + children, + className, + buttonClassName, +}: BasicListboxProps) { + return ( + + + {label} + + + + + {children} + + + + ); +} + +export type OptionProps = PropsWithChildren<{ + value: TValue; + forceSelected?: boolean; +}>; +export function Option({ value, children, forceSelected }: OptionProps) { + return ( + + {({ active, selected }) => ( +
    + + {children} +
    + )} +
    + ); +} + +BasicListbox.Option = Option; +BasicListbox.Placeholder = Placeholder; + +function Listbox({ className, ...props }: BasicListboxProps) { + return ( + + ); +} + +Listbox.Option = Option; +Listbox.Placeholder = Placeholder; +export default Listbox; diff --git a/ui/src/src/components/ui/SelectedValues.tsx b/ui/src/src/components/ui/SelectedValues.tsx index 636a1f91..ecc2bab2 100644 --- a/ui/src/src/components/ui/SelectedValues.tsx +++ b/ui/src/src/components/ui/SelectedValues.tsx @@ -1,5 +1,5 @@ import XCircleIcon from "@heroicons/react/solid/XCircleIcon"; -import { EventHandler, MouseEvent, ReactNode } from "react"; +import { MouseEvent, ReactNode } from "react"; export interface Value { id: string; @@ -23,7 +23,7 @@ const SelectedValue = ({ value, label, onRemoveClick }: SelectedValueProps) => ( export interface SelectedValuesProps { values: string[]; allValues: TValue[]; - onRemoveClick: EventHandler; + onRemoveClick: (e: MouseEvent, value: string) => void; } const SelectedValues = ({ values, allValues, onRemoveClick }: SelectedValuesProps) => (
    diff --git a/ui/src/src/components/ui/TextInput.js b/ui/src/src/components/ui/TextInput.js deleted file mode 100644 index d1f3968e..00000000 --- a/ui/src/src/components/ui/TextInput.js +++ /dev/null @@ -1,8 +0,0 @@ -import clsx from "clsx"; - -import { inputClasses } from "./input"; -const TextInput = ({ className, type = "text", ...other }) => ( - -); - -export default TextInput; diff --git a/ui/src/src/components/ui/TextInput.tsx b/ui/src/src/components/ui/TextInput.tsx new file mode 100644 index 00000000..65b901d8 --- /dev/null +++ b/ui/src/src/components/ui/TextInput.tsx @@ -0,0 +1,79 @@ +import clsx from "clsx"; +import { forwardRef, HTMLAttributes, InputHTMLAttributes, KeyboardEvent, MouseEvent, useEffect, useRef } from "react"; +import { iconClasses } from "./Button"; +import { focusClasses, focusWithinClasses, suppressFocusClasses } from "./focus"; +import CheckIcon from "@heroicons/react/solid/CheckIcon"; +import XIcon from "@heroicons/react/solid/XIcon"; + +import { inputBorderClasses, inputClasses, inputLayoutClasses, paddingClasses } from "./input"; + +const TextInput = forwardRef>( + ({ className, type = "text", ...other }: InputHTMLAttributes, ref) => ( + + ), +); + +export type OKCancelTextInputProps = HTMLAttributes & { + value: InputHTMLAttributes["value"]; + onChange: (value: string) => void; + onOKClick: (e: MouseEvent | KeyboardEvent) => void; + onCancelClick: (e: MouseEvent | KeyboardEvent) => void; +}; + +export const OKCancelTextInput = ({ + value, + onChange, + onOKClick, + onCancelClick, + className, + ...other +}: OKCancelTextInputProps) => { + const handleKeyPress = (e: KeyboardEvent) => { + if (e.key === "Enter") { + onOKClick?.(e); + } + }; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onCancelClick?.(e); + } + }; + const inputRef = useRef(null); + useEffect(() => { + inputRef.current?.focus(); + }, []); + return ( +
    + onChange(e.target.value)} + onKeyPress={handleKeyPress} + onKeyDown={handleKeyDown} + ref={inputRef} + /> + + +
    + ); +}; + +export default TextInput; diff --git a/ui/src/src/components/ui/focus.js b/ui/src/src/components/ui/focus.js deleted file mode 100644 index afd4e19d..00000000 --- a/ui/src/src/components/ui/focus.js +++ /dev/null @@ -1,2 +0,0 @@ -export const focusClasses = - "focus:transition-all focus:outline-none duration-100 focus:ring-offset-2 focus:ring focus:ring-neutral-fade5"; diff --git a/ui/src/src/components/ui/focus.ts b/ui/src/src/components/ui/focus.ts new file mode 100644 index 00000000..6ffa1984 --- /dev/null +++ b/ui/src/src/components/ui/focus.ts @@ -0,0 +1,6 @@ +export const focusClasses = + "ouline-none focus:transition-all focus:outline-none duration-100 focus:ring-offset-2 focus:ring focus:ring-neutral-fade5"; + +export const focusWithinClasses = + "outline-none focus-within:transition-all focus-within:outline-none duration-100 focus-within:ring-offset-2 focus-within:ring focus-within:ring-neutral-fade5"; +export const suppressFocusClasses = "focus:outline-none"; diff --git a/ui/src/src/components/ui/index.js b/ui/src/src/components/ui/index.ts similarity index 50% rename from ui/src/src/components/ui/index.js rename to ui/src/src/components/ui/index.ts index a1fa576d..161ee637 100644 --- a/ui/src/src/components/ui/index.js +++ b/ui/src/src/components/ui/index.ts @@ -1,2 +1,2 @@ -export { default as Listbox } from "./Listbox"; +export { default as Listbox } from "./ListBox"; export { default as Loading } from "./Loading"; diff --git a/ui/src/src/components/ui/input.js b/ui/src/src/components/ui/input.ts similarity index 76% rename from ui/src/src/components/ui/input.js rename to ui/src/src/components/ui/input.ts index f69482f7..2c7970ad 100644 --- a/ui/src/src/components/ui/input.js +++ b/ui/src/src/components/ui/input.ts @@ -1,9 +1,10 @@ import { focusClasses } from "./focus"; export const inputTextClasses = "text-neutral text-left text-base"; -export const inputLayoutClasses = "min-h-10 z-1"; +export const inputLayoutClasses = "box-border min-h-10 z-1"; export const inputBackgroundClasses = "bg-neutral-fade5 bg-opacity-20"; export const inputBorderClasses = "border border-neutral-fade5 rounded-md"; +export const paddingClasses = "pl-4 pr-4"; export const inputClasses = [ focusClasses, @@ -11,4 +12,5 @@ export const inputClasses = [ inputLayoutClasses, inputBackgroundClasses, inputBorderClasses, + paddingClasses, ]; diff --git a/ui/src/src/components/ui/typography.ts b/ui/src/src/components/ui/typography.ts new file mode 100644 index 00000000..bd1075a9 --- /dev/null +++ b/ui/src/src/components/ui/typography.ts @@ -0,0 +1 @@ +export const captionClasses = "uppercase text-sm font-medium text-neutral-fade2"; diff --git a/ui/src/src/domain/index.ts b/ui/src/src/domain/index.ts index bad2a6bd..6d7439be 100644 --- a/ui/src/src/domain/index.ts +++ b/ui/src/src/domain/index.ts @@ -2,17 +2,105 @@ export interface WithID { id: string; } +export type DeletedMeta = { + deleted: boolean; +}; + +export type TimeMeta = { + createdTime: number; + lastModifiedTime: number; +}; + +export type Meta = TimeMeta & + DeletedMeta & { + createdBy: string; + lastModifiedBy: string; + }; + export interface AttributeValue extends WithID { name: string; } -export interface Attribute extends WithID { - name: string; +export type ExistingAttribute = WithID & + Meta & { + name: string; + values: AttributeValue[]; + }; + +export type FakeAttribute = WithID & { + name: "Broken"; values: AttributeValue[]; +}; +export interface AttributeFilter { + attribute: string; + values: string[]; +} +export type AttributeFilterDraft = { + attribute: string | undefined; + values: string[]; +}; +export type ExistingAttributeFilter = AttributeFilter & DeletedMeta & TimeMeta; +export interface Suite { + name: string; + filters: ExistingAttributeFilter[]; + excludedTestCases: string[]; } -export interface TestCaseFilter {} -export interface Suite extends WithID { +export type SuiteDraft = { + name: string; + filters: AttributeFilterDraft[]; + excludedTestCases: string[]; +}; +export type ExistingSuite = WithID & Meta & Suite; + +export interface TestCase { name: string; - filter: TestCaseFilter; +} + +export type ExistingTestCase = WithID & TestCase; +export type TestCaseGroup = { + id: string; + count: number; + isLeaf: boolean; + testCases: ExistingTestCase[]; + children: TestCaseGroup[] | undefined; + title: string; +}; +export type RootTestCaseGroup = Omit & { children: TestCaseGroup[] }; + +export const flattenGroup = (group: TestCaseGroup): TestCaseGroup[] => [ + group, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ...(group.children ?? []).reduce((arr, child) => [...arr, ...flattenGroup(child)], []), +]; +export const flattenGroups = (groups: TestCaseGroup[]): TestCaseGroup[] => + groups.reduce((arr, group) => [...arr, ...flattenGroup(group)], []); +export const getAllTestCases = (groups: TestCaseGroup[]): ExistingTestCase[] => + flattenGroups(groups).reduce((cases, group) => [...cases, ...group.testCases], []); + +export const listToBoolHash = (list: string[], value: boolean = true) => { + const hash: Record = {}; + for (const id of list) { + hash[id] = value; + } + return hash; +}; +export const getFullySelectedTestCaseHash = (testCases: ExistingTestCase[], value: boolean = true) => + listToBoolHash(testCases.map(testCase => testCase.id)); +export const getFullySetHash = (groups: TestCaseGroup[], value: boolean = true) => + listToBoolHash( + flattenGroups(groups).map(group => group.id), + value, + ); + +export const getGroupLookup = (groups: TestCaseGroup[]) => + groups.reduce>((lookup, group) => ({ ...lookup, [group.id]: group }), {}); + +export enum GroupState { + // eslint-disable-next-line no-unused-vars + Selected = "Selected", + // eslint-disable-next-line no-unused-vars + Indeterminate = "Indeterminate", + // eslint-disable-next-line no-unused-vars + Unselected = "Unselected", } diff --git a/ui/src/src/index.css b/ui/src/src/index.css index 2a53bbf1..4b507db3 100644 --- a/ui/src/src/index.css +++ b/ui/src/src/index.css @@ -17,3 +17,9 @@ html { font-size: 15px; } +body .tailwind .pl-5 { + padding: 1.25rem !important; +} +body .tailwind .p-5 { + padding: 1.25rem !important; +} \ No newline at end of file diff --git a/ui/src/src/launches/LaunchesWidget.js b/ui/src/src/launches/LaunchesWidget.js index 52e1dfa7..ada31e80 100644 --- a/ui/src/src/launches/LaunchesWidget.js +++ b/ui/src/src/launches/LaunchesWidget.js @@ -54,22 +54,22 @@ class LaunchesWidget extends SubComponent { return (
    diff --git a/ui/src/src/lib/hooks.ts b/ui/src/src/lib/hooks.ts new file mode 100644 index 00000000..1ba1768c --- /dev/null +++ b/ui/src/src/lib/hooks.ts @@ -0,0 +1,42 @@ +import { useCallback, useMemo } from "react"; +import { useHistory, useLocation } from "react-router"; +import qs, { IParseOptions } from "qs"; + +const useParsedQuery = (options?: Omit) => { + const location = useLocation(); + const queryString = location.search; + return useMemo(() => qs.parse(queryString, { ignoreQueryPrefix: true, ...options }), [options, queryString]); +}; + +export const useQueryStringState = (name: string, initialValue?: string) => { + const query = useParsedQuery(); + const history = useHistory(); + const set = useCallback( + value => { + history.push( + qs.stringify( + { + ...query, + [name]: value, + }, + { addQueryPrefix: true }, + ), + ); + }, + [history, name, query], + ); + const value = (query[name] as string) || initialValue; + return [value, set] as [typeof value, typeof set]; +}; + +export const useJSONQueryStringState = (name: string, initialValue: TValue) => { + const [value, setValue] = useQueryStringState(name); + const parsedFilters = useMemo(() => (value ? (JSON.parse(value) as TValue) : initialValue), [initialValue, value]); + const updateFilters = useCallback( + (filters: TValue) => { + setValue(JSON.stringify(filters)); + }, + [setValue], + ); + return [parsedFilters, updateFilters] as [typeof parsedFilters, typeof updateFilters]; +}; diff --git a/ui/src/src/lib/listToLookup.ts b/ui/src/src/lib/listToLookup.ts new file mode 100644 index 00000000..3f567517 --- /dev/null +++ b/ui/src/src/lib/listToLookup.ts @@ -0,0 +1,11 @@ +import { WithID } from "../domain"; + +const listToLookup = (list: T[]) => { + const res: Record = {}; + for (const item of list) { + res[item.id] = item; + } + return res; +}; + +export default listToLookup; diff --git a/ui/src/src/services/backend.ts b/ui/src/src/services/backend.ts index c9e7b995..e564c259 100644 --- a/ui/src/src/services/backend.ts +++ b/ui/src/src/services/backend.ts @@ -1,6 +1,20 @@ import qs from "qs"; import * as Utils from "../common/Utils"; -import { Attribute, Suite } from "../domain"; +import { + ExistingAttribute, + ExistingSuite, + Meta, + SuiteDraft, + WithID, + FakeAttribute, + ExistingAttributeFilter, + TimeMeta, + DeletedMeta, + AttributeFilterDraft, + ExistingTestCase, + RootTestCaseGroup, +} from "../domain"; + export const getApiBaseUrl = (url: string) => (process.env.REACT_APP_BASE_API_URL || "/api/") + url; const getOptions = (options?: RequestInit) => ({ ...options, @@ -41,36 +55,179 @@ const backend = { }; export interface TestCasesFilter {} -const mapServerAttributesToClient = (response: Attribute[]) => { - const attributes = response.sort((a, b) => (a.name || "").localeCompare(b.name)); - attributes.unshift({ - id: "broken", - name: "Broken", - values: [ - { id: "true", name: "True" }, - { id: "false", name: "False" }, - ], - }); - return attributes; +type ServerAttribute = Meta & + WithID & { + name: string; + values: string[]; + attrValues: { uuid: string; value: string }[]; + }; +const mapServerAttributeToClient = (attribute: ServerAttribute) => ({ + ...copyMeta(attribute), + id: attribute.id, + name: attribute.name, + values: attribute.attrValues + ? attribute.attrValues.map(({ uuid, value }) => ({ id: uuid, name: value })) + : attribute.values.map(value => ({ id: value.toLowerCase(), name: value })), +}); + +const mapServerAttributesToClient = (response: ServerAttribute[]): (ExistingAttribute | FakeAttribute)[] => { + const clientAttributes = response.map(mapServerAttributeToClient); + const attributes = clientAttributes.sort((a, b) => (a.name || "").localeCompare(b.name)); + return [ + { + id: "broken", + name: "Broken", + values: [ + { id: "true", name: "True" }, + { id: "false", name: "False" }, + ], + }, + ...attributes, + ]; }; +const mapFiltersToQueryParams = (filters: AttributeFilterDraft[]): string => + filters + .reduce<{ key: string; value: string }[]>( + (acc, filter) => [ + ...acc, + ...filter.values.reduce<{ key: string; value: string }[]>( + (acc, value) => [...acc, { key: `attributes.${filter.attribute}`, value }], + [], + ), + ], + [], + ) + .map(param => `${param.key}=${encodeURIComponent(param.value)}`) + .join("&"); + +const mapGroupsToQueryParams = (groups: string[]) => groups.map(group => `groups=${group}`).join("&"); + export const backendService = { project: (projectId: string) => ({ testSuites: { single: (testSuiteId: string) => ({ - get: () => backend.get(projectId + "/testsuite/" + testSuiteId), + get: () => + backend.get(projectId + "/testsuite/" + testSuiteId).then(mapServerSuiteToClient), + update: async (suite: ExistingSuite) => + mapServerSuiteToClient( + await backend.put(projectId + "/testsuite", mapClientSuiteToBackend(suite)), + ), }), - create: (suite: Suite) => backend.post(projectId + "/testsuite/", suite), + create: async (suite: SuiteDraft) => + mapServerSuiteToClient(await backend.post(projectId + "/testsuite/", mapNewClientSuiteToBackend(suite))), }, attributes: { - list: () => backend.get(projectId + "/attribute").then(mapServerAttributesToClient), + list: () => backend.get(projectId + "/attribute").then(mapServerAttributesToClient), }, - testcases: { + testCases: { count: (filter: TestCasesFilter) => backend.get(projectId + "/testcase/count?" + qs.stringify(filter)), - tree: (filter: TestCasesFilter) => backend.get(projectId + "/testcase/tree?" + qs.stringify(filter)), - list: (filter: TestCasesFilter) => backend.get(projectId + "/testcase?" + qs.stringify(filter)), + tree: (filters: AttributeFilterDraft[], groups: string[]) => + backend.get( + projectId + "/testcase/tree?" + [mapFiltersToQueryParams(filters), mapGroupsToQueryParams(groups)].join("&"), + ), + list: (filters: AttributeFilterDraft[]) => + backend.get(projectId + "/testcase?" + mapFiltersToQueryParams(filters)), }, }), }; +export type ServerAttributeFilterPayload = { + id: string; + attrValues: AttrValue[]; +}; +export type ServerAttributeFilterDraft = { + id: string | undefined; + attrValues: AttrValue[]; +}; +const mapNewClientFilterToBackend = (filter: AttributeFilterDraft): ServerAttributeFilterDraft => ({ + id: filter.attribute, + attrValues: filter.values.map(value => ({ value })), +}); + +const copyMeta = (meta: Meta & WithID): Meta & WithID => ({ + createdBy: meta.createdBy, + createdTime: meta.createdTime, + lastModifiedBy: meta.lastModifiedBy, + lastModifiedTime: meta.lastModifiedTime, + deleted: meta.deleted, + id: meta.id, +}); + +export type AttrValue = { value: string }; +export type ServerAttributeFilter = DeletedMeta & TimeMeta & { id: string; attrValues: AttrValue[] }; +export type ExistingServerAttributeFilter = WithID & DeletedMeta & TimeMeta & {}; +export interface ServerSuite { + name: string; + filter: { + filters: ServerAttributeFilter[]; + groups: string[]; + notFields: Record; + }; +} +export type ExistingServerSuite = ServerSuite & WithID & Meta; +export type NewServerSuite = { + name: string; + filter: { + filters: ServerAttributeFilterDraft[]; + groups: string[]; + notFields: Record; + }; +}; + +export const mapNewSuiteToServer = (suite: SuiteDraft): NewServerSuite => ({ + name: suite.name, + filter: { + filters: suite.filters.map(mapNewClientFilterToBackend), + groups: [], + notFields: {}, + }, +}); + +const mapClientFilterToBackend = (filter: ExistingAttributeFilter): ServerAttributeFilter => ({ + attrValues: filter.values.map(value => ({ value: value })), + createdTime: filter.createdTime, + id: filter.attribute, + lastModifiedTime: filter.lastModifiedTime, + deleted: filter.deleted, +}); + +const mapNewClientSuiteToBackend = (suite: SuiteDraft) => ({ + name: suite.name, + filter: { + filters: suite.filters.map(mapNewClientFilterToBackend), + groups: [], + notFields: { + id: suite.excludedTestCases, + }, + }, +}); + +const mapClientSuiteToBackend = (suite: ExistingSuite): ExistingServerSuite => ({ + ...copyMeta(suite), + name: suite.name, + filter: { + filters: suite.filters.map(mapClientFilterToBackend), + groups: [], + notFields: { + id: suite.excludedTestCases, + }, + }, +}); + +const mapServerFilterToClient = (filter: ServerAttributeFilter): ExistingAttributeFilter => ({ + attribute: filter.id, + values: filter.attrValues.map(value => value.value), + createdTime: filter.createdTime, + deleted: filter.deleted, + lastModifiedTime: filter.lastModifiedTime, +}); + +const mapServerSuiteToClient = (suite: ExistingServerSuite): ExistingSuite => ({ + ...copyMeta(suite), + name: suite.name, + filters: (suite.filter?.filters ?? []).map(mapServerFilterToClient), + excludedTestCases: suite.filter.notFields.id ?? [], +}); + export default backend; diff --git a/ui/src/src/stories/components/suite/SuiteHeader.stories.js b/ui/src/src/stories/components/suite/SuiteHeader.stories.js new file mode 100644 index 00000000..a2a31414 --- /dev/null +++ b/ui/src/src/stories/components/suite/SuiteHeader.stories.js @@ -0,0 +1,13 @@ +import React from "react"; +import SuiteHeader from "../../../testcases/SuiteHeader"; + +export default { + component: SuiteHeader, + title: "components/suite/SuiteHeader", + args: { + name: "My suite name", + }, + argTypes: { onChange: { action: true } }, +}; + +export const Default = args => ; diff --git a/ui/src/src/stories/components/testcase/List.stories.tsx b/ui/src/src/stories/components/testcase/List.stories.tsx new file mode 100644 index 00000000..c836a3b4 --- /dev/null +++ b/ui/src/src/stories/components/testcase/List.stories.tsx @@ -0,0 +1,134 @@ +import { ExistingTestCase, GroupState, TestCaseGroup } from "../../../domain"; +import List from "../../../components/testcase/List"; +import TestCaseListItem from "../../../components/testcase/TestCaseListItem"; +import TestCaseGroupComponent from "../../../components/testcase/TestCaseGroup"; +import { useState } from "react"; +import { useTreeExclusion, useSelection, useExclusion } from "../../../testcases/hooks"; +import { atom } from "recoil"; + +export type Args = { + testCases: ExistingTestCase[]; +}; + +export type Actions = { + onClick: (id: string) => void; + onCheckboxClick: (id: string) => void; + onChevronClick: (id: string) => void; +}; + +const testCases: ExistingTestCase[] = [ + { + id: "1", + name: "TestCase 1", + }, + { + id: "2", + name: "TestCase 2. It has a very very very, incredibly long name.", + }, + { + id: "3", + name: "TestCase 3. It has a very very very, incredibly long name.", + }, + { + id: "4", + name: "TestCase 3. It has a very very very, incredibly long name.", + }, +]; + +const args: Args = { + testCases, +}; + +export default { + component: List, + title: "components/testcase/List", + args, + argTypes: { onClick: { action: true }, onCheckboxClick: { action: true }, onChevronClick: { action: true } }, +}; + +export const Default = ({ testCases, onClick, onCheckboxClick, ...other }: Args & Actions) => { + return ( + + + + + + + ); +}; + +const group1: TestCaseGroup = { + id: "group1", + title: "Group 1", + isLeaf: true, + count: 2, + children: [], + testCases: [testCases[0], testCases[1]], +}; +const group2: TestCaseGroup = { + id: "group2", + title: "Group2", + isLeaf: false, + count: 1, + children: [group1], + testCases: [testCases[2]], +}; + +export const Grouped = ({ testCases, onClick, onCheckboxClick, onChevronClick, ...other }: Args & Actions) => { + return ( + + GroupState.Unselected} + testCaseGroup={group2} + isGroupOpen={id => ["group1", "group2"].includes(id)} + selectedTestCase="1" + isTestCaseSelected={id => ["1", "2"].includes(id)} + onCheckboxClick={onCheckboxClick} + onTestCaseCheckboxClick={onCheckboxClick} + onTestCaseClick={onClick} + onChevronClick={onChevronClick} + level={0} + /> + + ); +}; +const exclusionAtom = atom>({ + key: "exclusionAtom", + default: {}, +}); +export const GroupedStateful = () => { + const [selectedTestCase, setSelectedTestCase] = useState(undefined); + const [openGroups, handleChevronClick] = useSelection(["group1"]); + const [isTestCaseSelected, handleTestCaseCheckboxClick, excludedTestCases, setExcludedTestCases] = + useExclusion(exclusionAtom); + const [isGroupSelected, handleCheckboxClick] = useTreeExclusion([group2], excludedTestCases, setExcludedTestCases); + return ( + + GroupState.Selected} + selectedTestCase={selectedTestCase} + isTestCaseSelected={isTestCaseSelected} + onCheckboxClick={handleCheckboxClick} + onTestCaseCheckboxClick={handleTestCaseCheckboxClick} + onTestCaseClick={setSelectedTestCase} + onChevronClick={handleChevronClick} + level={0} + /> + + ); +}; diff --git a/ui/src/src/stories/components/ui/Button.stories.js b/ui/src/src/stories/components/ui/Button.stories.tsx similarity index 57% rename from ui/src/src/stories/components/ui/Button.stories.js rename to ui/src/src/stories/components/ui/Button.stories.tsx index 81fe8579..85fb108a 100644 --- a/ui/src/src/stories/components/ui/Button.stories.js +++ b/ui/src/src/stories/components/ui/Button.stories.tsx @@ -1,5 +1,3 @@ -import React from "react"; - import Button from "../../../components/ui/Button"; export default { @@ -12,6 +10,6 @@ export default { argTypes: { onClick: { action: true } }, }; -export const Default = args => + + )} +
    + ); +}; + +export default SuiteHeader; diff --git a/ui/src/src/testcases/TestCaseList.tsx b/ui/src/src/testcases/TestCaseList.tsx new file mode 100644 index 00000000..7662c365 --- /dev/null +++ b/ui/src/src/testcases/TestCaseList.tsx @@ -0,0 +1,42 @@ +import { useRecoilValue } from "recoil"; +import { AttributeFilterDraft, ExistingAttribute, FakeAttribute } from "../domain"; + +import List from "../components/testcase/List"; +import TestCaseListItem from "../components/testcase/TestCaseListItem"; +import { useQueryStringState } from "../lib/hooks"; +import { testCaseListSelector } from "./testCasesScreen.data"; +import TestCasesPanel from "./TestCasesPanel"; + +export type TestCaseListProps = { + projectID: string; + isTestCaseSelected: (id: string) => boolean; + onToggleTestCase: (id: string) => void; + filters: AttributeFilterDraft[]; + attributes: (ExistingAttribute | FakeAttribute)[]; +}; + +const TestCaseList = ({ attributes, projectID, isTestCaseSelected, onToggleTestCase, filters }: TestCaseListProps) => { + const testCases = useRecoilValue(testCaseListSelector({ projectID, filters })); + const [selectedTestCaseID, setSelectedTestCaseID] = useQueryStringState("selected", testCases[0]?.id); + return ( + +
    + + {testCases.map(testCase => ( + + ))} + +
    +
    + ); +}; + +export default TestCaseList; diff --git a/ui/src/src/testcases/TestCaseTree.tsx b/ui/src/src/testcases/TestCaseTree.tsx new file mode 100644 index 00000000..2b2b582b --- /dev/null +++ b/ui/src/src/testcases/TestCaseTree.tsx @@ -0,0 +1,90 @@ +import { useRecoilCallback, useRecoilValue } from "recoil"; + +import List from "../components/testcase/List"; +import { useQueryStringState } from "../lib/hooks"; +import { TestCaseListProps } from "./TestCaseList"; +import TestCaseGroupComponent from "../components/testcase/TestCaseGroup"; +import { useSelection } from "./hooks"; + +import { groupStateSelector, testCaseTreeSelector, TestCaseTreeSelectorParams } from "./testCasesScreen.data"; +import { GroupState } from "../domain"; +import TestCasesPanel from "./TestCasesPanel"; + +export type TestCaseTreeProps = TestCaseListProps & { + groups: string[]; + excludedTestCases: Record; + setExcludedTestCases: (excludedTestCases: Record) => void; +}; + +const useGroupState = ({ projectID, filters, groups }: TestCaseTreeSelectorParams) => { + const getGroupState = useRecoilCallback( + ({ snapshot }) => + (groupID: string) => + snapshot.getLoadable(groupStateSelector({ projectID, filters, groups, groupID })).getValue(), + [projectID, filters, groups], + ); + + const toggleGroupState = useRecoilCallback( + ({ set }) => + (groupID: string) => { + set( + groupStateSelector({ projectID, filters, groups, groupID }), + oldValue => + ({ + [GroupState.Selected]: GroupState.Unselected, + [GroupState.Unselected]: GroupState.Selected, + [GroupState.Indeterminate]: GroupState.Selected, + }[oldValue]), + ); + }, + [], + ); + return [getGroupState, toggleGroupState] as [typeof getGroupState, typeof toggleGroupState]; +}; + +const TestCaseTree = ({ + attributes, + projectID, + isTestCaseSelected, + onToggleTestCase, + filters, + groups, +}: TestCaseTreeProps) => { + const testCases = useRecoilValue(testCaseTreeSelector({ projectID, filters, groups })); + const [selectedTestCaseID, setSelectedTestCaseID] = useQueryStringState("selected", undefined); + const [isGroupOpen, toggleGroup] = useSelection([]); + const [getGroupState, toggleGroupState] = useGroupState({ projectID, filters, groups }); + return ( + + + {testCases.children.map(testCaseGroup => ( + + ))} + {/* {testCases.map(testCase => ( + + ))} */} + + + ); +}; + +export default TestCaseTree; diff --git a/ui/src/src/testcases/TestCases.js b/ui/src/src/testcases/TestCases.js index 6dfc9e89..4f1ec222 100644 --- a/ui/src/src/testcases/TestCases.js +++ b/ui/src/src/testcases/TestCases.js @@ -3,7 +3,7 @@ import React, { useCallback } from "react"; import SubComponent from "../common/SubComponent"; import TestCaseForm from "../testcases/TestCaseForm"; -import TestCasesFilter from "../testcases/TestCasesFilter"; +import TestCasesFilter from "../testcases/TestCasesFilters"; import TestCase from "../testcases/TestCase"; import $ from "jquery"; import qs from "qs"; diff --git a/ui/src/src/testcases/TestCasesFilters.tsx b/ui/src/src/testcases/TestCasesFilters.tsx new file mode 100644 index 00000000..69e05366 --- /dev/null +++ b/ui/src/src/testcases/TestCasesFilters.tsx @@ -0,0 +1,74 @@ +import clsx from "clsx"; +import { MouseEvent } from "react"; +import Button from "../components/ui/Button"; +import { FilterValue } from "../components/ui/Filter"; +import Filters from "../components/ui/Filters"; +import { inputBackgroundClasses } from "../components/ui/input"; +import Listbox from "../components/ui/ListBox"; +import SelectedValues from "../components/ui/SelectedValues"; +import { captionClasses } from "../components/ui/typography"; +import { ExistingAttribute, FakeAttribute } from "../domain"; + +export type TestCasesFiltersProps = { + projectAttributes: (FakeAttribute | ExistingAttribute)[]; + groups: string[]; + filters: FilterValue[]; + onChangeGroups: (value: string[]) => void; + onChangeFilters: (value: FilterValue[]) => void; + onSaveSuiteClick: () => void; + showSave: boolean; +}; +const TestCasesFilter = ({ + projectAttributes, + groups, + filters, + onChangeGroups, + onChangeFilters, + onSaveSuiteClick, + showSave, +}: TestCasesFiltersProps) => { + const handleRemoveGroupClick = (e: MouseEvent, value: string) => { + e.stopPropagation(); + onChangeGroups(groups.filter(group => group !== value)); + }; + const handleChangeGroups = (value: string) => { + onChangeGroups(groups.includes(value) ? groups.filter(item => item !== value) : [...groups, value]); + }; + const projectAttributesToGroupBy = projectAttributes.filter(attribute => attribute.id !== "broken"); + return ( +
    +
    Grouping
    +
    + {projectAttributes.length > 0 && ( + + ) : ( + Select grouping + ) + } + > + {projectAttributesToGroupBy.map(attribute => ( + + {attribute.name} + + ))} + + )} + {showSave && ( +
    + Save as suite +
    + )} +
    +
    Attributes
    + +
    + ); +}; + +export default TestCasesFilter; diff --git a/ui/src/src/testcases/TestCasesPanel.tsx b/ui/src/src/testcases/TestCasesPanel.tsx new file mode 100644 index 00000000..431431c5 --- /dev/null +++ b/ui/src/src/testcases/TestCasesPanel.tsx @@ -0,0 +1,36 @@ +import Button from "../components/ui/Button"; +import { PropsWithChildren, Suspense } from "react"; +import PlayIcon from "@heroicons/react/solid/PlayIcon"; +import { ExistingAttribute, FakeAttribute } from "../domain"; +import TestCase from "./TestCase"; +import { Loading } from "../components/ui"; + +export type TestCasesPanelProps = PropsWithChildren<{ + projectID: string; + selectedTestCaseID: string | undefined; + attributes: (ExistingAttribute | FakeAttribute)[]; +}>; + +const TestCasesPanel = ({ children, selectedTestCaseID, projectID, attributes }: TestCasesPanelProps): JSX.Element => { + return ( +
    +
    +
    +

    Test cases

    + + + Launch + +
    + {children} +
    +
    + {selectedTestCaseID && ( + + )} +
    +
    + ); +}; + +export default TestCasesPanel; diff --git a/ui/src/src/testcases/TestCasesScreen.tsx b/ui/src/src/testcases/TestCasesScreen.tsx new file mode 100644 index 00000000..165e50d0 --- /dev/null +++ b/ui/src/src/testcases/TestCasesScreen.tsx @@ -0,0 +1,102 @@ +import { FunctionComponent, Suspense } from "react"; +import { backendService } from "../services/backend"; +import { selectorFamily, useRecoilValue } from "recoil"; +import { useJSONQueryStringState, useQueryStringState } from "../lib/hooks"; +import TestCasesFilter from "./TestCasesFilters"; +import { FilterValue } from "../components/ui/Filter"; +import { AttributeFilterDraft } from "../domain"; +import SuiteDialog from "../components/suite/Dialog"; +import { FormValues } from "../components/suite/Form"; +import { useHistory } from "react-router"; +import TestCaseList from "./TestCaseList"; +import TestCaseTree from "./TestCaseTree"; +import { useExclusion } from "./hooks"; +import { exclusionAtom } from "./testCasesScreen.data"; +import { Loading } from "../components/ui"; + +export type TestCasesScreenProps = { + projectID: string; +}; + +export type WithProjectID = { + projectID: string; +}; + +const attributesSelector = selectorFamily({ + key: "project-attributes", + get: + ({ projectID }: WithProjectID) => + () => + backendService.project(projectID).attributes.list(), +}); + +const useSaveSuite = (projectID: string, filters: AttributeFilterDraft[], excludedTestCases: string[]) => { + const history = useHistory(); + return async (values: FormValues) => { + const suite = await backendService + .project(projectID) + .testSuites.create({ filters, excludedTestCases, name: values.name }); + history.push(`/${projectID}/suites/${suite.id}`); + }; +}; + +const TextCasesScreen: FunctionComponent = ({ projectID }) => { + const attributes = useRecoilValue(attributesSelector({ projectID })); + const [filters, handleChangeFilters] = useJSONQueryStringState("filters", []); + const [groups, handleChangeGroups] = useJSONQueryStringState("groups", []); + const [showSaveDialog, setShowSaveDialog] = useQueryStringState("save"); + const [isTestCaseSelected, toggleTestCase, excludedTestCases, setExcludedTestCases] = useExclusion(exclusionAtom); + const saveSuite = useSaveSuite(projectID, filters!, groups!); + + const sharedListProps = { + projectID, + filters, + isTestCaseSelected, + onToggleTestCase: toggleTestCase, + attributes, + }; + + return ( +
    +
    + {showSaveDialog === "true" && ( + setShowSaveDialog(undefined)} + /> + )} + setShowSaveDialog("true")} + /> + + + +
    + } + > + {groups.length === 0 ? ( + + ) : ( + + )} + +
    +
    + ); +}; + +export default TextCasesScreen; diff --git a/ui/src/src/testcases/hooks.ts b/ui/src/src/testcases/hooks.ts new file mode 100644 index 00000000..9cb27d45 --- /dev/null +++ b/ui/src/src/testcases/hooks.ts @@ -0,0 +1,70 @@ +import { + flattenGroup, + flattenGroups, + getAllTestCases, + getFullySetHash, + getGroupLookup, + listToBoolHash, + TestCaseGroup, +} from "../domain"; +import { useCallback, useMemo, useState } from "react"; +import { RecoilState, useRecoilState } from "recoil"; + +export const useSelection = (initialState: string[]) => { + const [checked, setChecked] = useState(() => listToBoolHash(initialState)); + const toggle = (id: string) => { + setChecked({ ...checked, [id]: !checked[id] }); + }; + const isChecked = (id: string) => checked[id]; + return [isChecked, toggle, checked] as [typeof isChecked, typeof toggle, typeof checked]; +}; + +export const useExclusion = (atom: RecoilState>) => { + const [excluded, setExcluded] = useRecoilState(atom); + const toggle = useCallback( + (id: string) => { + setExcluded({ ...excluded, [id]: !excluded[id] }); + }, + [excluded, setExcluded], + ); + const isSelected = useCallback((id: string) => !excluded[id], [excluded]); + return [isSelected, toggle, excluded, setExcluded] as [ + typeof isSelected, + typeof toggle, + typeof excluded, + typeof setExcluded, + ]; +}; + +export const useTreeExclusion = ( + groups: TestCaseGroup[], + excludedTestCases: Record, + setExcludedTestCases: (testCases: Record) => void, +) => { + const flatGroups = useMemo(() => flattenGroups(groups), [groups]); + const groupLookup = useMemo(() => getGroupLookup(flatGroups), [flatGroups]); + + const [excludedGroups, setExcludedGroups] = useState>(() => ({})); + + const toggleGroup = useCallback( + (id: string) => { + const selected = !excludedGroups[id]; + setExcludedGroups({ ...excludedGroups, ...getFullySetHash(flattenGroup(groupLookup[id]), selected) }); + setExcludedTestCases({ + ...excludedTestCases, + ...listToBoolHash( + getAllTestCases([groupLookup[id]]).map(testCase => testCase.id), + selected, + ), + }); + }, + [excludedGroups, excludedTestCases, groupLookup, setExcludedTestCases], + ); + + const isGroupSelected = useCallback((id: string): boolean => !excludedGroups[id], [excludedGroups]); + return [isGroupSelected, toggleGroup, excludedGroups] as [ + typeof isGroupSelected, + typeof toggleGroup, + typeof excludedGroups, + ]; +}; diff --git a/ui/src/src/testcases/testCasesScreen.data.ts b/ui/src/src/testcases/testCasesScreen.data.ts new file mode 100644 index 00000000..6f70720c --- /dev/null +++ b/ui/src/src/testcases/testCasesScreen.data.ts @@ -0,0 +1,79 @@ +import { atom, selectorFamily } from "recoil"; +import { AttributeFilterDraft, flattenGroups, getAllTestCases, GroupState, listToBoolHash } from "../domain"; +import listToLookup from "../lib/listToLookup"; +import { backendService } from "../services/backend"; + +export type TestCaseTreeSelectorParams = TestCaseListSelectorParams & { + groups: string[]; +}; + +export type TestCaseListSelectorParams = { + projectID: string; + filters: AttributeFilterDraft[]; +}; + +export const testCaseListSelector = selectorFamily({ + key: "test-case-list-selector", + get: + ({ projectID, filters }: TestCaseListSelectorParams) => + () => + backendService.project(projectID).testCases.list(filters), +}); + +export const testCaseTreeSelector = selectorFamily({ + key: "test-case-list-selector", + get: + ({ projectID, filters, groups }: TestCaseTreeSelectorParams) => + () => + backendService.project(projectID).testCases.tree(filters, groups), +}); + +const testCaseGroupLookupSelector = selectorFamily({ + key: "test-case-group-lookup-selector", + get: + (params: TestCaseTreeSelectorParams) => + ({ get }) => + listToLookup(flattenGroups(get(testCaseTreeSelector(params)).children ?? [])), +}); + +export type GroupTestCasesSelectorParams = TestCaseTreeSelectorParams & { + groupID: string; +}; + +const groupTestCasesSelector = selectorFamily({ + key: "group-test-cases-selector", + get: + ({ groupID, ...other }: GroupTestCasesSelectorParams) => + ({ get }) => { + const lookup = get(testCaseGroupLookupSelector(other)); + const group = lookup[groupID]; + const testCases = getAllTestCases([group]).map(testCase => testCase.id); + return testCases; + }, +}); + +export const exclusionAtom = atom>({ key: "exlusion", default: {} }); + +export const groupStateSelector = selectorFamily({ + key: "group-state-selector", + get: + params => + ({ get }) => { + const testCases = get(groupTestCasesSelector(params)); + const excluded = get(exclusionAtom); + const haveSelected = testCases.find(id => !excluded[id]); + const haveNotSelected = testCases.find(id => !!excluded[id]); + if (haveSelected && haveNotSelected) { + return GroupState.Indeterminate; + } else if (!haveSelected) { + return GroupState.Unselected; + } + return GroupState.Selected; + }, + set: + params => + ({ get, set }, newValue) => { + const testCases = get(groupTestCasesSelector(params)); + set(exclusionAtom, { ...get(exclusionAtom), ...listToBoolHash(testCases, newValue === GroupState.Unselected) }); + }, +}); diff --git a/ui/src/src/testsuites/TestSuitesWidget.js b/ui/src/src/testsuites/TestSuitesWidget.js index 40393492..92eec838 100644 --- a/ui/src/src/testsuites/TestSuitesWidget.js +++ b/ui/src/src/testsuites/TestSuitesWidget.js @@ -64,8 +64,8 @@ class TestSuitesWidget extends SubComponent {
    -
    - +
    +
    @@ -76,7 +76,7 @@ class TestSuitesWidget extends SubComponent { {this.state.testSuitesToDisplay.map( function (testSuite) { return ( -
    +
    = 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== +mime-db@1.49.0: + version "1.49.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" + integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== + mime-types@^2.1.12, mime-types@^2.1.25, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24: version "2.1.31" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" @@ -10814,6 +11716,13 @@ mime-types@^2.1.12, mime-types@^2.1.25, mime-types@^2.1.27, mime-types@~2.1.17, dependencies: mime-db "1.48.0" +mime-types@^2.1.30: + version "2.1.32" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" + integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== + dependencies: + mime-db "1.49.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -10829,6 +11738,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" + integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== + mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -11097,12 +12011,12 @@ node-dir@^0.1.10, node-dir@^0.1.17: dependencies: minimatch "^3.0.2" -node-emoji@^1.8.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" - integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw== +node-emoji@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== dependencies: - lodash.toarray "^4.4.0" + lodash "^4.17.21" node-fetch@^1.0.1: version "1.7.3" @@ -11178,6 +12092,11 @@ node-releases@^1.1.61, node-releases@^1.1.71: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== +node-releases@^1.1.75: + version "1.1.75" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe" + integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw== + nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -11374,7 +12293,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0, object.values@^1.1.3, object.values@^1.1.4: +object.values@^1.1.0, object.values@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== @@ -11500,6 +12419,11 @@ p-cancelable@^1.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + p-each-series@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" @@ -12281,6 +13205,11 @@ postcss-modules-extract-imports@^2.0.0: dependencies: postcss "^7.0.5" +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + postcss-modules-local-by-default@^3.0.2, postcss-modules-local-by-default@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" @@ -12291,6 +13220,15 @@ postcss-modules-local-by-default@^3.0.2, postcss-modules-local-by-default@^3.0.3 postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + postcss-modules-scope@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" @@ -12299,6 +13237,13 @@ postcss-modules-scope@^2.2.0: postcss "^7.0.6" postcss-selector-parser "^6.0.0" +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + postcss-modules-values@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" @@ -12307,6 +13252,13 @@ postcss-modules-values@^3.0.0: icss-utils "^4.0.0" postcss "^7.0.6" +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + postcss-nested@^4: version "4.2.3" resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.3.tgz#c6f255b0a720549776d220d00c4b70cd244136f6" @@ -12564,7 +13516,7 @@ postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.6: +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.6: version "6.0.6" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== @@ -12627,7 +13579,16 @@ postcss@^6.0.9: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^8.1.0, postcss@^8.2.1: +postcss@^8.1.0, postcss@^8.2.15: + version "8.3.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" + integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== + dependencies: + colorette "^1.2.2" + nanoid "^3.1.23" + source-map-js "^0.6.2" + +postcss@^8.2.1: version "8.3.5" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709" integrity sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA== @@ -12663,10 +13624,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" - integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== +prettier@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.0.tgz#85bdfe0f70c3e777cf13a4ffff39713ca6f64cba" + integrity sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ== prettier@~2.2.1: version "2.2.1" @@ -12686,6 +13647,14 @@ pretty-error@^2.1.1: lodash "^4.17.20" renderkid "^2.0.4" +pretty-error@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-3.0.4.tgz#94b1d54f76c1ed95b9c604b9de2194838e5b574e" + integrity sha512-ytLFLfv1So4AO1UkoBF6GXQgJRaKbiSiGFICaOPNwQ3CMvBvXpLRubeQWyPGnsbV/t9ml9qto6IeCsho0aEvwQ== + dependencies: + lodash "^4.17.20" + renderkid "^2.0.6" + pretty-format@^26.6.0, pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" @@ -12901,10 +13870,10 @@ puppeteer-core@^2.1.1: rimraf "^2.6.1" ws "^6.1.0" -purgecss@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-3.1.3.tgz#26987ec09d12eeadc318e22f6e5a9eb0be094f41" - integrity sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ== +purgecss@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.0.3.tgz#8147b429f9c09db719e05d64908ea8b672913742" + integrity sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw== dependencies: commander "^6.0.0" glob "^7.0.0" @@ -13538,10 +14507,10 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -recoil@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.4.0.tgz#2a933ba7c043cbf6f50f52da0828a879c7cd7d69" - integrity sha512-FZ2ljI4ldZU820V0APbKOtS4bPwPJHvpDBQEl+Cf47DMaM35wuLXl2u37E0TSgdvKAZBOUsIwcBnzE+ncODRxQ== +recoil@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.4.1.tgz#1cb2171bc98d41361507131a059d7c3525d8c4fe" + integrity sha512-vp6KPwlHOjJ4bJofmdDchmgI9ilMTCoUisK8/WYLl8dThH7e7KmtZttiLgvDb2Em99dUfTEsk8vT8L1nUMgqXQ== dependencies: hamt_plus "1.0.2" @@ -13752,7 +14721,7 @@ remove-trailing-separator@^1.0.1: resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= -renderkid@^2.0.4: +renderkid@^2.0.4, renderkid@^2.0.6: version "2.0.7" resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.7.tgz#464f276a6bdcee606f4a15993f9b29fc74ca8609" integrity sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ== @@ -13856,7 +14825,7 @@ resolve@1.18.1: is-core-module "^2.0.0" path-parse "^1.0.6" -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.8.1: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.8.1: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -14119,6 +15088,15 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -14222,6 +15200,13 @@ serialize-javascript@^5.0.1: dependencies: randombytes "^2.1.0" +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + serve-favicon@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.5.0.tgz#935d240cdfe0f5805307fdfe967d88942a2cbcf0" @@ -14425,16 +15410,16 @@ snapdragon@^0.8.1: use "^3.1.0" sockjs-client@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6" - integrity sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ== + version "1.5.2" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.2.tgz#4bc48c2da9ce4769f19dc723396b50f5c12330a3" + integrity sha512-ZzRxPBISQE7RpzlH4tKJMQbHM9pabHluk0WBaxAQ+wm/UieeBVBou0p4wVnSQGN9QmpAZygQ0cDIypWuqOFmFQ== dependencies: debug "^3.2.6" eventsource "^1.0.7" faye-websocket "^0.11.3" inherits "^2.0.4" json3 "^3.3.3" - url-parse "^1.5.1" + url-parse "^1.5.3" sockjs@^0.3.21: version "0.3.21" @@ -14642,12 +15627,12 @@ storybook-addon-outline@^1.4.1: "@storybook/core-events" "^6.3.0" ts-dedent "^2.1.1" -storybook@^6.3.6: - version "6.3.6" - resolved "https://registry.yarnpkg.com/storybook/-/storybook-6.3.6.tgz#d48e2912aa7ae85dc0062aaf387c5130ce91a834" - integrity sha512-2y4HJu735S0ED1u1w0je5KVdTZDI6pmjxhsEErziz5uoDGItqzgc0wVVpFEpEuP8eQbKKfqG0G6UuqQeKip/Bg== +storybook@^6.3.8: + version "6.3.8" + resolved "https://registry.yarnpkg.com/storybook/-/storybook-6.3.8.tgz#f2b3a3f6931fca66ed7db2b0120421b2867aff26" + integrity sha512-Z+C5tuT2KnEVDxjVqhq5wyanWLzUdkmtmLSpRHnUhyKUQbGKJWcbz+JS+v3WfsnUl78oSE5KKcZW84GYck7zQw== dependencies: - "@storybook/cli" "6.3.6" + "@storybook/cli" "6.3.8" stream-browserify@^2.0.1: version "2.0.2" @@ -14891,6 +15876,14 @@ style-loader@1.3.0, style-loader@^1.3.0: loader-utils "^2.0.0" schema-utils "^2.7.0" +style-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c" + integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + style-to-object@0.3.0, style-to-object@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" @@ -14938,6 +15931,13 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" @@ -15003,30 +16003,30 @@ table@^6.0.9: strip-ansi "^6.0.0" "tailwindcss@npm:@tailwindcss/postcss7-compat": - version "2.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.4.tgz#0e5b446e94cfdf892e4f76cecb703c8d698050f3" - integrity sha512-lFIBdD1D2w3RgHFg7kNB7U5LOlfbd+KXTzcLyC/RlQ9eVko6GjNCKpN/kdmfF9wiGxbSDT/3mousXeMZdOOuBg== + version "2.2.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.14.tgz#bdd82d75cbc6b596519f2bd54c1047a615e06366" + integrity sha512-vtzYILqywIY1GWELwHVF7goPhaJpm/1P5kJZ0Kx8lNlarALTFEWwIWCM6MxQ7pXzDWa1eUeozVJVeqfOBXKwXg== dependencies: - "@fullhuman/postcss-purgecss" "^3.1.3" - arg "^5.0.0" + arg "^5.0.1" autoprefixer "^9" bytes "^3.0.0" - chalk "^4.1.1" + chalk "^4.1.2" chokidar "^3.5.2" - color "^3.1.3" - cosmiconfig "^7.0.0" + color "^4.0.1" + cosmiconfig "^7.0.1" detective "^5.2.0" - didyoumean "^1.2.1" + didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.2.5" + fast-glob "^3.2.7" fs-extra "^10.0.0" - glob-parent "^6.0.0" + glob-parent "^6.0.1" html-tags "^3.1.0" + is-color-stop "^1.1.0" is-glob "^4.0.1" lodash "^4.17.21" lodash.topath "^4.5.2" modern-normalize "^1.1.0" - node-emoji "^1.8.1" + node-emoji "^1.11.0" normalize-path "^3.0.0" object-hash "^2.2.0" postcss "^7" @@ -15037,6 +16037,7 @@ table@^6.0.9: postcss-selector-parser "^6.0.6" postcss-value-parser "^4.1.0" pretty-hrtime "^1.0.3" + purgecss "^4.0.3" quick-lru "^5.1.1" reduce-css-calc "^2.1.8" resolve "^1.20.0" @@ -15047,6 +16048,11 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" + integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== + tar@^6.0.2: version "6.1.0" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" @@ -15137,6 +16143,18 @@ terser-webpack-plugin@^1.4.3: webpack-sources "^1.4.0" worker-farm "^1.7.0" +terser-webpack-plugin@^5.0.3, terser-webpack-plugin@^5.1.3: + version "5.2.4" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz#ad1be7639b1cbe3ea49fab995cbe7224b31747a1" + integrity sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA== + dependencies: + jest-worker "^27.0.6" + p-limit "^3.1.0" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + terser "^5.7.2" + terser@^4.1.2, terser@^4.6.2, terser@^4.6.3: version "4.8.0" resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" @@ -15155,6 +16173,15 @@ terser@^5.3.4: source-map "~0.7.2" source-map-support "~0.5.19" +terser@^5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.2.tgz#d4d95ed4f8bf735cb933e802f2a1829abf545e3f" + integrity sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.19" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -15351,10 +16378,10 @@ ts-pnp@1.2.0, ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== -tsconfig-paths@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== +tsconfig-paths@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36" + integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" @@ -15445,7 +16472,7 @@ type@^1.0.1: resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== -type@^2.0.0: +type@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== @@ -15467,10 +16494,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^4.3.3: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324" + integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA== ua-parser-js@^0.7.18: version "0.7.28" @@ -15727,10 +16754,10 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.4.3, url-parse@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== +url-parse@^1.4.3, url-parse@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" + integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -15949,6 +16976,14 @@ watchpack@^1.7.4: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.1" +watchpack@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce" + integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" @@ -15982,6 +17017,18 @@ webpack-dev-middleware@^3.7.2, webpack-dev-middleware@^3.7.3: range-parser "^1.2.1" webpack-log "^2.0.0" +webpack-dev-middleware@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz#179cc40795882cae510b1aa7f3710cbe93c9333e" + integrity sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w== + dependencies: + colorette "^1.2.2" + mem "^8.1.1" + memfs "^3.2.2" + mime-types "^2.1.30" + range-parser "^1.2.1" + schema-utils "^3.0.0" + webpack-dev-server@3.11.1: version "3.11.1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.1.tgz#c74028bf5ba8885aaf230e48a20e8936ab8511f0" @@ -16069,6 +17116,11 @@ webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack- source-list-map "^2.0.0" source-map "~0.6.1" +webpack-sources@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d" + integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw== + webpack-virtual-modules@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.2.2.tgz#20863dc3cb6bb2104729fff951fbe14b18bd0299" @@ -16076,6 +17128,11 @@ webpack-virtual-modules@^0.2.2: dependencies: debug "^3.0.0" +webpack-virtual-modules@^0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.3.tgz#cd597c6d51d5a5ecb473eea1983a58fa8a17ded9" + integrity sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw== + webpack@4: version "4.46.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" @@ -16134,6 +17191,36 @@ webpack@4.44.2: watchpack "^1.7.4" webpack-sources "^1.4.1" +webpack@^5.9.0: + version "5.52.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.52.1.tgz#2dc1d9029ecb7acfb80da7bf67baab67baa517a7" + integrity sha512-wkGb0hLfrS7ML3n2xIKfUIwHbjB6gxwQHyLmVHoAqEQBw+nWo+G6LoHL098FEXqahqximsntjBLuewStrnJk0g== + dependencies: + "@types/eslint-scope" "^3.7.0" + "@types/estree" "^0.0.50" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.8.0" + es-module-lexer "^0.7.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.4" + json-parse-better-errors "^1.0.2" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.2.0" + webpack-sources "^3.2.0" + websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" From 46aa7c386628e9f9157c277f598264d3ca53cde8 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Wed, 22 Sep 2021 23:04:21 +1000 Subject: [PATCH 09/17] Test suite editing --- ui/src/src/common/Main.js | 13 ++ ui/src/src/domain/index.ts | 4 +- ui/src/src/services/backend.ts | 51 +++++-- ui/src/src/testcases/TestCasesFilters.tsx | 12 +- ui/src/src/testcases/TestCasesPanel.tsx | 3 +- ui/src/src/testcases/TestCasesScreen.tsx | 22 +-- ui/src/src/testcases/TestSuiteScreen.tsx | 140 +++++++++++++++++++ ui/src/src/testcases/testCasesScreen.data.ts | 10 ++ ui/src/src/testsuites/TestSuites.js | 50 +++---- 9 files changed, 239 insertions(+), 66 deletions(-) create mode 100644 ui/src/src/testcases/TestSuiteScreen.tsx diff --git a/ui/src/src/common/Main.js b/ui/src/src/common/Main.js index 62f8a1f1..97744229 100644 --- a/ui/src/src/common/Main.js +++ b/ui/src/src/common/Main.js @@ -1,6 +1,7 @@ import React, { Component } from "react"; import { Switch, Route } from "react-router-dom"; import TestCasesScreen from "../testcases/TestCasesScreen"; +import TestSuiteScreen from "../testcases/TestSuiteScreen"; import TestSuites from "../testsuites/TestSuites"; import Projects from "../projects/Projects"; import ProjectForm from "../projects/ProjectForm"; @@ -120,10 +121,22 @@ class Main extends Component { )} /> + ( + + }> + + + + )} + /> } /> + backend.get(projectId + "/testsuite/" + testSuiteId).then(mapServerSuiteToClient), update: async (suite: ExistingSuite) => - mapServerSuiteToClient( - await backend.put(projectId + "/testsuite", mapClientSuiteToBackend(suite)), - ), + mapServerSuiteToClient(await backend.put(projectId + "/testsuite/", mapClientSuiteToBackend(suite))), }), create: async (suite: SuiteDraft) => mapServerSuiteToClient(await backend.post(projectId + "/testsuite/", mapNewClientSuiteToBackend(suite))), @@ -155,17 +153,34 @@ const copyMeta = (meta: Meta & WithID): Meta & WithID => ({ }); export type AttrValue = { value: string }; -export type ServerAttributeFilter = DeletedMeta & TimeMeta & { id: string; attrValues: AttrValue[] }; +export type ServerAttributeFilter = DeletedMeta & TimeMeta & { id: string | undefined; attrValues: AttrValue[] }; + export type ExistingServerAttributeFilter = WithID & DeletedMeta & TimeMeta & {}; export interface ServerSuite { name: string; filter: { - filters: ServerAttributeFilter[]; + filters: (ServerAttributeFilter | ServerAttributeFilterDraft)[]; groups: string[]; notFields: Record; }; } -export type ExistingServerSuite = ServerSuite & WithID & Meta; +export type ExistingServerSuite = ServerSuite & + WithID & + Meta & { + filter?: { + filters: ServerAttributeFilter[]; + groups: string[]; + notFields: Record; + }; + }; +export type SaveExistingSuiteParams = ServerSuite & + WithID & + Meta & { + filter: { + filters: (ServerAttributeFilter | ServerAttributeFilterDraft)[]; + }; + }; + export type NewServerSuite = { name: string; filter: { @@ -183,13 +198,22 @@ export const mapNewSuiteToServer = (suite: SuiteDraft): NewServerSuite => ({ notFields: {}, }, }); +const isExistingAttributeFilter = ( + filter: ExistingAttributeFilter | AttributeFilterDraft, +): filter is ExistingAttributeFilter => !!(filter as ExistingAttributeFilter).createdTime; -const mapClientFilterToBackend = (filter: ExistingAttributeFilter): ServerAttributeFilter => ({ +const mapClientFilterToBackend = ( + filter: ExistingAttributeFilter | AttributeFilterDraft, +): ServerAttributeFilter | ServerAttributeFilterDraft => ({ attrValues: filter.values.map(value => ({ value: value })), - createdTime: filter.createdTime, id: filter.attribute, - lastModifiedTime: filter.lastModifiedTime, - deleted: filter.deleted, + ...(isExistingAttributeFilter(filter) + ? { + createdTime: filter.createdTime, + lastModifiedTime: filter.lastModifiedTime, + deleted: filter.deleted, + } + : {}), }); const mapNewClientSuiteToBackend = (suite: SuiteDraft) => ({ @@ -203,7 +227,7 @@ const mapNewClientSuiteToBackend = (suite: SuiteDraft) => ({ }, }); -const mapClientSuiteToBackend = (suite: ExistingSuite): ExistingServerSuite => ({ +const mapClientSuiteToBackend = (suite: ExistingSuite): SaveExistingSuiteParams => ({ ...copyMeta(suite), name: suite.name, filter: { @@ -216,7 +240,7 @@ const mapClientSuiteToBackend = (suite: ExistingSuite): ExistingServerSuite => ( }); const mapServerFilterToClient = (filter: ServerAttributeFilter): ExistingAttributeFilter => ({ - attribute: filter.id, + attribute: filter.id!, values: filter.attrValues.map(value => value.value), createdTime: filter.createdTime, deleted: filter.deleted, @@ -227,7 +251,8 @@ const mapServerSuiteToClient = (suite: ExistingServerSuite): ExistingSuite => ({ ...copyMeta(suite), name: suite.name, filters: (suite.filter?.filters ?? []).map(mapServerFilterToClient), - excludedTestCases: suite.filter.notFields.id ?? [], + groups: suite.filter?.groups ?? [], + excludedTestCases: suite.filter?.notFields.id ?? [], }); export default backend; diff --git a/ui/src/src/testcases/TestCasesFilters.tsx b/ui/src/src/testcases/TestCasesFilters.tsx index 69e05366..67494337 100644 --- a/ui/src/src/testcases/TestCasesFilters.tsx +++ b/ui/src/src/testcases/TestCasesFilters.tsx @@ -7,17 +7,19 @@ import { inputBackgroundClasses } from "../components/ui/input"; import Listbox from "../components/ui/ListBox"; import SelectedValues from "../components/ui/SelectedValues"; import { captionClasses } from "../components/ui/typography"; -import { ExistingAttribute, FakeAttribute } from "../domain"; +import { ExistingAttribute, ExistingAttributeFilter, FakeAttribute } from "../domain"; export type TestCasesFiltersProps = { projectAttributes: (FakeAttribute | ExistingAttribute)[]; groups: string[]; filters: FilterValue[]; onChangeGroups: (value: string[]) => void; - onChangeFilters: (value: FilterValue[]) => void; - onSaveSuiteClick: () => void; - showSave: boolean; -}; + onChangeFilters: (value: (FilterValue | ExistingAttributeFilter)[]) => void; +} & ( + | { showSave: true; onSaveSuiteClick: () => void } + | { showSave?: false | undefined; onSaveSuiteClick?: () => void } +); + const TestCasesFilter = ({ projectAttributes, groups, diff --git a/ui/src/src/testcases/TestCasesPanel.tsx b/ui/src/src/testcases/TestCasesPanel.tsx index 431431c5..cfcf3847 100644 --- a/ui/src/src/testcases/TestCasesPanel.tsx +++ b/ui/src/src/testcases/TestCasesPanel.tsx @@ -1,9 +1,8 @@ import Button from "../components/ui/Button"; -import { PropsWithChildren, Suspense } from "react"; +import { PropsWithChildren } from "react"; import PlayIcon from "@heroicons/react/solid/PlayIcon"; import { ExistingAttribute, FakeAttribute } from "../domain"; import TestCase from "./TestCase"; -import { Loading } from "../components/ui"; export type TestCasesPanelProps = PropsWithChildren<{ projectID: string; diff --git a/ui/src/src/testcases/TestCasesScreen.tsx b/ui/src/src/testcases/TestCasesScreen.tsx index 165e50d0..d0c20e40 100644 --- a/ui/src/src/testcases/TestCasesScreen.tsx +++ b/ui/src/src/testcases/TestCasesScreen.tsx @@ -1,6 +1,6 @@ import { FunctionComponent, Suspense } from "react"; import { backendService } from "../services/backend"; -import { selectorFamily, useRecoilValue } from "recoil"; +import { useRecoilValue } from "recoil"; import { useJSONQueryStringState, useQueryStringState } from "../lib/hooks"; import TestCasesFilter from "./TestCasesFilters"; import { FilterValue } from "../components/ui/Filter"; @@ -11,32 +11,20 @@ import { useHistory } from "react-router"; import TestCaseList from "./TestCaseList"; import TestCaseTree from "./TestCaseTree"; import { useExclusion } from "./hooks"; -import { exclusionAtom } from "./testCasesScreen.data"; +import { attributesSelector, exclusionAtom } from "./testCasesScreen.data"; import { Loading } from "../components/ui"; export type TestCasesScreenProps = { projectID: string; }; -export type WithProjectID = { - projectID: string; -}; - -const attributesSelector = selectorFamily({ - key: "project-attributes", - get: - ({ projectID }: WithProjectID) => - () => - backendService.project(projectID).attributes.list(), -}); - -const useSaveSuite = (projectID: string, filters: AttributeFilterDraft[], excludedTestCases: string[]) => { +const useCreateSuite = (projectID: string, filters: AttributeFilterDraft[], excludedTestCases: string[]) => { const history = useHistory(); return async (values: FormValues) => { const suite = await backendService .project(projectID) .testSuites.create({ filters, excludedTestCases, name: values.name }); - history.push(`/${projectID}/suites/${suite.id}`); + history.push(`/${projectID}/testsuites/${suite.id}`); }; }; @@ -46,7 +34,7 @@ const TextCasesScreen: FunctionComponent = ({ projectID }) const [groups, handleChangeGroups] = useJSONQueryStringState("groups", []); const [showSaveDialog, setShowSaveDialog] = useQueryStringState("save"); const [isTestCaseSelected, toggleTestCase, excludedTestCases, setExcludedTestCases] = useExclusion(exclusionAtom); - const saveSuite = useSaveSuite(projectID, filters!, groups!); + const saveSuite = useCreateSuite(projectID, filters!, groups!); const sharedListProps = { projectID, diff --git a/ui/src/src/testcases/TestSuiteScreen.tsx b/ui/src/src/testcases/TestSuiteScreen.tsx new file mode 100644 index 00000000..4eb0dbbf --- /dev/null +++ b/ui/src/src/testcases/TestSuiteScreen.tsx @@ -0,0 +1,140 @@ +import { FunctionComponent, Suspense } from "react"; +import { backendService } from "../services/backend"; +import { atomFamily, selectorFamily, useRecoilState, useRecoilValue } from "recoil"; + +import TestCasesFilter from "./TestCasesFilters"; +import { FilterValue } from "../components/ui/Filter"; +import { ExistingAttributeFilter, ExistingSuite, listToBoolHash } from "../domain"; + +import TestCaseList from "./TestCaseList"; +import TestCaseTree from "./TestCaseTree"; +import { useExclusion } from "./hooks"; +import { attributesSelector, WithProjectID } from "./testCasesScreen.data"; +import { Loading } from "../components/ui"; + +export type TestSuiteScreenProps = { + projectID: string; + suiteID: string; +}; + +type SuiteSelectorParams = WithProjectID & { suiteID: string }; + +const suiteAtom = atomFamily({ + key: "existing-suite", + default: undefined, +}); +const suiteSelector = selectorFamily({ + key: "suite", + get: + ({ projectID, suiteID }) => + ({ get }) => + get(suiteAtom({ projectID, suiteID })) || backendService.project(projectID).testSuites.single(suiteID).get(), + set: + params => + ({ set }, value) => { + set(suiteAtom(params), value); + }, +}); + +const useSaveSuite = (params: SuiteSelectorParams) => { + const [suite, setSuite] = useRecoilState(suiteSelector(params)); + return async (updatedSuite: Partial) => { + console.info("updated", updatedSuite); + const savedSuite = await backendService + .project(params.projectID) + .testSuites.single(params.suiteID) + .update({ ...suite, ...updatedSuite }); + setSuite(savedSuite); + }; +}; + +const exclusionSelector = selectorFamily, SuiteSelectorParams>({ + key: "exclusionsSelector", + get: + params => + ({ get }) => { + const suite = get(suiteSelector(params)); + return listToBoolHash(suite.excludedTestCases); + }, + set: + params => + ({ get, set }, newValue) => { + const selector = suiteSelector(params); + set(selector, { + ...get(selector), + excludedTestCases: Object.keys(newValue).filter(key => (newValue as Record)[key]), + }); + }, +}); + +const TestSuiteScreen: FunctionComponent = ({ projectID, suiteID }) => { + const selectorParams = { projectID, suiteID }; + const attributes = useRecoilValue(attributesSelector({ projectID })); + + const [isTestCaseSelected, , excludedTestCases] = useExclusion(exclusionSelector(selectorParams)); + + const suite = useRecoilValue(suiteSelector(selectorParams)); + const saveSuite = useSaveSuite(selectorParams); + const setExcludedTestCasesHandler = async (exclusion: Record) => { + await saveSuite({ ...suite, excludedTestCases: Object.keys(exclusion).filter(key => !!exclusion[key]) }); + }; + const handleToggleTestCase = async (id: string) => { + await saveSuite({ + ...suite, + excludedTestCases: suite.excludedTestCases.includes(id) + ? suite.excludedTestCases.filter(item => item !== id) + : [...suite.excludedTestCases, id], + }); + }; + + const handleChangeFilters = async (filters: (ExistingAttributeFilter | FilterValue)[]) => { + saveSuite({ ...suite, filters }); + }; + + const handleChangeGroups = async (groups: string[]) => { + saveSuite({ ...suite, groups }); + }; + + const sharedListProps = { + projectID, + filters: suite.filters, + isTestCaseSelected, + onToggleTestCase: handleToggleTestCase, + attributes, + }; + + return ( +
    +
    + + + + +
    + } + > + {suite.groups.length === 0 ? ( + + ) : ( + + )} + +
    +
    + ); +}; + +export default TestSuiteScreen; diff --git a/ui/src/src/testcases/testCasesScreen.data.ts b/ui/src/src/testcases/testCasesScreen.data.ts index 6f70720c..7bbb8fe1 100644 --- a/ui/src/src/testcases/testCasesScreen.data.ts +++ b/ui/src/src/testcases/testCasesScreen.data.ts @@ -11,7 +11,17 @@ export type TestCaseListSelectorParams = { projectID: string; filters: AttributeFilterDraft[]; }; +export type WithProjectID = { + projectID: string; +}; +export const attributesSelector = selectorFamily({ + key: "project-attributes", + get: + ({ projectID }: WithProjectID) => + () => + backendService.project(projectID).attributes.list(), +}); export const testCaseListSelector = selectorFamily({ key: "test-case-list-selector", get: diff --git a/ui/src/src/testsuites/TestSuites.js b/ui/src/src/testsuites/TestSuites.js index a9e43715..30ce202b 100644 --- a/ui/src/src/testsuites/TestSuites.js +++ b/ui/src/src/testsuites/TestSuites.js @@ -93,37 +93,31 @@ class TestSuites extends SubComponent {
    - {this.state.testSuitesToDisplay.map( - function (testSuite) { - return ( -
    -
    -
    -
    -
    -
    {testSuite.name}
    -
    -
    - this.removeTestSuiteConfirmation(testSuite.id)} - > - - -
    -
    -

    - +

    +
    +
    +
    +
    {testSuite.name}
    +
    +
    + this.removeTestSuiteConfirmation(testSuite.id)} + > + +
    -
    +

    + View +

    - ); - }.bind(this), - )} +
    +
    +
    + ))}
    - ), + ); + }, ); export default TestCaseGroupComponent; diff --git a/ui/src/src/components/testcase/TestCaseListItem.tsx b/ui/src/src/components/testcase/TestCaseListItem.tsx index 1312627b..7d3dc917 100644 --- a/ui/src/src/components/testcase/TestCaseListItem.tsx +++ b/ui/src/src/components/testcase/TestCaseListItem.tsx @@ -3,6 +3,8 @@ import { ExistingTestCase } from "../../domain"; import clsx from "clsx"; import Checkbox from "../ui/Checkbox"; +export const getIndentStyle = (level: number) => ({ marginLeft: `${level * 2 + 1.25}rem` }); + export type TestCaseListItemProps = { disabled?: boolean; testCase: ExistingTestCase; @@ -26,7 +28,7 @@ const TestCaseListItem: FunctionComponent = memo( return (
  • @@ -44,7 +46,7 @@ const TestCaseListItem: FunctionComponent = memo( iconClassName={clsx({ "text-neutral-fade1": !selected, })} - style={{ marginLeft: `${(level + 1) * 28}px` }} + style={getIndentStyle(level + 1)} />
    {testCase.name}
  • diff --git a/ui/src/src/components/ui/Checkbox.tsx b/ui/src/src/components/ui/Checkbox.tsx index f2b9b148..4bd7fdb9 100644 --- a/ui/src/src/components/ui/Checkbox.tsx +++ b/ui/src/src/components/ui/Checkbox.tsx @@ -1,9 +1,9 @@ import CheckIcon from "@heroicons/react/outline/CheckIcon"; import clsx from "clsx"; -import { forwardRef } from "react"; +import { forwardRef, HTMLAttributes } from "react"; import { focusClasses } from "../../components/ui/focus"; -export type CheckboxProps = React.HTMLAttributes & { +export type CheckboxProps = HTMLAttributes & { iconClassName?: string; checked?: boolean; disabled?: boolean; @@ -14,7 +14,7 @@ const Checkbox = forwardRef( ({ checked, indeterminate, className, onClick, iconClassName, disabled, ...other }, ref) => ( - + )}
    ); diff --git a/ui/src/src/testcases/TestCaseList.tsx b/ui/src/src/testcases/TestCaseList.tsx index 7662c365..47c325e9 100644 --- a/ui/src/src/testcases/TestCaseList.tsx +++ b/ui/src/src/testcases/TestCaseList.tsx @@ -1,11 +1,12 @@ import { useRecoilValue } from "recoil"; -import { AttributeFilterDraft, ExistingAttribute, FakeAttribute } from "../domain"; - +import { ExistingAttribute, FakeAttribute, AttributeFilterDraft } from "../domain"; import List from "../components/testcase/List"; import TestCaseListItem from "../components/testcase/TestCaseListItem"; -import { useQueryStringState } from "../lib/hooks"; -import { testCaseListSelector } from "./testCasesScreen.data"; import TestCasesPanel from "./TestCasesPanel"; +import { useQueryStringState } from "../lib/hooks"; +import { selectorFamily } from "recoil"; +import { backendService } from "../services/backend"; +import { WithProjectID } from "./testCasesScreen.data"; export type TestCaseListProps = { projectID: string; @@ -13,9 +14,42 @@ export type TestCaseListProps = { onToggleTestCase: (id: string) => void; filters: AttributeFilterDraft[]; attributes: (ExistingAttribute | FakeAttribute)[]; + disabled?: boolean; }; -const TestCaseList = ({ attributes, projectID, isTestCaseSelected, onToggleTestCase, filters }: TestCaseListProps) => { +export type TestCaseTreeSelectorParams = TestCaseListSelectorParams & { + groups: string[]; +}; + +export type TestCaseListSelectorParams = { + projectID: string; + filters: AttributeFilterDraft[]; +}; + +export const attributesSelector = selectorFamily({ + key: "project-attributes", + get: + ({ projectID }: WithProjectID) => + () => + backendService.project(projectID).attributes.list(), +}); + +export const testCaseListSelector = selectorFamily({ + key: "test-case-list-selector", + get: + ({ projectID, filters }: TestCaseListSelectorParams) => + () => + backendService.project(projectID).testCases.list(filters), +}); + +const TestCaseList = ({ + attributes, + projectID, + isTestCaseSelected, + onToggleTestCase, + filters, + disabled, +}: TestCaseListProps) => { const testCases = useRecoilValue(testCaseListSelector({ projectID, filters })); const [selectedTestCaseID, setSelectedTestCaseID] = useQueryStringState("selected", testCases[0]?.id); return ( @@ -31,6 +65,7 @@ const TestCaseList = ({ attributes, projectID, isTestCaseSelected, onToggleTestC onCheckboxClick={onToggleTestCase} selected={selectedTestCaseID === testCase.id} onClick={setSelectedTestCaseID} + disabled={disabled} /> ))} diff --git a/ui/src/src/testcases/TestCaseTree.tsx b/ui/src/src/testcases/TestCaseTree.tsx index 2b2b582b..a1bf8a4f 100644 --- a/ui/src/src/testcases/TestCaseTree.tsx +++ b/ui/src/src/testcases/TestCaseTree.tsx @@ -1,45 +1,44 @@ -import { useRecoilCallback, useRecoilValue } from "recoil"; +import { selectorFamily, useRecoilValue } from "recoil"; import List from "../components/testcase/List"; -import { useQueryStringState } from "../lib/hooks"; + import { TestCaseListProps } from "./TestCaseList"; import TestCaseGroupComponent from "../components/testcase/TestCaseGroup"; -import { useSelection } from "./hooks"; +import { ExclusionState, useSelection } from "./hooks"; -import { groupStateSelector, testCaseTreeSelector, TestCaseTreeSelectorParams } from "./testCasesScreen.data"; -import { GroupState } from "../domain"; +import { AttributeFilterDraft, getAllTestCases, listToBoolHash, TestCaseGroup } from "../domain"; import TestCasesPanel from "./TestCasesPanel"; +import { useQueryStringState } from "../lib/hooks"; +import { backendService } from "../services/backend"; export type TestCaseTreeProps = TestCaseListProps & { groups: string[]; - excludedTestCases: Record; - setExcludedTestCases: (excludedTestCases: Record) => void; + exclusionState: ExclusionState; }; +export type TestCaseListSelectorParams = { + projectID: string; + filters: AttributeFilterDraft[]; +}; +export type TestCaseTreeSelectorParams = TestCaseListSelectorParams & { + groups: string[]; +}; +export const testCaseTreeSelector = selectorFamily({ + key: "test-case-list-selector", + get: + ({ projectID, filters, groups }: TestCaseTreeSelectorParams) => + () => + backendService.project(projectID).testCases.tree(filters, groups), +}); -const useGroupState = ({ projectID, filters, groups }: TestCaseTreeSelectorParams) => { - const getGroupState = useRecoilCallback( - ({ snapshot }) => - (groupID: string) => - snapshot.getLoadable(groupStateSelector({ projectID, filters, groups, groupID })).getValue(), - [projectID, filters, groups], - ); - - const toggleGroupState = useRecoilCallback( - ({ set }) => - (groupID: string) => { - set( - groupStateSelector({ projectID, filters, groups, groupID }), - oldValue => - ({ - [GroupState.Selected]: GroupState.Unselected, - [GroupState.Unselected]: GroupState.Selected, - [GroupState.Indeterminate]: GroupState.Selected, - }[oldValue]), - ); - }, - [], - ); - return [getGroupState, toggleGroupState] as [typeof getGroupState, typeof toggleGroupState]; +export const useToggleGroupState = (exclusionState: ExclusionState) => (group: TestCaseGroup) => { + const cases = getAllTestCases([group]).map(testCase => testCase.id); + const exclusions = cases.map(id => exclusionState[0][id]); + const hasExcluded = exclusions.some(Boolean); + let value = false; + if (!hasExcluded) { + value = true; + } + exclusionState[1](listToBoolHash(cases, value)); }; const TestCaseTree = ({ @@ -49,16 +48,19 @@ const TestCaseTree = ({ onToggleTestCase, filters, groups, + exclusionState, + disabled, }: TestCaseTreeProps) => { const testCases = useRecoilValue(testCaseTreeSelector({ projectID, filters, groups })); const [selectedTestCaseID, setSelectedTestCaseID] = useQueryStringState("selected", undefined); - const [isGroupOpen, toggleGroup] = useSelection([]); - const [getGroupState, toggleGroupState] = useGroupState({ projectID, filters, groups }); + const [isGroupOpen, toggleGroup] = useSelection(); + const toggleGroupState = useToggleGroupState(exclusionState); return ( - {testCases.children.map(testCaseGroup => ( + {testCases.children?.map(testCaseGroup => ( ))} - {/* {testCases.map(testCase => ( - - ))} */} ); diff --git a/ui/src/src/testcases/TestCasesFilters.tsx b/ui/src/src/testcases/TestCasesFilters.tsx index 67494337..99fc6554 100644 --- a/ui/src/src/testcases/TestCasesFilters.tsx +++ b/ui/src/src/testcases/TestCasesFilters.tsx @@ -13,6 +13,7 @@ export type TestCasesFiltersProps = { projectAttributes: (FakeAttribute | ExistingAttribute)[]; groups: string[]; filters: FilterValue[]; + disabled?: boolean; onChangeGroups: (value: string[]) => void; onChangeFilters: (value: (FilterValue | ExistingAttributeFilter)[]) => void; } & ( @@ -28,6 +29,7 @@ const TestCasesFilter = ({ onChangeFilters, onSaveSuiteClick, showSave, + disabled, }: TestCasesFiltersProps) => { const handleRemoveGroupClick = (e: MouseEvent, value: string) => { e.stopPropagation(); @@ -43,6 +45,7 @@ const TestCasesFilter = ({
    {projectAttributes.length > 0 && (
    Attributes
    - +
    ); }; diff --git a/ui/src/src/testcases/TestCasesPanel.tsx b/ui/src/src/testcases/TestCasesPanel.tsx index cfcf3847..c178eadf 100644 --- a/ui/src/src/testcases/TestCasesPanel.tsx +++ b/ui/src/src/testcases/TestCasesPanel.tsx @@ -13,7 +13,7 @@ export type TestCasesPanelProps = PropsWithChildren<{ const TestCasesPanel = ({ children, selectedTestCaseID, projectID, attributes }: TestCasesPanelProps): JSX.Element => { return (
    -
    +

    Test cases

    diff --git a/ui/src/src/testcases/TestCasesScreen.tsx b/ui/src/src/testcases/TestCasesScreen.tsx index d0c20e40..d4dc35d2 100644 --- a/ui/src/src/testcases/TestCasesScreen.tsx +++ b/ui/src/src/testcases/TestCasesScreen.tsx @@ -1,18 +1,16 @@ -import { FunctionComponent, Suspense } from "react"; +import { FunctionComponent, useState } from "react"; import { backendService } from "../services/backend"; import { useRecoilValue } from "recoil"; import { useJSONQueryStringState, useQueryStringState } from "../lib/hooks"; -import TestCasesFilter from "./TestCasesFilters"; import { FilterValue } from "../components/ui/Filter"; import { AttributeFilterDraft } from "../domain"; import SuiteDialog from "../components/suite/Dialog"; import { FormValues } from "../components/suite/Form"; import { useHistory } from "react-router"; -import TestCaseList from "./TestCaseList"; -import TestCaseTree from "./TestCaseTree"; -import { useExclusion } from "./hooks"; -import { attributesSelector, exclusionAtom } from "./testCasesScreen.data"; -import { Loading } from "../components/ui"; +import { ExclusionHash, useExclusion } from "./hooks"; +import { attributesSelector } from "./testCasesScreen.data"; +import { TestCasesScreenStateless } from "./TestCasesScreenStateless"; +import { SuiteHeaderText } from "./SuiteHeader"; export type TestCasesScreenProps = { projectID: string; @@ -33,57 +31,28 @@ const TextCasesScreen: FunctionComponent = ({ projectID }) const [filters, handleChangeFilters] = useJSONQueryStringState("filters", []); const [groups, handleChangeGroups] = useJSONQueryStringState("groups", []); const [showSaveDialog, setShowSaveDialog] = useQueryStringState("save"); - const [isTestCaseSelected, toggleTestCase, excludedTestCases, setExcludedTestCases] = useExclusion(exclusionAtom); + const exclusionState = useState({}); + const [isTestCaseSelected, toggleTestCase] = useExclusion(exclusionState); const saveSuite = useCreateSuite(projectID, filters!, groups!); - const sharedListProps = { - projectID, - filters, - isTestCaseSelected, - onToggleTestCase: toggleTestCase, - attributes, - }; - return ( -
    -
    - {showSaveDialog === "true" && ( - setShowSaveDialog(undefined)} - /> - )} - setShowSaveDialog("true")} - /> - - - -
    - } - > - {groups.length === 0 ? ( - - ) : ( - - )} - -
    -
    + <> + {showSaveDialog === "true" && ( + setShowSaveDialog(undefined)} /> + )} + Test cases} + projectID={projectID} + filters={filters} + attributes={attributes} + exclusionState={exclusionState} + groups={groups} + isTestCaseSelected={isTestCaseSelected} + onChangeFilters={handleChangeFilters} + onChangeGroups={handleChangeGroups} + onToggleTestCase={toggleTestCase} + /> + ); }; diff --git a/ui/src/src/testcases/TestCasesScreenStateless.tsx b/ui/src/src/testcases/TestCasesScreenStateless.tsx new file mode 100644 index 00000000..d767981f --- /dev/null +++ b/ui/src/src/testcases/TestCasesScreenStateless.tsx @@ -0,0 +1,68 @@ +import { ReactNode, Suspense } from "react"; +import { Loading } from "../components/ui"; +import { AttributeFilterDraft, ExistingAttribute, ExistingAttributeFilter, FakeAttribute } from "../domain"; +import { ExclusionState } from "./hooks"; +import TestCaseList from "./TestCaseList"; +import TestCasesFilter from "./TestCasesFilters"; +import TestCaseTree from "./TestCaseTree"; + +export type TestCasesScreenStatelessProps = { + beforeFilters?: ReactNode; + disableTestCaseList?: boolean; + disableFilters?: boolean; + attributes: (ExistingAttribute | FakeAttribute)[]; + filters: (AttributeFilterDraft | ExistingAttributeFilter)[]; + groups: string[]; + onChangeFilters: (value: (AttributeFilterDraft | ExistingAttributeFilter)[]) => void; + onChangeGroups: (value: string[]) => void; + exclusionState: ExclusionState; + projectID: string; + isTestCaseSelected: (id: string) => boolean; + onToggleTestCase: (id: string) => void; +}; + +export const TestCasesScreenStateless = ({ + attributes, + groups, + filters, + onChangeFilters, + onChangeGroups, + exclusionState, + beforeFilters, + disableFilters, + ...other +}: TestCasesScreenStatelessProps) => ( +
    +
    + {beforeFilters} + + + + +
    + } + > + {groups.length === 0 ? ( + + ) : ( + + )} + +
    +
    +); diff --git a/ui/src/src/testcases/TestSuiteScreen.tsx b/ui/src/src/testcases/TestSuiteScreen.tsx index 4eb0dbbf..aa73c524 100644 --- a/ui/src/src/testcases/TestSuiteScreen.tsx +++ b/ui/src/src/testcases/TestSuiteScreen.tsx @@ -1,16 +1,14 @@ -import { FunctionComponent, Suspense } from "react"; +import { FunctionComponent, useCallback, useMemo, useState } from "react"; import { backendService } from "../services/backend"; -import { atomFamily, selectorFamily, useRecoilState, useRecoilValue } from "recoil"; +import { atomFamily, RecoilState, selectorFamily, useRecoilState, useRecoilValue } from "recoil"; -import TestCasesFilter from "./TestCasesFilters"; import { FilterValue } from "../components/ui/Filter"; import { ExistingAttributeFilter, ExistingSuite, listToBoolHash } from "../domain"; -import TestCaseList from "./TestCaseList"; -import TestCaseTree from "./TestCaseTree"; -import { useExclusion } from "./hooks"; import { attributesSelector, WithProjectID } from "./testCasesScreen.data"; -import { Loading } from "../components/ui"; + +import { TestCasesScreenStateless } from "./TestCasesScreenStateless"; +import SuiteHeader from "./SuiteHeader"; export type TestSuiteScreenProps = { projectID: string; @@ -23,6 +21,7 @@ const suiteAtom = atomFamily({ key: "existing-suite", default: undefined, }); + const suiteSelector = selectorFamily({ key: "suite", get: @@ -36,48 +35,43 @@ const suiteSelector = selectorFamily({ }, }); -const useSaveSuite = (params: SuiteSelectorParams) => { - const [suite, setSuite] = useRecoilState(suiteSelector(params)); - return async (updatedSuite: Partial) => { - console.info("updated", updatedSuite); - const savedSuite = await backendService - .project(params.projectID) - .testSuites.single(params.suiteID) - .update({ ...suite, ...updatedSuite }); - setSuite(savedSuite); +const useSaveSuite = (suiteState: RecoilState, projectID: string, suiteID: string) => { + const [suite, setSuite] = useRecoilState(suiteState); + const [isSaving, setIsSaving] = useState(false); + const save = async (updatedSuite: Partial) => { + setIsSaving(true); + try { + const savedSuite = await backendService + .project(projectID) + .testSuites.single(suiteID) + .update({ ...suite, ...updatedSuite }); + setSuite(savedSuite); + } finally { + setIsSaving(false); + } }; + return [save, isSaving] as [typeof save, typeof isSaving]; }; -const exclusionSelector = selectorFamily, SuiteSelectorParams>({ - key: "exclusionsSelector", - get: - params => - ({ get }) => { - const suite = get(suiteSelector(params)); - return listToBoolHash(suite.excludedTestCases); - }, - set: - params => - ({ get, set }, newValue) => { - const selector = suiteSelector(params); - set(selector, { - ...get(selector), - excludedTestCases: Object.keys(newValue).filter(key => (newValue as Record)[key]), - }); - }, -}); - const TestSuiteScreen: FunctionComponent = ({ projectID, suiteID }) => { const selectorParams = { projectID, suiteID }; const attributes = useRecoilValue(attributesSelector({ projectID })); - - const [isTestCaseSelected, , excludedTestCases] = useExclusion(exclusionSelector(selectorParams)); - - const suite = useRecoilValue(suiteSelector(selectorParams)); - const saveSuite = useSaveSuite(selectorParams); - const setExcludedTestCasesHandler = async (exclusion: Record) => { - await saveSuite({ ...suite, excludedTestCases: Object.keys(exclusion).filter(key => !!exclusion[key]) }); - }; + const suiteState = suiteSelector(selectorParams); + + const suite = useRecoilValue(suiteState); + const [saveSuite, isSaving] = useSaveSuite(suiteState, projectID, suiteID); + const exclusionHash = useMemo(() => listToBoolHash(suite.excludedTestCases), [suite.excludedTestCases]); + const isTestCaseSelected = (id: string) => !exclusionHash[id]; + const setExcludedTestCasesHandler = useCallback( + async (exclusion: Record) => { + await saveSuite({ ...suite, excludedTestCases: Object.keys(exclusion).filter(key => !!exclusion[key]) }); + }, + [saveSuite, suite], + ); + const exclusionState = useMemo( + () => [exclusionHash, setExcludedTestCasesHandler] as [typeof exclusionHash, typeof setExcludedTestCasesHandler], + [exclusionHash, setExcludedTestCasesHandler], + ); const handleToggleTestCase = async (id: string) => { await saveSuite({ ...suite, @@ -94,46 +88,24 @@ const TestSuiteScreen: FunctionComponent = ({ projectID, s const handleChangeGroups = async (groups: string[]) => { saveSuite({ ...suite, groups }); }; - - const sharedListProps = { - projectID, - filters: suite.filters, - isTestCaseSelected, - onToggleTestCase: handleToggleTestCase, - attributes, + const handleChangeSuiteName = (name: string) => { + saveSuite({ ...suite, name }); }; - return ( -
    -
    - - - - -
    - } - > - {suite.groups.length === 0 ? ( - - ) : ( - - )} - -
    -
    + } + disableFilters={isSaving} + projectID={projectID} + filters={suite.filters} + attributes={attributes} + exclusionState={exclusionState} + groups={suite.groups} + isTestCaseSelected={isTestCaseSelected} + onChangeFilters={handleChangeFilters} + onChangeGroups={handleChangeGroups} + onToggleTestCase={handleToggleTestCase} + /> ); }; diff --git a/ui/src/src/testcases/hooks.ts b/ui/src/src/testcases/hooks.ts index 9cb27d45..01d3958b 100644 --- a/ui/src/src/testcases/hooks.ts +++ b/ui/src/src/testcases/hooks.ts @@ -1,16 +1,7 @@ -import { - flattenGroup, - flattenGroups, - getAllTestCases, - getFullySetHash, - getGroupLookup, - listToBoolHash, - TestCaseGroup, -} from "../domain"; -import { useCallback, useMemo, useState } from "react"; -import { RecoilState, useRecoilState } from "recoil"; +import { listToBoolHash } from "../domain"; +import { useCallback, useState } from "react"; -export const useSelection = (initialState: string[]) => { +export const useSelection = (initialState: string[] = []) => { const [checked, setChecked] = useState(() => listToBoolHash(initialState)); const toggle = (id: string) => { setChecked({ ...checked, [id]: !checked[id] }); @@ -18,9 +9,10 @@ export const useSelection = (initialState: string[]) => { const isChecked = (id: string) => checked[id]; return [isChecked, toggle, checked] as [typeof isChecked, typeof toggle, typeof checked]; }; - -export const useExclusion = (atom: RecoilState>) => { - const [excluded, setExcluded] = useRecoilState(atom); +export type State = [T, (newState: T) => void]; +export type ExclusionHash = Record; +export type ExclusionState = State; +export const useExclusion = ([excluded, setExcluded]: ExclusionState) => { const toggle = useCallback( (id: string) => { setExcluded({ ...excluded, [id]: !excluded[id] }); @@ -28,43 +20,5 @@ export const useExclusion = (atom: RecoilState>) => { [excluded, setExcluded], ); const isSelected = useCallback((id: string) => !excluded[id], [excluded]); - return [isSelected, toggle, excluded, setExcluded] as [ - typeof isSelected, - typeof toggle, - typeof excluded, - typeof setExcluded, - ]; -}; - -export const useTreeExclusion = ( - groups: TestCaseGroup[], - excludedTestCases: Record, - setExcludedTestCases: (testCases: Record) => void, -) => { - const flatGroups = useMemo(() => flattenGroups(groups), [groups]); - const groupLookup = useMemo(() => getGroupLookup(flatGroups), [flatGroups]); - - const [excludedGroups, setExcludedGroups] = useState>(() => ({})); - - const toggleGroup = useCallback( - (id: string) => { - const selected = !excludedGroups[id]; - setExcludedGroups({ ...excludedGroups, ...getFullySetHash(flattenGroup(groupLookup[id]), selected) }); - setExcludedTestCases({ - ...excludedTestCases, - ...listToBoolHash( - getAllTestCases([groupLookup[id]]).map(testCase => testCase.id), - selected, - ), - }); - }, - [excludedGroups, excludedTestCases, groupLookup, setExcludedTestCases], - ); - - const isGroupSelected = useCallback((id: string): boolean => !excludedGroups[id], [excludedGroups]); - return [isGroupSelected, toggleGroup, excludedGroups] as [ - typeof isGroupSelected, - typeof toggleGroup, - typeof excludedGroups, - ]; + return [isSelected, toggle] as [typeof isSelected, typeof toggle]; }; diff --git a/ui/src/src/testcases/testCasesScreen.data.ts b/ui/src/src/testcases/testCasesScreen.data.ts index 7bbb8fe1..9157dcb2 100644 --- a/ui/src/src/testcases/testCasesScreen.data.ts +++ b/ui/src/src/testcases/testCasesScreen.data.ts @@ -1,16 +1,6 @@ -import { atom, selectorFamily } from "recoil"; -import { AttributeFilterDraft, flattenGroups, getAllTestCases, GroupState, listToBoolHash } from "../domain"; -import listToLookup from "../lib/listToLookup"; +import { selectorFamily } from "recoil"; import { backendService } from "../services/backend"; -export type TestCaseTreeSelectorParams = TestCaseListSelectorParams & { - groups: string[]; -}; - -export type TestCaseListSelectorParams = { - projectID: string; - filters: AttributeFilterDraft[]; -}; export type WithProjectID = { projectID: string; }; @@ -22,68 +12,3 @@ export const attributesSelector = selectorFamily({ () => backendService.project(projectID).attributes.list(), }); -export const testCaseListSelector = selectorFamily({ - key: "test-case-list-selector", - get: - ({ projectID, filters }: TestCaseListSelectorParams) => - () => - backendService.project(projectID).testCases.list(filters), -}); - -export const testCaseTreeSelector = selectorFamily({ - key: "test-case-list-selector", - get: - ({ projectID, filters, groups }: TestCaseTreeSelectorParams) => - () => - backendService.project(projectID).testCases.tree(filters, groups), -}); - -const testCaseGroupLookupSelector = selectorFamily({ - key: "test-case-group-lookup-selector", - get: - (params: TestCaseTreeSelectorParams) => - ({ get }) => - listToLookup(flattenGroups(get(testCaseTreeSelector(params)).children ?? [])), -}); - -export type GroupTestCasesSelectorParams = TestCaseTreeSelectorParams & { - groupID: string; -}; - -const groupTestCasesSelector = selectorFamily({ - key: "group-test-cases-selector", - get: - ({ groupID, ...other }: GroupTestCasesSelectorParams) => - ({ get }) => { - const lookup = get(testCaseGroupLookupSelector(other)); - const group = lookup[groupID]; - const testCases = getAllTestCases([group]).map(testCase => testCase.id); - return testCases; - }, -}); - -export const exclusionAtom = atom>({ key: "exlusion", default: {} }); - -export const groupStateSelector = selectorFamily({ - key: "group-state-selector", - get: - params => - ({ get }) => { - const testCases = get(groupTestCasesSelector(params)); - const excluded = get(exclusionAtom); - const haveSelected = testCases.find(id => !excluded[id]); - const haveNotSelected = testCases.find(id => !!excluded[id]); - if (haveSelected && haveNotSelected) { - return GroupState.Indeterminate; - } else if (!haveSelected) { - return GroupState.Unselected; - } - return GroupState.Selected; - }, - set: - params => - ({ get, set }, newValue) => { - const testCases = get(groupTestCasesSelector(params)); - set(exclusionAtom, { ...get(exclusionAtom), ...listToBoolHash(testCases, newValue === GroupState.Unselected) }); - }, -}); From 77a526421983b53cb3266d0a3512495dc6313176 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Tue, 28 Sep 2021 22:56:03 +1000 Subject: [PATCH 11/17] LoadMore --- ui/src/src/services/backend.ts | 9 +++-- ui/src/src/testcases/TestCaseList.tsx | 50 +++++++++++++++++++++++---- ui/src/tailwind.config.js | 2 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/ui/src/src/services/backend.ts b/ui/src/src/services/backend.ts index ef2d1db8..c7f4fda5 100644 --- a/ui/src/src/services/backend.ts +++ b/ui/src/src/services/backend.ts @@ -119,13 +119,16 @@ export const backendService = { list: () => backend.get(projectId + "/attribute").then(mapServerAttributesToClient), }, testCases: { - count: (filter: TestCasesFilter) => backend.get(projectId + "/testcase/count?" + qs.stringify(filter)), + count: (filters: AttributeFilterDraft[]) => + backend.get(projectId + "/testcase/count?" + mapFiltersToQueryParams(filters)), tree: (filters: AttributeFilterDraft[], groups: string[]) => backend.get( projectId + "/testcase/tree?" + [mapFiltersToQueryParams(filters), mapGroupsToQueryParams(groups)].join("&"), ), - list: (filters: AttributeFilterDraft[]) => - backend.get(projectId + "/testcase?" + mapFiltersToQueryParams(filters)), + list: (filters: AttributeFilterDraft[], offset: number = 0) => + backend.get( + projectId + "/testcase?skip=" + offset.toString() + mapFiltersToQueryParams(filters), + ), }, }), }; diff --git a/ui/src/src/testcases/TestCaseList.tsx b/ui/src/src/testcases/TestCaseList.tsx index 47c325e9..e0461567 100644 --- a/ui/src/src/testcases/TestCaseList.tsx +++ b/ui/src/src/testcases/TestCaseList.tsx @@ -1,5 +1,5 @@ -import { useRecoilValue } from "recoil"; -import { ExistingAttribute, FakeAttribute, AttributeFilterDraft } from "../domain"; +import { atomFamily, useRecoilState, useRecoilValue } from "recoil"; +import { ExistingAttribute, FakeAttribute, AttributeFilterDraft, ExistingTestCase } from "../domain"; import List from "../components/testcase/List"; import TestCaseListItem from "../components/testcase/TestCaseListItem"; import TestCasesPanel from "./TestCasesPanel"; @@ -7,7 +7,8 @@ import { useQueryStringState } from "../lib/hooks"; import { selectorFamily } from "recoil"; import { backendService } from "../services/backend"; import { WithProjectID } from "./testCasesScreen.data"; - +import Button from "../components/ui/Button"; +import { useState } from "react"; export type TestCaseListProps = { projectID: string; isTestCaseSelected: (id: string) => boolean; @@ -34,12 +35,28 @@ export const attributesSelector = selectorFamily({ backendService.project(projectID).attributes.list(), }); -export const testCaseListSelector = selectorFamily({ +const testCaseAtom = atomFamily({ + key: "test-case-atom", + default: undefined, +}); +export const testCaseListSelector = selectorFamily({ key: "test-case-list-selector", + get: + ({ projectID, filters }) => + ({ get }) => + get(testCaseAtom({ projectID, filters })) || backendService.project(projectID).testCases.list(filters), + set: + ({ projectID, filters }) => + ({ set }, value) => + set(testCaseAtom({ projectID, filters }), value), +}); + +export const testCaseCountSelector = selectorFamily({ + key: "test-case-count-selector", get: ({ projectID, filters }: TestCaseListSelectorParams) => () => - backendService.project(projectID).testCases.list(filters), + backendService.project(projectID).testCases.count(filters), }); const TestCaseList = ({ @@ -50,7 +67,19 @@ const TestCaseList = ({ filters, disabled, }: TestCaseListProps) => { - const testCases = useRecoilValue(testCaseListSelector({ projectID, filters })); + const [loadingMore, setLoadingMore] = useState(false); + const [testCases, setTestCases] = useRecoilState(testCaseListSelector({ projectID, filters })); + + const handleLoadMoreClick = async () => { + setLoadingMore(true); + try { + setTestCases(await backendService.project(projectID).testCases.list(filters, testCases.length)); + } finally { + setLoadingMore(false); + } + }; + const testCaseCount = useRecoilValue(testCaseCountSelector({ projectID, filters })); + console.info(testCaseCount); const [selectedTestCaseID, setSelectedTestCaseID] = useQueryStringState("selected", testCases[0]?.id); return ( @@ -69,6 +98,15 @@ const TestCaseList = ({ /> ))} + {testCases.length < testCaseCount && ( + + Load more + + )}
    ); diff --git a/ui/src/tailwind.config.js b/ui/src/tailwind.config.js index d2f7d694..d730c7ef 100644 --- a/ui/src/tailwind.config.js +++ b/ui/src/tailwind.config.js @@ -30,7 +30,7 @@ module.exports = { sans: ["Noto Sans", "sans-serif"], }, fontSize: { - base: "15px", + base: "16px", sm: "12px", lg: "28px", xl: "34px", From 406c4baece467663c0c5fd22467a783f3cf90bf5 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Tue, 28 Sep 2021 22:57:06 +1000 Subject: [PATCH 12/17] LoadMore quick fix --- ui/src/src/testcases/TestCaseList.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/src/src/testcases/TestCaseList.tsx b/ui/src/src/testcases/TestCaseList.tsx index e0461567..16c45230 100644 --- a/ui/src/src/testcases/TestCaseList.tsx +++ b/ui/src/src/testcases/TestCaseList.tsx @@ -73,13 +73,16 @@ const TestCaseList = ({ const handleLoadMoreClick = async () => { setLoadingMore(true); try { - setTestCases(await backendService.project(projectID).testCases.list(filters, testCases.length)); + setTestCases([ + ...testCases, + ...(await backendService.project(projectID).testCases.list(filters, testCases.length)), + ]); } finally { setLoadingMore(false); } }; const testCaseCount = useRecoilValue(testCaseCountSelector({ projectID, filters })); - console.info(testCaseCount); + const [selectedTestCaseID, setSelectedTestCaseID] = useQueryStringState("selected", testCases[0]?.id); return ( From 0cfd0eb9538c9aabae21ed06185906f951904d27 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Tue, 28 Sep 2021 23:50:31 +1000 Subject: [PATCH 13/17] Attributes fix --- ui/src/src/domain/index.ts | 5 +++++ ui/src/src/services/backend.ts | 23 +++++++++++++++++++---- ui/src/src/testcases/TestCasesPanel.tsx | 9 +++++++-- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/ui/src/src/domain/index.ts b/ui/src/src/domain/index.ts index 21659a7f..13ea23a5 100644 --- a/ui/src/src/domain/index.ts +++ b/ui/src/src/domain/index.ts @@ -27,6 +27,8 @@ export type ExistingAttribute = WithID & readonly values: AttributeValue[]; }; +export const isExistingAttribute = (attribute: ExistingAttribute | FakeAttribute): attribute is ExistingAttribute => + !!(attribute as ExistingAttribute).createdTime; export type FakeAttribute = WithID & { readonly name: "Broken"; readonly values: AttributeValue[]; @@ -40,6 +42,9 @@ export type AttributeFilterDraft = { readonly values: string[]; }; export type ExistingAttributeFilter = AttributeFilter & DeletedMeta & TimeMeta; +export const isExistingAttributeFilter = ( + filter: ExistingAttributeFilter | AttributeFilterDraft, +): filter is ExistingAttributeFilter => !!(filter as ExistingAttributeFilter).createdTime; export interface Suite { readonly name: string; diff --git a/ui/src/src/services/backend.ts b/ui/src/src/services/backend.ts index c7f4fda5..bf4a898d 100644 --- a/ui/src/src/services/backend.ts +++ b/ui/src/src/services/backend.ts @@ -13,6 +13,8 @@ import { AttributeFilterDraft, ExistingTestCase, RootTestCaseGroup, + isExistingAttributeFilter, + isExistingAttribute, } from "../domain"; export const getApiBaseUrl = (url: string) => (process.env.REACT_APP_BASE_API_URL || "/api/") + url; @@ -61,7 +63,23 @@ type ServerAttribute = Meta & readonly values: string[]; readonly attrValues: { readonly uuid: string; readonly value: string }[]; }; -const mapServerAttributeToClient = (attribute: ServerAttribute) => ({ + +type FakeServerAttribute = { + id: string; + readonly name: string; + readonly values: string[]; + readonly attrValues: { readonly uuid: string; readonly value: string }[]; +}; +export const mapClientAttributeToServer = ( + attribute: ExistingAttribute | FakeAttribute, +): ServerAttribute | FakeServerAttribute => ({ + ...(isExistingAttribute(attribute) ? copyMeta(attribute) : {}), + id: attribute.id, + name: attribute.name, + attrValues: attribute.values.map(({ id, name }) => ({ uuid: id, value: name })), + values: [], +}); +const mapServerAttributeToClient = (attribute: ServerAttribute): ExistingAttribute => ({ ...copyMeta(attribute), id: attribute.id, name: attribute.name, @@ -201,9 +219,6 @@ export const mapNewSuiteToServer = (suite: SuiteDraft): NewServerSuite => ({ notFields: {}, }, }); -const isExistingAttributeFilter = ( - filter: ExistingAttributeFilter | AttributeFilterDraft, -): filter is ExistingAttributeFilter => !!(filter as ExistingAttributeFilter).createdTime; const mapClientFilterToBackend = ( filter: ExistingAttributeFilter | AttributeFilterDraft, diff --git a/ui/src/src/testcases/TestCasesPanel.tsx b/ui/src/src/testcases/TestCasesPanel.tsx index c178eadf..22562f29 100644 --- a/ui/src/src/testcases/TestCasesPanel.tsx +++ b/ui/src/src/testcases/TestCasesPanel.tsx @@ -1,8 +1,9 @@ import Button from "../components/ui/Button"; -import { PropsWithChildren } from "react"; +import { PropsWithChildren, useMemo } from "react"; import PlayIcon from "@heroicons/react/solid/PlayIcon"; import { ExistingAttribute, FakeAttribute } from "../domain"; import TestCase from "./TestCase"; +import { mapClientAttributeToServer } from "../services/backend"; export type TestCasesPanelProps = PropsWithChildren<{ projectID: string; @@ -10,7 +11,11 @@ export type TestCasesPanelProps = PropsWithChildren<{ attributes: (ExistingAttribute | FakeAttribute)[]; }>; +const useLegacyAttributes = (attributes: (ExistingAttribute | FakeAttribute)[]) => + useMemo(() => attributes.map(mapClientAttributeToServer), [attributes]); + const TestCasesPanel = ({ children, selectedTestCaseID, projectID, attributes }: TestCasesPanelProps): JSX.Element => { + const legacyAttributes = useLegacyAttributes(attributes); return (
    @@ -25,7 +30,7 @@ const TestCasesPanel = ({ children, selectedTestCaseID, projectID, attributes }:
    {selectedTestCaseID && ( - + )}
    From 69b3d573269201e34ed61570d30385e8cc824400 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Wed, 29 Sep 2021 00:39:43 +1000 Subject: [PATCH 14/17] Save suite button fixes --- ui/src/src/components/ui/Dialog.tsx | 4 ++-- ui/src/src/index.css | 1 + ui/src/src/testcases/TestCasesFilters.tsx | 7 +++---- ui/src/src/testcases/TestCasesPanel.tsx | 13 ++++++++++--- ui/src/src/testcases/TestCasesScreen.tsx | 2 ++ .../src/testcases/TestCasesScreenStateless.tsx | 8 +++++++- ui/src/src/testcases/TestSuiteScreen.tsx | 16 +++++++--------- ui/src/tailwind.config.js | 4 ++-- 8 files changed, 34 insertions(+), 21 deletions(-) diff --git a/ui/src/src/components/ui/Dialog.tsx b/ui/src/src/components/ui/Dialog.tsx index 37d9c056..4591261f 100644 --- a/ui/src/src/components/ui/Dialog.tsx +++ b/ui/src/src/components/ui/Dialog.tsx @@ -25,7 +25,7 @@ const Dialog = ({ children, className, onOverlayClick, onClose, ...other }: Dial return (
    @@ -50,7 +50,7 @@ const Close = ({ className, ...other }: ButtonHTMLAttributes) )} {...other} > - + ); diff --git a/ui/src/src/index.css b/ui/src/src/index.css index 4b507db3..e3e73be1 100644 --- a/ui/src/src/index.css +++ b/ui/src/src/index.css @@ -5,6 +5,7 @@ @tailwind utilities; @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@300;400;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;700&display=swap'); body { margin: 0; diff --git a/ui/src/src/testcases/TestCasesFilters.tsx b/ui/src/src/testcases/TestCasesFilters.tsx index 99fc6554..c1fdeb7b 100644 --- a/ui/src/src/testcases/TestCasesFilters.tsx +++ b/ui/src/src/testcases/TestCasesFilters.tsx @@ -16,10 +16,9 @@ export type TestCasesFiltersProps = { disabled?: boolean; onChangeGroups: (value: string[]) => void; onChangeFilters: (value: (FilterValue | ExistingAttributeFilter)[]) => void; -} & ( - | { showSave: true; onSaveSuiteClick: () => void } - | { showSave?: false | undefined; onSaveSuiteClick?: () => void } -); + showSave?: boolean; + onSaveSuiteClick?: () => void; +}; const TestCasesFilter = ({ projectAttributes, diff --git a/ui/src/src/testcases/TestCasesPanel.tsx b/ui/src/src/testcases/TestCasesPanel.tsx index 22562f29..8273ab2b 100644 --- a/ui/src/src/testcases/TestCasesPanel.tsx +++ b/ui/src/src/testcases/TestCasesPanel.tsx @@ -1,9 +1,12 @@ import Button from "../components/ui/Button"; import { PropsWithChildren, useMemo } from "react"; import PlayIcon from "@heroicons/react/solid/PlayIcon"; +import PlusCircleIcon from "@heroicons/react/outline/PlusCircleIcon"; import { ExistingAttribute, FakeAttribute } from "../domain"; import TestCase from "./TestCase"; import { mapClientAttributeToServer } from "../services/backend"; +import { captionClasses } from "../components/ui/typography"; +import clsx from "clsx"; export type TestCasesPanelProps = PropsWithChildren<{ projectID: string; @@ -19,10 +22,14 @@ const TestCasesPanel = ({ children, selectedTestCaseID, projectID, attributes }: return (
    -
    -

    Test cases

    +
    +

    Test cases

    + + + Add + - + Launch
    diff --git a/ui/src/src/testcases/TestCasesScreen.tsx b/ui/src/src/testcases/TestCasesScreen.tsx index d4dc35d2..4d5b1ca7 100644 --- a/ui/src/src/testcases/TestCasesScreen.tsx +++ b/ui/src/src/testcases/TestCasesScreen.tsx @@ -51,6 +51,8 @@ const TextCasesScreen: FunctionComponent = ({ projectID }) onChangeFilters={handleChangeFilters} onChangeGroups={handleChangeGroups} onToggleTestCase={toggleTestCase} + showSaveSuite + onSaveSuiteClick={() => setShowSaveDialog("true")} /> ); diff --git a/ui/src/src/testcases/TestCasesScreenStateless.tsx b/ui/src/src/testcases/TestCasesScreenStateless.tsx index d767981f..2f3ec78d 100644 --- a/ui/src/src/testcases/TestCasesScreenStateless.tsx +++ b/ui/src/src/testcases/TestCasesScreenStateless.tsx @@ -19,6 +19,8 @@ export type TestCasesScreenStatelessProps = { projectID: string; isTestCaseSelected: (id: string) => boolean; onToggleTestCase: (id: string) => void; + showSaveSuite?: boolean; + onSaveSuiteClick?: () => void; }; export const TestCasesScreenStateless = ({ @@ -30,10 +32,12 @@ export const TestCasesScreenStateless = ({ exclusionState, beforeFilters, disableFilters, + showSaveSuite, + onSaveSuiteClick, ...other }: TestCasesScreenStatelessProps) => (
    -
    +
    {beforeFilters} ({ +const suiteAtom = atomFamily({ key: "existing-suite", default: undefined, }); -const suiteSelector = selectorFamily({ +const suiteSelector = selectorFamily({ key: "suite", get: ({ projectID, suiteID }) => @@ -35,15 +33,15 @@ const suiteSelector = selectorFamily({ }, }); -const useSaveSuite = (suiteState: RecoilState, projectID: string, suiteID: string) => { +const useSaveSuite = (suiteState: RecoilState, params: TestSuiteScreenProps) => { const [suite, setSuite] = useRecoilState(suiteState); const [isSaving, setIsSaving] = useState(false); const save = async (updatedSuite: Partial) => { setIsSaving(true); try { const savedSuite = await backendService - .project(projectID) - .testSuites.single(suiteID) + .project(params.projectID) + .testSuites.single(params.suiteID) .update({ ...suite, ...updatedSuite }); setSuite(savedSuite); } finally { @@ -59,7 +57,7 @@ const TestSuiteScreen: FunctionComponent = ({ projectID, s const suiteState = suiteSelector(selectorParams); const suite = useRecoilValue(suiteState); - const [saveSuite, isSaving] = useSaveSuite(suiteState, projectID, suiteID); + const [saveSuite, isSaving] = useSaveSuite(suiteState, selectorParams); const exclusionHash = useMemo(() => listToBoolHash(suite.excludedTestCases), [suite.excludedTestCases]); const isTestCaseSelected = (id: string) => !exclusionHash[id]; const setExcludedTestCasesHandler = useCallback( diff --git a/ui/src/tailwind.config.js b/ui/src/tailwind.config.js index d730c7ef..63a73a11 100644 --- a/ui/src/tailwind.config.js +++ b/ui/src/tailwind.config.js @@ -27,11 +27,11 @@ module.exports = { purple: "#8C54FF", }, fontFamily: { - sans: ["Noto Sans", "sans-serif"], + sans: ["Rubik", "Noto Sans", "sans-serif"], }, fontSize: { base: "16px", - sm: "12px", + sm: "13px", lg: "28px", xl: "34px", xxl: "48px", From 4cfa5bdef56181a56b331d6cec91cf1911fa0416 Mon Sep 17 00:00:00 2001 From: Nikolay Karev Date: Thu, 7 Oct 2021 20:38:35 +1100 Subject: [PATCH 15/17] Adding test cases --- ui/src/.storybook/preview.js | 1 + ui/src/package.json | 16 +- ui/src/src/App.js | 4 +- ui/src/src/components/suite/Form.tsx | 4 +- ui/src/src/components/testcase/Dialog.tsx | 34 + ui/src/src/components/testcase/Form.tsx | 71 ++ .../ui/{Filter.tsx => Attribute.tsx} | 13 +- .../ui/{Filters.tsx => Attributes.tsx} | 20 +- ui/src/src/components/ui/Button.tsx | 6 +- ui/src/src/components/ui/Checkbox.tsx | 31 +- ui/src/src/components/ui/Dialog.tsx | 6 +- ui/src/src/components/ui/ListBox.tsx | 12 +- ui/src/src/components/ui/typography.ts | 2 +- ui/src/src/domain/index.ts | 20 +- ui/src/src/index.css | 12 +- ui/src/src/services/backend.ts | 80 ++- .../{Form.stories.js => Form.stories.tsx} | 4 +- ...der.stories.js => SuiteHeader.stories.tsx} | 3 +- .../components/testcase/Form.stories.tsx | 29 + .../components/testcase/List.stories.tsx | 35 + ...lter.stories.tsx => Attribute.stories.tsx} | 8 +- .../components/ui/Attributes.stories.tsx | 20 + .../stories/components/ui/Filters.stories.tsx | 19 - .../ui/{attributes.js => attributes.ts} | 0 ui/src/src/testcases/SuiteHeader.tsx | 2 +- ui/src/src/testcases/TestCaseList.tsx | 15 +- ui/src/src/testcases/TestCaseTree.tsx | 55 +- ui/src/src/testcases/TestCasesFilters.tsx | 16 +- ui/src/src/testcases/TestCasesPanel.tsx | 73 +- ui/src/src/testcases/TestCasesScreen.tsx | 2 +- .../testcases/TestCasesScreenStateless.tsx | 2 +- ui/src/src/testcases/TestSuiteScreen.tsx | 2 +- ui/src/tailwind.config.js | 2 +- ui/src/yarn.lock | 648 +++++++++--------- 34 files changed, 815 insertions(+), 452 deletions(-) create mode 100644 ui/src/src/components/testcase/Dialog.tsx create mode 100644 ui/src/src/components/testcase/Form.tsx rename ui/src/src/components/ui/{Filter.tsx => Attribute.tsx} (92%) rename ui/src/src/components/ui/{Filters.tsx => Attributes.tsx} (71%) rename ui/src/src/stories/components/suite/{Form.stories.js => Form.stories.tsx} (73%) rename ui/src/src/stories/components/suite/{SuiteHeader.stories.js => SuiteHeader.stories.tsx} (73%) create mode 100644 ui/src/src/stories/components/testcase/Form.stories.tsx rename ui/src/src/stories/components/ui/{Filter.stories.tsx => Attribute.stories.tsx} (57%) create mode 100644 ui/src/src/stories/components/ui/Attributes.stories.tsx delete mode 100644 ui/src/src/stories/components/ui/Filters.stories.tsx rename ui/src/src/stories/components/ui/{attributes.js => attributes.ts} (100%) diff --git a/ui/src/.storybook/preview.js b/ui/src/.storybook/preview.js index 3cc1e409..fa526928 100644 --- a/ui/src/.storybook/preview.js +++ b/ui/src/.storybook/preview.js @@ -1,4 +1,5 @@ import '../src/index.css'; +import 'tailwindcss/tailwind.css'; export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, diff --git a/ui/src/package.json b/ui/src/package.json index 8f9bdbb4..968b51a4 100644 --- a/ui/src/package.json +++ b/ui/src/package.json @@ -104,20 +104,20 @@ }, "devDependencies": { "@craco/craco": "^6.3.0", - "@storybook/addon-actions": "^6.3.8", - "@storybook/addon-essentials": "^6.3.8", - "@storybook/addon-links": "^6.3.8", + "@storybook/addon-actions": "^6.3.10", + "@storybook/addon-essentials": "^6.3.10", + "@storybook/addon-links": "^6.3.10", "@storybook/addon-postcss": "^2.0.0", - "@storybook/builder-webpack5": "^6.3.8", - "@storybook/manager-webpack5": "^6.3.8", - "@storybook/node-logger": "^6.3.8", - "@storybook/react": "^6.3.8", + "@storybook/builder-webpack5": "^6.3.10", + "@storybook/manager-webpack5": "^6.3.10", + "@storybook/node-logger": "^6.3.10", + "@storybook/react": "^6.3.10", "@types/react-router": "^5.1.16", "autoprefixer": "^9", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "prettier": "^2.4.0", - "storybook": "^6.3.8", + "storybook": "^6.3.10", "tailwindcss": "npm:@tailwindcss/postcss7-compat", "typescript": "^4.4.3" } diff --git a/ui/src/src/App.js b/ui/src/src/App.js index ccdcba6a..5cf488cc 100644 --- a/ui/src/src/App.js +++ b/ui/src/src/App.js @@ -1,8 +1,10 @@ import React, { Component } from "react"; +import "tailwindcss/tailwind.css"; +import "./App.css"; import Header from "./common/Header"; import Main from "./common/Main"; import Footer from "./common/Footer"; -import "./App.css"; + import { library } from "@fortawesome/fontawesome-svg-core"; import { faStroopwafel } from "@fortawesome/free-solid-svg-icons"; import { RecoilRoot } from "recoil"; diff --git a/ui/src/src/components/suite/Form.tsx b/ui/src/src/components/suite/Form.tsx index 605c92d6..42570533 100644 --- a/ui/src/src/components/suite/Form.tsx +++ b/ui/src/src/components/suite/Form.tsx @@ -23,11 +23,11 @@ const Form = ({ initialValues, initialFocus, onSubmit, onCancel, ...other }: For {({ values, errors, handleSubmit, handleChange, handleBlur }) => (
    -

    Save a new test case

    +

    Save a new test suite

    Your new test suite requires a name. Please choose something meaningful and easy to remember.

    -