diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/package.json b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/package.json index 148d05556..967618c8d 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/package.json +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/package.json @@ -16,7 +16,7 @@ "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "react-app-rewired start", - "test:coverage": "npm run test -- --coverage --watchAll", + "test:coverage": "npm run test -- --coverage --watchAll --no-silent", "eject": "react-scripts eject", "components:compile": "react-app-rewired build", "test": "react-app-rewired test --watchAll=false --silent", diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/components/RoundButton.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/components/RoundButton.tsx index 227cfdbba..507f38952 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/components/RoundButton.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/components/RoundButton.tsx @@ -1,11 +1,12 @@ type RoundButtonProps = { testId?: string onClick: () => void + size?: 'normal' | 'small' } -export const RoundButton: React.FC = ({ testId, onClick, children }) => { +export const RoundButton: React.FC = ({ testId, onClick, children, size = 'normal' }) => { return ( - ) diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/components/icons/AddIcon.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/components/icons/AddIcon.tsx index 52a3f6b1f..bc3ab3ac4 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/components/icons/AddIcon.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/components/icons/AddIcon.tsx @@ -1,7 +1,7 @@ export const AddIcon = () => { return ( { describe('groupTasksByCompletedStatus', () => { it('should return open tasks and completed tasks', () => { - const tasks: TaskPayload[] = [ + const tasks: TaskModel[] = [ { id: 'test-1', description: 'Testing #1', @@ -147,13 +148,15 @@ describe('getTaskArrayFromGroupedTasks', () => { }, ] - const groupedTasks: GroupPayload[] = [ + const groupedTasks: GroupModel[] = [ { name: 'Work', + sections: DEFAULT_SECTIONS, tasks: workTasks, }, { name: 'Personal', + sections: DEFAULT_SECTIONS, tasks: personalTasks, }, ] @@ -216,20 +219,22 @@ describe('getPlainPreview', () => { }, ] - const groupedTasks: GroupPayload[] = [ + const groupedTasks: GroupModel[] = [ { name: 'Work', + sections: DEFAULT_SECTIONS, tasks: workTasks, }, { name: 'Personal', + sections: DEFAULT_SECTIONS, tasks: personalTasks, }, ] 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: [], sections: [] }])).toBe('0/0 tasks completed') }) }) @@ -251,7 +256,7 @@ describe('parseMarkdownTasks', () => { - [x] Bar - [ ] Foobar` - expect(parseMarkdownTasks(payload)).toMatchObject({ + expect(parseMarkdownTasks(payload)).toMatchObject({ name: 'Checklist', tasks: [ { @@ -273,6 +278,19 @@ describe('parseMarkdownTasks', () => { createdAt: expect.any(Date), }, ], + sections: DEFAULT_SECTIONS, }) }) }) + +describe('arrayDefault', () => { + it('should fallback to default value', () => { + expect(arrayDefault({ defaultValue: [] })).toEqual([]) + expect(arrayDefault({ value: undefined, defaultValue: [] })).toEqual([]) + expect(arrayDefault({ value: [], defaultValue: ['test'] })).toEqual(['test']) + }) + + it('should return value', () => { + expect(arrayDefault({ value: ['test'], defaultValue: [] })).toEqual(['test']) + }) +}) diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/utils.ts b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/utils.ts index 0e839bf5f..bbb8be716 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/utils.ts +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/common/utils.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from 'uuid' -import { GroupPayload, TaskPayload } from '../features/tasks/tasks-slice' +import { DEFAULT_SECTIONS, GroupModel, TaskModel } from '../features/tasks/tasks-slice' export function arrayMoveMutable(array: any[], fromIndex: number, toIndex: number) { const startIndex = fromIndex < 0 ? array.length + fromIndex : fromIndex @@ -26,7 +26,7 @@ export function getPercentage(numberA: number, numberB: number): number { return Number(percentage.toFixed(2)) } -export function groupTasksByCompletedStatus(tasks: TaskPayload[]) { +export function groupTasksByCompletedStatus(tasks: TaskModel[]) { const openTasks = tasks.filter((task) => !task.completed) const completedTasks = tasks.filter((task) => task.completed) return { @@ -35,8 +35,8 @@ export function groupTasksByCompletedStatus(tasks: TaskPayload[]) { } } -export function getTaskArrayFromGroupedTasks(groupedTasks: GroupPayload[]): TaskPayload[] { - let taskArray: TaskPayload[] = [] +export function getTaskArrayFromGroupedTasks(groupedTasks: GroupModel[]): TaskModel[] { + let taskArray: TaskModel[] = [] groupedTasks.forEach((group) => { taskArray = taskArray.concat(group.tasks) @@ -52,14 +52,14 @@ export function truncateText(text: string, limit: number = 50) { return text.substring(0, limit) + '...' } -export function getPlainPreview(groupedTasks: GroupPayload[]) { +export function getPlainPreview(groupedTasks: GroupModel[]) { const allTasks = getTaskArrayFromGroupedTasks(groupedTasks) const { completedTasks } = groupTasksByCompletedStatus(allTasks) return `${completedTasks.length}/${allTasks.length} tasks completed` } -function createTaskFromLine(rawTask: string): TaskPayload | undefined { +function createTaskFromLine(rawTask: string): TaskModel | undefined { const IS_COMPLETED = /^- \[x\] /i const OPEN_PREFIX = '- [ ] ' @@ -77,7 +77,7 @@ function createTaskFromLine(rawTask: string): TaskPayload | undefined { } } -export function parseMarkdownTasks(payload?: string): GroupPayload | undefined { +export function parseMarkdownTasks(payload?: string): GroupModel | undefined { if (!payload) { return } @@ -88,7 +88,7 @@ export function parseMarkdownTasks(payload?: string): GroupPayload | undefined { } const lines = payload.split('\n') - const tasks: TaskPayload[] = [] + const tasks: TaskModel[] = [] lines .filter((line) => line.replace(/ /g, '').length > 0) @@ -102,6 +102,7 @@ export function parseMarkdownTasks(payload?: string): GroupPayload | undefined { return { name: 'Checklist', tasks, + sections: DEFAULT_SECTIONS, } } @@ -114,7 +115,7 @@ export function isJsonString(rawString: string) { return true } -export function isLastActiveGroup(allGroups: GroupPayload[], groupName: string): boolean { +export function isLastActiveGroup(allGroups: GroupModel[], groupName: string): boolean { if (allGroups.length === 0) { return true } @@ -131,3 +132,13 @@ export function isLastActiveGroup(allGroups: GroupPayload[], groupName: string): return lastActiveGroup.name === groupName } + +export function arrayDefault({ value, defaultValue }: { value?: any[]; defaultValue: any[] }) { + if (!value) { + return defaultValue + } + if (value.length === 0) { + return defaultValue + } + return value +} diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateGroup.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateGroup.test.tsx index 82e563a44..2cda03b87 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateGroup.test.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateGroup.test.tsx @@ -8,6 +8,7 @@ import { tasksGroupAdded } from './tasks-slice' const defaultTasksState = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'test', diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateTask.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateTask.test.tsx index d0e92d6cd..eefcebf79 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateTask.test.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateTask.test.tsx @@ -3,7 +3,7 @@ import { RootState } from '../../app/store' import { testRender } from '../../testUtils' import CreateTask from './CreateTask' -import { taskAdded } from './tasks-slice' +import { DEFAULT_SECTIONS, taskAdded } from './tasks-slice' jest.mock('uuid', () => { return { @@ -14,6 +14,7 @@ jest.mock('uuid', () => { const defaultGroup = { name: 'My default group', tasks: [], + sections: DEFAULT_SECTIONS, } it('renders a button by default', () => { diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateTask.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateTask.tsx index b1ac0ca5b..a8783c053 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateTask.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/CreateTask.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components' import { v4 as uuidv4 } from 'uuid' import { useAppDispatch, useAppSelector } from '../../app/hooks' -import { GroupPayload, taskAdded, tasksGroupDraft } from './tasks-slice' +import { GroupModel, taskAdded, tasksGroupDraft } from './tasks-slice' import { TextInput } from '../../common/components' import { DottedCircleIcon } from '../../common/components/icons' @@ -21,7 +21,7 @@ const Container = styled.div` ` type CreateTaskProps = { - group: GroupPayload + group: GroupModel } const CreateTask: React.FC = ({ group }) => { diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/MergeTaskGroups.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/MergeTaskGroups.test.tsx index 765459f0f..5cd6b9f58 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/MergeTaskGroups.test.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/MergeTaskGroups.test.tsx @@ -12,6 +12,7 @@ it('renders the alert dialog when no groups are available to merge', () => { const defaultState: Partial = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -44,6 +45,7 @@ it('renders the alert dialog when there are groups available to merge', () => { const defaultState: Partial = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -106,6 +108,7 @@ it('should close the dialog if no group is selected and the Merge button is clic const defaultState: Partial = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -168,6 +171,7 @@ it('should dispatch the action to merge groups', () => { const defaultState: Partial = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/NotePreview.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/NotePreview.test.tsx index 4fb55ed72..838012347 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/NotePreview.test.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/NotePreview.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import NotePreview from './NotePreview' -import { GroupPayload } from './tasks-slice' +import { DEFAULT_SECTIONS, GroupModel } from './tasks-slice' const workTasks = [ { @@ -44,7 +44,7 @@ const miscTasks = [ ] it('should render without tasks', () => { - const groupedTasks: GroupPayload[] = [] + const groupedTasks: GroupModel[] = [] render() @@ -71,10 +71,12 @@ it('should render with tasks', () => { { name: 'Work', tasks: workTasks, + sections: DEFAULT_SECTIONS, }, { name: 'Personal', tasks: personalTasks, + sections: DEFAULT_SECTIONS, }, ] @@ -103,14 +105,17 @@ it('should render a summary of the remaining group(s)', () => { { name: 'Work', tasks: workTasks, + sections: DEFAULT_SECTIONS, }, { name: 'Personal', tasks: personalTasks, + sections: DEFAULT_SECTIONS, }, { name: 'Misc', tasks: miscTasks, + sections: DEFAULT_SECTIONS, }, { name: 'Groceries', @@ -121,6 +126,7 @@ it('should render a summary of the remaining group(s)', () => { createdAt: new Date(), }, ], + sections: DEFAULT_SECTIONS, }, ] diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/NotePreview.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/NotePreview.tsx index b2ca8dbce..612f4fac1 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/NotePreview.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/NotePreview.tsx @@ -4,7 +4,7 @@ import { groupTasksByCompletedStatus, truncateText, } from '../../common/utils' -import { GroupPayload, TaskPayload } from './tasks-slice' +import { GroupModel, TaskModel } from './tasks-slice' const GROUPS_PREVIEW_LIMIT = 3 const MAX_GROUP_DESCRIPTION_LENGTH = 30 @@ -14,7 +14,7 @@ const Title: React.FC = ({ children }) => { } type GroupSummaryProps = { - groups: GroupPayload[] + groups: GroupModel[] } const GroupSummary: React.FC = ({ groups }) => { @@ -37,7 +37,7 @@ const GroupSummary: React.FC = ({ groups }) => { return (

{truncateText(group.name, MAX_GROUP_DESCRIPTION_LENGTH)} - + {totalCompletedTasks}/{totalTasks}

@@ -54,11 +54,11 @@ const GroupSummary: React.FC = ({ groups }) => { } type NotePreviewProps = { - groupedTasks: GroupPayload[] + groupedTasks: GroupModel[] } const NotePreview: React.FC = ({ groupedTasks }) => { - const allTasks: TaskPayload[] = getTaskArrayFromGroupedTasks(groupedTasks) + const allTasks: TaskModel[] = getTaskArrayFromGroupedTasks(groupedTasks) const { completedTasks } = groupTasksByCompletedStatus(allTasks) const percentage = getPercentage(allTasks.length, completedTasks.length) const roundedPercentage = Math.floor(percentage / 10) * 10 diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/RenameTaskGroups.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/RenameTaskGroups.test.tsx index ac5233a6d..81d992623 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/RenameTaskGroups.test.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/RenameTaskGroups.test.tsx @@ -12,6 +12,7 @@ it('renders the alert dialog with an input box', () => { const defaultState: Partial = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: defaultGroup, @@ -44,6 +45,7 @@ it('should dispatch the action to merge groups', () => { const defaultState: Partial = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: defaultGroup, @@ -107,6 +109,7 @@ it('should dispatch the action to merge groups on Enter press', () => { const defaultState: Partial = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: defaultGroup, diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskGroup.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskGroup.test.tsx index e470fe5f7..7a9ca70ac 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskGroup.test.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskGroup.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, screen } from '@testing-library/react' import { RootState } from '../../app/store' import { testRender } from '../../testUtils' import TaskGroup from './TaskGroup' +import { DEFAULT_SECTIONS } from './tasks-slice' const defaultGroup = { name: 'default group', @@ -20,6 +21,7 @@ const defaultGroup = { createdAt: new Date(), }, ], + sections: DEFAULT_SECTIONS, } it('renders the group name', () => { @@ -58,23 +60,23 @@ it('renders the element that is used to create a new task', () => { it('renders the element that is used to display the list of tasks', () => { testRender() - expect(screen.getByTestId('task-list')).toBeInTheDocument() + expect(screen.getByTestId('task-section-list')).toBeInTheDocument() }) it('collapses the group', () => { testRender() const createTask = screen.getByTestId('create-task-input') - const taskItemList = screen.getByTestId('task-list') + const taskSectionList = screen.getByTestId('task-section-list') expect(createTask).toBeVisible() - expect(taskItemList).toBeVisible() + expect(taskSectionList).toBeVisible() const collapseButton = screen.getByTestId('collapse-task-group') fireEvent.click(collapseButton) expect(createTask).not.toBeVisible() - expect(taskItemList).not.toBeVisible() + expect(taskSectionList).not.toBeVisible() }) it('shows group options', () => { diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskGroup.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskGroup.tsx index 08a2f5123..b53b0a609 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskGroup.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskGroup.tsx @@ -3,10 +3,10 @@ import styled from 'styled-components' import { useAppDispatch, useAppSelector } from '../../app/hooks' import { getPercentage } from '../../common/utils' -import { GroupPayload, tasksGroupCollapsed } from './tasks-slice' +import { GroupModel, tasksGroupCollapsed } from './tasks-slice' import CreateTask from './CreateTask' -import TaskItemList from './TaskItemList' +import TaskSectionList from './TaskSectionList' import TaskGroupOptions from './TaskGroupOptions' @@ -31,7 +31,7 @@ const CollapsableContainer = styled.div` ` type TaskGroupProps = { - group: GroupPayload + group: GroupModel isDragging: boolean isLast?: boolean style?: React.CSSProperties @@ -65,7 +65,7 @@ const TaskGroup: React.FC = ({ const allTasksCompleted = totalTasks > 0 && totalTasks === completedTasks function handleCollapse() { - dispatch(tasksGroupCollapsed({ groupName, collapsed: !collapsed })) + dispatch(tasksGroupCollapsed({ groupName, type: 'group', collapsed: !collapsed })) setCollapsed(!collapsed) } @@ -117,7 +117,7 @@ const TaskGroup: React.FC = ({ - + ) diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItem.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItem.test.tsx index acad2a532..f3008151d 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItem.test.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItem.test.tsx @@ -2,10 +2,10 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { testRender } from '../../testUtils' import TaskItem from './TaskItem' -import { taskDeleted, taskModified, TaskPayload, taskToggled } from './tasks-slice' +import { taskDeleted, TaskModel, taskModified, taskToggled } from './tasks-slice' const groupName = 'default group' -const task: TaskPayload = { +const task: TaskModel = { id: 'test-1', description: 'Testing #1', completed: false, diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItem.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItem.tsx index 6fea164d5..7fcf8a96b 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItem.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItem.tsx @@ -4,7 +4,7 @@ import { ChangeEvent, createRef, KeyboardEvent, useEffect, useState } from 'reac import styled from 'styled-components' import { useAppDispatch, useAppSelector, useDidMount } from '../../app/hooks' -import { taskDeleted, taskModified, TaskPayload, taskToggled } from './tasks-slice' +import { taskDeleted, TaskModel, taskModified, taskToggled } from './tasks-slice' import { CheckBoxInput, TextAreaInput } from '../../common/components' @@ -37,7 +37,7 @@ const Container = styled.div<{ completed?: boolean }>` ` export type TaskItemProps = { - task: TaskPayload + task: TaskModel groupName: string innerRef?: (element?: HTMLElement | null | undefined) => any } diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItemList.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItemList.test.tsx deleted file mode 100644 index 7eb962459..000000000 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItemList.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { screen, within } from '@testing-library/react' - -import { testRender } from '../../testUtils' -import TaskItemList from './TaskItemList' - -const defaultGroup = { - name: 'default group', - tasks: [ - { - id: 'test-1', - description: 'Testing #1', - completed: false, - createdAt: new Date(), - }, - { - id: 'test-2', - description: 'Testing #2', - completed: false, - createdAt: new Date(), - }, - ], -} - -it('renders the open tasks container', async () => { - testRender() - - const openTasksContainer = screen.getByTestId('open-tasks-container') - - expect(openTasksContainer).toBeInTheDocument() - expect(openTasksContainer).toHaveTextContent('open tasks') - - const taskItems = within(openTasksContainer).getAllByTestId('task-item') - expect(taskItems).toHaveLength(2) - - const completedTasksActions = screen.queryByTestId('completed-tasks-actions') - expect(completedTasksActions).not.toBeInTheDocument() -}) - -it('renders the completed tasks container', () => { - const groupWithCompletedTask = { - name: 'a new group', - tasks: [ - ...defaultGroup.tasks, - { - id: 'test-3', - description: 'Testing #3', - completed: true, - createdAt: new Date(), - }, - ], - } - - testRender() - - const completedTasksContainer = screen.getByTestId('completed-tasks-container') - - expect(completedTasksContainer).toBeInTheDocument() - expect(completedTasksContainer).toHaveTextContent('completed tasks') - - const taskItems = within(completedTasksContainer).getAllByTestId('task-item') - expect(taskItems).toHaveLength(1) - - const completedTasksActions = screen.getByTestId('completed-tasks-actions') - expect(completedTasksActions).toBeInTheDocument() -}) diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItemList.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItemList.tsx deleted file mode 100644 index 068e5e1fc..000000000 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskItemList.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react' -import { DragDropContext, DropResult } from 'react-beautiful-dnd' -import styled from 'styled-components' - -import { useAppDispatch } from '../../app/hooks' -import { groupTasksByCompletedStatus } from '../../common/utils' -import { GroupPayload, tasksReordered } from './tasks-slice' - -import CompletedTasksActions from './CompletedTasksActions' -import TasksContainer from './TasksContainer' - -const Container = styled.div` - position: relative; -` - -type TaskItemListProps = { - group: GroupPayload -} - -const TaskItemList: React.FC = ({ group }) => { - const dispatch = useAppDispatch() - - const { openTasks, completedTasks } = groupTasksByCompletedStatus(group.tasks) - - function onDragEnd(result: DropResult) { - const droppedOutsideList = !result.destination - if (droppedOutsideList) { - return - } - - const { source, destination } = result - if (!destination) { - return - } - - dispatch( - tasksReordered({ - groupName: group.name, - swapTaskIndex: source.index, - withTaskIndex: destination.index, - isSameSection: source.droppableId === destination.droppableId, - }), - ) - } - - return ( - - - - - - {completedTasks.length > 0 && } - - - - ) -} - -export default TaskItemList diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskSectionList.test.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskSectionList.test.tsx new file mode 100644 index 000000000..ee696db15 --- /dev/null +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskSectionList.test.tsx @@ -0,0 +1,110 @@ +import { screen, within } from '@testing-library/react' +import { RootState } from '../../app/store' + +import { testRender } from '../../testUtils' +import { DEFAULT_SECTIONS, GroupModel } from './tasks-slice' +import TaskSectionList from './TaskSectionList' + +const defaultGroup: GroupModel = { + name: 'default group', + tasks: [ + { + id: 'test-1', + description: 'Testing #1', + completed: false, + createdAt: new Date(), + }, + { + id: 'test-2', + description: 'Testing #2', + completed: false, + createdAt: new Date(), + }, + ], + sections: [ + { + id: 'open-tasks', + name: 'Open tasks', + }, + { + id: 'completed-tasks', + name: 'Completed tasks', + }, + ], +} + +it('renders the open tasks container', async () => { + testRender() + + const openTasksContainer = screen.getByTestId('open-tasks-section') + expect(openTasksContainer).toBeInTheDocument() + expect(openTasksContainer).toHaveTextContent('Open tasks') + + const taskItems = within(openTasksContainer).getAllByTestId('task-item') + expect(taskItems).toHaveLength(2) + + const completedTasksActions = screen.queryByTestId('completed-tasks-actions') + expect(completedTasksActions).not.toBeInTheDocument() +}) + +it('renders the completed tasks section', () => { + const groupWithCompletedTask = defaultGroup + groupWithCompletedTask.tasks.push({ + id: 'test-3', + description: 'Testing #3', + completed: true, + createdAt: new Date(), + }) + + testRender() + + const completedTasksSection = screen.getByTestId('completed-tasks-section') + expect(completedTasksSection).toBeInTheDocument() + expect(completedTasksSection).toHaveTextContent('Completed tasks') + + const taskItems = within(completedTasksSection).getAllByTestId('task-item') + expect(taskItems).toHaveLength(1) + + const completedTasksActions = screen.getByTestId('completed-tasks-actions') + expect(completedTasksActions).toBeInTheDocument() +}) + +it('renders default sections', () => { + const defaultState: Partial = { + settings: { + canEdit: true, + isRunningOnMobile: false, + spellCheckerEnabled: true, + }, + tasks: { + schemaVersion: '1.0.0', + defaultSections: DEFAULT_SECTIONS, + groups: [], + }, + } + + const group: GroupModel = { + name: 'Test group', + tasks: [ + ...defaultGroup.tasks, + { + id: 'test-3', + description: 'Testing #3', + completed: true, + createdAt: new Date(), + }, + ], + } + + testRender(, {}, defaultState) + + const completedTasksSection = screen.getByTestId('completed-tasks-section') + expect(completedTasksSection).toBeInTheDocument() + expect(completedTasksSection).toHaveTextContent('Completed tasks') + + const taskItems = within(completedTasksSection).getAllByTestId('task-item') + expect(taskItems).toHaveLength(1) + + const completedTasksActions = screen.getByTestId('completed-tasks-actions') + expect(completedTasksActions).toBeInTheDocument() +}) diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskSectionList.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskSectionList.tsx new file mode 100644 index 000000000..9b36d45ed --- /dev/null +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TaskSectionList.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { DragDropContext, DropResult } from 'react-beautiful-dnd' +import styled from 'styled-components' + +import { useAppDispatch, useAppSelector } from '../../app/hooks' +import { GroupModel, tasksReordered } from './tasks-slice' + +import CompletedTasksActions from './CompletedTasksActions' +import TasksSection from './TasksSection' + +const Container = styled.div` + position: relative; +` + +type TaskSectionListProps = { + group: GroupModel +} + +const TaskSectionList: React.FC = ({ group }) => { + const dispatch = useAppDispatch() + const defaultSections = useAppSelector((state) => state.tasks.defaultSections) + + function onDragEnd(result: DropResult) { + const droppedOutsideList = !result.destination + if (droppedOutsideList) { + return + } + + const { source, destination } = result + if (!destination) { + return + } + + dispatch( + tasksReordered({ + groupName: group.name, + swapTaskIndex: source.index, + withTaskIndex: destination.index, + isSameSection: source.droppableId === destination.droppableId, + }), + ) + } + + const sections = group.sections ?? defaultSections + + return ( + + {sections.map((section) => { + const tasks = group.tasks.filter((task) => + section.id === 'completed-tasks' ? task.completed === true : !task.completed, + ) + return ( + + + {section.id === 'completed-tasks' && tasks.length > 0 && } + + + ) + })} + + ) +} + +export default TaskSectionList diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksContainer.scss b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksSection.scss similarity index 100% rename from packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksContainer.scss rename to packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksSection.scss diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksContainer.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksSection.tsx similarity index 66% rename from packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksContainer.tsx rename to packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksSection.tsx index 3ce56f354..11e84b768 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksContainer.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/TasksSection.tsx @@ -1,35 +1,18 @@ -import './TasksContainer.scss' +import './TasksSection.scss' -import React from 'react' +import React, { useState } from 'react' import { Draggable, DraggingStyle, Droppable, NotDraggingStyle } from 'react-beautiful-dnd' import { CSSTransition, TransitionGroup } from 'react-transition-group' import styled from 'styled-components' -import { useAppSelector } from '../../app/hooks' -import { SubTitle } from '../../common/components' -import { TaskPayload } from './tasks-slice' +import { useAppDispatch, useAppSelector } from '../../app/hooks' +import { RoundButton, SubTitle } from '../../common/components' +import { SectionModel, TaskModel, tasksGroupCollapsed } from './tasks-slice' +import { ChevronDownIcon, ChevronUpIcon } from '../../common/components/icons' import TaskItem from './TaskItem' -const InnerTasksContainer = styled.div<{ - type: ContainerType - items: number -}>` - display: flex; - flex-direction: column; - - & > *:not(:last-child) { - margin-bottom: 5px; - } - - ${({ 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' : '')}; -` - -const SubTitleContainer = styled.div` +const SectionHeader = styled.div` align-items: center; display: flex; @@ -38,6 +21,23 @@ const SubTitleContainer = styled.div` } ` +const InnerTasksContainer = styled.div<{ collapsed: boolean }>` + display: ${({ collapsed }) => (collapsed ? 'none' : 'flex')}; + flex-direction: column; + + & > *:not(:last-child) { + margin-bottom: 5px; + } +` + +const OuterContainer = styled.div<{ addMargin: boolean; items: number; collapsed: boolean }>` + margin-bottom: ${({ addMargin, items, collapsed }) => (addMargin && items > 0 && !collapsed ? '10px' : '0')}; +` + +const ChildrenContainer = styled.div<{ addMargin: boolean; items: number }>` + margin-top: ${({ addMargin, items }) => (addMargin && items > 0 ? '15px' : '0')}; +` + const Wrapper = styled.div` color: var(--sn-stylekit-foreground-color); ` @@ -50,33 +50,48 @@ const getItemStyle = (isDragging: boolean, draggableStyle?: DraggingStyle | NotD }), }) -type ContainerType = 'open' | 'completed' - -type TasksContainerProps = { +type TasksSectionProps = { groupName: string - tasks: TaskPayload[] - type: ContainerType + tasks: TaskModel[] + section: SectionModel testId?: string } -const TasksContainer: React.FC = ({ groupName, tasks, type, testId, children }) => { +const TasksSection: React.FC = ({ groupName, tasks, section, testId, children }) => { + const dispatch = useAppDispatch() const canEdit = useAppSelector((state) => state.settings.canEdit) - const droppableId = `${type}-tasks-droppable` + const droppableId = `${section.id}-droppable` + + const [collapsed, setCollapsed] = useState(!!section.collapsed) + + const handleCollapse = () => { + dispatch(tasksGroupCollapsed({ groupName, type: section.id, collapsed: !collapsed })) + setCollapsed(!collapsed) + } return ( - + {(provided) => ( - - {type} tasks - + + {section.name} + {tasks.length > 0 && ( + + {!collapsed ? : } + + )} + React.cloneElement(child)}> {tasks.map((task, index) => { @@ -148,7 +163,9 @@ const TasksContainer: React.FC = ({ groupName, tasks, type, {provided.placeholder} - {children} + + {children} + )} @@ -156,4 +173,4 @@ const TasksContainer: React.FC = ({ groupName, tasks, type, ) } -export default TasksContainer +export default TasksSection diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/BaseMigration.spec.ts b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/BaseMigration.spec.ts new file mode 100644 index 000000000..4816d9ca9 --- /dev/null +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/BaseMigration.spec.ts @@ -0,0 +1,37 @@ +import BaseMigration, { PartialData } from './BaseMigration' + +class MockMigration extends BaseMigration { + override get version() { + return '0.0.0' + } + + override upgrade(data: PartialData) { + return data + } + + override downgrade(data: PartialData) { + return data + } +} + +describe('BaseMigration', () => { + const mockMigration = new MockMigration() + + it('should throw error if version is not in the semantic version scheme', () => { + expect(() => { + mockMigration.run({ schemaVersion: '0.0.0.0', groups: [], defaultSections: [] }) + }).toThrowError("'0.0.0.0' is not in the semantic version scheme: MAJOR.MINOR.PATCH") + }) + + it('should throw error if version is not a number', () => { + expect(() => { + mockMigration.run({ schemaVersion: 'a.0.0', groups: [], defaultSections: [] }) + }).toThrowError('MAJOR version should be a number') + expect(() => { + mockMigration.run({ schemaVersion: '0.a.0', groups: [], defaultSections: [] }) + }).toThrowError('MINOR version should be a number') + expect(() => { + mockMigration.run({ schemaVersion: '0.0.a', groups: [], defaultSections: [] }) + }).toThrowError('PATCH version should be a number') + }) +}) diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/BaseMigration.ts b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/BaseMigration.ts new file mode 100644 index 000000000..481d57d1c --- /dev/null +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/BaseMigration.ts @@ -0,0 +1,63 @@ +const SemanticVersionParts = ['MAJOR', 'MINOR', 'PATCH'] + +enum MigrationAction { + Upgrade = 'up', + Downgrade = 'down', + Nothing = 'nothing', +} + +export type PartialData = { + schemaVersion: string + defaultSections?: any[] + groups: any[] +} + +abstract class BaseMigration { + protected abstract get version(): string + protected abstract upgrade(data: PartialData): PartialData + protected abstract downgrade(data: PartialData): PartialData + + private parseVersion(version: string): number[] { + const versionScheme = version.split('.') + if (versionScheme.length !== SemanticVersionParts.length) { + throw Error(`'${version}' is not in the semantic version scheme: ${SemanticVersionParts.join('.')}`) + } + return versionScheme.map((value, index) => { + const number = Number(value) + if (isNaN(number)) { + throw Error(`${SemanticVersionParts[index]} version should be a number`) + } + return number + }) + } + + protected getAction(schemaVersion: string): MigrationAction { + const fromVersion = this.parseVersion(schemaVersion) + const toVersion = this.parseVersion(this.version) + + for (let index = 0; index < fromVersion.length; index++) { + if (fromVersion[index] < toVersion[index]) { + return MigrationAction.Upgrade + } + if (fromVersion[index] > toVersion[index]) { + return MigrationAction.Downgrade + } + } + return MigrationAction.Nothing + } + + public run(data: PartialData): PartialData { + const { schemaVersion } = data + const migrationAction = this.getAction(schemaVersion) + switch (migrationAction) { + case MigrationAction.Upgrade: + return this.upgrade(data) + case MigrationAction.Downgrade: + return this.downgrade(data) + default: + return data + } + } +} + +export default BaseMigration diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/MigrationService.spec.ts b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/MigrationService.spec.ts new file mode 100644 index 000000000..85070d50c --- /dev/null +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/MigrationService.spec.ts @@ -0,0 +1,150 @@ +import { TasksState } from '../tasks-slice' +import BaseMigration, { PartialData } from './BaseMigration' +import MigrationService from './MigrationService' + +class MockMigration extends BaseMigration { + override get version() { + return '1.0.123' + } + + override upgrade(data: PartialData) { + return { + ...data, + schemaVersion: this.version, + } + } + + override downgrade(data: PartialData) { + return { + ...data, + schemaVersion: this.version, + } + } +} + +describe('MigrationService', () => { + it('should upgrade 1.0.0 to 1.0.123', () => { + const testData: Partial = { + schemaVersion: '1.0.0', + groups: [ + { + name: 'Test group #1', + tasks: [ + { + id: 'some-id', + description: 'A simple task', + completed: false, + createdAt: new Date(), + }, + { + id: 'another-id', + description: 'Another simple task', + completed: true, + createdAt: new Date(), + }, + ], + collapsed: true, + }, + { + name: 'Test group #2', + tasks: [ + { + id: 'yet-another-id', + description: 'Yet another simple task', + completed: true, + createdAt: new Date(), + }, + ], + }, + ], + } + + const migrationClasses = [MockMigration] + const migrationService = new MigrationService(migrationClasses) + const result = migrationService.performMigrations(testData as any) + + expect(result).toEqual>({ + ...testData, + schemaVersion: '1.0.123', + }) + }) + + it('should do nothing if latest version', () => { + const testData: Partial = { + schemaVersion: '1.0.123', + groups: [ + { + name: 'Test group #1', + tasks: [ + { + id: 'some-id', + description: 'A simple task', + completed: false, + createdAt: new Date(), + }, + ], + collapsed: true, + }, + { + name: 'Test group #2', + tasks: [ + { + id: 'yet-another-id', + description: 'Yet another simple task', + completed: true, + createdAt: new Date(), + }, + ], + }, + ], + } + + const migrationClasses = [MockMigration] + const migrationService = new MigrationService(migrationClasses) + const result = migrationService.performMigrations(testData as any) + + expect(result).toBe(testData) + }) + + it('should downgrade if version > 1.0.123', () => { + const testData: Partial = { + schemaVersion: '1.0.130', + groups: [ + { + name: 'Test group #1', + tasks: [ + { + id: 'some-id', + description: 'A simple task', + completed: false, + createdAt: new Date(), + }, + ], + collapsed: true, + }, + { + name: 'Test group #2', + tasks: [ + { + id: 'yet-another-id', + description: 'Yet another simple task', + completed: true, + createdAt: new Date(), + }, + ], + }, + ], + } + + const migrationClasses = [MockMigration] + const migrationService = new MigrationService(migrationClasses) + const result = migrationService.performMigrations(testData as any) + + expect(result).toMatchObject( + expect.objectContaining({ + schemaVersion: '1.0.123', + groups: testData.groups, + }), + ) + }) +}) diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/MigrationService.ts b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/MigrationService.ts new file mode 100644 index 000000000..7435bee20 --- /dev/null +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/MigrationService.ts @@ -0,0 +1,25 @@ +import { PartialData } from './BaseMigration' +import { MigrationClasses } from './versions' + +class MigrationService { + private migrationClasses: any[] + + constructor(migrationClasses?: any[]) { + this.migrationClasses = migrationClasses ?? MigrationClasses + } + + private getMigrationInstances() { + return this.migrationClasses.map((migrationClass) => { + return new migrationClass() + }) + } + + public performMigrations(data: PartialData) { + this.getMigrationInstances().forEach((migration) => { + data = migration.run(data) + }) + return data + } +} + +export default MigrationService diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/versions/index.ts b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/versions/index.ts new file mode 100644 index 000000000..78e2e4848 --- /dev/null +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/migrations/versions/index.ts @@ -0,0 +1 @@ +export const MigrationClasses: any[] = [] diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/tasks-slice.test.ts b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/tasks-slice.test.ts index 1f2013179..35981e3e7 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/tasks-slice.test.ts +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/tasks-slice.test.ts @@ -1,6 +1,7 @@ -import type { TasksState } from './tasks-slice' import reducer, { + DEFAULT_SECTIONS, deleteAllCompleted, + LATEST_SCHEMA_VERSION, openAllCompleted, taskAdded, taskDeleted, @@ -15,6 +16,7 @@ import reducer, { tasksGroupReordered, tasksLoaded, tasksReordered, + TasksState, taskToggled, } from './tasks-slice' @@ -23,11 +25,11 @@ it('should return the initial state', () => { reducer(undefined, { type: undefined, }), - ).toEqual({ schemaVersion: '1.0.0', groups: [] }) + ).toEqual({ schemaVersion: LATEST_SCHEMA_VERSION, groups: [], defaultSections: [] }) }) it('should handle a task being added to a non-existing group', () => { - const previousState: TasksState = { schemaVersion: '1.0.0', groups: [] } + const previousState: TasksState = { schemaVersion: '1.0.0', groups: [], defaultSections: [] } expect( reducer( @@ -37,8 +39,9 @@ it('should handle a task being added to a non-existing group', () => { groupName: 'Test', }), ), - ).toEqual({ + ).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [], }) }) @@ -46,6 +49,7 @@ it('should handle a task being added to a non-existing group', () => { it('should handle a task being added to the existing tasks store', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -69,8 +73,9 @@ it('should handle a task being added to the existing tasks store', () => { groupName: 'Test', }), ), - ).toEqual({ + ).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -96,6 +101,7 @@ it('should handle a task being added to the existing tasks store', () => { it('should handle an existing task being modified', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -107,6 +113,7 @@ it('should handle an existing task being modified', () => { createdAt: new Date(), }, ], + sections: DEFAULT_SECTIONS, }, ], } @@ -119,8 +126,9 @@ it('should handle an existing task being modified', () => { groupName: 'Test', }), ), - ).toEqual({ + ).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -133,6 +141,7 @@ it('should handle an existing task being modified', () => { updatedAt: expect.any(Date), }, ], + sections: DEFAULT_SECTIONS, }, ], }) @@ -141,6 +150,7 @@ it('should handle an existing task being modified', () => { it('should not modify tasks if an invalid id is provided', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -164,8 +174,9 @@ it('should not modify tasks if an invalid id is provided', () => { groupName: 'Test', }), ), - ).toEqual({ + ).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -185,6 +196,7 @@ it('should not modify tasks if an invalid id is provided', () => { it('should keep completed field as-is, if task is modified', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -211,8 +223,9 @@ it('should keep completed field as-is, if task is modified', () => { groupName: 'Test', }), ), - ).toEqual({ + ).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -233,6 +246,7 @@ it('should keep completed field as-is, if task is modified', () => { it('should handle an existing task being toggled', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -248,8 +262,9 @@ 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', + defaultSections: [], groups: [ { name: 'Test', @@ -271,6 +286,7 @@ it('should handle an existing task being toggled', () => { test('toggled tasks should be on top of the list', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -298,8 +314,9 @@ 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', + defaultSections: [], groups: [ { name: 'Test', @@ -333,6 +350,7 @@ test('toggled tasks should be on top of the list', () => { it('should handle an existing completed task being toggled', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -348,8 +366,9 @@ 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', + defaultSections: [], groups: [ { name: 'Test', @@ -370,6 +389,7 @@ it('should handle an existing completed task being toggled', () => { it('should handle an existing task being deleted', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -391,8 +411,9 @@ 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', + defaultSections: [], groups: [ { name: 'Test', @@ -412,6 +433,7 @@ it('should handle an existing task being deleted', () => { it('should handle opening all tasks that are marked as completed', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -439,8 +461,9 @@ 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', + defaultSections: [], groups: [ { name: 'Test', @@ -472,6 +495,7 @@ it('should handle opening all tasks that are marked as completed', () => { it('should handle clear all completed tasks', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -499,8 +523,9 @@ 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', + defaultSections: [], groups: [ { name: 'Test', @@ -520,6 +545,7 @@ it('should handle clear all completed tasks', () => { it('should handle loading tasks into the tasks store, if an invalid payload is provided', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -535,21 +561,23 @@ it('should handle loading tasks into the tasks store, if an invalid payload is p ], } - expect(reducer(previousState, tasksLoaded('null'))).toEqual({ - schemaVersion: '1.0.0', + expect(reducer(previousState, tasksLoaded('null'))).toEqual({ + schemaVersion: LATEST_SCHEMA_VERSION, + defaultSections: DEFAULT_SECTIONS, groups: [], - initialized: true + initialized: true, }) - expect(reducer(previousState, tasksLoaded('undefined'))).toMatchObject({ + expect(reducer(previousState, tasksLoaded('undefined'))).toMatchObject({ ...previousState, initialized: false, - lastError: expect.stringContaining('An error has occurred while parsing the note\'s content') + lastError: expect.stringContaining("An error has occurred while parsing the note's content"), }) }) it('should initialize the storage with an empty object', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -565,8 +593,9 @@ it('should initialize the storage with an empty object', () => { ], } - expect(reducer(previousState, tasksLoaded(''))).toEqual({ - schemaVersion: '1.0.0', + expect(reducer(previousState, tasksLoaded(''))).toEqual({ + schemaVersion: LATEST_SCHEMA_VERSION, + defaultSections: DEFAULT_SECTIONS, groups: [], initialized: true, }) @@ -574,12 +603,13 @@ it('should initialize the storage with an empty object', () => { it('should handle loading tasks into the tasks store, with a valid payload', () => { const previousState: TasksState = { - schemaVersion: '1.0.0', + schemaVersion: LATEST_SCHEMA_VERSION, + defaultSections: DEFAULT_SECTIONS, groups: [], } - const tasksPayload: TasksState = { - schemaVersion: '2.0.0', + const tasksPayload: Partial = { + schemaVersion: LATEST_SCHEMA_VERSION, groups: [ { name: 'Test', @@ -608,8 +638,9 @@ it('should handle loading tasks into the tasks store, with a valid payload', () } const serializedPayload = JSON.stringify(tasksPayload) - expect(reducer(previousState, tasksLoaded(serializedPayload))).toEqual({ - schemaVersion: '2.0.0', + expect(reducer(previousState, tasksLoaded(serializedPayload))).toEqual({ + schemaVersion: LATEST_SCHEMA_VERSION, + defaultSections: DEFAULT_SECTIONS, groups: [ { name: 'Test', @@ -639,11 +670,43 @@ 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: [] } +it('should set defaultSections property if not provided', () => { + const previousState: TasksState = { + schemaVersion: LATEST_SCHEMA_VERSION, + defaultSections: [], + groups: [], + } - expect(reducer(previousState, tasksGroupAdded({ groupName: 'New group' }))).toEqual({ + const tasksPayload: Partial = { + schemaVersion: LATEST_SCHEMA_VERSION, + groups: [ + { + name: 'Test', + tasks: [], + }, + ], + } + + const serializedPayload = JSON.stringify(tasksPayload) + expect(reducer(previousState, tasksLoaded(serializedPayload))).toEqual({ + schemaVersion: LATEST_SCHEMA_VERSION, + defaultSections: DEFAULT_SECTIONS, + groups: [ + { + name: 'Test', + tasks: [], + }, + ], + initialized: true, + }) +}) + +it('should handle adding a new task group', () => { + const previousState: TasksState = { schemaVersion: '1.0.0', groups: [], defaultSections: [] } + + expect(reducer(previousState, tasksGroupAdded({ groupName: 'New group' }))).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'New group', @@ -656,6 +719,7 @@ it('should handle adding a new task group', () => { it('should handle adding an existing task group', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Existing group', @@ -677,6 +741,7 @@ it('should handle adding an existing task group', () => { it('should handle reordering tasks from the same section', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -714,8 +779,9 @@ it('should handle reordering tasks from the same section', () => { isSameSection: true, }), ), - ).toEqual({ + ).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -747,6 +813,7 @@ it('should handle reordering tasks from the same section', () => { it('should handle reordering tasks from different sections', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -784,8 +851,9 @@ it('should handle reordering tasks from different sections', () => { isSameSection: false, }), ), - ).toEqual({ + ).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -819,6 +887,7 @@ it('should handle reordering task groups', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -864,8 +933,9 @@ it('should handle reordering task groups', () => { }), ) - const expectedState = { + const expectedState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Testing', @@ -909,6 +979,7 @@ it('should handle reordering task groups', () => { it('should handle deleting groups', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -948,8 +1019,9 @@ it('should handle deleting groups', () => { const currentState = reducer(previousState, tasksGroupDeleted({ groupName: 'Testing' })) - const expectedState = { + const expectedState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -982,6 +1054,7 @@ it('should handle deleting groups', () => { it('should not merge the same group', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1027,6 +1100,7 @@ it('should not merge the same group', () => { it('should handle merging groups', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test group #1', @@ -1069,8 +1143,9 @@ it('should handle merging groups', () => { tasksGroupMerged({ groupName: 'Test group #3', mergeWith: 'Test group #2' }), ) - expect(currentState).toMatchObject({ + expect(currentState).toMatchObject({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test group #1', @@ -1107,6 +1182,7 @@ it('should handle merging groups', () => { it('should handle renaming a group', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1135,8 +1211,9 @@ it('should handle renaming a group', () => { const currentState = reducer(previousState, tasksGroupRenamed({ groupName: 'Testing', newName: 'Tested' })) - expect(currentState).toEqual({ + expect(currentState).toEqual({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1167,6 +1244,7 @@ it('should handle renaming a group', () => { it("should rename a group and preserve it's current order", () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: '1st group', @@ -1206,8 +1284,9 @@ it("should rename a group and preserve it's current order", () => { const currentState = reducer(previousState, tasksGroupRenamed({ groupName: '2nd group', newName: 'Middle group' })) - expect(currentState).toMatchObject({ + expect(currentState).toMatchObject({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: '1st group', @@ -1249,6 +1328,7 @@ it("should rename a group and preserve it's current order", () => { it('should handle collapsing groups', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1286,10 +1366,14 @@ it('should handle collapsing groups', () => { ], } - const currentState = reducer(previousState, tasksGroupCollapsed({ groupName: 'Testing', collapsed: true })) + const currentState = reducer( + previousState, + tasksGroupCollapsed({ groupName: 'Testing', type: 'group', collapsed: true }), + ) - const expectedState = { + const expectedState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1334,6 +1418,7 @@ it('should handle collapsing groups', () => { it('should handle saving task draft for groups', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1373,8 +1458,9 @@ it('should handle saving task draft for groups', () => { const currentState = reducer(previousState, tasksGroupDraft({ groupName: 'Tests', draft: 'Remember to ...' })) - const expectedState = { + const expectedState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1419,6 +1505,7 @@ it('should handle saving task draft for groups', () => { it('should handle setting a group as last active', () => { const previousState: TasksState = { schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1447,8 +1534,9 @@ it('should handle setting a group as last active', () => { const currentState = reducer(previousState, tasksGroupLastActive({ groupName: 'Testing' })) - expect(currentState).toMatchObject({ + expect(currentState).toMatchObject({ schemaVersion: '1.0.0', + defaultSections: [], groups: [ { name: 'Test', @@ -1480,7 +1568,8 @@ it('should handle setting a group as last active', () => { it('should detect and load legacy content', () => { const payload = '- [ ] Foo bar' expect(reducer(undefined, tasksLoaded(payload))).toMatchObject({ - schemaVersion: '1.0.0', + schemaVersion: LATEST_SCHEMA_VERSION, + defaultSections: [], initialized: false, groups: [], legacyContent: { diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/tasks-slice.ts b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/tasks-slice.ts index f9ca9726c..c365ab746 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/tasks-slice.ts +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/features/tasks/tasks-slice.ts @@ -1,20 +1,34 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { arrayMoveImmutable, isJsonString, parseMarkdownTasks } from '../../common/utils' +import { arrayDefault, arrayMoveImmutable, isJsonString, parseMarkdownTasks } from '../../common/utils' + +export const LATEST_SCHEMA_VERSION = '1.0.0' +export const DEFAULT_SECTIONS: SectionModel[] = [ + { + id: 'open-tasks', + name: 'Open tasks', + }, + { + id: 'completed-tasks', + name: 'Completed tasks', + }, +] export type TasksState = { schemaVersion: string - groups: GroupPayload[] + groups: GroupModel[] + defaultSections: SectionModel[] initialized?: boolean - legacyContent?: GroupPayload + legacyContent?: GroupModel lastError?: string } const initialState: TasksState = { - schemaVersion: '1.0.0', + schemaVersion: LATEST_SCHEMA_VERSION, + defaultSections: [], groups: [], } -export type TaskPayload = { +export type TaskModel = { id: string description: string completed?: boolean @@ -23,12 +37,19 @@ export type TaskPayload = { completedAt?: Date } -export type GroupPayload = { +export type SectionModel = { + id: string + name: string + collapsed?: boolean +} + +export type GroupModel = { name: string collapsed?: boolean draft?: string lastActive?: Date - tasks: TaskPayload[] + tasks: TaskModel[] + sections?: SectionModel[] } const tasksSlice = createSlice({ @@ -221,15 +242,27 @@ const tasksSlice = createSlice({ state, action: PayloadAction<{ groupName: string + type: 'group' | 'open-tasks' | 'completed-tasks' | string collapsed: boolean }>, ) { - const { groupName, collapsed } = action.payload + const { groupName, type, collapsed } = action.payload const group = state.groups.find((item) => item.name === groupName) if (!group) { return } - group.collapsed = collapsed + if (type === 'group') { + group.collapsed = collapsed + return + } + if (!group.sections) { + group.sections = state.defaultSections.map((section) => ({ id: section.id, name: section.name })) + } + const section = group.sections.find((item) => item.id === type) + if (!section) { + return + } + section.collapsed = collapsed }, tasksGroupDraft( state, @@ -294,14 +327,16 @@ const tasksSlice = createSlice({ } const parsedState = JSON.parse(payload) as TasksState - const newState: TasksState = { - schemaVersion: parsedState?.schemaVersion ?? '1.0.0', + let newState: TasksState = { + schemaVersion: parsedState?.schemaVersion ?? LATEST_SCHEMA_VERSION, + defaultSections: arrayDefault({ value: parsedState?.defaultSections, defaultValue: DEFAULT_SECTIONS }), groups: parsedState?.groups ?? [], } if (newState !== initialState) { state.schemaVersion = newState.schemaVersion state.groups = newState.groups + state.defaultSections = newState.defaultSections state.initialized = true delete state.lastError } diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/index.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/index.tsx index c3952e2cd..e863e458a 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/index.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/index.tsx @@ -100,8 +100,8 @@ const TaskEditor: React.FC = () => { } editorKit.current!.saveItemWithPresave(currentNote, () => { - const { schemaVersion, groups } = store.getState().tasks - currentNote.content.text = JSON.stringify({ schemaVersion, groups }, null, 2) + const { schemaVersion, groups, defaultSections } = store.getState().tasks + currentNote.content.text = JSON.stringify({ schemaVersion, groups, defaultSections }, null, 2) currentNote.content.preview_plain = getPlainPreview(groups) currentNote.content.preview_html = renderToString() diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/stylesheets/main.scss b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/stylesheets/main.scss index 427da2f99..bb549df20 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/stylesheets/main.scss +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/stylesheets/main.scss @@ -28,7 +28,7 @@ html { margin-right: 0.75rem; } - .sn-icon.sm { + .sn-icon.small { height: 0.875rem; width: 0.875rem; } @@ -56,6 +56,12 @@ html { background-color: inherit; border-width: 1px; } + + &.small { + height: 1.25rem; + min-width: 1.25rem; + width: 1.25rem; + } } .pt-1px { diff --git a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/testUtils.tsx b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/testUtils.tsx index 543c03ad8..8761446ff 100644 --- a/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/testUtils.tsx +++ b/packages/components/src/Packages/Editors/org.standardnotes.advanced-checklist/src/testUtils.tsx @@ -8,6 +8,7 @@ import { RootState } from './app/store' const defaultMockState: RootState = { tasks: { schemaVersion: '1.0.0', + defaultSections: [], groups: [], }, settings: {