update
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ItemView, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian";
|
||||
import { ItemView, Notice, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian";
|
||||
import {
|
||||
VIEW_TYPE_OVERVIEW,
|
||||
VIEW_TYPE_DETAILS,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
createArea,
|
||||
createFeature,
|
||||
} from "../fs";
|
||||
import { card, menu, breadcrumb, emptyState, openMarkdown } from "../ui";
|
||||
import { menu, breadcrumb, emptyState, openMarkdown } from "../ui";
|
||||
import { NameModal } from "../modals/NameModal";
|
||||
import { ConfirmModal } from "../modals/ConfirmModal";
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface OverviewState extends Record<string, unknown> {
|
||||
}
|
||||
|
||||
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
||||
const FEATURE_DND_MIME = "application/x-pk-feature";
|
||||
|
||||
function validateName(name: string, taken: string[], current?: string): string | null {
|
||||
if (!name) return "Name darf nicht leer sein.";
|
||||
@@ -108,22 +109,24 @@ export class OverviewView extends ItemView {
|
||||
]);
|
||||
|
||||
const stack = root.createDiv({ cls: "pk-stack" });
|
||||
this.renderInfoCard(stack, "Core", core, corePath);
|
||||
this.renderInfoCard(stack, "Target", target, targetPath);
|
||||
this.renderInfoCard(stack, core, corePath);
|
||||
this.renderInfoCard(stack, target, targetPath);
|
||||
this.renderStoryCard(stack, story, storyPath);
|
||||
this.renderAreas(root);
|
||||
}
|
||||
|
||||
private renderInfoCard(parent: HTMLElement, title: string, content: string, path: string): void {
|
||||
card(parent, {
|
||||
title,
|
||||
body: content || "(leer)",
|
||||
onClick: () =>
|
||||
private renderInfoCard(parent: HTMLElement, content: string, path: string): void {
|
||||
const btn = parent.createDiv({
|
||||
cls: "pk-btn-card",
|
||||
attr: { role: "button", tabindex: "0" },
|
||||
});
|
||||
btn.setText(content || "(leer)");
|
||||
btn.addEventListener("click", () =>
|
||||
openMarkdown(this.app, path, {
|
||||
type: VIEW_TYPE_OVERVIEW,
|
||||
state: { project: this.project },
|
||||
}),
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
private renderStoryCard(parent: HTMLElement, content: string, path: string): void {
|
||||
@@ -170,24 +173,59 @@ export class OverviewView extends ItemView {
|
||||
areaPath(this.project, area.name),
|
||||
[],
|
||||
);
|
||||
card(stack, {
|
||||
title: area.name,
|
||||
cls: "pk-area-card",
|
||||
onClick: () => this.openDetails(area.name),
|
||||
onContextMenu: (ev) =>
|
||||
const areaCard = stack.createDiv({
|
||||
cls: "pk-btn-card pk-area-card",
|
||||
attr: { role: "button", tabindex: "0" },
|
||||
});
|
||||
areaCard.createEl("strong", { text: area.name });
|
||||
areaCard.addEventListener("click", () => this.openDetails(area.name));
|
||||
areaCard.addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
menu(ev, [
|
||||
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeature(area.name) },
|
||||
{ title: "Area umbenennen", icon: "pencil", onClick: () => this.openRenameArea(area.name, takenAreas) },
|
||||
{ title: "Area löschen", icon: "trash", onClick: () => this.openDeleteArea(area.name) },
|
||||
]),
|
||||
body: (body) => {
|
||||
]);
|
||||
});
|
||||
areaCard.addEventListener("dragover", (ev) => {
|
||||
if (!ev.dataTransfer?.types.includes(FEATURE_DND_MIME)) return;
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = "move";
|
||||
areaCard.addClass("pk-drop-target");
|
||||
});
|
||||
areaCard.addEventListener("dragleave", (ev) => {
|
||||
const next = ev.relatedTarget as Node | null;
|
||||
if (next && areaCard.contains(next)) return;
|
||||
areaCard.removeClass("pk-drop-target");
|
||||
});
|
||||
areaCard.addEventListener("drop", async (ev) => {
|
||||
areaCard.removeClass("pk-drop-target");
|
||||
const raw = ev.dataTransfer?.getData(FEATURE_DND_MIME);
|
||||
if (!raw) return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
await this.handleFeatureDrop(raw, area.name);
|
||||
});
|
||||
if (features.length === 0) {
|
||||
body.createDiv({ cls: "pk-empty", text: "Keine Features." });
|
||||
return;
|
||||
}
|
||||
const list = body.createDiv({ cls: "pk-features" });
|
||||
areaCard.createDiv({ cls: "pk-empty", text: "Keine Features." });
|
||||
} else {
|
||||
const list = areaCard.createDiv({ cls: "pk-features" });
|
||||
for (const f of features) {
|
||||
const chip = list.createDiv({ cls: "pk-feature-chip", text: f.basename });
|
||||
chip.draggable = true;
|
||||
chip.addEventListener("dragstart", (ev) => {
|
||||
if (!ev.dataTransfer) return;
|
||||
ev.dataTransfer.setData(
|
||||
FEATURE_DND_MIME,
|
||||
JSON.stringify({ sourceArea: area.name, feature: f.basename }),
|
||||
);
|
||||
ev.dataTransfer.effectAllowed = "move";
|
||||
chip.addClass("pk-feature-chip-dragging");
|
||||
});
|
||||
chip.addEventListener("dragend", () => {
|
||||
chip.removeClass("pk-feature-chip-dragging");
|
||||
});
|
||||
chip.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
openMarkdown(this.app, f.path, {
|
||||
@@ -203,10 +241,33 @@ export class OverviewView extends ItemView {
|
||||
]);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFeatureDrop(raw: string, targetArea: string): Promise<void> {
|
||||
let data: { sourceArea: string; feature: string };
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!data?.sourceArea || !data?.feature) return;
|
||||
if (data.sourceArea === targetArea) return;
|
||||
const newPath = featurePath(this.project, targetArea, data.feature);
|
||||
if (this.app.vault.getAbstractFileByPath(newPath)) {
|
||||
new Notice(`„${data.feature}" existiert in „${targetArea}" bereits.`);
|
||||
return;
|
||||
}
|
||||
const oldPath = featurePath(this.project, data.sourceArea, data.feature);
|
||||
try {
|
||||
await rename(this.app, oldPath, newPath);
|
||||
} catch (e) {
|
||||
new Notice(`Verschieben fehlgeschlagen: ${(e as Error).message}`);
|
||||
return;
|
||||
}
|
||||
await this.render();
|
||||
}
|
||||
|
||||
private openCreateArea(): void {
|
||||
const taken = listFolders(this.app, projectPath(this.project)).map((a) => a.name);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ItemView, Notice, WorkspaceLeaf } from "obsidian";
|
||||
import { ItemView, Notice, WorkspaceLeaf, normalizePath } from "obsidian";
|
||||
import { VIEW_TYPE_PROJECTS, VIEW_TYPE_OVERVIEW, RIBBON_ICON } from "../const";
|
||||
import {
|
||||
projectsPath,
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
createProject,
|
||||
rename,
|
||||
deleteRecursive,
|
||||
readFile,
|
||||
} from "../fs";
|
||||
import { card, menu, breadcrumb, emptyState } from "../ui";
|
||||
import { menu, breadcrumb, emptyState } from "../ui";
|
||||
import { NameModal } from "../modals/NameModal";
|
||||
import { ConfirmModal } from "../modals/ConfirmModal";
|
||||
|
||||
@@ -45,6 +46,11 @@ export class ProjectsView extends ItemView {
|
||||
this.registerEvent(this.app.vault.on("create", () => this.render()));
|
||||
this.registerEvent(this.app.vault.on("delete", () => this.render()));
|
||||
this.registerEvent(this.app.vault.on("rename", () => this.render()));
|
||||
this.registerEvent(this.app.vault.on("modify", (f) => {
|
||||
if (f.path.endsWith("/core.md") && f.path.startsWith(projectsPath() + "/")) {
|
||||
this.render();
|
||||
}
|
||||
}));
|
||||
this.registerDomEvent(this.containerEl, "contextmenu", (ev) => {
|
||||
if (ev.defaultPrevented) return;
|
||||
ev.preventDefault();
|
||||
@@ -70,16 +76,29 @@ export class ProjectsView extends ItemView {
|
||||
}
|
||||
|
||||
const taken = projects.map((p) => p.name);
|
||||
const cores = await Promise.all(
|
||||
projects.map((p) =>
|
||||
readFile(this.app, normalizePath(`${projectPath(p.name)}/core.md`)),
|
||||
),
|
||||
);
|
||||
|
||||
const grid = root.createDiv({ cls: "pk-grid" });
|
||||
for (const proj of projects) {
|
||||
card(grid, {
|
||||
title: proj.name,
|
||||
onClick: () => this.openOverview(proj.name),
|
||||
onContextMenu: (ev) =>
|
||||
for (let i = 0; i < projects.length; i++) {
|
||||
const proj = projects[i];
|
||||
const core = cores[i].trim();
|
||||
const btn = grid.createDiv({
|
||||
cls: "pk-btn-card",
|
||||
attr: { role: "button", tabindex: "0" },
|
||||
});
|
||||
btn.createEl("strong", { text: proj.name });
|
||||
if (core) btn.createDiv({ text: core });
|
||||
btn.addEventListener("click", () => this.openOverview(proj.name));
|
||||
btn.addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault();
|
||||
menu(ev, [
|
||||
{ title: "Umbenennen", icon: "pencil", onClick: () => this.openRename(proj.name, taken) },
|
||||
{ title: "Löschen", icon: "trash", onClick: () => this.openDelete(proj.name) },
|
||||
]),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
31
styles.css
31
styles.css
@@ -136,6 +136,37 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pk-btn-card {
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
overflow-wrap: anywhere;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pk-btn-card:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.pk-btn-card strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.pk-drop-target {
|
||||
outline: 2px dashed var(--text-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.pk-feature-chip-dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.pk-areas-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user