mirror of
https://github.com/sechan100/daily-routine-2.git
synced 2026-07-22 05:37:51 +00:00
feat: dnd reorder 로직 재구성
This commit is contained in:
parent
9aa4fe783d
commit
da93b7020b
10 changed files with 116 additions and 32 deletions
|
|
@ -14,11 +14,11 @@ type AllRecreatedNoteCb = (notes: RoutineNote[]) => void;
|
|||
export interface RoutineNoteSynchronizer {
|
||||
// @param cb 동기화 완료후 실행할 콜백
|
||||
(cb: AllRecreatedNoteCb): void;
|
||||
|
||||
(excludeDay?: Day): void;
|
||||
(): void;
|
||||
}
|
||||
|
||||
export const executeRoutineNotesSynchronize: RoutineNoteSynchronizer = async (cb?) => {
|
||||
export const executeRoutineNotesSynchronize: RoutineNoteSynchronizer = async (arg?) => {
|
||||
const noteCreator = await RoutineNoteCreator.withLoadFromRepositoryAsync();
|
||||
|
||||
const doSync = async (note: RoutineNote): Promise<RoutineNote> => {
|
||||
|
|
@ -39,10 +39,13 @@ export const executeRoutineNotesSynchronize: RoutineNoteSynchronizer = async (cb
|
|||
|
||||
// 모든 등록된 비동기 로직 이후에 실행을 예약
|
||||
Promise.resolve().then(async () => {
|
||||
const notes = await NoteRepository.loadBetween(Day.now(), Day.max());
|
||||
let notes = await NoteRepository.loadBetween(Day.now(), Day.max());
|
||||
if(arg instanceof Day){
|
||||
notes = notes.filter(n => !n.getDay().isSameDay(arg));
|
||||
}
|
||||
const syncedNotes = await Promise.all(notes.map(doSync));
|
||||
if(cb){
|
||||
(cb as AllRecreatedNoteCb)(syncedNotes);
|
||||
if(typeof arg === 'function'){
|
||||
(arg as AllRecreatedNoteCb)(syncedNotes);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -37,12 +37,12 @@ export abstract class AbstractTask<T extends TaskDto = RoutineTaskDto | TodoTask
|
|||
line = line.trim();
|
||||
if(line === '') throw new Error('empty-line');
|
||||
|
||||
const metaDataErr = 'invalid-task-meta-data-format';
|
||||
const regex = /%%(.*?)%%/;
|
||||
const match = line.match(regex);
|
||||
if(!match) throw new Error(metaDataErr);
|
||||
if(!match) throw new Error(`task metadata not found: ${line}`);
|
||||
|
||||
const metaData = JSON.parse(match[1]);
|
||||
if("type" in metaData === false) throw new Error(metaDataErr);
|
||||
if("type" in metaData === false) throw new Error(`task 'type' not found: ${line}`);
|
||||
|
||||
return metaData.type;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export class RoutineNote implements JsonConvertible<RoutineNoteDto>, TaskParent
|
|||
}
|
||||
|
||||
static deserialize(day: Day, content: string): Result<RoutineNote, string> {
|
||||
const regex = /##\s*.*?\n[\s\S]*?(?=\n##|$)/g;
|
||||
const regex = /##\s+.*(?:\n(?!##|$).*)*/g;
|
||||
const blocks = content.match(regex);
|
||||
if(!blocks) return err('no-task-blocks');
|
||||
|
||||
|
|
|
|||
|
|
@ -85,16 +85,15 @@ export class TaskGroup implements TaskElement<TaskGroupDto>, TaskParent {
|
|||
* @param groupBlock:
|
||||
* `
|
||||
* ## {name}
|
||||
* ...tasks
|
||||
* ...tasks?
|
||||
* ` 형식의 마크다운 블록
|
||||
*/
|
||||
static deserialize(groupBlock: string): Result<TaskGroup, string> {
|
||||
const regex = /##\s*(.+?)\s*\n([\s\S]*)/;
|
||||
const regex = /##\s*(.+?)\s*(?:\n([\s\S]*?))?(?=\n##|$)/;
|
||||
const match = groupBlock.match(regex);
|
||||
if(!match) return err('invalid-group-format');
|
||||
|
||||
const name = match[1].trim();
|
||||
const taskLines = match[2].trim().split("\n");
|
||||
const taskLines = match[2]?.trim().split("\n") ?? [];
|
||||
const tasks: AbstractTask[] = taskLines
|
||||
.map(l => {
|
||||
if(AbstractTask.investigateTaskType(l) === "routine"){
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ interface GroupRepository {
|
|||
isExist(groupName: string): Promise<boolean>;
|
||||
persist(group: RoutineGroup): Promise<boolean>;
|
||||
delete(groupName: string): Promise<void>;
|
||||
update(entity: RoutineGroup): Promise<RoutineGroup>;
|
||||
changeName(originalName: string, newName: string): Promise<void>;
|
||||
}
|
||||
export const GroupRepository: GroupRepository = {
|
||||
|
|
@ -58,7 +59,13 @@ export const GroupRepository: GroupRepository = {
|
|||
if(!file) return;
|
||||
await fileAccessor.deleteFile(file);
|
||||
},
|
||||
|
||||
|
||||
async update(group: RoutineGroup){
|
||||
const file = fileAccessor.loadFile(GROUP_PATH(group.getName()));
|
||||
if(!file) throw new Error('Group file not found.');
|
||||
await fileAccessor.writeFrontMatter(file, () => group.getProperties().toJSON());
|
||||
return group;
|
||||
},
|
||||
|
||||
async changeName(originalName: string, newName: string){
|
||||
const file = fileAccessor.loadFile(GROUP_PATH(originalName));
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const RoutineRepository: RoutineRepository = {
|
|||
async update(routine: Routine){
|
||||
const file = fileAccessor.loadFile(ROUTINE_PATH(routine.getName()));
|
||||
if(!file) throw new Error('Routine file not found.');
|
||||
await fileAccessor.writeFrontMatter(file, () => routine.getProperties().serialize());
|
||||
await fileAccessor.writeFrontMatter(file, () => routine.getProperties().toJSON());
|
||||
return routine;
|
||||
},
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ export const ROUTINE_PATH = (routineName: string) => {
|
|||
export const GROUP_PREFIX = "_g_";
|
||||
|
||||
export const GROUP_PATH = (groupName: string) => {
|
||||
return `${ROUTINE_FOLDER_PATH()}/${GROUP_PREFIX}${groupName}`;
|
||||
return `${ROUTINE_FOLDER_PATH()}/${GROUP_PREFIX}${groupName}.md`;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const HitAreaEvaluator = {
|
|||
}
|
||||
},
|
||||
|
||||
evaluateGroup({ y }: XYCoord, node: HTMLElement, isOpen: boolean): GroupHitArea | null {
|
||||
evaluateGroup({ y }: XYCoord, node: HTMLElement, activeInCmd: boolean): GroupHitArea | null {
|
||||
const rect: NodeRect = node.getBoundingClientRect();
|
||||
const dropTargetHeight = rect.height;
|
||||
const hitBoundary = 0.30;
|
||||
|
|
@ -54,17 +54,17 @@ export const HitAreaEvaluator = {
|
|||
|
||||
// BOTTOM HIT
|
||||
} else if(y > rect.bottom - hixbox){
|
||||
if(isOpen){
|
||||
if(activeInCmd){
|
||||
return null;
|
||||
} else {
|
||||
return "bottom";
|
||||
}
|
||||
// MIDDLE HIT
|
||||
} else {
|
||||
if(isOpen){
|
||||
return null;
|
||||
} else {
|
||||
if(activeInCmd){
|
||||
return "in";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -58,9 +58,10 @@ export const useGroupDnd = ({
|
|||
const evaluateHitArea = useCallback((dropped: TaskElementDto, monitor: DropTargetMonitor) => {
|
||||
if(dropped.name === group.name) return null;
|
||||
const coord = monitor.getClientOffset()??{x: -1, y: -1};
|
||||
const hit = HitAreaEvaluator.evaluateGroup(coord, groupRef.current as HTMLElement, isGroupOpen);
|
||||
const inCmdAvailable = !isGroupOpen || group.tasks.length === 0;
|
||||
const hit = HitAreaEvaluator.evaluateGroup(coord, groupRef.current as HTMLElement, inCmdAvailable);
|
||||
return hit;
|
||||
}, [group.name, groupRef, isGroupOpen])
|
||||
}, [group, groupRef, isGroupOpen])
|
||||
|
||||
const [{ isOver }, drop ] = useDrop({
|
||||
accept: ["TASK", "GROUP"],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { NoteRepository, RoutineNote, RoutineNoteDto, TaskDto, TaskElementDto, TaskGroupDto } from "@entities/note";
|
||||
import { NoteRepository, RoutineNote, RoutineNoteDto, TaskDto, TaskElementDto, TaskGroup, TaskGroupDto } from "@entities/note";
|
||||
import { executeRoutineNotesSynchronize } from "@entities/note-synchronize";
|
||||
import { TaskParent } from "@entities/note/domain/TaskParent";
|
||||
import { GroupRepository, Routine, RoutineGroup, RoutineRepository } from "@entities/routine";
|
||||
import { useRoutineNote } from "@features/note";
|
||||
import { flatMap } from "lodash";
|
||||
|
||||
|
||||
const NOTE_DOMAIN = () => {
|
||||
|
|
@ -13,6 +17,72 @@ const save = async (note: RoutineNote) => {
|
|||
}
|
||||
|
||||
|
||||
type RoutineObject = Routine | RoutineGroup;
|
||||
|
||||
type OrderChangeList = RoutineObject[];
|
||||
|
||||
const ORDER_OFFSET = 1000;
|
||||
/**
|
||||
* parent의 자식의 순서들을 반영하여 order, group등이 적절하게 변경된 Routine, RoutineGroup의 배열을 반환한다.
|
||||
* @param parent
|
||||
*/
|
||||
const resolveChangeList = async (parent: TaskParent): Promise<OrderChangeList> => {
|
||||
const routines = await RoutineRepository.loadAll();
|
||||
const groups = await GroupRepository.loadAll();
|
||||
const map = new Map<string, RoutineObject>([...routines, ...groups].map(r => [r.getName(), r]));
|
||||
|
||||
type Acc = {
|
||||
changeList: OrderChangeList;
|
||||
prevOrder: number;
|
||||
}
|
||||
|
||||
return parent.getChildren()
|
||||
.map(c => {
|
||||
const rOrG = map.get(c.getName());
|
||||
if(!rOrG) throw new Error("Routine or Group not found");
|
||||
return rOrG;
|
||||
})
|
||||
.reduce((acc, ro) => {
|
||||
// order
|
||||
let currentOrder = ro.getProperties().getOrder();
|
||||
if(!(acc.prevOrder < currentOrder)){
|
||||
currentOrder = acc.prevOrder + ORDER_OFFSET;
|
||||
ro.getProperties().setOrder(currentOrder);
|
||||
}
|
||||
acc.prevOrder = currentOrder;
|
||||
|
||||
// group
|
||||
if(ro instanceof Routine){
|
||||
const group = ro.getProperties().getGroup();
|
||||
if(parent instanceof RoutineNote && group !== RoutineGroup.UNGROUPED_NAME){
|
||||
ro.getProperties().setGroup(RoutineGroup.UNGROUPED_NAME);
|
||||
}
|
||||
else if(parent instanceof TaskGroup && group !== parent.getName()){
|
||||
ro.getProperties().setGroup(parent.getName());
|
||||
}
|
||||
}
|
||||
|
||||
acc.changeList.push(ro);
|
||||
return acc;
|
||||
}, { changeList: [], prevOrder: 0 } as Acc)
|
||||
.changeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 새로운 order로 routine, routine-group의 order를 업데이트한다.
|
||||
*/
|
||||
const updateRoutineAndRoutineGroups = async (parent: TaskParent) => {
|
||||
const list = await resolveChangeList(parent);
|
||||
for(const ro of list){
|
||||
if(ro instanceof Routine){
|
||||
await RoutineRepository.update(ro);
|
||||
} else {
|
||||
await GroupRepository.update(ro);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type TaskOnTask = {
|
||||
dropped: TaskDto;
|
||||
on: TaskDto;
|
||||
|
|
@ -32,7 +102,8 @@ const taskDropOnTask = (args: TaskOnTask) => {
|
|||
return args.hit === "top" ? idx : idx + 1
|
||||
});
|
||||
save(note);
|
||||
|
||||
updateRoutineAndRoutineGroups(parent)
|
||||
.then(() => executeRoutineNotesSynchronize(note.getDay()));
|
||||
return note.toJSON();
|
||||
}
|
||||
|
||||
|
|
@ -48,13 +119,13 @@ const groupDropOnTask = (args: GroupOnTask) => {
|
|||
const dropped = root.find(t => t.getName() === args.dropped.name);
|
||||
if(!on || !dropped) throw new Error("Dest Task or Dropped Task not found. Or group has dropped in another group.");
|
||||
|
||||
const idx = root.indexOf(on);
|
||||
note.addEl(dropped, (arr) => {
|
||||
const idx = arr.indexOf(on);
|
||||
return args.hit === "top" ? idx : idx + 1
|
||||
});
|
||||
save(note);
|
||||
|
||||
updateRoutineAndRoutineGroups(note)
|
||||
.then(() => executeRoutineNotesSynchronize(note.getDay()));
|
||||
return note.toJSON();
|
||||
}
|
||||
|
||||
|
|
@ -69,17 +140,20 @@ const taskDropOnGroup = (args: TaskOnGroup) => {
|
|||
const dropped = note.findTask(args.dropped.name);
|
||||
if(!on || !dropped) throw new Error("Dest Group or Dropped Task not found.");
|
||||
|
||||
let parent: TaskParent;
|
||||
if(args.hit === "in"){
|
||||
on.addTask(dropped);
|
||||
parent = on;
|
||||
} else {
|
||||
const idx = note.getChildren().indexOf(on);
|
||||
note.addTask(dropped, (arr) => {
|
||||
const idx = arr.indexOf(on);
|
||||
return args.hit === "top" ? idx : idx + 1
|
||||
});
|
||||
parent = note;
|
||||
}
|
||||
save(note);
|
||||
|
||||
updateRoutineAndRoutineGroups(parent)
|
||||
.then(() => executeRoutineNotesSynchronize(note.getDay()));
|
||||
return note.toJSON();
|
||||
}
|
||||
|
||||
|
|
@ -88,20 +162,20 @@ type GroupOnGroup = {
|
|||
on: TaskGroupDto;
|
||||
hit: "top" | "bottom";
|
||||
}
|
||||
const groupDropOnGroup = (args: GroupOnGroup) => {
|
||||
const groupDropOnGroup = (args: GroupOnGroup)=> {
|
||||
const note = NOTE_DOMAIN();
|
||||
const root = note.getChildren();
|
||||
const on = root.find(t => t.getName() === args.on.name);
|
||||
const dropped = root.find(t => t.getName() === args.dropped.name);
|
||||
if(!on || !dropped) throw new Error("Dest Group or Dropped Group not found.");
|
||||
|
||||
const idx = root.indexOf(on);
|
||||
note.addEl(dropped, (arr) => {
|
||||
const idx = arr.indexOf(on);
|
||||
return args.hit === "top" ? idx : idx + 1
|
||||
});
|
||||
save(note);
|
||||
|
||||
updateRoutineAndRoutineGroups(note)
|
||||
.then(() => executeRoutineNotesSynchronize(note.getDay()));
|
||||
return note.toJSON();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue