feat: dnd reorder 로직 재구성

This commit is contained in:
sechan100 2024-12-14 17:05:20 +09:00
parent 9aa4fe783d
commit da93b7020b
10 changed files with 116 additions and 32 deletions

View file

@ -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);
}
})
}

View file

@ -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;
}

View file

@ -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');

View file

@ -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"){

View file

@ -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));

View file

@ -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;
},
}

View file

@ -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`;
};

View file

@ -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;
}
}
},

View file

@ -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"],

View file

@ -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();
}