gui: fix sidebar icons, create modal, and type display
- TreeNode.svelte: no white spacer for leaf nodes, iconKind maps node.type directly, 32px rows with hover/selected states - App.svelte: header shows localized nodeKindLabel, auto-select created node, create modal with Пустое дело card + template descriptions, disable button until name+type chosen - i18n: add kind.folder/note/file, template descriptions, template.optionNone → Пустое дело / Empty case
This commit is contained in:
parent
a6b0f9d7e6
commit
f022f46909
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -16,8 +16,8 @@
|
||||||
background: #13131f;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-DqItQmr8.js"></script>
|
<script type="module" crossorigin src="/assets/main-BLPyZvHh.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-SsPgaNKa.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-DxSaNTHX.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -623,28 +623,31 @@
|
||||||
|
|
||||||
function openCreateRoot() {
|
function openCreateRoot() {
|
||||||
createInNode = null
|
createInNode = null
|
||||||
createWithTemplate = null
|
createWithTemplate = undefined
|
||||||
newNodeTitle = ''
|
newNodeTitle = ''
|
||||||
showCreateNode = true
|
showCreateNode = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelCreateNode() { showCreateNode = false; newNodeTitle = ''; createInNode = null; createWithTemplate = null }
|
function cancelCreateNode() { showCreateNode = false; newNodeTitle = ''; createInNode = null; createWithTemplate = undefined }
|
||||||
|
|
||||||
async function submitCreateNode() {
|
async function submitCreateNode() {
|
||||||
if (!newNodeTitle.trim()) return
|
if (!newNodeTitle.trim() || createWithTemplate === undefined) return
|
||||||
try {
|
try {
|
||||||
const parentID = createInNode ? createInNode.id : ''
|
const parentID = createInNode ? createInNode.id : ''
|
||||||
const templateID = createWithTemplate ? createWithTemplate.id : ''
|
const templateID = createWithTemplate ? createWithTemplate.id : ''
|
||||||
await wailsCall('CreateNodeFromTemplate', parentID, newNodeTitle.trim(), templateID)
|
const created = await wailsCall('CreateNodeFromTemplate', parentID, newNodeTitle.trim(), templateID)
|
||||||
showCreateNode = false
|
showCreateNode = false
|
||||||
newNodeTitle = ''
|
newNodeTitle = ''
|
||||||
|
const createdId = created ? created.id : null
|
||||||
createInNode = null
|
createInNode = null
|
||||||
createWithTemplate = null
|
createWithTemplate = undefined
|
||||||
workspaceTree = await wailsCall('ListWorkspaceTree') || workspaceTree
|
workspaceTree = await wailsCall('ListWorkspaceTree') || workspaceTree
|
||||||
if (!parentID) {
|
if (createdId) {
|
||||||
// If root node created, select it
|
const node = await wailsCall('GetNodeDetail', createdId)
|
||||||
const updated = await wailsCall('ListWorkspaceTree') || workspaceTree
|
if (node) {
|
||||||
workspaceTree = updated
|
selectedSection = ''
|
||||||
|
selectNode(node)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) { error = String(e) }
|
} catch (e) { error = String(e) }
|
||||||
}
|
}
|
||||||
|
|
@ -906,7 +909,7 @@
|
||||||
try { return new Date(str).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }) } catch (e) { return str }
|
try { return new Date(str).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }) } catch (e) { return str }
|
||||||
}
|
}
|
||||||
function nodeKindLabel(kind) {
|
function nodeKindLabel(kind) {
|
||||||
const labels = { 'project': t('kind.project'), 'client': t('kind.client'), 'document': t('kind.document'), 'recipe': t('kind.recipe'), 'archive': t('kind.archive'), 'case': t('kind.case') }
|
const labels = { 'project': t('kind.project'), 'client': t('kind.client'), 'document': t('kind.document'), 'recipe': t('kind.recipe'), 'folder': t('kind.folder'), 'note': t('kind.note'), 'file': t('kind.file'), 'archive': t('kind.archive'), 'case': t('kind.case') }
|
||||||
return labels[kind] || kind || t('kind.case')
|
return labels[kind] || kind || t('kind.case')
|
||||||
}
|
}
|
||||||
function pluralize(n, one, few, many) {
|
function pluralize(n, one, few, many) {
|
||||||
|
|
@ -1102,7 +1105,7 @@
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
{#if selectedNode}
|
{#if selectedNode}
|
||||||
<span class="crumb">{selectedNode.title}</span>
|
<span class="crumb">{selectedNode.title}</span>
|
||||||
<span class="crumb-type">{selectedNode.type}</span>
|
<span class="crumb-type">{nodeKindLabel(selectedNode.type)}</span>
|
||||||
{:else if selectedSection}
|
{:else if selectedSection}
|
||||||
<span class="crumb">{#each systemViews as v}{v.id === selectedSection ? v.label : ''}{/each}</span>
|
<span class="crumb">{#each systemViews as v}{v.id === selectedSection ? v.label : ''}{/each}</span>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -1491,7 +1494,7 @@
|
||||||
|
|
||||||
{#if showCreateNode}
|
{#if showCreateNode}
|
||||||
<div class="modal-overlay" on:click|self={cancelCreateNode}>
|
<div class="modal-overlay" on:click|self={cancelCreateNode}>
|
||||||
<div class="modal">
|
<div class="modal modal-create">
|
||||||
<h3>{createInNode ? t('nav.createInside') : t('case.new')}</h3>
|
<h3>{createInNode ? t('nav.createInside') : t('case.new')}</h3>
|
||||||
{#if createInNode}
|
{#if createInNode}
|
||||||
<div class="create-context">{t('nav.createInside')} «{createInNode.title}»</div>
|
<div class="create-context">{t('nav.createInside')} «{createInNode.title}»</div>
|
||||||
|
|
@ -1499,16 +1502,22 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{t('template.select')}</label>
|
<label>{t('template.select')}</label>
|
||||||
<div class="template-cards">
|
<div class="template-cards">
|
||||||
<button class="template-card" class:selected={!createWithTemplate}
|
<button class="template-card" class:selected={createWithTemplate === null}
|
||||||
on:click={() => createWithTemplate = null}>
|
on:click={() => createWithTemplate = null}>
|
||||||
<TemplateIcon kind="folder" size={22} />
|
<TemplateIcon kind="folder" size={24} />
|
||||||
<span>{t('template.optionNone')}</span>
|
<div class="template-card-text">
|
||||||
|
<span class="template-card-title">{t('template.optionNone')}</span>
|
||||||
|
<span class="template-card-desc">{t('template.none.desc')}</span>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{#each enabledTemplates as tpl}
|
{#each enabledTemplates as tpl}
|
||||||
<button class="template-card" class:selected={createWithTemplate?.id === tpl.id}
|
<button class="template-card" class:selected={createWithTemplate?.id === tpl.id}
|
||||||
on:click={() => createWithTemplate = tpl}>
|
on:click={() => createWithTemplate = tpl}>
|
||||||
<TemplateIcon kind={tpl.icon || 'generic'} size={22} />
|
<TemplateIcon kind={tpl.icon || 'generic'} size={24} />
|
||||||
<span>{t(tpl.title)}</span>
|
<div class="template-card-text">
|
||||||
|
<span class="template-card-title">{t(tpl.title)}</span>
|
||||||
|
<span class="template-card-desc">{t(tpl.title + '.desc')}</span>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1519,7 +1528,8 @@
|
||||||
on:keydown={(e) => e.key === 'Enter' && submitCreateNode()} autofocus />
|
on:keydown={(e) => e.key === 'Enter' && submitCreateNode()} autofocus />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-primary" on:click={submitCreateNode}>{t('common.create')}</button>
|
<button class="btn btn-primary" on:click={submitCreateNode}
|
||||||
|
disabled={!newNodeTitle.trim() || createWithTemplate === undefined}>{t('common.create')}</button>
|
||||||
<button class="btn" on:click={cancelCreateNode}>{t('common.cancel')}</button>
|
<button class="btn" on:click={cancelCreateNode}>{t('common.cancel')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1901,10 +1911,13 @@
|
||||||
.rename-error { color: #ff6b6b; font-size: 12px; margin-top: 4px; }
|
.rename-error { color: #ff6b6b; font-size: 12px; margin-top: 4px; }
|
||||||
|
|
||||||
/* Template cards */
|
/* Template cards */
|
||||||
.template-cards { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 4px; }
|
.template-cards { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
|
||||||
.template-card { display: flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #ccc; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; }
|
.template-card { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border: 1px solid #2a2a3c; background: #13131f; color: #ccc; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; width: 100%; text-align: left; }
|
||||||
.template-card:hover { background: #1e1e30; border-color: #3a3a5c; }
|
.template-card:hover { background: #1e1e30; border-color: #3a3a5c; }
|
||||||
.template-card.selected { background: #2a2a50; border-color: #6366f1; color: #e4e4ef; }
|
.template-card.selected { background: #2a2a50; border-color: #6366f1; color: #e4e4ef; }
|
||||||
|
.template-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||||
|
.template-card-title { font-weight: 500; }
|
||||||
|
.template-card-desc { font-size: 11px; color: #888; }
|
||||||
|
|
||||||
/* Today Dashboard */
|
/* Today Dashboard */
|
||||||
.today-dashboard { padding: 24px; overflow-y: auto; flex: 1; }
|
.today-dashboard { padding: 24px; overflow-y: auto; flex: 1; }
|
||||||
|
|
|
||||||
|
|
@ -10,53 +10,108 @@
|
||||||
export let onContextMenu
|
export let onContextMenu
|
||||||
|
|
||||||
function iconKind(node) {
|
function iconKind(node) {
|
||||||
if (node.type === 'folder' && node.template_id) {
|
if (node.type === 'client' || node.template_id === 'client.default') return 'client'
|
||||||
const m = { 'folder.default': 'folder', 'project.default': 'project', 'client.default': 'client', 'document.default': 'document', 'recipe.default': 'recipe' }
|
if (node.type === 'project' || node.template_id === 'project.default') return 'project'
|
||||||
if (m[node.template_id]) return m[node.template_id]
|
if (node.type === 'document' || node.template_id === 'document.default') return 'document'
|
||||||
}
|
if (node.type === 'recipe' || node.template_id === 'recipe.default') return 'recipe'
|
||||||
if (node.type === 'project' || node.type === 'folder') return 'folder'
|
if (node.type === 'folder' || node.template_id === 'folder.default') return 'folder'
|
||||||
if (node.type === 'note') return 'note'
|
if (node.type === 'note') return 'note'
|
||||||
if (node.type === 'file') return 'file'
|
if (node.type === 'file') return 'file'
|
||||||
return 'generic'
|
return 'generic'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasChildren(node) {
|
||||||
|
return node.children && node.children.length > 0
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each nodes as node}
|
{#each nodes as node}
|
||||||
<div class="tree-item"
|
<div class="tree-item"
|
||||||
class:selected={selectedNodeId === node.id}
|
class:selected={selectedNodeId === node.id}
|
||||||
style="padding-left: {level * 16 + 8}px"
|
style="padding-left: {level * 16 + 4}px"
|
||||||
on:contextmenu|preventDefault={(e) => onContextMenu && onContextMenu(e, node)}>
|
on:contextmenu|preventDefault={(e) => onContextMenu && onContextMenu(e, node)}>
|
||||||
|
{#if hasChildren(node)}
|
||||||
<button class="tree-toggle" on:click={() => onToggle && onToggle(node.id)}
|
<button class="tree-toggle" on:click={() => onToggle && onToggle(node.id)}
|
||||||
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
|
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
|
||||||
{#if node.children && node.children.length > 0}
|
<span class="tree-arrow">{expanded[node.id] ? '▾' : '▸'}</span>
|
||||||
<span class="tree-arrow">{expanded[node.id] ? '▼' : '▶'}</span>
|
|
||||||
{:else}
|
|
||||||
<span class="tree-spacer"></span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
|
{:else}
|
||||||
|
<span class="tree-toggle-placeholder"></span>
|
||||||
|
{/if}
|
||||||
<span class="tree-icon"><TemplateIcon kind={iconKind(node)} size={16} /></span>
|
<span class="tree-icon"><TemplateIcon kind={iconKind(node)} size={16} /></span>
|
||||||
<span class="tree-label" on:click={() => onSelect && onSelect(node)}
|
<span class="tree-label" on:click={() => onSelect && onSelect(node)}
|
||||||
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
|
on:contextmenu|preventDefault={(e) => e.stopPropagation()}>
|
||||||
{node.title}
|
{node.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if expanded[node.id] && node.children && node.children.length > 0}
|
{#if expanded[node.id] && hasChildren(node)}
|
||||||
<svelte:self nodes={node.children} {expanded} {selectedNodeId} level={level + 1}
|
<svelte:self nodes={node.children} {expanded} {selectedNodeId} level={level + 1}
|
||||||
{onSelect} {onToggle} {onContextMenu} />
|
{onSelect} {onToggle} {onContextMenu} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.tree-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 32px;
|
||||||
|
padding-right: 8px;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ccc;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.tree-item:hover {
|
||||||
|
background: #222233;
|
||||||
|
}
|
||||||
|
.tree-item.selected {
|
||||||
|
background: #2a2a4a;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.tree-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.tree-toggle:hover {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.tree-toggle-placeholder {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.tree-icon {
|
.tree-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
height: 32px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: #888;
|
color: #888;
|
||||||
margin-right: 2px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
.tree-item.selected .tree-icon {
|
.tree-item.selected .tree-icon {
|
||||||
color: #a5b4fc;
|
color: #a5b4fc;
|
||||||
}
|
}
|
||||||
|
.tree-label {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,16 @@ export default {
|
||||||
'nav.recipes': 'Recipes',
|
'nav.recipes': 'Recipes',
|
||||||
'nav.documents': 'Documents',
|
'nav.documents': 'Documents',
|
||||||
'nav.archive': 'Archive',
|
'nav.archive': 'Archive',
|
||||||
|
'nav.sections': 'Sections',
|
||||||
|
'nav.cases': 'Cases',
|
||||||
|
'nav.noCases': 'No cases',
|
||||||
'nav.system': 'System',
|
'nav.system': 'System',
|
||||||
'nav.workspace': 'Workspace',
|
'nav.workspace': 'Workspace',
|
||||||
'nav.noNodes': 'No nodes',
|
'nav.noNodes': 'No nodes',
|
||||||
'nav.openFolder': 'Open folder',
|
'nav.openFolder': 'Open folder',
|
||||||
'nav.createInside': 'Create inside',
|
'nav.createInside': 'Create inside',
|
||||||
|
'nav.selectPrompt': 'Select a section or case',
|
||||||
|
'nav.brand': 'Verstak',
|
||||||
|
|
||||||
'tab.overview': 'Overview',
|
'tab.overview': 'Overview',
|
||||||
'tab.notes': 'Notes',
|
'tab.notes': 'Notes',
|
||||||
|
|
@ -80,14 +85,35 @@ export default {
|
||||||
'sync.notConnected': 'Not connected',
|
'sync.notConnected': 'Not connected',
|
||||||
'sync.disabled': 'Disabled',
|
'sync.disabled': 'Disabled',
|
||||||
|
|
||||||
'template.optionNone': 'No template',
|
'kind.project': 'Project',
|
||||||
|
'kind.client': 'Client',
|
||||||
|
'kind.document': 'Document',
|
||||||
|
'kind.recipe': 'Recipe',
|
||||||
|
'kind.folder': 'Folder',
|
||||||
|
'kind.note': 'Note',
|
||||||
|
'kind.file': 'File',
|
||||||
|
'kind.archive': 'Archive',
|
||||||
|
'kind.case': 'Case',
|
||||||
|
|
||||||
|
'template.optionNone': 'Empty case',
|
||||||
'template.optional': 'Template (optional)',
|
'template.optional': 'Template (optional)',
|
||||||
|
'template.none.desc': 'No template, simple container node',
|
||||||
'template.folder': 'Folder',
|
'template.folder': 'Folder',
|
||||||
|
'template.folder.desc': 'A folder to group items inside a workspace',
|
||||||
'template.project': 'Project',
|
'template.project': 'Project',
|
||||||
|
'template.project.desc': 'A distinct project or task with files, notes and work log',
|
||||||
'template.client': 'Client',
|
'template.client': 'Client',
|
||||||
|
'template.client.desc': 'An organization or person for whom work is performed',
|
||||||
'template.document': 'Document',
|
'template.document': 'Document',
|
||||||
|
'template.document.desc': 'A document with description, notes and files',
|
||||||
'template.recipe': 'Recipe',
|
'template.recipe': 'Recipe',
|
||||||
'template.select': 'Select template',
|
'template.recipe.desc': 'A repeatable procedure or instruction',
|
||||||
|
'template.note': 'Note',
|
||||||
|
'template.file': 'File',
|
||||||
|
'template.select': 'Select type',
|
||||||
|
|
||||||
|
'case.new': 'New case',
|
||||||
|
'case.namePlaceholder': 'Case name',
|
||||||
|
|
||||||
'error.generic': 'An error occurred',
|
'error.generic': 'An error occurred',
|
||||||
'error.invalidCredentials': 'Invalid username or password',
|
'error.invalidCredentials': 'Invalid username or password',
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ export default {
|
||||||
'kind.client': 'Клиент',
|
'kind.client': 'Клиент',
|
||||||
'kind.document': 'Документ',
|
'kind.document': 'Документ',
|
||||||
'kind.recipe': 'Рецепт',
|
'kind.recipe': 'Рецепт',
|
||||||
|
'kind.folder': 'Папка',
|
||||||
|
'kind.note': 'Заметка',
|
||||||
|
'kind.file': 'Файл',
|
||||||
'kind.archive': 'Архив',
|
'kind.archive': 'Архив',
|
||||||
'kind.case': 'Дело',
|
'kind.case': 'Дело',
|
||||||
|
|
||||||
|
|
@ -214,14 +217,22 @@ export default {
|
||||||
'delete.folder': 'папку',
|
'delete.folder': 'папку',
|
||||||
'delete.file': 'файл',
|
'delete.file': 'файл',
|
||||||
|
|
||||||
'template.optionNone': 'Без шаблона',
|
'template.optionNone': 'Пустое дело',
|
||||||
'template.optional': 'Шаблон (опционально)',
|
'template.optional': 'Шаблон (опционально)',
|
||||||
|
'template.none.desc': 'Без шаблона, простой узел-контейнер',
|
||||||
'template.folder': 'Папка',
|
'template.folder': 'Папка',
|
||||||
|
'template.folder.desc': 'Папка для группировки элементов внутри рабочего пространства',
|
||||||
'template.project': 'Проект',
|
'template.project': 'Проект',
|
||||||
|
'template.project.desc': 'Отдельный проект или задача с файлами, заметками и журналом',
|
||||||
'template.client': 'Клиент',
|
'template.client': 'Клиент',
|
||||||
|
'template.client.desc': 'Организация или человек, для которых ведутся работы',
|
||||||
'template.document': 'Документ',
|
'template.document': 'Документ',
|
||||||
|
'template.document.desc': 'Документ с описанием, заметками и файлами',
|
||||||
'template.recipe': 'Рецепт',
|
'template.recipe': 'Рецепт',
|
||||||
'template.select': 'Выберите шаблон',
|
'template.recipe.desc': 'Повторяемая процедура или инструкция',
|
||||||
|
'template.note': 'Заметка',
|
||||||
|
'template.file': 'Файл',
|
||||||
|
'template.select': 'Выберите тип',
|
||||||
|
|
||||||
'mime.jpeg': 'Изображение JPEG',
|
'mime.jpeg': 'Изображение JPEG',
|
||||||
'mime.png': 'Изображение PNG',
|
'mime.png': 'Изображение PNG',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue