Skip to content

Commit 3d636fc

Browse files
committed
fix bug and add label selector
1 parent 4efc69c commit 3d636fc

File tree

3 files changed

+192
-209
lines changed

3 files changed

+192
-209
lines changed

tests/integration/project_workflow_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,5 +442,5 @@ func TestProjectWorkflowPermissions(t *testing.T) {
442442
session2 := loginUser(t, user2.Name)
443443
req = NewRequest(t, "POST",
444444
fmt.Sprintf("/%s/%s/projects/%d/workflows/%d/delete?_csrf=%s", user.Name, repo.Name, project.ID, workflow.ID, GetUserCSRFToken(t, session2)))
445-
session2.MakeRequest(t, req, http.StatusForbidden)
445+
session2.MakeRequest(t, req, http.StatusNotFound) // we use 404 to avoid leaking existence
446446
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<script lang="ts" setup>
2+
import {onMounted, useTemplateRef, computed, watch, nextTick} from 'vue';
3+
import {fomanticQuery} from '../modules/fomantic/base.ts';
4+
import {contrastColor} from '../utils/color.ts';
5+
6+
interface Label {
7+
id: number | string;
8+
name: string;
9+
color: string;
10+
description?: string;
11+
}
12+
13+
const props = withDefaults(defineProps<{
14+
modelValue: string[];
15+
labels: Label[];
16+
placeholder?: string;
17+
readonly?: boolean;
18+
multiple?: boolean;
19+
}>(), {
20+
placeholder: 'Select labels...',
21+
readonly: false,
22+
multiple: true,
23+
});
24+
25+
const emit = defineEmits<{
26+
'update:modelValue': [value: string[]];
27+
}>();
28+
29+
const elDropdown = useTemplateRef('elDropdown');
30+
31+
// Get selected labels for display
32+
const selectedLabels = computed(() => {
33+
return props.labels.filter((label) =>
34+
props.modelValue.includes(String(label.id)),
35+
);
36+
});
37+
38+
// Get contrast color for label text
39+
const getLabelTextColor = (hexColor: string) => {
40+
return contrastColor(hexColor);
41+
};
42+
43+
// Toggle label selection
44+
const toggleLabel = (labelId: string) => {
45+
if (props.readonly) return;
46+
47+
const currentValues = [...props.modelValue];
48+
const index = currentValues.indexOf(labelId);
49+
50+
if (index > -1) {
51+
currentValues.splice(index, 1);
52+
} else {
53+
if (props.multiple) {
54+
currentValues.push(labelId);
55+
} else {
56+
// Single selection mode: replace with new selection
57+
currentValues.length = 0;
58+
currentValues.push(labelId);
59+
}
60+
}
61+
62+
emit('update:modelValue', currentValues);
63+
};
64+
65+
// Check if a label is selected
66+
const isLabelSelected = (labelId: string) => {
67+
return props.modelValue.includes(labelId);
68+
};
69+
70+
// Initialize Fomantic UI dropdown
71+
const initDropdown = async () => {
72+
if (props.readonly || !elDropdown.value) return;
73+
74+
await nextTick();
75+
fomanticQuery(elDropdown.value).dropdown({
76+
action: 'nothing', // Don't hide on selection for multiple selection
77+
fullTextSearch: true,
78+
});
79+
};
80+
81+
// Watch for readonly changes to reinitialize
82+
watch(() => props.readonly, async (newVal) => {
83+
if (!newVal) {
84+
await initDropdown();
85+
}
86+
});
87+
88+
onMounted(async () => {
89+
await initDropdown();
90+
});
91+
</script>
92+
93+
<template>
94+
<!-- Edit Mode: Dropdown -->
95+
<div v-if="!readonly" ref="elDropdown" class="ui fluid multiple search selection dropdown label-dropdown">
96+
<input type="hidden" :value="modelValue.join(',')">
97+
<i class="dropdown icon"/>
98+
<div class="text" :class="{ default: !modelValue.length }">
99+
<span v-if="!modelValue.length">{{ placeholder }}</span>
100+
<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>
109+
</template>
110+
</div>
111+
<div class="menu">
112+
<div
113+
v-for="label in labels"
114+
:key="label.id"
115+
class="item"
116+
:data-value="String(label.id)"
117+
:class="{ active: isLabelSelected(String(label.id)), selected: isLabelSelected(String(label.id)) }"
118+
@click.prevent="toggleLabel(String(label.id))"
119+
>
120+
<span
121+
class="ui label"
122+
:style="`background-color: ${label.color}; color: ${getLabelTextColor(label.color)}`"
123+
>
124+
{{ label.name }}
125+
</span>
126+
</div>
127+
</div>
128+
</div>
129+
130+
<!-- Readonly Mode: Display Selected Labels -->
131+
<div v-else class="ui labels">
132+
<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>
141+
</div>
142+
</template>
143+
144+
<style scoped>
145+
/* Label selector styles */
146+
.label-dropdown.ui.dropdown .menu > .item.active,
147+
.label-dropdown.ui.dropdown .menu > .item.selected {
148+
background: var(--color-active);
149+
font-weight: var(--font-weight-normal);
150+
}
151+
152+
.label-dropdown.ui.dropdown .menu > .item .ui.label {
153+
margin: 0;
154+
}
155+
156+
.label-dropdown.ui.dropdown > .text > .ui.label {
157+
margin: 0.125rem;
158+
}
159+
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+
171+
.text-muted {
172+
color: var(--color-text-light-2);
173+
}
174+
</style>

0 commit comments

Comments
 (0)