Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "ai-toolbox",
"name": "AI Toolbox",
"version": "1.0.6",
"version": "1.0.7",
"minAppVersion": "0.15.0",
"description": "Personal collection of AI tools to enhance the Obsidian.md workflow.",
"author": "dalinicus",
Expand Down
53 changes: 39 additions & 14 deletions src/components/collapsible-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ export interface CollapsibleSectionConfig {
startExpanded: boolean;
/** Whether to show as a heading (bold) - defaults to true */
isHeading?: boolean;
/** Optional icon to display before the title */
icon?: string;
/** Optional icon(s) to display after the title - can be a single icon or array of icons */
icons?: string[];
/** Optional secondary text to display in the header (e.g., ID) */
secondaryText?: string;
/** Callback when the delete button is clicked (if provided, delete button is shown) */
onDelete?: () => void | Promise<void>;
/** Callback when the title changes (for dynamic name updates) */
onTitleChange?: (newTitle: string) => void;
/** Callback when the move up button is clicked (if provided, move up button is shown) */
onMoveUp?: () => void | Promise<void>;
/** Callback when the move down button is clicked (if provided, move down button is shown) */
onMoveDown?: () => void | Promise<void>;
}

/**
Expand Down Expand Up @@ -57,9 +61,11 @@ export function createCollapsibleSection(config: CollapsibleSectionConfig): Coll
headerClass,
startExpanded,
isHeading = true,
icon,
icons,
secondaryText,
onDelete,
onMoveUp,
onMoveDown,
} = config;

const container = containerEl.createDiv(containerClass);
Expand All @@ -71,11 +77,11 @@ export function createCollapsibleSection(config: CollapsibleSectionConfig): Coll

// Track current title for updates
let currentTitle = title;
let iconElement: HTMLElement | null = null;
let iconElements: HTMLElement[] = [];

// Helper to get the formatted title with arrow and optional icon
const getFormattedTitle = (name: string, expanded: boolean): string => {
// Arrow only, icon will be inserted separately
// Arrow only, icons will be inserted separately
return `${expanded ? '▾' : '▸'} ${name || 'Unnamed'}`;
};

Expand All @@ -87,11 +93,14 @@ export function createCollapsibleSection(config: CollapsibleSectionConfig): Coll
headerSetting.setHeading();
}

// Add icon if provided (at the end, after title text)
if (icon) {
iconElement = headerSetting.nameEl.createSpan({ cls: 'workflow-header-icon' });
setIcon(iconElement, icon);
headerSetting.nameEl.appendChild(iconElement);
// Add icons if provided (at the end, after title text)
if (icons && icons.length > 0) {
for (const iconName of icons) {
const iconElement = headerSetting.nameEl.createSpan({ cls: 'workflow-header-icon' });
setIcon(iconElement, iconName);
headerSetting.nameEl.appendChild(iconElement);
iconElements.push(iconElement);
}
}

// Add secondary text if provided (displayed before delete button)
Expand All @@ -100,6 +109,22 @@ export function createCollapsibleSection(config: CollapsibleSectionConfig): Coll
secondaryEl.textContent = secondaryText;
}

// Add move up button if callback provided
if (onMoveUp) {
headerSetting.addButton(button => button
.setIcon('chevron-up')
.setTooltip('Move up')
.onClick(() => { void onMoveUp(); }));
}

// Add move down button if callback provided
if (onMoveDown) {
headerSetting.addButton(button => button
.setIcon('chevron-down')
.setTooltip('Move down')
.onClick(() => { void onMoveDown(); }));
}

// Add delete button if callback provided
if (onDelete) {
headerSetting.addButton(button => button
Expand All @@ -117,8 +142,8 @@ export function createCollapsibleSection(config: CollapsibleSectionConfig): Coll
contentContainer.classList.toggle('is-expanded', isCollapsed);
headerSetting.setName(getFormattedTitle(currentTitle, isCollapsed));

// Re-add icon at the end
if (icon && iconElement) {
// Re-add icons at the end
for (const iconElement of iconElements) {
headerSetting.nameEl.appendChild(iconElement);
}
};
Expand All @@ -139,8 +164,8 @@ export function createCollapsibleSection(config: CollapsibleSectionConfig): Coll
const isExpanded = contentContainer.classList.contains('is-expanded');
headerSetting.setName(getFormattedTitle(newTitle, isExpanded));

// Re-add icon at the end
if (icon && iconElement) {
// Re-add icons at the end
for (const iconElement of iconElements) {
headerSetting.nameEl.appendChild(iconElement);
}
};
Expand Down
59 changes: 59 additions & 0 deletions src/components/delete-mode-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Manages delete mode state for a collection of entities.
* Provides a centralized way to track whether delete mode is enabled
* for different entity types (workflows, actions, providers, models).
*/
export class DeleteModeManager {
private deleteStates: Map<string, boolean> = new Map();

/**
* Get delete mode state for an entity
* @param entityId - The unique identifier for the entity (e.g., workflow ID for actions)
* @returns Whether delete mode is enabled
*/
get(entityId: string): boolean {
return this.deleteStates.get(entityId) ?? false;
}

/**
* Set delete mode state for an entity
* @param entityId - The unique identifier for the entity
* @param enabled - Whether delete mode should be enabled
*/
set(entityId: string, enabled: boolean): void {
this.deleteStates.set(entityId, enabled);
}

/**
* Toggle delete mode state for an entity
* @param entityId - The unique identifier for the entity
* @returns The new delete mode state
*/
toggle(entityId: string): boolean {
const newState = !this.get(entityId);
this.set(entityId, newState);
return newState;
}

/**
* Clear delete mode state for an entity
* @param entityId - The unique identifier for the entity
*/
clear(entityId: string): void {
this.deleteStates.delete(entityId);
}

/**
* Clear all delete mode states
*/
clearAll(): void {
this.deleteStates.clear();
}
}

// Global manager for top-level entity delete modes (workflows, providers)
export const globalDeleteModeManager = new DeleteModeManager();

// Global manager for nested entity delete modes (actions within workflows, models within providers)
export const nestedDeleteModeManager = new DeleteModeManager();

103 changes: 103 additions & 0 deletions src/components/entity-list-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Setting, setIcon } from "obsidian";

/**
* Configuration for creating an entity list header with delete mode toggle and add button
*/
export interface EntityListHeaderConfig {
/** Container element where the header will be created */
containerEl: HTMLElement;
/** Label for the entities (e.g., 'Workflows', 'Actions', 'Models') */
label?: string;
/** Description for the setting */
description?: string;
/** Current delete mode state */
isDeleteMode: boolean;
/** Callback when delete mode is toggled */
onDeleteModeChange: (enabled: boolean) => void | Promise<void>;
/** Text for the add button (e.g., 'Add workflow', 'Add model') */
addButtonText?: string;
/** Whether the add button should be CTA styled */
addButtonCta?: boolean;
/** Callback when add button is clicked (if not provided, no add button shown) */
onAdd?: () => void | Promise<void>;
/** Optional dropdown options for add (alternative to button) */
addDropdownOptions?: Record<string, string>;
/** Callback when a dropdown option is selected */
onDropdownSelect?: (value: string) => void | Promise<void>;
}

/**
* Creates a header for an entity list with delete mode toggle and add button.
* This provides a consistent UI pattern for managing collections of entities.
*/
export function createEntityListHeader(config: EntityListHeaderConfig): Setting {
const {
containerEl,
label,
description,
isDeleteMode,
onDeleteModeChange,
addButtonText,
addButtonCta = false,
onAdd,
addDropdownOptions,
onDropdownSelect,
} = config;

const setting = new Setting(containerEl);

if (label) {
setting.setName(label);
}
if (description) {
setting.setDesc(description);
}

// Add delete mode toggle with trash icon
setting.addToggle(toggle => {
toggle
.setValue(isDeleteMode)
.setTooltip(isDeleteMode ? 'Exit delete mode' : 'Enter delete mode')
.onChange(async (value) => {
await onDeleteModeChange(value);
});

// Create a custom label with trash icon before the toggle
const toggleContainer = toggle.toggleEl.parentElement;
if (toggleContainer) {
const labelContainer = toggleContainer.createDiv({ cls: 'delete-mode-toggle-label' });
const iconSpan = labelContainer.createSpan({ cls: 'delete-mode-toggle-icon' });
setIcon(iconSpan, 'trash');
// Move the label before the toggle element
toggleContainer.insertBefore(labelContainer, toggle.toggleEl);
}
});

// Add dropdown if options provided
if (addDropdownOptions && onDropdownSelect) {
setting.addDropdown(dropdown => dropdown
.addOption('', 'Add...')
.addOptions(addDropdownOptions)
.onChange(async (value) => {
if (value) {
await onDropdownSelect(value);
}
}));
}

// Add button if callback provided
if (onAdd && addButtonText) {
setting.addButton(button => {
button.setButtonText(addButtonText);
if (addButtonCta) {
button.setCta();
}
button.onClick(async () => {
await onAdd();
});
});
}

return setting;
}

Loading