Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion src/ui/src/builder/settings/BuilderFieldsText.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
<template>
<div class="BuilderFieldsText" :data-automation-key="props.fieldKey">
<template v-if="fieldControl == FieldControl.Text">
<WdsSelect
v-if="shouldUseDropdown"
v-model="selectValue"
class="content"
:options="selectOptions"
:placeholder="defaultValue"
default-icon=""
/>
<BuilderTemplateInput
v-else
class="content"
:component-id="componentId"
:input-id="inputId"
Expand Down Expand Up @@ -38,6 +47,12 @@ import { Component, FieldControl } from "@/writerTypes";
import { useComponentFieldViewModel } from "../useComponentFieldViewModel";
import injectionKeys from "@/injectionKeys";
import BuilderTemplateInput from "./BuilderTemplateInput.vue";
import { defineAsyncComponentWithLoader } from "@/utils/defineAsyncComponentWithLoader";
import type { Option } from "@/wds/WdsSelect.vue";

const WdsSelect = defineAsyncComponentWithLoader({
loader: () => import("@/wds/WdsSelect.vue"),
});

const wf = inject(injectionKeys.core);

Expand Down Expand Up @@ -78,6 +93,24 @@ const predefinedOptionFns = {
});
return options;
},
pageKeys: () => {
const pages = wf
.getComponents("root", { sortedByPosition: true })
.filter((component) => component.type === "page");
return pages.reduce((acc, page) => {
const key = page.content?.["key"];
if (!key) {
return acc;
}
const label =
page.content?.["title"] ??
page.content?.["name"] ??
key ??
page.id;
acc[key] = label;
return acc;
}, {});
},
uiComponentsWithEvents: () => {
return wf
.getComponents(undefined, { sortedByPosition: true })
Expand All @@ -101,7 +134,7 @@ const predefinedOptionFns = {
},
};

const options = computed(() => {
const options = computed<Record<string, string>>(() => {
const component = wf.getComponentById(props.componentId);
const componentDefinition = wf.getComponentDefinition(component.type);
const field = componentDefinition.fields[props.fieldKey];
Expand All @@ -116,12 +149,35 @@ const options = computed(() => {
return field.options;
});

const selectOptions = computed<Option[]>(() =>
Object.entries(options.value ?? {}).map(([value, label]) => ({
value,
label,
})),
);

const shouldUseDropdown = computed(
() => props.type === "template" && selectOptions.value.length > 0,
);

const inputType = computed(() =>
["state", "state-template"].includes(props.type) ? "state" : "template",
);

const inputValue = computed(() => parseContentValue(fieldViewModel.value));

const selectValue = computed<string | undefined>({
get: () => {
const value = inputValue.value;
return selectOptions.value.some((option) => option.value === value)
? value
: undefined;
},
set(value) {
fieldViewModel.value = transformToContentValue(value ?? "");
},
});

const handleInput = (ev: Event) => {
fieldViewModel.value = transformToContentValue(
(ev.target as HTMLInputElement).value,
Expand Down
1 change: 1 addition & 0 deletions src/ui/src/wds/WdsDropdownMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type WdsDropdownMenuOption = {
label: string;
detail?: string;
shortcut?: string;
isPlaceholder?: boolean;
/**
* A font icon or an array of image URL
*/
Expand Down
7 changes: 7 additions & 0 deletions src/ui/src/wds/WdsDropdownMenuItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const iconStyle = computed(() => {
'WdsDropdownMenuItem--selected': selected,
'WdsDropdownMenuItem--hideIcon': hideIcons,
'WdsDropdownMenuItem--danger': option.variant === 'danger',
'WdsDropdownMenuItem--placeholder': option.isPlaceholder,
}"
:style
:data-automation-key="option.value"
Expand Down Expand Up @@ -153,6 +154,12 @@ const iconStyle = computed(() => {
.WdsDropdownMenuItem--danger {
color: var(--wdsColorOrange5);
}
.WdsDropdownMenuItem--placeholder {
color: var(--wdsColorGray4);
}
.WdsDropdownMenuItem--placeholder .WdsDropdownMenuItem__detail {
color: var(--wdsColorGray3);
}

.WdsDropdownMenuItem__detail,
.WdsDropdownMenuItem__label,
Expand Down
141 changes: 119 additions & 22 deletions src/ui/src/wds/WdsSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<!-- use a `<div>` instead of button because Firefox has an issue with draggable `<button>` https://bugzilla.mozilla.org/show_bug.cgi?id=568313 -->
<div
class="WdsSelect__trigger"
:class="{
'WdsSelect__trigger--placeholder': isPlaceholderSelected,
}"
role="button"
tabindex="0"
@click="isOpen = !isOpen"
Expand Down Expand Up @@ -34,10 +37,10 @@
@close="handleRemoveValue(option.value)"
/>
<p
v-if="selectedOptions.length === 0 && placeholder"
v-if="selectedOptions.length === 0"
class="WdsSelect__trigger__multiSelectLabel__placeholder"
>
{{ placeholder }}
{{ placeholderLabel }}
</p>
</div>
<div
Expand All @@ -46,7 +49,7 @@
data-writer-tooltip-strategy="overflow"
:data-writer-tooltip="currentLabel"
>
{{ currentLabel ?? placeholder }}
{{ currentLabel ?? placeholderLabel }}
</div>
<div class="WdsSelect__trigger__arrow">
<WdsIcon :name="isOpen ? 'chevron-up' : 'chevron-down'" />
Expand All @@ -60,7 +63,7 @@
:enable-multi-selection="enableMultiSelection"
:hide-icons="hideIcons"
:loading="loading"
:options="options"
:options="selectOptions"
:selected="currentValue"
:style="floatingStyles"
@select="onSelect"
Expand Down Expand Up @@ -102,6 +105,7 @@ const props = defineProps({
enableSearch: { type: Boolean, required: false },
enableMultiSelection: { type: Boolean, required: false },
loading: { type: Boolean, required: false },
required: { type: Boolean, required: false, default: false },
});

const currentValue = defineModel({
Expand Down Expand Up @@ -129,35 +133,84 @@ const { floatingStyles, update: updateFloatingStyle } = useFloating(
},
);

const PLACEHOLDER_VALUE = "";
const placeholderLabel = computed(
() => props.placeholder ?? "Select an option...",
);

const shouldInjectPlaceholder = computed(
() => !props.required && !props.enableMultiSelection,
);

const selectOptions = computed<WdsDropdownMenuOption[]>(() => {
const normalized = (props.options ?? []).map((option) => ({
...option,
label:
option.label ??
(option.value !== undefined ? String(option.value) : ""),
}));

const hasEmptyValueOption = normalized.some(
(option) => option.value === PLACEHOLDER_VALUE,
);

const shouldAddPlaceholder =
shouldInjectPlaceholder.value && !hasEmptyValueOption;

if (!shouldAddPlaceholder) {
return normalized;
}

return [
{
value: PLACEHOLDER_VALUE,
label: placeholderLabel.value,
isPlaceholder: true,
},
...normalized.filter((option) => !option.isPlaceholder),
];
});

const currentValueArray = computed(() => {
if (!currentValue.value) return [];
const array = Array.isArray(currentValue.value)
? currentValue.value
: [currentValue.value];
return array.filter(Boolean);
const value = currentValue.value;
if (value === undefined || value === null) return [];
const array = Array.isArray(value) ? value : [value];
return array.filter((v) => v !== undefined && v !== null) as string[];
});

function findOption(value: string | undefined) {
if (value === undefined) return undefined;
return selectOptions.value.find((option) => option.value === value);
}

const selectedOptions = computed<WdsDropdownMenuOption[]>(() =>
currentValueArray.value.map(
(v) =>
props.options.find((o) => o.value === v) ?? { value: v, label: v },
(value) =>
findOption(value) ?? {
value,
label: String(value),
},
),
);

const hasUnknowOptionSelected = computed(() => {
return (
currentValue.value &&
!props.options.some((o) => o.value === currentValue.value)
);
});
const hasUnknowOptionSelected = computed(() =>
currentValueArray.value.some((value) => !findOption(value)),
);

const currentLabel = computed(() => {
if (hasUnknowOptionSelected.value) return String(currentValue.value);
if (hasUnknowOptionSelected.value) {
return Array.isArray(currentValue.value)
? currentValue.value.filter(Boolean).join(" / ")
: String(currentValue.value ?? "");
}

const labels = selectedOptions.value.map((o) => o.label).filter(Boolean);
if (!labels.length) return undefined;

return selectedOptions.value
.map((o) => o.label)
.sort()
.join(" / ");
const sortedLabels = [...labels].sort((a, b) => a.localeCompare(b));
return props.enableMultiSelection
? sortedLabels.join(" / ")
: sortedLabels[0];
});

const currentIcon = computed(() => {
Expand All @@ -170,6 +223,47 @@ const currentIcon = computed(() => {
);
});

const isPlaceholderSelected = computed(() => {
if (props.enableMultiSelection || props.required) return false;
return findOption(
typeof currentValue.value === "string" ? currentValue.value : undefined,
)?.isPlaceholder;
});

watch(
[
selectOptions,
() => props.required,
() => props.enableMultiSelection,
() => currentValue.value,
],
ensureValidSelection,
{ immediate: true },
);

function ensureValidSelection() {
if (props.enableMultiSelection) return;

const options = selectOptions.value;
if (!options.length) return;

const current = currentValue.value;
const asString = typeof current === "string" ? current : undefined;
const hasCurrentSelection =
asString !== undefined && Boolean(findOption(asString));

if (props.required) {
if (!hasCurrentSelection || asString === "") {
currentValue.value = options[0].value;
}
return;
}

if (!hasCurrentSelection) {
currentValue.value = PLACEHOLDER_VALUE;
}
}

// close the dropdown when clicking outside
const hasFocus = useFocusWithin(trigger);
watch(
Expand Down Expand Up @@ -244,6 +338,9 @@ function handleRemoveValue(value: string) {
font-weight: 300;
cursor: pointer;
}
.WdsSelect__trigger--placeholder .WdsSelect__trigger__label {
color: var(--wdsColorGray4);
}

.WdsSelect__trigger__multiSelectLabel {
flex-grow: 1;
Expand Down
1 change: 1 addition & 0 deletions src/writer/blocks/changepage.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def register(cls, type: str):
"name": "Page key",
"type": "Text",
"desc": "The identifying key of the target page.",
"options": "pageKeys",
},
},
"outs": {
Expand Down
Loading