feat(M02): 「구조물 조합」 제목 줄 오른쪽 끝 「새 항목」 (PLAN 30-5 구조물 쪽)
- 쓸 수 있는 층만 · 구조물 조합일 때만 보임 · 나무 「항목 추가」와 같은 창 · 고른 폴더 안(없으면 맨 위) - 공용 모양 newItemButton · m02-main__new(일위대가 「새 조합」이 같은 자리 · 모양을 씀) - 시험 한 건 · 머리 줄 시험 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,8 @@ export interface TreePanelHandle {
|
||||
refresh: () => Promise<void>;
|
||||
/** 표에서 고른 열을 나무에 표시(null = 지움) */
|
||||
select: (colId: string | null) => void;
|
||||
/** 「새 항목」 — 고른 폴더 안(없으면 맨 위)에 항목 추가 창 */
|
||||
newItem: () => void;
|
||||
}
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
||||
@@ -672,5 +674,11 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
}
|
||||
});
|
||||
|
||||
return { root, search: search.root, add: tools, refresh, select };
|
||||
/** 상세 제목 줄 「새 항목」(30-5) — 나무 「항목 추가」와 같은 창 · 고른 폴더(하나)가 있으면 그 안 · 아니면 맨 위 */
|
||||
const newItem = (): void => {
|
||||
const at = anchor && picked.has(anchor) ? targets.get(anchor) : undefined;
|
||||
void addItem(at?.kind === "folder" ? at.path : []);
|
||||
};
|
||||
|
||||
return { root, search: search.root, add: tools, refresh, select, newItem };
|
||||
}
|
||||
|
||||
@@ -106,13 +106,23 @@ export function canEdit(layer: Layer, isAdmin: boolean): boolean {
|
||||
}
|
||||
|
||||
/** 페이지가 끼우는 것 — `top` = 편집 칸 맨 위 막대(회사용 읽기만 안내) · `onColumn` = 집계표에서 고른 열 ·
|
||||
* `search` = 「구조물 조합」을 열 때만 상세 위쪽에 보이는 나무 찾기 칸(나무는 좌측 패널 · PLAN 29-2) */
|
||||
* `search` = 「구조물 조합」을 열 때만 상세 위쪽에 보이는 나무 찾기 칸(나무는 좌측 패널 · PLAN 29-2) ·
|
||||
* `newItem` = 「구조물 조합」 제목 줄 오른쪽 끝 「새 항목」(쓸 수 있는 층만 · PLAN 30-5 · 공용 `m02-main__new`) */
|
||||
export interface MainExtras {
|
||||
top?: HTMLElement;
|
||||
search?: HTMLElement;
|
||||
newItem?: HTMLElement;
|
||||
onColumn?: (column: string | null) => void;
|
||||
}
|
||||
|
||||
/** 상세 제목 줄 오른쪽 끝 「새 …」 단추 — 공용 모양 `m02-main__new`(일위대가 「새 조합」과 같게 · 30-5) */
|
||||
export function newItemButton(run: () => void, label = L("M02_TreeNewItem")): HTMLElement {
|
||||
const button = createButton({ label, variant: "filled", size: "sm" });
|
||||
button.classList.add("m02-main__new");
|
||||
button.addEventListener("click", run);
|
||||
return button;
|
||||
}
|
||||
|
||||
/** `onOpened` — 무엇을 열든(좌측 · 카드 [도면 수정] · 다시 읽기) 연 바로 뒤 부름(좌측 고름 표시) */
|
||||
export function createMain(
|
||||
isAdmin: boolean,
|
||||
@@ -133,7 +143,10 @@ export function createMain(
|
||||
className: "m02-main",
|
||||
children: [
|
||||
...(extras.top ? [extras.top] : []),
|
||||
el("div", { className: "m02-main__head", children: [title, back, slot, reload, save] }),
|
||||
el("div", {
|
||||
className: "m02-main__head",
|
||||
children: [title, back, slot, reload, save, ...(extras.newItem ? [extras.newItem] : [])],
|
||||
}),
|
||||
...(extras.search ? [extras.search] : []),
|
||||
notice,
|
||||
host,
|
||||
@@ -143,9 +156,10 @@ export function createMain(
|
||||
const root = column;
|
||||
const isCombo = (at: Selection | null): boolean =>
|
||||
!!at && at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project";
|
||||
// 찾기 칸은 「구조물 조합」일 때만
|
||||
// 찾기 칸 · 「새 항목」은 「구조물 조합」일 때만
|
||||
const showAside = (): void => {
|
||||
if (extras.search) extras.search.hidden = !isCombo(sel);
|
||||
if (extras.newItem) extras.newItem.hidden = !isCombo(sel);
|
||||
};
|
||||
|
||||
let sel: Selection | null = null;
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
createPeopleTree,
|
||||
seesOthers,
|
||||
} from "./M02_MasterTemplete_TreePanel_Roots";
|
||||
import { canEdit, createMain, type Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
import { canEdit, createMain, newItemButton, type Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
import { buildSide } from "./M02_MasterTemplete_UI_Side";
|
||||
import "./M02_MasterTemplete_UI_Style.css";
|
||||
|
||||
@@ -129,6 +129,8 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
{
|
||||
top: readOnlyBar?.root,
|
||||
search: tree.search,
|
||||
// 「새 항목」(30-5) — 제목 줄 오른쪽 끝 · 쓸 수 있는 층만 · 나무 「항목 추가」와 같은 창
|
||||
...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => tree.newItem()) } : {}),
|
||||
onColumn: (column) => {
|
||||
readOnlyBar?.setColumn(column);
|
||||
tree.select(column);
|
||||
|
||||
@@ -226,4 +226,13 @@
|
||||
|
||||
.m02-main__head + .m02-tree__search {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
}
|
||||
|
||||
/* 상세 제목 줄 오른쪽 끝 「새 항목」 · 「새 조합」(PLAN 30-5 · 공용) — 늘어나는 칸이 없어도 끝으로 */
|
||||
.m02-main__new {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.m02-main__new[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -233,3 +233,25 @@ def test_켜진_층_단추는_목록으로_복사_끌기():
|
||||
panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
|
||||
assert "if (opt.canEdit) {\n button.draggable = true;" in panel
|
||||
assert 'dragging?.some((t) => t.kind === "folder" && dest && isUnder(dest, t.path))' in panel
|
||||
|
||||
|
||||
def test_구조물_조합_제목_줄_새_항목():
|
||||
"""30-5 — 「구조물 조합」 제목 줄 오른쪽 끝 「새 항목」(공용 `m02-main__new` · 쓸 수 있는 층만 ·
|
||||
구조물 조합일 때만 보임) · 누르면 나무 「항목 추가」와 같은 창(고른 폴더 안 · 없으면 맨 위)."""
|
||||
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
|
||||
assert "export function newItemButton(" in main and 'classList.add("m02-main__new")' in main
|
||||
assert (
|
||||
"children: [title, back, slot, reload, save, ...(extras.newItem ? [extras.newItem] : [])]"
|
||||
in main
|
||||
)
|
||||
assert "if (extras.newItem) extras.newItem.hidden = !isCombo(sel);" in main
|
||||
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
||||
assert (
|
||||
"...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => tree.newItem()) } : {})"
|
||||
in page
|
||||
)
|
||||
panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
|
||||
assert 'void addItem(at?.kind === "folder" ? at.path : []);' in panel
|
||||
assert ".m02-main__new {\n margin-left: auto;" in _text(
|
||||
M02 / "M02_MasterTemplete_UI_Style.css"
|
||||
)
|
||||
|
||||
@@ -209,7 +209,7 @@ def test_표_도구_막대에_줄_더하기_줄_지우기_손_입력_열_없음(
|
||||
|
||||
def test_상세_화면_돌아가기는_집계표로_그_열_고른_채():
|
||||
main = _text(MAIN)
|
||||
assert "children: [title, back, slot, reload, save]" in main # 제목 바로 오른쪽
|
||||
assert "children: [title, back, slot, reload, save, " in main # 제목 바로 오른쪽
|
||||
assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 면적산출 · 재료산출만
|
||||
body = main.split('back.addEventListener("click"')[1]
|
||||
assert "confirmLeave()" in body and "STRUCTURE_TABLE" in body
|
||||
|
||||
@@ -131,6 +131,7 @@ export const ui_locales_m2 = {
|
||||
"Copied {n} — right-click a folder in a writable layer or press Ctrl+V",
|
||||
],
|
||||
M02_TreePicked: ["{n}개 고름", "{n} selected"],
|
||||
M02_TreeNewItem: ["새 항목", "New item"],
|
||||
M02_TreeCopyToMaster: ["마스터로 복사 ({n}개)", "Copy to master ({n})"],
|
||||
M02_TreeCopyToCompany: ["회사로 복사 ({n}개)", "Copy to company ({n})"],
|
||||
M02_TreeCopyToPersonal: ["개인으로 복사 ({n}개)", "Copy to personal ({n})"],
|
||||
|
||||
Reference in New Issue
Block a user