update
This commit is contained in:
20
main.ts
20
main.ts
@@ -8,7 +8,7 @@ import {
|
|||||||
import { ProjectView } from "./src/views/ProjectView";
|
import { ProjectView } from "./src/views/ProjectView";
|
||||||
import { ProjectDetailsView } from "./src/views/ProjectDetailsView";
|
import { ProjectDetailsView } from "./src/views/ProjectDetailsView";
|
||||||
import { CollectionView } from "./src/views/CollectionView";
|
import { CollectionView } from "./src/views/CollectionView";
|
||||||
import { parseProjectFilePath } from "./src/fs";
|
import { parseProjectFilePath, projectPathFromChain } 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 {
|
||||||
@@ -63,7 +63,7 @@ export default class ProjektkontextPlugin extends Plugin {
|
|||||||
injectMobileBreadcrumb(view, []);
|
injectMobileBreadcrumb(view, []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const loc = parseProjectFilePath(file.path);
|
const loc = parseProjectFilePath(this.app, file.path);
|
||||||
if (!loc) {
|
if (!loc) {
|
||||||
injectMobileBreadcrumb(view, []);
|
injectMobileBreadcrumb(view, []);
|
||||||
return;
|
return;
|
||||||
@@ -73,24 +73,28 @@ export default class ProjektkontextPlugin extends Plugin {
|
|||||||
label: "Projekte",
|
label: "Projekte",
|
||||||
onClick: () => void leaf.setViewState({ type: VIEW_TYPE_PROJECT_VIEW, active: true }),
|
onClick: () => void leaf.setViewState({ type: VIEW_TYPE_PROJECT_VIEW, active: true }),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
label: loc.project,
|
for (let i = 0; i < loc.projectChain.length; i++) {
|
||||||
|
const projectPath = projectPathFromChain(loc.projectChain.slice(0, i + 1));
|
||||||
|
segments.push({
|
||||||
|
label: loc.projectChain[i],
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
void leaf.setViewState({
|
void leaf.setViewState({
|
||||||
type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
|
type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
|
||||||
active: true,
|
active: true,
|
||||||
state: { project: loc.project },
|
state: { projectPath },
|
||||||
}),
|
}),
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
if (loc.collection) {
|
if (loc.collection) {
|
||||||
|
const projectPath = projectPathFromChain(loc.projectChain);
|
||||||
segments.push({
|
segments.push({
|
||||||
label: loc.collection,
|
label: loc.collection,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
void leaf.setViewState({
|
void leaf.setViewState({
|
||||||
type: VIEW_TYPE_COLLECTION_VIEW,
|
type: VIEW_TYPE_COLLECTION_VIEW,
|
||||||
active: true,
|
active: true,
|
||||||
state: { project: loc.project, collection: loc.collection, zone: loc.zone },
|
state: { projectPath, collection: loc.collection },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
export const PROJECTS_ROOT = "projects";
|
|
||||||
export const TO_UPDATE_DIR = "_to-update";
|
|
||||||
export const IDEAS_DIR = "_ideas";
|
|
||||||
|
|
||||||
export const VIEW_TYPE_PROJECT_VIEW = "projektkontext-projects";
|
export const VIEW_TYPE_PROJECT_VIEW = "projektkontext-projects";
|
||||||
export const VIEW_TYPE_PROJECT_DETAILS_VIEW = "projektkontext-overview";
|
export const VIEW_TYPE_PROJECT_DETAILS_VIEW = "projektkontext-overview";
|
||||||
export const VIEW_TYPE_COLLECTION_VIEW = "projektkontext-details";
|
export const VIEW_TYPE_COLLECTION_VIEW = "projektkontext-details";
|
||||||
@@ -9,6 +5,6 @@ export const VIEW_TYPE_COLLECTION_VIEW = "projektkontext-details";
|
|||||||
export const RIBBON_ICON = "layout-grid";
|
export const RIBBON_ICON = "layout-grid";
|
||||||
|
|
||||||
export const CORE_FILE = "_core.md";
|
export const CORE_FILE = "_core.md";
|
||||||
export const TARGET_FILE = "_target.md";
|
export const DESCRIPTION_FILE = "_description.md";
|
||||||
|
|
||||||
export const PROJECT_FILES = [CORE_FILE, TARGET_FILE] as const;
|
export const PROJECT_FILES = [CORE_FILE, DESCRIPTION_FILE] as const;
|
||||||
|
|||||||
165
src/fs.ts
165
src/fs.ts
@@ -1,42 +1,54 @@
|
|||||||
import { App, TFile, TFolder, normalizePath } from "obsidian";
|
import { App, TFile, TFolder, normalizePath } from "obsidian";
|
||||||
import { PROJECTS_ROOT, PROJECT_FILES, TO_UPDATE_DIR, IDEAS_DIR } from "./const";
|
import { PROJECT_FILES, CORE_FILE, DESCRIPTION_FILE } from "./const";
|
||||||
|
|
||||||
export type Zone = "ready" | "to-update" | "ideas";
|
function hasFile(app: App, folderPath: string, fileName: string): boolean {
|
||||||
|
const p = normalizePath(`${folderPath}/${fileName}`);
|
||||||
export function projectsPath(): string {
|
return app.vault.getAbstractFileByPath(p) instanceof TFile;
|
||||||
return PROJECTS_ROOT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectPath(name: string): string {
|
export function projectFolder(projectPath: string): string {
|
||||||
return normalizePath(`${PROJECTS_ROOT}/${name}`);
|
return normalizePath(projectPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toUpdatePath(project: string): string {
|
export function subProjectPath(parent: string, name: string): string {
|
||||||
return normalizePath(`${PROJECTS_ROOT}/${project}/${TO_UPDATE_DIR}`);
|
return normalizePath(`${parent}/${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ideasPath(project: string): string {
|
export function collectionPath(projectPath: string, collection: string): string {
|
||||||
return normalizePath(`${PROJECTS_ROOT}/${project}/${IDEAS_DIR}`);
|
return normalizePath(`${projectPath}/${collection}`);
|
||||||
}
|
|
||||||
|
|
||||||
export function zoneRootPath(project: string, zone: Zone): string {
|
|
||||||
if (zone === "ready") return projectPath(project);
|
|
||||||
if (zone === "to-update") return toUpdatePath(project);
|
|
||||||
return ideasPath(project);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function collectionPath(project: string, collection: string, zone: Zone = "ready"): string {
|
|
||||||
return normalizePath(`${zoneRootPath(project, zone)}/${collection}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function featurePath(
|
export function featurePath(
|
||||||
project: string,
|
projectPath: string,
|
||||||
collection: string,
|
collection: string,
|
||||||
feature: string,
|
feature: string,
|
||||||
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)}/${collection}/${file}`);
|
return normalizePath(`${projectPath}/${collection}/${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectFeaturePath(projectPath: string, feature: string): string {
|
||||||
|
const file = feature.endsWith(".md") ? feature : `${feature}.md`;
|
||||||
|
return normalizePath(`${projectPath}/${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasCore(app: App, folderPath: string): boolean {
|
||||||
|
return hasFile(app, folderPath, CORE_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasDescription(app: App, folderPath: string): boolean {
|
||||||
|
return hasFile(app, folderPath, DESCRIPTION_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasMarker(app: App, folderPath: string): boolean {
|
||||||
|
return hasCore(app, folderPath) || hasDescription(app, folderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCollectionFolder(app: App, folderPath: string): boolean {
|
||||||
|
const parts = normalizePath(folderPath).split("/");
|
||||||
|
if (parts.length < 2) return false;
|
||||||
|
const parent = parts.slice(0, -1).join("/");
|
||||||
|
return hasMarker(app, parent) && !hasMarker(app, folderPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureFolder(app: App, path: string): Promise<void> {
|
export async function ensureFolder(app: App, path: string): Promise<void> {
|
||||||
@@ -54,16 +66,32 @@ export async function ensureFile(app: App, path: string, content = ""): Promise<
|
|||||||
return await app.vault.create(p, content);
|
return await app.vault.create(p, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listFolders(app: App, path: string): TFolder[] {
|
function visibleFolders(folder: TFolder): TFolder[] {
|
||||||
const p = normalizePath(path);
|
|
||||||
const folder = app.vault.getAbstractFileByPath(p);
|
|
||||||
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("_") || c.name.startsWith("__"))
|
.filter((c) => !c.name.startsWith("_") || c.name.startsWith("__"))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listFolders(app: App, path: string): TFolder[] {
|
||||||
|
const folder = app.vault.getAbstractFileByPath(normalizePath(path));
|
||||||
|
if (!(folder instanceof TFolder)) return [];
|
||||||
|
return visibleFolders(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listTopLevelProjects(app: App): TFolder[] {
|
||||||
|
const root = app.vault.getRoot();
|
||||||
|
return visibleFolders(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listSubProjects(app: App, projectPath: string): TFolder[] {
|
||||||
|
return listFolders(app, projectPath).filter((f) => !isCollectionFolder(app, f.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listCollections(app: App, projectPath: string): TFolder[] {
|
||||||
|
return listFolders(app, projectPath).filter((f) => isCollectionFolder(app, f.path));
|
||||||
|
}
|
||||||
|
|
||||||
export function listMarkdownFiles(app: App, path: string, exclude: string[] = []): TFile[] {
|
export function listMarkdownFiles(app: App, path: string, exclude: string[] = []): TFile[] {
|
||||||
const p = normalizePath(path);
|
const p = normalizePath(path);
|
||||||
const folder = app.vault.getAbstractFileByPath(p);
|
const folder = app.vault.getAbstractFileByPath(p);
|
||||||
@@ -75,6 +103,10 @@ export function listMarkdownFiles(app: App, path: string, exclude: string[] = []
|
|||||||
.sort((a, b) => a.basename.localeCompare(b.basename));
|
.sort((a, b) => a.basename.localeCompare(b.basename));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listProjectFeatures(app: App, projectPath: string): TFile[] {
|
||||||
|
return listMarkdownFiles(app, projectPath, [...PROJECT_FILES]);
|
||||||
|
}
|
||||||
|
|
||||||
export async function readFile(app: App, path: string): Promise<string> {
|
export async function readFile(app: App, path: string): Promise<string> {
|
||||||
const p = normalizePath(path);
|
const p = normalizePath(path);
|
||||||
const f = app.vault.getAbstractFileByPath(p);
|
const f = app.vault.getAbstractFileByPath(p);
|
||||||
@@ -95,77 +127,66 @@ export async function rename(app: App, oldPath: string, newPath: string): Promis
|
|||||||
await app.fileManager.renameFile(f, normalizePath(newPath));
|
await app.fileManager.renameFile(f, normalizePath(newPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createProject(app: App, name: string): Promise<void> {
|
export async function createProject(app: App, projectPath: string): Promise<void> {
|
||||||
const root = projectPath(name);
|
const root = normalizePath(projectPath);
|
||||||
await ensureFolder(app, projectsPath());
|
|
||||||
await ensureFolder(app, root);
|
await ensureFolder(app, root);
|
||||||
for (const file of PROJECT_FILES) {
|
await ensureFile(app, normalizePath(`${root}/${CORE_FILE}`), "");
|
||||||
await ensureFile(app, normalizePath(`${root}/${file}`), "");
|
await ensureFile(app, normalizePath(`${root}/${DESCRIPTION_FILE}`), "");
|
||||||
}
|
|
||||||
await createCollection(app, name, "story");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCollection(
|
export async function createCollection(
|
||||||
app: App,
|
app: App,
|
||||||
project: string,
|
projectPath: string,
|
||||||
collection: string,
|
collection: string,
|
||||||
zone: Zone = "to-update",
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (zone !== "ready") await ensureFolder(app, zoneRootPath(project, zone));
|
await ensureFolder(app, collectionPath(projectPath, collection));
|
||||||
await ensureFolder(app, collectionPath(project, collection, zone));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFeature(
|
export async function createFeature(
|
||||||
app: App,
|
app: App,
|
||||||
project: string,
|
projectPath: string,
|
||||||
collection: string,
|
collection: string,
|
||||||
feature: string,
|
feature: string,
|
||||||
zone: Zone = "ready",
|
|
||||||
): Promise<TFile> {
|
): Promise<TFile> {
|
||||||
return await ensureFile(app, featurePath(project, collection, feature, zone), "");
|
return await ensureFile(app, featurePath(projectPath, collection, feature), "");
|
||||||
}
|
|
||||||
|
|
||||||
export function projectFeaturePath(
|
|
||||||
project: string,
|
|
||||||
feature: string,
|
|
||||||
zone: Zone = "ready",
|
|
||||||
): string {
|
|
||||||
const file = feature.endsWith(".md") ? feature : `${feature}.md`;
|
|
||||||
return normalizePath(`${zoneRootPath(project, zone)}/${file}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createProjectFeature(
|
export async function createProjectFeature(
|
||||||
app: App,
|
app: App,
|
||||||
project: string,
|
projectPath: string,
|
||||||
feature: string,
|
feature: string,
|
||||||
zone: Zone = "to-update",
|
|
||||||
): Promise<TFile> {
|
): Promise<TFile> {
|
||||||
if (zone !== "ready") await ensureFolder(app, zoneRootPath(project, zone));
|
return await ensureFile(app, projectFeaturePath(projectPath, feature), "");
|
||||||
return await ensureFile(app, projectFeaturePath(project, feature, zone), "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectFileLocation {
|
export interface ProjectFileLocation {
|
||||||
project: string;
|
projectChain: string[];
|
||||||
collection?: string;
|
collection?: string;
|
||||||
zone: Zone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseProjectFilePath(path: string): ProjectFileLocation | null {
|
export function parseProjectFilePath(app: App, 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.length < 2) return null;
|
||||||
if (parts.length < 3) return null;
|
const segments = parts.slice(0, -1);
|
||||||
const project = parts[1];
|
if (segments.length === 0) return null;
|
||||||
const rest = parts.slice(2);
|
|
||||||
let zone: Zone = "ready";
|
const projectChain: string[] = [segments[0]];
|
||||||
if (rest[0] === TO_UPDATE_DIR) {
|
let collection: string | undefined;
|
||||||
zone = "to-update";
|
|
||||||
rest.shift();
|
for (let i = 1; i < segments.length; i++) {
|
||||||
} else if (rest[0] === IDEAS_DIR) {
|
const prefix = segments.slice(0, i + 1).join("/");
|
||||||
zone = "ideas";
|
if (isCollectionFolder(app, prefix)) {
|
||||||
rest.shift();
|
if (i !== segments.length - 1) return null;
|
||||||
|
collection = segments[i];
|
||||||
|
} else {
|
||||||
|
projectChain.push(segments[i]);
|
||||||
}
|
}
|
||||||
if (rest.length === 1) return { project, zone };
|
}
|
||||||
if (rest.length === 2) return { project, collection: rest[0], zone };
|
|
||||||
return null;
|
return { projectChain, collection };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectPathFromChain(chain: string[]): string {
|
||||||
|
return normalizePath(chain.join("/"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
RIBBON_ICON,
|
RIBBON_ICON,
|
||||||
} from "../const";
|
} from "../const";
|
||||||
import {
|
import {
|
||||||
Zone,
|
|
||||||
collectionPath,
|
collectionPath,
|
||||||
featurePath,
|
featurePath,
|
||||||
listMarkdownFiles,
|
listMarkdownFiles,
|
||||||
@@ -14,13 +13,12 @@ import {
|
|||||||
createFeature,
|
createFeature,
|
||||||
deleteRecursive,
|
deleteRecursive,
|
||||||
} from "../fs";
|
} from "../fs";
|
||||||
import { menu, breadcrumb, emptyState, openMarkdown } from "../ui";
|
import { menu, breadcrumb, emptyState, openMarkdown, BreadcrumbSegment } from "../ui";
|
||||||
import { NameModal } from "../modals/NameModal";
|
import { NameModal } from "../modals/NameModal";
|
||||||
|
|
||||||
export interface CollectionViewState extends Record<string, unknown> {
|
export interface CollectionViewState extends Record<string, unknown> {
|
||||||
project: string;
|
projectPath: string;
|
||||||
collection: string;
|
collection: string;
|
||||||
zone?: Zone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
||||||
@@ -33,9 +31,8 @@ function validateName(name: string, taken: string[]): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CollectionView extends ItemView {
|
export class CollectionView extends ItemView {
|
||||||
project = "";
|
projectPath = "";
|
||||||
collection = "";
|
collection = "";
|
||||||
zone: Zone = "ready";
|
|
||||||
private renderToken = 0;
|
private renderToken = 0;
|
||||||
|
|
||||||
constructor(leaf: WorkspaceLeaf) {
|
constructor(leaf: WorkspaceLeaf) {
|
||||||
@@ -56,15 +53,14 @@ export class CollectionView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setState(state: CollectionViewState, result: ViewStateResult): Promise<void> {
|
async setState(state: CollectionViewState, result: ViewStateResult): Promise<void> {
|
||||||
this.project = state?.project ?? "";
|
this.projectPath = state?.projectPath ?? "";
|
||||||
this.collection = state?.collection ?? (state as { area?: string })?.area ?? "";
|
this.collection = state?.collection ?? "";
|
||||||
this.zone = state?.zone ?? "ready";
|
|
||||||
await super.setState(state, result);
|
await super.setState(state, result);
|
||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(): CollectionViewState {
|
getState(): CollectionViewState {
|
||||||
return { project: this.project, collection: this.collection, zone: this.zone };
|
return { projectPath: this.projectPath, collection: this.collection };
|
||||||
}
|
}
|
||||||
|
|
||||||
async onOpen(): Promise<void> {
|
async onOpen(): Promise<void> {
|
||||||
@@ -87,7 +83,7 @@ export class CollectionView 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.collection) {
|
if (!this.projectPath || !this.collection) {
|
||||||
root.empty();
|
root.empty();
|
||||||
root.addClass("pk-root");
|
root.addClass("pk-root");
|
||||||
emptyState(root, "Keine Collection ausgewählt.");
|
emptyState(root, "Keine Collection ausgewählt.");
|
||||||
@@ -96,7 +92,7 @@ export class CollectionView extends ItemView {
|
|||||||
|
|
||||||
const features = listMarkdownFiles(
|
const features = listMarkdownFiles(
|
||||||
this.app,
|
this.app,
|
||||||
collectionPath(this.project, this.collection, this.zone),
|
collectionPath(this.projectPath, this.collection),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
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)));
|
||||||
@@ -105,11 +101,7 @@ export class CollectionView extends ItemView {
|
|||||||
root.empty();
|
root.empty();
|
||||||
root.addClass("pk-root");
|
root.addClass("pk-root");
|
||||||
|
|
||||||
breadcrumb(root, [
|
this.renderBreadcrumb(root);
|
||||||
{ label: "Projekte", onClick: () => this.openProjectView() },
|
|
||||||
{ label: this.project, onClick: () => this.openProjectDetails() },
|
|
||||||
{ label: this.collection },
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (features.length === 0) {
|
if (features.length === 0) {
|
||||||
emptyState(root, "Keine Features");
|
emptyState(root, "Keine Features");
|
||||||
@@ -141,10 +133,26 @@ export class CollectionView extends ItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderBreadcrumb(parent: HTMLElement): void {
|
||||||
|
const chain = this.projectPath.split("/");
|
||||||
|
const segments: BreadcrumbSegment[] = [
|
||||||
|
{ label: "Projekte", onClick: () => this.openProjectView() },
|
||||||
|
];
|
||||||
|
for (let i = 0; i < chain.length; i++) {
|
||||||
|
const path = chain.slice(0, i + 1).join("/");
|
||||||
|
segments.push({
|
||||||
|
label: chain[i],
|
||||||
|
onClick: () => this.openProjectDetails(path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
segments.push({ label: this.collection });
|
||||||
|
breadcrumb(parent, segments);
|
||||||
|
}
|
||||||
|
|
||||||
private openCreate(): void {
|
private openCreate(): void {
|
||||||
const taken = listMarkdownFiles(
|
const taken = listMarkdownFiles(
|
||||||
this.app,
|
this.app,
|
||||||
collectionPath(this.project, this.collection, this.zone),
|
collectionPath(this.projectPath, this.collection),
|
||||||
[],
|
[],
|
||||||
).map((f) => f.basename);
|
).map((f) => f.basename);
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
@@ -153,7 +161,7 @@ export class CollectionView 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.collection, name, this.zone);
|
await createFeature(this.app, this.projectPath, this.collection, name);
|
||||||
await this.render();
|
await this.render();
|
||||||
},
|
},
|
||||||
}).open();
|
}).open();
|
||||||
@@ -162,16 +170,16 @@ export class CollectionView extends ItemView {
|
|||||||
private async openDelete(feature: string): Promise<void> {
|
private async openDelete(feature: string): Promise<void> {
|
||||||
await deleteRecursive(
|
await deleteRecursive(
|
||||||
this.app,
|
this.app,
|
||||||
featurePath(this.project, this.collection, feature, this.zone),
|
featurePath(this.projectPath, this.collection, feature),
|
||||||
);
|
);
|
||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openProjectDetails(): Promise<void> {
|
private async openProjectDetails(projectPath: string): Promise<void> {
|
||||||
await this.leaf.setViewState({
|
await this.leaf.setViewState({
|
||||||
type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
|
type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
|
||||||
active: true,
|
active: true,
|
||||||
state: { project: this.project },
|
state: { projectPath },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,35 +13,36 @@ import {
|
|||||||
VIEW_TYPE_COLLECTION_VIEW,
|
VIEW_TYPE_COLLECTION_VIEW,
|
||||||
VIEW_TYPE_PROJECT_VIEW,
|
VIEW_TYPE_PROJECT_VIEW,
|
||||||
RIBBON_ICON,
|
RIBBON_ICON,
|
||||||
PROJECT_FILES,
|
|
||||||
CORE_FILE,
|
CORE_FILE,
|
||||||
TARGET_FILE,
|
DESCRIPTION_FILE,
|
||||||
} from "../const";
|
} from "../const";
|
||||||
import {
|
import {
|
||||||
Zone,
|
projectFolder,
|
||||||
projectPath,
|
subProjectPath,
|
||||||
zoneRootPath,
|
collectionPath,
|
||||||
listFolders,
|
listSubProjects,
|
||||||
|
listCollections,
|
||||||
|
listProjectFeatures,
|
||||||
listMarkdownFiles,
|
listMarkdownFiles,
|
||||||
readFile,
|
readFile,
|
||||||
rename,
|
rename,
|
||||||
deleteRecursive,
|
deleteRecursive,
|
||||||
ensureFolder,
|
|
||||||
createCollection,
|
createCollection,
|
||||||
|
createProject,
|
||||||
createProjectFeature,
|
createProjectFeature,
|
||||||
} from "../fs";
|
} from "../fs";
|
||||||
import { menu, breadcrumb, emptyState, openMarkdown } from "../ui";
|
import { menu, breadcrumb, emptyState, openMarkdown, BreadcrumbSegment } from "../ui";
|
||||||
import { NameModal } from "../modals/NameModal";
|
import { NameModal } from "../modals/NameModal";
|
||||||
|
|
||||||
export interface ProjectDetailsState extends Record<string, unknown> {
|
export interface ProjectDetailsState extends Record<string, unknown> {
|
||||||
project: string;
|
projectPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
const NAME_RX = /^[^\\/:*?"<>|]+$/;
|
||||||
const PK_DND_MIME = "application/x-pk-item";
|
const PK_DND_MIME = "application/x-pk-item";
|
||||||
|
|
||||||
interface DndPayload {
|
interface DndPayload {
|
||||||
kind: "feature" | "collection";
|
kind: "feature";
|
||||||
sourcePath: string;
|
sourcePath: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,7 @@ function validateName(name: string, taken: string[], current?: string): string |
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectDetailsView extends ItemView {
|
export class ProjectDetailsView extends ItemView {
|
||||||
project = "";
|
projectPath = "";
|
||||||
private renderToken = 0;
|
private renderToken = 0;
|
||||||
|
|
||||||
constructor(leaf: WorkspaceLeaf) {
|
constructor(leaf: WorkspaceLeaf) {
|
||||||
@@ -67,7 +68,9 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDisplayText(): string {
|
getDisplayText(): string {
|
||||||
return this.project ? `Projekt: ${this.project}` : "Projekt";
|
if (!this.projectPath) return "Projekt";
|
||||||
|
const segs = this.projectPath.split("/");
|
||||||
|
return `Projekt: ${segs[segs.length - 1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getIcon(): string {
|
getIcon(): string {
|
||||||
@@ -75,13 +78,13 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setState(state: ProjectDetailsState, result: ViewStateResult): Promise<void> {
|
async setState(state: ProjectDetailsState, result: ViewStateResult): Promise<void> {
|
||||||
this.project = state?.project ?? "";
|
this.projectPath = state?.projectPath ?? "";
|
||||||
await super.setState(state, result);
|
await super.setState(state, result);
|
||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(): ProjectDetailsState {
|
getState(): ProjectDetailsState {
|
||||||
return { project: this.project };
|
return { projectPath: this.projectPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
async onOpen(): Promise<void> {
|
async onOpen(): Promise<void> {
|
||||||
@@ -90,7 +93,7 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
this.registerEvent(this.app.vault.on("delete", () => this.render()));
|
this.registerEvent(this.app.vault.on("delete", () => this.render()));
|
||||||
this.registerEvent(this.app.vault.on("rename", () => this.render()));
|
this.registerEvent(this.app.vault.on("rename", () => this.render()));
|
||||||
this.registerEvent(this.app.vault.on("modify", (f) => {
|
this.registerEvent(this.app.vault.on("modify", (f) => {
|
||||||
if (f.path.startsWith(projectPath(this.project) + "/")) this.render();
|
if (this.projectPath && f.path.startsWith(this.projectPath + "/")) this.render();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,34 +103,55 @@ export class ProjectDetailsView 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) {
|
if (!this.projectPath) {
|
||||||
root.empty();
|
root.empty();
|
||||||
root.addClass("pk-root");
|
root.addClass("pk-root");
|
||||||
emptyState(root, "Kein Projekt ausgewählt.");
|
emptyState(root, "Kein Projekt ausgewählt.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projRoot = projectPath(this.project);
|
const projRoot = projectFolder(this.projectPath);
|
||||||
const corePath = normalizePath(`${projRoot}/${CORE_FILE}`);
|
const corePath = normalizePath(`${projRoot}/${CORE_FILE}`);
|
||||||
const targetPath = normalizePath(`${projRoot}/${TARGET_FILE}`);
|
const descPath = normalizePath(`${projRoot}/${DESCRIPTION_FILE}`);
|
||||||
const [core, target] = await Promise.all([
|
const subProjectsList = listSubProjects(this.app, this.projectPath);
|
||||||
|
const subCorePaths = subProjectsList.map((s) => normalizePath(`${s.path}/${CORE_FILE}`));
|
||||||
|
const [core, desc, ...subCores] = await Promise.all([
|
||||||
readFile(this.app, corePath),
|
readFile(this.app, corePath),
|
||||||
readFile(this.app, targetPath),
|
readFile(this.app, descPath),
|
||||||
|
...subCorePaths.map((p) => readFile(this.app, p)),
|
||||||
]);
|
]);
|
||||||
if (token !== this.renderToken) return;
|
if (token !== this.renderToken) return;
|
||||||
|
|
||||||
root.empty();
|
root.empty();
|
||||||
root.addClass("pk-root");
|
root.addClass("pk-root");
|
||||||
|
|
||||||
breadcrumb(root, [
|
this.renderBreadcrumb(root);
|
||||||
{ label: "Projekte", onClick: () => this.openProjectView() },
|
|
||||||
{ label: this.project },
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
const infoCards: Array<{ content: string; path: string }> = [];
|
||||||
|
if (core.trim()) infoCards.push({ content: core, path: corePath });
|
||||||
|
if (desc.trim()) infoCards.push({ content: desc, path: descPath });
|
||||||
|
if (infoCards.length > 0) {
|
||||||
const info = root.createDiv({ cls: "pk-info-grid" });
|
const info = root.createDiv({ cls: "pk-info-grid" });
|
||||||
this.renderInfoCard(info, core, corePath);
|
for (const ic of infoCards) this.renderInfoCard(info, ic.content, ic.path);
|
||||||
this.renderInfoCard(info, target, targetPath);
|
}
|
||||||
this.renderCollections(root);
|
|
||||||
|
this.renderChildren(root, subProjectsList, subCorePaths, subCores);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderBreadcrumb(parent: HTMLElement): void {
|
||||||
|
const chain = this.projectPath.split("/");
|
||||||
|
const segments: BreadcrumbSegment[] = [
|
||||||
|
{ label: "Projekte", onClick: () => this.openProjectView() },
|
||||||
|
];
|
||||||
|
for (let i = 0; i < chain.length; i++) {
|
||||||
|
const path = chain.slice(0, i + 1).join("/");
|
||||||
|
const isLast = i === chain.length - 1;
|
||||||
|
segments.push({
|
||||||
|
label: chain[i],
|
||||||
|
onClick: isLast ? undefined : () => this.openProjectDetails(path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
breadcrumb(parent, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderInfoCard(parent: HTMLElement, content: string, path: string): void {
|
private renderInfoCard(parent: HTMLElement, content: string, path: string): void {
|
||||||
@@ -136,60 +160,25 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
attr: { role: "button", tabindex: "0" },
|
attr: { role: "button", tabindex: "0" },
|
||||||
});
|
});
|
||||||
btn.addEventListener("click", () => openMarkdown(this.app, path, this.leaf));
|
btn.addEventListener("click", () => openMarkdown(this.app, path, this.leaf));
|
||||||
if (content.trim()) {
|
|
||||||
void MarkdownRenderer.render(this.app, content, btn, path, this);
|
void MarkdownRenderer.render(this.app, content, btn, path, this);
|
||||||
} else {
|
|
||||||
btn.setText("(leer)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderCollections(parent: HTMLElement): void {
|
private renderChildren(
|
||||||
const section = parent.createDiv({ cls: "pk-areas-section" });
|
|
||||||
|
|
||||||
const ready = this.collectZoneItems("ready");
|
|
||||||
const toUpdate = this.collectZoneItems("to-update");
|
|
||||||
const ideas = this.collectZoneItems("ideas");
|
|
||||||
|
|
||||||
this.renderZone(section, "ready", ready);
|
|
||||||
section.createEl("hr", { cls: "pk-zone-divider" });
|
|
||||||
this.renderZone(section, "to-update", toUpdate);
|
|
||||||
section.createEl("hr", { cls: "pk-zone-divider" });
|
|
||||||
this.renderZone(section, "ideas", ideas);
|
|
||||||
}
|
|
||||||
|
|
||||||
private collectZoneItems(zone: Zone): { collections: TFolder[]; features: TFile[] } {
|
|
||||||
const root = zoneRootPath(this.project, zone);
|
|
||||||
const folders = listFolders(this.app, root);
|
|
||||||
const features = listMarkdownFiles(
|
|
||||||
this.app,
|
|
||||||
root,
|
|
||||||
zone === "ready" ? [...PROJECT_FILES] : [],
|
|
||||||
);
|
|
||||||
return { collections: folders, features };
|
|
||||||
}
|
|
||||||
|
|
||||||
private zoneEmptyText(zone: Zone): string {
|
|
||||||
if (zone === "ready") return "Keine fertigen Features";
|
|
||||||
if (zone === "to-update") return "Keine unfertigen Features";
|
|
||||||
return "Keine neuen Features";
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderZone(
|
|
||||||
parent: HTMLElement,
|
parent: HTMLElement,
|
||||||
zone: Zone,
|
subProjects: TFolder[],
|
||||||
items: { collections: TFolder[]; features: TFile[] },
|
subCorePaths: string[],
|
||||||
|
subCores: string[],
|
||||||
): void {
|
): void {
|
||||||
const flex = parent.createDiv({
|
const collections = listCollections(this.app, this.projectPath);
|
||||||
cls: `pk-areas-flex pk-zone-${zone}`,
|
const features = listProjectFeatures(this.app, this.projectPath);
|
||||||
attr: { "data-zone": zone },
|
|
||||||
});
|
const section = parent.createDiv({ cls: "pk-areas-section" });
|
||||||
|
const flex = section.createDiv({ cls: "pk-areas-flex" });
|
||||||
|
|
||||||
flex.addEventListener("contextmenu", (ev) => {
|
flex.addEventListener("contextmenu", (ev) => {
|
||||||
if (ev.defaultPrevented) return;
|
if (ev.defaultPrevented) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
menu(ev, [
|
this.openProjectLevelMenu(ev);
|
||||||
{ title: "Neue Collection", icon: "plus", onClick: () => this.openCreateCollection(zone) },
|
|
||||||
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature(zone) },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
flex.addEventListener("dragover", (ev) => {
|
flex.addEventListener("dragover", (ev) => {
|
||||||
if (!ev.dataTransfer?.types.includes(PK_DND_MIME)) return;
|
if (!ev.dataTransfer?.types.includes(PK_DND_MIME)) return;
|
||||||
@@ -207,56 +196,88 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
const raw = ev.dataTransfer?.getData(PK_DND_MIME);
|
const raw = ev.dataTransfer?.getData(PK_DND_MIME);
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
await this.handleDropOnZone(raw, zone);
|
await this.handleDropOnProjectRoot(raw);
|
||||||
});
|
});
|
||||||
|
|
||||||
const takenCollections = items.collections.map((a) => a.name);
|
const takenChildren = [
|
||||||
for (const collection of items.collections) {
|
...subProjects.map((f) => f.name),
|
||||||
this.renderCollectionCard(flex, zone, collection, takenCollections);
|
...collections.map((f) => f.name),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < subProjects.length; i++) {
|
||||||
|
this.renderSubProjectCard(flex, subProjects[i], takenChildren, subCores[i] ?? "", subCorePaths[i] ?? "");
|
||||||
}
|
}
|
||||||
for (const f of items.features) {
|
for (const col of collections) {
|
||||||
this.renderProjectFeatureCard(flex, zone, f);
|
this.renderCollectionCard(flex, col, takenChildren);
|
||||||
}
|
}
|
||||||
if (items.collections.length === 0 && items.features.length === 0) {
|
for (const f of features) {
|
||||||
flex.createDiv({ cls: "pk-zone-placeholder", text: this.zoneEmptyText(zone) });
|
this.renderProjectFeatureCard(flex, f);
|
||||||
}
|
}
|
||||||
|
if (subProjects.length === 0 && collections.length === 0 && features.length === 0) {
|
||||||
|
flex.createDiv({ cls: "pk-zone-placeholder", text: "Keine Inhalte. Rechtsklick fügt Sub-Projekt, Collection oder Feature hinzu." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private openProjectLevelMenu(ev: MouseEvent): void {
|
||||||
|
menu(ev, [
|
||||||
|
{ title: "Neues Sub-Projekt", icon: "plus", onClick: () => this.openCreateSubProject() },
|
||||||
|
{ title: "Neue Collection", icon: "plus", onClick: () => this.openCreateCollection() },
|
||||||
|
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature() },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSubProjectCard(
|
||||||
|
parent: HTMLElement,
|
||||||
|
sub: TFolder,
|
||||||
|
takenChildren: string[],
|
||||||
|
coreContent: string,
|
||||||
|
corePath: string,
|
||||||
|
): void {
|
||||||
|
const card = parent.createDiv({
|
||||||
|
cls: "pk-btn-card pk-area-card",
|
||||||
|
attr: { role: "button", tabindex: "0" },
|
||||||
|
});
|
||||||
|
card.createEl("strong", { text: sub.name });
|
||||||
|
const trimmed = coreContent.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
const body = card.createDiv({ cls: "pk-project-core" });
|
||||||
|
void MarkdownRenderer.render(this.app, coreContent, body, corePath, this);
|
||||||
|
}
|
||||||
|
card.addEventListener("click", () => this.openProjectDetails(sub.path));
|
||||||
|
card.addEventListener("contextmenu", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
menu(ev, [
|
||||||
|
{ title: "Neues Sub-Projekt", icon: "plus", onClick: () => this.openCreateSubProject() },
|
||||||
|
{ title: "Sub-Projekt umbenennen", icon: "pencil", onClick: () => this.openRenameChildFolder(sub, takenChildren) },
|
||||||
|
{ title: "Sub-Projekt löschen", icon: "trash", onClick: () => this.openDeletePath(sub.path) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderCollectionCard(
|
private renderCollectionCard(
|
||||||
parent: HTMLElement,
|
parent: HTMLElement,
|
||||||
zone: Zone,
|
|
||||||
collection: TFolder,
|
collection: TFolder,
|
||||||
takenCollections: string[],
|
takenChildren: string[],
|
||||||
): void {
|
): void {
|
||||||
const folderPath = collection.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" },
|
||||||
});
|
});
|
||||||
card.createEl("strong", { text: collection.name });
|
card.createEl("strong", { text: collection.name });
|
||||||
card.addEventListener("click", () => this.openCollectionDetails(collection.name, zone));
|
card.addEventListener("click", () => this.openCollectionDetails(collection.name));
|
||||||
card.addEventListener("contextmenu", (ev) => {
|
card.addEventListener("contextmenu", (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
menu(ev, [
|
menu(ev, [
|
||||||
{ title: "Neue Collection", icon: "plus", onClick: () => this.openCreateCollection(zone) },
|
{ title: "Neue Collection", icon: "plus", onClick: () => this.openCreateCollection() },
|
||||||
{ title: "Collection umbenennen", icon: "pencil", onClick: () => this.openRenameCollectionAt(folderPath, collection.name, takenCollections) },
|
{ title: "Collection umbenennen", icon: "pencil", onClick: () => this.openRenameChildFolder(collection, takenChildren) },
|
||||||
{ title: "Collection löschen", icon: "trash", onClick: () => this.openDeletePath(folderPath) },
|
{ title: "Collection löschen", icon: "trash", onClick: () => this.openDeletePath(folderPath) },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
card.addEventListener("dragstart", (ev) => {
|
|
||||||
if (!ev.dataTransfer) return;
|
|
||||||
ev.dataTransfer.setData(PK_DND_MIME, JSON.stringify({
|
|
||||||
kind: "collection",
|
|
||||||
sourcePath: folderPath,
|
|
||||||
name: collection.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) => {
|
card.addEventListener("dragover", (ev) => {
|
||||||
if (!ev.dataTransfer?.types.includes(PK_DND_MIME)) return;
|
if (!ev.dataTransfer?.types.includes(PK_DND_MIME)) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@@ -312,7 +333,7 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderProjectFeatureCard(parent: HTMLElement, zone: Zone, file: TFile): void {
|
private renderProjectFeatureCard(parent: HTMLElement, 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" },
|
||||||
@@ -334,7 +355,7 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
menu(ev, [
|
menu(ev, [
|
||||||
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature(zone) },
|
{ title: "Neues Feature", icon: "plus", onClick: () => this.openCreateProjectFeature() },
|
||||||
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeletePath(file.path) },
|
{ title: "Feature löschen", icon: "trash", onClick: () => this.openDeletePath(file.path) },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -344,30 +365,25 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(raw);
|
const data = JSON.parse(raw);
|
||||||
if (!data?.sourcePath || !data?.name) return null;
|
if (!data?.sourcePath || !data?.name) return null;
|
||||||
if (data.kind !== "feature" && data.kind !== "collection") return null;
|
if (data.kind !== "feature") return null;
|
||||||
return data as DndPayload;
|
return data as DndPayload;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleDropOnZone(raw: string, zone: Zone): Promise<void> {
|
private async handleDropOnProjectRoot(raw: string): Promise<void> {
|
||||||
const data = this.parseDnd(raw);
|
const data = this.parseDnd(raw);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const root = zoneRootPath(this.project, zone);
|
const file = data.name.endsWith(".md") ? data.name : `${data.name}.md`;
|
||||||
const targetName = data.kind === "feature"
|
const newPath = normalizePath(`${this.projectPath}/${file}`);
|
||||||
? (data.name.endsWith(".md") ? data.name : `${data.name}.md`)
|
|
||||||
: data.name;
|
|
||||||
const newPath = normalizePath(`${root}/${targetName}`);
|
|
||||||
if (data.sourcePath === newPath) return;
|
if (data.sourcePath === newPath) return;
|
||||||
if (zone !== "ready") await ensureFolder(this.app, root);
|
|
||||||
await this.movePath(data.sourcePath, newPath);
|
await this.movePath(data.sourcePath, newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleDropOnCollection(raw: string, collectionFolderPath: 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;
|
|
||||||
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(`${collectionFolderPath}/${file}`);
|
const newPath = normalizePath(`${collectionFolderPath}/${file}`);
|
||||||
if (data.sourcePath === newPath) return;
|
if (data.sourcePath === newPath) return;
|
||||||
@@ -388,38 +404,49 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openCreateProjectFeature(zone: Zone = "to-update"): void {
|
private openCreateProjectFeature(): void {
|
||||||
const root = zoneRootPath(this.project, zone);
|
const taken = listProjectFeatures(this.app, this.projectPath).map((f) => f.basename);
|
||||||
const existing = listMarkdownFiles(
|
|
||||||
this.app,
|
|
||||||
root,
|
|
||||||
zone === "ready" ? [...PROJECT_FILES] : [],
|
|
||||||
);
|
|
||||||
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",
|
||||||
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, zone);
|
await createProjectFeature(this.app, this.projectPath, name);
|
||||||
await this.render();
|
await this.render();
|
||||||
},
|
},
|
||||||
}).open();
|
}).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openCreateCollection(zone: Zone = "to-update"): void {
|
private openCreateCollection(): void {
|
||||||
const allZones: Zone[] = ["ready", "to-update", "ideas"];
|
const taken = [
|
||||||
const taken = allZones.flatMap((z) =>
|
...listCollections(this.app, this.projectPath).map((f) => f.name),
|
||||||
listFolders(this.app, zoneRootPath(this.project, z)).map((a) => a.name),
|
...listSubProjects(this.app, this.projectPath).map((f) => f.name),
|
||||||
);
|
];
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
title: "Neue Collection",
|
title: "Neue Collection",
|
||||||
label: "Collection-Name",
|
label: "Collection-Name",
|
||||||
cta: "Erstellen",
|
cta: "Erstellen",
|
||||||
validate: (n) => validateName(n, taken),
|
validate: (n) => validateName(n, taken),
|
||||||
onSubmit: async (name) => {
|
onSubmit: async (name) => {
|
||||||
await createCollection(this.app, this.project, name, zone);
|
await createCollection(this.app, this.projectPath, name);
|
||||||
|
await this.render();
|
||||||
|
},
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
private openCreateSubProject(): void {
|
||||||
|
const taken = [
|
||||||
|
...listSubProjects(this.app, this.projectPath).map((f) => f.name),
|
||||||
|
...listCollections(this.app, this.projectPath).map((f) => f.name),
|
||||||
|
];
|
||||||
|
new NameModal(this.app, {
|
||||||
|
title: "Neues Sub-Projekt",
|
||||||
|
label: "Projektname",
|
||||||
|
cta: "Erstellen",
|
||||||
|
validate: (n) => validateName(n, taken),
|
||||||
|
onSubmit: async (name) => {
|
||||||
|
await createProject(this.app, subProjectPath(this.projectPath, name));
|
||||||
await this.render();
|
await this.render();
|
||||||
},
|
},
|
||||||
}).open();
|
}).open();
|
||||||
@@ -442,17 +469,19 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
}).open();
|
}).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openRenameCollectionAt(folderPath: string, current: string, taken: string[]): void {
|
private openRenameChildFolder(folder: TFolder, taken: string[]): void {
|
||||||
|
const current = folder.name;
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
title: "Collection umbenennen",
|
title: "Umbenennen",
|
||||||
label: "Collection-Name",
|
label: "Name",
|
||||||
initial: current,
|
initial: current,
|
||||||
cta: "Speichern",
|
cta: "Speichern",
|
||||||
validate: (n) => validateName(n, taken, current),
|
validate: (n) => validateName(n, taken, current),
|
||||||
onSubmit: async (name) => {
|
onSubmit: async (name) => {
|
||||||
if (name === current) return;
|
if (name === current) return;
|
||||||
const parent = folderPath.substring(0, folderPath.lastIndexOf("/"));
|
const parent = folder.path.substring(0, folder.path.lastIndexOf("/"));
|
||||||
await rename(this.app, folderPath, normalizePath(`${parent}/${name}`));
|
const newPath = parent ? normalizePath(`${parent}/${name}`) : normalizePath(name);
|
||||||
|
await rename(this.app, folder.path, newPath);
|
||||||
await this.render();
|
await this.render();
|
||||||
},
|
},
|
||||||
}).open();
|
}).open();
|
||||||
@@ -463,11 +492,19 @@ export class ProjectDetailsView extends ItemView {
|
|||||||
await this.render();
|
await this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openCollectionDetails(collection: string, zone: Zone): Promise<void> {
|
private async openCollectionDetails(collection: string): Promise<void> {
|
||||||
await this.leaf.setViewState({
|
await this.leaf.setViewState({
|
||||||
type: VIEW_TYPE_COLLECTION_VIEW,
|
type: VIEW_TYPE_COLLECTION_VIEW,
|
||||||
active: true,
|
active: true,
|
||||||
state: { project: this.project, collection, zone },
|
state: { projectPath: this.projectPath, collection },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openProjectDetails(projectPath: string): Promise<void> {
|
||||||
|
await this.leaf.setViewState({
|
||||||
|
type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
|
||||||
|
active: true,
|
||||||
|
state: { projectPath },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import {
|
|||||||
CORE_FILE,
|
CORE_FILE,
|
||||||
} from "../const";
|
} from "../const";
|
||||||
import {
|
import {
|
||||||
projectsPath,
|
listTopLevelProjects,
|
||||||
projectPath,
|
|
||||||
listFolders,
|
|
||||||
ensureFolder,
|
|
||||||
createProject,
|
createProject,
|
||||||
rename,
|
rename,
|
||||||
deleteRecursive,
|
deleteRecursive,
|
||||||
@@ -54,15 +51,12 @@ export class ProjectView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onOpen(): Promise<void> {
|
async onOpen(): Promise<void> {
|
||||||
await ensureFolder(this.app, projectsPath());
|
|
||||||
await this.render();
|
await this.render();
|
||||||
this.registerEvent(this.app.vault.on("create", () => this.render()));
|
this.registerEvent(this.app.vault.on("create", () => this.render()));
|
||||||
this.registerEvent(this.app.vault.on("delete", () => this.render()));
|
this.registerEvent(this.app.vault.on("delete", () => this.render()));
|
||||||
this.registerEvent(this.app.vault.on("rename", () => this.render()));
|
this.registerEvent(this.app.vault.on("rename", () => this.render()));
|
||||||
this.registerEvent(this.app.vault.on("modify", (f) => {
|
this.registerEvent(this.app.vault.on("modify", (f) => {
|
||||||
if (f.path.endsWith("/" + CORE_FILE) && f.path.startsWith(projectsPath() + "/")) {
|
if (f.path.endsWith("/" + CORE_FILE)) this.render();
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
this.registerDomEvent(this.containerEl, "contextmenu", (ev) => {
|
this.registerDomEvent(this.containerEl, "contextmenu", (ev) => {
|
||||||
if (ev.defaultPrevented) return;
|
if (ev.defaultPrevented) return;
|
||||||
@@ -79,7 +73,7 @@ export class ProjectView 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;
|
||||||
|
|
||||||
const projects = listFolders(this.app, projectsPath());
|
const projects = listTopLevelProjects(this.app);
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
if (token !== this.renderToken) return;
|
if (token !== this.renderToken) return;
|
||||||
root.empty();
|
root.empty();
|
||||||
@@ -90,7 +84,7 @@ export class ProjectView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const taken = projects.map((p) => p.name);
|
const taken = projects.map((p) => p.name);
|
||||||
const corePaths = projects.map((p) => normalizePath(`${projectPath(p.name)}/${CORE_FILE}`));
|
const corePaths = projects.map((p) => normalizePath(`${p.path}/${CORE_FILE}`));
|
||||||
const cores = await Promise.all(corePaths.map((p) => readFile(this.app, p)));
|
const cores = await Promise.all(corePaths.map((p) => readFile(this.app, p)));
|
||||||
if (token !== this.renderToken) return;
|
if (token !== this.renderToken) return;
|
||||||
|
|
||||||
@@ -124,7 +118,7 @@ export class ProjectView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private openCreate(): void {
|
private openCreate(): void {
|
||||||
const taken = listFolders(this.app, projectsPath()).map((p) => p.name);
|
const taken = listTopLevelProjects(this.app).map((p) => p.name);
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
title: "Neues Projekt",
|
title: "Neues Projekt",
|
||||||
label: "Projektname",
|
label: "Projektname",
|
||||||
@@ -152,7 +146,7 @@ export class ProjectView extends ItemView {
|
|||||||
onSubmit: async (name) => {
|
onSubmit: async (name) => {
|
||||||
if (name === current) return;
|
if (name === current) return;
|
||||||
try {
|
try {
|
||||||
await rename(this.app, projectPath(current), projectPath(name));
|
await rename(this.app, current, name);
|
||||||
await this.render();
|
await this.render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(`Fehler: ${(e as Error).message}`);
|
new Notice(`Fehler: ${(e as Error).message}`);
|
||||||
@@ -163,7 +157,7 @@ export class ProjectView extends ItemView {
|
|||||||
|
|
||||||
private async openDelete(name: string): Promise<void> {
|
private async openDelete(name: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await deleteRecursive(this.app, projectPath(name));
|
await deleteRecursive(this.app, name);
|
||||||
await this.render();
|
await this.render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(`Fehler: ${(e as Error).message}`);
|
new Notice(`Fehler: ${(e as Error).message}`);
|
||||||
@@ -174,7 +168,7 @@ export class ProjectView extends ItemView {
|
|||||||
await this.leaf.setViewState({
|
await this.leaf.setViewState({
|
||||||
type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
|
type: VIEW_TYPE_PROJECT_DETAILS_VIEW,
|
||||||
active: true,
|
active: true,
|
||||||
state: { project: name },
|
state: { projectPath: name },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,8 +257,6 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pk-zone-divider {
|
.pk-sub-project-card {
|
||||||
border: 0;
|
border-left: 3px solid var(--text-accent);
|
||||||
border-top: 1px solid var(--background-modifier-border);
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user