mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(projects): add context menu with edit and delete functionality
- Add right-click context menu to project items with Edit/Delete options - Create EditProjectModal for editing project display names and colors - Support separate display names from internal project names - Implement project deletion with confirmation dialog - Disable menu options for task-derived (non-custom) projects - Add forceRefresh parameter to AutoComplete cache
This commit is contained in:
parent
adc7e3e8e0
commit
743b9ca866
4 changed files with 714 additions and 22 deletions
|
|
@ -512,7 +512,8 @@ export interface ProjectNamingStrategy {
|
|||
/** Custom project definition for V2 */
|
||||
export interface CustomProject {
|
||||
id: string;
|
||||
name: string;
|
||||
name: string; // Internal name with dashes for metadata
|
||||
displayName?: string; // Original name with spaces for display
|
||||
color: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
|
|
|
|||
|
|
@ -20,9 +20,16 @@ let globalCache: GlobalAutoCompleteCache | null = null;
|
|||
const CACHE_DURATION = 30000; // 30 seconds
|
||||
|
||||
// Helper function to get cached data
|
||||
async function getCachedData(plugin: TaskProgressBarPlugin): Promise<GlobalAutoCompleteCache> {
|
||||
async function getCachedData(
|
||||
plugin: TaskProgressBarPlugin,
|
||||
forceRefresh: boolean = false
|
||||
): Promise<GlobalAutoCompleteCache> {
|
||||
const now = Date.now();
|
||||
|
||||
if (forceRefresh) {
|
||||
globalCache = null;
|
||||
}
|
||||
|
||||
if (!globalCache || now - globalCache.lastUpdate > CACHE_DURATION) {
|
||||
// Fetch fresh data
|
||||
const tags = Object.keys(plugin.app.metadataCache.getTags() || {}).map(
|
||||
|
|
@ -32,7 +39,7 @@ async function getCachedData(plugin: TaskProgressBarPlugin): Promise<GlobalAutoC
|
|||
// Get projects and contexts from dataflow using the new convenience method
|
||||
let projects: string[] = [];
|
||||
let contexts: string[] = [];
|
||||
|
||||
|
||||
if (plugin.dataflowOrchestrator) {
|
||||
try {
|
||||
const queryAPI = plugin.dataflowOrchestrator.getQueryAPI();
|
||||
|
|
@ -40,10 +47,39 @@ async function getCachedData(plugin: TaskProgressBarPlugin): Promise<GlobalAutoC
|
|||
projects = data.projects;
|
||||
contexts = data.contexts;
|
||||
} catch (error) {
|
||||
console.warn("Failed to get projects/contexts from dataflow:", error);
|
||||
console.warn(
|
||||
"Failed to get projects/contexts from dataflow:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge settings-defined projects so newly added ones appear immediately
|
||||
try {
|
||||
const cfg = plugin.settings?.projectConfig;
|
||||
if (cfg) {
|
||||
// Custom projects (V2)
|
||||
const custom = (cfg.customProjects || [])
|
||||
.map((p) => p.name)
|
||||
.filter(Boolean);
|
||||
projects.push(...custom);
|
||||
// Path mappings
|
||||
const mapped = (cfg.pathMappings || [])
|
||||
.filter((m) => (m as any).enabled !== false)
|
||||
.map((m) => m.projectName)
|
||||
.filter(Boolean);
|
||||
projects.push(...mapped);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to merge settings-defined projects:", e);
|
||||
}
|
||||
|
||||
// Deduplicate and sort
|
||||
const uniq = (arr: string[]) =>
|
||||
Array.from(new Set(arr.filter(Boolean)));
|
||||
projects = uniq(projects).sort();
|
||||
contexts = uniq(contexts).sort();
|
||||
|
||||
globalCache = {
|
||||
tags,
|
||||
projects,
|
||||
|
|
@ -128,11 +164,24 @@ export class ProjectSuggest extends CustomSuggest {
|
|||
) {
|
||||
// Initialize with empty list, will be populated asynchronously
|
||||
super(app, inputEl, []);
|
||||
|
||||
// Load cached data asynchronously
|
||||
getCachedData(plugin).then(cachedData => {
|
||||
|
||||
// Load fresh data immediately so newly added projects appear
|
||||
getCachedData(plugin, true).then((cachedData) => {
|
||||
this.availableChoices = cachedData.projects;
|
||||
});
|
||||
|
||||
// Refresh on focus to pick up recent changes (settings/index updates)
|
||||
inputEl.addEventListener("focus", async () => {
|
||||
try {
|
||||
const data = await getCachedData(plugin, true);
|
||||
this.availableChoices = data.projects;
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"ProjectSuggest: failed to refresh projects on focus",
|
||||
e
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,9 +196,9 @@ export class ContextSuggest extends CustomSuggest {
|
|||
) {
|
||||
// Initialize with empty list, will be populated asynchronously
|
||||
super(app, inputEl, []);
|
||||
|
||||
|
||||
// Load cached data asynchronously
|
||||
getCachedData(plugin).then(cachedData => {
|
||||
getCachedData(plugin).then((cachedData) => {
|
||||
this.availableChoices = cachedData.contexts;
|
||||
});
|
||||
}
|
||||
|
|
@ -166,9 +215,9 @@ export class TagSuggest extends CustomSuggest {
|
|||
) {
|
||||
// Initialize with empty list, will be populated asynchronously
|
||||
super(app, inputEl, []);
|
||||
|
||||
|
||||
// Load cached data asynchronously
|
||||
getCachedData(plugin).then(cachedData => {
|
||||
getCachedData(plugin).then((cachedData) => {
|
||||
this.availableChoices = cachedData.tags;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Component, Platform, setIcon, Menu } from "obsidian";
|
||||
import { Component, Platform, setIcon, Menu, Modal, App } from "obsidian";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
import { Task } from "../../../types/task";
|
||||
import { ProjectPopover, ProjectModal } from "./ProjectPopover";
|
||||
import { ProjectPopover, ProjectModal, EditProjectModal } from "./ProjectPopover";
|
||||
import type { CustomProject } from "../../../common/setting-definition";
|
||||
import { t } from "@/translations/helper";
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName?: string;
|
||||
color: string;
|
||||
taskCount: number;
|
||||
createdAt?: number;
|
||||
|
|
@ -82,9 +83,12 @@ export class ProjectList extends Component {
|
|||
const projectName = task.metadata?.project;
|
||||
if (projectName) {
|
||||
if (!projectMap.has(projectName)) {
|
||||
// Convert dashes back to spaces for display
|
||||
const displayName = projectName.replace(/-/g, ' ');
|
||||
projectMap.set(projectName, {
|
||||
id: projectName,
|
||||
name: projectName,
|
||||
displayName: displayName,
|
||||
color: this.generateColorForProject(projectName),
|
||||
taskCount: 0,
|
||||
});
|
||||
|
|
@ -136,9 +140,9 @@ export class ProjectList extends Component {
|
|||
this.projects.sort((a, b) => {
|
||||
switch (this.currentSort) {
|
||||
case "name-asc":
|
||||
return this.collator.compare(a.name, b.name);
|
||||
return this.collator.compare(a.displayName || a.name, b.displayName || b.name);
|
||||
case "name-desc":
|
||||
return this.collator.compare(b.name, a.name);
|
||||
return this.collator.compare(b.displayName || b.name, a.displayName || a.name);
|
||||
case "tasks-asc":
|
||||
return a.taskCount - b.taskCount;
|
||||
case "tasks-desc":
|
||||
|
|
@ -198,7 +202,7 @@ export class ProjectList extends Component {
|
|||
|
||||
const projectName = projectItem.createSpan({
|
||||
cls: "v2-project-name",
|
||||
text: project.name,
|
||||
text: project.displayName || project.name,
|
||||
});
|
||||
|
||||
const projectCount = projectItem.createSpan({
|
||||
|
|
@ -210,6 +214,12 @@ export class ProjectList extends Component {
|
|||
this.setActiveProject(project.id);
|
||||
this.onProjectSelect(project.id);
|
||||
});
|
||||
|
||||
// Add context menu handler
|
||||
this.registerDomEvent(projectItem, "contextmenu", (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.showProjectContextMenu(e, project);
|
||||
});
|
||||
});
|
||||
|
||||
// Add new project button
|
||||
|
|
@ -350,6 +360,7 @@ export class ProjectList extends Component {
|
|||
this.projects.push({
|
||||
id: customProject.id,
|
||||
name: customProject.name,
|
||||
displayName: customProject.displayName || customProject.name,
|
||||
color: customProject.color,
|
||||
taskCount: 0, // Will be updated by task counting
|
||||
createdAt: customProject.createdAt,
|
||||
|
|
@ -359,6 +370,7 @@ export class ProjectList extends Component {
|
|||
// Update existing project with custom color
|
||||
this.projects[existingIndex].id = customProject.id;
|
||||
this.projects[existingIndex].color = customProject.color;
|
||||
this.projects[existingIndex].displayName = customProject.displayName || customProject.name;
|
||||
this.projects[existingIndex].createdAt =
|
||||
customProject.createdAt;
|
||||
this.projects[existingIndex].updatedAt =
|
||||
|
|
@ -425,4 +437,190 @@ export class ProjectList extends Component {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
private showProjectContextMenu(event: MouseEvent, project: Project) {
|
||||
const menu = new Menu();
|
||||
|
||||
// Check if this is a custom project
|
||||
const isCustomProject = this.plugin.settings.projectConfig?.customProjects?.some(
|
||||
cp => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
|
||||
// Edit Project option
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("Edit Project"))
|
||||
.setIcon("edit");
|
||||
|
||||
if (isCustomProject) {
|
||||
item.onClick(() => {
|
||||
this.editProject(project);
|
||||
});
|
||||
} else {
|
||||
item.setDisabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete Project option
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("Delete Project"))
|
||||
.setIcon("trash");
|
||||
|
||||
if (isCustomProject) {
|
||||
item.onClick(() => {
|
||||
this.deleteProject(project);
|
||||
});
|
||||
} else {
|
||||
item.setDisabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
menu.showAtMouseEvent(event);
|
||||
}
|
||||
|
||||
private editProject(project: Project) {
|
||||
// Find the custom project data
|
||||
let customProject = this.plugin.settings.projectConfig?.customProjects?.find(
|
||||
cp => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
|
||||
if (!customProject) {
|
||||
// Create a new custom project entry if it doesn't exist
|
||||
customProject = {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
displayName: project.displayName || project.name,
|
||||
color: project.color,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// Open edit modal
|
||||
const modal = new EditProjectModal(
|
||||
this.plugin.app,
|
||||
this.plugin,
|
||||
customProject,
|
||||
async (updatedProject) => {
|
||||
await this.updateProject(updatedProject);
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}
|
||||
|
||||
private async updateProject(updatedProject: CustomProject) {
|
||||
// Initialize if needed
|
||||
if (!this.plugin.settings.projectConfig) {
|
||||
this.plugin.settings.projectConfig = {
|
||||
enableEnhancedProject: false,
|
||||
pathMappings: [],
|
||||
metadataConfig: {
|
||||
metadataKey: "project",
|
||||
enabled: false,
|
||||
},
|
||||
configFile: {
|
||||
fileName: "project.md",
|
||||
searchRecursively: true,
|
||||
enabled: false,
|
||||
},
|
||||
metadataMappings: [],
|
||||
defaultProjectNaming: {
|
||||
strategy: "filename",
|
||||
stripExtension: true,
|
||||
enabled: false,
|
||||
},
|
||||
customProjects: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.plugin.settings.projectConfig.customProjects) {
|
||||
this.plugin.settings.projectConfig.customProjects = [];
|
||||
}
|
||||
|
||||
// Find and update the project
|
||||
const index = this.plugin.settings.projectConfig.customProjects.findIndex(
|
||||
cp => cp.id === updatedProject.id
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
this.plugin.settings.projectConfig.customProjects[index] = updatedProject;
|
||||
} else {
|
||||
this.plugin.settings.projectConfig.customProjects.push(updatedProject);
|
||||
}
|
||||
|
||||
// Save settings
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Refresh the project list
|
||||
this.loadProjects();
|
||||
}
|
||||
|
||||
private deleteProject(project: Project) {
|
||||
// Confirm deletion
|
||||
const modal = new (class extends Modal {
|
||||
private onConfirm: () => void;
|
||||
|
||||
constructor(app: App, onConfirm: () => void) {
|
||||
super(app);
|
||||
this.onConfirm = onConfirm;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl("h2", { text: t("Delete Project") });
|
||||
contentEl.createEl("p", {
|
||||
text: t(`Are you sure you want to delete "${project.displayName || project.name}"?`)
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
cls: "mod-warning",
|
||||
text: t("This action cannot be undone.")
|
||||
});
|
||||
|
||||
const buttonContainer = contentEl.createDiv({ cls: "modal-button-container" });
|
||||
|
||||
const cancelBtn = buttonContainer.createEl("button", {
|
||||
text: t("Cancel")
|
||||
});
|
||||
cancelBtn.addEventListener("click", () => this.close());
|
||||
|
||||
const confirmBtn = buttonContainer.createEl("button", {
|
||||
text: t("Delete"),
|
||||
cls: "mod-warning"
|
||||
});
|
||||
confirmBtn.addEventListener("click", () => {
|
||||
this.onConfirm();
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
})(this.plugin.app, async () => {
|
||||
// Remove from custom projects
|
||||
if (this.plugin.settings.projectConfig?.customProjects) {
|
||||
const index = this.plugin.settings.projectConfig.customProjects.findIndex(
|
||||
cp => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
this.plugin.settings.projectConfig.customProjects.splice(index, 1);
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// If this was the active project, clear selection
|
||||
if (this.activeProjectId === project.id) {
|
||||
this.setActiveProject(null);
|
||||
this.onProjectSelect("");
|
||||
}
|
||||
|
||||
// Refresh the project list
|
||||
this.loadProjects();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,17 +216,22 @@ export class ProjectPopover extends Component {
|
|||
private save() {
|
||||
if (!this.nameInput) return;
|
||||
|
||||
const name = this.nameInput.value.trim();
|
||||
if (!name) {
|
||||
const inputValue = this.nameInput.value.trim();
|
||||
if (!inputValue) {
|
||||
this.nameInput.focus();
|
||||
this.nameInput.addClass("is-error");
|
||||
setTimeout(() => this.nameInput?.removeClass("is-error"), 300);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert spaces to dashes for internal name, keep original for display
|
||||
const internalName = inputValue.replace(/\s+/g, '-');
|
||||
const displayName = inputValue;
|
||||
|
||||
const project: CustomProject = {
|
||||
id: `project-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
name,
|
||||
name: internalName,
|
||||
displayName: displayName,
|
||||
color: this.selectedColor,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
|
|
@ -266,6 +271,265 @@ export class ProjectPopover extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
export class EditProjectPopover extends Component {
|
||||
private popoverEl: HTMLElement | null = null;
|
||||
private plugin: TaskProgressBarPlugin;
|
||||
private project: CustomProject;
|
||||
private onSave: (project: CustomProject) => void;
|
||||
private onClose: () => void;
|
||||
private nameInput: HTMLInputElement | null = null;
|
||||
private displayNameInput: HTMLInputElement | null = null;
|
||||
private selectedColor: string;
|
||||
private popperInstance: PopperInstance | null = null;
|
||||
private referenceEl: HTMLElement;
|
||||
private clickHandler: ((e: MouseEvent) => void) | null = null;
|
||||
private escapeHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
private readonly colors = [
|
||||
"#e74c3c",
|
||||
"#3498db",
|
||||
"#2ecc71",
|
||||
"#f39c12",
|
||||
"#9b59b6",
|
||||
"#1abc9c",
|
||||
"#34495e",
|
||||
"#e67e22",
|
||||
"#16a085",
|
||||
"#27ae60",
|
||||
"#2980b9",
|
||||
"#8e44ad",
|
||||
"#2c3e50",
|
||||
"#f1c40f",
|
||||
"#d35400"
|
||||
];
|
||||
|
||||
constructor(
|
||||
plugin: TaskProgressBarPlugin,
|
||||
referenceEl: HTMLElement,
|
||||
project: CustomProject,
|
||||
onSave: (project: CustomProject) => void,
|
||||
onClose: () => void
|
||||
) {
|
||||
super();
|
||||
this.plugin = plugin;
|
||||
this.referenceEl = referenceEl;
|
||||
this.project = { ...project }; // Clone the project to avoid direct mutation
|
||||
this.onSave = onSave;
|
||||
this.onClose = onClose;
|
||||
this.selectedColor = project.color || "#3498db";
|
||||
}
|
||||
|
||||
onload() {
|
||||
this.createPopover();
|
||||
this.setupPopper();
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private createPopover() {
|
||||
this.popoverEl = document.createElement("div");
|
||||
this.popoverEl.addClass("v2-project-popover-container");
|
||||
document.body.appendChild(this.popoverEl);
|
||||
|
||||
const popover = this.popoverEl.createDiv({ cls: "v2-project-popover" });
|
||||
|
||||
// Arrow
|
||||
const arrow = popover.createDiv({ cls: "v2-popover-arrow" });
|
||||
arrow.setAttribute("data-popper-arrow", "");
|
||||
|
||||
// Content
|
||||
const content = popover.createDiv({ cls: "v2-popover-content" });
|
||||
|
||||
// Title
|
||||
const header = content.createDiv({ cls: "v2-popover-header" });
|
||||
header.createEl("h3", { text: t("Edit Project") });
|
||||
|
||||
// Display name input
|
||||
const displayNameSection = content.createDiv({ cls: "v2-popover-section" });
|
||||
displayNameSection.createEl("label", { text: t("Display Name") });
|
||||
this.displayNameInput = displayNameSection.createEl("input", {
|
||||
cls: "v2-popover-input",
|
||||
attr: {
|
||||
type: "text",
|
||||
placeholder: t("Enter display name"),
|
||||
value: this.project.displayName || this.project.name
|
||||
}
|
||||
});
|
||||
|
||||
// Color picker
|
||||
const colorSection = content.createDiv({ cls: "v2-popover-section" });
|
||||
colorSection.createEl("label", { text: t("Choose Color") });
|
||||
|
||||
const colorGrid = colorSection.createDiv({ cls: "v2-color-grid" });
|
||||
this.colors.forEach(color => {
|
||||
const colorButton = colorGrid.createDiv({
|
||||
cls: "v2-color-button",
|
||||
attr: { "data-color": color }
|
||||
});
|
||||
colorButton.style.backgroundColor = color;
|
||||
|
||||
if (color === this.selectedColor) {
|
||||
colorButton.addClass("is-selected");
|
||||
}
|
||||
|
||||
this.registerDomEvent(colorButton, "click", () => {
|
||||
this.selectColor(color);
|
||||
});
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
const actions = content.createDiv({ cls: "v2-popover-actions" });
|
||||
|
||||
const cancelBtn = actions.createEl("button", {
|
||||
cls: "v2-button v2-button-secondary",
|
||||
text: t("Cancel")
|
||||
});
|
||||
this.registerDomEvent(cancelBtn, "click", () => this.close());
|
||||
|
||||
const saveBtn = actions.createEl("button", {
|
||||
cls: "v2-button v2-button-primary",
|
||||
text: t("Save")
|
||||
});
|
||||
this.registerDomEvent(saveBtn, "click", () => this.save());
|
||||
|
||||
// Handle Enter key on input
|
||||
this.registerDomEvent(this.displayNameInput, "keydown", (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
this.save();
|
||||
}
|
||||
});
|
||||
|
||||
// Focus input
|
||||
setTimeout(() => this.displayNameInput?.focus(), 50);
|
||||
}
|
||||
|
||||
private setupPopper() {
|
||||
if (!this.popoverEl) return;
|
||||
|
||||
const popover = this.popoverEl.querySelector(".v2-project-popover") as HTMLElement;
|
||||
if (!popover) return;
|
||||
|
||||
this.popperInstance = createPopper(this.referenceEl, popover, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 8],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arrow",
|
||||
options: {
|
||||
element: ".v2-popover-arrow",
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
boundary: "viewport",
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flip",
|
||||
options: {
|
||||
fallbackPlacements: ["top-start", "right", "left"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private setupEventHandlers() {
|
||||
// Click outside to close
|
||||
this.clickHandler = (e: MouseEvent) => {
|
||||
if (
|
||||
this.popoverEl &&
|
||||
!this.popoverEl.contains(e.target as Node) &&
|
||||
!this.referenceEl.contains(e.target as Node)
|
||||
) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Delay to avoid immediate close
|
||||
setTimeout(() => {
|
||||
if (this.clickHandler) {
|
||||
document.addEventListener("click", this.clickHandler);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Escape key to close
|
||||
this.escapeHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", this.escapeHandler);
|
||||
}
|
||||
|
||||
private selectColor(color: string) {
|
||||
if (!this.popoverEl) return;
|
||||
|
||||
this.selectedColor = color;
|
||||
|
||||
// Update selected state
|
||||
this.popoverEl.querySelectorAll(".v2-color-button").forEach(btn => {
|
||||
btn.removeClass("is-selected");
|
||||
});
|
||||
const selectedBtn = this.popoverEl.querySelector(`[data-color="${color}"]`);
|
||||
selectedBtn?.addClass("is-selected");
|
||||
}
|
||||
|
||||
private save() {
|
||||
if (!this.displayNameInput) return;
|
||||
|
||||
const displayNameValue = this.displayNameInput.value.trim();
|
||||
|
||||
// Update the project object
|
||||
this.project.displayName = displayNameValue || this.project.name;
|
||||
this.project.color = this.selectedColor;
|
||||
this.project.updatedAt = Date.now();
|
||||
|
||||
this.onSave(this.project);
|
||||
this.close();
|
||||
}
|
||||
|
||||
private close() {
|
||||
this.onClose();
|
||||
this.unload();
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
// Clean up event listeners
|
||||
if (this.clickHandler) {
|
||||
document.removeEventListener("click", this.clickHandler);
|
||||
this.clickHandler = null;
|
||||
}
|
||||
if (this.escapeHandler) {
|
||||
document.removeEventListener("keydown", this.escapeHandler);
|
||||
this.escapeHandler = null;
|
||||
}
|
||||
|
||||
// Clean up Popper instance
|
||||
if (this.popperInstance) {
|
||||
this.popperInstance.destroy();
|
||||
this.popperInstance = null;
|
||||
}
|
||||
|
||||
// Remove DOM elements
|
||||
if (this.popoverEl) {
|
||||
this.popoverEl.remove();
|
||||
this.popoverEl = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectModal extends Modal {
|
||||
private plugin: TaskProgressBarPlugin;
|
||||
private onSave: (project: CustomProject) => void;
|
||||
|
|
@ -420,17 +684,22 @@ export class ProjectModal extends Modal {
|
|||
private save() {
|
||||
if (!this.nameInput) return;
|
||||
|
||||
const name = this.nameInput.value.trim();
|
||||
if (!name) {
|
||||
const inputValue = this.nameInput.value.trim();
|
||||
if (!inputValue) {
|
||||
this.nameInput.focus();
|
||||
this.nameInput.addClass("is-error");
|
||||
setTimeout(() => this.nameInput?.removeClass("is-error"), 300);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert spaces to dashes for internal name, keep original for display
|
||||
const internalName = inputValue.replace(/\s+/g, '-');
|
||||
const displayName = inputValue;
|
||||
|
||||
const project: CustomProject = {
|
||||
id: `project-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
name,
|
||||
name: internalName,
|
||||
displayName: displayName,
|
||||
color: this.selectedColor,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
|
|
@ -439,4 +708,179 @@ export class ProjectModal extends Modal {
|
|||
this.onSave(project);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditProjectModal extends Modal {
|
||||
private plugin: TaskProgressBarPlugin;
|
||||
private project: CustomProject;
|
||||
private onSave: (project: CustomProject) => void;
|
||||
private nameInput: HTMLInputElement | null = null;
|
||||
private displayNameInput: HTMLInputElement | null = null;
|
||||
private selectedColor: string;
|
||||
private eventListeners: Array<{ element: HTMLElement; event: string; handler: EventListener }> = [];
|
||||
|
||||
private readonly colors = [
|
||||
"#e74c3c",
|
||||
"#3498db",
|
||||
"#2ecc71",
|
||||
"#f39c12",
|
||||
"#9b59b6",
|
||||
"#1abc9c",
|
||||
"#34495e",
|
||||
"#e67e22",
|
||||
"#16a085",
|
||||
"#27ae60",
|
||||
"#2980b9",
|
||||
"#8e44ad",
|
||||
"#2c3e50",
|
||||
"#f1c40f",
|
||||
"#d35400"
|
||||
];
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
project: CustomProject,
|
||||
onSave: (project: CustomProject) => void
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.project = { ...project }; // Clone to avoid direct mutation
|
||||
this.onSave = onSave;
|
||||
this.selectedColor = project.color || "#3498db";
|
||||
}
|
||||
|
||||
private addEventListener(element: HTMLElement, event: string, handler: EventListener) {
|
||||
element.addEventListener(event, handler);
|
||||
this.eventListeners.push({ element, event, handler });
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// Add custom class for styling
|
||||
this.modalEl.addClass("v2-project-modal");
|
||||
|
||||
// Title
|
||||
contentEl.createEl("h2", { text: t("Edit Project") });
|
||||
|
||||
// Display name input section
|
||||
const displayNameSection = contentEl.createDiv({ cls: "v2-modal-section" });
|
||||
displayNameSection.createEl("label", { text: t("Display Name") });
|
||||
this.displayNameInput = displayNameSection.createEl("input", {
|
||||
cls: "v2-modal-input",
|
||||
attr: {
|
||||
type: "text",
|
||||
placeholder: t("Enter display name"),
|
||||
value: this.project.displayName || this.project.name
|
||||
}
|
||||
});
|
||||
|
||||
// Color picker section
|
||||
const colorSection = contentEl.createDiv({ cls: "v2-modal-section" });
|
||||
colorSection.createEl("label", { text: t("Choose Color") });
|
||||
|
||||
const colorGrid = colorSection.createDiv({ cls: "v2-modal-color-grid" });
|
||||
this.colors.forEach(color => {
|
||||
const colorButton = colorGrid.createDiv({
|
||||
cls: "v2-modal-color-button",
|
||||
attr: { "data-color": color }
|
||||
});
|
||||
colorButton.style.backgroundColor = color;
|
||||
|
||||
if (color === this.selectedColor) {
|
||||
colorButton.addClass("is-selected");
|
||||
}
|
||||
|
||||
this.addEventListener(colorButton, "click", () => {
|
||||
this.selectColor(color);
|
||||
});
|
||||
});
|
||||
|
||||
// Preview section
|
||||
const previewSection = contentEl.createDiv({ cls: "v2-modal-section" });
|
||||
previewSection.createEl("label", { text: t("Preview") });
|
||||
const preview = previewSection.createDiv({ cls: "v2-modal-preview" });
|
||||
const previewItem = preview.createDiv({ cls: "v2-project-item-preview" });
|
||||
const previewColor = previewItem.createDiv({ cls: "v2-project-color" });
|
||||
previewColor.style.backgroundColor = this.selectedColor;
|
||||
const previewName = previewItem.createSpan({ cls: "v2-project-name" });
|
||||
|
||||
// Update preview on name change
|
||||
this.addEventListener(this.displayNameInput, "input", () => {
|
||||
previewName.setText(this.displayNameInput?.value || this.project.name);
|
||||
});
|
||||
previewName.setText(this.project.displayName || this.project.name);
|
||||
|
||||
// Footer with action buttons
|
||||
const footer = contentEl.createDiv({ cls: "v2-modal-footer" });
|
||||
|
||||
const cancelBtn = footer.createEl("button", {
|
||||
cls: "v2-button v2-button-secondary",
|
||||
text: t("Cancel")
|
||||
});
|
||||
this.addEventListener(cancelBtn, "click", () => this.close());
|
||||
|
||||
const saveBtn = footer.createEl("button", {
|
||||
cls: "v2-button v2-button-primary",
|
||||
text: t("Save Changes")
|
||||
});
|
||||
this.addEventListener(saveBtn, "click", () => this.save());
|
||||
|
||||
// Handle Enter key
|
||||
this.addEventListener(this.displayNameInput, "keydown", (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
this.save();
|
||||
}
|
||||
});
|
||||
|
||||
// Focus input
|
||||
setTimeout(() => {
|
||||
this.displayNameInput?.focus();
|
||||
this.displayNameInput?.select();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
// Clean up all event listeners
|
||||
this.eventListeners.forEach(({ element, event, handler }) => {
|
||||
element.removeEventListener(event, handler);
|
||||
});
|
||||
this.eventListeners = [];
|
||||
|
||||
// Clear content
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
|
||||
private selectColor(color: string) {
|
||||
this.selectedColor = color;
|
||||
|
||||
// Update selected state
|
||||
this.modalEl.querySelectorAll(".v2-modal-color-button").forEach(btn => {
|
||||
btn.removeClass("is-selected");
|
||||
});
|
||||
const selectedBtn = this.modalEl.querySelector(`[data-color="${color}"]`);
|
||||
selectedBtn?.addClass("is-selected");
|
||||
|
||||
// Update preview
|
||||
const previewColor = this.modalEl.querySelector(".v2-project-color") as HTMLElement;
|
||||
if (previewColor) {
|
||||
previewColor.style.backgroundColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
private save() {
|
||||
if (!this.displayNameInput) return;
|
||||
|
||||
const displayNameValue = this.displayNameInput.value.trim();
|
||||
|
||||
// Update the project object
|
||||
this.project.displayName = displayNameValue || this.project.name;
|
||||
this.project.color = this.selectedColor;
|
||||
this.project.updatedAt = Date.now();
|
||||
|
||||
this.onSave(this.project);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue