This commit is contained in:
2026-05-04 17:56:00 +02:00
parent 27f7d14d4f
commit ed26305193
7 changed files with 178 additions and 133 deletions

40
main.ts
View File

@@ -1,30 +1,30 @@
import { MarkdownView, Platform, Plugin, WorkspaceLeaf } from "obsidian"; import { MarkdownView, Platform, Plugin, WorkspaceLeaf } from "obsidian";
import { import {
VIEW_TYPE_PROJECTS, VIEW_TYPE_PROJECT_VIEW,
VIEW_TYPE_OVERVIEW, VIEW_TYPE_PROJECT_DETAILS_VIEW,
VIEW_TYPE_DETAILS, VIEW_TYPE_COLLECTION_VIEW,
RIBBON_ICON, RIBBON_ICON,
} from "./src/const"; } from "./src/const";
import { ProjectsView } from "./src/views/ProjectsView"; import { ProjectView } from "./src/views/ProjectView";
import { OverviewView } from "./src/views/OverviewView"; import { ProjectDetailsView } from "./src/views/ProjectDetailsView";
import { DetailsView } from "./src/views/DetailsView"; import { CollectionView } from "./src/views/CollectionView";
import { parseProjectFilePath } from "./src/fs"; import { parseProjectFilePath } from "./src/fs";
import { BreadcrumbSegment, injectMobileBreadcrumb } from "./src/ui"; import { BreadcrumbSegment, injectMobileBreadcrumb } from "./src/ui";
export default class ProjektkontextPlugin extends Plugin { export default class ProjektkontextPlugin extends Plugin {
async onload(): Promise<void> { async onload(): Promise<void> {
this.registerView(VIEW_TYPE_PROJECTS, (leaf) => new ProjectsView(leaf)); this.registerView(VIEW_TYPE_PROJECT_VIEW, (leaf) => new ProjectView(leaf));
this.registerView(VIEW_TYPE_OVERVIEW, (leaf) => new OverviewView(leaf)); this.registerView(VIEW_TYPE_PROJECT_DETAILS_VIEW, (leaf) => new ProjectDetailsView(leaf));
this.registerView(VIEW_TYPE_DETAILS, (leaf) => new DetailsView(leaf)); this.registerView(VIEW_TYPE_COLLECTION_VIEW, (leaf) => new CollectionView(leaf));
this.addRibbonIcon(RIBBON_ICON, "Projekte", () => { this.addRibbonIcon(RIBBON_ICON, "Projekte", () => {
void this.activateProjectsView(); void this.activateProjectView();
}); });
this.addCommand({ this.addCommand({
id: "open-projects", id: "open-projects",
name: "Projekte öffnen", name: "Projekte öffnen",
callback: () => void this.activateProjectsView(), callback: () => void this.activateProjectView(),
}); });
if (Platform.isMobile) { if (Platform.isMobile) {
@@ -41,12 +41,12 @@ export default class ProjektkontextPlugin extends Plugin {
async onunload(): Promise<void> {} async onunload(): Promise<void> {}
async activateProjectsView(): Promise<void> { async activateProjectView(): Promise<void> {
const { workspace } = this.app; const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = workspace.getLeavesOfType(VIEW_TYPE_PROJECTS)[0] ?? null; let leaf: WorkspaceLeaf | null = workspace.getLeavesOfType(VIEW_TYPE_PROJECT_VIEW)[0] ?? null;
if (!leaf) { if (!leaf) {
leaf = workspace.getLeaf(false); leaf = workspace.getLeaf(false);
await leaf.setViewState({ type: VIEW_TYPE_PROJECTS, active: true }); await leaf.setViewState({ type: VIEW_TYPE_PROJECT_VIEW, active: true });
} }
workspace.revealLeaf(leaf); workspace.revealLeaf(leaf);
} }
@@ -71,26 +71,26 @@ export default class ProjektkontextPlugin extends Plugin {
const segments: BreadcrumbSegment[] = [ const segments: BreadcrumbSegment[] = [
{ {
label: "Projekte", label: "Projekte",
onClick: () => void leaf.setViewState({ type: VIEW_TYPE_PROJECTS, active: true }), onClick: () => void leaf.setViewState({ type: VIEW_TYPE_PROJECT_VIEW, active: true }),
}, },
{ {
label: loc.project, label: loc.project,
onClick: () => onClick: () =>
void leaf.setViewState({ void leaf.setViewState({
type: VIEW_TYPE_OVERVIEW, type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
active: true, active: true,
state: { project: loc.project }, state: { project: loc.project },
}), }),
}, },
]; ];
if (loc.area) { if (loc.collection) {
segments.push({ segments.push({
label: loc.area, label: loc.collection,
onClick: () => onClick: () =>
void leaf.setViewState({ void leaf.setViewState({
type: VIEW_TYPE_DETAILS, type: VIEW_TYPE_COLLECTION_VIEW,
active: true, active: true,
state: { project: loc.project, area: loc.area }, state: { project: loc.project, collection: loc.collection, zone: loc.zone },
}), }),
}); });
} }

View File

@@ -1,9 +1,9 @@
export const PROJECTS_ROOT = "projects"; export const PROJECTS_ROOT = "projects";
export const TO_UPDATE_DIR = "_to-update"; export const TO_UPDATE_DIR = "_to-update";
export const VIEW_TYPE_PROJECTS = "projektkontext-projects"; export const VIEW_TYPE_PROJECT_VIEW = "projektkontext-projects";
export const VIEW_TYPE_OVERVIEW = "projektkontext-overview"; export const VIEW_TYPE_PROJECT_DETAILS_VIEW = "projektkontext-overview";
export const VIEW_TYPE_DETAILS = "projektkontext-details"; export const VIEW_TYPE_COLLECTION_VIEW = "projektkontext-details";
export const RIBBON_ICON = "layout-grid"; export const RIBBON_ICON = "layout-grid";

View File

@@ -19,18 +19,18 @@ export function zoneRootPath(project: string, zone: Zone): string {
return zone === "ready" ? projectPath(project) : toUpdatePath(project); return zone === "ready" ? projectPath(project) : toUpdatePath(project);
} }
export function areaPath(project: string, area: string, zone: Zone = "ready"): string { export function collectionPath(project: string, collection: string, zone: Zone = "ready"): string {
return normalizePath(`${zoneRootPath(project, zone)}/${area}`); return normalizePath(`${zoneRootPath(project, zone)}/${collection}`);
} }
export function featurePath( export function featurePath(
project: string, project: string,
area: string, collection: string,
feature: string, feature: string,
zone: Zone = "ready", zone: Zone = "ready",
): string { ): string {
const file = feature.endsWith(".md") ? feature : `${feature}.md`; const file = feature.endsWith(".md") ? feature : `${feature}.md`;
return normalizePath(`${zoneRootPath(project, zone)}/${area}/${file}`); return normalizePath(`${zoneRootPath(project, zone)}/${collection}/${file}`);
} }
export async function ensureFolder(app: App, path: string): Promise<void> { export async function ensureFolder(app: App, path: string): Promise<void> {
@@ -54,6 +54,7 @@ export function listFolders(app: App, path: string): TFolder[] {
if (!(folder instanceof TFolder)) return []; if (!(folder instanceof TFolder)) return [];
return folder.children return folder.children
.filter((c): c is TFolder => c instanceof TFolder) .filter((c): c is TFolder => c instanceof TFolder)
.filter((c) => !c.name.startsWith("__"))
.sort((a, b) => a.name.localeCompare(b.name)); .sort((a, b) => a.name.localeCompare(b.name));
} }
@@ -64,6 +65,7 @@ export function listMarkdownFiles(app: App, path: string, exclude: string[] = []
return folder.children return folder.children
.filter((c): c is TFile => c instanceof TFile && c.extension === "md") .filter((c): c is TFile => c instanceof TFile && c.extension === "md")
.filter((f) => !exclude.includes(f.name)) .filter((f) => !exclude.includes(f.name))
.filter((f) => !f.basename.startsWith("__"))
.sort((a, b) => a.basename.localeCompare(b.basename)); .sort((a, b) => a.basename.localeCompare(b.basename));
} }
@@ -94,27 +96,27 @@ export async function createProject(app: App, name: string): Promise<void> {
for (const file of PROJECT_FILES) { for (const file of PROJECT_FILES) {
await ensureFile(app, normalizePath(`${root}/${file}`), ""); await ensureFile(app, normalizePath(`${root}/${file}`), "");
} }
await createArea(app, name, "story"); await createCollection(app, name, "story");
} }
export async function createArea( export async function createCollection(
app: App, app: App,
project: string, project: string,
area: string, collection: string,
zone: Zone = "to-update", zone: Zone = "to-update",
): Promise<void> { ): Promise<void> {
if (zone === "to-update") await ensureFolder(app, toUpdatePath(project)); if (zone === "to-update") await ensureFolder(app, toUpdatePath(project));
await ensureFolder(app, areaPath(project, area, zone)); await ensureFolder(app, collectionPath(project, collection, zone));
} }
export async function createFeature( export async function createFeature(
app: App, app: App,
project: string, project: string,
area: string, collection: string,
feature: string, feature: string,
zone: Zone = "ready", zone: Zone = "ready",
): Promise<TFile> { ): Promise<TFile> {
return await ensureFile(app, featurePath(project, area, feature, zone), ""); return await ensureFile(app, featurePath(project, collection, feature, zone), "");
} }
export function projectFeaturePath( export function projectFeaturePath(
@@ -136,13 +138,9 @@ export async function createProjectFeature(
return await ensureFile(app, projectFeaturePath(project, feature, zone), ""); return await ensureFile(app, projectFeaturePath(project, feature, zone), "");
} }
export function isProjectRootFile(name: string): boolean {
return (PROJECT_FILES as readonly string[]).includes(name);
}
export interface ProjectFileLocation { export interface ProjectFileLocation {
project: string; project: string;
area?: string; collection?: string;
zone: Zone; zone: Zone;
} }
@@ -159,6 +157,6 @@ export function parseProjectFilePath(path: string): ProjectFileLocation | null {
rest.shift(); rest.shift();
} }
if (rest.length === 1) return { project, zone }; if (rest.length === 1) return { project, zone };
if (rest.length === 2) return { project, area: rest[0], zone }; if (rest.length === 2) return { project, collection: rest[0], zone };
return null; return null;
} }

View File

@@ -1,13 +1,13 @@
import { ItemView, MarkdownRenderer, WorkspaceLeaf, ViewStateResult } from "obsidian"; import { ItemView, MarkdownRenderer, WorkspaceLeaf, ViewStateResult } from "obsidian";
import { import {
VIEW_TYPE_DETAILS, VIEW_TYPE_COLLECTION_VIEW,
VIEW_TYPE_OVERVIEW, VIEW_TYPE_PROJECT_DETAILS_VIEW,
VIEW_TYPE_PROJECTS, VIEW_TYPE_PROJECT_VIEW,
RIBBON_ICON, RIBBON_ICON,
} from "../const"; } from "../const";
import { import {
Zone, Zone,
areaPath, collectionPath,
featurePath, featurePath,
listMarkdownFiles, listMarkdownFiles,
readFile, readFile,
@@ -17,9 +17,9 @@ import {
import { menu, breadcrumb, emptyState, openMarkdown } from "../ui"; import { menu, breadcrumb, emptyState, openMarkdown } from "../ui";
import { NameModal } from "../modals/NameModal"; import { NameModal } from "../modals/NameModal";
export interface DetailsState extends Record<string, unknown> { export interface CollectionViewState extends Record<string, unknown> {
project: string; project: string;
area: string; collection: string;
zone?: Zone; zone?: Zone;
} }
@@ -32,9 +32,9 @@ function validateName(name: string, taken: string[]): string | null {
return null; return null;
} }
export class DetailsView extends ItemView { export class CollectionView extends ItemView {
project = ""; project = "";
area = ""; collection = "";
zone: Zone = "ready"; zone: Zone = "ready";
private renderToken = 0; private renderToken = 0;
@@ -44,27 +44,27 @@ export class DetailsView extends ItemView {
} }
getViewType(): string { getViewType(): string {
return VIEW_TYPE_DETAILS; return VIEW_TYPE_COLLECTION_VIEW;
} }
getDisplayText(): string { getDisplayText(): string {
return this.area ? `Area: ${this.area}` : "Area"; return this.collection ? `Collection: ${this.collection}` : "Collection";
} }
getIcon(): string { getIcon(): string {
return RIBBON_ICON; return RIBBON_ICON;
} }
async setState(state: DetailsState, result: ViewStateResult): Promise<void> { async setState(state: CollectionViewState, result: ViewStateResult): Promise<void> {
this.project = state?.project ?? ""; this.project = state?.project ?? "";
this.area = state?.area ?? ""; this.collection = state?.collection ?? (state as { area?: string })?.area ?? "";
this.zone = state?.zone ?? "ready"; this.zone = state?.zone ?? "ready";
await super.setState(state, result); await super.setState(state, result);
await this.render(); await this.render();
} }
getState(): DetailsState { getState(): CollectionViewState {
return { project: this.project, area: this.area, zone: this.zone }; return { project: this.project, collection: this.collection, zone: this.zone };
} }
async onOpen(): Promise<void> { async onOpen(): Promise<void> {
@@ -87,14 +87,18 @@ export class DetailsView extends ItemView {
const token = ++this.renderToken; const token = ++this.renderToken;
const root = this.containerEl.children[1] as HTMLElement; const root = this.containerEl.children[1] as HTMLElement;
if (!this.project || !this.area) { if (!this.project || !this.collection) {
root.empty(); root.empty();
root.addClass("pk-root"); root.addClass("pk-root");
emptyState(root, "Keine Area ausgewählt."); emptyState(root, "Keine Collection ausgewählt.");
return; return;
} }
const features = listMarkdownFiles(this.app, areaPath(this.project, this.area, this.zone), []); const features = listMarkdownFiles(
this.app,
collectionPath(this.project, this.collection, this.zone),
[],
);
const contents = await Promise.all(features.map((f) => readFile(this.app, f.path))); const contents = await Promise.all(features.map((f) => readFile(this.app, f.path)));
if (token !== this.renderToken) return; if (token !== this.renderToken) return;
@@ -102,13 +106,13 @@ export class DetailsView extends ItemView {
root.addClass("pk-root"); root.addClass("pk-root");
breadcrumb(root, [ breadcrumb(root, [
{ label: "Projekte", onClick: () => this.openProjectsView() }, { label: "Projekte", onClick: () => this.openProjectView() },
{ label: this.project, onClick: () => this.openOverview() }, { label: this.project, onClick: () => this.openProjectDetails() },
{ label: this.area }, { label: this.collection },
]); ]);
if (features.length === 0) { if (features.length === 0) {
emptyState(root, "Noch keine Features. Rechtsklick → Neues Feature."); emptyState(root, "Keine Features");
return; return;
} }
@@ -130,6 +134,7 @@ export class DetailsView extends ItemView {
btn.addEventListener("contextmenu", (ev) => { btn.addEventListener("contextmenu", (ev) => {
ev.preventDefault(); ev.preventDefault();
menu(ev, [ menu(ev, [
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreate() },
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDelete(f.basename) }, { title: "Feature löschen", icon: "trash", onClick: () => this.openDelete(f.basename) },
]); ]);
}); });
@@ -139,7 +144,7 @@ export class DetailsView extends ItemView {
private openCreate(): void { private openCreate(): void {
const taken = listMarkdownFiles( const taken = listMarkdownFiles(
this.app, this.app,
areaPath(this.project, this.area, this.zone), collectionPath(this.project, this.collection, this.zone),
[], [],
).map((f) => f.basename); ).map((f) => f.basename);
new NameModal(this.app, { new NameModal(this.app, {
@@ -148,26 +153,29 @@ export class DetailsView extends ItemView {
cta: "Erstellen", cta: "Erstellen",
validate: (n) => validateName(n, taken), validate: (n) => validateName(n, taken),
onSubmit: async (name) => { onSubmit: async (name) => {
await createFeature(this.app, this.project, this.area, name, this.zone); await createFeature(this.app, this.project, this.collection, name, this.zone);
await this.render(); await this.render();
}, },
}).open(); }).open();
} }
private async openDelete(feature: string): Promise<void> { private async openDelete(feature: string): Promise<void> {
await deleteRecursive(this.app, featurePath(this.project, this.area, feature, this.zone)); await deleteRecursive(
this.app,
featurePath(this.project, this.collection, feature, this.zone),
);
await this.render(); await this.render();
} }
private async openOverview(): Promise<void> { private async openProjectDetails(): Promise<void> {
await this.leaf.setViewState({ await this.leaf.setViewState({
type: VIEW_TYPE_OVERVIEW, type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
active: true, active: true,
state: { project: this.project }, state: { project: this.project },
}); });
} }
private async openProjectsView(): Promise<void> { private async openProjectView(): Promise<void> {
await this.leaf.setViewState({ type: VIEW_TYPE_PROJECTS, active: true }); await this.leaf.setViewState({ type: VIEW_TYPE_PROJECT_VIEW, active: true });
} }
} }

View File

@@ -1,8 +1,17 @@
import { ItemView, MarkdownRenderer, Notice, TFile, TFolder, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian";
import { import {
VIEW_TYPE_OVERVIEW, ItemView,
VIEW_TYPE_DETAILS, MarkdownRenderer,
VIEW_TYPE_PROJECTS, Notice,
TFile,
TFolder,
WorkspaceLeaf,
ViewStateResult,
normalizePath,
} from "obsidian";
import {
VIEW_TYPE_PROJECT_DETAILS_VIEW,
VIEW_TYPE_COLLECTION_VIEW,
VIEW_TYPE_PROJECT_VIEW,
RIBBON_ICON, RIBBON_ICON,
PROJECT_FILES, PROJECT_FILES,
CORE_FILE, CORE_FILE,
@@ -20,14 +29,13 @@ import {
rename, rename,
deleteRecursive, deleteRecursive,
ensureFolder, ensureFolder,
createArea, createCollection,
createFeature,
createProjectFeature, createProjectFeature,
} from "../fs"; } from "../fs";
import { menu, breadcrumb, emptyState, openMarkdown } from "../ui"; import { menu, breadcrumb, emptyState, openMarkdown } from "../ui";
import { NameModal } from "../modals/NameModal"; import { NameModal } from "../modals/NameModal";
export interface OverviewState extends Record<string, unknown> { export interface ProjectDetailsState extends Record<string, unknown> {
project: string; project: string;
} }
@@ -47,7 +55,7 @@ function validateName(name: string, taken: string[], current?: string): string |
return null; return null;
} }
export class OverviewView extends ItemView { export class ProjectDetailsView extends ItemView {
project = ""; project = "";
private renderToken = 0; private renderToken = 0;
@@ -57,7 +65,7 @@ export class OverviewView extends ItemView {
} }
getViewType(): string { getViewType(): string {
return VIEW_TYPE_OVERVIEW; return VIEW_TYPE_PROJECT_DETAILS_VIEW;
} }
getDisplayText(): string { getDisplayText(): string {
@@ -68,13 +76,13 @@ export class OverviewView extends ItemView {
return RIBBON_ICON; return RIBBON_ICON;
} }
async setState(state: OverviewState, result: ViewStateResult): Promise<void> { async setState(state: ProjectDetailsState, result: ViewStateResult): Promise<void> {
this.project = state?.project ?? ""; this.project = state?.project ?? "";
await super.setState(state, result); await super.setState(state, result);
await this.render(); await this.render();
} }
getState(): OverviewState { getState(): ProjectDetailsState {
return { project: this.project }; return { project: this.project };
} }
@@ -114,14 +122,14 @@ export class OverviewView extends ItemView {
root.addClass("pk-root"); root.addClass("pk-root");
breadcrumb(root, [ breadcrumb(root, [
{ label: "Projekte", onClick: () => this.openProjectsView() }, { label: "Projekte", onClick: () => this.openProjectView() },
{ label: this.project }, { label: this.project },
]); ]);
const info = root.createDiv({ cls: "pk-info-grid" }); const info = root.createDiv({ cls: "pk-info-grid" });
this.renderInfoCard(info, core, corePath); this.renderInfoCard(info, core, corePath);
this.renderInfoCard(info, target, targetPath); this.renderInfoCard(info, target, targetPath);
this.renderAreas(root); this.renderCollections(root);
} }
private renderInfoCard(parent: HTMLElement, content: string, path: string): void { private renderInfoCard(parent: HTMLElement, content: string, path: string): void {
@@ -137,13 +145,13 @@ export class OverviewView extends ItemView {
} }
} }
private renderAreas(parent: HTMLElement): void { private renderCollections(parent: HTMLElement): void {
const section = parent.createDiv({ cls: "pk-areas-section" }); const section = parent.createDiv({ cls: "pk-areas-section" });
section.addEventListener("contextmenu", (ev) => { section.addEventListener("contextmenu", (ev) => {
if (ev.defaultPrevented) return; if (ev.defaultPrevented) return;
ev.preventDefault(); ev.preventDefault();
menu(ev, [ menu(ev, [
{ title: "Neue Collection", icon: "plus", onClick: () => this.openCreateArea() }, { title: "Neue Collection", icon: "plus", onClick: () => this.openCreateCollection() },
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature() }, { title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature() },
]); ]);
}); });
@@ -156,10 +164,10 @@ export class OverviewView extends ItemView {
this.renderZone(section, "to-update", toUpdate); this.renderZone(section, "to-update", toUpdate);
} }
private collectZoneItems(zone: Zone): { areas: TFolder[]; features: TFile[] } { private collectZoneItems(zone: Zone): { collections: TFolder[]; features: TFile[] } {
const root = zoneRootPath(this.project, zone); const root = zoneRootPath(this.project, zone);
const folders = listFolders(this.app, root); const folders = listFolders(this.app, root);
const areas = zone === "ready" const collections = zone === "ready"
? folders.filter((f) => f.name !== TO_UPDATE_DIR) ? folders.filter((f) => f.name !== TO_UPDATE_DIR)
: folders; : folders;
const features = listMarkdownFiles( const features = listMarkdownFiles(
@@ -167,13 +175,13 @@ export class OverviewView extends ItemView {
root, root,
zone === "ready" ? [...PROJECT_FILES] : [], zone === "ready" ? [...PROJECT_FILES] : [],
); );
return { areas, features }; return { collections, features };
} }
private renderZone( private renderZone(
parent: HTMLElement, parent: HTMLElement,
zone: Zone, zone: Zone,
items: { areas: TFolder[]; features: TFile[] }, items: { collections: TFolder[]; features: TFile[] },
): void { ): void {
const flex = parent.createDiv({ const flex = parent.createDiv({
cls: `pk-areas-flex pk-zone-${zone}`, cls: `pk-areas-flex pk-zone-${zone}`,
@@ -198,42 +206,39 @@ export class OverviewView extends ItemView {
await this.handleDropOnZone(raw, zone); await this.handleDropOnZone(raw, zone);
}); });
const takenAreas = items.areas.map((a) => a.name); const takenCollections = items.collections.map((a) => a.name);
for (const area of items.areas) { for (const collection of items.collections) {
this.renderAreaCard(flex, zone, area, takenAreas); this.renderCollectionCard(flex, zone, collection, takenCollections);
} }
for (const f of items.features) { for (const f of items.features) {
this.renderProjectFeatureCard(flex, f); this.renderProjectFeatureCard(flex, zone, f);
} }
if (items.areas.length === 0 && items.features.length === 0) { if (items.collections.length === 0 && items.features.length === 0) {
flex.createDiv({ flex.createDiv({ cls: "pk-zone-placeholder", text: "Keine Features" });
cls: "pk-zone-placeholder",
text: zone === "to-update" ? "Nothing to update" : "Empty",
});
} }
} }
private renderAreaCard( private renderCollectionCard(
parent: HTMLElement, parent: HTMLElement,
zone: Zone, zone: Zone,
area: TFolder, collection: TFolder,
takenAreas: string[], takenCollections: string[],
): void { ): void {
const folderPath = area.path; const folderPath = collection.path;
const features = listMarkdownFiles(this.app, folderPath, []); const features = listMarkdownFiles(this.app, folderPath, []);
const card = parent.createDiv({ const card = parent.createDiv({
cls: "pk-btn-card pk-area-card", cls: "pk-btn-card pk-area-card",
attr: { role: "button", tabindex: "0", draggable: "true" }, attr: { role: "button", tabindex: "0", draggable: "true" },
}); });
card.createEl("strong", { text: area.name }); card.createEl("strong", { text: collection.name });
card.addEventListener("click", () => this.openDetails(area.name, zone)); card.addEventListener("click", () => this.openCollectionDetails(collection.name, zone));
card.addEventListener("contextmenu", (ev) => { card.addEventListener("contextmenu", (ev) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
menu(ev, [ menu(ev, [
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeatureIn(folderPath) }, { title: "Neue Collection", icon: "plus", onClick: () => this.openCreateCollection() },
{ title: "Collection umbenennen", icon: "pencil", onClick: () => this.openRenameAreaAt(folderPath, area.name, takenAreas) }, { title: "Collection umbenennen", icon: "pencil", onClick: () => this.openRenameCollectionAt(folderPath, collection.name, takenCollections) },
{ title: "Collection löschen", icon: "trash", onClick: () => this.openDeletePath(folderPath) }, { title: "Collection löschen", icon: "trash", onClick: () => this.openDeletePath(folderPath) },
]); ]);
}); });
@@ -242,7 +247,7 @@ export class OverviewView extends ItemView {
ev.dataTransfer.setData(PK_DND_MIME, JSON.stringify({ ev.dataTransfer.setData(PK_DND_MIME, JSON.stringify({
kind: "collection", kind: "collection",
sourcePath: folderPath, sourcePath: folderPath,
name: area.name, name: collection.name,
} satisfies DndPayload)); } satisfies DndPayload));
ev.dataTransfer.effectAllowed = "move"; ev.dataTransfer.effectAllowed = "move";
card.addClass("pk-feature-chip-dragging"); card.addClass("pk-feature-chip-dragging");
@@ -265,17 +270,18 @@ export class OverviewView extends ItemView {
if (!raw) return; if (!raw) return;
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
await this.handleDropOnArea(raw, folderPath); await this.handleDropOnCollection(raw, folderPath);
}); });
if (features.length === 0) { if (features.length === 0) {
card.createDiv({ cls: "pk-empty", text: "Keine Features." }); card.createDiv({ cls: "pk-empty", text: "Keine Features" });
} else { } else {
const list = card.createDiv({ cls: "pk-features" }); const list = card.createDiv({ cls: "pk-features" });
for (const f of features) { for (const f of features) {
const chip = list.createDiv({ cls: "pk-feature-chip", text: f.basename }); const chip = list.createDiv({ cls: "pk-feature-chip", text: f.basename });
chip.draggable = true; chip.draggable = true;
chip.addEventListener("dragstart", (ev) => { chip.addEventListener("dragstart", (ev) => {
ev.stopPropagation();
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;
ev.dataTransfer.setData(PK_DND_MIME, JSON.stringify({ ev.dataTransfer.setData(PK_DND_MIME, JSON.stringify({
kind: "feature", kind: "feature",
@@ -294,6 +300,7 @@ export class OverviewView extends ItemView {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
menu(ev, [ menu(ev, [
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeatureIn(folderPath) },
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeletePath(f.path) }, { title: "Feature löschen", icon: "trash", onClick: () => this.openDeletePath(f.path) },
]); ]);
}); });
@@ -301,7 +308,7 @@ export class OverviewView extends ItemView {
} }
} }
private renderProjectFeatureCard(parent: HTMLElement, file: TFile): void { private renderProjectFeatureCard(parent: HTMLElement, zone: Zone, file: TFile): void {
const card = parent.createDiv({ const card = parent.createDiv({
cls: "pk-btn-card pk-area-card", cls: "pk-btn-card pk-area-card",
attr: { role: "button", tabindex: "0", draggable: "true" }, attr: { role: "button", tabindex: "0", draggable: "true" },
@@ -323,6 +330,7 @@ export class OverviewView extends ItemView {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
menu(ev, [ menu(ev, [
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeatureInZone(zone) },
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeletePath(file.path) }, { title: "Feature löschen", icon: "trash", onClick: () => this.openDeletePath(file.path) },
]); ]);
}); });
@@ -352,12 +360,12 @@ export class OverviewView extends ItemView {
await this.movePath(data.sourcePath, newPath); await this.movePath(data.sourcePath, newPath);
} }
private async handleDropOnArea(raw: string, areaFolderPath: string): Promise<void> { private async handleDropOnCollection(raw: string, collectionFolderPath: string): Promise<void> {
const data = this.parseDnd(raw); const data = this.parseDnd(raw);
if (!data) return; if (!data) return;
if (data.kind !== "feature") return; if (data.kind !== "feature") return;
const file = data.name.endsWith(".md") ? data.name : `${data.name}.md`; const file = data.name.endsWith(".md") ? data.name : `${data.name}.md`;
const newPath = normalizePath(`${areaFolderPath}/${file}`); const newPath = normalizePath(`${collectionFolderPath}/${file}`);
if (data.sourcePath === newPath) return; if (data.sourcePath === newPath) return;
await this.movePath(data.sourcePath, newPath); await this.movePath(data.sourcePath, newPath);
} }
@@ -377,8 +385,16 @@ export class OverviewView extends ItemView {
} }
private openCreateProjectFeature(): void { private openCreateProjectFeature(): void {
const root = toUpdatePath(this.project); this.openCreateProjectFeatureInZone("to-update");
const existing = listMarkdownFiles(this.app, root, []); }
private openCreateProjectFeatureInZone(zone: Zone): void {
const root = zoneRootPath(this.project, zone);
const existing = listMarkdownFiles(
this.app,
root,
zone === "ready" ? [...PROJECT_FILES] : [],
);
const taken = existing.map((f) => f.basename); const taken = existing.map((f) => f.basename);
new NameModal(this.app, { new NameModal(this.app, {
title: "Neues Feature", title: "Neues Feature",
@@ -386,13 +402,13 @@ export class OverviewView extends ItemView {
cta: "Erstellen", cta: "Erstellen",
validate: (n) => validateName(n, taken), validate: (n) => validateName(n, taken),
onSubmit: async (name) => { onSubmit: async (name) => {
await createProjectFeature(this.app, this.project, name); await createProjectFeature(this.app, this.project, name, zone);
await this.render(); await this.render();
}, },
}).open(); }).open();
} }
private openCreateArea(): void { private openCreateCollection(): void {
const ready = listFolders(this.app, projectPath(this.project)) const ready = listFolders(this.app, projectPath(this.project))
.filter((f) => f.name !== TO_UPDATE_DIR).map((a) => a.name); .filter((f) => f.name !== TO_UPDATE_DIR).map((a) => a.name);
const inToUpdate = listFolders(this.app, toUpdatePath(this.project)).map((a) => a.name); const inToUpdate = listFolders(this.app, toUpdatePath(this.project)).map((a) => a.name);
@@ -403,7 +419,7 @@ export class OverviewView extends ItemView {
cta: "Erstellen", cta: "Erstellen",
validate: (n) => validateName(n, taken), validate: (n) => validateName(n, taken),
onSubmit: async (name) => { onSubmit: async (name) => {
await createArea(this.app, this.project, name); await createCollection(this.app, this.project, name);
await this.render(); await this.render();
}, },
}).open(); }).open();
@@ -426,7 +442,7 @@ export class OverviewView extends ItemView {
}).open(); }).open();
} }
private openRenameAreaAt(folderPath: string, current: string, taken: string[]): void { private openRenameCollectionAt(folderPath: string, current: string, taken: string[]): void {
new NameModal(this.app, { new NameModal(this.app, {
title: "Collection umbenennen", title: "Collection umbenennen",
label: "Collection-Name", label: "Collection-Name",
@@ -447,15 +463,15 @@ export class OverviewView extends ItemView {
await this.render(); await this.render();
} }
private async openDetails(area: string, zone: Zone): Promise<void> { private async openCollectionDetails(collection: string, zone: Zone): Promise<void> {
await this.leaf.setViewState({ await this.leaf.setViewState({
type: VIEW_TYPE_DETAILS, type: VIEW_TYPE_COLLECTION_VIEW,
active: true, active: true,
state: { project: this.project, area, zone }, state: { project: this.project, collection, zone },
}); });
} }
private async openProjectsView(): Promise<void> { private async openProjectView(): Promise<void> {
await this.leaf.setViewState({ type: VIEW_TYPE_PROJECTS, active: true }); await this.leaf.setViewState({ type: VIEW_TYPE_PROJECT_VIEW, active: true });
} }
} }

View File

@@ -1,5 +1,16 @@
import { ItemView, Notice, WorkspaceLeaf, normalizePath } from "obsidian"; import {
import { VIEW_TYPE_PROJECTS, VIEW_TYPE_OVERVIEW, RIBBON_ICON, CORE_FILE } from "../const"; ItemView,
MarkdownRenderer,
Notice,
WorkspaceLeaf,
normalizePath,
} from "obsidian";
import {
VIEW_TYPE_PROJECT_VIEW,
VIEW_TYPE_PROJECT_DETAILS_VIEW,
RIBBON_ICON,
CORE_FILE,
} from "../const";
import { import {
projectsPath, projectsPath,
projectPath, projectPath,
@@ -22,7 +33,7 @@ function validateName(name: string, taken: string[], current?: string): string |
return null; return null;
} }
export class ProjectsView extends ItemView { export class ProjectView extends ItemView {
private renderToken = 0; private renderToken = 0;
constructor(leaf: WorkspaceLeaf) { constructor(leaf: WorkspaceLeaf) {
@@ -31,7 +42,7 @@ export class ProjectsView extends ItemView {
} }
getViewType(): string { getViewType(): string {
return VIEW_TYPE_PROJECTS; return VIEW_TYPE_PROJECT_VIEW;
} }
getDisplayText(): string { getDisplayText(): string {
@@ -79,11 +90,8 @@ export class ProjectsView extends ItemView {
} }
const taken = projects.map((p) => p.name); const taken = projects.map((p) => p.name);
const cores = await Promise.all( const corePaths = projects.map((p) => normalizePath(`${projectPath(p.name)}/${CORE_FILE}`));
projects.map((p) => const cores = await Promise.all(corePaths.map((p) => readFile(this.app, p)));
readFile(this.app, normalizePath(`${projectPath(p.name)}/${CORE_FILE}`)),
),
);
if (token !== this.renderToken) return; if (token !== this.renderToken) return;
root.empty(); root.empty();
@@ -94,13 +102,17 @@ export class ProjectsView extends ItemView {
for (let i = 0; i < projects.length; i++) { for (let i = 0; i < projects.length; i++) {
const proj = projects[i]; const proj = projects[i];
const core = cores[i].trim(); const core = cores[i].trim();
const corePath = corePaths[i];
const btn = grid.createDiv({ const btn = grid.createDiv({
cls: "pk-btn-card", cls: "pk-btn-card",
attr: { role: "button", tabindex: "0" }, attr: { role: "button", tabindex: "0" },
}); });
btn.createEl("strong", { text: proj.name }); btn.createEl("strong", { text: proj.name });
if (core) btn.createDiv({ text: core }); if (core) {
btn.addEventListener("click", () => this.openOverview(proj.name)); const body = btn.createDiv({ cls: "pk-project-core" });
void MarkdownRenderer.render(this.app, core, body, corePath, this);
}
btn.addEventListener("click", () => this.openProjectDetails(proj.name));
btn.addEventListener("contextmenu", (ev) => { btn.addEventListener("contextmenu", (ev) => {
ev.preventDefault(); ev.preventDefault();
menu(ev, [ menu(ev, [
@@ -158,9 +170,9 @@ export class ProjectsView extends ItemView {
} }
} }
private async openOverview(name: string): Promise<void> { private async openProjectDetails(name: string): Promise<void> {
await this.leaf.setViewState({ await this.leaf.setViewState({
type: VIEW_TYPE_OVERVIEW, type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
active: true, active: true,
state: { project: name }, state: { project: name },
}); });

View File

@@ -192,6 +192,17 @@
font-weight: 600; font-weight: 600;
} }
.pk-project-core {
font-size: 0.85em;
color: var(--text-muted);
}
.pk-project-core > p:first-child { margin-top: 0; }
.pk-project-core > p:last-child { margin-bottom: 0; }
.pk-project-core > ul:first-child,
.pk-project-core > ol:first-child { margin-top: 0; }
.pk-project-core > ul:last-child,
.pk-project-core > ol:last-child { margin-bottom: 0; }
.pk-info-card > p:first-child { margin-top: 0; } .pk-info-card > p:first-child { margin-top: 0; }
.pk-info-card > p:last-child { margin-bottom: 0; } .pk-info-card > p:last-child { margin-bottom: 0; }
.pk-info-card > ul:first-child, .pk-info-card > ul:first-child,