diff --git a/doc/_core.md b/doc/_core.md new file mode 100644 index 0000000..68c71ec --- /dev/null +++ b/doc/_core.md @@ -0,0 +1 @@ +Obsidian Plugin - Projektkontext managen \ No newline at end of file diff --git a/doc/_project.md b/doc/_project.md new file mode 100644 index 0000000..82e9666 --- /dev/null +++ b/doc/_project.md @@ -0,0 +1,24 @@ +**Core** +- Obsidian Plugin - Projektkontext managen +**Target** +- Für große Projekte geht der Kontext schnell verloren. +- Sie werden unübersichtlich und schwer zu managen. +- Das Plugin hilft Projekte kontextbezogen zu strukturieren. +- So lassen sich große Projekte wieder managen. +**Story** +- Großes Projekt soll geplannt werden +- Core definieren (Worum geht es?) +- Target beschreiben (Welches Problem löst es?) +- Story schreiben (Wie wird es verwendet?) +- Areas und Features definieren (Welche Funktionen hat es?) +- Features implementieren +- Projekt v1 +**Areas** +- **Core**: Kompakter Titel zum Projekt +- **Target**: Problem -> Folge -> Lösung -> Ergebnis zum Projekt +- **Story**: Kernverwendung vom Projekt +- **Area**: Sammlung von Features +- **Feature**: Abgeschlossene Funktion im Bereich +- **Projects**: Übersicht der Projekte +- **Overview**: Übersicht zu einem Projekt +- **Detail**: Übersicht zu einem Area diff --git a/doc/_story.md b/doc/_story.md new file mode 100644 index 0000000..71bb8ba --- /dev/null +++ b/doc/_story.md @@ -0,0 +1,7 @@ +- Großes Projekt soll geplannt werden +- Core definieren (Worum geht es?) +- Target beschreiben (Welches Problem löst es?) +- Story schreiben (Wie wird es verwendet?) +- Areas und Features definieren (Welche Funktionen hat es?) +- Features implementieren +- Projekt v1 \ No newline at end of file diff --git a/doc/_target.md b/doc/_target.md new file mode 100644 index 0000000..a8c26d2 --- /dev/null +++ b/doc/_target.md @@ -0,0 +1,2 @@ +Für große Projekte geht der Kontext schnell verloren, sie werden unübersichtlich und schwer zu managen. +Das Plugin hilft Projekte kontextbezogen zu strukturieren, so lassen sich große Projekte wieder managen. \ No newline at end of file diff --git a/doc/allgemein/allgemein.md b/doc/allgemein.md similarity index 100% rename from doc/allgemein/allgemein.md rename to doc/allgemein.md diff --git a/doc/allgemein/core.md b/doc/allgemein/core.md deleted file mode 100644 index c6732fd..0000000 --- a/doc/allgemein/core.md +++ /dev/null @@ -1,3 +0,0 @@ -- Max 64 Zeichen -- wird als ./core.md gespeichert -- beschreibt kompakt was das projekt ist diff --git a/doc/allgemein/project.md b/doc/allgemein/project.md deleted file mode 100644 index db40996..0000000 --- a/doc/allgemein/project.md +++ /dev/null @@ -1,9 +0,0 @@ -- kann ich projekt oder subprojekt zu einem großen projekt sein -- ist abgeschlossen, also hat keine abhängigkeit -- ist der root ordner -- ordnername ist der projektname - -kerndatei -- wird als project.md gespeichert -- enthält kern, zweck, stories und kategorien als md dateien -- enthält bereiche als ordner diff --git a/doc/allgemein/story.md b/doc/allgemein/story.md deleted file mode 100644 index abea003..0000000 --- a/doc/allgemein/story.md +++ /dev/null @@ -1,5 +0,0 @@ -- beschreibt wie das fertige projekt in der regel verwendet werden soll -- Max 1024 Zeichen -- wird als ./story.md gespeichert -- nur die hauptanwendung -- abfolge von schritten (start ... ende) \ No newline at end of file diff --git a/doc/allgemein/target.md b/doc/allgemein/target.md deleted file mode 100644 index bd0557f..0000000 --- a/doc/allgemein/target.md +++ /dev/null @@ -1,6 +0,0 @@ -- beschreibt simpel welches problem das projekt löst - - Was ist das Problem und die Folge davon? - - Wie löst das Projekt das Problem und was ist die Folge davon? -- der zweck muss klar erkennbar sein -- max 255 zeichen -- wird als ./target.md gespeichert diff --git a/doc/allgemein/area.md b/doc/area.md similarity index 59% rename from doc/allgemein/area.md rename to doc/area.md index e695f4e..3eaea46 100644 --- a/doc/allgemein/area.md +++ b/doc/area.md @@ -1,3 +1,3 @@ - container für features - Auflistung als Columns -- wird gespeichert als ./{area}/ +- wird gespeichert als `./{area}/` diff --git a/doc/core.md b/doc/core.md index 68c71ec..b4c2d0f 100644 --- a/doc/core.md +++ b/doc/core.md @@ -1 +1,3 @@ -Obsidian Plugin - Projektkontext managen \ No newline at end of file +- Max 64 Zeichen +- wird als `./_core.md` gespeichert +- beschreibt kompakt was das projekt ist diff --git a/doc/allgemein/details.md b/doc/details.md similarity index 89% rename from doc/allgemein/details.md rename to doc/details.md index 7294403..e39568c 100644 --- a/doc/allgemein/details.md +++ b/doc/details.md @@ -5,7 +5,7 @@ layout: features features - alle features zum area als cards -- LMB auf feature öffnet die {feature}.md +- LMB auf feature öffnet die `{feature}.md` - RMB auf feature öffnet die feature optionen feature option create diff --git a/doc/allgemein/feature.md b/doc/feature.md similarity index 58% rename from doc/allgemein/feature.md rename to doc/feature.md index 1fe76ce..fed43ca 100644 --- a/doc/allgemein/feature.md +++ b/doc/feature.md @@ -1,3 +1,3 @@ - beschreibt eine funktionalität der area - wie ein git commit -- wird gespeichert als ./{area}/{feature}.md \ No newline at end of file +- wird gespeichert als `./{area}/{feature}.md` \ No newline at end of file diff --git a/doc/allgemein/overview.md b/doc/overview.md similarity index 100% rename from doc/allgemein/overview.md rename to doc/overview.md diff --git a/doc/project.md b/doc/project.md index 82e9666..db40996 100644 --- a/doc/project.md +++ b/doc/project.md @@ -1,24 +1,9 @@ -**Core** -- Obsidian Plugin - Projektkontext managen -**Target** -- Für große Projekte geht der Kontext schnell verloren. -- Sie werden unübersichtlich und schwer zu managen. -- Das Plugin hilft Projekte kontextbezogen zu strukturieren. -- So lassen sich große Projekte wieder managen. -**Story** -- Großes Projekt soll geplannt werden -- Core definieren (Worum geht es?) -- Target beschreiben (Welches Problem löst es?) -- Story schreiben (Wie wird es verwendet?) -- Areas und Features definieren (Welche Funktionen hat es?) -- Features implementieren -- Projekt v1 -**Areas** -- **Core**: Kompakter Titel zum Projekt -- **Target**: Problem -> Folge -> Lösung -> Ergebnis zum Projekt -- **Story**: Kernverwendung vom Projekt -- **Area**: Sammlung von Features -- **Feature**: Abgeschlossene Funktion im Bereich -- **Projects**: Übersicht der Projekte -- **Overview**: Übersicht zu einem Projekt -- **Detail**: Übersicht zu einem Area +- kann ich projekt oder subprojekt zu einem großen projekt sein +- ist abgeschlossen, also hat keine abhängigkeit +- ist der root ordner +- ordnername ist der projektname + +kerndatei +- wird als project.md gespeichert +- enthält kern, zweck, stories und kategorien als md dateien +- enthält bereiche als ordner diff --git a/doc/test/projects.md b/doc/projects.md similarity index 100% rename from doc/test/projects.md rename to doc/projects.md diff --git a/doc/story.md b/doc/story.md index 71bb8ba..b6f651e 100644 --- a/doc/story.md +++ b/doc/story.md @@ -1,7 +1,5 @@ -- Großes Projekt soll geplannt werden -- Core definieren (Worum geht es?) -- Target beschreiben (Welches Problem löst es?) -- Story schreiben (Wie wird es verwendet?) -- Areas und Features definieren (Welche Funktionen hat es?) -- Features implementieren -- Projekt v1 \ No newline at end of file +- beschreibt wie das fertige projekt in der regel verwendet werden soll +- Max 1024 Zeichen +- wird als `./_story.md` gespeichert +- nur die hauptanwendung +- abfolge von schritten (start ... ende) \ No newline at end of file diff --git a/doc/target.md b/doc/target.md index a8c26d2..644e594 100644 --- a/doc/target.md +++ b/doc/target.md @@ -1,2 +1,6 @@ -Für große Projekte geht der Kontext schnell verloren, sie werden unübersichtlich und schwer zu managen. -Das Plugin hilft Projekte kontextbezogen zu strukturieren, so lassen sich große Projekte wieder managen. \ No newline at end of file +- beschreibt simpel welches problem das projekt löst + - Was ist das Problem und die Folge davon? + - Wie löst das Projekt das Problem und was ist die Folge davon? +- der zweck muss klar erkennbar sein +- max 255 zeichen +- wird als `./_target.md` gespeichert diff --git a/src/const.ts b/src/const.ts index d4eb50b..26827a7 100644 --- a/src/const.ts +++ b/src/const.ts @@ -6,4 +6,9 @@ export const VIEW_TYPE_DETAILS = "projektkontext-details"; export const RIBBON_ICON = "layout-grid"; -export const PROJECT_FILES = ["core.md", "target.md", "story.md"] as const; +export const PROJECT_FILE = "_project.md"; +export const CORE_FILE = "_core.md"; +export const TARGET_FILE = "_target.md"; +export const STORY_FILE = "_story.md"; + +export const PROJECT_FILES = [PROJECT_FILE, CORE_FILE, TARGET_FILE, STORY_FILE] as const; diff --git a/src/fs.ts b/src/fs.ts index 24f076a..c49641a 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -94,6 +94,19 @@ export async function createFeature( return await ensureFile(app, featurePath(project, area, feature), ""); } +export function projectFeaturePath(project: string, feature: string): string { + const file = feature.endsWith(".md") ? feature : `${feature}.md`; + return normalizePath(`${PROJECTS_ROOT}/${project}/${file}`); +} + +export async function createProjectFeature( + app: App, + project: string, + feature: string, +): Promise { + return await ensureFile(app, projectFeaturePath(project, feature), ""); +} + export function isProjectRootFile(name: string): boolean { return (PROJECT_FILES as readonly string[]).includes(name); } diff --git a/src/views/OverviewView.ts b/src/views/OverviewView.ts index 0deb24a..0248125 100644 --- a/src/views/OverviewView.ts +++ b/src/views/OverviewView.ts @@ -4,11 +4,16 @@ import { VIEW_TYPE_DETAILS, VIEW_TYPE_PROJECTS, RIBBON_ICON, + PROJECT_FILES, + CORE_FILE, + TARGET_FILE, + STORY_FILE, } from "../const"; import { projectPath, areaPath, featurePath, + projectFeaturePath, listFolders, listMarkdownFiles, readFile, @@ -16,6 +21,7 @@ import { deleteRecursive, createArea, createFeature, + createProjectFeature, } from "../fs"; import { menu, breadcrumb, emptyState, openMarkdown } from "../ui"; import { NameModal } from "../modals/NameModal"; @@ -90,9 +96,9 @@ export class OverviewView extends ItemView { } const projRoot = projectPath(this.project); - const corePath = normalizePath(`${projRoot}/core.md`); - const targetPath = normalizePath(`${projRoot}/target.md`); - const storyPath = normalizePath(`${projRoot}/story.md`); + const corePath = normalizePath(`${projRoot}/${CORE_FILE}`); + const targetPath = normalizePath(`${projRoot}/${TARGET_FILE}`); + const storyPath = normalizePath(`${projRoot}/${STORY_FILE}`); const [core, target, story] = await Promise.all([ readFile(this.app, corePath), readFile(this.app, targetPath), @@ -157,12 +163,36 @@ export class OverviewView extends ItemView { ev.preventDefault(); menu(ev, [ { title: "Neue Area", icon: "plus", onClick: () => this.openCreateArea() }, + { title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature() }, ]); }); + section.addEventListener("dragover", (ev) => { + if (!ev.dataTransfer?.types.includes(FEATURE_DND_MIME)) return; + ev.preventDefault(); + ev.dataTransfer.dropEffect = "move"; + section.addClass("pk-drop-zone"); + }); + section.addEventListener("dragleave", (ev) => { + const next = ev.relatedTarget as Node | null; + if (next && section.contains(next)) return; + section.removeClass("pk-drop-zone"); + }); + section.addEventListener("drop", async (ev) => { + section.removeClass("pk-drop-zone"); + const raw = ev.dataTransfer?.getData(FEATURE_DND_MIME); + if (!raw) return; + ev.preventDefault(); + await this.handleFeatureDropToProject(raw); + }); const areas = listFolders(this.app, projectPath(this.project)); - if (areas.length === 0) { - emptyState(section, "Noch keine Areas. Rechtsklick → Neue Area."); + const projectFeatures = listMarkdownFiles( + this.app, + projectPath(this.project), + [...PROJECT_FILES], + ); + if (areas.length === 0 && projectFeatures.length === 0) { + emptyState(section, "Noch leer. Rechtsklick → Neue Area / Neues Feature."); return; } const takenAreas = areas.map((a) => a.name); @@ -243,23 +273,109 @@ export class OverviewView extends ItemView { } } } + + for (const f of projectFeatures) { + const fc = stack.createDiv({ + cls: "pk-btn-card pk-area-card", + attr: { role: "button", tabindex: "0", draggable: "true" }, + }); + fc.createEl("strong", { text: f.basename }); + fc.addEventListener("dragstart", (ev) => { + if (!ev.dataTransfer) return; + ev.dataTransfer.setData( + FEATURE_DND_MIME, + JSON.stringify({ sourceArea: null, feature: f.basename }), + ); + ev.dataTransfer.effectAllowed = "move"; + fc.addClass("pk-feature-chip-dragging"); + }); + fc.addEventListener("dragend", () => { + fc.removeClass("pk-feature-chip-dragging"); + }); + fc.addEventListener("click", () => + openMarkdown(this.app, f.path, { + type: VIEW_TYPE_OVERVIEW, + state: { project: this.project }, + }), + ); + fc.addEventListener("contextmenu", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + menu(ev, [ + { title: "Feature löschen", icon: "trash", onClick: () => this.openDeleteProjectFeature(f.basename) }, + ]); + }); + } } - private async handleFeatureDrop(raw: string, targetArea: string): Promise { - let data: { sourceArea: string; feature: string }; + private openCreateProjectFeature(): void { + const taken = listMarkdownFiles(this.app, projectPath(this.project), []).map((f) => f.basename); + new NameModal(this.app, { + title: "Neues Feature", + label: "Feature-Name", + cta: "Erstellen", + validate: (n) => validateName(n, taken), + onSubmit: async (name) => { + await createProjectFeature(this.app, this.project, name); + await this.render(); + }, + }).open(); + } + + private openDeleteProjectFeature(feature: string): void { + new ConfirmModal(this.app, { + title: "Feature löschen", + message: `Feature „${feature}" wird gelöscht. Fortfahren?`, + cta: "Löschen", + destructive: true, + onConfirm: async () => { + await deleteRecursive(this.app, projectFeaturePath(this.project, feature)); + await this.render(); + }, + }).open(); + } + + private async handleFeatureDropToProject(raw: string): Promise { + let data: { sourceArea: string | null; feature: string }; try { data = JSON.parse(raw); } catch { return; } - if (!data?.sourceArea || !data?.feature) return; + if (!data?.feature) return; + if (data.sourceArea === null) return; + const newPath = projectFeaturePath(this.project, data.feature); + if (this.app.vault.getAbstractFileByPath(newPath)) { + new Notice(`„${data.feature}" existiert im Projekt 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 async handleFeatureDrop(raw: string, targetArea: string): Promise { + let data: { sourceArea: string | null; feature: string }; + try { + data = JSON.parse(raw); + } catch { + return; + } + if (!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); + const oldPath = data.sourceArea === null + ? projectFeaturePath(this.project, data.feature) + : featurePath(this.project, data.sourceArea, data.feature); try { await rename(this.app, oldPath, newPath); } catch (e) { diff --git a/src/views/ProjectsView.ts b/src/views/ProjectsView.ts index db48f59..2a5688a 100644 --- a/src/views/ProjectsView.ts +++ b/src/views/ProjectsView.ts @@ -1,5 +1,5 @@ import { ItemView, Notice, WorkspaceLeaf, normalizePath } from "obsidian"; -import { VIEW_TYPE_PROJECTS, VIEW_TYPE_OVERVIEW, RIBBON_ICON } from "../const"; +import { VIEW_TYPE_PROJECTS, VIEW_TYPE_OVERVIEW, RIBBON_ICON, CORE_FILE } from "../const"; import { projectsPath, projectPath, @@ -47,7 +47,7 @@ export class ProjectsView extends ItemView { 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() + "/")) { + if (f.path.endsWith("/" + CORE_FILE) && f.path.startsWith(projectsPath() + "/")) { this.render(); } })); @@ -78,7 +78,7 @@ 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`)), + readFile(this.app, normalizePath(`${projectPath(p.name)}/${CORE_FILE}`)), ), ); diff --git a/styles.css b/styles.css index aec5391..a5ad3e4 100644 --- a/styles.css +++ b/styles.css @@ -163,6 +163,11 @@ outline-offset: -2px; } +.pk-drop-zone { + background: var(--background-modifier-hover); + border-radius: 6px; +} + .pk-feature-chip-dragging { opacity: 0.4; } @@ -171,6 +176,7 @@ display: flex; flex-direction: column; gap: 10px; + padding: 5px; } .pk-areas-flex {