diff --git a/__tests__/__snapshots__/storyshots.test.js.snap b/__tests__/__snapshots__/storyshots.test.js.snap index 0b26c05ad..c996240ea 100644 --- a/__tests__/__snapshots__/storyshots.test.js.snap +++ b/__tests__/__snapshots__/storyshots.test.js.snap @@ -1,5 +1,122 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Storyshots Component/QueryInfo Basic 1`] = ` +
+
+
+ +
+ Update the module successfully! +
+
+`; + +exports[`Storyshots Component/QueryInfo Basic With Dismiss 1`] = ` +
+
+`; + +exports[`Storyshots Component/QueryInfo Error 1`] = ` +
+
+
+ +
+ An error occurred. Please try again. +
+
+`; + +exports[`Storyshots Component/QueryInfo Error With Dismiss 1`] = ` +
+
+`; + +exports[`Storyshots Component/QueryInfo With Loading 1`] = ` +
+
+ + Sending the request... + +
+`; + exports[`Storyshots Components/AdditionalResources Basic 1`] = `
+ -
-
, - +`; + +exports[`Storyshots Components/DropdownMenu With Separators 1`] = ` +
+ +
+`; + +exports[`Storyshots Components/Error Internal 1`] = ` +Array [ + ,
+
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
, -
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
, -
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
, -
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
, -
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
, -
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
, -] -`; - -exports[`Storyshots Components/DropdownMenu Directions 1`] = ` -
-
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
-
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
-
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
-
-
-
- -
-
-
-
- - Lessons - -
-
- - Users - -
-
- - Alerts0 - -
-
-
-
-`; - -exports[`Storyshots Components/DropdownMenu With Separators 1`] = ` -Array [ -
-
- -
-
, -
-
- - Lessons - -
-
-
- - Users - -
-
-
- - Alerts1 - -
-
- - Alerts2 - -
-
- - Alerts3 - -
-
-
- - Alerts4 - -
-
, -] -`; - -exports[`Storyshots Components/Error Internal 1`] = ` -Array [ - , -
-
- -

500 Error! @@ -7609,47 +6965,40 @@ exports[`Storyshots Components/FormCard Basic 1`] = ` Swiggity swag
-
- -
-
-
-
- -
- + Lolzz +

Continue to dashboard diff --git a/components/DropdownMenu.test.js b/components/DropdownMenu.test.js index 7b67e6a7b..97c4f130e 100644 --- a/components/DropdownMenu.test.js +++ b/components/DropdownMenu.test.js @@ -2,6 +2,9 @@ import React from 'react' import { render, fireEvent, screen, waitFor } from '@testing-library/react' import { DropdownMenu } from './DropdownMenu' +// Imported to be able to use expect(...).toBeInTheDocument() +import '@testing-library/jest-dom' + const dropdownMenuItems = [ { title: 'Lessons', path: '/admin/lessons', as: 'button' }, null, @@ -22,6 +25,12 @@ describe('MdInput Component', () => { expect(container).toMatchSnapshot() }) + test('Should display default title when no title is passed', () => { + const { queryByText } = render() + + expect(queryByText('None')).toBeInTheDocument() + }) + test('Should change value of testBtnOnClick upon click', () => { dropdownMenuItems[0].onClick = val => (testBtnOnClick = val) diff --git a/components/DropdownMenu.tsx b/components/DropdownMenu.tsx index e3d38e3f1..3352d0a36 100644 --- a/components/DropdownMenu.tsx +++ b/components/DropdownMenu.tsx @@ -1,8 +1,8 @@ -import React from 'react' -import DropdownButton from 'react-bootstrap/DropdownButton' +import React, { useState } from 'react' import Dropdown from 'react-bootstrap/Dropdown' import styles from '../scss/dropDown.module.scss' import { DropDirection } from 'react-bootstrap/esm/DropdownContext' +import { ChevronRightIcon } from '@primer/octicons-react' //a null item indicates a dropdown divider export type Item = { @@ -14,62 +14,49 @@ export type Item = { type DropDownMenuProps = { drop?: DropDirection - items: Item[] - title: string - size?: 'sm' | 'lg' | undefined - variant?: - | 'primary' - | 'secondary' - | 'success' - | 'info' - | 'warning' - | 'danger' - | 'none' + items?: Item[] | null + title?: string //changes the underlying component CSS base class name //https://react-bootstrap.github.io/components/dropdowns/#api bsPrefix?: string } +const ChevronRight = () => + export const DropdownMenu: React.FC = ({ - drop = 'down', - variant = 'none', - title, - size, items, - bsPrefix + title, + bsPrefix = '' }) => { - const menuItems = items.map((item: Item, itemsIndex: number) => - !item ? ( - - ) : ( -
- item.onClick && item.onClick(item.title)} - bsPrefix={bsPrefix} - > - {item.title} - -
- ) - ) + const [activeItem, setActiveItem] = useState({ title }) return ( - <> -
- - {menuItems} - -
-
{menuItems}
- + + + {activeItem.title || 'None'} + + + + + {items?.map((item, index) => + item ? ( + { + item?.onClick?.(item) + + setActiveItem({ + title: item?.title + }) + }} + > + {item?.title} + + ) : ( + + ) + )} + + ) } diff --git a/components/QueryInfo/QueryInfo.test.js b/components/QueryInfo/QueryInfo.test.js new file mode 100644 index 000000000..92e1e9181 --- /dev/null +++ b/components/QueryInfo/QueryInfo.test.js @@ -0,0 +1,95 @@ +import React from 'react' +import QueryInfo from './' +import { render, screen } from '@testing-library/react' + +// Imported to be able to use .toBeInTheDocument() +import '@testing-library/jest-dom' + +describe('QueryInfo component', () => { + it('should display successful state', () => { + expect.assertions(1) + + render( + + ) + + expect(screen.getByText('Submitted successfully')).toBeInTheDocument() + }) + + it('should display loading state', () => { + expect.assertions(1) + + render( + + ) + + expect(screen.getByText('Loading message...')).toBeInTheDocument() + }) + + it('should display error state', () => { + expect.assertions(1) + + render( + + ) + + expect( + screen.getByText('An error occurred. Please try again.') + ).toBeInTheDocument() + }) + + it('should render nothing when there is no data, error, and loading', () => { + expect.assertions(2) + + render( + + ) + + expect( + screen.queryByText('Submitted the item successfully!') + ).not.toBeInTheDocument() + expect(screen.queryByText('Loading message...')).not.toBeInTheDocument() + }) +}) diff --git a/components/QueryInfo/QueryInfo.tsx b/components/QueryInfo/QueryInfo.tsx new file mode 100644 index 000000000..37ebdc71f --- /dev/null +++ b/components/QueryInfo/QueryInfo.tsx @@ -0,0 +1,67 @@ +import { get } from 'lodash' +import React from 'react' +import { Spinner } from 'react-bootstrap' +import styles from '../../scss/queryInfo.module.scss' +import Alert from '../Alert' + +type QueryInfo = { + data: T + loading: boolean + error: string + texts: { + loading: string + data: string + } + dismiss?: { + onDismissError?: (id: number) => void + onDismissData?: (id: number) => void + } +} + +const QueryInfo = ({ + data, + loading, + error, + texts, + dismiss +}: QueryInfo) => { + if (loading) { + return ( +
+ + {texts.loading} +
+ ) + } + + if (error) { + return ( + + ) + } + + if (data) { + return ( + + ) + } + + return <> +} + +export default QueryInfo diff --git a/components/QueryInfo/index.tsx b/components/QueryInfo/index.tsx new file mode 100644 index 000000000..b2454455e --- /dev/null +++ b/components/QueryInfo/index.tsx @@ -0,0 +1 @@ +export { default } from './QueryInfo' diff --git a/components/__snapshots__/DropdownMenu.test.js.snap b/components/__snapshots__/DropdownMenu.test.js.snap index 40b253685..98d6b3b3e 100644 --- a/components/__snapshots__/DropdownMenu.test.js.snap +++ b/components/__snapshots__/DropdownMenu.test.js.snap @@ -3,102 +3,67 @@ exports[`MdInput Component Should change value of testBtnOnClick upon click 1`] = `
- -
-
@@ -107,60 +72,31 @@ exports[`MdInput Component Should change value of testBtnOnClick upon click 1`] exports[`MdInput Component Should render divider when an item is null 1`] = `
- -
-
-
- -
- -
- -
-
- -
+ + +
`; diff --git a/components/admin/alerts/__snapshots__/AdminNewAlert.test.js.snap b/components/admin/alerts/__snapshots__/AdminNewAlert.test.js.snap index 6eb9444f0..53361350f 100644 --- a/components/admin/alerts/__snapshots__/AdminNewAlert.test.js.snap +++ b/components/admin/alerts/__snapshots__/AdminNewAlert.test.js.snap @@ -79,43 +79,31 @@ exports[`AdminLewAlert component Should create new alert 1`] = ` Type
- -
-
-
- -
-
- -
+ + +
- -
-
-
- -
- + + +
{ it('Should add module', async () => { expect.assertions(1) - const { getByText, getByTestId, container } = render( + const { getByText, getByTestId } = render( { await waitFor(() => expect( - container.querySelector('.octicon-check-circle') + getByText('Added the item Functions successfully!') ).toBeInTheDocument() ) }) @@ -142,7 +142,7 @@ describe('AdminLessonInputs component', () => { it('Should update module', async () => { expect.assertions(1) - const { getByText, getByTestId, container } = render( + const { getByText, getByTestId } = render( { await waitFor(() => expect( - container.querySelector('.octicon-check-circle') + getByText('Updated the item Functions successfully!') ).toBeInTheDocument() ) }) @@ -176,7 +176,7 @@ describe('AdminLessonInputs component', () => { it('Should display error message if inputs are empty', async () => { expect.assertions(1) - const { getByText, getByTestId, container } = render( + const { getByText, getByTestId } = render( @@ -189,14 +189,16 @@ describe('AdminLessonInputs component', () => { await userEvent.click(submit) await waitFor(() => - expect(container.querySelector('.octicon-alert-fill')).toBeInTheDocument() + expect( + getByText('An error occurred. Please try again.') + ).toBeInTheDocument() ) }) it('Should display error message if network or GraphQL error', async () => { expect.assertions(1) - const { getByText, getByTestId, container } = render( + const { getByText, getByTestId } = render( @@ -213,7 +215,9 @@ describe('AdminLessonInputs component', () => { await userEvent.click(submit) await waitFor(() => - expect(container.querySelector('.octicon-alert-fill')).toBeInTheDocument() + expect( + getByText('An error occurred. Please try again.') + ).toBeInTheDocument() ) }) @@ -458,7 +462,66 @@ describe('AdminLessonInputs component', () => { await userEvent.click(submit) await waitFor(() => - expect(getByText('Updated the module successfully!')).toBeInTheDocument() + expect(getByText('Updated the item successfully!')).toBeInTheDocument() + ) + }) + + it('Should dismiss success message', async () => { + expect.assertions(1) + + const { getByText, getByTestId, queryByText, getByLabelText } = render( + + {}} + /> + + ) + + await userEvent.type(getByTestId('input0'), 'Functions', { + delay: 1 + }) + await userEvent.type(getByTestId('input2'), '1', { + delay: 1 + }) + await userEvent.type(getByTestId('textbox'), 'Functions are cool', { + delay: 1 + }) + + const submit = getByText('ADD MODULE') + await userEvent.click(submit) + + await userEvent.click(getByLabelText('Close alert')) + + await waitFor(() => + expect( + queryByText('Added the item Functions successfully!') + ).not.toBeInTheDocument() + ) + }) + + it('Should dismiss error message', async () => { + expect.assertions(1) + + const { getByText, getByTestId, queryByText, getByLabelText } = render( + + + + ) + + await userEvent.clear(getByTestId('input0')) + await userEvent.clear(getByTestId('textbox')) + + const submit = getByText('ADD MODULE') + await userEvent.click(submit) + + await userEvent.click(getByLabelText('Close alert')) + + await waitFor(() => + expect( + queryByText('An error occurred. Please try again.') + ).not.toBeInTheDocument() ) }) }) diff --git a/components/admin/lessons/AdminLessonInputs/AdminLessonInputs.tsx b/components/admin/lessons/AdminLessonInputs/AdminLessonInputs.tsx index 19fb6b995..36f79f479 100644 --- a/components/admin/lessons/AdminLessonInputs/AdminLessonInputs.tsx +++ b/components/admin/lessons/AdminLessonInputs/AdminLessonInputs.tsx @@ -7,15 +7,14 @@ import { } from '../../../../graphql' import { formChange } from '../../../../helpers/formChange' import { FormCard, MD_INPUT, Option, TextField } from '../../../FormCard' -import { AlertFillIcon, CheckCircleIcon } from '@primer/octicons-react' import styles from './adminLessonInputs.module.scss' -import { Spinner } from 'react-bootstrap' -import { get } from 'lodash' +import { get, isEqual } from 'lodash' import { ApolloError, OperationVariables, ApolloQueryResult } from '@apollo/client' +import QueryInfo from '../../../QueryInfo' type Module = { id: number; name: string; content: string; order: number } @@ -96,6 +95,12 @@ const AdminModuleInputs = ({ }) : useAddModuleMutation({ variables: mutationVariables }) + const [dataDiff, setDataDiff] = useState(data) + useEffect( + () => setDataDiff(prev => (isEqual(data, prev) ? undefined : data)), + [data] + ) + const [errorMsg, setErrorMsg] = useState(get(error, 'message', '')) const handleChange = async (value: string, propertyIndex: number) => { @@ -148,49 +153,33 @@ const AdminModuleInputs = ({ } } - const QueryStateMessage = () => { - if (loading) { - return ( -
- - Adding the module... -
- ) - } - - if (errorMsg) { - return ( -
- - Failed to add the module: {errorMsg} -
- ) - } + const dataText = () => { + const updateModule = get(data, 'updateModule') + const addModule = get(data, 'addModule') - if (data) { - const updateModule = get(data, 'updateModule') - const addModule = get(data, 'addModule') - - return ( -
- - - {updateModule ? 'Updated' : 'Added'} the module{' '} - - {get(addModule, 'name') || get(updateModule, 'name') || ''} - {' '} - successfully! - -
- ) - } - - return <> + return `${updateModule ? 'Updated' : 'Added'} the item ${ + get(addModule, 'name') || get(updateModule, 'name') || '' + } successfully!` } return (
- + { + setErrorMsg('') + setDataDiff(undefined) + }, + onDismissData: () => {} + }} + /> ( - - Continue to dashboard - + + Continue to dashboard + ) diff --git a/scss/dropDown.module.scss b/scss/dropDown.module.scss index aefe3315d..e43d5da54 100644 --- a/scss/dropDown.module.scss +++ b/scss/dropDown.module.scss @@ -2,6 +2,50 @@ @use '_variables.module.scss'; @import 'colors.module.scss'; +.dropdown { + display: flex; + column-gap: 13px; + padding: 8px 16px; + background: white; + border: 1px solid hsl(220, 13%, 91%); + box-shadow: 0px 1px 2px hsla(215, 28%, 17%, 0.08); + border-radius: 4px; + align-items: center; + color: variables.$dark; + + &:hover { + background-color: white; + border: 1px solid hsl(216, 4%, 75%); + color: variables.$dark; + } + + &:active, + &:focus, + &:focus:active { + background-color: hsla(0, 0%, 95%, 0.695); + box-shadow: none; + border: 1px solid hsl(216, 4%, 65%); + color: variables.$dark; + } + + & .dropdown__menu { + width: 100%; + } + + & svg { + transform: rotate(90deg); + transition: transform 0.15s ease-in; + } + + // Means the dropdown is opened + &[aria-expanded='true'] { + & svg { + transform: rotate(-90deg); + transition: transform 0.2s ease-out; + } + } +} + .nav-user-toggle { display: flex; text-decoration: none; diff --git a/scss/queryInfo.module.scss b/scss/queryInfo.module.scss new file mode 100644 index 000000000..2266b5054 --- /dev/null +++ b/scss/queryInfo.module.scss @@ -0,0 +1,22 @@ +@use '_variables.module'; + +@mixin message($bg, $color) { + display: flex; + align-items: center; + column-gap: 5px; + background-color: rgba($color: $bg, $alpha: 0.2); + padding: 0.75rem 1.25rem; + border-radius: 0.25rem; + margin-bottom: 0; + + @if $color { + color: $color; + border: 1px solid rgba($color: $color, $alpha: 0.2); + } @else { + color: $bg; + } +} + +.loading { + @include message(variables.$mute, variables.$mute); +} diff --git a/stories/components/DropdownMenu.stories.tsx b/stories/components/DropdownMenu.stories.tsx index a8f638cd4..cb68afb0b 100644 --- a/stories/components/DropdownMenu.stories.tsx +++ b/stories/components/DropdownMenu.stories.tsx @@ -5,6 +5,7 @@ export default { component: DropdownMenu, title: 'Components/DropdownMenu' } + const dropdownMenuItems = [ { title: 'Lessons', path: '/admin/lessons' }, { title: 'Users', path: '/admin/users' }, @@ -23,15 +24,6 @@ const separatedMenu = [ { title: 'Alerts4', path: '/admin/alerts' } ] -const variants: any[] = [ - 'Primary', - 'Success', - 'Danger', - 'Info', - 'Warning', - 'None' -] - export const Basic: React.FC = () => ( ) @@ -39,32 +31,3 @@ export const Basic: React.FC = () => ( export const _WithSeparators: React.FC = () => ( ) - -export const Colors = () => { - return variants.map((variant: any, i: number) => ( -
- -
- )) -} - -export const Directions = () => { - const directions = ['Left', 'Down', 'Up', 'Right'].map( - (direction: any, i: number) => ( -
- -
- ) - ) - - return
{directions}
-} diff --git a/stories/components/QueryInfo.stories.tsx b/stories/components/QueryInfo.stories.tsx new file mode 100644 index 000000000..48998597f --- /dev/null +++ b/stories/components/QueryInfo.stories.tsx @@ -0,0 +1,83 @@ +import React from 'react' +import QueryInfo from '../../components/QueryInfo' + +export default { + component: QueryInfo, + title: 'Component/QueryInfo' +} + +export const Basic = () => ( + +) + +export const BasicWithDismiss = () => ( + {} + }} + /> +) + +export const Error = () => ( + +) + +export const ErrorWithDismiss = () => ( + {} + }} + /> +) + +export const _WithLoading = () => ( + +)