- 열린 문서면 저장 안 한 고침 물음 뒤 새 이름으로 다시 엶 · 서버 새 길 없음 - 새 지킴 시험 test_720_m02_folder_layer(임시 storage 이름 바꾸기 왕복 · 화면 글 대조) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
498 lines
20 KiB
TypeScript
498 lines
20 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_UI_Side_Library.ts
|
|
* 좌측 서고 칸(716-4) — 일위대가(`precombo`) · 단가산출(`sangeun`) 두 칸이 같이 쓰는 층 단추 + 그 층 나무.
|
|
* 칸마다 다른 것(종류 · 목록 고름 · 새 문서 몸 · 글)은 `LibraryOptions` 로 받음(일위대가 = `UI_Side_Precombo`).
|
|
* 나무 = 자유 폴더(158-1 · 깊이 자유 · 층마다 따로 · 자리 없는 문서 = 뿌리) — 폴더 자리만 서버
|
|
* (`{종류}-tree`)에 적음 · 문서 파일은 안 옮김. 폴더 오른쪽 클릭 [새 폴더] · [여기에 새 …] · [이름 바꾸기] ·
|
|
* [지우기](안에 든 문서째 + 확인 창 · 쓰는 곳 있으면 서버 409 까닭 그대로) · 빈 자리 = 뿌리 메뉴 · 끌어 옮기기.
|
|
* 여럿(구조물 조합 나무 꼴 · `TreePanel_Pick`) — Ctrl · Shift 클릭 고름 · 오른쪽 클릭 · Ctrl+C · Ctrl+V ·
|
|
* Delete 가 고른 것 전부에 · 복사는 읽을 수 있는 층 어디서나 → 쓸 수 있는 층에 붙여넣기(새 키).
|
|
* 문서 누름 = 편집 칸 · 켜진 층 단추 다시 누름 = 목록표(페이지 몫).
|
|
* 최상위 폴더(720-2) — 층 단추 옆 「+」(맨 위 메뉴) · 폴더 · 문서 줄 [최상위 폴더 만들기] = 같은 `addFolder([])`.
|
|
* 문서 줄 [이름 바꾸기](720-3) — 읽고 문서 `이름` 만 갈아 저장(서버 새 길 없음) · 열린 문서면 새 이름으로 다시 엶.
|
|
* ========================================================================== */
|
|
|
|
import { isCtrlKey } from "../A00_Common/key_shortcut";
|
|
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
|
import { createEmpty } from "@ui/ui_template_empty";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import {
|
|
createTreeRow,
|
|
openTreeMenu,
|
|
renderTree,
|
|
type TreeMenuItem,
|
|
type TreeNode,
|
|
} from "@ui/ui_template_tree";
|
|
import {
|
|
StaleError,
|
|
editPrecomboTree,
|
|
fetchPrecomboTree,
|
|
listTemplates,
|
|
newLibrary,
|
|
pasteLibraryTree,
|
|
readTemplate,
|
|
saveTemplate,
|
|
type Layer,
|
|
type LibraryKind,
|
|
type PrecomboTree,
|
|
} from "./M02_MasterTemplete_Api_Fetch";
|
|
import { nameDialog } from "./M02_MasterTemplete_Library_List";
|
|
import type { Root } from "./M02_MasterTemplete_Layer";
|
|
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
|
import { askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
|
import { attachTreeDrag } from "./M02_MasterTemplete_TreePanel_Drag";
|
|
import { folderIdOf, samePath } from "./M02_MasterTemplete_TreePanel_Model";
|
|
import {
|
|
clipOf,
|
|
keepClip,
|
|
pickClick,
|
|
refOf,
|
|
type Target,
|
|
} from "./M02_MasterTemplete_TreePanel_Pick";
|
|
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
|
|
|
/** 서고 칸 층(134-3) — 페이지 층과 따로 · `owner` null = 자기 층 */
|
|
export interface LibraryScope {
|
|
root: Root;
|
|
layer: Layer;
|
|
owner: string | null;
|
|
canWrite: boolean;
|
|
}
|
|
|
|
export interface LibraryOptions {
|
|
kind: LibraryKind;
|
|
scope: () => LibraryScope;
|
|
/** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) · `add` = 단추 옆 「+」(720-2) */
|
|
bar: (add: HTMLElement) => HTMLElement;
|
|
confirmLeave: () => Promise<boolean>;
|
|
onOpen: (sel: Selection | null) => void;
|
|
getOpen: () => Selection | null;
|
|
/** 그 층 목록표 고름 — 문서 열기의 바탕(종류 · 층 · 주인 · 권한) */
|
|
list: (scope: LibraryScope) => Selection;
|
|
/** [여기에 새 …] 문서 몸 */
|
|
blank: (name: string) => Record<string, unknown>;
|
|
/** 빈 나무 글 · 새 문서 메뉴 글 */
|
|
text: { none: string; newHere: string };
|
|
/** 문서 줄 덧메뉴(706-1 회사에 올리기 요청) — 없거나 빈 목록이면 메뉴 없음 */
|
|
more?: (key: string, name: string) => TreeMenuItem[];
|
|
}
|
|
|
|
export interface LibraryHandle {
|
|
body: HTMLElement;
|
|
/** 고름 표시만 */
|
|
paint: () => void;
|
|
/** 층 단추 · 나무를 그 층 목록으로 다시 그림 */
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
export interface LibraryRow {
|
|
이름: string;
|
|
종류: string;
|
|
제목?: string | null;
|
|
구분?: string | null;
|
|
상세구분?: string | null;
|
|
}
|
|
|
|
export interface LibraryFolder {
|
|
path: string[];
|
|
folders: LibraryFolder[];
|
|
docs: LibraryRow[];
|
|
}
|
|
|
|
/** 폴더 나무(158-1) — 적어 둔 폴더(그 차례) + 문서 자리(없으면 뿌리 · 모르는 폴더면 그 폴더를 만듦) */
|
|
export function buildLibraryFolders(
|
|
rows: LibraryRow[],
|
|
view: Pick<PrecomboTree, "폴더" | "자리">,
|
|
): LibraryFolder {
|
|
const root: LibraryFolder = { path: [], folders: [], docs: [] };
|
|
const at = (path: string[]): LibraryFolder =>
|
|
path.reduce((node, name, i) => {
|
|
let next = node.folders.find((f) => f.path[i] === name);
|
|
if (!next) {
|
|
next = { path: path.slice(0, i + 1), folders: [], docs: [] };
|
|
node.folders.push(next);
|
|
}
|
|
return next;
|
|
}, root);
|
|
view.폴더.forEach(at);
|
|
for (const row of rows) at(view.자리[row.이름] ?? []).docs.push(row);
|
|
return root;
|
|
}
|
|
|
|
/** 폴더 안 문서 수(아래 폴더까지) */
|
|
export const docCount = (node: LibraryFolder): number =>
|
|
node.docs.length + node.folders.reduce((n, f) => n + docCount(f), 0);
|
|
|
|
type TreeBody = Parameters<typeof editPrecomboTree>[1];
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
|
|
|
export function buildLibrarySection(opt: LibraryOptions): LibraryHandle {
|
|
const kind = opt.kind;
|
|
const clipKind = `library:${kind}`;
|
|
const barHost = el("div");
|
|
const treeHost = el("div", {
|
|
className: "ui-tree m02-tree__body",
|
|
attrs: { "data-library-tree": kind, tabindex: "0" },
|
|
});
|
|
// 층 단추 옆 「+」(720-2 · 구조물 조합 꼴) — 맨 위 메뉴 · 읽기만 층 = 숨김(그릴 때)
|
|
const add = createButton({
|
|
label: "+",
|
|
variant: "ghost",
|
|
onClick: (event) => openTreeMenu(event, menuAt([])),
|
|
});
|
|
add.classList.add("m02-tree__add");
|
|
add.title = L("M02_TreeAddTop");
|
|
add.setAttribute("aria-label", L("M02_TreeAddTop"));
|
|
const leaves = new Map<string, HTMLElement>();
|
|
const toggled = new Map<string, boolean>(); // 손으로 펴고 접은 폴더 — 다시 그려도 그대로
|
|
const targets = new Map<string, Target>();
|
|
const counts = new Map<string, number>();
|
|
let picked = new Set<string>();
|
|
let anchor: string | null = null;
|
|
let rows: LibraryRow[] = [];
|
|
let view: PrecomboTree | null = null;
|
|
let seq = 0;
|
|
let busy = false;
|
|
|
|
const open = async (row: LibraryRow): Promise<void> => {
|
|
const at = opt.scope();
|
|
if (!(await opt.confirmLeave())) return;
|
|
opt.onOpen({
|
|
...opt.list(at),
|
|
name: row.이름,
|
|
label: row.제목 ? `${row.이름} ${row.제목}` : row.이름,
|
|
});
|
|
};
|
|
|
|
/** 나무 고침 한 번 — 판 다름 = 다시 읽음 · 막힘 까닭(같은 이름 · 쓰는 곳) 그대로 */
|
|
const apply = async (run: (판: string) => Promise<PrecomboTree>): Promise<void> => {
|
|
if (!view || busy) return;
|
|
busy = true;
|
|
try {
|
|
view = await run(view.판);
|
|
const gone = view.알림?.지운문서 ?? [];
|
|
if (gone.length) {
|
|
rows = rows.filter((r) => !gone.includes(r.이름));
|
|
const cur = opt.getOpen();
|
|
if (cur && cur.kind === kind && gone.includes(cur.name)) opt.onOpen(null);
|
|
}
|
|
if (view.알림?.만든문서?.length) await refresh(true);
|
|
else draw();
|
|
showToast(L("M02_TreeDone"), "success");
|
|
} catch (error) {
|
|
if (error instanceof StaleError && error.message === "HTTP 409") {
|
|
await refresh();
|
|
showToast(L("M02_TreeStale"), "error");
|
|
} else {
|
|
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error");
|
|
}
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
};
|
|
const edit = async (body: TreeBody): Promise<void> => {
|
|
const at = opt.scope();
|
|
for (const path of [body.경로, body.새경로])
|
|
path?.forEach((_, i) => toggled.set(folderIdOf(path.slice(0, i + 1)), true));
|
|
await apply((판) => editPrecomboTree(at.layer, body, 판, at.owner, kind));
|
|
};
|
|
|
|
/** [여기에 새 …] — 뿌리에 생성 → 폴더면 그 자리로 옮김 → 나무 다시 · 상세 엶 */
|
|
const make = (path: string[]): void => {
|
|
const at = opt.scope();
|
|
nameDialog(opt.text.newHere, "", async (name) => {
|
|
const got = await newLibrary(at.layer, kind, { 문서: opt.blank(name) }, at.owner);
|
|
showToast(L("M02_Created"), "success");
|
|
await refresh();
|
|
if (path.length) await edit({ op: "move", 키: got.이름, 새경로: path });
|
|
await open({ 이름: got.이름, 종류: kind, 제목: name });
|
|
});
|
|
};
|
|
const addFolder = async (parent: string[]): Promise<void> => {
|
|
const got = await askText(L("M02_TreeAddFolder"), [
|
|
{ key: "name", label: L("M02_TreeFolderName") },
|
|
]);
|
|
if (got) await edit({ op: "add-folder", 경로: parent, 이름: got.name });
|
|
};
|
|
const rename = async (path: string[]): Promise<void> => {
|
|
const was = path[path.length - 1];
|
|
const got = await askText(L("M02_TreeRename"), [
|
|
{ key: "name", label: L("M02_TreeFolderName"), value: was },
|
|
]);
|
|
if (got && got.name !== was) await edit({ op: "rename", 경로: path, 이름: got.name });
|
|
};
|
|
|
|
/** 문서 이름 바꾸기(720-3) — 열린 문서면 저장 안 한 고침 물음 → 읽고 `이름` 만 갈아 저장 → 나무 · 상세 새 이름 */
|
|
const renameDoc = async (row: LibraryRow): Promise<void> => {
|
|
const at = opt.scope();
|
|
const was = row.제목 || row.이름;
|
|
const got = await askText(L("M02_TreeRename"), [
|
|
{ key: "name", label: L("M02_TreeItemName"), value: was },
|
|
]);
|
|
const name = got?.name.trim();
|
|
if (!name || name === was) return;
|
|
const cur = opt.getOpen();
|
|
const shown =
|
|
cur?.kind === kind &&
|
|
cur.name === row.이름 &&
|
|
cur.layer === at.layer &&
|
|
(cur.owner ?? null) === at.owner;
|
|
if (shown && !(await opt.confirmLeave())) return;
|
|
try {
|
|
const doc = await readTemplate(at.layer, kind, row.이름, null, at.owner);
|
|
const body = { ...(doc.문서 as Record<string, unknown>), 이름: name };
|
|
await saveTemplate(at.layer, kind, row.이름, null, doc.판 ?? "", body, at.owner);
|
|
showToast(L("M02_TreeDone"), "success");
|
|
await refresh(true);
|
|
if (shown) await open({ ...row, 제목: name });
|
|
} catch (error) {
|
|
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error");
|
|
}
|
|
};
|
|
|
|
/* --- 지우기 · 여럿 · 복사 붙여넣기(구조물 조합 나무 꼴) --- */
|
|
const pickedTargets = (): Target[] =>
|
|
[...picked].map((id) => targets.get(id)).filter((t): t is Target => !!t);
|
|
/** 지우기 — 확인 창(폴더 = 안의 문서 수) 뒤 한 번에 · 쓰는 곳이 있으면 서버가 막음(그 이름) */
|
|
const remove = async (list: Target[]): Promise<void> => {
|
|
if (!list.length || !opt.scope().canWrite) return;
|
|
const [one] = list;
|
|
const ask =
|
|
list.length > 1
|
|
? L("M02_TreeDeleteManyAsk").replace("{n}", String(list.length))
|
|
: one.kind === "item"
|
|
? L("M02_TreeDeleteItem").replace("{value}", one.item.이름)
|
|
: L("M02_TreeDeleteFolder")
|
|
.replace("{value}", one.path[one.path.length - 1])
|
|
.replace("{n}", String(counts.get(folderIdOf(one.path)) ?? 0));
|
|
if (!(await showConfirmDialog(ask, L("M02_Delete")))) return;
|
|
await edit({ op: "delete", 대상: list.map(refOf) });
|
|
};
|
|
const copy = (list: Target[]): void => {
|
|
if (!list.length) return;
|
|
const at = opt.scope();
|
|
keepClip({ kind: clipKind, from: { layer: at.layer, owner: at.owner }, refs: list.map(refOf) });
|
|
showToast(L("M02_TreeCopied").replace("{n}", String(list.length)), "success");
|
|
};
|
|
const paste = async (dest: string[]): Promise<void> => {
|
|
const got = clipOf(clipKind);
|
|
const at = opt.scope();
|
|
if (!got || !at.canWrite) return;
|
|
toggled.set(folderIdOf(dest), true);
|
|
await apply(() => pasteLibraryTree(kind, at.layer, at.owner, got, dest));
|
|
};
|
|
|
|
const menuAt = (path: string[]): TreeMenuItem[] => {
|
|
const off = opt.scope().canWrite ? undefined : tc("ReadOnly");
|
|
const item = (label: string, run: () => void): TreeMenuItem => ({
|
|
label,
|
|
run,
|
|
disabledReason: off,
|
|
});
|
|
const got = clipOf(clipKind);
|
|
const target: Target = { kind: "folder", path };
|
|
return [
|
|
item(L("M02_TreeAddFolder"), () => void addFolder(path)),
|
|
item(opt.text.newHere, () => make(path)),
|
|
...(path.length
|
|
? [
|
|
item(L("M02_TreeAddTopFolder"), () => void addFolder([])),
|
|
item(L("M02_TreeRename"), () => void rename(path)),
|
|
item(L("M02_Delete"), () => void remove([target])),
|
|
{ label: L("M02_TreeCopy").replace("{n}", "1"), run: () => copy([target]) },
|
|
]
|
|
: []),
|
|
...(got
|
|
? [item(L("M02_TreePaste").replace("{n}", String(got.refs.length)), () => void paste(path))]
|
|
: []),
|
|
];
|
|
};
|
|
/** 오른쪽 클릭 — 고른 것 안의 줄이면 고른 것 전부(복사 · 지우기) · 아니면 그 줄 하나 */
|
|
const menuFor = (id: string, single: () => TreeMenuItem[]): TreeMenuItem[] => {
|
|
if (!picked.has(id)) setPicked(new Set([id]), id);
|
|
const list = pickedTargets();
|
|
if (list.length < 2) return single();
|
|
const n = String(list.length);
|
|
const off = opt.scope().canWrite ? undefined : tc("ReadOnly");
|
|
return [
|
|
{ label: L("M02_TreeCopy").replace("{n}", n), run: () => copy(list) },
|
|
{
|
|
label: L("M02_TreeDeleteMany").replace("{n}", n),
|
|
run: () => void remove(list),
|
|
disabledReason: off,
|
|
},
|
|
];
|
|
};
|
|
|
|
const leaf = (row: LibraryRow, folder: string[]): TreeNode => {
|
|
const id = `doc:${row.이름}`;
|
|
const label = row.제목 || row.이름;
|
|
const item = { key: row.이름, 열: row.이름, 경로: [...folder, label], 이름: label };
|
|
const target: Target = { kind: "item", item };
|
|
targets.set(id, target);
|
|
const off = (): string | undefined => (opt.scope().canWrite ? undefined : tc("ReadOnly"));
|
|
const single = (): TreeMenuItem[] => [
|
|
{ label: L("M02_TreeAddTopFolder"), run: () => void addFolder([]), disabledReason: off() },
|
|
{ label: L("M02_TreeRename"), run: () => void renameDoc(row), disabledReason: off() },
|
|
{ label: L("M02_TreeCopy").replace("{n}", "1"), run: () => copy([target]) },
|
|
{
|
|
label: L("M02_Delete"),
|
|
run: () => void remove([target]),
|
|
disabledReason: off(),
|
|
},
|
|
...(opt.more?.(row.이름, label) ?? []),
|
|
];
|
|
const menu = () => menuFor(id, single);
|
|
return { id, label, tip: row.이름, count: null, run: () => void open(row), menu };
|
|
};
|
|
const folderNode = (node: LibraryFolder): TreeNode => {
|
|
const id = folderIdOf(node.path);
|
|
const count = docCount(node);
|
|
targets.set(id, { kind: "folder", path: node.path });
|
|
counts.set(id, count);
|
|
return {
|
|
id,
|
|
label: node.path[node.path.length - 1],
|
|
count,
|
|
folder: true,
|
|
open: toggled.get(id) ?? count > 0,
|
|
children: kids(node),
|
|
menu: () => menuFor(id, () => menuAt(node.path)),
|
|
};
|
|
};
|
|
const kids = (node: LibraryFolder): TreeNode[] => [
|
|
...node.folders.map(folderNode),
|
|
...node.docs.map((row) => leaf(row, node.path)),
|
|
];
|
|
|
|
/** 끌어 옮기기(구조물 조합 나무와 같은 부품) — 고른 것 전부 · 폴더 줄 위 = 그 안 · 빈 자리 = 뿌리 */
|
|
const drag = attachTreeDrag({
|
|
body: treeHost,
|
|
targetOf: (id) => targets.get(id),
|
|
listFor: (id) =>
|
|
picked.size > 1 && picked.has(id)
|
|
? pickedTargets()
|
|
: targets.has(id)
|
|
? [targets.get(id)!]
|
|
: null,
|
|
move: (list, dest) => {
|
|
const from = (t: Target): string[] =>
|
|
t.kind === "item" ? t.item.경로.slice(0, -1) : t.path.slice(0, -1);
|
|
const moving = list.filter((t) => !samePath(from(t), dest));
|
|
if (moving.length) void edit({ op: "move", 대상: moving.map(refOf), 새경로: dest });
|
|
},
|
|
});
|
|
|
|
const visibleIds = (): string[] =>
|
|
[...treeHost.querySelectorAll<HTMLElement>("[data-node]")]
|
|
.filter((b) => !b.closest("[hidden]"))
|
|
.map((b) => b.dataset.node ?? "");
|
|
const setPicked = (next: Set<string>, at: string | null): void => {
|
|
picked = next;
|
|
anchor = at;
|
|
for (const row of treeHost.querySelectorAll<HTMLElement>("[data-node]"))
|
|
row.classList.toggle("is-picked", picked.size > 1 && picked.has(row.dataset.node ?? ""));
|
|
};
|
|
|
|
const draw = (): void => {
|
|
leaves.clear();
|
|
targets.clear();
|
|
counts.clear();
|
|
const tree = buildLibraryFolders(rows, view ?? { 폴더: [], 자리: {} });
|
|
if (!tree.docs.length && !tree.folders.length)
|
|
return void treeHost.replaceChildren(createEmpty({ text: opt.text.none, small: true }));
|
|
const canWrite = opt.scope().canWrite;
|
|
treeHost.replaceChildren(
|
|
...renderTree(kids(tree), (node, caret, depth, onClick) => {
|
|
const row = createTreeRow(node, caret, depth, () => {
|
|
onClick();
|
|
if (!node.children?.length) return;
|
|
toggled.set(node.id, row.querySelector(".ui-tree__caret")?.textContent !== "▸");
|
|
});
|
|
row.dataset.node = node.id;
|
|
// Ctrl · Shift 클릭 = 고르기만(열기 · 접기 안 함) — 줄 본디 click 보다 먼저(capture)
|
|
row.addEventListener(
|
|
"click",
|
|
(event) => {
|
|
const mod = { ctrl: event.ctrlKey || event.metaKey, shift: event.shiftKey };
|
|
const got = pickClick(picked, anchor, node.id, mod, visibleIds());
|
|
if (got.handled) event.stopImmediatePropagation();
|
|
setPicked(got.picked, got.anchor);
|
|
},
|
|
{ capture: true },
|
|
);
|
|
if (node.run) {
|
|
row.dataset.libraryKey = node.id.slice(4);
|
|
leaves.set(node.id.slice(4), row);
|
|
}
|
|
if (canWrite) drag.arm(row, node.id);
|
|
return row;
|
|
}),
|
|
);
|
|
setPicked(new Set([...picked].filter((id) => targets.has(id))), anchor);
|
|
paint();
|
|
};
|
|
|
|
// 빈 자리 오른쪽 클릭 = 뿌리 메뉴(새 폴더 · 새 문서 · 붙여넣기)
|
|
treeHost.addEventListener("contextmenu", (event) => {
|
|
if (!view || (event.target as HTMLElement).closest("[data-node]")) return;
|
|
event.preventDefault();
|
|
openTreeMenu(event, menuAt([]));
|
|
});
|
|
// 키 — Delete = 고른 것(없으면 초점 줄) 지움 · Ctrl+C 복사 · Ctrl+V = 초점 폴더 안(아니면 뿌리)
|
|
treeHost.addEventListener("keydown", (event) => {
|
|
const row = (event.target as HTMLElement).closest<HTMLElement>("[data-node]");
|
|
const id = row?.dataset.node ?? "";
|
|
const target = targets.get(id);
|
|
const list = picked.has(id) ? pickedTargets() : target ? [target] : [];
|
|
if (event.key === "Delete" && list.length) {
|
|
event.preventDefault();
|
|
void remove(list);
|
|
} else if (isCtrlKey(event, "c") && list.length) {
|
|
event.preventDefault();
|
|
copy(list);
|
|
} else if (isCtrlKey(event, "v")) {
|
|
event.preventDefault();
|
|
void paste(target?.kind === "folder" ? target.path : []);
|
|
}
|
|
});
|
|
|
|
const paint = (): void => {
|
|
const cur = opt.getOpen();
|
|
const at = opt.scope();
|
|
const mine =
|
|
cur && cur.kind === kind && cur.layer === at.layer && (cur.owner ?? null) === at.owner;
|
|
for (const [key, row] of leaves) row.classList.toggle("is-active", !!mine && cur.name === key);
|
|
};
|
|
|
|
/** `keep` = 나무 자리 그대로 두고 목록만 다시(붙여넣기 뒤 새 문서) */
|
|
const refresh = async (keep = false): Promise<void> => {
|
|
const mine = ++seq;
|
|
const at = opt.scope();
|
|
add.hidden = !at.canWrite;
|
|
barHost.replaceChildren(opt.bar(add));
|
|
if (!keep) treeHost.replaceChildren(createEmpty({ text: tc("Loading"), small: true }));
|
|
const [got, tree] = await Promise.all([
|
|
// 못 받으면 빈 나무(목록표에서 까닭) · 폴더를 못 받으면 문서는 모두 뿌리
|
|
listTemplates(at.layer, null, at.owner).then(
|
|
(list): LibraryRow[] => list.filter((r) => r.종류 === kind),
|
|
(): LibraryRow[] => [],
|
|
),
|
|
keep && view
|
|
? Promise.resolve(view)
|
|
: fetchPrecomboTree(at.layer, at.owner, kind).catch(() => null),
|
|
]);
|
|
if (mine !== seq) return;
|
|
rows = got;
|
|
view = tree;
|
|
draw();
|
|
};
|
|
|
|
const body = el("div", {
|
|
className: "m02-side__items ui-sidebar-list",
|
|
attrs: { "data-library": kind },
|
|
children: [barHost, treeHost],
|
|
});
|
|
return { body, paint, refresh: () => refresh() };
|
|
}
|