update
This commit is contained in:
@@ -1,9 +0,0 @@
|
|||||||
- links sind die übersichten
|
|
||||||
- projektübersicht
|
|
||||||
- projektdetails
|
|
||||||
- areadetails
|
|
||||||
- links bleibt im selben fenster
|
|
||||||
- rechts sind die md dateien
|
|
||||||
- md datei öffnen
|
|
||||||
- öffnen sich immer rechts beim nächstmöglichen fenster
|
|
||||||
- nicht vorhanden, dann wird ein neues geöffnet
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
- projektübersicht, projektdetails, areadetails und md dateien bleiben im selben fenster
|
|
||||||
- navigation über die breadcrumb
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
**Obsidian Plugin**
|
|
||||||
Projektkontext managen
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
- sammlung von features
|
|
||||||
- Auflistung als Columns (aufstockend)
|
|
||||||
- wird gespeichert als `./{area}/`
|
|
||||||
- `__` sind allgemeine zum ordner
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
- steht ganz oben
|
|
||||||
- desktop
|
|
||||||
- Projekte > {projektname}
|
|
||||||
- Projekte > {projektname} > {areaname}
|
|
||||||
- mobile
|
|
||||||
- Projekte > {projektname} > {md-name}
|
|
||||||
- Projekte > {projektname} > {areaname} > {md-name}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
- beschreibt kompakt was das projekt ist
|
|
||||||
- Max 128 Zeichen
|
|
||||||
- wird als `./_core.md` gespeichert
|
|
||||||
- als md gerendert
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- LMB auf area öffnet die areatdetails
|
|
||||||
- enthält details zur area
|
|
||||||
- layout: features
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- erstellt eine neue feature md datei im area ordner
|
|
||||||
- modal für den feature namen
|
|
||||||
- erstellen und abbrechen buttons
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- erstellt eine neue feature md datei im area ordner
|
|
||||||
- modal für den feature namen
|
|
||||||
- erstellen und abbrechen buttons
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
- alle features zum area als button-cards
|
|
||||||
- name ind bold
|
|
||||||
- inhalt als md gerendert dadrunter
|
|
||||||
- max width für cards ist hier 300px
|
|
||||||
- LMB auf feature öffnet die `{feature}.md`
|
|
||||||
- RMB auf feature container öffnet die feature optionen
|
|
||||||
- feature erstellen
|
|
||||||
- RMB auf feature öffnet die feature optionen
|
|
||||||
- feature löschen
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- beschreibt eine funktionalität der area
|
|
||||||
- wie ein git commit
|
|
||||||
- wird gespeichert als `./{area}/{feature}.md`
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
- LMB auf projekt öffnet die projektdetails
|
|
||||||
- enthält details zum projekt
|
|
||||||
- layout als grid responsiv
|
|
||||||
- core+target
|
|
||||||
- areas
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
- zwei varienten
|
|
||||||
- area mit features
|
|
||||||
- einzelnes feature
|
|
||||||
- beide sind button-cards
|
|
||||||
- area enthält features als flex liste
|
|
||||||
- featurename ist dateiname
|
|
||||||
- zuerst einzelne features verwenden
|
|
||||||
- areas, wenn 2 oder mehr features zusammen gehören
|
|
||||||
- RMB auf area container öffnet optionen zur area container
|
|
||||||
- area erstellen
|
|
||||||
- feature erstellen
|
|
||||||
- RMB auf area öffnet optionen zur area
|
|
||||||
- feature erstellen
|
|
||||||
- area umbenenen
|
|
||||||
- area löschen
|
|
||||||
- RMB auf feature öffnet die feature optionen
|
|
||||||
- feature löschen
|
|
||||||
- LMB auf feature öffnet die {feature}.md
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
- inhalt aus `_core.md`
|
|
||||||
- LMB öffnet `_core.md`
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- erstellt eine neuen area ordner
|
|
||||||
- modal für den area namen
|
|
||||||
- erstellen und abbrechen buttons
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- erstellt eine neue feature md datei im area ordner
|
|
||||||
- modal für den feature namen
|
|
||||||
- erstellen und abbrechen buttons
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- entfernt area rekursiv
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- entfernt feature
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
- featues kann man per d&d verschieben
|
|
||||||
- featue aus area -> area container
|
|
||||||
- featue aus area container -> area
|
|
||||||
- featue aus area -> area
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- ändert den ordnernamen zur area
|
|
||||||
- modal mit dem aktuellen area namen
|
|
||||||
- speichern und abbrechen buttons
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
- inhalt aus `_target.md`
|
|
||||||
- LMB öffnet `_target.md`
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- root ordner zum projekt
|
|
||||||
- ist abgeschlossen, also hat keine abhängigkeit
|
|
||||||
- ordnername ist der projektname
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
- enthält alle projekte
|
|
||||||
- LMB auf plugin icon öffnet die projektübersicht
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
- erstellt ein neues projekt
|
|
||||||
- erstellt automatisch `_core.md`, `_target.md` und story area
|
|
||||||
- modal für den projektnamen
|
|
||||||
- erstellen und abbrechen buttons
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- entfernt projekt rekursiv
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
- lädt alle ordner aus ./projects/
|
|
||||||
- projekte als button-cards
|
|
||||||
- grid layout
|
|
||||||
- name ist ordnername bold und core inhalt dadrunter
|
|
||||||
- RMB auf projekt Container öffnet optionen zum projekt
|
|
||||||
- projekt erstellen
|
|
||||||
- RMB auf projekt öffnet optionen zum projekt
|
|
||||||
- projekt umbenennen
|
|
||||||
- projekt löschen
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
- layout-grid icon
|
|
||||||
- icon links in obsidian navigation
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
- ändert den ordnernamen zum projekt
|
|
||||||
- modal mit dem aktuellen projektnamen
|
|
||||||
- speichern und abbrechen buttons
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
- beschreibt wie das fertige projekt in der regel verwendet werden soll
|
|
||||||
- abfolge von schritten (start ... ende)
|
|
||||||
- eine oder mehrere stories
|
|
||||||
- stehen in der area story
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
- 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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
- Projekt soll erweitert werden
|
|
||||||
- Areas und Features erstellen / anpassen / entfernen
|
|
||||||
- Features implementieren
|
|
||||||
- Projekt v2
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
- beschreibt simpel welches problem das projekt löst
|
|
||||||
- Folgende Fragen werden beantwortet
|
|
||||||
- Was ist das Problem?
|
|
||||||
- Was ist die Folge vom Problem?
|
|
||||||
- Wie löst das Projekt das Problem?
|
|
||||||
- Was ist die folge der Lösung?
|
|
||||||
- der zweck muss klar erkennbar sein
|
|
||||||
- max 256 zeichen
|
|
||||||
- wird als `./_target.md` gespeichert
|
|
||||||
- als md gerendert
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export const PROJECTS_ROOT = "projects";
|
export const PROJECTS_ROOT = "projects";
|
||||||
|
export const TO_UPDATE_DIR = "_to-update";
|
||||||
|
|
||||||
export const VIEW_TYPE_PROJECTS = "projektkontext-projects";
|
export const VIEW_TYPE_PROJECTS = "projektkontext-projects";
|
||||||
export const VIEW_TYPE_OVERVIEW = "projektkontext-overview";
|
export const VIEW_TYPE_OVERVIEW = "projektkontext-overview";
|
||||||
|
|||||||
63
src/fs.ts
63
src/fs.ts
@@ -1,5 +1,7 @@
|
|||||||
import { App, TFile, TFolder, normalizePath } from "obsidian";
|
import { App, TFile, TFolder, normalizePath } from "obsidian";
|
||||||
import { PROJECTS_ROOT, PROJECT_FILES } from "./const";
|
import { PROJECTS_ROOT, PROJECT_FILES, TO_UPDATE_DIR } from "./const";
|
||||||
|
|
||||||
|
export type Zone = "ready" | "to-update";
|
||||||
|
|
||||||
export function projectsPath(): string {
|
export function projectsPath(): string {
|
||||||
return PROJECTS_ROOT;
|
return PROJECTS_ROOT;
|
||||||
@@ -9,13 +11,26 @@ export function projectPath(name: string): string {
|
|||||||
return normalizePath(`${PROJECTS_ROOT}/${name}`);
|
return normalizePath(`${PROJECTS_ROOT}/${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function areaPath(project: string, area: string): string {
|
export function toUpdatePath(project: string): string {
|
||||||
return normalizePath(`${PROJECTS_ROOT}/${project}/${area}`);
|
return normalizePath(`${PROJECTS_ROOT}/${project}/${TO_UPDATE_DIR}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function featurePath(project: string, area: string, feature: string): string {
|
export function zoneRootPath(project: string, zone: Zone): string {
|
||||||
|
return zone === "ready" ? projectPath(project) : toUpdatePath(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function areaPath(project: string, area: string, zone: Zone = "ready"): string {
|
||||||
|
return normalizePath(`${zoneRootPath(project, zone)}/${area}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function featurePath(
|
||||||
|
project: string,
|
||||||
|
area: string,
|
||||||
|
feature: string,
|
||||||
|
zone: Zone = "ready",
|
||||||
|
): string {
|
||||||
const file = feature.endsWith(".md") ? feature : `${feature}.md`;
|
const file = feature.endsWith(".md") ? feature : `${feature}.md`;
|
||||||
return normalizePath(`${PROJECTS_ROOT}/${project}/${area}/${file}`);
|
return normalizePath(`${zoneRootPath(project, zone)}/${area}/${file}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureFolder(app: App, path: string): Promise<void> {
|
export async function ensureFolder(app: App, path: string): Promise<void> {
|
||||||
@@ -82,8 +97,14 @@ export async function createProject(app: App, name: string): Promise<void> {
|
|||||||
await createArea(app, name, "story");
|
await createArea(app, name, "story");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createArea(app: App, project: string, area: string): Promise<void> {
|
export async function createArea(
|
||||||
await ensureFolder(app, areaPath(project, area));
|
app: App,
|
||||||
|
project: string,
|
||||||
|
area: string,
|
||||||
|
zone: Zone = "to-update",
|
||||||
|
): Promise<void> {
|
||||||
|
if (zone === "to-update") await ensureFolder(app, toUpdatePath(project));
|
||||||
|
await ensureFolder(app, areaPath(project, area, zone));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFeature(
|
export async function createFeature(
|
||||||
@@ -91,21 +112,28 @@ export async function createFeature(
|
|||||||
project: string,
|
project: string,
|
||||||
area: string,
|
area: string,
|
||||||
feature: string,
|
feature: string,
|
||||||
|
zone: Zone = "ready",
|
||||||
): Promise<TFile> {
|
): Promise<TFile> {
|
||||||
return await ensureFile(app, featurePath(project, area, feature), "");
|
return await ensureFile(app, featurePath(project, area, feature, zone), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectFeaturePath(project: string, feature: string): string {
|
export function projectFeaturePath(
|
||||||
|
project: string,
|
||||||
|
feature: string,
|
||||||
|
zone: Zone = "ready",
|
||||||
|
): string {
|
||||||
const file = feature.endsWith(".md") ? feature : `${feature}.md`;
|
const file = feature.endsWith(".md") ? feature : `${feature}.md`;
|
||||||
return normalizePath(`${PROJECTS_ROOT}/${project}/${file}`);
|
return normalizePath(`${zoneRootPath(project, zone)}/${file}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createProjectFeature(
|
export async function createProjectFeature(
|
||||||
app: App,
|
app: App,
|
||||||
project: string,
|
project: string,
|
||||||
feature: string,
|
feature: string,
|
||||||
|
zone: Zone = "to-update",
|
||||||
): Promise<TFile> {
|
): Promise<TFile> {
|
||||||
return await ensureFile(app, projectFeaturePath(project, feature), "");
|
if (zone === "to-update") await ensureFolder(app, toUpdatePath(project));
|
||||||
|
return await ensureFile(app, projectFeaturePath(project, feature, zone), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isProjectRootFile(name: string): boolean {
|
export function isProjectRootFile(name: string): boolean {
|
||||||
@@ -115,13 +143,22 @@ export function isProjectRootFile(name: string): boolean {
|
|||||||
export interface ProjectFileLocation {
|
export interface ProjectFileLocation {
|
||||||
project: string;
|
project: string;
|
||||||
area?: string;
|
area?: string;
|
||||||
|
zone: Zone;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseProjectFilePath(path: string): ProjectFileLocation | null {
|
export function parseProjectFilePath(path: string): ProjectFileLocation | null {
|
||||||
if (!path.endsWith(".md")) return null;
|
if (!path.endsWith(".md")) return null;
|
||||||
const parts = normalizePath(path).split("/");
|
const parts = normalizePath(path).split("/");
|
||||||
if (parts[0] !== PROJECTS_ROOT) return null;
|
if (parts[0] !== PROJECTS_ROOT) return null;
|
||||||
if (parts.length === 3) return { project: parts[1] };
|
if (parts.length < 3) return null;
|
||||||
if (parts.length === 4) return { project: parts[1], area: parts[2] };
|
const project = parts[1];
|
||||||
|
const rest = parts.slice(2);
|
||||||
|
let zone: Zone = "ready";
|
||||||
|
if (rest[0] === TO_UPDATE_DIR) {
|
||||||
|
zone = "to-update";
|
||||||
|
rest.shift();
|
||||||
|
}
|
||||||
|
if (rest.length === 1) return { project, zone };
|
||||||
|
if (rest.length === 2) return { project, area: rest[0], zone };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
RIBBON_ICON,
|
RIBBON_ICON,
|
||||||
} from "../const";
|
} from "../const";
|
||||||
import {
|
import {
|
||||||
|
Zone,
|
||||||
areaPath,
|
areaPath,
|
||||||
featurePath,
|
featurePath,
|
||||||
listMarkdownFiles,
|
listMarkdownFiles,
|
||||||
@@ -19,6 +20,7 @@ import { NameModal } from "../modals/NameModal";
|
|||||||
export interface DetailsState extends Record<string, unknown> {
|
export interface DetailsState extends Record<string, unknown> {
|
||||||
project: string;
|
project: string;
|
||||||
area: string;
|
area: string;
|
||||||
|
zone?: Zone;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
||||||
@@ -33,6 +35,7 @@ function validateName(name: string, taken: string[]): string | null {
|
|||||||
export class DetailsView extends ItemView {
|
export class DetailsView extends ItemView {
|
||||||
project = "";
|
project = "";
|
||||||
area = "";
|
area = "";
|
||||||
|
zone: Zone = "ready";
|
||||||
private renderToken = 0;
|
private renderToken = 0;
|
||||||
|
|
||||||
constructor(leaf: WorkspaceLeaf) {
|
constructor(leaf: WorkspaceLeaf) {
|
||||||
@@ -55,12 +58,13 @@ export class DetailsView extends ItemView {
|
|||||||
async setState(state: DetailsState, result: ViewStateResult): Promise<void> {
|
async setState(state: DetailsState, result: ViewStateResult): Promise<void> {
|
||||||
this.project = state?.project ?? "";
|
this.project = state?.project ?? "";
|
||||||
this.area = state?.area ?? "";
|
this.area = state?.area ?? "";
|
||||||
|
this.zone = state?.zone ?? "ready";
|
||||||
await super.setState(state, result);
|
await super.setState(state, result);
|
||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(): DetailsState {
|
getState(): DetailsState {
|
||||||
return { project: this.project, area: this.area };
|
return { project: this.project, area: this.area, zone: this.zone };
|
||||||
}
|
}
|
||||||
|
|
||||||
async onOpen(): Promise<void> {
|
async onOpen(): Promise<void> {
|
||||||
@@ -90,7 +94,7 @@ export class DetailsView extends ItemView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const features = listMarkdownFiles(this.app, areaPath(this.project, this.area), []);
|
const features = listMarkdownFiles(this.app, areaPath(this.project, this.area, 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;
|
||||||
|
|
||||||
@@ -133,23 +137,25 @@ export class DetailsView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private openCreate(): void {
|
private openCreate(): void {
|
||||||
const taken = listMarkdownFiles(this.app, areaPath(this.project, this.area), []).map(
|
const taken = listMarkdownFiles(
|
||||||
(f) => f.basename,
|
this.app,
|
||||||
);
|
areaPath(this.project, this.area, this.zone),
|
||||||
|
[],
|
||||||
|
).map((f) => f.basename);
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
title: "Neues Feature",
|
title: "Neues Feature",
|
||||||
label: "Feature-Name",
|
label: "Feature-Name",
|
||||||
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);
|
await createFeature(this.app, this.project, this.area, 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));
|
await deleteRecursive(this.app, featurePath(this.project, this.area, feature, this.zone));
|
||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemView, MarkdownRenderer, Notice, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian";
|
import { ItemView, MarkdownRenderer, Notice, TFile, TFolder, WorkspaceLeaf, ViewStateResult, normalizePath } from "obsidian";
|
||||||
import {
|
import {
|
||||||
VIEW_TYPE_OVERVIEW,
|
VIEW_TYPE_OVERVIEW,
|
||||||
VIEW_TYPE_DETAILS,
|
VIEW_TYPE_DETAILS,
|
||||||
@@ -7,17 +7,19 @@ import {
|
|||||||
PROJECT_FILES,
|
PROJECT_FILES,
|
||||||
CORE_FILE,
|
CORE_FILE,
|
||||||
TARGET_FILE,
|
TARGET_FILE,
|
||||||
|
TO_UPDATE_DIR,
|
||||||
} from "../const";
|
} from "../const";
|
||||||
import {
|
import {
|
||||||
|
Zone,
|
||||||
projectPath,
|
projectPath,
|
||||||
areaPath,
|
toUpdatePath,
|
||||||
featurePath,
|
zoneRootPath,
|
||||||
projectFeaturePath,
|
|
||||||
listFolders,
|
listFolders,
|
||||||
listMarkdownFiles,
|
listMarkdownFiles,
|
||||||
readFile,
|
readFile,
|
||||||
rename,
|
rename,
|
||||||
deleteRecursive,
|
deleteRecursive,
|
||||||
|
ensureFolder,
|
||||||
createArea,
|
createArea,
|
||||||
createFeature,
|
createFeature,
|
||||||
createProjectFeature,
|
createProjectFeature,
|
||||||
@@ -30,7 +32,13 @@ export interface OverviewState extends Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
||||||
const FEATURE_DND_MIME = "application/x-pk-feature";
|
const PK_DND_MIME = "application/x-pk-item";
|
||||||
|
|
||||||
|
interface DndPayload {
|
||||||
|
kind: "feature" | "collection";
|
||||||
|
sourcePath: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
function validateName(name: string, taken: string[], current?: string): string | null {
|
function validateName(name: string, taken: string[], current?: string): string | null {
|
||||||
if (!name) return "Name darf nicht leer sein.";
|
if (!name) return "Name darf nicht leer sein.";
|
||||||
@@ -135,100 +143,149 @@ export class OverviewView extends ItemView {
|
|||||||
if (ev.defaultPrevented) return;
|
if (ev.defaultPrevented) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
menu(ev, [
|
menu(ev, [
|
||||||
{ title: "Neue Area", icon: "plus", onClick: () => this.openCreateArea() },
|
{ title: "Neue Collection", icon: "plus", onClick: () => this.openCreateArea() },
|
||||||
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature() },
|
{ 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));
|
const ready = this.collectZoneItems("ready");
|
||||||
const projectFeatures = listMarkdownFiles(
|
const toUpdate = this.collectZoneItems("to-update");
|
||||||
this.app,
|
|
||||||
projectPath(this.project),
|
this.renderZone(section, "ready", ready);
|
||||||
[...PROJECT_FILES],
|
section.createEl("hr", { cls: "pk-zone-divider" });
|
||||||
);
|
this.renderZone(section, "to-update", toUpdate);
|
||||||
if (areas.length === 0 && projectFeatures.length === 0) {
|
|
||||||
emptyState(section, "Noch leer. Rechtsklick → Neue Area / Neues Feature.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const takenAreas = areas.map((a) => a.name);
|
|
||||||
const stack = section.createDiv({ cls: "pk-areas-flex" });
|
private collectZoneItems(zone: Zone): { areas: TFolder[]; features: TFile[] } {
|
||||||
for (const area of areas) {
|
const root = zoneRootPath(this.project, zone);
|
||||||
|
const folders = listFolders(this.app, root);
|
||||||
|
const areas = zone === "ready"
|
||||||
|
? folders.filter((f) => f.name !== TO_UPDATE_DIR)
|
||||||
|
: folders;
|
||||||
const features = listMarkdownFiles(
|
const features = listMarkdownFiles(
|
||||||
this.app,
|
this.app,
|
||||||
areaPath(this.project, area.name),
|
root,
|
||||||
[],
|
zone === "ready" ? [...PROJECT_FILES] : [],
|
||||||
);
|
);
|
||||||
const areaCard = stack.createDiv({
|
return { areas, features };
|
||||||
cls: "pk-btn-card pk-area-card",
|
}
|
||||||
attr: { role: "button", tabindex: "0" },
|
|
||||||
|
private renderZone(
|
||||||
|
parent: HTMLElement,
|
||||||
|
zone: Zone,
|
||||||
|
items: { areas: TFolder[]; features: TFile[] },
|
||||||
|
): void {
|
||||||
|
const flex = parent.createDiv({
|
||||||
|
cls: `pk-areas-flex pk-zone-${zone}`,
|
||||||
|
attr: { "data-zone": zone },
|
||||||
});
|
});
|
||||||
areaCard.createEl("strong", { text: area.name });
|
flex.addEventListener("dragover", (ev) => {
|
||||||
areaCard.addEventListener("click", () => this.openDetails(area.name));
|
if (!ev.dataTransfer?.types.includes(PK_DND_MIME)) return;
|
||||||
areaCard.addEventListener("contextmenu", (ev) => {
|
ev.preventDefault();
|
||||||
|
ev.dataTransfer.dropEffect = "move";
|
||||||
|
flex.addClass("pk-drop-zone");
|
||||||
|
});
|
||||||
|
flex.addEventListener("dragleave", (ev) => {
|
||||||
|
const next = ev.relatedTarget as Node | null;
|
||||||
|
if (next && flex.contains(next)) return;
|
||||||
|
flex.removeClass("pk-drop-zone");
|
||||||
|
});
|
||||||
|
flex.addEventListener("drop", async (ev) => {
|
||||||
|
flex.removeClass("pk-drop-zone");
|
||||||
|
const raw = ev.dataTransfer?.getData(PK_DND_MIME);
|
||||||
|
if (!raw) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
await this.handleDropOnZone(raw, zone);
|
||||||
|
});
|
||||||
|
|
||||||
|
const takenAreas = items.areas.map((a) => a.name);
|
||||||
|
for (const area of items.areas) {
|
||||||
|
this.renderAreaCard(flex, zone, area, takenAreas);
|
||||||
|
}
|
||||||
|
for (const f of items.features) {
|
||||||
|
this.renderProjectFeatureCard(flex, f);
|
||||||
|
}
|
||||||
|
if (items.areas.length === 0 && items.features.length === 0) {
|
||||||
|
flex.createDiv({
|
||||||
|
cls: "pk-zone-placeholder",
|
||||||
|
text: zone === "to-update" ? "Nothing to update" : "Empty",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAreaCard(
|
||||||
|
parent: HTMLElement,
|
||||||
|
zone: Zone,
|
||||||
|
area: TFolder,
|
||||||
|
takenAreas: string[],
|
||||||
|
): void {
|
||||||
|
const folderPath = area.path;
|
||||||
|
const features = listMarkdownFiles(this.app, folderPath, []);
|
||||||
|
|
||||||
|
const card = parent.createDiv({
|
||||||
|
cls: "pk-btn-card pk-area-card",
|
||||||
|
attr: { role: "button", tabindex: "0", draggable: "true" },
|
||||||
|
});
|
||||||
|
card.createEl("strong", { text: area.name });
|
||||||
|
card.addEventListener("click", () => this.openDetails(area.name, zone));
|
||||||
|
card.addEventListener("contextmenu", (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
menu(ev, [
|
menu(ev, [
|
||||||
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeature(area.name) },
|
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateFeatureIn(folderPath) },
|
||||||
{ title: "Area umbenennen", icon: "pencil", onClick: () => this.openRenameArea(area.name, takenAreas) },
|
{ title: "Collection umbenennen", icon: "pencil", onClick: () => this.openRenameAreaAt(folderPath, area.name, takenAreas) },
|
||||||
{ title: "Area löschen", icon: "trash", onClick: () => this.openDeleteArea(area.name) },
|
{ title: "Collection löschen", icon: "trash", onClick: () => this.openDeletePath(folderPath) },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
areaCard.addEventListener("dragover", (ev) => {
|
card.addEventListener("dragstart", (ev) => {
|
||||||
if (!ev.dataTransfer?.types.includes(FEATURE_DND_MIME)) return;
|
if (!ev.dataTransfer) return;
|
||||||
|
ev.dataTransfer.setData(PK_DND_MIME, JSON.stringify({
|
||||||
|
kind: "collection",
|
||||||
|
sourcePath: folderPath,
|
||||||
|
name: area.name,
|
||||||
|
} satisfies DndPayload));
|
||||||
|
ev.dataTransfer.effectAllowed = "move";
|
||||||
|
card.addClass("pk-feature-chip-dragging");
|
||||||
|
});
|
||||||
|
card.addEventListener("dragend", () => card.removeClass("pk-feature-chip-dragging"));
|
||||||
|
card.addEventListener("dragover", (ev) => {
|
||||||
|
if (!ev.dataTransfer?.types.includes(PK_DND_MIME)) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.dataTransfer.dropEffect = "move";
|
ev.dataTransfer.dropEffect = "move";
|
||||||
areaCard.addClass("pk-drop-target");
|
card.addClass("pk-drop-target");
|
||||||
});
|
});
|
||||||
areaCard.addEventListener("dragleave", (ev) => {
|
card.addEventListener("dragleave", (ev) => {
|
||||||
const next = ev.relatedTarget as Node | null;
|
const next = ev.relatedTarget as Node | null;
|
||||||
if (next && areaCard.contains(next)) return;
|
if (next && card.contains(next)) return;
|
||||||
areaCard.removeClass("pk-drop-target");
|
card.removeClass("pk-drop-target");
|
||||||
});
|
});
|
||||||
areaCard.addEventListener("drop", async (ev) => {
|
card.addEventListener("drop", async (ev) => {
|
||||||
areaCard.removeClass("pk-drop-target");
|
card.removeClass("pk-drop-target");
|
||||||
const raw = ev.dataTransfer?.getData(FEATURE_DND_MIME);
|
const raw = ev.dataTransfer?.getData(PK_DND_MIME);
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
await this.handleFeatureDrop(raw, area.name);
|
await this.handleDropOnArea(raw, folderPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (features.length === 0) {
|
if (features.length === 0) {
|
||||||
areaCard.createDiv({ cls: "pk-empty", text: "Keine Features." });
|
card.createDiv({ cls: "pk-empty", text: "Keine Features." });
|
||||||
} else {
|
} else {
|
||||||
const list = areaCard.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) => {
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
ev.dataTransfer.setData(
|
ev.dataTransfer.setData(PK_DND_MIME, JSON.stringify({
|
||||||
FEATURE_DND_MIME,
|
kind: "feature",
|
||||||
JSON.stringify({ sourceArea: area.name, feature: f.basename }),
|
sourcePath: f.path,
|
||||||
);
|
name: f.basename,
|
||||||
|
} satisfies DndPayload));
|
||||||
ev.dataTransfer.effectAllowed = "move";
|
ev.dataTransfer.effectAllowed = "move";
|
||||||
chip.addClass("pk-feature-chip-dragging");
|
chip.addClass("pk-feature-chip-dragging");
|
||||||
});
|
});
|
||||||
chip.addEventListener("dragend", () => {
|
chip.addEventListener("dragend", () => chip.removeClass("pk-feature-chip-dragging"));
|
||||||
chip.removeClass("pk-feature-chip-dragging");
|
|
||||||
});
|
|
||||||
chip.addEventListener("click", (ev) => {
|
chip.addEventListener("click", (ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
openMarkdown(this.app, f.path, this.leaf);
|
openMarkdown(this.app, f.path, this.leaf);
|
||||||
@@ -237,44 +294,92 @@ export class OverviewView extends ItemView {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
menu(ev, [
|
menu(ev, [
|
||||||
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeleteFeature(area.name, f.basename) },
|
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeletePath(f.path) },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const f of projectFeatures) {
|
private renderProjectFeatureCard(parent: HTMLElement, file: TFile): void {
|
||||||
const fc = stack.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" },
|
||||||
});
|
});
|
||||||
fc.createEl("strong", { text: f.basename });
|
card.createEl("strong", { text: file.basename });
|
||||||
fc.addEventListener("dragstart", (ev) => {
|
card.addEventListener("dragstart", (ev) => {
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
ev.dataTransfer.setData(
|
ev.dataTransfer.setData(PK_DND_MIME, JSON.stringify({
|
||||||
FEATURE_DND_MIME,
|
kind: "feature",
|
||||||
JSON.stringify({ sourceArea: null, feature: f.basename }),
|
sourcePath: file.path,
|
||||||
);
|
name: file.basename,
|
||||||
|
} satisfies DndPayload));
|
||||||
ev.dataTransfer.effectAllowed = "move";
|
ev.dataTransfer.effectAllowed = "move";
|
||||||
fc.addClass("pk-feature-chip-dragging");
|
card.addClass("pk-feature-chip-dragging");
|
||||||
});
|
});
|
||||||
fc.addEventListener("dragend", () => {
|
card.addEventListener("dragend", () => card.removeClass("pk-feature-chip-dragging"));
|
||||||
fc.removeClass("pk-feature-chip-dragging");
|
card.addEventListener("click", () => openMarkdown(this.app, file.path, this.leaf));
|
||||||
});
|
card.addEventListener("contextmenu", (ev) => {
|
||||||
fc.addEventListener("click", () => openMarkdown(this.app, f.path, this.leaf));
|
|
||||||
fc.addEventListener("contextmenu", (ev) => {
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
menu(ev, [
|
menu(ev, [
|
||||||
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeleteProjectFeature(f.basename) },
|
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeletePath(file.path) },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseDnd(raw: string): DndPayload | null {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
if (!data?.sourcePath || !data?.name) return null;
|
||||||
|
if (data.kind !== "feature" && data.kind !== "collection") return null;
|
||||||
|
return data as DndPayload;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleDropOnZone(raw: string, zone: Zone): Promise<void> {
|
||||||
|
const data = this.parseDnd(raw);
|
||||||
|
if (!data) return;
|
||||||
|
const root = zoneRootPath(this.project, zone);
|
||||||
|
const targetName = data.kind === "feature"
|
||||||
|
? (data.name.endsWith(".md") ? data.name : `${data.name}.md`)
|
||||||
|
: data.name;
|
||||||
|
const newPath = normalizePath(`${root}/${targetName}`);
|
||||||
|
if (data.sourcePath === newPath) return;
|
||||||
|
if (zone === "to-update") await ensureFolder(this.app, toUpdatePath(this.project));
|
||||||
|
await this.movePath(data.sourcePath, newPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleDropOnArea(raw: string, areaFolderPath: string): Promise<void> {
|
||||||
|
const data = this.parseDnd(raw);
|
||||||
|
if (!data) return;
|
||||||
|
if (data.kind !== "feature") return;
|
||||||
|
const file = data.name.endsWith(".md") ? data.name : `${data.name}.md`;
|
||||||
|
const newPath = normalizePath(`${areaFolderPath}/${file}`);
|
||||||
|
if (data.sourcePath === newPath) return;
|
||||||
|
await this.movePath(data.sourcePath, newPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async movePath(oldPath: string, newPath: string): Promise<void> {
|
||||||
|
if (this.app.vault.getAbstractFileByPath(newPath)) {
|
||||||
|
new Notice(`„${newPath}" existiert bereits.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await rename(this.app, oldPath, newPath);
|
||||||
|
} catch (e) {
|
||||||
|
new Notice(`Verschieben fehlgeschlagen: ${(e as Error).message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openCreateProjectFeature(): void {
|
private openCreateProjectFeature(): void {
|
||||||
const taken = listMarkdownFiles(this.app, projectPath(this.project), []).map((f) => f.basename);
|
const root = toUpdatePath(this.project);
|
||||||
|
const existing = listMarkdownFiles(this.app, root, []);
|
||||||
|
const taken = existing.map((f) => f.basename);
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
title: "Neues Feature",
|
title: "Neues Feature",
|
||||||
label: "Feature-Name",
|
label: "Feature-Name",
|
||||||
@@ -287,66 +392,14 @@ export class OverviewView extends ItemView {
|
|||||||
}).open();
|
}).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openDeleteProjectFeature(feature: string): Promise<void> {
|
|
||||||
await deleteRecursive(this.app, projectFeaturePath(this.project, feature));
|
|
||||||
await this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleFeatureDropToProject(raw: string): Promise<void> {
|
|
||||||
let data: { sourceArea: string | null; feature: string };
|
|
||||||
try {
|
|
||||||
data = JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
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<void> {
|
|
||||||
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 = data.sourceArea === null
|
|
||||||
? projectFeaturePath(this.project, data.feature)
|
|
||||||
: 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 {
|
private openCreateArea(): void {
|
||||||
const taken = listFolders(this.app, projectPath(this.project)).map((a) => a.name);
|
const ready = listFolders(this.app, projectPath(this.project))
|
||||||
|
.filter((f) => f.name !== TO_UPDATE_DIR).map((a) => a.name);
|
||||||
|
const inToUpdate = listFolders(this.app, toUpdatePath(this.project)).map((a) => a.name);
|
||||||
|
const taken = [...ready, ...inToUpdate];
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
title: "Neue Area",
|
title: "Neue Collection",
|
||||||
label: "Area-Name",
|
label: "Collection-Name",
|
||||||
cta: "Erstellen",
|
cta: "Erstellen",
|
||||||
validate: (n) => validateName(n, taken),
|
validate: (n) => validateName(n, taken),
|
||||||
onSubmit: async (name) => {
|
onSubmit: async (name) => {
|
||||||
@@ -356,54 +409,49 @@ export class OverviewView extends ItemView {
|
|||||||
}).open();
|
}).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openRenameArea(current: string, taken: string[]): void {
|
private openCreateFeatureIn(folderPath: string): void {
|
||||||
new NameModal(this.app, {
|
const taken = listMarkdownFiles(this.app, folderPath, []).map((f) => f.basename);
|
||||||
title: "Area umbenennen",
|
|
||||||
label: "Area-Name",
|
|
||||||
initial: current,
|
|
||||||
cta: "Speichern",
|
|
||||||
validate: (n) => validateName(n, taken, current),
|
|
||||||
onSubmit: async (name) => {
|
|
||||||
if (name === current) return;
|
|
||||||
await rename(
|
|
||||||
this.app,
|
|
||||||
areaPath(this.project, current),
|
|
||||||
areaPath(this.project, name),
|
|
||||||
);
|
|
||||||
await this.render();
|
|
||||||
},
|
|
||||||
}).open();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async openDeleteArea(name: string): Promise<void> {
|
|
||||||
await deleteRecursive(this.app, areaPath(this.project, name));
|
|
||||||
await this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
private openCreateFeature(area: string): void {
|
|
||||||
const taken = listMarkdownFiles(this.app, areaPath(this.project, area), []).map((f) => f.basename);
|
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
title: "Neues Feature",
|
title: "Neues Feature",
|
||||||
label: "Feature-Name",
|
label: "Feature-Name",
|
||||||
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, area, name);
|
const file = name.endsWith(".md") ? name : `${name}.md`;
|
||||||
|
const path = normalizePath(`${folderPath}/${file}`);
|
||||||
|
const existing = this.app.vault.getAbstractFileByPath(path);
|
||||||
|
if (!existing) await this.app.vault.create(path, "");
|
||||||
await this.render();
|
await this.render();
|
||||||
},
|
},
|
||||||
}).open();
|
}).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openDeleteFeature(area: string, feature: string): Promise<void> {
|
private openRenameAreaAt(folderPath: string, current: string, taken: string[]): void {
|
||||||
await deleteRecursive(this.app, featurePath(this.project, area, feature));
|
new NameModal(this.app, {
|
||||||
|
title: "Collection umbenennen",
|
||||||
|
label: "Collection-Name",
|
||||||
|
initial: current,
|
||||||
|
cta: "Speichern",
|
||||||
|
validate: (n) => validateName(n, taken, current),
|
||||||
|
onSubmit: async (name) => {
|
||||||
|
if (name === current) return;
|
||||||
|
const parent = folderPath.substring(0, folderPath.lastIndexOf("/"));
|
||||||
|
await rename(this.app, folderPath, normalizePath(`${parent}/${name}`));
|
||||||
|
await this.render();
|
||||||
|
},
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openDeletePath(path: string): Promise<void> {
|
||||||
|
await deleteRecursive(this.app, path);
|
||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openDetails(area: string): Promise<void> {
|
private async openDetails(area: string, zone: Zone): Promise<void> {
|
||||||
await this.leaf.setViewState({
|
await this.leaf.setViewState({
|
||||||
type: VIEW_TYPE_DETAILS,
|
type: VIEW_TYPE_DETAILS,
|
||||||
active: true,
|
active: true,
|
||||||
state: { project: this.project, area },
|
state: { project: this.project, area, zone },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
styles.css
14
styles.css
@@ -230,9 +230,23 @@
|
|||||||
.pk-areas-flex {
|
.pk-areas-flex {
|
||||||
column-width: 200px;
|
column-width: 200px;
|
||||||
column-gap: 5px;
|
column-gap: 5px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-zone-placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 12px;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pk-area-card {
|
.pk-area-card {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pk-zone-divider {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--background-modifier-border);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user