Skip to content

Commit 887e36c

Browse files
CopilotFloEdelmann
andauthored
Improve existing channel dialog UX in fixture editor (#5412)
Co-authored-by: Flo Edelmann <git@flo-edelmann.de>
1 parent f6d5da2 commit 887e36c

File tree

1 file changed

+211
-50
lines changed

1 file changed

+211
-50
lines changed

ui/components/editor/EditorChannelDialog.vue

Lines changed: 211 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,34 @@
1414
@submit.prevent="onSubmit()">
1515

1616
<div v-if="channel.editMode === `add-existing`" class="existing-channel-input-container">
17-
<LabeledInput :formstate="formstate" name="existingChannelUuid" label="Select an existing channel">
18-
<select
19-
v-model="channel.uuid"
17+
<LabeledInput
18+
:formstate="formstate"
19+
name="existingChannelUuid"
20+
multiple-inputs>
21+
<input
22+
v-model="selectedChannelUuidsString"
2023
name="existingChannelUuid"
21-
size="10"
22-
style="width: 100%;"
24+
type="hidden"
2325
required>
24-
<option
25-
v-for="channelUuid of currentModeUnchosenChannels"
26-
:key="channelUuid"
27-
:value="channelUuid">
28-
{{ getChannelName(channelUuid) }}
29-
</option>
30-
</select>
26+
<fieldset class="channel-list">
27+
<legend>Select existing channel(s)</legend>
28+
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -- double click is just a shortcut, all functionality is still accessible via keyboard -->
29+
<label
30+
v-for="item of currentModeUnchosenChannels"
31+
:key="item.uuid"
32+
:for="item.inputId"
33+
class="channel-list-item"
34+
@dblclick="onChannelDoubleClick(item.uuid)">
35+
<input
36+
:id="item.inputId"
37+
:checked="item.isSelected"
38+
type="checkbox"
39+
class="channel-checkbox"
40+
@change="toggleChannelSelection(item.uuid)">
41+
<span class="channel-name">{{ item.name }}</span>
42+
<code v-if="item.showUuid" class="channel-uuid">{{ item.uuid }}</code>
43+
</label>
44+
</fieldset>
3145
</LabeledInput>
3246

3347
<p>or <a href="#create-channel" @click.prevent="setEditModeCreate()">create a new channel</a></p>
@@ -230,6 +244,60 @@
230244
display: block;
231245
}
232246
247+
.channel-list {
248+
max-height: 400px;
249+
padding: 0;
250+
margin: 0;
251+
overflow-y: auto;
252+
list-style: none;
253+
background-color: theme-color(card-background);
254+
border: 1px solid theme-color(text-secondary);
255+
256+
legend {
257+
display: block;
258+
width: calc(100% + 2px);
259+
padding: 0;
260+
margin: 0 -1px;
261+
color: theme-color(text-secondary);
262+
border-bottom: 1px solid theme-color(text-secondary);
263+
}
264+
}
265+
266+
.channel-list-item {
267+
display: flex;
268+
gap: 1ex;
269+
align-items: center;
270+
padding: 0.5ex 2ex;
271+
cursor: pointer;
272+
user-select: none;
273+
border-bottom: 1px solid theme-color(divider);
274+
transition: background-color 0.15s;
275+
276+
&:where(:has(.channel-checkbox:checked)) {
277+
background-color: theme-color(active-background);
278+
}
279+
280+
&:last-child {
281+
border-bottom: none;
282+
}
283+
284+
&:hover,
285+
&:has(.channel-checkbox:focus-visible) {
286+
background-color: theme-color(hover-background);
287+
}
288+
}
289+
290+
.channel-checkbox {
291+
flex-shrink: 0;
292+
margin: 0;
293+
cursor: pointer;
294+
}
295+
296+
.channel-uuid {
297+
font-size: 0.85em;
298+
color: theme-color(text-secondary);
299+
}
300+
233301
@media (min-width: $phone) {
234302
#channel-dialog ::v-deep .dialog {
235303
width: 80%;
@@ -291,6 +359,7 @@ export default {
291359
channelProperties,
292360
singleColors: capabilityTypes.ColorIntensity.properties.color.enum,
293361
constants,
362+
selectedChannelUuids: [],
294363
};
295364
},
296365
computed: {
@@ -302,43 +371,22 @@ export default {
302371
const modeIndex = this.fixture.modes.findIndex(mode => mode.uuid === uuid);
303372
return this.fixture.modes[modeIndex];
304373
},
374+
currentModeUnchosenChannelUuids() {
375+
return Object.keys(this.fixture.availableChannels).filter(
376+
channelUuid => !this.currentMode.channels.includes(channelUuid),
377+
);
378+
},
305379
currentModeUnchosenChannels() {
306-
return Object.keys(this.fixture.availableChannels).filter(channelUuid => {
307-
if (this.currentMode.channels.includes(channelUuid)) {
308-
// already used
309-
return false;
310-
}
311-
312-
const channel = this.fixture.availableChannels[channelUuid];
313-
if (`coarseChannelId` in channel) {
314-
// should we include this fine channel?
315-
316-
if (!this.currentMode.channels.includes(channel.coarseChannelId)) {
317-
// its coarse channel is not yet in the mode
318-
return false;
319-
}
320-
321-
const modeChannels = this.currentMode.channels.map(
322-
uuid => this.fixture.availableChannels[uuid],
323-
);
324-
325-
const otherFineChannels = modeChannels.filter(
326-
otherChannel => `coarseChannelId` in otherChannel && otherChannel.coarseChannelId === channel.coarseChannelId,
327-
);
328-
329-
const maxFoundResolution = Math.max(
330-
constants.RESOLUTION_8BIT,
331-
...otherFineChannels.map(otherFineChannel => otherFineChannel.resolution),
332-
);
333-
334-
if (maxFoundResolution !== channel.resolution - 1) {
335-
// the finest channel currently used is not its next coarser channel
336-
return false;
337-
}
338-
}
339-
340-
return true;
341-
});
380+
return this.currentModeUnchosenChannelUuids.map(channelUuid => ({
381+
inputId: `unchosen-channel-${channelUuid}`,
382+
uuid: channelUuid,
383+
name: this.getChannelName(channelUuid),
384+
showUuid: !this.isChannelNameUnique(channelUuid),
385+
isSelected: this.isChannelSelected(channelUuid),
386+
}));
387+
},
388+
selectedChannelUuidsString() {
389+
return this.selectedChannelUuids.join(`,`);
342390
},
343391
currentModeDisplayName() {
344392
let modeName = `#${this.fixture.modes.indexOf(this.currentMode) + 1}`;
@@ -372,7 +420,8 @@ export default {
372420
},
373421
submitButtonTitle() {
374422
if (this.channel.editMode === `add-existing`) {
375-
return `Add channel`;
423+
const count = this.selectedChannelUuids.length;
424+
return count <= 1 ? `Add channel` : `Add ${count} channels`;
376425
}
377426
378427
if (this.channel.editMode === `create`) {
@@ -404,6 +453,110 @@ export default {
404453
return fixtureEditor.getChannelName(channelUuid);
405454
},
406455
456+
isChannelNameUnique(channelUuid) {
457+
const fixtureEditor = this.$parent;
458+
return fixtureEditor.isChannelNameUnique(channelUuid);
459+
},
460+
461+
isChannelSelected(channelUuid) {
462+
return this.selectedChannelUuids.includes(channelUuid);
463+
},
464+
465+
modeHasChannel(channelUuid) {
466+
return this.currentMode.channels.includes(channelUuid);
467+
},
468+
469+
toggleChannelSelection(channelUuid) {
470+
if (this.isChannelSelected(channelUuid)) {
471+
this.deselectChannel(channelUuid);
472+
}
473+
else {
474+
this.selectChannel(channelUuid);
475+
}
476+
477+
this.channelChanged = true;
478+
},
479+
480+
deselectChannel(channelUuid) {
481+
const deselectedChannel = this.fixture.availableChannels[channelUuid];
482+
const isFineChannel = `coarseChannelId` in deselectedChannel;
483+
484+
// Deselect the channel
485+
this.selectedChannelUuids = this.selectedChannelUuids.filter(uuid => uuid !== channelUuid);
486+
487+
if (isFineChannel) {
488+
// Deselect all finer channels
489+
this.selectedChannelUuids = this.selectedChannelUuids.filter(uuid => {
490+
const channel = this.fixture.availableChannels[uuid];
491+
return (
492+
!(`coarseChannelId` in channel) ||
493+
channel.coarseChannelId !== deselectedChannel.coarseChannelId ||
494+
channel.resolution < deselectedChannel.resolution
495+
);
496+
});
497+
return;
498+
}
499+
500+
// Deselect all fine channels belonging to this coarse channel
501+
this.selectedChannelUuids = this.selectedChannelUuids.filter(uuid => {
502+
const channel = this.fixture.availableChannels[uuid];
503+
return !(`coarseChannelId` in channel) || channel.coarseChannelId !== channelUuid;
504+
});
505+
},
506+
507+
selectChannel(channelUuid) {
508+
if (this.isChannelSelected(channelUuid)) {
509+
return;
510+
}
511+
512+
const selectedChannel = this.fixture.availableChannels[channelUuid];
513+
const isFineChannel = `coarseChannelId` in selectedChannel;
514+
515+
if (!isFineChannel) {
516+
this.selectedChannelUuids.push(channelUuid);
517+
return;
518+
}
519+
520+
// Add the coarse channel if not already selected
521+
const coarseChannelId = selectedChannel.coarseChannelId;
522+
if (!this.isChannelSelected(coarseChannelId) && !this.modeHasChannel(coarseChannelId)) {
523+
this.selectedChannelUuids.push(coarseChannelId);
524+
}
525+
526+
// Add all finer channels between coarse and selected fine channel
527+
const currentResolution = selectedChannel.resolution;
528+
for (const uuid of this.currentModeUnchosenChannelUuids) {
529+
const channel = this.fixture.availableChannels[uuid];
530+
if (
531+
`coarseChannelId` in channel &&
532+
channel.coarseChannelId === coarseChannelId &&
533+
channel.resolution < currentResolution &&
534+
!this.isChannelSelected(uuid) &&
535+
!this.modeHasChannel(uuid)
536+
) {
537+
this.selectedChannelUuids.push(uuid);
538+
}
539+
}
540+
541+
this.selectedChannelUuids.push(channelUuid);
542+
},
543+
544+
async onChannelDoubleClick(channelUuid) {
545+
// Select the channel if not already selected
546+
if (!this.isChannelSelected(channelUuid)) {
547+
this.toggleChannelSelection(channelUuid);
548+
}
549+
550+
if (this.selectedChannelUuids.some(uuid => uuid !== channelUuid)) {
551+
// another channel is already selected, do not submit on double-click
552+
return;
553+
}
554+
555+
// wait until validation state is updated
556+
await this.$nextTick();
557+
this.onSubmit();
558+
},
559+
407560
async onChannelDialogOpen() {
408561
if (this.restored) {
409562
this.restored = false;
@@ -415,6 +568,7 @@ export default {
415568
}
416569
else if (this.channel.editMode === `add-existing`) {
417570
this.channel.uuid = ``;
571+
this.selectedChannelUuids = [];
418572
}
419573
else if (this.channel.editMode === `edit-all` || this.channel.editMode === `edit-duplicate`) {
420574
this.copyPropertiesFromChannel(this.fixture.availableChannels[this.channel.uuid]);
@@ -624,7 +778,14 @@ export default {
624778
},
625779
626780
addExistingChannel() {
627-
this.currentMode.channels.push(this.channel.uuid);
781+
for (const channelUuid of this.selectedChannelUuids) {
782+
if (!this.modeHasChannel(channelUuid)) {
783+
this.currentMode.channels.push(channelUuid);
784+
}
785+
}
786+
787+
// Reset selection
788+
this.selectedChannelUuids = [];
628789
},
629790
630791
/**

0 commit comments

Comments
 (0)