Merge remote-tracking branch 'origin/sub_laptop_4' into sub_laptop_1
This commit is contained in:
@@ -510,9 +510,11 @@ export const pasteTree = (
|
||||
export interface LayerOwner {
|
||||
owner: string;
|
||||
name: string;
|
||||
/** 개인 = 집계표가 아직 없음(사람 나무 「비어 있음」 · 39-6) */
|
||||
비어있음?: boolean;
|
||||
}
|
||||
|
||||
/** 회사 = 시스템 관리자만 회사 목록 · 개인 = 그 회사 사람(본인 뺌 · 층 자리 있는 것만) */
|
||||
/** 회사 = 시스템 관리자만 회사 목록 · 개인 = 그 회사를 맡은 사람 · 회사 사람 전부(나 맨 위) */
|
||||
export const fetchOwners = (
|
||||
layer: "company" | "personal",
|
||||
company: string | null = null,
|
||||
|
||||
@@ -79,7 +79,8 @@ export interface LayerAccess {
|
||||
needCompany: boolean;
|
||||
}
|
||||
|
||||
/** 서버 `M02_Layer_Access.grant` 와 같은 규칙 — 남의 층(`owner`)은 늘 읽기만(읽을 수 있나는 서버가 가름) */
|
||||
/** 서버 `M02_Layer_Access.grant` 와 같은 규칙 — 남의 층(`owner`) = 그 회사를 맡았으면 고침(39-6) ·
|
||||
* 읽을 수 있나는 서버가 가름 */
|
||||
export function accessFor(
|
||||
layer: ManageLayer,
|
||||
owner: string | null,
|
||||
@@ -87,10 +88,14 @@ export function accessFor(
|
||||
): LayerAccess {
|
||||
if (layer === "system") return { canWrite: user?.role === "SYSTEM_ADMIN", needCompany: false };
|
||||
const own = ownOwner(layer, user);
|
||||
if (owner && owner !== own) return { canWrite: false, needCompany: false };
|
||||
if (owner && owner !== own) {
|
||||
return { canWrite: manages(owner.split("/")[0], user), needCompany: false };
|
||||
}
|
||||
if (!own) return { canWrite: false, needCompany: true };
|
||||
const companyAdmin = user?.role === "ADMIN" || user?.role === "SYSTEM_ADMIN" || !!user?.is_master;
|
||||
return { canWrite: layer === "personal" || companyAdmin, needCompany: false };
|
||||
return {
|
||||
canWrite: layer === "personal" || manages(ownOwner("company", user), user),
|
||||
needCompany: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** 회사 C 를 맡음 — 시스템 관리자 · 또는 C 의 회사 관리자(서버 `M02_Layer_Access.manages` 짝 · 39-6) */
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type TreeRef,
|
||||
type TreeView,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { askCopyFolder, type CopyTarget } from "./M02_MasterTemplete_TreePanel_CopyTo";
|
||||
import { askFolder, askNewItem, askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
||||
import { attachTreeDrag } from "./M02_MasterTemplete_TreePanel_Drag";
|
||||
import {
|
||||
@@ -83,8 +84,8 @@ export interface TreePanelOptions {
|
||||
};
|
||||
/** 항목 추가 창의 일위대가 고르기(PLAN 31-2 · 세 층 조합 · 찾기 AND) — 없으면 그 줄 없음 */
|
||||
pickCombo?: () => Promise<{ key: string; label: string } | null>;
|
||||
/** 「〇〇로 복사」 — 고른 것을 그 층 맨 위에 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로) */
|
||||
copyTo?: { layer: Layer; owner: string | null; label: string }[];
|
||||
/** 「〇〇로 복사…」 — 그 층 폴더를 골라 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로 · 39-6) */
|
||||
copyTo?: CopyTarget[];
|
||||
/** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */
|
||||
canEdit: boolean;
|
||||
/** 항목을 누름 — 표에서 그 열을 고름 */
|
||||
@@ -542,18 +543,20 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
showToast(L("M02_TreeCopied").replace("{n}", String(list.length)), "success");
|
||||
}
|
||||
|
||||
/** 「〇〇로 복사」 — 그 층 맨 위에 바로(새 키 · 같은 이름 「(2)」 · 이 나무는 그대로) */
|
||||
async function copyTo(
|
||||
list: Target[],
|
||||
to: { layer: Layer; owner: string | null; label: string },
|
||||
): Promise<void> {
|
||||
/** 「〇〇로 복사…」 — 그 층 폴더를 고름(`askCopyFolder`) → 새 키 · 같은 이름 「(2)」 · 이 나무는 그대로 */
|
||||
async function copyTo(list: Target[], to: CopyTarget): Promise<void> {
|
||||
if (!api.paste || !list.length) return;
|
||||
const clip = { kind: clipKind, from: { layer: opt.layer, owner }, refs: list.map(refOf) };
|
||||
try {
|
||||
await api.paste(to.layer, to.owner, clip, []);
|
||||
const where = to.label.replace(/ ?\(.*$/, "").replace(/(으로|로) 복사$/, "");
|
||||
const dest = await askCopyFolder(api.fetch, to, list.length);
|
||||
if (!dest) return;
|
||||
await api.paste(to.layer, to.owner, clip, dest);
|
||||
const where = to.label.replace(/ ?\(.*$/, "").replace(/(으로|로) 복사…?$/, "");
|
||||
showToast(
|
||||
L("M02_TreeCopiedTo").replace("{n}", String(list.length)).replace("{layer}", where),
|
||||
L("M02_TreeCopiedTo")
|
||||
.replace("{n}", String(list.length))
|
||||
.replace("{layer}", where)
|
||||
.replace("{folder}", dest.join(" / ") || L("M02_TreeTop")),
|
||||
"success",
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_TreePanel_CopyTo.ts
|
||||
* 「〇〇로 복사…」(39-6) — 붙일 층의 나무를 읽어 대상 폴더를 고름(맨 위 · 폴더) → 경로 · 취소 = null.
|
||||
* 아직 만든 것이 없는 남의 층(404)은 맨 위만 — 첫 쓰기 때 서버가 소속을 보고 뼈대를 세움.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { Layer, TreeView } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { askFolder } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
||||
import { buildFolders, listFolders } from "./M02_MasterTemplete_TreePanel_Model";
|
||||
|
||||
export interface CopyTarget {
|
||||
layer: Layer;
|
||||
owner: string | null;
|
||||
/** 메뉴 글 — `{n}` = 개수 */
|
||||
label: string;
|
||||
}
|
||||
|
||||
export async function askCopyFolder(
|
||||
fetchTree: (layer: Layer, owner: string | null) => Promise<TreeView>,
|
||||
to: CopyTarget,
|
||||
n: number,
|
||||
): Promise<string[] | null> {
|
||||
const view = await fetchTree(to.layer, to.owner).catch(() => null);
|
||||
const folders = view ? listFolders(buildFolders(view)) : [];
|
||||
return askFolder(to.label.replace("{n}", String(n)), folders, () => false, []);
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
* 「구조물 조합」 아래 한 줄 · 나무 종류와 무관한 공용 부품. 고른 층의 나무는 부르는 쪽이 이 줄 아래에 붙임.
|
||||
* 권한 없는 층 = 단추 글 끝에 「읽기」(권한 = `accessFor` · 서버 `M02_Layer_Access` 와 짝)
|
||||
* 회사 = 볼 회사(`viewCompany` — 시스템 관리자는 해시 `company` · `aislo.adminCompany` · 자기 회사)
|
||||
* 시스템 관리자가 남의 회사를 보면 그 회사 관리자처럼(PLAN 29-6) — 「개인」 = 그 회사 사람 나무
|
||||
* (`createPeopleTree` · 사람 줄 → 그 사람 라이브러리 · 읽기) · 자기 개인 층은 자기 회사를 고를 때만
|
||||
* 볼 회사를 맡으면(`manages` · 39-6 — 시스템 관리자 · 그 회사 관리자) 「개인」 = 그 회사 사람 나무
|
||||
* (`createPeopleTree` · 나 맨 위 · 사람 줄 → 그 사람 라이브러리 · 고침) · 일반 = 내 개인 그대로
|
||||
* 누르면 `go`(부르는 쪽이 해시로 다시 엶).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
accessFor,
|
||||
buildNeedCompany,
|
||||
layerOfRoot,
|
||||
manages,
|
||||
ownOwner,
|
||||
type Root,
|
||||
} from "./M02_MasterTemplete_Layer";
|
||||
@@ -40,9 +41,9 @@ export interface LayerBarOptions {
|
||||
export const companyOwner = (company: string | null, user: SessionUser | null): string | null =>
|
||||
company && company !== ownOwner("company", user) ? company : null;
|
||||
|
||||
/** 시스템 관리자가 남의 회사를 봄 — 「개인」 단추 = 그 회사 사람 나무(읽기) */
|
||||
/** 볼 회사를 맡음(39-6) — 「개인」 단추 = 그 회사 사람 나무 */
|
||||
export const seesOthers = (company: string | null, user: SessionUser | null): boolean =>
|
||||
user?.role === "SYSTEM_ADMIN" && !!companyOwner(company, user);
|
||||
manages(company, user);
|
||||
|
||||
export function createLayerBar(opt: LayerBarOptions): HTMLElement {
|
||||
const { user, active } = opt;
|
||||
@@ -57,7 +58,7 @@ export function createLayerBar(opt: LayerBarOptions): HTMLElement {
|
||||
}
|
||||
|
||||
function button(root: Root, owner: string | null, people = false): HTMLButtonElement {
|
||||
const readOnly = people || !accessFor(layerOfRoot(root), owner, user).canWrite;
|
||||
const readOnly = !people && !accessFor(layerOfRoot(root), owner, user).canWrite;
|
||||
const name = L(
|
||||
root === "master"
|
||||
? "M02_PickerSystem"
|
||||
@@ -113,7 +114,8 @@ export function createPeopleTree(opt: PeopleTreeOptions): HTMLElement {
|
||||
box.replaceChildren(
|
||||
...rows.flatMap((one) => {
|
||||
const current = one.owner === opt.active;
|
||||
const node = { id: one.owner, label: one.name, count: null, folder: true };
|
||||
const label = one.비어있음 ? `${one.name} · ${L("M02_PeopleEmpty")}` : one.name;
|
||||
const node = { id: one.owner, label, count: null, folder: true };
|
||||
const row = createTreeRow(node, current ? "▾" : "▸", 0, () => {
|
||||
if (!current) return opt.go(one.owner);
|
||||
wrap.hidden = !wrap.hidden;
|
||||
|
||||
@@ -81,12 +81,15 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
// 자기 회사용을 읽기만 볼 때 — 안내 + [개인용으로 복사]
|
||||
const readOnlyBar = layer === "company" && !owner && !isAdmin ? buildReadOnlyBar(user) : null;
|
||||
// 고른 층의 나무 — 고르기 모달과 같은 폴더 나무 · 좌측 「구조물 조합」 줄 아래
|
||||
// 「〇〇로 복사」(30-2) — 이 사람이 쓸 수 있는 자기 층(지금 보는 층은 뺌) 맨 위로 바로
|
||||
// 「〇〇로 복사…」(30-2 · 39-6) — 쓸 수 있는 층 전부(마스터 · 볼 회사 · 내 개인 · 지금 보는 층은 뺌) ·
|
||||
// 대상 폴더는 고르는 창
|
||||
const here = owner === ownOwner(layer, user) ? null : owner;
|
||||
const copyTo = (["master", "company", "personal"] as Root[]).flatMap((one) => {
|
||||
const to = layerOfRoot(one);
|
||||
if (!accessFor(to, null, user).canWrite || (to === layer && !owner)) return [];
|
||||
const toOwner = one === "company" ? companyOwner(company, user) : null;
|
||||
if (!accessFor(to, toOwner, user).canWrite || (to === layer && toOwner === here)) return [];
|
||||
const key = one === "master" ? "Master" : one === "company" ? "Company" : "Personal";
|
||||
return [{ layer: to, owner: null, label: L(`M02_TreeCopyTo${key}`) }];
|
||||
return [{ layer: to, owner: toOwner, label: L(`M02_TreeCopyTo${key}`) }];
|
||||
});
|
||||
const tree = createTreePanel({
|
||||
layer,
|
||||
|
||||
Reference in New Issue
Block a user