This commit is contained in:
+77
-2
@@ -47,6 +47,8 @@ class GuideEditor {
|
|||||||
this.steps = [];
|
this.steps = [];
|
||||||
this.stepMap = new Map();
|
this.stepMap = new Map();
|
||||||
this.selectedStepId = null;
|
this.selectedStepId = null;
|
||||||
|
this.stepSelectMode = false;
|
||||||
|
this.selectedSteps = new Set();
|
||||||
this.selectedAnnotationId = null;
|
this.selectedAnnotationId = null;
|
||||||
this.currentTool = 'select';
|
this.currentTool = 'select';
|
||||||
this.currentZoom = 'fit';
|
this.currentZoom = 'fit';
|
||||||
@@ -166,10 +168,12 @@ class GuideEditor {
|
|||||||
el('div.row', {},
|
el('div.row', {},
|
||||||
this.dom.addStepBtn = el('button.primary', { type: 'button' }, 'Add'),
|
this.dom.addStepBtn = el('button.primary', { type: 'button' }, 'Add'),
|
||||||
this.dom.importBtn = el('button', { type: 'button' }, 'Import'),
|
this.dom.importBtn = el('button', { type: 'button' }, 'Import'),
|
||||||
|
this.dom.selectStepsBtn = el('button', { type: 'button' }, 'Select'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
this.dom.stepsList = el('div.steps-list'),
|
this.dom.stepsList = el('div.steps-list'),
|
||||||
el('div.pane-foot', {},
|
this.dom.stepBulkBar = el('div'),
|
||||||
|
this.dom.paneFoot = el('div.pane-foot', {},
|
||||||
this.dom.moveUpBtn = el('button.icon', { type: 'button', title: 'Move step up' }, '↑'),
|
this.dom.moveUpBtn = el('button.icon', { type: 'button', title: 'Move step up' }, '↑'),
|
||||||
this.dom.moveDownBtn = el('button.icon', { type: 'button', title: 'Move step down' }, '↓'),
|
this.dom.moveDownBtn = el('button.icon', { type: 'button', title: 'Move step down' }, '↓'),
|
||||||
this.dom.duplicateBtn = el('button', { type: 'button' }, 'Duplicate'),
|
this.dom.duplicateBtn = el('button', { type: 'button' }, 'Duplicate'),
|
||||||
@@ -289,6 +293,7 @@ class GuideEditor {
|
|||||||
bindShellEvents() {
|
bindShellEvents() {
|
||||||
this.dom.addStepBtn.addEventListener('click', () => this.addEmptyStep());
|
this.dom.addStepBtn.addEventListener('click', () => this.addEmptyStep());
|
||||||
this.dom.importBtn.addEventListener('click', () => this.importImageSteps());
|
this.dom.importBtn.addEventListener('click', () => this.importImageSteps());
|
||||||
|
this.dom.selectStepsBtn.addEventListener('click', () => this.toggleStepSelectMode());
|
||||||
this.dom.moveUpBtn.addEventListener('click', () => this.moveSelectedStep(-1));
|
this.dom.moveUpBtn.addEventListener('click', () => this.moveSelectedStep(-1));
|
||||||
this.dom.moveDownBtn.addEventListener('click', () => this.moveSelectedStep(1));
|
this.dom.moveDownBtn.addEventListener('click', () => this.moveSelectedStep(1));
|
||||||
this.dom.duplicateBtn.addEventListener('click', () => this.duplicateSelectedStep());
|
this.dom.duplicateBtn.addEventListener('click', () => this.duplicateSelectedStep());
|
||||||
@@ -528,6 +533,7 @@ class GuideEditor {
|
|||||||
const numbers = stepNumberMap(this.steps);
|
const numbers = stepNumberMap(this.steps);
|
||||||
clearNode(this.dom.stepsList);
|
clearNode(this.dom.stepsList);
|
||||||
this.dom.stepCount.textContent = `${this.steps.length} step${this.steps.length === 1 ? '' : 's'}`;
|
this.dom.stepCount.textContent = `${this.steps.length} step${this.steps.length === 1 ? '' : 's'}`;
|
||||||
|
this.dom.selectStepsBtn.className = this.stepSelectMode ? 'primary' : '';
|
||||||
for (const step of this.steps) {
|
for (const step of this.steps) {
|
||||||
const number = numbers.get(step.stepId) || '';
|
const number = numbers.get(step.stepId) || '';
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
@@ -537,12 +543,17 @@ class GuideEditor {
|
|||||||
parent = this.stepMap.get(parent).parentStepId;
|
parent = this.stepMap.get(parent).parentStepId;
|
||||||
}
|
}
|
||||||
const selected = current && current.stepId === step.stepId;
|
const selected = current && current.stepId === step.stepId;
|
||||||
|
const checked = this.selectedSteps.has(step.stepId);
|
||||||
const item = el('div.step-item', {
|
const item = el('div.step-item', {
|
||||||
className: `step-item${selected ? ' selected' : ''}${depth ? ' sub' : ''}${step.skipped ? ' skipped' : ''}${step.hidden ? ' hiddenstep' : ''}`,
|
className: `step-item${selected ? ' selected' : ''}${depth ? ' sub' : ''}${step.skipped ? ' skipped' : ''}${step.hidden ? ' hiddenstep' : ''}`,
|
||||||
dataset: { stepId: step.stepId },
|
dataset: { stepId: step.stepId },
|
||||||
onClick: () => this.selectStep(step.stepId),
|
onClick: () => {
|
||||||
|
if (this.stepSelectMode) this.toggleStepSelection(step.stepId);
|
||||||
|
else this.selectStep(step.stepId);
|
||||||
|
},
|
||||||
onContextMenu: (e) => {
|
onContextMenu: (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (this.stepSelectMode) return;
|
||||||
this.selectStep(step.stepId);
|
this.selectStep(step.stepId);
|
||||||
contextMenu(e.clientX, e.clientY, [
|
contextMenu(e.clientX, e.clientY, [
|
||||||
{ label: 'Add substep', action: () => this.addSubstep(step.stepId) },
|
{ label: 'Add substep', action: () => this.addSubstep(step.stepId) },
|
||||||
@@ -555,6 +566,14 @@ class GuideEditor {
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
this.stepSelectMode
|
||||||
|
? el('input', {
|
||||||
|
type: 'checkbox',
|
||||||
|
checked,
|
||||||
|
onClick: (e) => e.stopPropagation(),
|
||||||
|
onChange: () => this.toggleStepSelection(step.stepId),
|
||||||
|
})
|
||||||
|
: null,
|
||||||
el('span.status-dot', { className: `status-dot status-${step.status}` }),
|
el('span.status-dot', { className: `status-dot status-${step.status}` }),
|
||||||
el('span.num', {}, number || '•'),
|
el('span.num', {}, number || '•'),
|
||||||
el('span.t', {}, step.title || 'Untitled step'),
|
el('span.t', {}, step.title || 'Untitled step'),
|
||||||
@@ -568,6 +587,62 @@ class GuideEditor {
|
|||||||
if (!this.steps.length) {
|
if (!this.steps.length) {
|
||||||
this.dom.stepsList.append(el('div.empty-state', { style: { marginTop: '40px' } }, 'No steps yet.'));
|
this.dom.stepsList.append(el('div.empty-state', { style: { marginTop: '40px' } }, 'No steps yet.'));
|
||||||
}
|
}
|
||||||
|
this.renderStepBulkBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleStepSelectMode() {
|
||||||
|
this.stepSelectMode = !this.stepSelectMode;
|
||||||
|
this.selectedSteps = new Set();
|
||||||
|
this.renderStepList();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleStepSelection(stepId) {
|
||||||
|
if (this.selectedSteps.has(stepId)) this.selectedSteps.delete(stepId);
|
||||||
|
else this.selectedSteps.add(stepId);
|
||||||
|
this.renderStepList();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAllSteps() {
|
||||||
|
this.selectedSteps = new Set(this.steps.map((s) => s.stepId));
|
||||||
|
this.renderStepList();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearStepSelection() {
|
||||||
|
this.selectedSteps = new Set();
|
||||||
|
this.renderStepList();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderStepBulkBar() {
|
||||||
|
clearNode(this.dom.stepBulkBar);
|
||||||
|
this.dom.paneFoot.classList.toggle('hidden', this.stepSelectMode);
|
||||||
|
if (!this.stepSelectMode) return;
|
||||||
|
const n = this.selectedSteps.size;
|
||||||
|
const allSelected = this.steps.length > 0 && n === this.steps.length;
|
||||||
|
this.dom.stepBulkBar.append(
|
||||||
|
el('div.bulk-bar', {},
|
||||||
|
el('span', {}, n ? `${n} selected` : 'Select steps to delete'),
|
||||||
|
el('span.spacer', {}),
|
||||||
|
el('button', {
|
||||||
|
type: 'button',
|
||||||
|
onClick: () => (allSelected ? this.clearStepSelection() : this.selectAllSteps()),
|
||||||
|
}, allSelected ? 'Clear' : 'Select all'),
|
||||||
|
el('button.danger', { type: 'button', disabled: !n, onClick: () => this.deleteSelectedSteps() }, 'Delete'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSelectedSteps() {
|
||||||
|
const ids = [...this.selectedSteps];
|
||||||
|
if (!ids.length) return;
|
||||||
|
const ok = await confirmDialog(`Delete ${ids.length} step${ids.length === 1 ? '' : 's'}?`, { danger: true, okLabel: 'Delete' });
|
||||||
|
if (!ok) return;
|
||||||
|
for (const stepId of ids) {
|
||||||
|
await api.step.delete({ guideId: this.guideId, stepId });
|
||||||
|
}
|
||||||
|
this.stepSelectMode = false;
|
||||||
|
this.selectedSteps = new Set();
|
||||||
|
await this.reload(null);
|
||||||
|
this.onToast(`${ids.length} step${ids.length === 1 ? '' : 's'} deleted.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
syncStepFields() {
|
syncStepFields() {
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ Initial release.
|
|||||||
"Open guide" from the right-click menu) now arms a paused capture
|
"Open guide" from the right-click menu) now arms a paused capture
|
||||||
session for it, so the red REC bar appears immediately with a "Start
|
session for it, so the red REC bar appears immediately with a "Start
|
||||||
recording" option to resume capturing more steps.
|
recording" option to resume capturing more steps.
|
||||||
|
- Editor step list: a "Select" toggle enables multi-select (checkboxes)
|
||||||
|
with a "Select all" / "Delete" bar for removing several steps at once.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user