diff --git a/packages/components/.eslintrc.js b/packages/components/.eslintrc.js new file mode 100644 index 0000000000..d592a653c8 --- /dev/null +++ b/packages/components/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], + rules: { + // TODO: add prop types to JS and remove + 'react/prop-types': 0, + 'jest/expect-expect': 0, + }, +}; diff --git a/packages/components/.npmrc b/packages/components/.npmrc new file mode 100644 index 0000000000..214c29d139 --- /dev/null +++ b/packages/components/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md new file mode 100644 index 0000000000..fc61083340 --- /dev/null +++ b/packages/components/CHANGELOG.md @@ -0,0 +1,358 @@ +# @backstage/core + +## 0.7.0 + +### Minor Changes + +- 4c049a1a1: - Adds onClick and other props to IconLinkVertical; + + - Allows TriggerButton component to render when pager duty key is missing; + - Refactors TriggerButton and PagerDutyCard not to have shared state; + - Removes the `action` prop of the IconLinkVertical component while adding `onClick`. + + Instead of having an action including a button with onClick, now the whole component can be clickable making it easier to implement and having a better UX. + + Before: + + ```ts + const myLink: IconLinkVerticalProps = { + label: 'Click me', + action: + + + + Secondary Button: + Used for actions that cancel, skip, and in general perform negative + functions, etc. +
+
color="secondary" variant="contained"
+
+ + +
+ + + Tertiary Button: + Used commonly in a ButtonGroup and when the button function itself is + not a primary function on a page. +
+
color="default" variant="outlined"
+
+ + +
+ + ); +}; + +export const ButtonLinks = () => { + const routeRef = createRouteRef({ + path: '/hello', + title: 'Hi there!', + }); + + const handleClick = () => { + return 'Your click worked!'; + }; + + return ( + <> + + { + // TODO: Refactor to use new routing mechanisms + } + + +   has props for both Material-UI's component as well as for + react-router-dom's Route object. + + + + +   links to a statically defined route. In general, this should be + avoided. + + + + + View URL + +   links to a defined URL using Material-UI's Button. + + + + + Trigger Event + +   triggers an onClick event using Material-UI's Button. + + + + ); +}; diff --git a/packages/components/src/components/Button/Button.test.tsx b/packages/components/src/components/Button/Button.test.tsx new file mode 100644 index 0000000000..8bae5f2767 --- /dev/null +++ b/packages/components/src/components/Button/Button.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, fireEvent, act } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { Button } from './Button'; +import { Route, Routes } from 'react-router'; + +describe(' + , + ), + ); + + expect(() => getByText(testString)).toThrow(); + await act(async () => { + fireEvent.click(getByText(buttonLabel)); + }); + expect(getByText(testString)).toBeInTheDocument(); + }); +}); diff --git a/packages/components/src/components/Button/Button.tsx b/packages/components/src/components/Button/Button.tsx new file mode 100644 index 0000000000..ca45b3da7f --- /dev/null +++ b/packages/components/src/components/Button/Button.tsx @@ -0,0 +1,30 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ComponentProps } from 'react'; +import { Button as MaterialButton } from '@material-ui/core'; +import { Link as RouterLink } from 'react-router-dom'; + +type Props = ComponentProps & + ComponentProps; + +/** + * Thin wrapper on top of material-ui's Button component + * Makes the Button to utilise react-router + */ +export const Button = React.forwardRef((props, ref) => ( + +)); diff --git a/packages/components/src/components/Button/index.ts b/packages/components/src/components/Button/index.ts new file mode 100644 index 0000000000..7b584ed799 --- /dev/null +++ b/packages/components/src/components/Button/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Button } from './Button'; diff --git a/packages/components/src/components/CheckboxTree/CheckboxTree.stories.tsx b/packages/components/src/components/CheckboxTree/CheckboxTree.stories.tsx new file mode 100644 index 0000000000..db590e6524 --- /dev/null +++ b/packages/components/src/components/CheckboxTree/CheckboxTree.stories.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState } from 'react'; +import { CheckboxTree } from '.'; + +const CHECKBOX_TREE_ITEMS = [ + { + label: 'Genereic subcategory name 1', + options: [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + ], + }, + { + label: 'Genereic subcategory name 2', + options: [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + ], + }, + { + label: 'Genereic subcategory name 3', + options: [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + ], + }, +]; + +export default { + title: 'Inputs/CheckboxTree', + component: CheckboxTree, +}; + +export const Default = () => ( + {}} + label="default" + subCategories={CHECKBOX_TREE_ITEMS} + /> +); + +export const DynamicTree = () => { + function generateTree(showMore: boolean = false) { + const t = [ + { + label: 'Show more', + options: [], + }, + ]; + + if (showMore) { + t.push({ + label: 'More', + options: [], + }); + } + + return t; + } + + const [tree, setTree] = useState(generateTree()); + + return ( + { + setTree(generateTree(state.some(c => c.category === 'Show more'))); + }} + label="default" + subCategories={tree} + /> + ); +}; diff --git a/packages/components/src/components/CheckboxTree/CheckboxTree.test.tsx b/packages/components/src/components/CheckboxTree/CheckboxTree.test.tsx new file mode 100644 index 0000000000..f5eaa0e58b --- /dev/null +++ b/packages/components/src/components/CheckboxTree/CheckboxTree.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; + +import { CheckboxTree } from '.'; + +const CHECKBOX_TREE_ITEMS = [ + { + label: 'Genereic subcategory name 1', + options: [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + ], + }, +]; + +const minProps = { + onChange: jest.fn(), + label: 'Default', + subCategories: CHECKBOX_TREE_ITEMS, +}; + +describe('', () => { + it('renders without exploding', async () => { + const { getByText, getByTestId } = render(); + + expect(getByText('Genereic subcategory name 1')).toBeInTheDocument(); + const checkbox = await getByTestId('expandable'); + + // Simulate click on expandable arrow + fireEvent.click(checkbox); + + // Simulate click on option + const option = getByText('Option 1'); + expect(getByText('Option 1')).toBeInTheDocument(); + fireEvent.click(option); + expect(minProps.onChange).toHaveBeenCalled(); + }); +}); diff --git a/packages/components/src/components/CheckboxTree/CheckboxTree.tsx b/packages/components/src/components/CheckboxTree/CheckboxTree.tsx new file mode 100644 index 0000000000..d6d3410913 --- /dev/null +++ b/packages/components/src/components/CheckboxTree/CheckboxTree.tsx @@ -0,0 +1,358 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable guard-for-in */ +import { + Checkbox, + Collapse, + List, + ListItem, + ListItemIcon, + ListItemText, + Typography, +} from '@material-ui/core'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import ExpandLess from '@material-ui/icons/ExpandLess'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import produce from 'immer'; +import { isEqual } from 'lodash'; +import React, { useEffect, useReducer } from 'react'; +import { usePrevious } from 'react-use'; + +type IndexedObject = { + [key: string]: T; +}; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '100%', + minWidth: 10, + maxWidth: 360, + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent', + }, + '&:active': { + animation: 'none', + transform: 'none', + }, + }, + nested: { + paddingLeft: theme.spacing(5), + height: '32px', + '&:hover': { + backgroundColor: 'transparent', + }, + }, + listItemIcon: { + minWidth: 10, + }, + listItem: { + '&:hover': { + backgroundColor: 'transparent', + }, + }, + text: { + '& span, & svg': { + fontWeight: 'normal', + fontSize: 14, + }, + }, + }), +); + +/* SUB_CATEGORY */ + +type SubCategory = { + label: string; + isChecked?: boolean; + isOpen?: boolean; + options?: Option[]; +}; + +type SubCategoryWithIndexedOptions = { + label: string; + isChecked?: boolean; + isOpen?: boolean; + options: IndexedObject