diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index 2d6f7d9..9bf4329 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -34,10 +34,10 @@ '.files-breadcrumb-item:hover{background:var(--vt-color-accent-muted,rgba(78,204,163,.14))}', '.files-breadcrumb-current{color:var(--vt-color-text-primary,#f4f7fb);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', '.files-breadcrumb-sep{color:var(--vt-color-text-muted,#7f8aa3)}', - '.files-filter,.files-sort,.files-create-input,.files-rename-input{font-size:.78rem;padding:.32rem .5rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);outline:none}', + '.files-filter,.files-sort{font-size:.78rem;padding:.32rem .5rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);outline:none}', '.files-filter{width:11rem}', '.files-sort{width:9.5rem;appearance:none;background-color:#0f1424;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.6rem}.files-sort option{background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb)}', - '.files-filter:focus,.files-sort:focus,.files-create-input:focus,.files-rename-input:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}', + '.files-filter:focus,.files-sort:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}', '.files-list{flex:1;overflow:auto;min-height:0}', '.files-header,.files-item{display:grid;grid-template-columns:minmax(160px,1fr) 90px 90px 150px 220px;align-items:center;gap:.5rem;padding:.38rem .75rem;border-bottom:1px solid rgba(22,33,62,.55)}', '.files-header{position:sticky;top:0;background:var(--vt-color-surface-muted,#111629);color:var(--vt-color-text-muted,#7f8aa3);font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;z-index:1}', @@ -60,10 +60,6 @@ '.files-empty-btn svg{width:15px;height:15px;display:block;fill:currentColor}', '.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--vt-color-danger,#e94560);gap:.5rem;padding:1rem}', '.files-error-msg{font-size:.85rem;color:var(--vt-color-text-secondary,#b7c0d4);max-width:420px;text-align:center}', - '.files-panel{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-top:1px solid var(--vt-color-border,#202b46);flex-shrink:0;background:var(--vt-color-surface-muted,#111629)}', - '.files-field-stack{display:flex;flex:1;min-width:160px;flex-direction:column;gap:.25rem}', - '.files-panel-error{display:none;color:#ff9aaa;font-size:.72rem;line-height:1.2}', - '.files-create-input,.files-rename-input{flex:1;min-width:160px}', '@media(max-width:760px){.files-header,.files-item{grid-template-columns:minmax(130px,1fr) 70px 0 0 150px}.files-header span:nth-child(3),.files-header span:nth-child(4),.files-item-meta.hide-narrow{display:none}.files-toolbar{align-items:stretch}.files-filter,.files-sort{width:100%}}', '.files-ctx-menu{position:fixed;z-index:9999;min-width:180px;background:var(--vt-color-surface,#15152c);border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);padding:6px 0;box-shadow:var(--vt-elevation-menu,0 14px 32px rgba(0,0,0,.42));font-size:.84rem;color:var(--vt-color-text-primary,#f4f7fb);user-select:none}', '.files-ctx-menu-item{padding:6px 16px;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:.5rem}', @@ -75,6 +71,12 @@ '.files-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center}', '.files-modal{width:400px;max-width:90vw;padding:24px;background:#1a1a2e;border:1px solid #333;border-radius:12px;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.5)}', '.files-modal-title{font-size:.95rem;line-height:1.5;margin-bottom:20px;word-wrap:break-word}', + '.files-modal-form{display:grid;gap:.65rem}', + '.files-modal-form .files-modal-title{margin-bottom:0}', + '.files-modal-input{width:100%;font:inherit;font-size:.86rem;padding:.52rem .65rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);outline:none}', + '.files-modal-input:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}', + '.files-modal-input[aria-invalid="true"]{border-color:var(--vt-color-danger,#e94560)}', + '.files-modal-error{display:none;color:#ff9aaa;font-size:.78rem;line-height:1.35}', '.files-modal-actions{display:flex;justify-content:flex-end;gap:8px}', '.files-modal-btn{font-size:.82rem;padding:.4rem 1rem;border:1px solid #333;border-radius:6px;cursor:pointer;font-family:inherit}', '.files-modal-btn.cancel{background:#2a2a4e;color:#ccc;border-color:#444}', @@ -346,7 +348,7 @@ var workspaceNode = props && props.workspaceNode; var workspaceRoot = cleanPath(props && (props.workspaceRootPath || (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id))) || ''); - var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title || workspaceNode.id)) || 'Workspace'; + var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title || workspaceNode.id)) || ''; window.__filesHistoryByWorkspace = window.__filesHistoryByWorkspace || {}; var historyKey = workspaceRoot || workspaceName; var savedHistory = window.__filesHistoryByWorkspace[historyKey] || { stack: [''], index: 0, currentPath: '' }; @@ -358,6 +360,12 @@ var sortMode = 'folder-name'; var createMode = ''; var renameTarget = null; + var createModal = null; + var createInput = null; + var createError = null; + var renameModal = null; + var renameInput = null; + var renameError = null; var disposed = false; var fileActions = []; var contextMenuEntries = []; @@ -438,32 +446,6 @@ var listContainer = el('div', { className: 'files-list', 'data-files-list': '' }); containerEl.appendChild(listContainer); - var createPanel = el('div', { className: 'files-panel', style: { display: 'none' } }); - var createField = el('div', { className: 'files-field-stack' }); - var createInput = el('input', { className: 'files-create-input', 'data-files-create-input': '' }); - var createError = el('div', { className: 'files-panel-error', 'data-files-create-error': '', role: 'alert' }); - var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, [tr('ui.create', null, 'Create')]); - var createCancel = el('button', { className: 'files-toolbar-btn' }, [tr('ui.cancel', null, 'Cancel')]); - createField.appendChild(createInput); - createField.appendChild(createError); - createPanel.appendChild(createField); - createPanel.appendChild(createConfirm); - createPanel.appendChild(createCancel); - containerEl.appendChild(createPanel); - - var renamePanel = el('div', { className: 'files-panel', style: { display: 'none' } }); - var renameField = el('div', { className: 'files-field-stack' }); - var renameInput = el('input', { className: 'files-rename-input', 'data-files-rename-input': '' }); - var renameError = el('div', { className: 'files-panel-error', 'data-files-rename-error': '', role: 'alert' }); - var renameConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-rename-confirm': '' }, [tr('ui.rename', null, 'Rename')]); - var renameCancel = el('button', { className: 'files-toolbar-btn' }, [tr('ui.cancel', null, 'Cancel')]); - renameField.appendChild(renameInput); - renameField.appendChild(renameError); - renamePanel.appendChild(renameField); - renamePanel.appendChild(renameConfirm); - renamePanel.appendChild(renameCancel); - containerEl.appendChild(renamePanel); - function selectedEntry() { var keys = Object.keys(selectedPaths); if (keys.length === 0) return null; @@ -820,35 +802,131 @@ }).catch(function (err) { console.error('[files] openResource error:', err); }); } + function showFileFormModal(mode, details) { + var isRename = mode === 'rename'; + var overlay = el('div', { + className: 'files-modal-overlay', + role: 'presentation' + }); + overlay.setAttribute('data-files-' + mode + '-modal', ''); + var input = el('input', { + className: 'files-modal-input', + type: 'text', + value: details.value || '', + placeholder: details.placeholder || '', + autocomplete: 'off', + 'aria-invalid': 'false' + }); + input.setAttribute('data-files-' + mode + '-input', ''); + input.value = details.value || ''; + var error = el('div', { + className: 'files-modal-error', + role: 'alert' + }); + error.setAttribute('data-files-' + mode + '-error', ''); + function closeForm() { + if (isRename) cancelRename(); + else cancelCreate(); + } + function confirmForm() { + if (isRename) confirmRename(); + else confirmCreate(); + } + input.addEventListener('input', function () { + if (isRename) setRenameError(''); + else setCreateError(''); + }); + input.addEventListener('keydown', function (event) { + if (event.key === 'Enter') { + if (event.preventDefault) event.preventDefault(); + confirmForm(); + } + if (event.key === 'Escape') { + if (event.preventDefault) event.preventDefault(); + closeForm(); + } + }); + var modal = el('div', { + className: 'files-modal files-modal-form', + role: 'dialog', + 'aria-modal': 'true', + 'aria-label': details.title + }, [ + el('div', { className: 'files-modal-title', textContent: details.title }), + input, + error, + el('div', { className: 'files-modal-actions' }, [ + el('button', { + className: 'files-modal-btn cancel', + type: 'button', + textContent: tr('ui.cancel', null, 'Cancel'), + onClick: closeForm + }), + el('button', { + className: 'files-modal-btn confirm', + type: 'button', + textContent: details.confirmLabel, + onClick: confirmForm + }) + ]) + ]); + overlay.appendChild(modal); + document.body.appendChild(overlay); + if (isRename) { + renameInput = input; + renameError = error; + } else { + createInput = input; + createError = error; + } + input.focus(); + if (isRename && input.select) input.select(); + return overlay; + } + function startCreate(mode) { + cancelCreate(); createMode = mode; - createInput.value = ''; - createInput.placeholder = mode === 'folder' ? 'Folder name' : (mode === 'markdown' ? 'Markdown file name' : 'Text file name'); - setCreateError(''); - createPanel.style.display = 'flex'; - createInput.focus(); + createModal = showFileFormModal('create', { + title: mode === 'folder' + ? tr('ui.createFolderTitle', null, 'Create folder') + : (mode === 'markdown' + ? tr('ui.createMarkdownTitle', null, 'Create Markdown file') + : tr('ui.createTextTitle', null, 'Create text file')), + placeholder: mode === 'folder' + ? tr('ui.folderName', null, 'Folder name') + : (mode === 'markdown' + ? tr('ui.markdownFileName', null, 'Markdown file name') + : tr('ui.textFileName', null, 'Text file name')), + confirmLabel: tr('ui.create', null, 'Create') + }); } function setCreateError(message) { + if (!createError || !createInput) return; createError.textContent = message || ''; createError.style.display = message ? 'block' : 'none'; createInput.setAttribute('aria-invalid', message ? 'true' : 'false'); } function validateCreateName(name) { - if (!name) return 'Name is required'; - if (/[\\/:*?"<>|\x00-\x1f]/.test(name)) return 'Invalid characters in name'; - if (name === '.' || name === '..' || name[0] === ' ' || name[name.length - 1] === ' ' || name[name.length - 1] === '.') return 'Invalid name'; + if (!name) return tr('ui.nameRequired', null, 'Enter a name.'); + if (/[\\/:*?"<>|\x00-\x1f]/.test(name)) return tr('ui.invalidCharacters', null, 'The name contains invalid characters.'); + if (name === '.' || name === '..' || name[0] === ' ' || name[name.length - 1] === ' ' || name[name.length - 1] === '.') return tr('ui.invalidName', null, 'Enter a valid name.'); return ''; } function cancelCreate() { createMode = ''; - setCreateError(''); - createPanel.style.display = 'none'; + if (createModal && typeof createModal.remove === 'function') createModal.remove(); + else if (createModal && createModal.parentNode) createModal.parentNode.removeChild(createModal); + createModal = null; + createInput = null; + createError = null; } function confirmCreate() { + if (!createInput || !createMode) return; var name = createInput.value.trim(); var validationError = validateCreateName(name); if (validationError) { @@ -877,39 +955,49 @@ function beginRename(entry) { entry = entry || selectedEntry(); if (!entry) return; + cancelRename(); renameTarget = entry; - renameInput.value = entry.name; - setRenameError(''); - renamePanel.style.display = 'flex'; - renameInput.focus(); - renameInput.select(); + renameModal = showFileFormModal('rename', { + title: tr('ui.renameTitle', { name: entry.name }, 'Rename ' + entry.name), + placeholder: tr('ui.fileName', null, 'File or folder name'), + value: entry.name, + confirmLabel: tr('ui.rename', null, 'Rename') + }); } function cancelRename() { renameTarget = null; - setRenameError(''); - renamePanel.style.display = 'none'; + if (renameModal && typeof renameModal.remove === 'function') renameModal.remove(); + else if (renameModal && renameModal.parentNode) renameModal.parentNode.removeChild(renameModal); + renameModal = null; + renameInput = null; + renameError = null; } function setRenameError(message) { + if (!renameError || !renameInput) return; renameError.textContent = message || ''; renameError.style.display = message ? 'block' : 'none'; renameInput.setAttribute('aria-invalid', message ? 'true' : 'false'); } function confirmRename() { - if (!renameTarget) return; + if (!renameTarget || !renameInput) return; var newName = renameInput.value.trim(); - if (!newName || newName === renameTarget.name) { + if (!newName) { + setRenameError(tr('ui.nameRequired', null, 'Enter a name.')); + return; + } + if (newName === renameTarget.name) { cancelRename(); return; } if (/[\\/:*?"<>|\x00-\x1f]/.test(newName)) { - setRenameError('Invalid characters in name'); + setRenameError(tr('ui.invalidCharacters', null, 'The name contains invalid characters.')); return; } if (newName === '.' || newName === '..' || newName[0] === ' ' || newName[newName.length - 1] === ' ' || newName[newName.length - 1] === '.') { - setRenameError('Invalid name'); + setRenameError(tr('ui.invalidName', null, 'Enter a valid name.')); return; } var from = renameTarget.relativePath; @@ -917,17 +1005,17 @@ var to = targetParent ? targetParent + '/' + newName : newName; api.files.metadata(to).then(function () { if (to.toLowerCase() === from.toLowerCase() && to !== from) { - setRenameError('Name differs only by case'); + setRenameError(tr('ui.nameDiffersOnlyByCase', null, 'The name differs only by letter case.')); return; } - setRenameError('A file with that name already exists'); + setRenameError(tr('ui.nameConflict', null, 'An item with that name already exists.')); }, function () { api.files.move(from, to, { overwrite: false }).then(function () { cancelRename(); loadEntries(); }).catch(function (err) { if (isConflictError(err)) { - setRenameError('A file with that name already exists'); + setRenameError(tr('ui.nameConflict', null, 'An item with that name already exists.')); return; } setRenameError(reportError('ui.renameError', 'Could not rename this item. Please try again.', err)); @@ -963,14 +1051,6 @@ filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); }); sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); }); - createConfirm.addEventListener('click', confirmCreate); - createCancel.addEventListener('click', cancelCreate); - renameConfirm.addEventListener('click', confirmRename); - renameCancel.addEventListener('click', cancelRename); - createInput.addEventListener('input', function () { setCreateError(''); }); - createInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmCreate(); if (event.key === 'Escape') cancelCreate(); }); - renameInput.addEventListener('input', function () { setRenameError(''); }); - renameInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmRename(); if (event.key === 'Escape') cancelRename(); }); /* --- Context menu --- */ var ctxMenu = el('div', { className: 'files-ctx-menu', style: { display: 'none' } }); document.body.appendChild(ctxMenu); @@ -1508,10 +1588,6 @@ item[0].setAttribute('aria-label', label); }); filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter current folder')); - createConfirm.textContent = tr('ui.create', null, 'Create'); - createCancel.textContent = tr('ui.cancel', null, 'Cancel'); - renameConfirm.textContent = tr('ui.rename', null, 'Rename'); - renameCancel.textContent = tr('ui.cancel', null, 'Cancel'); renderList(); }); } @@ -1530,6 +1606,8 @@ containerEl.__filesCleanup = function () { disposed = true; + cancelCreate(); + cancelRename(); if (typeof localeUnsubscribe === 'function') localeUnsubscribe(); if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe(); document.removeEventListener('click', onDocClick); diff --git a/plugins/files/locales/en.json b/plugins/files/locales/en.json index c8f900d..6ca34fd 100644 --- a/plugins/files/locales/en.json +++ b/plugins/files/locales/en.json @@ -1,6 +1,6 @@ { "manifest.name": "Files", - "manifest.description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.", + "manifest.description": "Deal-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.", "contributions.workspaceItems.verstak.files.workspace.title": "Files", "ui.back": "Back", "ui.forward": "Forward", @@ -24,6 +24,19 @@ "ui.column.actions": "Actions", "ui.create": "Create", "ui.cancel": "Cancel", + "ui.createFolderTitle": "Create folder", + "ui.createMarkdownTitle": "Create Markdown file", + "ui.createTextTitle": "Create text file", + "ui.renameTitle": "Rename {name}", + "ui.folderName": "Folder name", + "ui.markdownFileName": "Markdown file name", + "ui.textFileName": "Text file name", + "ui.fileName": "File or folder name", + "ui.nameRequired": "Enter a name.", + "ui.invalidCharacters": "The name contains invalid characters.", + "ui.invalidName": "Enter a valid name.", + "ui.nameDiffersOnlyByCase": "The name differs only by letter case.", + "ui.nameConflict": "An item with that name already exists.", "ui.emptyFolder": "Empty folder", "ui.noMatches": "No matches", "ui.clearFilter": "Clear filter", diff --git a/plugins/files/locales/ru.json b/plugins/files/locales/ru.json index 099a4f0..5248060 100644 --- a/plugins/files/locales/ru.json +++ b/plugins/files/locales/ru.json @@ -1,6 +1,6 @@ { "manifest.name": "Файлы", - "manifest.description": "Файловый менеджер рабочего пространства с созданием, переименованием, корзиной, фильтрацией и сортировкой.", + "manifest.description": "Файловый менеджер Дела с созданием, переименованием, корзиной, фильтрацией и сортировкой.", "contributions.workspaceItems.verstak.files.workspace.title": "Файлы", "ui.back": "Назад", "ui.forward": "Вперёд", @@ -24,6 +24,19 @@ "ui.column.actions": "Действия", "ui.create": "Создать", "ui.cancel": "Отмена", + "ui.createFolderTitle": "Создать папку", + "ui.createMarkdownTitle": "Создать Markdown-файл", + "ui.createTextTitle": "Создать текстовый файл", + "ui.renameTitle": "Переименовать {name}", + "ui.folderName": "Название папки", + "ui.markdownFileName": "Название Markdown-файла", + "ui.textFileName": "Название текстового файла", + "ui.fileName": "Название файла или папки", + "ui.nameRequired": "Введите название.", + "ui.invalidCharacters": "В названии есть недопустимые символы.", + "ui.invalidName": "Введите корректное название.", + "ui.nameDiffersOnlyByCase": "Название отличается только регистром букв.", + "ui.nameConflict": "Элемент с таким названием уже существует.", "ui.emptyFolder": "Папка пуста", "ui.noMatches": "Совпадений нет", "ui.clearFilter": "Сбросить фильтр", diff --git a/plugins/files/plugin.json b/plugins/files/plugin.json index bc47b51..455a336 100644 --- a/plugins/files/plugin.json +++ b/plugins/files/plugin.json @@ -4,7 +4,7 @@ "name": "Files", "version": "0.1.0", "apiVersion": "0.1.0", - "description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.", + "description": "Deal-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.", "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "folder", diff --git a/scripts/smoke-files-plugin.js b/scripts/smoke-files-plugin.js index be0d452..5a6cfef 100755 --- a/scripts/smoke-files-plugin.js +++ b/scripts/smoke-files-plugin.js @@ -198,6 +198,8 @@ function loadFilesComponent(document) { function makeApi() { const externalCalls = []; const contributionCalls = []; + const created = []; + const moved = []; const eventHandlers = {}; let restored = false; let externalVisible = false; @@ -212,6 +214,8 @@ function makeApi() { return { externalCalls, contributionCalls, + created, + moved, emitFileChanged(payload) { (eventHandlers['file.changed'] || []).forEach((handler) => handler({ name: 'file.changed', @@ -253,9 +257,15 @@ function makeApi() { }, metadata: async () => { throw new Error('not-found'); }, readText: async () => '# Readme\n', - writeText: async () => undefined, - createFolder: async () => undefined, - move: async () => undefined, + writeText: async (relativePath, content, options) => { + created.push({ type: 'file', relativePath, content, options }); + }, + createFolder: async (relativePath) => { + created.push({ type: 'folder', relativePath }); + }, + move: async (fromRelativePath, toRelativePath) => { + moved.push({ fromRelativePath, toRelativePath }); + }, trash: async () => undefined, listTrash: async () => trashEntries.slice(), restoreTrash: async (trashId) => { @@ -328,6 +338,62 @@ async function flush() { const row = walk(container, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/readme.md'); if (!row) throw new Error('file row not rendered'); + const newFolder = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-action') === 'new-folder'); + if (!newFolder) throw new Error('new folder button not found'); + newFolder.click(); + const createModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-create-modal') !== undefined); + if (!createModal) throw new Error('create file modal not found'); + const createInput = walk(createModal, (node) => node.getAttribute && node.getAttribute('data-files-create-input') !== undefined); + if (!createInput) throw new Error('create file input not found'); + const createConfirm = walk(createModal, (node) => node.tagName === 'BUTTON' && node.textContent === 'Create'); + if (!createConfirm) throw new Error('create file confirm button not found'); + createConfirm.click(); + const createError = walk(createModal, (node) => node.getAttribute && node.getAttribute('data-files-create-error') !== undefined); + if (!createError || !createError.textContent.includes('Enter a name') || createInput.getAttribute('aria-invalid') !== 'true') { + throw new Error('create file modal does not show a validation error for an empty name'); + } + createInput.value = 'New folder'; + createInput.dispatchEvent('keydown', { key: 'Enter' }); + await flush(); + if (!api.created.some((entry) => entry.type === 'folder' && entry.relativePath === 'New folder')) { + throw new Error(`new folder was not created: ${JSON.stringify(api.created)}`); + } + if (walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-create-modal') !== undefined)) { + throw new Error('create file modal should close after a successful create'); + } + + const newMarkdown = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-action') === 'new-markdown'); + if (!newMarkdown) throw new Error('new markdown button not found'); + newMarkdown.click(); + const markdownModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-create-modal') !== undefined); + const markdownInput = walk(markdownModal, (node) => node.getAttribute && node.getAttribute('data-files-create-input') !== undefined); + markdownInput.value = 'Readme'; + markdownInput.dispatchEvent('keydown', { key: 'Enter' }); + await flush(); + if (!api.created.some((entry) => entry.type === 'file' && entry.relativePath === 'Readme.md')) { + throw new Error(`new markdown file was not created: ${JSON.stringify(api.created)}`); + } + + const rename = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-action') === 'row-rename'); + if (!rename) throw new Error('rename file button not found'); + rename.click(); + const renameModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-rename-modal') !== undefined); + if (!renameModal) throw new Error('rename file modal not found'); + const renameInput = walk(renameModal, (node) => node.getAttribute && node.getAttribute('data-files-rename-input') !== undefined); + if (!renameInput || renameInput.value !== 'readme.md') throw new Error('rename file input should keep the current filename and extension'); + renameInput.value = ''; + renameInput.dispatchEvent('keydown', { key: 'Enter' }); + const renameError = walk(renameModal, (node) => node.getAttribute && node.getAttribute('data-files-rename-error') !== undefined); + if (!renameError || !renameError.textContent.includes('Enter a name') || renameInput.getAttribute('aria-invalid') !== 'true') { + throw new Error('rename file modal does not show a validation error for an empty name'); + } + renameInput.value = 'renamed.md'; + renameInput.dispatchEvent('keydown', { key: 'Enter' }); + await flush(); + if (!api.moved.some((entry) => entry.fromRelativePath === 'Docs/readme.md' && entry.toRelativePath === 'Docs/renamed.md')) { + throw new Error(`rename did not preserve the extension: ${JSON.stringify(api.moved)}`); + } + const list = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-list') !== undefined); if (!list) throw new Error('files list not rendered'); list.dispatchEvent('contextmenu', { target: row, clientX: 20, clientY: 20 });