feat(advanced checklist): collapsible group sections (#1167)
* feat(advanced checklist): collapsible group sections * fix: type checking * fix: type checking for migrations * fix: remove stale files * fix: type checking * fix: copy default sections when collapsing sections * chore: format Co-authored-by: Johnny Almonte <johnny243@users.noreply.github.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "react-app-rewired start",
|
"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",
|
"eject": "react-scripts eject",
|
||||||
"components:compile": "react-app-rewired build",
|
"components:compile": "react-app-rewired build",
|
||||||
"test": "react-app-rewired test --watchAll=false --silent",
|
"test": "react-app-rewired test --watchAll=false --silent",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
type RoundButtonProps = {
|
type RoundButtonProps = {
|
||||||
testId?: string
|
testId?: string
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
|
size?: 'normal' | 'small'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RoundButton: React.FC<RoundButtonProps> = ({ testId, onClick, children }) => {
|
export const RoundButton: React.FC<RoundButtonProps> = ({ testId, onClick, children, size = 'normal' }) => {
|
||||||
return (
|
return (
|
||||||
<button data-testid={testId} className="sn-icon-button" onClick={onClick}>
|
<button data-testid={testId} className={`sn-icon-button ${size}`} onClick={onClick}>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const AddIcon = () => {
|
export const AddIcon = () => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className="sn-icon sm block"
|
className="sn-icon small block"
|
||||||
fill="none"
|
fill="none"
|
||||||
height="14"
|
height="14"
|
||||||
viewBox="0 0 14 14"
|
viewBox="0 0 14 14"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { GroupPayload, TaskPayload } from '../features/tasks/tasks-slice'
|
import { DEFAULT_SECTIONS, GroupModel, TaskModel } from '../features/tasks/tasks-slice'
|
||||||
import {
|
import {
|
||||||
|
arrayDefault,
|
||||||
arrayMoveImmutable,
|
arrayMoveImmutable,
|
||||||
arrayMoveMutable,
|
arrayMoveMutable,
|
||||||
getPercentage,
|
getPercentage,
|
||||||
@@ -86,7 +87,7 @@ describe('getPercentage', () => {
|
|||||||
|
|
||||||
describe('groupTasksByCompletedStatus', () => {
|
describe('groupTasksByCompletedStatus', () => {
|
||||||
it('should return open tasks and completed tasks', () => {
|
it('should return open tasks and completed tasks', () => {
|
||||||
const tasks: TaskPayload[] = [
|
const tasks: TaskModel[] = [
|
||||||
{
|
{
|
||||||
id: 'test-1',
|
id: 'test-1',
|
||||||
description: 'Testing #1',
|
description: 'Testing #1',
|
||||||
@@ -147,13 +148,15 @@ describe('getTaskArrayFromGroupedTasks', () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const groupedTasks: GroupPayload[] = [
|
const groupedTasks: GroupModel[] = [
|
||||||
{
|
{
|
||||||
name: 'Work',
|
name: 'Work',
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
tasks: workTasks,
|
tasks: workTasks,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Personal',
|
name: 'Personal',
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
tasks: personalTasks,
|
tasks: personalTasks,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -216,20 +219,22 @@ describe('getPlainPreview', () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const groupedTasks: GroupPayload[] = [
|
const groupedTasks: GroupModel[] = [
|
||||||
{
|
{
|
||||||
name: 'Work',
|
name: 'Work',
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
tasks: workTasks,
|
tasks: workTasks,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Personal',
|
name: 'Personal',
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
tasks: personalTasks,
|
tasks: personalTasks,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(getPlainPreview(groupedTasks)).toBe('2/5 tasks completed')
|
expect(getPlainPreview(groupedTasks)).toBe('2/5 tasks completed')
|
||||||
expect(getPlainPreview([])).toBe('0/0 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
|
- [x] Bar
|
||||||
- [ ] Foobar`
|
- [ ] Foobar`
|
||||||
|
|
||||||
expect(parseMarkdownTasks(payload)).toMatchObject<GroupPayload>({
|
expect(parseMarkdownTasks(payload)).toMatchObject<GroupModel>({
|
||||||
name: 'Checklist',
|
name: 'Checklist',
|
||||||
tasks: [
|
tasks: [
|
||||||
{
|
{
|
||||||
@@ -273,6 +278,19 @@ describe('parseMarkdownTasks', () => {
|
|||||||
createdAt: expect.any(Date),
|
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'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
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) {
|
export function arrayMoveMutable(array: any[], fromIndex: number, toIndex: number) {
|
||||||
const startIndex = fromIndex < 0 ? array.length + fromIndex : fromIndex
|
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))
|
return Number(percentage.toFixed(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupTasksByCompletedStatus(tasks: TaskPayload[]) {
|
export function groupTasksByCompletedStatus(tasks: TaskModel[]) {
|
||||||
const openTasks = tasks.filter((task) => !task.completed)
|
const openTasks = tasks.filter((task) => !task.completed)
|
||||||
const completedTasks = tasks.filter((task) => task.completed)
|
const completedTasks = tasks.filter((task) => task.completed)
|
||||||
return {
|
return {
|
||||||
@@ -35,8 +35,8 @@ export function groupTasksByCompletedStatus(tasks: TaskPayload[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTaskArrayFromGroupedTasks(groupedTasks: GroupPayload[]): TaskPayload[] {
|
export function getTaskArrayFromGroupedTasks(groupedTasks: GroupModel[]): TaskModel[] {
|
||||||
let taskArray: TaskPayload[] = []
|
let taskArray: TaskModel[] = []
|
||||||
|
|
||||||
groupedTasks.forEach((group) => {
|
groupedTasks.forEach((group) => {
|
||||||
taskArray = taskArray.concat(group.tasks)
|
taskArray = taskArray.concat(group.tasks)
|
||||||
@@ -52,14 +52,14 @@ export function truncateText(text: string, limit: number = 50) {
|
|||||||
return text.substring(0, limit) + '...'
|
return text.substring(0, limit) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlainPreview(groupedTasks: GroupPayload[]) {
|
export function getPlainPreview(groupedTasks: GroupModel[]) {
|
||||||
const allTasks = getTaskArrayFromGroupedTasks(groupedTasks)
|
const allTasks = getTaskArrayFromGroupedTasks(groupedTasks)
|
||||||
const { completedTasks } = groupTasksByCompletedStatus(allTasks)
|
const { completedTasks } = groupTasksByCompletedStatus(allTasks)
|
||||||
|
|
||||||
return `${completedTasks.length}/${allTasks.length} tasks completed`
|
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 IS_COMPLETED = /^- \[x\] /i
|
||||||
const OPEN_PREFIX = '- [ ] '
|
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) {
|
if (!payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ export function parseMarkdownTasks(payload?: string): GroupPayload | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lines = payload.split('\n')
|
const lines = payload.split('\n')
|
||||||
const tasks: TaskPayload[] = []
|
const tasks: TaskModel[] = []
|
||||||
|
|
||||||
lines
|
lines
|
||||||
.filter((line) => line.replace(/ /g, '').length > 0)
|
.filter((line) => line.replace(/ /g, '').length > 0)
|
||||||
@@ -102,6 +102,7 @@ export function parseMarkdownTasks(payload?: string): GroupPayload | undefined {
|
|||||||
return {
|
return {
|
||||||
name: 'Checklist',
|
name: 'Checklist',
|
||||||
tasks,
|
tasks,
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ export function isJsonString(rawString: string) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLastActiveGroup(allGroups: GroupPayload[], groupName: string): boolean {
|
export function isLastActiveGroup(allGroups: GroupModel[], groupName: string): boolean {
|
||||||
if (allGroups.length === 0) {
|
if (allGroups.length === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -131,3 +132,13 @@ export function isLastActiveGroup(allGroups: GroupPayload[], groupName: string):
|
|||||||
|
|
||||||
return lastActiveGroup.name === groupName
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { tasksGroupAdded } from './tasks-slice'
|
|||||||
const defaultTasksState = {
|
const defaultTasksState = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RootState } from '../../app/store'
|
|||||||
|
|
||||||
import { testRender } from '../../testUtils'
|
import { testRender } from '../../testUtils'
|
||||||
import CreateTask from './CreateTask'
|
import CreateTask from './CreateTask'
|
||||||
import { taskAdded } from './tasks-slice'
|
import { DEFAULT_SECTIONS, taskAdded } from './tasks-slice'
|
||||||
|
|
||||||
jest.mock('uuid', () => {
|
jest.mock('uuid', () => {
|
||||||
return {
|
return {
|
||||||
@@ -14,6 +14,7 @@ jest.mock('uuid', () => {
|
|||||||
const defaultGroup = {
|
const defaultGroup = {
|
||||||
name: 'My default group',
|
name: 'My default group',
|
||||||
tasks: [],
|
tasks: [],
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
}
|
}
|
||||||
|
|
||||||
it('renders a button by default', () => {
|
it('renders a button by default', () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import styled from 'styled-components'
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks'
|
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 { TextInput } from '../../common/components'
|
||||||
import { DottedCircleIcon } from '../../common/components/icons'
|
import { DottedCircleIcon } from '../../common/components/icons'
|
||||||
@@ -21,7 +21,7 @@ const Container = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
type CreateTaskProps = {
|
type CreateTaskProps = {
|
||||||
group: GroupPayload
|
group: GroupModel
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateTask: React.FC<CreateTaskProps> = ({ group }) => {
|
const CreateTask: React.FC<CreateTaskProps> = ({ group }) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ it('renders the alert dialog when no groups are available to merge', () => {
|
|||||||
const defaultState: Partial<RootState> = {
|
const defaultState: Partial<RootState> = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -44,6 +45,7 @@ it('renders the alert dialog when there are groups available to merge', () => {
|
|||||||
const defaultState: Partial<RootState> = {
|
const defaultState: Partial<RootState> = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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<RootState> = {
|
const defaultState: Partial<RootState> = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -168,6 +171,7 @@ it('should dispatch the action to merge groups', () => {
|
|||||||
const defaultState: Partial<RootState> = {
|
const defaultState: Partial<RootState> = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import NotePreview from './NotePreview'
|
import NotePreview from './NotePreview'
|
||||||
import { GroupPayload } from './tasks-slice'
|
import { DEFAULT_SECTIONS, GroupModel } from './tasks-slice'
|
||||||
|
|
||||||
const workTasks = [
|
const workTasks = [
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ const miscTasks = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
it('should render without tasks', () => {
|
it('should render without tasks', () => {
|
||||||
const groupedTasks: GroupPayload[] = []
|
const groupedTasks: GroupModel[] = []
|
||||||
|
|
||||||
render(<NotePreview groupedTasks={groupedTasks} />)
|
render(<NotePreview groupedTasks={groupedTasks} />)
|
||||||
|
|
||||||
@@ -71,10 +71,12 @@ it('should render with tasks', () => {
|
|||||||
{
|
{
|
||||||
name: 'Work',
|
name: 'Work',
|
||||||
tasks: workTasks,
|
tasks: workTasks,
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Personal',
|
name: 'Personal',
|
||||||
tasks: personalTasks,
|
tasks: personalTasks,
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -103,14 +105,17 @@ it('should render a summary of the remaining group(s)', () => {
|
|||||||
{
|
{
|
||||||
name: 'Work',
|
name: 'Work',
|
||||||
tasks: workTasks,
|
tasks: workTasks,
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Personal',
|
name: 'Personal',
|
||||||
tasks: personalTasks,
|
tasks: personalTasks,
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Misc',
|
name: 'Misc',
|
||||||
tasks: miscTasks,
|
tasks: miscTasks,
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Groceries',
|
name: 'Groceries',
|
||||||
@@ -121,6 +126,7 @@ it('should render a summary of the remaining group(s)', () => {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
groupTasksByCompletedStatus,
|
groupTasksByCompletedStatus,
|
||||||
truncateText,
|
truncateText,
|
||||||
} from '../../common/utils'
|
} from '../../common/utils'
|
||||||
import { GroupPayload, TaskPayload } from './tasks-slice'
|
import { GroupModel, TaskModel } from './tasks-slice'
|
||||||
|
|
||||||
const GROUPS_PREVIEW_LIMIT = 3
|
const GROUPS_PREVIEW_LIMIT = 3
|
||||||
const MAX_GROUP_DESCRIPTION_LENGTH = 30
|
const MAX_GROUP_DESCRIPTION_LENGTH = 30
|
||||||
@@ -14,7 +14,7 @@ const Title: React.FC = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GroupSummaryProps = {
|
type GroupSummaryProps = {
|
||||||
groups: GroupPayload[]
|
groups: GroupModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupSummary: React.FC<GroupSummaryProps> = ({ groups }) => {
|
const GroupSummary: React.FC<GroupSummaryProps> = ({ groups }) => {
|
||||||
@@ -37,7 +37,7 @@ const GroupSummary: React.FC<GroupSummaryProps> = ({ groups }) => {
|
|||||||
return (
|
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)}
|
{truncateText(group.name, MAX_GROUP_DESCRIPTION_LENGTH)}
|
||||||
<span className="px-2 text-neutral">
|
<span className="px-2 neutral">
|
||||||
{totalCompletedTasks}/{totalTasks}
|
{totalCompletedTasks}/{totalTasks}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -54,11 +54,11 @@ const GroupSummary: React.FC<GroupSummaryProps> = ({ groups }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NotePreviewProps = {
|
type NotePreviewProps = {
|
||||||
groupedTasks: GroupPayload[]
|
groupedTasks: GroupModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotePreview: React.FC<NotePreviewProps> = ({ groupedTasks }) => {
|
const NotePreview: React.FC<NotePreviewProps> = ({ groupedTasks }) => {
|
||||||
const allTasks: TaskPayload[] = getTaskArrayFromGroupedTasks(groupedTasks)
|
const allTasks: TaskModel[] = getTaskArrayFromGroupedTasks(groupedTasks)
|
||||||
const { completedTasks } = groupTasksByCompletedStatus(allTasks)
|
const { completedTasks } = groupTasksByCompletedStatus(allTasks)
|
||||||
const percentage = getPercentage(allTasks.length, completedTasks.length)
|
const percentage = getPercentage(allTasks.length, completedTasks.length)
|
||||||
const roundedPercentage = Math.floor(percentage / 10) * 10
|
const roundedPercentage = Math.floor(percentage / 10) * 10
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ it('renders the alert dialog with an input box', () => {
|
|||||||
const defaultState: Partial<RootState> = {
|
const defaultState: Partial<RootState> = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: defaultGroup,
|
name: defaultGroup,
|
||||||
@@ -44,6 +45,7 @@ it('should dispatch the action to merge groups', () => {
|
|||||||
const defaultState: Partial<RootState> = {
|
const defaultState: Partial<RootState> = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: defaultGroup,
|
name: defaultGroup,
|
||||||
@@ -107,6 +109,7 @@ it('should dispatch the action to merge groups on Enter press', () => {
|
|||||||
const defaultState: Partial<RootState> = {
|
const defaultState: Partial<RootState> = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: defaultGroup,
|
name: defaultGroup,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { fireEvent, screen } from '@testing-library/react'
|
|||||||
import { RootState } from '../../app/store'
|
import { RootState } from '../../app/store'
|
||||||
import { testRender } from '../../testUtils'
|
import { testRender } from '../../testUtils'
|
||||||
import TaskGroup from './TaskGroup'
|
import TaskGroup from './TaskGroup'
|
||||||
|
import { DEFAULT_SECTIONS } from './tasks-slice'
|
||||||
|
|
||||||
const defaultGroup = {
|
const defaultGroup = {
|
||||||
name: 'default group',
|
name: 'default group',
|
||||||
@@ -20,6 +21,7 @@ const defaultGroup = {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
}
|
}
|
||||||
|
|
||||||
it('renders the group name', () => {
|
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', () => {
|
it('renders the element that is used to display the list of tasks', () => {
|
||||||
testRender(<TaskGroup group={defaultGroup} isDragging={false} />)
|
testRender(<TaskGroup group={defaultGroup} isDragging={false} />)
|
||||||
|
|
||||||
expect(screen.getByTestId('task-list')).toBeInTheDocument()
|
expect(screen.getByTestId('task-section-list')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('collapses the group', () => {
|
it('collapses the group', () => {
|
||||||
testRender(<TaskGroup group={defaultGroup} isDragging={false} />)
|
testRender(<TaskGroup group={defaultGroup} isDragging={false} />)
|
||||||
|
|
||||||
const createTask = screen.getByTestId('create-task-input')
|
const createTask = screen.getByTestId('create-task-input')
|
||||||
const taskItemList = screen.getByTestId('task-list')
|
const taskSectionList = screen.getByTestId('task-section-list')
|
||||||
|
|
||||||
expect(createTask).toBeVisible()
|
expect(createTask).toBeVisible()
|
||||||
expect(taskItemList).toBeVisible()
|
expect(taskSectionList).toBeVisible()
|
||||||
|
|
||||||
const collapseButton = screen.getByTestId('collapse-task-group')
|
const collapseButton = screen.getByTestId('collapse-task-group')
|
||||||
fireEvent.click(collapseButton)
|
fireEvent.click(collapseButton)
|
||||||
|
|
||||||
expect(createTask).not.toBeVisible()
|
expect(createTask).not.toBeVisible()
|
||||||
expect(taskItemList).not.toBeVisible()
|
expect(taskSectionList).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows group options', () => {
|
it('shows group options', () => {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks'
|
import { useAppDispatch, useAppSelector } from '../../app/hooks'
|
||||||
import { getPercentage } from '../../common/utils'
|
import { getPercentage } from '../../common/utils'
|
||||||
import { GroupPayload, tasksGroupCollapsed } from './tasks-slice'
|
import { GroupModel, tasksGroupCollapsed } from './tasks-slice'
|
||||||
|
|
||||||
import CreateTask from './CreateTask'
|
import CreateTask from './CreateTask'
|
||||||
import TaskItemList from './TaskItemList'
|
import TaskSectionList from './TaskSectionList'
|
||||||
|
|
||||||
import TaskGroupOptions from './TaskGroupOptions'
|
import TaskGroupOptions from './TaskGroupOptions'
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ const CollapsableContainer = styled.div<CollapsableContainerProps>`
|
|||||||
`
|
`
|
||||||
|
|
||||||
type TaskGroupProps = {
|
type TaskGroupProps = {
|
||||||
group: GroupPayload
|
group: GroupModel
|
||||||
isDragging: boolean
|
isDragging: boolean
|
||||||
isLast?: boolean
|
isLast?: boolean
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
@@ -65,7 +65,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
const allTasksCompleted = totalTasks > 0 && totalTasks === completedTasks
|
const allTasksCompleted = totalTasks > 0 && totalTasks === completedTasks
|
||||||
|
|
||||||
function handleCollapse() {
|
function handleCollapse() {
|
||||||
dispatch(tasksGroupCollapsed({ groupName, collapsed: !collapsed }))
|
dispatch(tasksGroupCollapsed({ groupName, type: 'group', collapsed: !collapsed }))
|
||||||
setCollapsed(!collapsed)
|
setCollapsed(!collapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
|
|||||||
|
|
||||||
<CollapsableContainer collapsed={collapsed}>
|
<CollapsableContainer collapsed={collapsed}>
|
||||||
<CreateTask group={group} />
|
<CreateTask group={group} />
|
||||||
<TaskItemList group={group} />
|
<TaskSectionList group={group} />
|
||||||
</CollapsableContainer>
|
</CollapsableContainer>
|
||||||
</TaskGroupContainer>
|
</TaskGroupContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'
|
|||||||
|
|
||||||
import { testRender } from '../../testUtils'
|
import { testRender } from '../../testUtils'
|
||||||
import TaskItem from './TaskItem'
|
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 groupName = 'default group'
|
||||||
const task: TaskPayload = {
|
const task: TaskModel = {
|
||||||
id: 'test-1',
|
id: 'test-1',
|
||||||
description: 'Testing #1',
|
description: 'Testing #1',
|
||||||
completed: false,
|
completed: false,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ChangeEvent, createRef, KeyboardEvent, useEffect, useState } from 'reac
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector, useDidMount } from '../../app/hooks'
|
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'
|
import { CheckBoxInput, TextAreaInput } from '../../common/components'
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ const Container = styled.div<{ completed?: boolean }>`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export type TaskItemProps = {
|
export type TaskItemProps = {
|
||||||
task: TaskPayload
|
task: TaskModel
|
||||||
groupName: string
|
groupName: string
|
||||||
innerRef?: (element?: HTMLElement | null | undefined) => any
|
innerRef?: (element?: HTMLElement | null | undefined) => any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(<TaskItemList group={defaultGroup} />)
|
|
||||||
|
|
||||||
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(<TaskItemList group={groupWithCompletedTask} />)
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
@@ -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<TaskItemListProps> = ({ 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 (
|
|
||||||
<Container data-testid="task-list">
|
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
|
||||||
<TasksContainer testId="open-tasks-container" type="open" tasks={openTasks} groupName={group.name} />
|
|
||||||
|
|
||||||
<TasksContainer
|
|
||||||
testId="completed-tasks-container"
|
|
||||||
type="completed"
|
|
||||||
tasks={completedTasks}
|
|
||||||
groupName={group.name}
|
|
||||||
>
|
|
||||||
{completedTasks.length > 0 && <CompletedTasksActions groupName={group.name} />}
|
|
||||||
</TasksContainer>
|
|
||||||
</DragDropContext>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TaskItemList
|
|
||||||
@@ -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(<TaskSectionList group={defaultGroup} />)
|
||||||
|
|
||||||
|
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(<TaskSectionList group={groupWithCompletedTask} />)
|
||||||
|
|
||||||
|
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<RootState> = {
|
||||||
|
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(<TaskSectionList group={group} />, {}, 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()
|
||||||
|
})
|
||||||
@@ -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<TaskSectionListProps> = ({ 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 (
|
||||||
|
<Container data-testid="task-section-list">
|
||||||
|
{sections.map((section) => {
|
||||||
|
const tasks = group.tasks.filter((task) =>
|
||||||
|
section.id === 'completed-tasks' ? task.completed === true : !task.completed,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<DragDropContext key={`${section.id}-section-dnd`} onDragEnd={onDragEnd}>
|
||||||
|
<TasksSection testId={`${section.id}-section`} groupName={group.name} section={section} tasks={tasks}>
|
||||||
|
{section.id === 'completed-tasks' && tasks.length > 0 && <CompletedTasksActions groupName={group.name} />}
|
||||||
|
</TasksSection>
|
||||||
|
</DragDropContext>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaskSectionList
|
||||||
@@ -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 { Draggable, DraggingStyle, Droppable, NotDraggingStyle } from 'react-beautiful-dnd'
|
||||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { useAppSelector } from '../../app/hooks'
|
import { useAppDispatch, useAppSelector } from '../../app/hooks'
|
||||||
import { SubTitle } from '../../common/components'
|
import { RoundButton, SubTitle } from '../../common/components'
|
||||||
import { TaskPayload } from './tasks-slice'
|
import { SectionModel, TaskModel, tasksGroupCollapsed } from './tasks-slice'
|
||||||
|
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon } from '../../common/components/icons'
|
||||||
import TaskItem from './TaskItem'
|
import TaskItem from './TaskItem'
|
||||||
|
|
||||||
const InnerTasksContainer = styled.div<{
|
const SectionHeader = 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`
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
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`
|
const Wrapper = styled.div`
|
||||||
color: var(--sn-stylekit-foreground-color);
|
color: var(--sn-stylekit-foreground-color);
|
||||||
`
|
`
|
||||||
@@ -50,33 +50,48 @@ const getItemStyle = (isDragging: boolean, draggableStyle?: DraggingStyle | NotD
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
type ContainerType = 'open' | 'completed'
|
type TasksSectionProps = {
|
||||||
|
|
||||||
type TasksContainerProps = {
|
|
||||||
groupName: string
|
groupName: string
|
||||||
tasks: TaskPayload[]
|
tasks: TaskModel[]
|
||||||
type: ContainerType
|
section: SectionModel
|
||||||
testId?: string
|
testId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const TasksContainer: React.FC<TasksContainerProps> = ({ groupName, tasks, type, testId, children }) => {
|
const TasksSection: React.FC<TasksSectionProps> = ({ groupName, tasks, section, testId, children }) => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
const canEdit = useAppSelector((state) => state.settings.canEdit)
|
const canEdit = useAppSelector((state) => state.settings.canEdit)
|
||||||
const droppableId = `${type}-tasks-droppable`
|
const droppableId = `${section.id}-droppable`
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState<boolean>(!!section.collapsed)
|
||||||
|
|
||||||
|
const handleCollapse = () => {
|
||||||
|
dispatch(tasksGroupCollapsed({ groupName, type: section.id, collapsed: !collapsed }))
|
||||||
|
setCollapsed(!collapsed)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OuterContainer data-testid={testId} type={type} items={tasks.length}>
|
<OuterContainer
|
||||||
|
data-testid={testId}
|
||||||
|
addMargin={section.id === 'open-tasks'}
|
||||||
|
items={tasks.length}
|
||||||
|
collapsed={collapsed}
|
||||||
|
>
|
||||||
<Droppable droppableId={droppableId} isDropDisabled={!canEdit}>
|
<Droppable droppableId={droppableId} isDropDisabled={!canEdit}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<SubTitleContainer>
|
<SectionHeader>
|
||||||
<SubTitle>{type} tasks</SubTitle>
|
<SubTitle>{section.name}</SubTitle>
|
||||||
</SubTitleContainer>
|
{tasks.length > 0 && (
|
||||||
|
<RoundButton onClick={handleCollapse} size="small">
|
||||||
|
{!collapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||||
|
</RoundButton>
|
||||||
|
)}
|
||||||
|
</SectionHeader>
|
||||||
<InnerTasksContainer
|
<InnerTasksContainer
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
className={`${type}-tasks-container`}
|
className={`${section.id}-container`}
|
||||||
items={tasks.length}
|
collapsed={collapsed}
|
||||||
ref={provided.innerRef}
|
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) => {
|
{tasks.map((task, index) => {
|
||||||
@@ -148,7 +163,9 @@ const TasksContainer: React.FC<TasksContainerProps> = ({ groupName, tasks, type,
|
|||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</InnerTasksContainer>
|
</InnerTasksContainer>
|
||||||
{children}
|
<ChildrenContainer addMargin={section.id === 'completed-tasks'} items={tasks.length}>
|
||||||
|
{children}
|
||||||
|
</ChildrenContainer>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
@@ -156,4 +173,4 @@ const TasksContainer: React.FC<TasksContainerProps> = ({ groupName, tasks, type,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TasksContainer
|
export default TasksSection
|
||||||
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
||||||
@@ -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<TasksState> = {
|
||||||
|
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<Partial<TasksState>>({
|
||||||
|
...testData,
|
||||||
|
schemaVersion: '1.0.123',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should do nothing if latest version', () => {
|
||||||
|
const testData: Partial<TasksState> = {
|
||||||
|
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<TasksState> = {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const MigrationClasses: any[] = []
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { TasksState } from './tasks-slice'
|
|
||||||
import reducer, {
|
import reducer, {
|
||||||
|
DEFAULT_SECTIONS,
|
||||||
deleteAllCompleted,
|
deleteAllCompleted,
|
||||||
|
LATEST_SCHEMA_VERSION,
|
||||||
openAllCompleted,
|
openAllCompleted,
|
||||||
taskAdded,
|
taskAdded,
|
||||||
taskDeleted,
|
taskDeleted,
|
||||||
@@ -15,6 +16,7 @@ import reducer, {
|
|||||||
tasksGroupReordered,
|
tasksGroupReordered,
|
||||||
tasksLoaded,
|
tasksLoaded,
|
||||||
tasksReordered,
|
tasksReordered,
|
||||||
|
TasksState,
|
||||||
taskToggled,
|
taskToggled,
|
||||||
} from './tasks-slice'
|
} from './tasks-slice'
|
||||||
|
|
||||||
@@ -23,11 +25,11 @@ it('should return the initial state', () => {
|
|||||||
reducer(undefined, {
|
reducer(undefined, {
|
||||||
type: undefined,
|
type: undefined,
|
||||||
}),
|
}),
|
||||||
).toEqual({ schemaVersion: '1.0.0', groups: [] })
|
).toEqual<TasksState>({ schemaVersion: LATEST_SCHEMA_VERSION, groups: [], defaultSections: [] })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle a task being added to a non-existing group', () => {
|
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(
|
expect(
|
||||||
reducer(
|
reducer(
|
||||||
@@ -37,8 +39,9 @@ it('should handle a task being added to a non-existing group', () => {
|
|||||||
groupName: 'Test',
|
groupName: 'Test',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [],
|
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', () => {
|
it('should handle a task being added to the existing tasks store', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -69,8 +73,9 @@ it('should handle a task being added to the existing tasks store', () => {
|
|||||||
groupName: 'Test',
|
groupName: 'Test',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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', () => {
|
it('should handle an existing task being modified', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -107,6 +113,7 @@ it('should handle an existing task being modified', () => {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
sections: DEFAULT_SECTIONS,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -119,8 +126,9 @@ it('should handle an existing task being modified', () => {
|
|||||||
groupName: 'Test',
|
groupName: 'Test',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -133,6 +141,7 @@ it('should handle an existing task being modified', () => {
|
|||||||
updatedAt: expect.any(Date),
|
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', () => {
|
it('should not modify tasks if an invalid id is provided', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -164,8 +174,9 @@ it('should not modify tasks if an invalid id is provided', () => {
|
|||||||
groupName: 'Test',
|
groupName: 'Test',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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', () => {
|
it('should keep completed field as-is, if task is modified', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -211,8 +223,9 @@ it('should keep completed field as-is, if task is modified', () => {
|
|||||||
groupName: 'Test',
|
groupName: 'Test',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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', () => {
|
it('should handle an existing task being toggled', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -271,6 +286,7 @@ it('should handle an existing task being toggled', () => {
|
|||||||
test('toggled tasks should be on top of the list', () => {
|
test('toggled tasks should be on top of the list', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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', () => {
|
it('should handle an existing completed task being toggled', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -370,6 +389,7 @@ it('should handle an existing completed task being toggled', () => {
|
|||||||
it('should handle an existing task being deleted', () => {
|
it('should handle an existing task being deleted', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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', () => {
|
it('should handle opening all tasks that are marked as completed', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -472,6 +495,7 @@ it('should handle opening all tasks that are marked as completed', () => {
|
|||||||
it('should handle clear all completed tasks', () => {
|
it('should handle clear all completed tasks', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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', () => {
|
it('should handle loading tasks into the tasks store, if an invalid payload is provided', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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({
|
expect(reducer(previousState, tasksLoaded('null'))).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
|
defaultSections: DEFAULT_SECTIONS,
|
||||||
groups: [],
|
groups: [],
|
||||||
initialized: true
|
initialized: true,
|
||||||
})
|
})
|
||||||
expect(reducer(previousState, tasksLoaded('undefined'))).toMatchObject({
|
expect(reducer(previousState, tasksLoaded('undefined'))).toMatchObject<TasksState>({
|
||||||
...previousState,
|
...previousState,
|
||||||
initialized: false,
|
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', () => {
|
it('should initialize the storage with an empty object', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -565,8 +593,9 @@ it('should initialize the storage with an empty object', () => {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(reducer(previousState, tasksLoaded(''))).toEqual({
|
expect(reducer(previousState, tasksLoaded(''))).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
|
defaultSections: DEFAULT_SECTIONS,
|
||||||
groups: [],
|
groups: [],
|
||||||
initialized: true,
|
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', () => {
|
it('should handle loading tasks into the tasks store, with a valid payload', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
|
defaultSections: DEFAULT_SECTIONS,
|
||||||
groups: [],
|
groups: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasksPayload: TasksState = {
|
const tasksPayload: Partial<TasksState> = {
|
||||||
schemaVersion: '2.0.0',
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -608,8 +638,9 @@ it('should handle loading tasks into the tasks store, with a valid payload', ()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const serializedPayload = JSON.stringify(tasksPayload)
|
const serializedPayload = JSON.stringify(tasksPayload)
|
||||||
expect(reducer(previousState, tasksLoaded(serializedPayload))).toEqual({
|
expect(reducer(previousState, tasksLoaded(serializedPayload))).toEqual<TasksState>({
|
||||||
schemaVersion: '2.0.0',
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
|
defaultSections: DEFAULT_SECTIONS,
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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', () => {
|
it('should set defaultSections property if not provided', () => {
|
||||||
const previousState: TasksState = { schemaVersion: '1.0.0', groups: [] }
|
const previousState: TasksState = {
|
||||||
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
|
defaultSections: [],
|
||||||
|
groups: [],
|
||||||
|
}
|
||||||
|
|
||||||
expect(reducer(previousState, tasksGroupAdded({ groupName: 'New group' }))).toEqual({
|
const tasksPayload: Partial<TasksState> = {
|
||||||
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
name: 'Test',
|
||||||
|
tasks: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializedPayload = JSON.stringify(tasksPayload)
|
||||||
|
expect(reducer(previousState, tasksLoaded(serializedPayload))).toEqual<TasksState>({
|
||||||
|
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<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'New group',
|
name: 'New group',
|
||||||
@@ -656,6 +719,7 @@ it('should handle adding a new task group', () => {
|
|||||||
it('should handle adding an existing task group', () => {
|
it('should handle adding an existing task group', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Existing group',
|
name: 'Existing group',
|
||||||
@@ -677,6 +741,7 @@ it('should handle adding an existing task group', () => {
|
|||||||
it('should handle reordering tasks from the same section', () => {
|
it('should handle reordering tasks from the same section', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -714,8 +779,9 @@ it('should handle reordering tasks from the same section', () => {
|
|||||||
isSameSection: true,
|
isSameSection: true,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -747,6 +813,7 @@ it('should handle reordering tasks from the same section', () => {
|
|||||||
it('should handle reordering tasks from different sections', () => {
|
it('should handle reordering tasks from different sections', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -784,8 +851,9 @@ it('should handle reordering tasks from different sections', () => {
|
|||||||
isSameSection: false,
|
isSameSection: false,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -819,6 +887,7 @@ it('should handle reordering task groups', () => {
|
|||||||
|
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -864,8 +933,9 @@ it('should handle reordering task groups', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const expectedState = {
|
const expectedState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Testing',
|
name: 'Testing',
|
||||||
@@ -909,6 +979,7 @@ it('should handle reordering task groups', () => {
|
|||||||
it('should handle deleting groups', () => {
|
it('should handle deleting groups', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -948,8 +1019,9 @@ it('should handle deleting groups', () => {
|
|||||||
|
|
||||||
const currentState = reducer(previousState, tasksGroupDeleted({ groupName: 'Testing' }))
|
const currentState = reducer(previousState, tasksGroupDeleted({ groupName: 'Testing' }))
|
||||||
|
|
||||||
const expectedState = {
|
const expectedState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -982,6 +1054,7 @@ it('should handle deleting groups', () => {
|
|||||||
it('should not merge the same group', () => {
|
it('should not merge the same group', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -1027,6 +1100,7 @@ it('should not merge the same group', () => {
|
|||||||
it('should handle merging groups', () => {
|
it('should handle merging groups', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test group #1',
|
name: 'Test group #1',
|
||||||
@@ -1069,8 +1143,9 @@ it('should handle merging groups', () => {
|
|||||||
tasksGroupMerged({ groupName: 'Test group #3', mergeWith: 'Test group #2' }),
|
tasksGroupMerged({ groupName: 'Test group #3', mergeWith: 'Test group #2' }),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(currentState).toMatchObject({
|
expect(currentState).toMatchObject<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test group #1',
|
name: 'Test group #1',
|
||||||
@@ -1107,6 +1182,7 @@ it('should handle merging groups', () => {
|
|||||||
it('should handle renaming a group', () => {
|
it('should handle renaming a group', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -1135,8 +1211,9 @@ 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({
|
expect(currentState).toEqual<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -1167,6 +1244,7 @@ it('should handle renaming a group', () => {
|
|||||||
it("should rename a group and preserve it's current order", () => {
|
it("should rename a group and preserve it's current order", () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: '1st group',
|
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' }))
|
const currentState = reducer(previousState, tasksGroupRenamed({ groupName: '2nd group', newName: 'Middle group' }))
|
||||||
|
|
||||||
expect(currentState).toMatchObject({
|
expect(currentState).toMatchObject<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: '1st group',
|
name: '1st group',
|
||||||
@@ -1249,6 +1328,7 @@ it("should rename a group and preserve it's current order", () => {
|
|||||||
it('should handle collapsing groups', () => {
|
it('should handle collapsing groups', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -1334,6 +1418,7 @@ it('should handle collapsing groups', () => {
|
|||||||
it('should handle saving task draft for groups', () => {
|
it('should handle saving task draft for groups', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
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 currentState = reducer(previousState, tasksGroupDraft({ groupName: 'Tests', draft: 'Remember to ...' }))
|
||||||
|
|
||||||
const expectedState = {
|
const expectedState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -1419,6 +1505,7 @@ it('should handle saving task draft for groups', () => {
|
|||||||
it('should handle setting a group as last active', () => {
|
it('should handle setting a group as last active', () => {
|
||||||
const previousState: TasksState = {
|
const previousState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -1447,8 +1534,9 @@ 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({
|
expect(currentState).toMatchObject<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
@@ -1480,7 +1568,8 @@ it('should handle setting a group as last active', () => {
|
|||||||
it('should detect and load legacy content', () => {
|
it('should detect and load legacy content', () => {
|
||||||
const payload = '- [ ] Foo bar'
|
const payload = '- [ ] Foo bar'
|
||||||
expect(reducer(undefined, tasksLoaded(payload))).toMatchObject<TasksState>({
|
expect(reducer(undefined, tasksLoaded(payload))).toMatchObject<TasksState>({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
|
defaultSections: [],
|
||||||
initialized: false,
|
initialized: false,
|
||||||
groups: [],
|
groups: [],
|
||||||
legacyContent: {
|
legacyContent: {
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
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 = {
|
export type TasksState = {
|
||||||
schemaVersion: string
|
schemaVersion: string
|
||||||
groups: GroupPayload[]
|
groups: GroupModel[]
|
||||||
|
defaultSections: SectionModel[]
|
||||||
initialized?: boolean
|
initialized?: boolean
|
||||||
legacyContent?: GroupPayload
|
legacyContent?: GroupModel
|
||||||
lastError?: string
|
lastError?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TasksState = {
|
const initialState: TasksState = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: LATEST_SCHEMA_VERSION,
|
||||||
|
defaultSections: [],
|
||||||
groups: [],
|
groups: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskPayload = {
|
export type TaskModel = {
|
||||||
id: string
|
id: string
|
||||||
description: string
|
description: string
|
||||||
completed?: boolean
|
completed?: boolean
|
||||||
@@ -23,12 +37,19 @@ export type TaskPayload = {
|
|||||||
completedAt?: Date
|
completedAt?: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupPayload = {
|
export type SectionModel = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
collapsed?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GroupModel = {
|
||||||
name: string
|
name: string
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
draft?: string
|
draft?: string
|
||||||
lastActive?: Date
|
lastActive?: Date
|
||||||
tasks: TaskPayload[]
|
tasks: TaskModel[]
|
||||||
|
sections?: SectionModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasksSlice = createSlice({
|
const tasksSlice = createSlice({
|
||||||
@@ -221,15 +242,27 @@ const tasksSlice = createSlice({
|
|||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
groupName: string
|
groupName: string
|
||||||
|
type: 'group' | 'open-tasks' | 'completed-tasks' | string
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
const { groupName, collapsed } = action.payload
|
const { groupName, type, collapsed } = action.payload
|
||||||
const group = state.groups.find((item) => item.name === groupName)
|
const group = state.groups.find((item) => item.name === groupName)
|
||||||
if (!group) {
|
if (!group) {
|
||||||
return
|
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(
|
tasksGroupDraft(
|
||||||
state,
|
state,
|
||||||
@@ -294,14 +327,16 @@ const tasksSlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedState = JSON.parse(payload) as TasksState
|
const parsedState = JSON.parse(payload) as TasksState
|
||||||
const newState: TasksState = {
|
let newState: TasksState = {
|
||||||
schemaVersion: parsedState?.schemaVersion ?? '1.0.0',
|
schemaVersion: parsedState?.schemaVersion ?? LATEST_SCHEMA_VERSION,
|
||||||
|
defaultSections: arrayDefault({ value: parsedState?.defaultSections, defaultValue: DEFAULT_SECTIONS }),
|
||||||
groups: parsedState?.groups ?? [],
|
groups: parsedState?.groups ?? [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState !== initialState) {
|
if (newState !== initialState) {
|
||||||
state.schemaVersion = newState.schemaVersion
|
state.schemaVersion = newState.schemaVersion
|
||||||
state.groups = newState.groups
|
state.groups = newState.groups
|
||||||
|
state.defaultSections = newState.defaultSections
|
||||||
state.initialized = true
|
state.initialized = true
|
||||||
delete state.lastError
|
delete state.lastError
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ const TaskEditor: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editorKit.current!.saveItemWithPresave(currentNote, () => {
|
editorKit.current!.saveItemWithPresave(currentNote, () => {
|
||||||
const { schemaVersion, groups } = store.getState().tasks
|
const { schemaVersion, groups, defaultSections } = store.getState().tasks
|
||||||
currentNote.content.text = JSON.stringify({ schemaVersion, groups }, null, 2)
|
currentNote.content.text = JSON.stringify({ schemaVersion, groups, defaultSections }, null, 2)
|
||||||
|
|
||||||
currentNote.content.preview_plain = getPlainPreview(groups)
|
currentNote.content.preview_plain = getPlainPreview(groups)
|
||||||
currentNote.content.preview_html = renderToString(<NotePreview groupedTasks={groups} />)
|
currentNote.content.preview_html = renderToString(<NotePreview groupedTasks={groups} />)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ html {
|
|||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sn-icon.sm {
|
.sn-icon.small {
|
||||||
height: 0.875rem;
|
height: 0.875rem;
|
||||||
width: 0.875rem;
|
width: 0.875rem;
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,12 @@ html {
|
|||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
height: 1.25rem;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pt-1px {
|
.pt-1px {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { RootState } from './app/store'
|
|||||||
const defaultMockState: RootState = {
|
const defaultMockState: RootState = {
|
||||||
tasks: {
|
tasks: {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: '1.0.0',
|
||||||
|
defaultSections: [],
|
||||||
groups: [],
|
groups: [],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
Reference in New Issue
Block a user