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`] = `
-
-
-
- Lessons
-
-
-
-
-
- Users
-
-
-
-
- Alerts
-
-
+
+
+
`;
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
-
-
-
- info
-
-
-
-
- urgent
-
-
+
+
+
{
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 = () => (
+
+)