x-ipe 1.0.23__py3-none-any.whl → 1.0.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- x_ipe/app.py +32 -1
- x_ipe/handlers/terminal_handlers.py +6 -0
- x_ipe/handlers/voice_handlers.py +5 -0
- x_ipe/resources/copilot-instructions.md +19 -6
- x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
- x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
- x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
- x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
- x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
- x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
- x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
- x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
- x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
- x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
- x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
- x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
- x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
- x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
- x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
- x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
- x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
- x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
- x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
- x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
- x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
- x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
- x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
- x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
- x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
- x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
- x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
- x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
- x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
- x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
- x_ipe/routes/__init__.py +2 -0
- x_ipe/routes/ideas_routes.py +289 -0
- x_ipe/routes/kb_routes.py +80 -0
- x_ipe/routes/main_routes.py +18 -0
- x_ipe/routes/project_routes.py +7 -0
- x_ipe/routes/proxy_routes.py +10 -2
- x_ipe/routes/quality_evaluation_routes.py +193 -0
- x_ipe/routes/settings_routes.py +6 -0
- x_ipe/routes/tools_routes.py +6 -0
- x_ipe/routes/tracing_routes.py +232 -0
- x_ipe/routes/uiux_feedback_routes.py +50 -0
- x_ipe/services/__init__.py +5 -0
- x_ipe/services/config_service.py +6 -0
- x_ipe/services/file_service.py +20 -0
- x_ipe/services/homepage_service.py +160 -0
- x_ipe/services/ideas_service.py +535 -2
- x_ipe/services/kb_service.py +378 -0
- x_ipe/services/proxy_service.py +37 -7
- x_ipe/services/settings_service.py +13 -0
- x_ipe/services/skills_service.py +4 -0
- x_ipe/services/terminal_service.py +24 -0
- x_ipe/services/themes_service.py +4 -0
- x_ipe/services/tools_config_service.py +4 -0
- x_ipe/services/tracing_service.py +333 -0
- x_ipe/services/uiux_feedback_service.py +148 -1
- x_ipe/services/voice_input_service_v2.py +11 -0
- x_ipe/static/css/base.css +7 -0
- x_ipe/static/css/homepage-infinity.css +330 -0
- x_ipe/static/css/kb-core.css +301 -0
- x_ipe/static/css/quality-evaluation.css +345 -0
- x_ipe/static/css/sidebar.css +14 -4
- x_ipe/static/css/terminal.css +23 -0
- x_ipe/static/css/tracing-dashboard.css +796 -0
- x_ipe/static/css/uiux-feedback.css +7 -1
- x_ipe/static/css/workplace.css +636 -0
- x_ipe/static/img/homepage-infinity-loop.png +0 -0
- x_ipe/static/js/features/confirm-dialog.js +169 -0
- x_ipe/static/js/features/folder-view.js +742 -0
- x_ipe/static/js/features/homepage-infinity.js +314 -0
- x_ipe/static/js/features/kb-core.js +371 -0
- x_ipe/static/js/features/quality-evaluation.js +387 -0
- x_ipe/static/js/features/sidebar.js +255 -12
- x_ipe/static/js/features/tracing-dashboard.js +855 -0
- x_ipe/static/js/features/tracing-graph.js +1031 -0
- x_ipe/static/js/features/tree-drag.js +227 -0
- x_ipe/static/js/features/tree-search.js +228 -0
- x_ipe/static/js/features/workplace.js +661 -33
- x_ipe/static/js/init.js +76 -0
- x_ipe/static/js/terminal-v2.js +45 -14
- x_ipe/static/js/terminal.js +50 -49
- x_ipe/static/js/uiux-feedback.js +75 -16
- x_ipe/templates/base.html +24 -0
- x_ipe/templates/index.html +10 -1
- x_ipe/templates/knowledge-base.html +110 -0
- x_ipe/templates/workplace.html +4 -0
- x_ipe/tracing/__init__.py +37 -0
- x_ipe/tracing/buffer.py +135 -0
- x_ipe/tracing/context.py +125 -0
- x_ipe/tracing/decorator.py +288 -0
- x_ipe/tracing/middleware.py +197 -0
- x_ipe/tracing/parser.py +235 -0
- x_ipe/tracing/redactor.py +111 -0
- x_ipe/tracing/writer.py +122 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/RECORD +138 -65
- x_ipe/app.py.bak +0 -1333
- x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
- x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
- x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
- x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
- x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
- x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
- x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FolderViewManager - Detailed folder view panel
|
|
3
|
+
* FEATURE-008 CR-006: Folder Tree UX Enhancement
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - Folder contents view replacing preview panel
|
|
7
|
+
* - Breadcrumb navigation
|
|
8
|
+
* - Action bar (Add File, Add Folder, Rename, Delete)
|
|
9
|
+
* - File/folder items with hover actions
|
|
10
|
+
* - Subfolder expansion in place
|
|
11
|
+
*/
|
|
12
|
+
class FolderViewManager {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.container = options.container;
|
|
15
|
+
this.onAction = options.onAction; // Callback: (action, path, data) => Promise<boolean>
|
|
16
|
+
this.onNavigate = options.onNavigate; // Callback: (path) => void (when clicking file)
|
|
17
|
+
this.onClose = options.onClose; // Callback: () => void (when closing folder view)
|
|
18
|
+
this.currentPath = null;
|
|
19
|
+
this.expandedFolders = new Set();
|
|
20
|
+
this.confirmDialog = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize folder view
|
|
25
|
+
*/
|
|
26
|
+
async init() {
|
|
27
|
+
// Lazy load ConfirmDialog
|
|
28
|
+
if (typeof ConfirmDialog !== 'undefined') {
|
|
29
|
+
this.confirmDialog = new ConfirmDialog();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Render folder view for given path
|
|
35
|
+
* @param {string} folderPath - Folder path to display
|
|
36
|
+
*/
|
|
37
|
+
async render(folderPath) {
|
|
38
|
+
this.currentPath = folderPath;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const contents = await this._loadContents(folderPath);
|
|
42
|
+
|
|
43
|
+
this.container.innerHTML = `
|
|
44
|
+
<div class="folder-view">
|
|
45
|
+
<header class="folder-view-header">
|
|
46
|
+
<div class="folder-view-header-row">
|
|
47
|
+
${this._renderPathBar(folderPath)}
|
|
48
|
+
<button class="folder-view-close" title="Close folder view">
|
|
49
|
+
<i class="bi bi-x-lg"></i>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
${this._renderActionBar()}
|
|
53
|
+
</header>
|
|
54
|
+
<div class="folder-view-content">
|
|
55
|
+
${this._renderContents(contents)}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
this._bindEvents();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Failed to load folder contents:', error);
|
|
63
|
+
this.container.innerHTML = `
|
|
64
|
+
<div class="folder-view folder-view-error">
|
|
65
|
+
<i class="bi bi-exclamation-triangle"></i>
|
|
66
|
+
<p>Failed to load folder contents</p>
|
|
67
|
+
<button class="btn btn-sm btn-outline-primary" onclick="location.reload()">
|
|
68
|
+
Retry
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load folder contents from API
|
|
77
|
+
* @param {string} folderPath
|
|
78
|
+
* @returns {Promise<Array>}
|
|
79
|
+
*/
|
|
80
|
+
async _loadContents(folderPath) {
|
|
81
|
+
const response = await fetch(`/api/ideas/folder-contents?path=${encodeURIComponent(folderPath)}`);
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
|
|
84
|
+
if (!data.success) {
|
|
85
|
+
throw new Error(data.error || 'Failed to load folder');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return data.items || [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Render breadcrumb path bar
|
|
93
|
+
* @param {string} path
|
|
94
|
+
* @returns {string} HTML
|
|
95
|
+
*/
|
|
96
|
+
_renderPathBar(path) {
|
|
97
|
+
const parts = path.split('/').filter(Boolean);
|
|
98
|
+
|
|
99
|
+
const breadcrumbs = parts.map((part, i) => {
|
|
100
|
+
const fullPath = parts.slice(0, i + 1).join('/');
|
|
101
|
+
const isLast = i === parts.length - 1;
|
|
102
|
+
return `<span class="breadcrumb-item ${isLast ? 'current' : 'clickable'}"
|
|
103
|
+
data-path="${fullPath}">${this._escapeHtml(part)}</span>`;
|
|
104
|
+
}).join('<span class="breadcrumb-sep"><i class="bi bi-chevron-right"></i></span>');
|
|
105
|
+
|
|
106
|
+
return `<nav class="folder-view-breadcrumb" aria-label="Folder path">
|
|
107
|
+
<span class="breadcrumb-item clickable" data-path="">
|
|
108
|
+
<i class="bi bi-house"></i> Ideas
|
|
109
|
+
</span>
|
|
110
|
+
${breadcrumbs ? '<span class="breadcrumb-sep"><i class="bi bi-chevron-right"></i></span>' + breadcrumbs : ''}
|
|
111
|
+
</nav>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Render action bar with buttons
|
|
116
|
+
* @returns {string} HTML
|
|
117
|
+
*/
|
|
118
|
+
_renderActionBar() {
|
|
119
|
+
return `<div class="folder-view-actions">
|
|
120
|
+
<button class="folder-view-action-btn" data-action="add-file" title="Add new file">
|
|
121
|
+
<i class="bi bi-file-earmark-plus"></i>
|
|
122
|
+
<span>Add File</span>
|
|
123
|
+
</button>
|
|
124
|
+
<button class="folder-view-action-btn" data-action="add-folder" title="Create new folder">
|
|
125
|
+
<i class="bi bi-folder-plus"></i>
|
|
126
|
+
<span>Add Folder</span>
|
|
127
|
+
</button>
|
|
128
|
+
<button class="folder-view-action-btn" data-action="rename" title="Rename folder">
|
|
129
|
+
<i class="bi bi-pencil"></i>
|
|
130
|
+
<span>Rename</span>
|
|
131
|
+
</button>
|
|
132
|
+
<button class="folder-view-action-btn danger" data-action="delete" title="Delete folder">
|
|
133
|
+
<i class="bi bi-trash"></i>
|
|
134
|
+
<span>Delete</span>
|
|
135
|
+
</button>
|
|
136
|
+
</div>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Render folder contents
|
|
141
|
+
* @param {Array} items
|
|
142
|
+
* @returns {string} HTML
|
|
143
|
+
*/
|
|
144
|
+
_renderContents(items) {
|
|
145
|
+
if (!items || items.length === 0) {
|
|
146
|
+
return `<div class="folder-view-empty">
|
|
147
|
+
<i class="bi bi-folder2-open"></i>
|
|
148
|
+
<p>This folder is empty</p>
|
|
149
|
+
<p class="text-muted">Use the buttons above to add files or folders</p>
|
|
150
|
+
</div>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Sort: folders first, then files, alphabetically
|
|
154
|
+
const sorted = [...items].sort((a, b) => {
|
|
155
|
+
if (a.type !== b.type) {
|
|
156
|
+
return a.type === 'folder' ? -1 : 1;
|
|
157
|
+
}
|
|
158
|
+
return a.name.localeCompare(b.name);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return `<div class="folder-view-list">
|
|
162
|
+
${sorted.map(item => this._renderItem(item)).join('')}
|
|
163
|
+
</div>`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Render a single item (file or folder)
|
|
168
|
+
* @param {Object} item
|
|
169
|
+
* @returns {string} HTML
|
|
170
|
+
*/
|
|
171
|
+
_renderItem(item) {
|
|
172
|
+
const isFolder = item.type === 'folder';
|
|
173
|
+
const icon = isFolder ? 'bi-folder-fill folder-icon' : this._getFileIcon(item.name);
|
|
174
|
+
const isExpanded = this.expandedFolders.has(item.path);
|
|
175
|
+
|
|
176
|
+
// TASK-240: Add 'into' action for folders (enter folder view)
|
|
177
|
+
const actions = isFolder
|
|
178
|
+
? ['into', 'rename', 'delete', 'duplicate']
|
|
179
|
+
: ['rename', 'delete', 'duplicate', 'download'];
|
|
180
|
+
|
|
181
|
+
// TASK-241: Add draggable support
|
|
182
|
+
return `
|
|
183
|
+
<div class="folder-view-item ${isFolder ? 'is-folder' : 'is-file'} ${isExpanded ? 'expanded' : ''}"
|
|
184
|
+
data-path="${item.path}"
|
|
185
|
+
data-type="${item.type}"
|
|
186
|
+
data-name="${this._escapeHtml(item.name)}"
|
|
187
|
+
draggable="true">
|
|
188
|
+
<div class="folder-view-item-main">
|
|
189
|
+
${isFolder ? `
|
|
190
|
+
<button class="folder-view-item-toggle" title="Expand folder">
|
|
191
|
+
<i class="bi bi-chevron-right"></i>
|
|
192
|
+
</button>
|
|
193
|
+
` : '<span class="folder-view-item-spacer"></span>'}
|
|
194
|
+
<i class="folder-view-item-icon bi ${icon}"></i>
|
|
195
|
+
<span class="folder-view-item-name">${this._escapeHtml(item.name)}</span>
|
|
196
|
+
<div class="folder-view-item-actions">
|
|
197
|
+
${actions.map(action => `
|
|
198
|
+
<button class="folder-view-item-action ${action === 'delete' ? 'danger' : ''} ${action === 'into' ? 'into-btn' : ''}"
|
|
199
|
+
data-action="${action}"
|
|
200
|
+
title="${this._getActionTitle(action)}">
|
|
201
|
+
<i class="bi bi-${this._getActionIcon(action)}"></i>
|
|
202
|
+
</button>
|
|
203
|
+
`).join('')}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
${isFolder ? '<div class="folder-view-item-children"></div>' : ''}
|
|
207
|
+
</div>
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get file icon based on extension
|
|
213
|
+
* @param {string} filename
|
|
214
|
+
* @returns {string} Bootstrap icon class
|
|
215
|
+
*/
|
|
216
|
+
_getFileIcon(filename) {
|
|
217
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
218
|
+
const iconMap = {
|
|
219
|
+
'md': 'bi-markdown',
|
|
220
|
+
'txt': 'bi-file-text',
|
|
221
|
+
'json': 'bi-braces',
|
|
222
|
+
'html': 'bi-filetype-html',
|
|
223
|
+
'css': 'bi-filetype-css',
|
|
224
|
+
'js': 'bi-filetype-js',
|
|
225
|
+
'py': 'bi-filetype-py',
|
|
226
|
+
'pdf': 'bi-file-pdf',
|
|
227
|
+
'png': 'bi-file-image',
|
|
228
|
+
'jpg': 'bi-file-image',
|
|
229
|
+
'jpeg': 'bi-file-image',
|
|
230
|
+
'gif': 'bi-file-image',
|
|
231
|
+
'svg': 'bi-file-image'
|
|
232
|
+
};
|
|
233
|
+
return iconMap[ext] || 'bi-file-earmark';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get action icon
|
|
238
|
+
* @param {string} action
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
_getActionIcon(action) {
|
|
242
|
+
const icons = {
|
|
243
|
+
'into': 'box-arrow-in-right', // TASK-240: Enter folder icon
|
|
244
|
+
'rename': 'pencil',
|
|
245
|
+
'delete': 'trash',
|
|
246
|
+
'duplicate': 'copy',
|
|
247
|
+
'download': 'download'
|
|
248
|
+
};
|
|
249
|
+
return icons[action] || action;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get action title
|
|
254
|
+
* @param {string} action
|
|
255
|
+
* @returns {string}
|
|
256
|
+
*/
|
|
257
|
+
_getActionTitle(action) {
|
|
258
|
+
const titles = {
|
|
259
|
+
'into': 'Enter folder', // TASK-240: Enter folder tooltip
|
|
260
|
+
'rename': 'Rename',
|
|
261
|
+
'delete': 'Delete',
|
|
262
|
+
'duplicate': 'Duplicate',
|
|
263
|
+
'download': 'Download'
|
|
264
|
+
};
|
|
265
|
+
return titles[action] || action;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Bind event listeners
|
|
270
|
+
*/
|
|
271
|
+
_bindEvents() {
|
|
272
|
+
const container = this.container.querySelector('.folder-view');
|
|
273
|
+
if (!container) return;
|
|
274
|
+
|
|
275
|
+
// Close button
|
|
276
|
+
const closeBtn = container.querySelector('.folder-view-close');
|
|
277
|
+
if (closeBtn) {
|
|
278
|
+
closeBtn.addEventListener('click', () => {
|
|
279
|
+
if (this.onClose) this.onClose();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Breadcrumb navigation
|
|
284
|
+
container.querySelectorAll('.breadcrumb-item.clickable').forEach(item => {
|
|
285
|
+
item.addEventListener('click', () => {
|
|
286
|
+
const path = item.dataset.path;
|
|
287
|
+
if (path === '') {
|
|
288
|
+
// Navigate to root - close folder view
|
|
289
|
+
if (this.onClose) this.onClose();
|
|
290
|
+
} else {
|
|
291
|
+
this.render(path);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Action bar buttons
|
|
297
|
+
container.querySelectorAll('.folder-view-action-btn').forEach(btn => {
|
|
298
|
+
btn.addEventListener('click', () => {
|
|
299
|
+
const action = btn.dataset.action;
|
|
300
|
+
this._handleFolderAction(action);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Item clicks (file navigation, folder expansion)
|
|
305
|
+
container.querySelectorAll('.folder-view-item').forEach(item => {
|
|
306
|
+
// Main area click (not action buttons)
|
|
307
|
+
const mainArea = item.querySelector('.folder-view-item-main');
|
|
308
|
+
mainArea.addEventListener('click', (e) => {
|
|
309
|
+
// Ignore if clicking on action buttons or toggle
|
|
310
|
+
if (e.target.closest('.folder-view-item-actions') ||
|
|
311
|
+
e.target.closest('.folder-view-item-toggle')) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const path = item.dataset.path;
|
|
316
|
+
const type = item.dataset.type;
|
|
317
|
+
|
|
318
|
+
if (type === 'file' && this.onNavigate) {
|
|
319
|
+
this.onNavigate(path);
|
|
320
|
+
} else if (type === 'folder') {
|
|
321
|
+
this._toggleFolder(item);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Toggle button for folders
|
|
326
|
+
const toggle = item.querySelector('.folder-view-item-toggle');
|
|
327
|
+
if (toggle) {
|
|
328
|
+
toggle.addEventListener('click', (e) => {
|
|
329
|
+
e.stopPropagation();
|
|
330
|
+
this._toggleFolder(item);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Item action buttons
|
|
335
|
+
item.querySelectorAll('.folder-view-item-action').forEach(btn => {
|
|
336
|
+
btn.addEventListener('click', (e) => {
|
|
337
|
+
e.stopPropagation();
|
|
338
|
+
const action = btn.dataset.action;
|
|
339
|
+
const path = item.dataset.path;
|
|
340
|
+
const name = item.dataset.name;
|
|
341
|
+
const type = item.dataset.type;
|
|
342
|
+
this._handleItemAction(action, path, name, type);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// TASK-241: Drag and drop events
|
|
347
|
+
this._bindDragEvents(item);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// TASK-241: Allow dropping on the folder view content area (move to current folder)
|
|
351
|
+
const contentArea = container.querySelector('.folder-view-content');
|
|
352
|
+
if (contentArea) {
|
|
353
|
+
this._bindDropZone(contentArea);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* TASK-241: Bind drag events to an item
|
|
359
|
+
* @param {HTMLElement} item
|
|
360
|
+
*/
|
|
361
|
+
_bindDragEvents(item) {
|
|
362
|
+
item.addEventListener('dragstart', (e) => {
|
|
363
|
+
e.stopPropagation();
|
|
364
|
+
const path = item.dataset.path;
|
|
365
|
+
const type = item.dataset.type;
|
|
366
|
+
const name = item.dataset.name;
|
|
367
|
+
|
|
368
|
+
e.dataTransfer.setData('text/plain', JSON.stringify({ path, type, name }));
|
|
369
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
370
|
+
item.classList.add('dragging');
|
|
371
|
+
|
|
372
|
+
// Store reference for drop validation
|
|
373
|
+
this._draggingItem = { path, type, name };
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
item.addEventListener('dragend', (e) => {
|
|
377
|
+
item.classList.remove('dragging');
|
|
378
|
+
this._draggingItem = null;
|
|
379
|
+
|
|
380
|
+
// Clean up any drop-target classes
|
|
381
|
+
this.container.querySelectorAll('.drop-target').forEach(el => {
|
|
382
|
+
el.classList.remove('drop-target');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Only folders can be drop targets
|
|
387
|
+
if (item.dataset.type === 'folder') {
|
|
388
|
+
item.addEventListener('dragover', (e) => {
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
e.stopPropagation();
|
|
391
|
+
|
|
392
|
+
// Validate drop is allowed
|
|
393
|
+
if (this._canDropOn(item.dataset.path)) {
|
|
394
|
+
e.dataTransfer.dropEffect = 'move';
|
|
395
|
+
item.classList.add('drop-target');
|
|
396
|
+
} else {
|
|
397
|
+
e.dataTransfer.dropEffect = 'none';
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
item.addEventListener('dragleave', (e) => {
|
|
402
|
+
// Only remove if actually leaving this element
|
|
403
|
+
if (!item.contains(e.relatedTarget)) {
|
|
404
|
+
item.classList.remove('drop-target');
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
item.addEventListener('drop', async (e) => {
|
|
409
|
+
e.preventDefault();
|
|
410
|
+
e.stopPropagation();
|
|
411
|
+
item.classList.remove('drop-target');
|
|
412
|
+
|
|
413
|
+
const targetPath = item.dataset.path;
|
|
414
|
+
await this._handleDrop(e, targetPath);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* TASK-241: Bind drop zone events (for dropping into current folder)
|
|
421
|
+
* @param {HTMLElement} zone
|
|
422
|
+
*/
|
|
423
|
+
_bindDropZone(zone) {
|
|
424
|
+
zone.addEventListener('dragover', (e) => {
|
|
425
|
+
// Only handle if not over a folder item
|
|
426
|
+
if (!e.target.closest('.folder-view-item.is-folder')) {
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
if (this._canDropOn(this.currentPath)) {
|
|
429
|
+
e.dataTransfer.dropEffect = 'move';
|
|
430
|
+
zone.classList.add('drop-target-zone');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
zone.addEventListener('dragleave', (e) => {
|
|
436
|
+
if (!zone.contains(e.relatedTarget)) {
|
|
437
|
+
zone.classList.remove('drop-target-zone');
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
zone.addEventListener('drop', async (e) => {
|
|
442
|
+
// Only handle if not over a folder item
|
|
443
|
+
if (!e.target.closest('.folder-view-item.is-folder')) {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
zone.classList.remove('drop-target-zone');
|
|
446
|
+
await this._handleDrop(e, this.currentPath);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* TASK-241: Check if item can be dropped on target
|
|
453
|
+
* @param {string} targetPath
|
|
454
|
+
* @returns {boolean}
|
|
455
|
+
*/
|
|
456
|
+
_canDropOn(targetPath) {
|
|
457
|
+
if (!this._draggingItem) return false;
|
|
458
|
+
|
|
459
|
+
const sourcePath = this._draggingItem.path;
|
|
460
|
+
|
|
461
|
+
// Can't drop on self
|
|
462
|
+
if (sourcePath === targetPath) return false;
|
|
463
|
+
|
|
464
|
+
// Can't drop parent into child
|
|
465
|
+
if (targetPath.startsWith(sourcePath + '/')) return false;
|
|
466
|
+
|
|
467
|
+
// Can't drop into same parent (already there)
|
|
468
|
+
const sourceParent = sourcePath.substring(0, sourcePath.lastIndexOf('/'));
|
|
469
|
+
if (sourceParent === targetPath) return false;
|
|
470
|
+
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* TASK-241: Handle drop event
|
|
476
|
+
* @param {DragEvent} e
|
|
477
|
+
* @param {string} targetPath
|
|
478
|
+
*/
|
|
479
|
+
async _handleDrop(e, targetPath) {
|
|
480
|
+
try {
|
|
481
|
+
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
|
|
482
|
+
const sourcePath = data.path;
|
|
483
|
+
|
|
484
|
+
if (!this._canDropOn(targetPath)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Call move action through onAction callback
|
|
489
|
+
if (this.onAction) {
|
|
490
|
+
const success = await this.onAction('move', sourcePath, {
|
|
491
|
+
targetPath,
|
|
492
|
+
name: data.name,
|
|
493
|
+
type: data.type
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
if (success) {
|
|
497
|
+
await this.refresh();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error('Drop failed:', error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Handle folder-level actions (from action bar)
|
|
507
|
+
* @param {string} action
|
|
508
|
+
*/
|
|
509
|
+
async _handleFolderAction(action) {
|
|
510
|
+
switch (action) {
|
|
511
|
+
case 'add-file':
|
|
512
|
+
if (this.onAction) {
|
|
513
|
+
await this.onAction('add-file', this.currentPath, {});
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
case 'add-folder':
|
|
517
|
+
if (this.onAction) {
|
|
518
|
+
await this.onAction('add-folder', this.currentPath, {});
|
|
519
|
+
}
|
|
520
|
+
break;
|
|
521
|
+
case 'rename':
|
|
522
|
+
if (this.onAction) {
|
|
523
|
+
await this.onAction('rename', this.currentPath, { type: 'folder' });
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
case 'delete':
|
|
527
|
+
await this._confirmAndDelete(this.currentPath, 'folder');
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Handle item-level actions
|
|
534
|
+
* @param {string} action
|
|
535
|
+
* @param {string} path
|
|
536
|
+
* @param {string} name
|
|
537
|
+
* @param {string} type
|
|
538
|
+
*/
|
|
539
|
+
async _handleItemAction(action, path, name, type) {
|
|
540
|
+
switch (action) {
|
|
541
|
+
case 'into': // TASK-240: Navigate into folder
|
|
542
|
+
if (type === 'folder') {
|
|
543
|
+
await this.render(path);
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
case 'rename':
|
|
547
|
+
if (this.onAction) {
|
|
548
|
+
await this.onAction('rename', path, { name, type });
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
case 'delete':
|
|
552
|
+
await this._confirmAndDelete(path, type, name);
|
|
553
|
+
break;
|
|
554
|
+
case 'duplicate':
|
|
555
|
+
if (this.onAction) {
|
|
556
|
+
const success = await this.onAction('duplicate', path, { name, type });
|
|
557
|
+
if (success) {
|
|
558
|
+
await this.refresh();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
case 'download':
|
|
563
|
+
this._downloadFile(path);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Confirm and delete an item
|
|
570
|
+
* @param {string} path
|
|
571
|
+
* @param {string} type
|
|
572
|
+
* @param {string} name
|
|
573
|
+
*/
|
|
574
|
+
async _confirmAndDelete(path, type, name = null) {
|
|
575
|
+
// Get delete info for confirmation
|
|
576
|
+
let itemCount = 0;
|
|
577
|
+
const itemName = name || path.split('/').pop();
|
|
578
|
+
|
|
579
|
+
if (type === 'folder') {
|
|
580
|
+
try {
|
|
581
|
+
const response = await fetch(`/api/ideas/delete-info?path=${encodeURIComponent(path)}`);
|
|
582
|
+
const data = await response.json();
|
|
583
|
+
if (data.success) {
|
|
584
|
+
itemCount = data.item_count || 0;
|
|
585
|
+
}
|
|
586
|
+
} catch (e) {
|
|
587
|
+
console.warn('Failed to get delete info:', e);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Show confirmation
|
|
592
|
+
let confirmed = true;
|
|
593
|
+
if (this.confirmDialog) {
|
|
594
|
+
confirmed = await this.confirmDialog.confirmDelete(itemName, type, itemCount);
|
|
595
|
+
} else {
|
|
596
|
+
const msg = type === 'folder' && itemCount > 0
|
|
597
|
+
? `Delete "${itemName}" and all ${itemCount} items inside?`
|
|
598
|
+
: `Delete "${itemName}"?`;
|
|
599
|
+
confirmed = confirm(msg);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (confirmed && this.onAction) {
|
|
603
|
+
const success = await this.onAction('delete', path, { name: itemName, type });
|
|
604
|
+
if (success) {
|
|
605
|
+
if (path === this.currentPath) {
|
|
606
|
+
// Deleted current folder - close view
|
|
607
|
+
if (this.onClose) this.onClose();
|
|
608
|
+
} else {
|
|
609
|
+
await this.refresh();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Download a file
|
|
617
|
+
* @param {string} path
|
|
618
|
+
*/
|
|
619
|
+
_downloadFile(path) {
|
|
620
|
+
const url = `/api/ideas/download?path=${encodeURIComponent(path)}`;
|
|
621
|
+
const link = document.createElement('a');
|
|
622
|
+
link.href = url;
|
|
623
|
+
link.download = path.split('/').pop();
|
|
624
|
+
document.body.appendChild(link);
|
|
625
|
+
link.click();
|
|
626
|
+
document.body.removeChild(link);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Toggle folder expansion
|
|
631
|
+
* @param {HTMLElement} folderItem
|
|
632
|
+
*/
|
|
633
|
+
async _toggleFolder(folderItem) {
|
|
634
|
+
const path = folderItem.dataset.path;
|
|
635
|
+
const childrenContainer = folderItem.querySelector('.folder-view-item-children');
|
|
636
|
+
|
|
637
|
+
if (!childrenContainer) return;
|
|
638
|
+
|
|
639
|
+
const isExpanded = folderItem.classList.contains('expanded');
|
|
640
|
+
|
|
641
|
+
if (isExpanded) {
|
|
642
|
+
// Collapse
|
|
643
|
+
folderItem.classList.remove('expanded');
|
|
644
|
+
childrenContainer.innerHTML = '';
|
|
645
|
+
this.expandedFolders.delete(path);
|
|
646
|
+
} else {
|
|
647
|
+
// Expand - load children
|
|
648
|
+
try {
|
|
649
|
+
const contents = await this._loadContents(path);
|
|
650
|
+
childrenContainer.innerHTML = this._renderContents(contents);
|
|
651
|
+
folderItem.classList.add('expanded');
|
|
652
|
+
this.expandedFolders.add(path);
|
|
653
|
+
|
|
654
|
+
// Rebind events for new items
|
|
655
|
+
this._bindChildEvents(childrenContainer);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
console.error('Failed to expand folder:', error);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Bind events for dynamically loaded children
|
|
664
|
+
* @param {HTMLElement} container
|
|
665
|
+
*/
|
|
666
|
+
_bindChildEvents(container) {
|
|
667
|
+
container.querySelectorAll('.folder-view-item').forEach(item => {
|
|
668
|
+
const mainArea = item.querySelector('.folder-view-item-main');
|
|
669
|
+
mainArea.addEventListener('click', (e) => {
|
|
670
|
+
if (e.target.closest('.folder-view-item-actions') ||
|
|
671
|
+
e.target.closest('.folder-view-item-toggle')) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const path = item.dataset.path;
|
|
676
|
+
const type = item.dataset.type;
|
|
677
|
+
|
|
678
|
+
if (type === 'file' && this.onNavigate) {
|
|
679
|
+
this.onNavigate(path);
|
|
680
|
+
} else if (type === 'folder') {
|
|
681
|
+
this._toggleFolder(item);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const toggle = item.querySelector('.folder-view-item-toggle');
|
|
686
|
+
if (toggle) {
|
|
687
|
+
toggle.addEventListener('click', (e) => {
|
|
688
|
+
e.stopPropagation();
|
|
689
|
+
this._toggleFolder(item);
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
item.querySelectorAll('.folder-view-item-action').forEach(btn => {
|
|
694
|
+
btn.addEventListener('click', (e) => {
|
|
695
|
+
e.stopPropagation();
|
|
696
|
+
const action = btn.dataset.action;
|
|
697
|
+
const path = item.dataset.path;
|
|
698
|
+
const name = item.dataset.name;
|
|
699
|
+
const type = item.dataset.type;
|
|
700
|
+
this._handleItemAction(action, path, name, type);
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// TASK-241: Bind drag events for dynamically loaded children
|
|
705
|
+
this._bindDragEvents(item);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Refresh current folder view
|
|
711
|
+
*/
|
|
712
|
+
async refresh() {
|
|
713
|
+
if (this.currentPath) {
|
|
714
|
+
await this.render(this.currentPath);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Close the folder view
|
|
720
|
+
*/
|
|
721
|
+
close() {
|
|
722
|
+
this.currentPath = null;
|
|
723
|
+
this.expandedFolders.clear();
|
|
724
|
+
this.container.innerHTML = '';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Escape HTML to prevent XSS
|
|
729
|
+
* @param {string} str
|
|
730
|
+
* @returns {string}
|
|
731
|
+
*/
|
|
732
|
+
_escapeHtml(str) {
|
|
733
|
+
const div = document.createElement('div');
|
|
734
|
+
div.textContent = str;
|
|
735
|
+
return div.innerHTML;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Export for module usage
|
|
740
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
741
|
+
module.exports = FolderViewManager;
|
|
742
|
+
}
|