Skip to content

Commit 045945d

Browse files
committed
Fix label menu
1 parent 3d636fc commit 045945d

File tree

3 files changed

+240
-36
lines changed

3 files changed

+240
-36
lines changed

routers/web/projects/workflows.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,16 +290,22 @@ func WorkflowsLabels(ctx *context.Context) {
290290
}
291291

292292
type Label struct {
293-
ID int64 `json:"id"`
294-
Name string `json:"name"`
295-
Color string `json:"color"`
293+
ID int64 `json:"id"`
294+
Name string `json:"name"`
295+
Color string `json:"color"`
296+
Description string `json:"description"`
297+
Exclusive bool `json:"exclusive"`
298+
ExclusiveOrder int `json:"exclusiveOrder"`
296299
}
297300
outputLabels := make([]*Label, 0, len(labels))
298301
for _, label := range labels {
299302
outputLabels = append(outputLabels, &Label{
300-
ID: label.ID,
301-
Name: label.Name,
302-
Color: label.Color,
303+
ID: label.ID,
304+
Name: label.Name,
305+
Color: label.Color,
306+
Description: label.Description,
307+
Exclusive: label.Exclusive,
308+
ExclusiveOrder: label.ExclusiveOrder,
303309
})
304310
}
305311

web_src/css/modules/label.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,26 @@ If the labels-list itself needs some layouts, use extra classes or "tw" helpers.
334334
border-top-left-radius: 0;
335335
}
336336

337+
/* Exclusive label priority order - the numeric display (div.scope-right without ui.label class) */
338+
.ui.label.scope-parent > .scope-right:not(.ui) {
339+
display: inline-flex;
340+
align-items: center;
341+
justify-content: center;
342+
background-color: rgba(0, 0, 0, 0.08);
343+
color: var(--color-text);
344+
font-weight: 600;
345+
min-width: 2em;
346+
padding: 0.5833em 0.833em;
347+
border-bottom-left-radius: 0;
348+
border-top-left-radius: 0;
349+
border-bottom-right-radius: var(--border-radius);
350+
border-top-right-radius: var(--border-radius);
351+
}
352+
353+
[data-theme="dark"] .ui.label.scope-parent > .scope-right:not(.ui) {
354+
background-color: rgba(255, 255, 255, 0.15);
355+
}
356+
337357
.ui.label.archived-label {
338358
filter: grayscale(0.5);
339359
opacity: 0.5;

web_src/js/components/LabelSelector.vue

Lines changed: 208 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ interface Label {
88
name: string;
99
color: string;
1010
description?: string;
11+
exclusive?: boolean;
12+
exclusiveOrder?: number;
1113
}
1214
1315
const props = withDefaults(defineProps<{
@@ -40,16 +42,109 @@ const getLabelTextColor = (hexColor: string) => {
4042
return contrastColor(hexColor);
4143
};
4244
45+
// Convert hex color to RGB
46+
const hexToRGB = (hex: string): {r: number; g: number; b: number} => {
47+
const color = hex.replace(/^#/, '');
48+
return {
49+
r: Number.parseInt(color.substring(0, 2), 16),
50+
g: Number.parseInt(color.substring(2, 4), 16),
51+
b: Number.parseInt(color.substring(4, 6), 16),
52+
};
53+
};
54+
55+
// Get relative luminance of a color
56+
const getRelativeLuminance = (hex: string): number => {
57+
const {r, g, b} = hexToRGB(hex);
58+
const rsRGB = r / 255;
59+
const gsRGB = g / 255;
60+
const bsRGB = b / 255;
61+
62+
const rLinear = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
63+
const gLinear = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
64+
const bLinear = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
65+
66+
return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
67+
};
68+
69+
// Get scope and item colors for exclusive labels
70+
const getScopeColors = (baseColor: string): {scopeColor: string; itemColor: string} => {
71+
const luminance = getRelativeLuminance(baseColor);
72+
const contrast = 0.01 + luminance * 0.03;
73+
const darken = contrast + Math.max(luminance + contrast - 1.0, 0.0);
74+
const lighten = contrast + Math.max(contrast - luminance, 0.0);
75+
const darkenFactor = Math.max(luminance - darken, 0.0) / Math.max(luminance, 1.0 / 255.0);
76+
const lightenFactor = Math.min(luminance + lighten, 1.0) / Math.max(luminance, 1.0 / 255.0);
77+
78+
const {r, g, b} = hexToRGB(baseColor);
79+
80+
const scopeR = Math.min(Math.round(r * darkenFactor), 255);
81+
const scopeG = Math.min(Math.round(g * darkenFactor), 255);
82+
const scopeB = Math.min(Math.round(b * darkenFactor), 255);
83+
84+
const itemR = Math.min(Math.round(r * lightenFactor), 255);
85+
const itemG = Math.min(Math.round(g * lightenFactor), 255);
86+
const itemB = Math.min(Math.round(b * lightenFactor), 255);
87+
88+
const scopeColor = `#${scopeR.toString(16).padStart(2, '0')}${scopeG.toString(16).padStart(2, '0')}${scopeB.toString(16).padStart(2, '0')}`;
89+
const itemColor = `#${itemR.toString(16).padStart(2, '0')}${itemG.toString(16).padStart(2, '0')}${itemB.toString(16).padStart(2, '0')}`;
90+
91+
return {scopeColor, itemColor};
92+
};
93+
94+
// Get exclusive scope from label name
95+
const getExclusiveScope = (label: Label): string => {
96+
if (!label.exclusive) return '';
97+
const lastIndex = label.name.lastIndexOf('/');
98+
if (lastIndex === -1 || lastIndex === 0 || lastIndex === label.name.length - 1) {
99+
return '';
100+
}
101+
return label.name.substring(0, lastIndex);
102+
};
103+
104+
// Get label scope part (before the '/')
105+
const getLabelScope = (label: Label): string => {
106+
const scope = getExclusiveScope(label);
107+
return scope || '';
108+
};
109+
110+
// Get label item part (after the '/')
111+
const getLabelItem = (label: Label): string => {
112+
const scope = getExclusiveScope(label);
113+
if (!scope) return label.name;
114+
return label.name.substring(scope.length + 1);
115+
};
116+
43117
// Toggle label selection
44118
const toggleLabel = (labelId: string) => {
45119
if (props.readonly) return;
46120
121+
const clickedLabel = props.labels.find((l) => String(l.id) === labelId);
122+
if (!clickedLabel) return;
123+
47124
const currentValues = [...props.modelValue];
48125
const index = currentValues.indexOf(labelId);
49126
50127
if (index > -1) {
128+
// Remove the label if already selected
51129
currentValues.splice(index, 1);
52130
} else {
131+
// Handle exclusive labels: remove other labels in same scope
132+
const exclusiveScope = getExclusiveScope(clickedLabel);
133+
if (exclusiveScope) {
134+
// Remove all labels with the same exclusive scope
135+
const labelsToRemove = props.labels
136+
.filter((l) => {
137+
const scope = getExclusiveScope(l);
138+
return scope === exclusiveScope && String(l.id) !== labelId;
139+
})
140+
.map((l) => String(l.id));
141+
142+
labelsToRemove.forEach((id) => {
143+
const idx = currentValues.indexOf(id);
144+
if (idx > -1) currentValues.splice(idx, 1);
145+
});
146+
}
147+
53148
if (props.multiple) {
54149
currentValues.push(labelId);
55150
} else {
@@ -98,14 +193,45 @@ onMounted(async () => {
98193
<div class="text" :class="{ default: !modelValue.length }">
99194
<span v-if="!modelValue.length">{{ placeholder }}</span>
100195
<template v-else>
101-
<span
102-
v-for="labelId in modelValue"
103-
:key="labelId"
104-
class="ui label"
105-
:style="`background-color: ${labels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(labels.find(l => String(l.id) === labelId)?.color)}`"
106-
>
107-
{{ labels.find(l => String(l.id) === labelId)?.name }}
108-
</span>
196+
<template v-for="labelId in modelValue" :key="labelId">
197+
<template v-if="labels.find(l => String(l.id) === labelId)">
198+
<!-- Regular label (no exclusive scope) -->
199+
<span
200+
v-if="!labels.find(l => String(l.id) === labelId).exclusive || !getLabelScope(labels.find(l => String(l.id) === labelId))"
201+
class="ui label"
202+
:style="`background-color: ${labels.find(l => String(l.id) === labelId).color}; color: ${getLabelTextColor(labels.find(l => String(l.id) === labelId).color)}`"
203+
>
204+
{{ labels.find(l => String(l.id) === labelId).name }}
205+
</span>
206+
<!-- Exclusive label with order: scope | item | order -->
207+
<span
208+
v-else-if="labels.find(l => String(l.id) === labelId).exclusiveOrder && labels.find(l => String(l.id) === labelId).exclusiveOrder > 0"
209+
class="ui label scope-parent"
210+
>
211+
<div class="ui label scope-left" :style="`color: ${getLabelTextColor(labels.find(l => String(l.id) === labelId).color)} !important; background-color: ${getScopeColors(labels.find(l => String(l.id) === labelId).color).scopeColor} !important`">
212+
{{ getLabelScope(labels.find(l => String(l.id) === labelId)) }}
213+
</div>
214+
<div class="ui label scope-middle" :style="`color: ${getLabelTextColor(labels.find(l => String(l.id) === labelId).color)} !important; background-color: ${getScopeColors(labels.find(l => String(l.id) === labelId).color).itemColor} !important`">
215+
{{ getLabelItem(labels.find(l => String(l.id) === labelId)) }}
216+
</div>
217+
<div class="ui label scope-right">
218+
{{ labels.find(l => String(l.id) === labelId).exclusiveOrder }}
219+
</div>
220+
</span>
221+
<!-- Exclusive label without order: scope | item -->
222+
<span
223+
v-else
224+
class="ui label scope-parent"
225+
>
226+
<div class="ui label scope-left" :style="`color: ${getLabelTextColor(labels.find(l => String(l.id) === labelId).color)} !important; background-color: ${getScopeColors(labels.find(l => String(l.id) === labelId).color).scopeColor} !important`">
227+
{{ getLabelScope(labels.find(l => String(l.id) === labelId)) }}
228+
</div>
229+
<div class="ui label scope-right" :style="`color: ${getLabelTextColor(labels.find(l => String(l.id) === labelId).color)} !important; background-color: ${getScopeColors(labels.find(l => String(l.id) === labelId).color).itemColor} !important`">
230+
{{ getLabelItem(labels.find(l => String(l.id) === labelId)) }}
231+
</div>
232+
</span>
233+
</template>
234+
</template>
109235
</template>
110236
</div>
111237
<div class="menu">
@@ -117,32 +243,94 @@ onMounted(async () => {
117243
:class="{ active: isLabelSelected(String(label.id)), selected: isLabelSelected(String(label.id)) }"
118244
@click.prevent="toggleLabel(String(label.id))"
119245
>
246+
<!-- Regular label (no exclusive scope) -->
120247
<span
248+
v-if="!label.exclusive || !getLabelScope(label)"
121249
class="ui label"
122250
:style="`background-color: ${label.color}; color: ${getLabelTextColor(label.color)}`"
123251
>
124252
{{ label.name }}
125253
</span>
254+
<!-- Exclusive label with order: scope | item | order -->
255+
<span
256+
v-else-if="label.exclusiveOrder && label.exclusiveOrder > 0"
257+
class="ui label scope-parent"
258+
:title="label.description"
259+
>
260+
<div class="ui label scope-left" :style="`color: ${getLabelTextColor(label.color)} !important; background-color: ${getScopeColors(label.color).scopeColor} !important`">
261+
{{ getLabelScope(label) }}
262+
</div>
263+
<div class="ui label scope-middle" :style="`color: ${getLabelTextColor(label.color)} !important; background-color: ${getScopeColors(label.color).itemColor} !important`">
264+
{{ getLabelItem(label) }}
265+
</div>
266+
<div class="ui label scope-right">
267+
{{ label.exclusiveOrder }}
268+
</div>
269+
</span>
270+
<!-- Exclusive label without order: scope | item -->
271+
<span
272+
v-else
273+
class="ui label scope-parent"
274+
:title="label.description"
275+
>
276+
<div class="ui label scope-left" :style="`color: ${getLabelTextColor(label.color)} !important; background-color: ${getScopeColors(label.color).scopeColor} !important`">
277+
{{ getLabelScope(label) }}
278+
</div>
279+
<div class="ui label scope-right" :style="`color: ${getLabelTextColor(label.color)} !important; background-color: ${getScopeColors(label.color).itemColor} !important`">
280+
{{ getLabelItem(label) }}
281+
</div>
282+
</span>
126283
</div>
127284
</div>
128285
</div>
129286

130287
<!-- Readonly Mode: Display Selected Labels -->
131288
<div v-else class="ui labels">
132289
<span v-if="!selectedLabels.length" class="text-muted">None</span>
133-
<span
134-
v-for="label in selectedLabels"
135-
:key="label.id"
136-
class="ui label"
137-
:style="`background-color: ${label.color}; color: ${getLabelTextColor(label.color)}`"
138-
>
139-
{{ label.name }}
140-
</span>
290+
<template v-for="label in selectedLabels" :key="label.id">
291+
<!-- Regular label (no exclusive scope) -->
292+
<span
293+
v-if="!label.exclusive || !getLabelScope(label)"
294+
class="ui label"
295+
:style="`background-color: ${label.color}; color: ${getLabelTextColor(label.color)}`"
296+
>
297+
{{ label.name }}
298+
</span>
299+
<!-- Exclusive label with order: scope | item | order -->
300+
<span
301+
v-else-if="label.exclusiveOrder && label.exclusiveOrder > 0"
302+
class="ui label scope-parent"
303+
:title="label.description"
304+
>
305+
<div class="ui label scope-left" :style="`color: ${getLabelTextColor(label.color)} !important; background-color: ${getScopeColors(label.color).scopeColor} !important`">
306+
{{ getLabelScope(label) }}
307+
</div>
308+
<div class="ui label scope-middle" :style="`color: ${getLabelTextColor(label.color)} !important; background-color: ${getScopeColors(label.color).itemColor} !important`">
309+
{{ getLabelItem(label) }}
310+
</div>
311+
<div class="ui label scope-right">
312+
{{ label.exclusiveOrder }}
313+
</div>
314+
</span>
315+
<!-- Exclusive label without order: scope | item -->
316+
<span
317+
v-else
318+
class="ui label scope-parent"
319+
:title="label.description"
320+
>
321+
<div class="ui label scope-left" :style="`color: ${getLabelTextColor(label.color)} !important; background-color: ${getScopeColors(label.color).scopeColor} !important`">
322+
{{ getLabelScope(label) }}
323+
</div>
324+
<div class="ui label scope-right" :style="`color: ${getLabelTextColor(label.color)} !important; background-color: ${getScopeColors(label.color).itemColor} !important`">
325+
{{ getLabelItem(label) }}
326+
</div>
327+
</span>
328+
</template>
141329
</div>
142330
</template>
143331

144-
<style scoped>
145-
/* Label selector styles */
332+
<style>
333+
/* Label selector specific styles - not scoped to allow global label.css to work */
146334
.label-dropdown.ui.dropdown .menu > .item.active,
147335
.label-dropdown.ui.dropdown .menu > .item.selected {
148336
background: var(--color-active);
@@ -153,21 +341,11 @@ onMounted(async () => {
153341
margin: 0;
154342
}
155343
156-
.label-dropdown.ui.dropdown > .text > .ui.label {
344+
.label-dropdown.ui.dropdown > .text > .ui.label,
345+
.label-dropdown.ui.dropdown > .text > .ui.label.scope-parent {
157346
margin: 0.125rem;
158347
}
159348
160-
.ui.labels {
161-
display: flex;
162-
flex-wrap: wrap;
163-
gap: 0.5rem;
164-
align-items: center;
165-
}
166-
167-
.ui.labels .ui.label {
168-
margin: 0;
169-
}
170-
171349
.text-muted {
172350
color: var(--color-text-light-2);
173351
}

0 commit comments

Comments
 (0)