refactor: add markdown visual editor source (#1088)

This commit is contained in:
Johnny A
2022-06-11 17:08:15 -04:00
committed by GitHub
parent a6b9e992f8
commit 28566c9a43
89 changed files with 6207 additions and 792 deletions

View File

@@ -1,4 +0,0 @@
.env
coverage
node_modules
ext.json

View File

@@ -1,6 +0,0 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "es5",
"jsxSingleQuote": false
}

View File

@@ -6,18 +6,8 @@
"Standard Notes",
"Standard Notes Extensions"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"private": true,
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "https://github.com/standardnotes/advanced-checklist.git"
},
"bugs": {
"url": "https://github.com/standardnotes/advanced-checklist/issues"
},
"sn": {
"main": "build/index.html"
},
@@ -39,7 +29,7 @@
"server-public": "http-server -p 3000 --cors",
"server-root": "http-server ./ -p 3000 --cors",
"server": "http-server ./build -p 3000 --cors",
"pretty": "prettier --write 'src/**/*.{html,css,scss,js,jsx,ts,tsx,json}' README.md",
"lint": "prettier --write 'src/**/*.{html,css,scss,js,jsx,ts,tsx,json}' README.md",
"prepare": "husky install"
},
"eslintConfig": {

View File

@@ -5,10 +5,7 @@ import type { RootState, AppDispatch } from './store'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useDidMount = (
effect: React.EffectCallback,
deps?: React.DependencyList
) => {
export const useDidMount = (effect: React.EffectCallback, deps?: React.DependencyList) => {
const [didMount, setDidMount] = useState(false)
useEffect(() => {

View File

@@ -32,7 +32,7 @@ const actionsWithGroup = isAnyOf(
tasksGroupAdded,
tasksGroupDeleted,
tasksGroupMerged,
tasksGroupCollapsed
tasksGroupCollapsed,
)
listenerMiddleware.startListening({

View File

@@ -9,8 +9,7 @@ export const store = configureStore({
tasks: tasksReducer,
settings: settingsReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(listenerMiddleware),
middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddleware),
})
export type AppDispatch = typeof store.dispatch

View File

@@ -9,12 +9,8 @@ type CheckBoxInputProps = {
export const CheckBoxInput = forwardRef<HTMLInputElement, CheckBoxInputProps>(
({ checked, disabled, testId, onChange }, ref) => {
function onCheckBoxButtonClick({
currentTarget,
}: React.MouseEvent<SVGElement>) {
!checked
? currentTarget.classList.add('explode')
: currentTarget.classList.remove('explode')
function onCheckBoxButtonClick({ currentTarget }: React.MouseEvent<SVGElement>) {
!checked ? currentTarget.classList.add('explode') : currentTarget.classList.remove('explode')
}
return (
@@ -41,5 +37,5 @@ export const CheckBoxInput = forwardRef<HTMLInputElement, CheckBoxInputProps>(
</svg>
</label>
)
}
},
)

View File

@@ -17,10 +17,7 @@ type CircularProgressBarProps = {
percentage: number
}
export const CircularProgressBar: React.FC<CircularProgressBarProps> = ({
size,
percentage,
}) => {
export const CircularProgressBar: React.FC<CircularProgressBarProps> = ({ size, percentage }) => {
const [progress, setProgress] = useState(0)
useEffect(() => {
@@ -34,18 +31,8 @@ export const CircularProgressBar: React.FC<CircularProgressBarProps> = ({
const dash = (progress * circumference) / 100
return (
<svg
height={size}
viewBox={viewBox}
width={size}
data-testid="circular-progress-bar"
>
<ProgressBarBackground
cx={size / 2}
cy={size / 2}
r={radius}
strokeWidth={strokeWidth}
/>
<svg height={size} viewBox={viewBox} width={size} data-testid="circular-progress-bar">
<ProgressBarBackground cx={size / 2} cy={size / 2} r={radius} strokeWidth={strokeWidth} />
<ProgressBarStroke
cx={size / 2}
cy={size / 2}

View File

@@ -1,11 +1,7 @@
import '@reach/dialog/styles.css'
import React, { useRef } from 'react'
import {
AlertDialog,
AlertDialogLabel,
AlertDialogDescription,
} from '@reach/alert-dialog'
import { AlertDialog, AlertDialogLabel, AlertDialogDescription } from '@reach/alert-dialog'
import { sanitizeHtmlString } from '@standardnotes/utils'
@@ -32,11 +28,7 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
const cancelRef = useRef<HTMLButtonElement>(null)
return (
<AlertDialog
data-testid={testId}
onDismiss={cancelButtonCb}
leastDestructiveRef={cancelRef}
>
<AlertDialog data-testid={testId} onDismiss={cancelButtonCb} leastDestructiveRef={cancelRef}>
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">

View File

@@ -17,11 +17,7 @@ type MainTitleProps = {
crossed?: boolean
}
export const MainTitle: React.FC<MainTitleProps> = ({
children,
highlight = false,
crossed = false,
}) => {
export const MainTitle: React.FC<MainTitleProps> = ({ children, highlight = false, crossed = false }) => {
return (
<Header1 className={`sk-h1 ${highlight ? 'info' : ''}`} crossed={crossed}>
{children}

View File

@@ -3,11 +3,7 @@ type RoundButtonProps = {
onClick: () => void
}
export const RoundButton: React.FC<RoundButtonProps> = ({
testId,
onClick,
children,
}) => {
export const RoundButton: React.FC<RoundButtonProps> = ({ testId, onClick, children }) => {
return (
<button data-testid={testId} className="sn-icon-button" onClick={onClick}>
{children}

View File

@@ -26,24 +26,8 @@ type TextAreaInputProps = {
onKeyUp?: (event: KeyboardEvent<HTMLTextAreaElement>) => void
}
export const TextAreaInput = forwardRef<
HTMLTextAreaElement,
TextAreaInputProps
>(
(
{
value,
className,
dir = 'auto',
disabled,
spellCheck,
testId,
onChange,
onKeyPress,
onKeyUp,
},
ref
) => {
export const TextAreaInput = forwardRef<HTMLTextAreaElement, TextAreaInputProps>(
({ value, className, dir = 'auto', disabled, spellCheck, testId, onChange, onKeyPress, onKeyUp }, ref) => {
return (
<StyledTextArea
className={className}
@@ -58,5 +42,5 @@ export const TextAreaInput = forwardRef<
value={value}
/>
)
}
},
)

View File

@@ -11,8 +11,7 @@ const StyledInput = styled.input<StyledInputProps>`
background-color: unset;
border: none;
color: var(--sn-stylekit-foreground-color);
font-size: ${({ textSize }) =>
textSize === 'big' ? '1.125rem' : 'var(--sn-stylekit-font-size-h3)'};
font-size: ${({ textSize }) => (textSize === 'big' ? '1.125rem' : 'var(--sn-stylekit-font-size-h3)')};
font-weight: ${({ textSize }) => (textSize === 'big' ? '500' : '400')};
height: auto;
margin: 6px 0 6px 0;
@@ -35,14 +34,7 @@ type TextInputProps = {
autoFocus?: boolean
dir?: 'ltr' | 'rtl' | 'auto'
disabled?: boolean
enterKeyHint?:
| 'enter'
| 'done'
| 'go'
| 'next'
| 'previous'
| 'search'
| 'send'
enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'
placeholder?: string
spellCheck?: boolean
testId?: string
@@ -68,7 +60,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
onChange,
onKeyPress,
},
ref
ref,
) => {
return (
<StyledInput
@@ -88,5 +80,5 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
value={value}
/>
)
}
},
)

View File

@@ -1,11 +1,6 @@
export const ChevronDownIcon = () => {
return (
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="sn-icon block"
>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className="sn-icon block">
<path d="M6.17622 7.15015L10.0012 10.9751L13.8262 7.15015L15.0012 8.33348L10.0012 13.3335L5.00122 8.33348L6.17622 7.15015Z" />
</svg>
)

View File

@@ -1,11 +1,6 @@
export const ChevronUpIcon = () => {
return (
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="sn-icon block"
>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className="sn-icon block">
<path d="M13.826 13.3335L10.001 9.5085L6.17597 13.3335L5.00097 12.1502L10.001 7.15017L15.001 12.1502L13.826 13.3335Z" />
</svg>
)

View File

@@ -6,14 +6,7 @@ export const DottedCircleIcon = () => {
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="19"
height="19"
rx="9.5"
strokeDasharray="2 2"
/>
<rect x="0.5" y="0.5" width="19" height="19" rx="9.5" strokeDasharray="2 2" />
</svg>
)
}

View File

@@ -1,11 +1,6 @@
export const MergeIcon = () => {
return (
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="sn-icon block"
>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className="sn-icon block">
<path d="M8 17L12 13H15.2C15.6 14.2 16.7 15 18 15C19.7 15 21 13.7 21 12C21 10.3 19.7 9 18 9C16.7 9 15.6 9.8 15.2 11H12L8 7V3H3V8H6L10.2 12L6 16H3V21H8V17Z" />
</svg>
)

View File

@@ -1,11 +1,6 @@
export const RenameIcon = () => {
return (
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="sn-icon block"
>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className="sn-icon block">
<path d="M11.7167 7.5L12.5 8.28333L4.93333 15.8333H4.16667V15.0667L11.7167 7.5ZM14.7167 2.5C14.5083 2.5 14.2917 2.58333 14.1333 2.74167L12.6083 4.26667L15.7333 7.39167L17.2583 5.86667C17.5833 5.54167 17.5833 5 17.2583 4.69167L15.3083 2.74167C15.1417 2.575 14.9333 2.5 14.7167 2.5ZM11.7167 5.15833L2.5 14.375V17.5H5.625L14.8417 8.28333L11.7167 5.15833Z" />
</svg>
)

View File

@@ -2,9 +2,7 @@ type ReorderIconProps = {
highlight?: boolean
}
export const ReorderIcon: React.FC<ReorderIconProps> = ({
highlight = false,
}) => {
export const ReorderIcon: React.FC<ReorderIconProps> = ({ highlight = false }) => {
return (
<svg
viewBox="0 0 24 24"

View File

@@ -1,11 +1,6 @@
export const TrashIcon = () => {
return (
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="sn-icon block"
>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className="sn-icon block">
<path d="M7.49992 2.5V3.33333H3.33325V5H4.16659V15.8333C4.16659 16.2754 4.34218 16.6993 4.65474 17.0118C4.9673 17.3244 5.39122 17.5 5.83325 17.5H14.1666C14.6086 17.5 15.0325 17.3244 15.3451 17.0118C15.6577 16.6993 15.8333 16.2754 15.8333 15.8333V5H16.6666V3.33333H12.4999V2.5H7.49992ZM5.83325 5H14.1666V15.8333H5.83325V5ZM7.49992 6.66667V14.1667H9.16658V6.66667H7.49992ZM10.8333 6.66667V14.1667H12.4999V6.66667H10.8333Z" />
</svg>
)

View File

@@ -38,8 +38,7 @@ $transition-duration: 750ms;
.checkbox-square,
.checkbox-mark {
cursor: pointer;
transition: stroke-dashoffset $transition-duration
cubic-bezier(0.9, 0, 0.5, 1);
transition: stroke-dashoffset $transition-duration cubic-bezier(0.9, 0, 0.5, 1);
}
.checkbox-circle {

View File

@@ -2,10 +2,7 @@ import './CheckBoxElementsDefs.scss'
export const CheckBoxElementsDefs = () => {
return (
<svg
viewBox="0 0 0 0"
style={{ position: 'absolute', zIndex: -1, opacity: 0 }}
>
<svg viewBox="0 0 0 0" style={{ position: 'absolute', zIndex: -1, opacity: 0 }}>
<defs>
<path
id="checkbox-square"

View File

@@ -229,9 +229,7 @@ describe('getPlainPreview', () => {
expect(getPlainPreview(groupedTasks)).toBe('2/5 tasks completed')
expect(getPlainPreview([])).toBe('0/0 tasks completed')
expect(getPlainPreview([{ name: 'Test', tasks: [] }])).toBe(
'0/0 tasks completed'
)
expect(getPlainPreview([{ name: 'Test', tasks: [] }])).toBe('0/0 tasks completed')
})
})

View File

@@ -1,11 +1,7 @@
import { v4 as uuidv4 } from 'uuid'
import { GroupPayload, TaskPayload } from '../features/tasks/tasks-slice'
export function arrayMoveMutable(
array: any[],
fromIndex: number,
toIndex: number
) {
export function arrayMoveMutable(array: any[], fromIndex: number, toIndex: number) {
const startIndex = fromIndex < 0 ? array.length + fromIndex : fromIndex
if (startIndex >= 0 && startIndex < array.length) {
const endIndex = toIndex < 0 ? array.length + toIndex : toIndex
@@ -14,11 +10,7 @@ export function arrayMoveMutable(
}
}
export function arrayMoveImmutable(
array: any[],
fromIndex: number,
toIndex: number
) {
export function arrayMoveImmutable(array: any[], fromIndex: number, toIndex: number) {
array = [...array]
arrayMoveMutable(array, fromIndex, toIndex)
return array
@@ -43,9 +35,7 @@ export function groupTasksByCompletedStatus(tasks: TaskPayload[]) {
}
}
export function getTaskArrayFromGroupedTasks(
groupedTasks: GroupPayload[]
): TaskPayload[] {
export function getTaskArrayFromGroupedTasks(groupedTasks: GroupPayload[]): TaskPayload[] {
let taskArray: TaskPayload[] = []
groupedTasks.forEach((group) => {
@@ -124,10 +114,7 @@ export function isJsonString(rawString: string) {
return true
}
export function isLastActiveGroup(
allGroups: GroupPayload[],
groupName: string
): boolean {
export function isLastActiveGroup(allGroups: GroupPayload[], groupName: string): boolean {
if (allGroups.length === 0) {
return true
}

View File

@@ -1,15 +1,11 @@
import reducer, {
setCanEdit,
setIsRunningOnMobile,
setSpellCheckerEnabled,
} from './settings-slice'
import reducer, { setCanEdit, setIsRunningOnMobile, setSpellCheckerEnabled } from './settings-slice'
import type { SettingsState } from './settings-slice'
it('should return the initial state', () => {
return expect(
reducer(undefined, {
type: undefined,
})
}),
).toEqual({
canEdit: true,
isRunningOnMobile: false,

View File

@@ -28,6 +28,5 @@ const settingsSlice = createSlice({
},
})
export const { setCanEdit, setIsRunningOnMobile, setSpellCheckerEnabled } =
settingsSlice.actions
export const { setCanEdit, setIsRunningOnMobile, setSpellCheckerEnabled } = settingsSlice.actions
export default settingsSlice.reducer

View File

@@ -10,12 +10,8 @@ const group = 'default group'
it('renders two buttons', () => {
testRender(<CompletedTasksActions groupName={group} />)
expect(screen.getByTestId('reopen-completed-button')).toHaveTextContent(
'Reopen Completed'
)
expect(screen.getByTestId('delete-completed-button')).toHaveTextContent(
'Delete Completed'
)
expect(screen.getByTestId('reopen-completed-button')).toHaveTextContent('Reopen Completed')
expect(screen.getByTestId('delete-completed-button')).toHaveTextContent('Delete Completed')
})
it('should not render buttons if can not edit', () => {
@@ -29,12 +25,8 @@ it('should not render buttons if can not edit', () => {
testRender(<CompletedTasksActions groupName={group} />, {}, defaultState)
expect(
screen.queryByTestId('reopen-completed-button')
).not.toBeInTheDocument()
expect(
screen.queryByTestId('delete-completed-button')
).not.toBeInTheDocument()
expect(screen.queryByTestId('reopen-completed-button')).not.toBeInTheDocument()
expect(screen.queryByTestId('delete-completed-button')).not.toBeInTheDocument()
})
it('should dispatch openAllCompleted action', () => {
@@ -45,18 +37,14 @@ it('should dispatch openAllCompleted action', () => {
const confirmDialog = screen.getByTestId('reopen-all-tasks-dialog')
expect(confirmDialog).toBeInTheDocument()
expect(confirmDialog).toHaveTextContent(
`Are you sure you want to reopen completed tasks in the '${group}' group?`
)
expect(confirmDialog).toHaveTextContent(`Are you sure you want to reopen completed tasks in the '${group}' group?`)
const confirmButton = screen.getByTestId('confirm-dialog-button')
fireEvent.click(confirmButton)
const dispatchedActions = mockStore.getActions()
expect(dispatchedActions).toHaveLength(1)
expect(dispatchedActions[0]).toMatchObject(
openAllCompleted({ groupName: group })
)
expect(dispatchedActions[0]).toMatchObject(openAllCompleted({ groupName: group }))
})
it('should dispatch deleteCompleted action', () => {
@@ -67,18 +55,14 @@ it('should dispatch deleteCompleted action', () => {
const confirmDialog = screen.getByTestId('delete-completed-tasks-dialog')
expect(confirmDialog).toBeInTheDocument()
expect(confirmDialog).toHaveTextContent(
`Are you sure you want to delete completed tasks in the '${group}' group?`
)
expect(confirmDialog).toHaveTextContent(`Are you sure you want to delete completed tasks in the '${group}' group?`)
const confirmButton = screen.getByTestId('confirm-dialog-button')
fireEvent.click(confirmButton)
const dispatchedActions = mockStore.getActions()
expect(dispatchedActions).toHaveLength(1)
expect(dispatchedActions[0]).toMatchObject(
deleteAllCompleted({ groupName: group })
)
expect(dispatchedActions[0]).toMatchObject(deleteAllCompleted({ groupName: group }))
})
it('should dismiss dialogs', () => {

View File

@@ -30,9 +30,7 @@ type CompletedTasksActionsProps = {
groupName: string
}
const CompletedTasksActions: React.FC<CompletedTasksActionsProps> = ({
groupName,
}) => {
const CompletedTasksActions: React.FC<CompletedTasksActionsProps> = ({ groupName }) => {
const dispatch = useAppDispatch()
const canEdit = useAppSelector((state) => state.settings.canEdit)
@@ -46,16 +44,10 @@ const CompletedTasksActions: React.FC<CompletedTasksActionsProps> = ({
return (
<div data-testid="completed-tasks-actions">
<ActionButton
onClick={() => setShowReopenDialog(true)}
data-testid="reopen-completed-button"
>
<ActionButton onClick={() => setShowReopenDialog(true)} data-testid="reopen-completed-button">
Reopen Completed
</ActionButton>
<ActionButton
onClick={() => setShowDeleteDialog(true)}
data-testid="delete-completed-button"
>
<ActionButton onClick={() => setShowDeleteDialog(true)} data-testid="delete-completed-button">
Delete Completed
</ActionButton>
{showReopenDialog && (
@@ -65,8 +57,7 @@ const CompletedTasksActions: React.FC<CompletedTasksActionsProps> = ({
confirmButtonCb={() => dispatch(openAllCompleted({ groupName }))}
cancelButtonCb={() => setShowReopenDialog(false)}
>
Are you sure you want to reopen completed tasks in the '
<strong>{groupName}</strong>' group?
Are you sure you want to reopen completed tasks in the '<strong>{groupName}</strong>' group?
</ConfirmDialog>
)}
{showDeleteDialog && (
@@ -76,8 +67,7 @@ const CompletedTasksActions: React.FC<CompletedTasksActionsProps> = ({
confirmButtonCb={() => dispatch(deleteAllCompleted({ groupName }))}
cancelButtonCb={() => setShowDeleteDialog(false)}
>
Are you sure you want to delete completed tasks in the '
<strong>{groupName}</strong>' group?
Are you sure you want to delete completed tasks in the '<strong>{groupName}</strong>' group?
</ConfirmDialog>
)}
</div>

View File

@@ -1,10 +1,4 @@
import {
ChangeEvent,
createRef,
FocusEvent,
KeyboardEvent,
useState,
} from 'react'
import { ChangeEvent, createRef, FocusEvent, KeyboardEvent, useState } from 'react'
import styled from 'styled-components'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
@@ -82,9 +76,7 @@ const CreateGroup: React.FC = () => {
const [isCreateMode, setIsCreateMode] = useState(false)
const canEdit = useAppSelector((state) => state.settings.canEdit)
const spellCheckerEnabled = useAppSelector(
(state) => state.settings.spellCheckerEnabled
)
const spellCheckerEnabled = useAppSelector((state) => state.settings.spellCheckerEnabled)
const groupedTasks = useAppSelector((state) => state.tasks.groups)
const taskGroupCount = groupedTasks.length
@@ -144,9 +136,7 @@ const CreateGroup: React.FC = () => {
<TutorialContainer>
<Tutorial>
<ArrowVector style={{ marginRight: 140, marginBottom: 12 }} />
<TutorialText>
Get started by naming your first task group
</TutorialText>
<TutorialText>Get started by naming your first task group</TutorialText>
</Tutorial>
<EmptyContainer1 />
<EmptyContainer2 />

View File

@@ -79,6 +79,6 @@ test('pressing enter when input box is not empty, should create a new task', ()
taskAdded({
task: { id: 'my-fake-uuid', description: 'My awesome task' },
groupName: defaultGroup.name,
})
}),
)
})

View File

@@ -29,9 +29,7 @@ const CreateTask: React.FC<CreateTaskProps> = ({ group }) => {
const dispatch = useAppDispatch()
const spellCheckerEnabled = useAppSelector(
(state) => state.settings.spellCheckerEnabled
)
const spellCheckerEnabled = useAppSelector((state) => state.settings.spellCheckerEnabled)
const canEdit = useAppSelector((state) => state.settings.canEdit)
const allGroups = useAppSelector((state) => state.tasks.groups)
@@ -51,9 +49,7 @@ const CreateTask: React.FC<CreateTaskProps> = ({ group }) => {
return
}
dispatch(
taskAdded({ task: { id: uuidv4(), description: rawString }, groupName })
)
dispatch(taskAdded({ task: { id: uuidv4(), description: rawString }, groupName }))
setTaskDraft('')
}
}

View File

@@ -13,12 +13,7 @@ const Container = styled.div`
const InvalidContentError: React.FC = () => {
const lastError = useAppSelector((state) => state.tasks.lastError)
return (
<Container>
{lastError ||
'An unknown error has occurred, and the content cannot be displayed.'}
</Container>
)
return <Container>{lastError || 'An unknown error has occurred, and the content cannot be displayed.'}</Container>
}
export default InvalidContentError

View File

@@ -28,20 +28,12 @@ it('renders the alert dialog when no groups are available to merge', () => {
},
}
testRender(
<MergeTaskGroups groupName={defaultGroup} handleClose={handleClose} />,
{},
defaultState
)
testRender(<MergeTaskGroups groupName={defaultGroup} handleClose={handleClose} />, {}, defaultState)
const alertDialog = screen.getByTestId('merge-task-group-dialog')
expect(alertDialog).toBeInTheDocument()
expect(alertDialog).toHaveTextContent(
`There are no other groups to merge '${defaultGroup}' with.`
)
expect(alertDialog).not.toHaveTextContent(
`Select which group you want to merge '${defaultGroup}' into:`
)
expect(alertDialog).toHaveTextContent(`There are no other groups to merge '${defaultGroup}' with.`)
expect(alertDialog).not.toHaveTextContent(`Select which group you want to merge '${defaultGroup}' into:`)
// There shouldn't be any radio buttons
expect(screen.queryAllByRole('radio')).toHaveLength(0)
@@ -90,20 +82,12 @@ it('renders the alert dialog when there are groups available to merge', () => {
},
}
testRender(
<MergeTaskGroups groupName={defaultGroup} handleClose={handleClose} />,
{},
defaultState
)
testRender(<MergeTaskGroups groupName={defaultGroup} handleClose={handleClose} />, {}, defaultState)
const alertDialog = screen.getByTestId('merge-task-group-dialog')
expect(alertDialog).toBeInTheDocument()
expect(alertDialog).toHaveTextContent(
`Select which group you want to merge '${defaultGroup}' into:`
)
expect(alertDialog).not.toHaveTextContent(
`There are no other groups to merge '${defaultGroup}' with.`
)
expect(alertDialog).toHaveTextContent(`Select which group you want to merge '${defaultGroup}' into:`)
expect(alertDialog).not.toHaveTextContent(`There are no other groups to merge '${defaultGroup}' with.`)
const radioButtons = screen.queryAllByRole('radio')
expect(radioButtons).toHaveLength(2)
@@ -163,7 +147,7 @@ it('should close the dialog if no group is selected and the Merge button is clic
const { mockStore } = testRender(
<MergeTaskGroups groupName={defaultGroup} handleClose={handleClose} />,
{},
defaultState
defaultState,
)
const buttons = screen.queryAllByRole('button')
@@ -225,7 +209,7 @@ it('should dispatch the action to merge groups', () => {
const { mockStore } = testRender(
<MergeTaskGroups groupName={defaultGroup} handleClose={handleClose} />,
{},
defaultState
defaultState,
)
const radioButtons = screen.queryAllByRole('radio')
@@ -250,8 +234,6 @@ it('should dispatch the action to merge groups', () => {
dispatchedActions = mockStore.getActions()
expect(dispatchedActions).toHaveLength(1)
expect(dispatchedActions[0]).toMatchObject(
tasksGroupMerged({ groupName: defaultGroup, mergeWith: 'Testing' })
)
expect(dispatchedActions[0]).toMatchObject(tasksGroupMerged({ groupName: defaultGroup, mergeWith: 'Testing' }))
expect(handleClose).toHaveBeenCalledTimes(1)
})

View File

@@ -1,11 +1,7 @@
import '@reach/dialog/styles.css'
import React, { useRef, useState } from 'react'
import {
AlertDialog,
AlertDialogLabel,
AlertDialogDescription,
} from '@reach/alert-dialog'
import { AlertDialog, AlertDialogLabel, AlertDialogDescription } from '@reach/alert-dialog'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
import { tasksGroupMerged } from './tasks-slice'
@@ -15,10 +11,7 @@ type MergeTaskGroupsProps = {
handleClose: () => void
}
const MergeTaskGroups: React.FC<MergeTaskGroupsProps> = ({
groupName,
handleClose,
}) => {
const MergeTaskGroups: React.FC<MergeTaskGroupsProps> = ({ groupName, handleClose }) => {
const cancelRef = useRef<HTMLButtonElement>(null)
const dispatch = useAppDispatch()
@@ -45,39 +38,25 @@ const MergeTaskGroups: React.FC<MergeTaskGroupsProps> = ({
}
return (
<AlertDialog
data-testid="merge-task-group-dialog"
leastDestructiveRef={cancelRef}
>
<AlertDialog data-testid="merge-task-group-dialog" leastDestructiveRef={cancelRef}>
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
<div className="sk-panel-content">
<div className="sk-panel-section">
<AlertDialogLabel className="sk-h3 sk-panel-section-title">
Merging task groups
</AlertDialogLabel>
<AlertDialogLabel className="sk-h3 sk-panel-section-title">Merging task groups</AlertDialogLabel>
{mergeableGroups.length > 0 ? (
<>
<AlertDialogDescription className="sk-panel-row">
<p className="color-foreground">
Select which group you want to merge '
<strong>{groupName}</strong>' into:
Select which group you want to merge '<strong>{groupName}</strong>' into:
</p>
</AlertDialogDescription>
<fieldset className="flex flex-col" onChange={handleChange}>
{mergeableGroups.map((item) => (
<label
key={item.name}
className="flex items-center mb-1"
>
<input
type="radio"
value={item.name}
checked={item.name === mergeWith}
readOnly
/>
<label key={item.name} className="flex items-center mb-1">
<input type="radio" value={item.name} checked={item.name === mergeWith} readOnly />
{item.name}
</label>
))}
@@ -86,24 +65,16 @@ const MergeTaskGroups: React.FC<MergeTaskGroupsProps> = ({
) : (
<AlertDialogDescription>
<p className="color-foreground">
There are no other groups to merge '
<strong>{groupName}</strong>' with.
There are no other groups to merge '<strong>{groupName}</strong>' with.
</p>
</AlertDialogDescription>
)}
<div className="flex my-1 mt-4">
<button
className="sn-button small neutral"
onClick={handleClose}
ref={cancelRef}
>
<button className="sn-button small neutral" onClick={handleClose} ref={cancelRef}>
{!mergeWith ? 'Close' : 'Cancel'}
</button>
<button
className="sn-button small ml-2 info"
onClick={handleMergeGroups}
>
<button className="sn-button small ml-2 info" onClick={handleMergeGroups}>
Merge groups
</button>
</div>

View File

@@ -20,32 +20,21 @@ const MigrateLegacyContent: React.FC = () => {
}
return (
<AlertDialog
data-testid="migrate-legacy-content-dialog"
leastDestructiveRef={cancelRef}
>
<AlertDialog data-testid="migrate-legacy-content-dialog" leastDestructiveRef={cancelRef}>
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
<div className="sk-panel-content">
<div className="sk-panel-section">
<AlertDialogLabel className="sk-h3 sk-panel-section-title">
Are you sure you want to migrate legacy content to the new
format?
Are you sure you want to migrate legacy content to the new format?
</AlertDialogLabel>
<div className="flex my-1 mt-4">
<button
className="sn-button small neutral"
onClick={handleCancel}
ref={cancelRef}
>
<button className="sn-button small neutral" onClick={handleCancel} ref={cancelRef}>
Cancel
</button>
<button
className="sn-button small ml-2 info"
onClick={handleMigrate}
>
<button className="sn-button small ml-2 info" onClick={handleMigrate}>
Migrate
</button>
</div>

View File

@@ -19,10 +19,7 @@ type GroupSummaryProps = {
const GroupSummary: React.FC<GroupSummaryProps> = ({ groups }) => {
const totalGroups = groups.length
const groupsToPreview = groups.slice(
0,
Math.min(totalGroups, GROUPS_PREVIEW_LIMIT)
)
const groupsToPreview = groups.slice(0, Math.min(totalGroups, GROUPS_PREVIEW_LIMIT))
if (groupsToPreview.length === 0) {
return <></>
}
@@ -35,16 +32,10 @@ const GroupSummary: React.FC<GroupSummaryProps> = ({ groups }) => {
<div className="my-2">
{groupsToPreview.map((group, index) => {
const totalTasks = group.tasks.length
const totalCompletedTasks = group.tasks.filter(
(task) => task.completed === true
).length
const totalCompletedTasks = group.tasks.filter((task) => task.completed === true).length
return (
<p
data-testid="group-summary"
key={`group-${group.name}`}
className="mb-1"
>
<p data-testid="group-summary" key={`group-${group.name}`} className="mb-1">
{truncateText(group.name, MAX_GROUP_DESCRIPTION_LENGTH)}
<span className="px-2 neutral">
{totalCompletedTasks}/{totalTasks}
@@ -75,11 +66,7 @@ const NotePreview: React.FC<NotePreviewProps> = ({ groupedTasks }) => {
return (
<>
<div className="flex flex-grow items-center mb-3">
<svg
data-testid="circular-progress-bar"
className="sk-circular-progress"
viewBox="0 0 18 18"
>
<svg data-testid="circular-progress-bar" className="sk-circular-progress" viewBox="0 0 18 18">
<circle className="background" />
<circle className={`progress p-${roundedPercentage}`} />
</svg>

View File

@@ -28,11 +28,7 @@ it('renders the alert dialog with an input box', () => {
},
}
testRender(
<RenameTaskGroups groupName={defaultGroup} handleClose={handleClose} />,
{},
defaultState
)
testRender(<RenameTaskGroups groupName={defaultGroup} handleClose={handleClose} />, {}, defaultState)
const alertDialog = screen.getByTestId('rename-task-group-dialog')
expect(alertDialog).toBeInTheDocument()
@@ -78,14 +74,12 @@ it('should dispatch the action to merge groups', () => {
const { mockStore } = testRender(
<RenameTaskGroups groupName={defaultGroup} handleClose={handleClose} />,
{},
defaultState
defaultState,
)
const newGroupName = 'My new group name'
const inputBox = screen.getByTestId(
'new-group-name-input'
) as HTMLInputElement
const inputBox = screen.getByTestId('new-group-name-input') as HTMLInputElement
fireEvent.change(inputBox, { target: { value: newGroupName } })
@@ -104,9 +98,7 @@ it('should dispatch the action to merge groups', () => {
const dispatchedActions = mockStore.getActions()
expect(dispatchedActions).toHaveLength(1)
expect(dispatchedActions[0]).toMatchObject(
tasksGroupRenamed({ groupName: defaultGroup, newName: newGroupName })
)
expect(dispatchedActions[0]).toMatchObject(tasksGroupRenamed({ groupName: defaultGroup, newName: newGroupName }))
expect(handleClose).toHaveBeenCalledTimes(1)
})
@@ -145,14 +137,12 @@ it('should dispatch the action to merge groups on Enter press', () => {
const { mockStore } = testRender(
<RenameTaskGroups groupName={defaultGroup} handleClose={handleClose} />,
{},
defaultState
defaultState,
)
const newGroupName = 'My new group name'
const inputBox = screen.getByTestId(
'new-group-name-input'
) as HTMLInputElement
const inputBox = screen.getByTestId('new-group-name-input') as HTMLInputElement
fireEvent.change(inputBox, { target: { value: newGroupName } })
fireEvent.keyPress(inputBox, {
@@ -164,8 +154,6 @@ it('should dispatch the action to merge groups on Enter press', () => {
const dispatchedActions = mockStore.getActions()
expect(dispatchedActions).toHaveLength(1)
expect(dispatchedActions[0]).toMatchObject(
tasksGroupRenamed({ groupName: defaultGroup, newName: newGroupName })
)
expect(dispatchedActions[0]).toMatchObject(tasksGroupRenamed({ groupName: defaultGroup, newName: newGroupName }))
expect(handleClose).toHaveBeenCalledTimes(1)
})

View File

@@ -1,11 +1,7 @@
import '@reach/dialog/styles.css'
import React, { KeyboardEvent, useRef, useState } from 'react'
import {
AlertDialog,
AlertDialogLabel,
AlertDialogDescription,
} from '@reach/alert-dialog'
import { AlertDialog, AlertDialogLabel, AlertDialogDescription } from '@reach/alert-dialog'
import { useAppDispatch } from '../../app/hooks'
import { tasksGroupRenamed } from './tasks-slice'
@@ -16,10 +12,7 @@ type RenameTaskGroupsProps = {
handleClose: () => void
}
const RenameTaskGroups: React.FC<RenameTaskGroupsProps> = ({
groupName,
handleClose,
}) => {
const RenameTaskGroups: React.FC<RenameTaskGroupsProps> = ({ groupName, handleClose }) => {
const cancelRef = useRef<HTMLButtonElement>(null)
const dispatch = useAppDispatch()
@@ -44,10 +37,7 @@ const RenameTaskGroups: React.FC<RenameTaskGroupsProps> = ({
}
return (
<AlertDialog
data-testid="rename-task-group-dialog"
leastDestructiveRef={cancelRef}
>
<AlertDialog data-testid="rename-task-group-dialog" leastDestructiveRef={cancelRef}>
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
@@ -69,11 +59,7 @@ const RenameTaskGroups: React.FC<RenameTaskGroupsProps> = ({
</AlertDialogDescription>
<div className="flex my-1 mt-4">
<button
className="sn-button small neutral"
onClick={handleClose}
ref={cancelRef}
>
<button className="sn-button small neutral" onClick={handleClose} ref={cancelRef}>
{renameTo.length === 0 ? 'Close' : 'Cancel'}
</button>
<button

View File

@@ -31,14 +31,10 @@ it('renders the group name', () => {
it('renders the number of completed tasks and total tasks', () => {
testRender(<TaskGroup group={defaultGroup} isDragging={false} />)
const completedTasks = defaultGroup.tasks.filter(
(task) => task.completed
).length
const completedTasks = defaultGroup.tasks.filter((task) => task.completed).length
const totalTasks = defaultGroup.tasks.length
expect(screen.getByTestId('task-group-stats')).toHaveTextContent(
`${completedTasks}/${totalTasks}`
)
expect(screen.getByTestId('task-group-stats')).toHaveTextContent(`${completedTasks}/${totalTasks}`)
})
it('renders the circular progress bar', () => {
@@ -96,11 +92,7 @@ it('hides group options if can not edit', () => {
},
}
testRender(
<TaskGroup group={defaultGroup} isDragging={false} />,
{},
defaultState
)
testRender(<TaskGroup group={defaultGroup} isDragging={false} />, {}, defaultState)
expect(screen.queryByTestId('task-group-options')).not.toBeInTheDocument()
})
@@ -114,11 +106,7 @@ it('shows a reorder icon when on mobile', () => {
},
}
testRender(
<TaskGroup group={defaultGroup} isDragging={false} />,
{},
defaultState
)
testRender(<TaskGroup group={defaultGroup} isDragging={false} />, {}, defaultState)
expect(screen.queryByTestId('reorder-icon')).not.toBeInTheDocument()
@@ -130,11 +118,7 @@ it('shows a reorder icon when on mobile', () => {
},
}
testRender(
<TaskGroup group={defaultGroup} isDragging={false} />,
{},
defaultState
)
testRender(<TaskGroup group={defaultGroup} isDragging={false} />, {}, defaultState)
expect(screen.getByTestId('reorder-icon')).toBeInTheDocument()
})

View File

@@ -10,17 +10,8 @@ import TaskItemList from './TaskItemList'
import TaskGroupOptions from './TaskGroupOptions'
import {
CircularProgressBar,
GenericInlineText,
MainTitle,
RoundButton,
} from '../../common/components'
import {
ChevronDownIcon,
ReorderIcon,
ChevronUpIcon,
} from '../../common/components/icons'
import { CircularProgressBar, GenericInlineText, MainTitle, RoundButton } from '../../common/components'
import { ChevronDownIcon, ReorderIcon, ChevronUpIcon } from '../../common/components/icons'
const TaskGroupContainer = styled.div<{ isLast?: boolean }>`
background-color: var(--sn-stylekit-background-color);
@@ -100,10 +91,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
<ReorderIcon highlight={isDragging} />
</div>
)}
<MainTitle
crossed={allTasksCompleted && collapsed}
highlight={isDragging}
>
<MainTitle crossed={allTasksCompleted && collapsed} highlight={isDragging}>
{groupName}
</MainTitle>
<CircularProgressBar size={18} percentage={percentageCompleted} />
@@ -119,10 +107,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
</div>
)}
<div className="ml-3">
<RoundButton
testId="collapse-task-group"
onClick={handleCollapse}
>
<RoundButton testId="collapse-task-group" onClick={handleCollapse}>
{!collapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
</RoundButton>
</div>

View File

@@ -1,10 +1,5 @@
import React from 'react'
import {
DragDropContext,
Draggable,
Droppable,
DropResult,
} from 'react-beautiful-dnd'
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
import { tasksGroupReordered } from './tasks-slice'
@@ -32,16 +27,13 @@ const TaskGroupList: React.FC = () => {
tasksGroupReordered({
swapGroupIndex: source.index,
withGroupIndex: destination.index,
})
}),
)
}
return (
<DragDropContext data-testid="task-group-list" onDragEnd={onDragEnd}>
<Droppable
droppableId={'droppable-task-group-list'}
isDropDisabled={!canEdit}
>
<Droppable droppableId={'droppable-task-group-list'} isDropDisabled={!canEdit}>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{groupedTasks.map((group, index) => {
@@ -52,12 +44,8 @@ const TaskGroupList: React.FC = () => {
index={index}
isDragDisabled={!canEdit}
>
{(
{ innerRef, draggableProps, dragHandleProps },
{ isDragging }
) => {
const { onTransitionEnd, ...restDraggableProps } =
draggableProps
{({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
const { onTransitionEnd, ...restDraggableProps } = draggableProps
return (
<TaskGroup
key={`group-${group.name}`}

View File

@@ -43,9 +43,7 @@ it('should dispatch tasksGroupDeleted action', () => {
const confirmDialog = screen.getByTestId('delete-task-group-dialog')
expect(confirmDialog).toBeInTheDocument()
expect(confirmDialog).toHaveTextContent(
`Are you sure you want to delete the group '${groupName}'?`
)
expect(confirmDialog).toHaveTextContent(`Are you sure you want to delete the group '${groupName}'?`)
const confirmButton = screen.getByTestId('confirm-dialog-button')
fireEvent.click(confirmButton)
@@ -103,9 +101,7 @@ it('should close the delete task group dialog', () => {
const cancelButton = screen.getByTestId('cancel-dialog-button')
clickButton(cancelButton)
expect(
screen.queryByTestId('trash-task-group-dialog')
).not.toBeInTheDocument()
expect(screen.queryByTestId('trash-task-group-dialog')).not.toBeInTheDocument()
})
it('should close the merge task group dialog', () => {
@@ -120,9 +116,7 @@ it('should close the merge task group dialog', () => {
const cancelButton = screen.queryAllByRole('button')[0]
clickButton(cancelButton)
expect(
screen.queryByTestId('merge-task-group-dialog')
).not.toBeInTheDocument()
expect(screen.queryByTestId('merge-task-group-dialog')).not.toBeInTheDocument()
})
it('should close the rename task group dialog', () => {
@@ -137,7 +131,5 @@ it('should close the rename task group dialog', () => {
const cancelButton = screen.queryAllByRole('button')[0]
clickButton(cancelButton)
expect(
screen.queryByTestId('rename-task-group-dialog')
).not.toBeInTheDocument()
expect(screen.queryByTestId('rename-task-group-dialog')).not.toBeInTheDocument()
})

View File

@@ -5,12 +5,7 @@ import VisuallyHidden from '@reach/visually-hidden'
import { useAppDispatch } from '../../app/hooks'
import { tasksGroupDeleted } from './tasks-slice'
import {
MoreIcon,
MergeIcon,
TrashIcon,
RenameIcon,
} from '../../common/components/icons'
import { MoreIcon, MergeIcon, TrashIcon, RenameIcon } from '../../common/components/icons'
import { ConfirmDialog } from '../../common/components'
@@ -36,24 +31,15 @@ const TaskGroupOptions: React.FC<TaskGroupOptionsProps> = ({ groupName }) => {
<MoreIcon />
</MenuButton>
<MenuList>
<MenuItem
data-testid="delete-task-group"
onSelect={() => setShowDeleteDialog(true)}
>
<MenuItem data-testid="delete-task-group" onSelect={() => setShowDeleteDialog(true)}>
<TrashIcon />
<span className="px-1">Delete group</span>
</MenuItem>
<MenuItem
data-testid="merge-task-group"
onSelect={() => setShowMergeDialog(true)}
>
<MenuItem data-testid="merge-task-group" onSelect={() => setShowMergeDialog(true)}>
<MergeIcon />
<span className="px-1">Merge into another group</span>
</MenuItem>
<MenuItem
data-testid="rename-task-group"
onSelect={() => setShowRenameDialog(true)}
>
<MenuItem data-testid="rename-task-group" onSelect={() => setShowRenameDialog(true)}>
<RenameIcon />
<span className="px-1">Rename</span>
</MenuItem>
@@ -68,22 +54,11 @@ const TaskGroupOptions: React.FC<TaskGroupOptionsProps> = ({ groupName }) => {
confirmButtonCb={() => dispatch(tasksGroupDeleted({ groupName }))}
cancelButtonCb={() => setShowDeleteDialog(false)}
>
Are you sure you want to delete the group '
<strong>{groupName}</strong>'?
Are you sure you want to delete the group '<strong>{groupName}</strong>'?
</ConfirmDialog>
)}
{showMergeDialog && (
<MergeTaskGroups
groupName={groupName}
handleClose={() => setShowMergeDialog(false)}
/>
)}
{showRenameDialog && (
<RenameTaskGroups
groupName={groupName}
handleClose={() => setShowRenameDialog(false)}
/>
)}
{showMergeDialog && <MergeTaskGroups groupName={groupName} handleClose={() => setShowMergeDialog(false)} />}
{showRenameDialog && <RenameTaskGroups groupName={groupName} handleClose={() => setShowRenameDialog(false)} />}
</>
)
}

View File

@@ -1,11 +1,6 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import {
taskDeleted,
taskModified,
TaskPayload,
taskToggled,
} from './tasks-slice'
import { taskDeleted, taskModified, TaskPayload, taskToggled } from './tasks-slice'
import { testRender } from '../../testUtils'
import TaskItem from './TaskItem'
@@ -27,9 +22,7 @@ it('renders a check box and textarea input', async () => {
test('clicking the check box should toggle the task as open/completed', () => {
jest.useFakeTimers()
const { mockStore } = testRender(
<TaskItem groupName={groupName} task={task} />
)
const { mockStore } = testRender(<TaskItem groupName={groupName} task={task} />)
const checkBox = screen.getByTestId('check-box-input')
fireEvent.click(checkBox)
@@ -43,7 +36,7 @@ test('clicking the check box should toggle the task as open/completed', () => {
taskToggled({
id: task.id,
groupName,
})
}),
)
fireEvent.click(checkBox)
@@ -57,7 +50,7 @@ test('clicking the check box should toggle the task as open/completed', () => {
taskToggled({
id: task.id,
groupName,
})
}),
)
})
@@ -66,13 +59,9 @@ test('changing the textarea input text should update the task description', asyn
const newTaskDescription = 'My new task'
const { mockStore } = testRender(
<TaskItem groupName={groupName} task={task} />
)
const { mockStore } = testRender(<TaskItem groupName={groupName} task={task} />)
const textAreaInput = screen.getByTestId(
'text-area-input'
) as HTMLTextAreaElement
const textAreaInput = screen.getByTestId('text-area-input') as HTMLTextAreaElement
fireEvent.change(textAreaInput, {
target: { value: newTaskDescription },
})
@@ -96,14 +85,12 @@ test('changing the textarea input text should update the task description', asyn
description: newTaskDescription,
},
groupName,
})
}),
)
})
test('clearing the textarea input text should delete the task', () => {
const { mockStore } = testRender(
<TaskItem groupName={groupName} task={task} />
)
const { mockStore } = testRender(<TaskItem groupName={groupName} task={task} />)
const textAreaInput = screen.getByTestId('text-area-input')
fireEvent.change(textAreaInput, {
@@ -123,14 +110,12 @@ test('clearing the textarea input text should delete the task', () => {
taskDeleted({
id: task.id,
groupName,
})
}),
)
})
test('pressing enter should not update the task description', () => {
const { mockStore } = testRender(
<TaskItem groupName={groupName} task={task} />
)
const { mockStore } = testRender(<TaskItem groupName={groupName} task={task} />)
const textAreaInput = screen.getByTestId('text-area-input')
fireEvent.keyPress(textAreaInput, {

View File

@@ -1,20 +1,9 @@
import './TaskItem.scss'
import {
ChangeEvent,
createRef,
KeyboardEvent,
useEffect,
useState,
} from 'react'
import { ChangeEvent, createRef, KeyboardEvent, useEffect, useState } from 'react'
import styled from 'styled-components'
import {
taskDeleted,
taskModified,
TaskPayload,
taskToggled,
} from './tasks-slice'
import { taskDeleted, taskModified, TaskPayload, taskToggled } from './tasks-slice'
import { useAppDispatch, useAppSelector, useDidMount } from '../../app/hooks'
import { CheckBoxInput, TextAreaInput } from '../../common/components'
@@ -53,20 +42,13 @@ export type TaskItemProps = {
innerRef?: (element?: HTMLElement | null | undefined) => any
}
const TaskItem: React.FC<TaskItemProps> = ({
task,
groupName,
innerRef,
...props
}) => {
const TaskItem: React.FC<TaskItemProps> = ({ task, groupName, innerRef, ...props }) => {
const textAreaRef = createRef<HTMLTextAreaElement>()
const dispatch = useAppDispatch()
const canEdit = useAppSelector((state) => state.settings.canEdit)
const spellCheckEnabled = useAppSelector(
(state) => state.settings.spellCheckerEnabled
)
const spellCheckEnabled = useAppSelector((state) => state.settings.spellCheckerEnabled)
const [completed, setCompleted] = useState(!!task.completed)
const [description, setDescription] = useState(task.description)
@@ -96,9 +78,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
? textAreaRef.current!.classList.add('cross-out')
: textAreaRef.current!.classList.add('no-text-decoration')
const dispatchDelay = newCompletedState
? DISPATCH_COMPLETED_DELAY_MS
: DISPATCH_OPENED_DELAY_MS
const dispatchDelay = newCompletedState ? DISPATCH_COMPLETED_DELAY_MS : DISPATCH_OPENED_DELAY_MS
setTimeout(() => {
dispatch(taskToggled({ id: task.id, groupName }))
@@ -135,9 +115,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
useDidMount(() => {
const timeoutId = setTimeout(() => {
if (description !== task.description) {
dispatch(
taskModified({ task: { id: task.id, description }, groupName })
)
dispatch(taskModified({ task: { id: task.id, description }, groupName }))
}
}, 500)
@@ -145,18 +123,8 @@ const TaskItem: React.FC<TaskItemProps> = ({
}, [description, groupName])
return (
<Container
data-testid="task-item"
completed={completed}
ref={innerRef}
{...props}
>
<CheckBoxInput
testId="check-box-input"
checked={completed}
disabled={!canEdit}
onChange={onCheckBoxToggle}
/>
<Container data-testid="task-item" completed={completed} ref={innerRef} {...props}>
<CheckBoxInput testId="check-box-input" checked={completed} disabled={!canEdit} onChange={onCheckBoxToggle} />
<TextAreaInput
testId="text-area-input"
className="text-area-input"

View File

@@ -52,9 +52,7 @@ it('renders the completed tasks container', () => {
testRender(<TaskItemList group={groupWithCompletedTask} />)
const completedTasksContainer = screen.getByTestId(
'completed-tasks-container'
)
const completedTasksContainer = screen.getByTestId('completed-tasks-container')
expect(completedTasksContainer).toBeInTheDocument()
expect(completedTasksContainer).toHaveTextContent('completed tasks')

View File

@@ -39,19 +39,14 @@ const TaskItemList: React.FC<TaskItemListProps> = ({ group }) => {
swapTaskIndex: source.index,
withTaskIndex: destination.index,
isSameSection: source.droppableId === destination.droppableId,
})
}),
)
}
return (
<Container data-testid="task-list">
<DragDropContext onDragEnd={onDragEnd}>
<TasksContainer
testId="open-tasks-container"
type="open"
tasks={openTasks}
groupName={group.name}
/>
<TasksContainer testId="open-tasks-container" type="open" tasks={openTasks} groupName={group.name} />
<TasksContainer
testId="completed-tasks-container"
@@ -59,9 +54,7 @@ const TaskItemList: React.FC<TaskItemListProps> = ({ group }) => {
tasks={completedTasks}
groupName={group.name}
>
{completedTasks.length > 0 && (
<CompletedTasksActions groupName={group.name} />
)}
{completedTasks.length > 0 && <CompletedTasksActions groupName={group.name} />}
</TasksContainer>
</DragDropContext>
</Container>

View File

@@ -1,12 +1,7 @@
import './TasksContainer.scss'
import React from 'react'
import {
Draggable,
DraggingStyle,
Droppable,
NotDraggingStyle,
} from 'react-beautiful-dnd'
import { Draggable, DraggingStyle, Droppable, NotDraggingStyle } from 'react-beautiful-dnd'
import styled from 'styled-components'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
@@ -27,13 +22,11 @@ const InnerTasksContainer = styled.div<{
margin-bottom: 5px;
}
${({ type, items }) =>
type === 'completed' && items > 0 ? 'margin-bottom: 28px' : ''};
${({ type, items }) => (type === 'completed' && items > 0 ? 'margin-bottom: 28px' : '')};
`
const OuterContainer = styled.div<{ type: ContainerType; items: number }>`
${({ type, items }) =>
type === 'open' && items > 0 ? 'margin-bottom: 18px' : ''};
${({ type, items }) => (type === 'open' && items > 0 ? 'margin-bottom: 18px' : '')};
`
const SubTitleContainer = styled.div`
@@ -49,10 +42,7 @@ const Wrapper = styled.div`
color: var(--sn-stylekit-foreground-color);
`
const getItemStyle = (
isDragging: boolean,
draggableStyle?: DraggingStyle | NotDraggingStyle
) => ({
const getItemStyle = (isDragging: boolean, draggableStyle?: DraggingStyle | NotDraggingStyle) => ({
...draggableStyle,
...(isDragging && {
color: 'var(--sn-stylekit-info-color)',
@@ -69,13 +59,7 @@ type TasksContainerProps = {
testId?: string
}
const TasksContainer: React.FC<TasksContainerProps> = ({
groupName,
tasks,
type,
testId,
children,
}) => {
const TasksContainer: React.FC<TasksContainerProps> = ({ groupName, tasks, type, testId, children }) => {
const canEdit = useAppSelector((state) => state.settings.canEdit)
const droppableId = `${type}-tasks-droppable`
@@ -94,10 +78,7 @@ const TasksContainer: React.FC<TasksContainerProps> = ({
ref={provided.innerRef}
type={type}
>
<TransitionGroup
component={null}
childFactory={(child) => React.cloneElement(child)}
>
<TransitionGroup component={null} childFactory={(child) => React.cloneElement(child)}>
{tasks.map((task, index) => {
return (
<CSSTransition
@@ -128,7 +109,7 @@ const TasksContainer: React.FC<TasksContainerProps> = ({
() => {
node.classList.remove('explode')
},
false
false,
)
}}
onExited={(node: HTMLElement) => {
@@ -146,18 +127,10 @@ const TasksContainer: React.FC<TasksContainerProps> = ({
index={index}
isDragDisabled={!canEdit}
>
{(
{ innerRef, draggableProps, dragHandleProps },
{ isDragging }
) => {
const { style, ...restDraggableProps } =
draggableProps
{({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
const { style, ...restDraggableProps } = draggableProps
return (
<div
className="task-item"
style={getItemStyle(isDragging, style)}
{...restDraggableProps}
>
<div className="task-item" style={getItemStyle(isDragging, style)} {...restDraggableProps}>
<TaskItem
key={`task-item-${task.id}`}
task={task}

View File

@@ -22,7 +22,7 @@ it('should return the initial state', () => {
return expect(
reducer(undefined, {
type: undefined,
})
}),
).toEqual({ schemaVersion: '1.0.0', groups: [] })
})
@@ -35,8 +35,8 @@ it('should handle a task being added to a non-existing group', () => {
taskAdded({
task: { id: 'some-id', description: 'A simple task' },
groupName: 'Test',
})
)
}),
),
).toEqual({
schemaVersion: '1.0.0',
groups: [],
@@ -67,8 +67,8 @@ it('should handle a task being added to the existing tasks store', () => {
taskAdded({
task: { id: 'another-id', description: 'Another simple task' },
groupName: 'Test',
})
)
}),
),
).toEqual({
schemaVersion: '1.0.0',
groups: [
@@ -117,8 +117,8 @@ it('should handle an existing task being modified', () => {
taskModified({
task: { id: 'some-id', description: 'Task description changed' },
groupName: 'Test',
})
)
}),
),
).toEqual({
schemaVersion: '1.0.0',
groups: [
@@ -162,8 +162,8 @@ it('should not modify tasks if an invalid id is provided', () => {
taskModified({
task: { id: 'some-invalid-id', description: 'New description' },
groupName: 'Test',
})
)
}),
),
).toEqual({
schemaVersion: '1.0.0',
groups: [
@@ -209,8 +209,8 @@ it('should keep completed field as-is, if task is modified', () => {
description: 'New description',
},
groupName: 'Test',
})
)
}),
),
).toEqual({
schemaVersion: '1.0.0',
groups: [
@@ -248,9 +248,7 @@ it('should handle an existing task being toggled', () => {
],
}
expect(
reducer(previousState, taskToggled({ id: 'some-id', groupName: 'Test' }))
).toEqual({
expect(reducer(previousState, taskToggled({ id: 'some-id', groupName: 'Test' }))).toEqual({
schemaVersion: '1.0.0',
groups: [
{
@@ -300,9 +298,7 @@ test('toggled tasks should be on top of the list', () => {
],
}
expect(
reducer(previousState, taskToggled({ id: 'another-id', groupName: 'Test' }))
).toEqual({
expect(reducer(previousState, taskToggled({ id: 'another-id', groupName: 'Test' }))).toEqual({
schemaVersion: '1.0.0',
groups: [
{
@@ -352,9 +348,7 @@ it('should handle an existing completed task being toggled', () => {
],
}
expect(
reducer(previousState, taskToggled({ id: 'some-id', groupName: 'Test' }))
).toEqual({
expect(reducer(previousState, taskToggled({ id: 'some-id', groupName: 'Test' }))).toEqual({
schemaVersion: '1.0.0',
groups: [
{
@@ -397,9 +391,7 @@ it('should handle an existing task being deleted', () => {
],
}
expect(
reducer(previousState, taskDeleted({ id: 'some-id', groupName: 'Test' }))
).toEqual({
expect(reducer(previousState, taskDeleted({ id: 'some-id', groupName: 'Test' }))).toEqual({
schemaVersion: '1.0.0',
groups: [
{
@@ -447,9 +439,7 @@ it('should handle opening all tasks that are marked as completed', () => {
],
}
expect(
reducer(previousState, openAllCompleted({ groupName: 'Test' }))
).toEqual({
expect(reducer(previousState, openAllCompleted({ groupName: 'Test' }))).toEqual({
schemaVersion: '1.0.0',
groups: [
{
@@ -509,9 +499,7 @@ it('should handle clear all completed tasks', () => {
],
}
expect(
reducer(previousState, deleteAllCompleted({ groupName: 'Test' }))
).toEqual({
expect(reducer(previousState, deleteAllCompleted({ groupName: 'Test' }))).toEqual({
schemaVersion: '1.0.0',
groups: [
{
@@ -548,9 +536,7 @@ it('should handle loading tasks into the tasks store, if an invalid payload is p
}
expect(reducer(previousState, tasksLoaded('null'))).toEqual(previousState)
expect(reducer(previousState, tasksLoaded('undefined'))).toEqual(
previousState
)
expect(reducer(previousState, tasksLoaded('undefined'))).toEqual(previousState)
})
it('should initialize the storage with an empty object', () => {
@@ -670,9 +656,7 @@ it('should handle loading tasks into the tasks store, with a valid payload', ()
it('should handle adding a new task group', () => {
const previousState: TasksState = { schemaVersion: '1.0.0', groups: [] }
expect(
reducer(previousState, tasksGroupAdded({ groupName: 'New group' }))
).toEqual({
expect(reducer(previousState, tasksGroupAdded({ groupName: 'New group' }))).toEqual({
schemaVersion: '1.0.0',
groups: [
{
@@ -701,9 +685,7 @@ it('should handle adding an existing task group', () => {
],
}
expect(
reducer(previousState, tasksGroupAdded({ groupName: 'Existing group' }))
).toEqual(previousState)
expect(reducer(previousState, tasksGroupAdded({ groupName: 'Existing group' }))).toEqual(previousState)
})
it('should handle reordering tasks from the same section', () => {
@@ -744,8 +726,8 @@ it('should handle reordering tasks from the same section', () => {
swapTaskIndex: 0,
withTaskIndex: 1,
isSameSection: true,
})
)
}),
),
).toEqual({
schemaVersion: '1.0.0',
groups: [
@@ -814,8 +796,8 @@ it('should handle reordering tasks from different sections', () => {
swapTaskIndex: 0,
withTaskIndex: 1,
isSameSection: false,
})
)
}),
),
).toEqual({
schemaVersion: '1.0.0',
groups: [
@@ -893,7 +875,7 @@ it('should handle reordering task groups', () => {
tasksGroupReordered({
swapGroupIndex: 0,
withGroupIndex: 1,
})
}),
)
const expectedState = {
@@ -978,10 +960,7 @@ it('should handle deleting groups', () => {
],
}
const currentState = reducer(
previousState,
tasksGroupDeleted({ groupName: 'Testing' })
)
const currentState = reducer(previousState, tasksGroupDeleted({ groupName: 'Testing' }))
const expectedState = {
schemaVersion: '1.0.0',
@@ -1054,10 +1033,7 @@ it('should not merge the same group', () => {
],
}
const currentState = reducer(
previousState,
tasksGroupMerged({ groupName: 'Testing', mergeWith: 'Testing' })
)
const currentState = reducer(previousState, tasksGroupMerged({ groupName: 'Testing', mergeWith: 'Testing' }))
expect(currentState).toEqual(previousState)
})
@@ -1104,7 +1080,7 @@ it('should handle merging groups', () => {
const currentState = reducer(
previousState,
tasksGroupMerged({ groupName: 'Test group #3', mergeWith: 'Test group #2' })
tasksGroupMerged({ groupName: 'Test group #3', mergeWith: 'Test group #2' }),
)
expect(currentState).toMatchObject({
@@ -1171,10 +1147,7 @@ it('should handle renaming a group', () => {
],
}
const currentState = reducer(
previousState,
tasksGroupRenamed({ groupName: 'Testing', newName: 'Tested' })
)
const currentState = reducer(previousState, tasksGroupRenamed({ groupName: 'Testing', newName: 'Tested' }))
expect(currentState).toEqual({
schemaVersion: '1.0.0',
@@ -1245,10 +1218,7 @@ it("should rename a group and preserve it's current order", () => {
],
}
const currentState = reducer(
previousState,
tasksGroupRenamed({ groupName: '2nd group', newName: 'Middle group' })
)
const currentState = reducer(previousState, tasksGroupRenamed({ groupName: '2nd group', newName: 'Middle group' }))
expect(currentState).toMatchObject({
schemaVersion: '1.0.0',
@@ -1330,10 +1300,7 @@ it('should handle collapsing groups', () => {
],
}
const currentState = reducer(
previousState,
tasksGroupCollapsed({ groupName: 'Testing', collapsed: true })
)
const currentState = reducer(previousState, tasksGroupCollapsed({ groupName: 'Testing', collapsed: true }))
const expectedState = {
schemaVersion: '1.0.0',
@@ -1418,10 +1385,7 @@ it('should handle saving task draft for groups', () => {
],
}
const currentState = reducer(
previousState,
tasksGroupDraft({ groupName: 'Tests', draft: 'Remember to ...' })
)
const currentState = reducer(previousState, tasksGroupDraft({ groupName: 'Tests', draft: 'Remember to ...' }))
const expectedState = {
schemaVersion: '1.0.0',
@@ -1495,10 +1459,7 @@ it('should handle setting a group as last active', () => {
],
}
const currentState = reducer(
previousState,
tasksGroupLastActive({ groupName: 'Testing' })
)
const currentState = reducer(previousState, tasksGroupLastActive({ groupName: 'Testing' }))
expect(currentState).toMatchObject({
schemaVersion: '1.0.0',

View File

@@ -1,9 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import {
arrayMoveImmutable,
isJsonString,
parseMarkdownTasks,
} from '../../common/utils'
import { arrayMoveImmutable, isJsonString, parseMarkdownTasks } from '../../common/utils'
export type TasksState = {
schemaVersion: string
@@ -44,7 +40,7 @@ const tasksSlice = createSlice({
action: PayloadAction<{
task: { id: string; description: string }
groupName: string
}>
}>,
) {
const { groupName, task } = action.payload
const group = state.groups.find((item) => item.name === groupName)
@@ -63,7 +59,7 @@ const tasksSlice = createSlice({
action: PayloadAction<{
task: { id: string; description: string }
groupName: string
}>
}>,
) {
const { groupName, task } = action.payload
const group = state.groups.find((item) => item.name === groupName)
@@ -77,10 +73,7 @@ const tasksSlice = createSlice({
currentTask.description = task.description
currentTask.updatedAt = new Date()
},
taskDeleted(
state,
action: PayloadAction<{ id: string; groupName: string }>
) {
taskDeleted(state, action: PayloadAction<{ id: string; groupName: string }>) {
const { id, groupName } = action.payload
const group = state.groups.find((item) => item.name === groupName)
if (!group) {
@@ -88,10 +81,7 @@ const tasksSlice = createSlice({
}
group.tasks = group.tasks.filter((task) => task.id !== id)
},
taskToggled(
state,
action: PayloadAction<{ id: string; groupName: string }>
) {
taskToggled(state, action: PayloadAction<{ id: string; groupName: string }>) {
const { id, groupName } = action.payload
const group = state.groups.find((item) => item.name === groupName)
if (!group) {
@@ -140,10 +130,9 @@ const tasksSlice = createSlice({
swapTaskIndex: number
withTaskIndex: number
isSameSection: boolean
}>
}>,
) {
const { groupName, swapTaskIndex, withTaskIndex, isSameSection } =
action.payload
const { groupName, swapTaskIndex, withTaskIndex, isSameSection } = action.payload
if (!isSameSection) {
return
}
@@ -151,17 +140,13 @@ const tasksSlice = createSlice({
if (!group) {
return
}
group.tasks = arrayMoveImmutable(
group.tasks,
swapTaskIndex,
withTaskIndex
)
group.tasks = arrayMoveImmutable(group.tasks, swapTaskIndex, withTaskIndex)
},
tasksGroupAdded(
state,
action: PayloadAction<{
groupName: string
}>
}>,
) {
const { groupName } = action.payload
const group = state.groups.find((item) => item.name === groupName)
@@ -178,20 +163,16 @@ const tasksSlice = createSlice({
action: PayloadAction<{
swapGroupIndex: number
withGroupIndex: number
}>
}>,
) {
const { swapGroupIndex, withGroupIndex } = action.payload
state.groups = arrayMoveImmutable(
state.groups,
swapGroupIndex,
withGroupIndex
)
state.groups = arrayMoveImmutable(state.groups, swapGroupIndex, withGroupIndex)
},
tasksGroupDeleted(
state,
action: PayloadAction<{
groupName: string
}>
}>,
) {
const { groupName } = action.payload
state.groups = state.groups.filter((item) => item.name !== groupName)
@@ -201,7 +182,7 @@ const tasksSlice = createSlice({
action: PayloadAction<{
groupName: string
mergeWith: string
}>
}>,
) {
const { groupName, mergeWith } = action.payload
if (groupName === mergeWith) {
@@ -224,7 +205,7 @@ const tasksSlice = createSlice({
action: PayloadAction<{
groupName: string
newName: string
}>
}>,
) {
const { groupName, newName } = action.payload
if (groupName === newName) {
@@ -241,7 +222,7 @@ const tasksSlice = createSlice({
action: PayloadAction<{
groupName: string
collapsed: boolean
}>
}>,
) {
const { groupName, collapsed } = action.payload
const group = state.groups.find((item) => item.name === groupName)
@@ -255,7 +236,7 @@ const tasksSlice = createSlice({
action: PayloadAction<{
groupName: string
draft: string
}>
}>,
) {
const { groupName, draft } = action.payload
const group = state.groups.find((item) => item.name === groupName)
@@ -268,7 +249,7 @@ const tasksSlice = createSlice({
state,
action: PayloadAction<{
groupName: string
}>
}>,
) {
const { groupName } = action.payload
const group = state.groups.find((item) => item.name === groupName)
@@ -277,10 +258,7 @@ const tasksSlice = createSlice({
}
group.lastActive = new Date()
},
tasksLegacyContentMigrated(
state,
{ payload }: PayloadAction<{ continue: boolean }>
) {
tasksLegacyContentMigrated(state, { payload }: PayloadAction<{ continue: boolean }>) {
if (!state.legacyContent) {
return
}

View File

@@ -10,11 +10,7 @@ import styled from 'styled-components'
import { store } from './app/store'
import { useAppDispatch, useAppSelector } from './app/hooks'
import CreateGroup from './features/tasks/CreateGroup'
import {
setCanEdit,
setIsRunningOnMobile,
setSpellCheckerEnabled,
} from './features/settings/settings-slice'
import { setCanEdit, setIsRunningOnMobile, setSpellCheckerEnabled } from './features/settings/settings-slice'
import { tasksLoaded } from './features/tasks/tasks-slice'
import InvalidContentError from './features/tasks/InvalidContentError'
import MigrateLegacyContent from './features/tasks/MigrateLegacyContent'
@@ -69,8 +65,7 @@ const TaskEditor: React.FC = () => {
onNoteValueChange: async (currentNote: any) => {
note.current = currentNote
const editable =
!currentNote.content.appData['org.standardnotes.sn'].locked ?? true
const editable = !currentNote.content.appData['org.standardnotes.sn'].locked ?? true
const spellCheckEnabled = currentNote.content.spellcheck
dispatch(setCanEdit(editable))
@@ -106,16 +101,10 @@ const TaskEditor: React.FC = () => {
editorKit.current!.saveItemWithPresave(currentNote, () => {
const { schemaVersion, groups } = store.getState().tasks
currentNote.content.text = JSON.stringify(
{ schemaVersion, groups },
null,
2
)
currentNote.content.text = JSON.stringify({ schemaVersion, groups }, null, 2)
currentNote.content.preview_plain = getPlainPreview(groups)
currentNote.content.preview_html = renderToString(
<NotePreview groupedTasks={groups} />
)
currentNote.content.preview_html = renderToString(<NotePreview groupedTasks={groups} />)
})
}, [])
@@ -186,5 +175,5 @@ ReactDOM.render(
<TaskEditor />
</Provider>
</React.StrictMode>,
document.getElementById('root')
document.getElementById('root'),
)

View File

@@ -17,20 +17,12 @@ const defaultMockState: RootState = {
},
}
export function testRender(
ui: React.ReactElement,
renderOptions?: RenderOptions,
state?: Partial<RootState>
) {
export function testRender(ui: React.ReactElement, renderOptions?: RenderOptions, state?: Partial<RootState>) {
const mockStore = configureStore()({
...defaultMockState,
...state,
})
function Wrapper({
children,
}: {
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
}) {
function Wrapper({ children }: { children: React.ReactElement<any, string | React.JSXElementConstructor<any>> }) {
return <Provider store={mockStore}>{children}</Provider>
}
return {