Skip to content

Commit

Permalink
Fix #952; Support recurring tasks with custom status workflows
Browse files Browse the repository at this point in the history
  • Loading branch information
mgmeyers committed May 19, 2024
1 parent 302d27a commit 29698a9
Show file tree
Hide file tree
Showing 17 changed files with 250 additions and 96 deletions.
126 changes: 91 additions & 35 deletions src/DragDropApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { c, getDateColorFn, getTagColorFn, maybeCompleteForMove } from './compon
import { Board, DataTypes, Item, Lane } from './components/types';
import { DndContext } from './dnd/components/DndContext';
import { DragOverlay } from './dnd/components/DragOverlay';
import { Entity } from './dnd/types';
import { Entity, Nestable } from './dnd/types';
import {
getEntityFromPath,
insertEntity,
Expand All @@ -21,6 +21,11 @@ import {
import { getBoardModifiers } from './helpers/boardModifiers';
import KanbanPlugin from './main';
import { frontmatterKey } from './parsers/common';
import {
getTaskStatusDone,
getTaskStatusPreDone,
toggleTask,
} from './parsers/helpers/inlineMetadata';

export function createApp(win: Window, plugin: KanbanPlugin) {
return <DragDropApp win={win} plugin={plugin} />;
Expand All @@ -46,33 +51,42 @@ export function DragDropApp({ win, plugin }: { win: Window; plugin: KanbanPlugin
const dropPath = dropEntity.getPath();
const destinationParent = getEntityFromPath(stateManager.state, dropPath.slice(0, -1));

const parseItems = (titles: string[]) => {
return Promise.all(
titles.map((title) => {
return stateManager.getNewItem(title);
})
);
};

parseItems(data.content)
.then((items) => {
const processed = items.map((item) =>
update(item, {
data: {
isComplete: {
$set: !!destinationParent?.data?.shouldMarkItemsComplete,
},
},
})
);
try {
const items: Item[] = data.content.map((title: string) => {
let item = stateManager.getNewItem(title, ' ');
const isComplete = !!destinationParent?.data?.shouldMarkItemsComplete;

if (isComplete) {
item = update(item, { data: { checkChar: { $set: getTaskStatusPreDone() } } });
const updates = toggleTask(item, stateManager.file);
if (updates) {
const [itemStrings, checkChars, thisIndex] = updates;
const nextItem = itemStrings[thisIndex];
const checkChar = checkChars[thisIndex];
return stateManager.getNewItem(nextItem, checkChar);
}
}

return stateManager.setState((board) => insertEntity(board, dropPath, processed));
})
.catch((e) => {
stateManager.setError(e);
console.error(e);
return update(item, {
data: {
checked: {
$set: !!destinationParent?.data?.shouldMarkItemsComplete,
},
checkChar: {
$set: destinationParent?.data?.shouldMarkItemsComplete
? getTaskStatusDone()
: ' ',
},
},
});
});

return stateManager.setState((board) => insertEntity(board, dropPath, items));
} catch (e) {
stateManager.setError(e);
console.error(e);
}

return;
}

Expand All @@ -97,12 +111,40 @@ export function DragDropApp({ win, plugin }: { win: Window; plugin: KanbanPlugin

return stateManager.setState((board) => {
const entity = getEntityFromPath(board, dragPath);
const newBoard: Board = moveEntity(board, dragPath, dropPath, (entity) => {
if (entity.type === DataTypes.Item) {
return maybeCompleteForMove(board, dragPath, board, dropPath, entity);
const newBoard: Board = moveEntity(
board,
dragPath,
dropPath,
(entity) => {
if (entity.type === DataTypes.Item) {
const { next } = maybeCompleteForMove(
stateManager,
board,
dragPath,
stateManager,
board,
dropPath,
entity
);
return next;
}
return entity;
},
(entity) => {
if (entity.type === DataTypes.Item) {
const { replacement } = maybeCompleteForMove(
stateManager,
board,
dragPath,
stateManager,
board,
dropPath,
entity
);
return replacement;
}
}
return entity;
});
);

if (entity.type === DataTypes.Lane) {
const from = dragPath.last();
Expand Down Expand Up @@ -147,6 +189,7 @@ export function DragDropApp({ win, plugin }: { win: Window; plugin: KanbanPlugin

sourceStateManager.setState((sourceBoard) => {
const entity = getEntityFromPath(sourceBoard, dragPath);
let replacementEntity: Nestable;

destinationStateManager.setState((destinationBoard) => {
if (inDropArea) {
Expand All @@ -159,10 +202,23 @@ export function DragDropApp({ win, plugin }: { win: Window; plugin: KanbanPlugin
else dropPath.push(0);
}

const toInsert =
entity.type === DataTypes.Item
? maybeCompleteForMove(sourceBoard, dragPath, destinationBoard, dropPath, entity)
: [entity];
const toInsert: Nestable[] = [];

if (entity.type === DataTypes.Item) {
const { next, replacement } = maybeCompleteForMove(
sourceStateManager,
sourceBoard,
dragPath,
destinationStateManager,
destinationBoard,
dropPath,
entity
);
replacementEntity = replacement;
toInsert.push(next);
} else {
toInsert.push(entity);
}

if (entity.type === DataTypes.Lane) {
const collapsedState = destinationView.getViewState('list-collapse');
Expand Down Expand Up @@ -196,7 +252,7 @@ export function DragDropApp({ win, plugin }: { win: Window; plugin: KanbanPlugin
data: { settings: { 'list-collapse': { $set: op(collapsedState) } } },
});
} else {
return removeEntity(sourceBoard, dragPath);
return removeEntity(sourceBoard, dragPath, replacementEntity);
}
});
},
Expand Down
10 changes: 6 additions & 4 deletions src/StateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getDefaultDateFormat, getDefaultTimeFormat } from './components/helpers
import { Board, BoardTemplate, Item } from './components/types';
import { ListFormat } from './parsers/List';
import { BaseFormat, frontmatterKey, shouldRefreshBoard } from './parsers/common';
import { getTaskStatusDone } from './parsers/helpers/inlineMetadata';
import { defaultDateTrigger, defaultMetadataPosition, defaultTimeTrigger } from './settingHelpers';

export class StateManager {
Expand Down Expand Up @@ -388,11 +389,12 @@ export class StateManager {
return update(lane, {
children: {
$set: lane.children.filter((item) => {
if (lane.data.shouldMarkItemsComplete || item.data.isComplete) {
const isComplete = item.data.checked && item.data.checkChar === getTaskStatusDone();
if (lane.data.shouldMarkItemsComplete || isComplete) {
archived.push(item);
}

return !item.data.isComplete && !lane.data.shouldMarkItemsComplete;
return !isComplete && !lane.data.shouldMarkItemsComplete;
}),
},
});
Expand All @@ -418,8 +420,8 @@ export class StateManager {
}
}

getNewItem(content: string, isComplete?: boolean, forceEdit?: boolean) {
return this.parser.newItem(content, isComplete, forceEdit);
getNewItem(content: string, checkChar: string, forceEdit?: boolean) {
return this.parser.newItem(content, checkChar, forceEdit);
}

updateItemContent(item: Item, content: string) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/Editor/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ export function MarkdownEditor({
editor: Editor,
lineHasGlobalFilter: boolean
) {
return lineHasGlobalFilter && cursor.line === 0;
if (lineHasGlobalFilter && cursor.line === 0) return true;
return undefined;
}

updateBottomPadding() {}
Expand Down
29 changes: 15 additions & 14 deletions src/components/Item/ItemCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import update from 'immutability-helper';
import { memo, useCallback, useEffect, useState } from 'preact/compat';
import { StateManager } from 'src/StateManager';
import { Path } from 'src/dnd/types';
import { toggleItem } from 'src/parsers/helpers/inlineMetadata';
import { getTaskStatusDone, toggleTask } from 'src/parsers/helpers/inlineMetadata';

import { BoardModifiers } from '../../helpers/boardModifiers';
import { Icon } from '../Icon/Icon';
Expand Down Expand Up @@ -30,18 +30,13 @@ export const ItemCheckbox = memo(function ItemCheckbox({
const [isHoveringCheckbox, setIsHoveringCheckbox] = useState(false);

const onCheckboxChange = useCallback(() => {
const updates = toggleItem(item, stateManager.file);
const updates = toggleTask(item, stateManager.file);
if (updates) {
const [itemStrings, thisIndex] = updates;
const [itemStrings, checkChars, thisIndex] = updates;
const replacements: Item[] = itemStrings.map((str, i) => {
const newItem = stateManager.getNewItem(
str,
i === thisIndex ? !item.data.isComplete : false
);
if (i === thisIndex) {
newItem.id = item.id;
}
return newItem;
const next = stateManager.getNewItem(str, checkChars[i]);
if (i === thisIndex) next.id = item.id;
return next;
});

boardModifiers.replaceItem(path, replacements);
Expand All @@ -50,12 +45,17 @@ export const ItemCheckbox = memo(function ItemCheckbox({
path,
update(item, {
data: {
$toggle: ['isComplete'],
checkChar: {
$apply: (v) => {
return v === ' ' ? getTaskStatusDone() : ' ';
},
},
$toggle: ['checked'],
},
})
);
}
}, [item, stateManager, boardModifiers]);
}, [item, stateManager, boardModifiers, ...path]);

useEffect(() => {
if (isHoveringCheckbox) {
Expand Down Expand Up @@ -104,7 +104,8 @@ export const ItemCheckbox = memo(function ItemCheckbox({
onChange={onCheckboxChange}
type="checkbox"
className="task-list-item-checkbox"
checked={!!item.data.isComplete}
checked={item.data.checked}
data-task={item.data.checkChar}
/>
)}
{(isCtrlHoveringCheckbox || (!shouldShowCheckbox && shouldMarkItemsComplete)) && (
Expand Down
6 changes: 3 additions & 3 deletions src/components/Item/ItemContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { StateManager } from 'src/StateManager';
import { useNestedEntityPath } from 'src/dnd/components/Droppable';
import { Path } from 'src/dnd/types';
import { toggleItemString } from 'src/parsers/helpers/inlineMetadata';
import { getTaskStatusDone, toggleTaskString } from 'src/parsers/helpers/inlineMetadata';

import { MarkdownEditor, allowNewLine } from '../Editor/MarkdownEditor';
import {
Expand Down Expand Up @@ -100,11 +100,11 @@ function checkCheckbox(stateManager: StateManager, title: string, checkboxIndex:

if (match) {
if (count === checkboxIndex) {
const updates = toggleItemString(line, stateManager.file);
const updates = toggleTaskString(line, stateManager.file);
if (updates) {
results.push(updates);
} else {
const check = match[3] === ' ' ? 'x' : ' ';
const check = match[3] === ' ' ? getTaskStatusDone() : ' ';
const m1 = match[1] ?? '';
const m2 = match[2] ?? '';
const m4 = match[4] ?? '';
Expand Down
2 changes: 1 addition & 1 deletion src/components/Item/ItemForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function ItemForm({ addItems, editState, setEditState, hideButton }: Item
});

const createItem = (title: string) => {
addItems([stateManager.getNewItem(title)]);
addItems([stateManager.getNewItem(title, ' ')]);
const cm = editorRef.current;
if (cm) {
cm.dispatch({
Expand Down
6 changes: 3 additions & 3 deletions src/components/Item/ItemMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function useItemMenu({
const titles = item.data.titleRaw.split(/[\r\n]+/g).map((t) => t.trim());
const newItems = await Promise.all(
titles.map((title) => {
return stateManager.getNewItem(title);
return stateManager.getNewItem(title, ' ');
})
);

Expand All @@ -143,7 +143,7 @@ export function useItemMenu({
i.setIcon('lucide-list-start')
.setTitle(t('Insert card before'))
.onClick(() =>
boardModifiers.insertItems(path, [stateManager.getNewItem('', false, true)])
boardModifiers.insertItems(path, [stateManager.getNewItem('', ' ', true)])
);
})
.addItem((i) => {
Expand All @@ -154,7 +154,7 @@ export function useItemMenu({

newPath[newPath.length - 1] = newPath[newPath.length - 1] + 1;

boardModifiers.insertItems(newPath, [stateManager.getNewItem('', false, true)]);
boardModifiers.insertItems(newPath, [stateManager.getNewItem('', ' ', true)]);
});
})
.addItem((i) => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/Item/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Platform, TFile, TFolder, htmlToMarkdown, moment, parseLinktext, setIco
import { StateManager } from 'src/StateManager';
import { Path } from 'src/dnd/types';
import { buildLinkToDailyNote } from 'src/helpers';
import { getTaskStatusDone } from 'src/parsers/helpers/inlineMetadata';

import { BoardModifiers } from '../../helpers/boardModifiers';
import { getDefaultLocale } from '../Editor/datePickerLocale';
Expand Down Expand Up @@ -290,7 +291,7 @@ export function getItemClassModifiers(item: Item) {
}
}

if (item.data.isComplete) {
if (item.data.checked && item.data.checkChar === getTaskStatusDone()) {
classModifiers.push('is-complete');
}

Expand Down
6 changes: 5 additions & 1 deletion src/components/Lane/Lane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SortPlaceholder } from 'src/dnd/components/SortPlaceholder';
import { Sortable, StaticSortable } from 'src/dnd/components/Sortable';
import { useDragHandle } from 'src/dnd/managers/DragManager';
import { frontmatterKey } from 'src/parsers/common';
import { getTaskStatusDone } from 'src/parsers/helpers/inlineMetadata';

import { Items } from '../Item/Item';
import { ItemForm } from '../Item/ItemForm';
Expand Down Expand Up @@ -85,10 +86,13 @@ function DraggableLaneRaw({
items.map((item) =>
update(item, {
data: {
isComplete: {
checked: {
// Mark the item complete if we're moving into a completed lane
$set: shouldMarkItemsComplete,
},
checkChar: {
$set: shouldMarkItemsComplete ? getTaskStatusDone() : ' ',
},
},
})
)
Expand Down
Loading

0 comments on commit 29698a9

Please sign in to comment.