Skip to content

Commit 8df9a41

Browse files
feat(ui): enhance WdsSelect and BuilderFieldsText components with dropdown support and placeholder options
- Added WdsSelect component with dynamic placeholder handling and improved option selection logic. - Integrated dropdown functionality into BuilderFieldsText for better user experience. - Updated WdsDropdownMenu and WdsDropdownMenuItem to support placeholder styling. - Introduced page key options in ChangePage block for better page navigation.
1 parent c002d4b commit 8df9a41

5 files changed

Lines changed: 177 additions & 23 deletions

File tree

src/ui/src/builder/settings/BuilderFieldsText.vue

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
<template>
22
<div class="BuilderFieldsText" :data-automation-key="props.fieldKey">
33
<template v-if="fieldControl == FieldControl.Text">
4+
<WdsSelect
5+
v-if="shouldUseDropdown"
6+
v-model="selectValue"
7+
class="content"
8+
:options="selectOptions"
9+
:placeholder="defaultValue"
10+
/>
411
<BuilderTemplateInput
12+
v-else
513
class="content"
614
:component-id="componentId"
715
:input-id="inputId"
@@ -38,6 +46,12 @@ import { Component, FieldControl } from "@/writerTypes";
3846
import { useComponentFieldViewModel } from "../useComponentFieldViewModel";
3947
import injectionKeys from "@/injectionKeys";
4048
import BuilderTemplateInput from "./BuilderTemplateInput.vue";
49+
import { defineAsyncComponentWithLoader } from "@/utils/defineAsyncComponentWithLoader";
50+
import type { Option } from "@/wds/WdsSelect.vue";
51+
52+
const WdsSelect = defineAsyncComponentWithLoader({
53+
loader: () => import("@/wds/WdsSelect.vue"),
54+
});
4155
4256
const wf = inject(injectionKeys.core);
4357
@@ -78,6 +92,24 @@ const predefinedOptionFns = {
7892
});
7993
return options;
8094
},
95+
pageKeys: () => {
96+
const pages = wf
97+
.getComponents("root", { sortedByPosition: true })
98+
.filter((component) => component.type === "page");
99+
return pages.reduce((acc, page) => {
100+
const key = page.content?.["key"];
101+
if (!key) {
102+
return acc;
103+
}
104+
const label =
105+
page.content?.["title"] ??
106+
page.content?.["name"] ??
107+
key ??
108+
page.id;
109+
acc[key] = label;
110+
return acc;
111+
}, {});
112+
},
81113
uiComponentsWithEvents: () => {
82114
return wf
83115
.getComponents(undefined, { sortedByPosition: true })
@@ -101,7 +133,7 @@ const predefinedOptionFns = {
101133
},
102134
};
103135
104-
const options = computed(() => {
136+
const options = computed<Record<string, string>>(() => {
105137
const component = wf.getComponentById(props.componentId);
106138
const componentDefinition = wf.getComponentDefinition(component.type);
107139
const field = componentDefinition.fields[props.fieldKey];
@@ -116,12 +148,35 @@ const options = computed(() => {
116148
return field.options;
117149
});
118150
151+
const selectOptions = computed<Option[]>(() =>
152+
Object.entries(options.value ?? {}).map(([value, label]) => ({
153+
value,
154+
label,
155+
})),
156+
);
157+
158+
const shouldUseDropdown = computed(
159+
() => props.type === "template" && selectOptions.value.length > 0,
160+
);
161+
119162
const inputType = computed(() =>
120163
["state", "state-template"].includes(props.type) ? "state" : "template",
121164
);
122165
123166
const inputValue = computed(() => parseContentValue(fieldViewModel.value));
124167
168+
const selectValue = computed<string | undefined>({
169+
get: () => {
170+
const value = inputValue.value;
171+
return selectOptions.value.some((option) => option.value === value)
172+
? value
173+
: undefined;
174+
},
175+
set(value) {
176+
fieldViewModel.value = transformToContentValue(value ?? "");
177+
},
178+
});
179+
125180
const handleInput = (ev: Event) => {
126181
fieldViewModel.value = transformToContentValue(
127182
(ev.target as HTMLInputElement).value,

src/ui/src/wds/WdsDropdownMenu.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export type WdsDropdownMenuOption = {
8989
label: string;
9090
detail?: string;
9191
shortcut?: string;
92+
isPlaceholder?: boolean;
9293
/**
9394
* A font icon or an array of image URL
9495
*/

src/ui/src/wds/WdsDropdownMenuItem.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const iconStyle = computed(() => {
4949
'WdsDropdownMenuItem--selected': selected,
5050
'WdsDropdownMenuItem--hideIcon': hideIcons,
5151
'WdsDropdownMenuItem--danger': option.variant === 'danger',
52+
'WdsDropdownMenuItem--placeholder': option.isPlaceholder,
5253
}"
5354
:style
5455
:data-automation-key="option.value"
@@ -153,6 +154,12 @@ const iconStyle = computed(() => {
153154
.WdsDropdownMenuItem--danger {
154155
color: var(--wdsColorOrange5);
155156
}
157+
.WdsDropdownMenuItem--placeholder {
158+
color: var(--wdsColorGray4);
159+
}
160+
.WdsDropdownMenuItem--placeholder .WdsDropdownMenuItem__detail {
161+
color: var(--wdsColorGray3);
162+
}
156163
157164
.WdsDropdownMenuItem__detail,
158165
.WdsDropdownMenuItem__label,

src/ui/src/wds/WdsSelect.vue

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
<!-- use a `<div>` instead of button because Firefox has an issue with draggable `<button>` https://bugzilla.mozilla.org/show_bug.cgi?id=568313 -->
44
<div
55
class="WdsSelect__trigger"
6+
:class="{
7+
'WdsSelect__trigger--placeholder': isPlaceholderSelected,
8+
}"
69
role="button"
710
tabindex="0"
811
@click="isOpen = !isOpen"
@@ -34,10 +37,10 @@
3437
@close="handleRemoveValue(option.value)"
3538
/>
3639
<p
37-
v-if="selectedOptions.length === 0 && placeholder"
40+
v-if="selectedOptions.length === 0"
3841
class="WdsSelect__trigger__multiSelectLabel__placeholder"
3942
>
40-
{{ placeholder }}
43+
{{ placeholderLabel }}
4144
</p>
4245
</div>
4346
<div
@@ -46,7 +49,7 @@
4649
data-writer-tooltip-strategy="overflow"
4750
:data-writer-tooltip="currentLabel"
4851
>
49-
{{ currentLabel ?? placeholder }}
52+
{{ currentLabel ?? placeholderLabel }}
5053
</div>
5154
<div class="WdsSelect__trigger__arrow">
5255
<WdsIcon :name="isOpen ? 'chevron-up' : 'chevron-down'" />
@@ -60,7 +63,7 @@
6063
:enable-multi-selection="enableMultiSelection"
6164
:hide-icons="hideIcons"
6265
:loading="loading"
63-
:options="options"
66+
:options="selectOptions"
6467
:selected="currentValue"
6568
:style="floatingStyles"
6669
@select="onSelect"
@@ -102,6 +105,7 @@ const props = defineProps({
102105
enableSearch: { type: Boolean, required: false },
103106
enableMultiSelection: { type: Boolean, required: false },
104107
loading: { type: Boolean, required: false },
108+
required: { type: Boolean, required: false, default: false },
105109
});
106110
107111
const currentValue = defineModel({
@@ -129,35 +133,77 @@ const { floatingStyles, update: updateFloatingStyle } = useFloating(
129133
},
130134
);
131135
136+
const PLACEHOLDER_VALUE = "";
137+
const placeholderLabel = computed(
138+
() => props.placeholder ?? "Select an option..",
139+
);
140+
141+
const shouldInjectPlaceholder = computed(
142+
() => !props.required && !props.enableMultiSelection,
143+
);
144+
145+
const selectOptions = computed<WdsDropdownMenuOption[]>(() => {
146+
const normalized = (props.options ?? []).map((option) => ({
147+
...option,
148+
label:
149+
option.label ??
150+
(option.value !== undefined ? String(option.value) : ""),
151+
}));
152+
153+
if (!shouldInjectPlaceholder.value) {
154+
return normalized;
155+
}
156+
157+
return [
158+
{
159+
value: PLACEHOLDER_VALUE,
160+
label: placeholderLabel.value,
161+
isPlaceholder: true,
162+
},
163+
...normalized.filter(
164+
(option) =>
165+
!option.isPlaceholder && option.value !== PLACEHOLDER_VALUE,
166+
),
167+
];
168+
});
169+
132170
const currentValueArray = computed(() => {
133-
if (!currentValue.value) return [];
134-
const array = Array.isArray(currentValue.value)
135-
? currentValue.value
136-
: [currentValue.value];
137-
return array.filter(Boolean);
171+
const value = currentValue.value;
172+
if (value === undefined || value === null) return [];
173+
const array = Array.isArray(value) ? value : [value];
174+
return array.filter((v) => v !== undefined && v !== null) as string[];
138175
});
139176
177+
function findOption(value: string | undefined) {
178+
if (value === undefined) return undefined;
179+
return selectOptions.value.find((option) => option.value === value);
180+
}
181+
140182
const selectedOptions = computed<WdsDropdownMenuOption[]>(() =>
141183
currentValueArray.value.map(
142-
(v) =>
143-
props.options.find((o) => o.value === v) ?? { value: v, label: v },
184+
(value) =>
185+
findOption(value) ?? {
186+
value,
187+
label: String(value),
188+
},
144189
),
145190
);
146191
147-
const hasUnknowOptionSelected = computed(() => {
148-
return (
149-
currentValue.value &&
150-
!props.options.some((o) => o.value === currentValue.value)
151-
);
152-
});
192+
const hasUnknowOptionSelected = computed(() =>
193+
currentValueArray.value.some((value) => !findOption(value)),
194+
);
153195
154196
const currentLabel = computed(() => {
155-
if (hasUnknowOptionSelected.value) return String(currentValue.value);
197+
if (hasUnknowOptionSelected.value) {
198+
return Array.isArray(currentValue.value)
199+
? currentValue.value.filter(Boolean).join(" / ")
200+
: String(currentValue.value ?? "");
201+
}
156202
157-
return selectedOptions.value
158-
.map((o) => o.label)
159-
.sort()
160-
.join(" / ");
203+
if (!selectedOptions.value.length) return undefined;
204+
return props.enableMultiSelection
205+
? selectedOptions.value.map((o) => o.label).join(" / ")
206+
: selectedOptions.value[0].label;
161207
});
162208
163209
const currentIcon = computed(() => {
@@ -170,6 +216,47 @@ const currentIcon = computed(() => {
170216
);
171217
});
172218
219+
const isPlaceholderSelected = computed(() => {
220+
if (props.enableMultiSelection || props.required) return false;
221+
return findOption(
222+
typeof currentValue.value === "string" ? currentValue.value : undefined,
223+
)?.isPlaceholder;
224+
});
225+
226+
watch(
227+
[
228+
selectOptions,
229+
() => props.required,
230+
() => props.enableMultiSelection,
231+
() => currentValue.value,
232+
],
233+
ensureValidSelection,
234+
{ immediate: true },
235+
);
236+
237+
function ensureValidSelection() {
238+
if (props.enableMultiSelection) return;
239+
240+
const options = selectOptions.value;
241+
if (!options.length) return;
242+
243+
const current = currentValue.value;
244+
const asString = typeof current === "string" ? current : undefined;
245+
const hasCurrentSelection =
246+
asString !== undefined && Boolean(findOption(asString));
247+
248+
if (props.required) {
249+
if (!hasCurrentSelection || asString === "") {
250+
currentValue.value = options[0].value;
251+
}
252+
return;
253+
}
254+
255+
if (!hasCurrentSelection) {
256+
currentValue.value = PLACEHOLDER_VALUE;
257+
}
258+
}
259+
173260
// close the dropdown when clicking outside
174261
const hasFocus = useFocusWithin(trigger);
175262
watch(
@@ -244,6 +331,9 @@ function handleRemoveValue(value: string) {
244331
font-weight: 300;
245332
cursor: pointer;
246333
}
334+
.WdsSelect__trigger--placeholder .WdsSelect__trigger__label {
335+
color: var(--wdsColorGray4);
336+
}
247337
248338
.WdsSelect__trigger__multiSelectLabel {
249339
flex-grow: 1;

src/writer/blocks/changepage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def register(cls, type: str):
2020
"name": "Page key",
2121
"type": "Text",
2222
"desc": "The identifying key of the target page.",
23+
"options": "pageKeys",
2324
},
2425
},
2526
"outs": {

0 commit comments

Comments
 (0)