feat(ux): simplify publish center workflow
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const STATUS_DRAWER_KEY = 'vendoo_publish_status_open_v1';
|
||||
const OPERATIONAL_VIEWS = new Set(['queue', 'errors', 'scheduled']);
|
||||
const VIEW_META = {
|
||||
smart: {
|
||||
title: 'Manuell veröffentlichen',
|
||||
help: 'Daten und Fotos kontrolliert kopieren',
|
||||
stage: 2,
|
||||
context: 'Artikel auswählen und Smart Copy öffnen.',
|
||||
},
|
||||
direct: {
|
||||
title: 'Direkt veröffentlichen',
|
||||
help: 'Über verbundene Marktplatzkonten',
|
||||
stage: 2,
|
||||
context: 'Artikel prüfen, speichern und sicher einreihen.',
|
||||
},
|
||||
queue: {
|
||||
title: 'Aufträge',
|
||||
help: 'Publishing-Queue überwachen',
|
||||
stage: 3,
|
||||
context: 'Laufende und abgeschlossene Publishing-Aufträge verwalten.',
|
||||
},
|
||||
scheduled: {
|
||||
title: 'Geplant',
|
||||
help: 'Zeitgesteuerte Veröffentlichungen',
|
||||
stage: 3,
|
||||
context: 'Geplante Veröffentlichungen kontrollieren oder entfernen.',
|
||||
},
|
||||
errors: {
|
||||
title: 'Fehler & Verlauf',
|
||||
help: 'Probleme und Ereignisse prüfen',
|
||||
stage: 3,
|
||||
context: 'Fehler nachvollziehen und betroffene Aufträge öffnen.',
|
||||
},
|
||||
};
|
||||
|
||||
let refreshQueued = false;
|
||||
let publishObserver = null;
|
||||
|
||||
function publishSection() {
|
||||
return document.getElementById('publish');
|
||||
}
|
||||
|
||||
function createElement(tag, className, text) {
|
||||
const element = document.createElement(tag);
|
||||
if (className) element.className = className;
|
||||
if (text !== undefined) element.textContent = text;
|
||||
return element;
|
||||
}
|
||||
|
||||
function setText(node, value) {
|
||||
if (node && node.textContent !== String(value)) node.textContent = String(value);
|
||||
}
|
||||
|
||||
function activeView() {
|
||||
return publishSection()?.querySelector('[data-publish-view].active')?.dataset.publishView || 'smart';
|
||||
}
|
||||
|
||||
function decorateViewButton(button) {
|
||||
if (!button || button.dataset.uxPublishDecorated === 'true') return;
|
||||
const view = button.dataset.publishView;
|
||||
const meta = VIEW_META[view];
|
||||
if (!meta) return;
|
||||
const badge = button.querySelector('.publish-tab-count, .publish-tab-badge');
|
||||
const title = createElement('strong', 'ux-publish-tab-title', meta.title);
|
||||
const help = createElement('small', 'ux-publish-tab-help', meta.help);
|
||||
const copy = createElement('span', 'ux-publish-tab-copy');
|
||||
copy.append(title, help);
|
||||
button.replaceChildren(copy);
|
||||
if (badge) button.append(badge);
|
||||
button.setAttribute('role', 'tab');
|
||||
button.setAttribute('aria-label', `${meta.title}: ${meta.help}`);
|
||||
button.dataset.uxPublishDecorated = 'true';
|
||||
}
|
||||
|
||||
function createViewGroup(label, views) {
|
||||
const group = createElement('section', 'ux-publish-view-group');
|
||||
group.dataset.publishViewGroup = label.toLowerCase();
|
||||
const heading = createElement('span', 'ux-publish-view-group-label', label);
|
||||
const items = createElement('div', 'ux-publish-view-group-items');
|
||||
for (const view of views) {
|
||||
const button = publishSection()?.querySelector(`[data-publish-view="${view}"]`);
|
||||
if (!button) continue;
|
||||
decorateViewButton(button);
|
||||
items.append(button);
|
||||
}
|
||||
group.append(heading, items);
|
||||
return group;
|
||||
}
|
||||
|
||||
function reorganizeViewNavigation() {
|
||||
const nav = publishSection()?.querySelector('.publish-view-tabs');
|
||||
if (!nav || nav.querySelector('.ux-publish-view-groups')) return;
|
||||
nav.setAttribute('role', 'tablist');
|
||||
const shell = createElement('div', 'ux-publish-view-groups');
|
||||
shell.append(
|
||||
createViewGroup('Vorbereiten', ['smart', 'direct']),
|
||||
createViewGroup('Betrieb', ['queue', 'scheduled', 'errors']),
|
||||
);
|
||||
nav.replaceChildren(shell);
|
||||
nav.addEventListener('keydown', event => {
|
||||
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
||||
const buttons = [...nav.querySelectorAll('[data-publish-view]')];
|
||||
if (!buttons.length) return;
|
||||
const current = Math.max(0, buttons.indexOf(document.activeElement));
|
||||
let target = current;
|
||||
if (event.key === 'ArrowRight') target = (current + 1) % buttons.length;
|
||||
if (event.key === 'ArrowLeft') target = (current - 1 + buttons.length) % buttons.length;
|
||||
if (event.key === 'Home') target = 0;
|
||||
if (event.key === 'End') target = buttons.length - 1;
|
||||
event.preventDefault();
|
||||
buttons[target].focus();
|
||||
buttons[target].click();
|
||||
});
|
||||
}
|
||||
|
||||
function createGuidance() {
|
||||
const section = publishSection();
|
||||
const nav = section?.querySelector('.publish-view-tabs');
|
||||
if (!section || !nav || section.querySelector('.ux-publish-guide')) return;
|
||||
|
||||
const guide = createElement('section', 'ux-publish-guide');
|
||||
guide.setAttribute('aria-label', 'Publishing-Ablauf');
|
||||
const copy = createElement('div', 'ux-publish-guide-copy');
|
||||
const eyebrow = createElement('span', 'ux-publish-guide-eyebrow', 'Sicher veröffentlichen');
|
||||
const title = createElement('h3', '', 'Vom fertigen Artikel zum kontrollierten Auftrag');
|
||||
const context = createElement('p', '', VIEW_META.smart.context);
|
||||
context.id = 'ux-publish-context';
|
||||
copy.append(eyebrow, title, context);
|
||||
|
||||
const stages = createElement('div', 'ux-publish-stages');
|
||||
const stageData = [
|
||||
['1', 'Artikel wählen', 'Suche und Filter nutzen'],
|
||||
['2', 'Methode wählen', 'Smart Copy oder Direkt'],
|
||||
['3', 'Auftrag überwachen', 'Queue, Planung und Fehler'],
|
||||
];
|
||||
for (const [number, label, help] of stageData) {
|
||||
const stage = createElement('div', 'ux-publish-stage');
|
||||
stage.dataset.publishStage = number;
|
||||
const marker = createElement('span', 'ux-publish-stage-number', number);
|
||||
const stageCopy = createElement('span', 'ux-publish-stage-copy');
|
||||
stageCopy.append(createElement('strong', '', label), createElement('small', '', help));
|
||||
stage.append(marker, stageCopy);
|
||||
stages.append(stage);
|
||||
}
|
||||
guide.append(copy, stages);
|
||||
nav.before(guide);
|
||||
}
|
||||
|
||||
function moveStatusRail() {
|
||||
const section = publishSection();
|
||||
const grid = section?.querySelector('.publish-center-grid');
|
||||
const rail = grid?.querySelector('.publish-status-rail');
|
||||
if (!section || !grid || !rail || section.querySelector('.ux-publish-status-drawer')) return;
|
||||
|
||||
const details = createElement('details', 'ux-publish-status-drawer');
|
||||
details.open = localStorage.getItem(STATUS_DRAWER_KEY) === 'true';
|
||||
const summary = createElement('summary', 'ux-publish-status-summary');
|
||||
const summaryCopy = createElement('span', 'ux-publish-status-summary-copy');
|
||||
summaryCopy.append(
|
||||
createElement('strong', '', 'Status, Schnellaktionen und Aktivität'),
|
||||
createElement('small', '', 'Nur öffnen, wenn du den Publishing-Betrieb prüfen möchtest.'),
|
||||
);
|
||||
const health = createElement('span', 'ux-publish-health', 'Keine offenen Probleme');
|
||||
health.id = 'ux-publish-health';
|
||||
summary.append(summaryCopy, health);
|
||||
const content = createElement('div', 'ux-publish-status-content');
|
||||
content.append(rail);
|
||||
details.append(summary, content);
|
||||
details.addEventListener('toggle', () => localStorage.setItem(STATUS_DRAWER_KEY, String(details.open)));
|
||||
grid.before(details);
|
||||
}
|
||||
|
||||
function enhanceBatchActions() {
|
||||
const bar = publishSection()?.querySelector('.publish-batch-bar');
|
||||
if (!bar || bar.querySelector('.ux-publish-batch-primary')) return;
|
||||
const primary = createElement('div', 'ux-publish-batch-primary');
|
||||
const more = createElement('details', 'ux-publish-batch-more');
|
||||
const summary = createElement('summary', '', 'Weitere Aktionen');
|
||||
const secondary = createElement('div', 'ux-publish-batch-secondary');
|
||||
for (const id of ['publish-batch-copy', 'publish-batch-ebay', 'publish-batch-schedule']) {
|
||||
const button = document.getElementById(id);
|
||||
if (button) primary.append(button);
|
||||
}
|
||||
for (const id of ['publish-download-photos', 'publish-batch-etsy']) {
|
||||
const button = document.getElementById(id);
|
||||
if (button) secondary.append(button);
|
||||
}
|
||||
more.append(summary, secondary);
|
||||
bar.append(primary, more);
|
||||
}
|
||||
|
||||
function createActiveFilters() {
|
||||
const filters = publishSection()?.querySelector('.publish-list-filters');
|
||||
if (!filters || document.getElementById('ux-publish-active-filters')) return;
|
||||
const active = createElement('div', 'ux-publish-active-filters');
|
||||
active.id = 'ux-publish-active-filters';
|
||||
active.setAttribute('aria-live', 'polite');
|
||||
filters.after(active);
|
||||
}
|
||||
|
||||
function dispatchControl(control, value) {
|
||||
if (!control) return;
|
||||
control.value = value;
|
||||
control.dispatchEvent(new Event(control.tagName === 'INPUT' ? 'input' : 'change', { bubbles: true }));
|
||||
queueRefresh();
|
||||
}
|
||||
|
||||
function createFilterChip(label, control, value = '') {
|
||||
const chip = createElement('span', 'ux-publish-filter-chip');
|
||||
chip.append(createElement('span', '', label));
|
||||
const clear = createElement('button', 'ux-publish-filter-clear', '×');
|
||||
clear.type = 'button';
|
||||
clear.setAttribute('aria-label', `${label} entfernen`);
|
||||
clear.addEventListener('click', () => dispatchControl(control, value));
|
||||
chip.append(clear);
|
||||
return chip;
|
||||
}
|
||||
|
||||
function selectedOptionText(select) {
|
||||
if (!select?.value) return '';
|
||||
return select.options[select.selectedIndex]?.textContent?.trim() || select.value;
|
||||
}
|
||||
|
||||
function updateActiveFilters() {
|
||||
const container = document.getElementById('ux-publish-active-filters');
|
||||
if (!container) return;
|
||||
const search = document.getElementById('publish-search');
|
||||
const platform = document.getElementById('publish-filter-platform');
|
||||
const status = document.getElementById('publish-filter-status');
|
||||
const chips = [];
|
||||
if (search?.value.trim()) chips.push(createFilterChip(`Suche: ${search.value.trim()}`, search));
|
||||
if (platform?.value) chips.push(createFilterChip(`Plattform: ${selectedOptionText(platform)}`, platform));
|
||||
if (status?.value) chips.push(createFilterChip(`Status: ${selectedOptionText(status)}`, status));
|
||||
container.replaceChildren();
|
||||
container.classList.toggle('is-empty', chips.length === 0);
|
||||
if (!chips.length) return;
|
||||
container.append(createElement('strong', '', 'Aktive Filter'), ...chips);
|
||||
}
|
||||
|
||||
function annotateListingCards() {
|
||||
document.querySelectorAll('#publish-list .publish-list-card').forEach(card => {
|
||||
card.tabIndex = 0;
|
||||
const title = card.querySelector('.publish-card-copy > strong')?.textContent?.trim() || 'Artikel';
|
||||
card.setAttribute('aria-label', `${title} für die Veröffentlichung auswählen`);
|
||||
const status = card.querySelector('.publish-card-status');
|
||||
if (status) card.dataset.publishStatus = [...status.classList].find(name => name.startsWith('status-'))?.slice(7) || 'prepared';
|
||||
if (card.dataset.uxPublishKeyboard === 'true') return;
|
||||
card.dataset.uxPublishKeyboard = 'true';
|
||||
card.addEventListener('keydown', event => {
|
||||
if ((event.key === 'Enter' || event.key === ' ') && event.target === card) {
|
||||
event.preventDefault();
|
||||
card.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function selectionCount() {
|
||||
const text = document.getElementById('publish-selected-count')?.textContent || '0';
|
||||
return Number.parseInt(text, 10) || 0;
|
||||
}
|
||||
|
||||
function updateSelectionState() {
|
||||
const section = publishSection();
|
||||
const count = selectionCount();
|
||||
if (!section) return;
|
||||
section.toggleAttribute('data-has-publish-selection', count > 0);
|
||||
const more = section.querySelector('.ux-publish-batch-more');
|
||||
if (!count && more) more.open = false;
|
||||
const headingAction = document.getElementById('publish-add-selected-ebay');
|
||||
if (headingAction) headingAction.hidden = count === 0;
|
||||
}
|
||||
|
||||
function updateHealth() {
|
||||
const failed = Number(document.getElementById('publish-status-failed')?.textContent || 0);
|
||||
const pending = Number(document.getElementById('publish-status-pending')?.textContent || 0);
|
||||
const processing = Number(document.getElementById('publish-status-processing')?.textContent || 0);
|
||||
const health = document.getElementById('ux-publish-health');
|
||||
if (!health) return;
|
||||
health.className = `ux-publish-health${failed ? ' is-danger' : pending || processing ? ' is-active' : ''}`;
|
||||
setText(health, failed ? `${failed} Fehler prüfen` : processing ? `${processing} in Bearbeitung` : pending ? `${pending} wartend` : 'Keine offenen Probleme');
|
||||
}
|
||||
|
||||
function updateViewState() {
|
||||
const section = publishSection();
|
||||
if (!section) return;
|
||||
const view = activeView();
|
||||
const meta = VIEW_META[view] || VIEW_META.smart;
|
||||
section.dataset.publishMode = view;
|
||||
section.toggleAttribute('data-publish-operational', OPERATIONAL_VIEWS.has(view));
|
||||
section.querySelectorAll('[data-publish-view]').forEach(button => {
|
||||
const active = button.dataset.publishView === view;
|
||||
button.setAttribute('aria-selected', String(active));
|
||||
button.tabIndex = active ? 0 : -1;
|
||||
});
|
||||
section.querySelectorAll('[data-publish-stage]').forEach(stage => {
|
||||
const number = Number(stage.dataset.publishStage);
|
||||
stage.classList.toggle('is-active', number === meta.stage);
|
||||
stage.classList.toggle('is-complete', number < meta.stage);
|
||||
});
|
||||
setText(document.getElementById('ux-publish-context'), meta.context);
|
||||
}
|
||||
|
||||
function refreshPresentation() {
|
||||
refreshQueued = false;
|
||||
updateViewState();
|
||||
updateSelectionState();
|
||||
updateActiveFilters();
|
||||
updateHealth();
|
||||
annotateListingCards();
|
||||
}
|
||||
|
||||
function queueRefresh() {
|
||||
if (refreshQueued) return;
|
||||
refreshQueued = true;
|
||||
window.requestAnimationFrame(refreshPresentation);
|
||||
}
|
||||
|
||||
function bindControls() {
|
||||
for (const id of ['publish-search', 'publish-filter-platform', 'publish-filter-status']) {
|
||||
const control = document.getElementById(id);
|
||||
if (!control || control.dataset.uxPublishBound === 'true') continue;
|
||||
control.dataset.uxPublishBound = 'true';
|
||||
control.addEventListener('input', queueRefresh);
|
||||
control.addEventListener('change', queueRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
function observePublishCenter() {
|
||||
const section = publishSection();
|
||||
if (!section) return;
|
||||
publishObserver = new MutationObserver(queueRefresh);
|
||||
publishObserver.observe(section, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'disabled', 'hidden'],
|
||||
});
|
||||
window.addEventListener('beforeunload', () => publishObserver?.disconnect(), { once: true });
|
||||
}
|
||||
|
||||
function initializePublishCenterUx() {
|
||||
const section = publishSection();
|
||||
if (!section || section.classList.contains('ux-publish-ready')) return;
|
||||
reorganizeViewNavigation();
|
||||
createGuidance();
|
||||
moveStatusRail();
|
||||
enhanceBatchActions();
|
||||
createActiveFilters();
|
||||
bindControls();
|
||||
observePublishCenter();
|
||||
refreshPresentation();
|
||||
section.classList.add('ux-publish-ready');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePublishCenterUx, { once: true });
|
||||
} else {
|
||||
initializePublishCenterUx();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user