feat: simplify inbox actions and group task tabs
This commit is contained in:
parent
6d15639b41
commit
23f517dee3
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
|
|
@ -19,8 +19,8 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-D6zAtuqe.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CtRnbH6M.css">
|
||||
<script type="module" crossorigin src="/assets/main-BQHjHDrT.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-DfazBFdN.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -177,11 +177,11 @@
|
|||
{ id: 'overview', label: t('tab.overview') },
|
||||
{ id: 'notes', label: t('tab.notes') },
|
||||
{ id: 'files', label: t('tab.files') },
|
||||
{ id: 'inbox', label: t('tab.inbox') },
|
||||
{ id: 'links', label: t('tab.links') },
|
||||
{ id: 'actions', label: t('tab.actions') },
|
||||
{ id: 'worklog', label: t('tab.worklog') },
|
||||
{ id: 'activity', label: t('tab.activity') },
|
||||
{ id: 'activity', label: t('tab.activity'), group: 'service' },
|
||||
{ id: 'worklog', label: t('tab.worklog'), group: 'service' },
|
||||
{ id: 'inbox', label: t('tab.inbox'), group: 'service' },
|
||||
]
|
||||
|
||||
let unlistenDrop = null
|
||||
|
|
@ -2197,6 +2197,9 @@
|
|||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
{#each tabs as tab}
|
||||
{#if tab.id === 'activity'}
|
||||
<span class="tab-separator" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<button class="tab" class:active={activeTab === tab.id} on:click={() => { activeTab = tab.id; if (tab.id === 'files' && selectedNode && fileItems.length === 0 && !currentFolderId) loadFolder(selectedNode.id) }}>{tab.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -2372,9 +2375,15 @@
|
|||
<span class="inbox-item-meta">{inboxMetaText(item)}</span>
|
||||
</div>
|
||||
<div class="inbox-item-actions">
|
||||
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => resolveInboxHere(item)}>{t('inbox.keepHere')}</button>
|
||||
<button class="btn btn-sm" on:click|stopPropagation={() => openAssignInbox(item)}>{t('inbox.assign')}</button>
|
||||
<button class="btn btn-sm btn-danger" on:click|stopPropagation={() => confirmDeleteInbox(item)}>{t('common.delete')}</button>
|
||||
<button class="inbox-icon-btn inbox-icon-btn-primary" title={t('inbox.keepHere')} aria-label={t('inbox.keepHere')} on:click|stopPropagation={() => resolveInboxHere(item)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
</button>
|
||||
<button class="inbox-icon-btn" title={t('inbox.assign')} aria-label={t('inbox.assign')} on:click|stopPropagation={() => openAssignInbox(item)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3h7v7"/><path d="M10 14 21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>
|
||||
</button>
|
||||
<button class="inbox-icon-btn inbox-icon-btn-danger" title={t('common.delete')} aria-label={t('common.delete')} on:click|stopPropagation={() => confirmDeleteInbox(item)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -2600,14 +2609,22 @@
|
|||
</div>
|
||||
<div class="inbox-item-actions">
|
||||
{#if item.suggestedTargetNodeId}
|
||||
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => resolveInboxHere(item)}>{t('inbox.keepHere')}</button>
|
||||
<button class="inbox-icon-btn inbox-icon-btn-primary" title={t('inbox.keepHere')} aria-label={t('inbox.keepHere')} on:click|stopPropagation={() => resolveInboxHere(item)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn btn-sm btn-primary" on:click|stopPropagation={() => openAssignInbox(item)}>{t('inbox.assign')}</button>
|
||||
<button class="btn btn-sm" on:click|stopPropagation={() => openInboxArtifact(item)}>{t('common.open')}</button>
|
||||
<button class="inbox-icon-btn" title={t('inbox.assign')} aria-label={t('inbox.assign')} on:click|stopPropagation={() => openAssignInbox(item)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3h7v7"/><path d="M10 14 21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>
|
||||
</button>
|
||||
<button class="inbox-icon-btn" title={t('common.open')} aria-label={t('common.open')} on:click|stopPropagation={() => openInboxArtifact(item)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="M10 14 21 3"/></svg>
|
||||
</button>
|
||||
{#if canShowInboxArtifactInFolder(item)}
|
||||
<button class="btn btn-sm" on:click|stopPropagation={() => openNodeFolder(item)}>{t('file.showInExplorer')}</button>
|
||||
{/if}
|
||||
<button class="btn btn-sm btn-danger" on:click|stopPropagation={() => confirmDeleteInbox(item)}>{t('common.delete')}</button>
|
||||
<button class="inbox-icon-btn inbox-icon-btn-danger" title={t('common.delete')} aria-label={t('common.delete')} on:click|stopPropagation={() => confirmDeleteInbox(item)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -3372,10 +3389,11 @@
|
|||
.dismiss-btn:hover { color: #ff4444; }
|
||||
|
||||
/* Tabs */
|
||||
.tabs { display: flex; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; padding: 0 24px; }
|
||||
.tabs { display: flex; align-items: stretch; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; padding: 0 24px; }
|
||||
.tab { padding: 10px 16px; border: none; background: none; color: #888; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; font-family: inherit; }
|
||||
.tab:hover { color: #a5b4fc; }
|
||||
.tab.active { color: #e4e4ef; border-bottom-color: #818cf8; background: rgba(99,102,241,0.12); font-weight: 600; }
|
||||
.tab-separator { width: 1px; height: 22px; margin: 8px 10px 0; background: #2a2a3c; flex-shrink: 0; }
|
||||
|
||||
/* Tab content */
|
||||
.tab-content { flex: 1; overflow-y: auto; }
|
||||
|
|
@ -3462,7 +3480,13 @@
|
|||
.inbox-item-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
||||
.inbox-item-title { color: #e4e4ef; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.inbox-item-meta { color: #8888a0; font-size: 12px; }
|
||||
.inbox-item-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
||||
.inbox-item-actions { display: flex; gap: 6px; flex-shrink: 0; align-items: center; }
|
||||
.inbox-icon-btn { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid #2a2a3c; border-radius: 6px; background: #13131f; color: #a0a0b8; cursor: pointer; transition: color 0.12s, border-color 0.12s, background 0.12s; }
|
||||
.inbox-icon-btn:hover { color: #e4e4ef; border-color: #3a3a5c; background: #222238; }
|
||||
.inbox-icon-btn-primary { color: #a5b4fc; border-color: #34346a; }
|
||||
.inbox-icon-btn-primary:hover { color: #fff; border-color: #6366f1; background: #272750; }
|
||||
.inbox-icon-btn-danger { color: #f87171; border-color: #4a252c; }
|
||||
.inbox-icon-btn-danger:hover { color: #fff; border-color: #dc2626; background: #3a1f24; }
|
||||
.inbox-tab { padding: 24px; }
|
||||
|
||||
/* Links tab */
|
||||
|
|
|
|||
|
|
@ -145,6 +145,14 @@ async function runReadyScenario(cdp, url) {
|
|||
await assertText(cdp, 'example.test', 'inbox: clipboard URL captured')
|
||||
await assertText(cdp, 'Ссылка', 'inbox: clipboard URL kind visible')
|
||||
await assertInboxItemButtonAbsent(cdp, 'example.test', 'Показать в проводнике', 'inbox: link has no explorer action')
|
||||
await assertEval(cdp, `
|
||||
(() => {
|
||||
const row = [...document.querySelectorAll('.inbox-item')].find((el) => el.innerText.includes('example.test'));
|
||||
if (!row) return false;
|
||||
const iconButtons = [...row.querySelectorAll('.inbox-icon-btn')];
|
||||
return iconButtons.length >= 3 && iconButtons.every((btn) => btn.title && btn.innerText.trim() === '');
|
||||
})()
|
||||
`, 'inbox: frequent actions are icon buttons with titles')
|
||||
await clickInboxItemButton(cdp, 'example.test', 'Открыть')
|
||||
await assertEval(cdp, `window.__VERSTAK_GUI_SMOKE__.state.openedUrls.includes('https://example.test/from-clipboard')`, 'inbox: open URL launches external URL')
|
||||
await clickInboxItemButton(cdp, 'example.test', 'Разложить')
|
||||
|
|
@ -202,6 +210,8 @@ async function runReadyScenario(cdp, url) {
|
|||
await waitForSelector(cdp, '.tabs')
|
||||
await assertText(cdp, 'Smoke Project', 'node: selected project visible')
|
||||
await assertEval(cdp, `document.querySelectorAll('.tab').length === 8`, 'node: all primary tabs rendered')
|
||||
await assertEval(cdp, `[...document.querySelectorAll('.tab')].map((el) => el.innerText.trim()).join('|') === 'Обзор|Заметки|Файлы|Ссылки|Действия|Активность|Журнал|Неразобранное'`, 'node: tabs use content-first order')
|
||||
await assertEval(cdp, `document.querySelector('.tab-separator') !== null`, 'node: service tabs are visually separated')
|
||||
await screenshot(cdp, 'node-overview.png')
|
||||
|
||||
await clickText(cdp, '.tab', 'Заметки')
|
||||
|
|
@ -492,7 +502,9 @@ async function clickInboxItemButton(cdp, title, buttonText) {
|
|||
.find((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(title)}));
|
||||
if (!row) return false;
|
||||
const btn = [...row.querySelectorAll('button')]
|
||||
.find((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(buttonText)}));
|
||||
.find((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(buttonText)}) ||
|
||||
norm(node.getAttribute('title')).includes(${JSON.stringify(buttonText)}) ||
|
||||
norm(node.getAttribute('aria-label')).includes(${JSON.stringify(buttonText)}));
|
||||
if (!btn) return false;
|
||||
btn.click();
|
||||
return true;
|
||||
|
|
@ -510,7 +522,9 @@ async function assertInboxItemButtonAbsent(cdp, title, buttonText, label) {
|
|||
.find((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(title)}));
|
||||
if (!row) return false;
|
||||
return ![...row.querySelectorAll('button')]
|
||||
.some((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(buttonText)}));
|
||||
.some((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(buttonText)}) ||
|
||||
norm(node.getAttribute('title')).includes(${JSON.stringify(buttonText)}) ||
|
||||
norm(node.getAttribute('aria-label')).includes(${JSON.stringify(buttonText)}));
|
||||
})()
|
||||
`, label)
|
||||
}
|
||||
|
|
@ -848,7 +862,7 @@ function wailsMockSource() {
|
|||
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.hostname ? raw : '';
|
||||
} catch {
|
||||
try {
|
||||
const withScheme = `https://${raw}`;
|
||||
const withScheme = 'https://' + raw;
|
||||
const parsed = new URL(withScheme);
|
||||
return parsed.hostname && parsed.hostname.includes('.') ? withScheme : '';
|
||||
} catch {
|
||||
|
|
|
|||
Loading…
Reference in New Issue