Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add BuilderList #4240

Open
wants to merge 45 commits into
base: master-mysterious-egg
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4b60801
add EventPage Exhibitor/Sponsor
FrancoisGe Mar 28, 2025
2b1a6c3
fix custom title option
FrancoisGe Mar 28, 2025
9363652
add Course Page
FrancoisGe Mar 31, 2025
ca578bc
fix sublevel row
FrancoisGe Mar 31, 2025
206e243
add border on card
FrancoisGe Mar 31, 2025
825891a
Add custom filter ranges
Goaman Mar 24, 2025
cdf2ce4
enabledTabs and disabledTabs in colorpicker
Goaman Mar 24, 2025
03d231e
setCustomFilter blend & filterColor
Goaman Mar 24, 2025
4cc53eb
set image format
Goaman Mar 26, 2025
87752a5
set image quality
Goaman Mar 26, 2025
f49f9c7
fix tests
Goaman Mar 26, 2025
2bcca70
gif and shapes
Goaman Mar 31, 2025
74ca06b
this.window
Goaman Mar 31, 2025
d1b69f2
remove disabledTabs
Goaman Apr 1, 2025
1f686f3
fix window for tests
Goaman Apr 1, 2025
3a94a0b
[FIX] html_builder: resolve race condition in WebsiteBuilder edit flow
shsa-odoo Apr 1, 2025
d4687da
gradient in theme options
bso-odoo Apr 1, 2025
8c5c8e0
add shape label and remove button
Goaman Mar 31, 2025
9bad9c7
computeAvailableFormats
Goaman Mar 31, 2025
19fd2e7
remove comment
Goaman Mar 31, 2025
9074d43
translate selectLabel
Goaman Apr 1, 2025
9c6d925
[IMP] website: Improve tour_utils selector
msh-odoo Apr 1, 2025
38ed37c
[FIX]: adapt clickOnSave from tour_utils
shsa-odoo Apr 2, 2025
b0881cb
fix tests
FrancoisGe Apr 2, 2025
eaff182
use page option by default
FrancoisGe Mar 31, 2025
91aed12
each o_editable is contenteditable=true (for restricted access)
FrancoisGe Apr 1, 2025
8c4754a
add access groups
FrancoisGe Apr 1, 2025
e592e0c
remove dev file
FrancoisGe Apr 2, 2025
584f86f
extraStepInfos
Goaman Apr 1, 2025
bb692b1
palette icon in theme color options dropdown
bso-odoo Apr 1, 2025
9a97230
main:has(#o_wblog_post_main) - Blog Page options
blse-odoo Apr 1, 2025
69c8277
add groups in FooterOptionPlugin
blse-odoo Apr 2, 2025
29eaf21
support negated views in websiteConfig action
bso-odoo Mar 28, 2025
af0f8b7
advance theme option and move theme tab to website_builder
bso-odoo Apr 2, 2025
7f9a9cb
Fix empty social_facebook
blse-odoo Apr 2, 2025
10b7445
interactions vs history
bso-odoo Mar 25, 2025
3036466
separate image gallery edit from images
bso-odoo Mar 27, 2025
78430a7
[REV] Do not open the option containers after dropping a snippet
sobo-odoo Mar 26, 2025
e9cb381
Fix and improve the drag and drop from right panel + dropzones
sobo-odoo Mar 27, 2025
8b532f1
Add a hook/resource to preview the snippets correctly when over dropzone
sobo-odoo Apr 1, 2025
963d4d5
Add ImageSnippetOption
sobo-odoo Apr 1, 2025
940a140
add basic BuilderList
agau-odoo Mar 21, 2025
24a24a5
option container buttons: move min-width: min-content; to scss
agau-odoo Mar 27, 2025
27f2247
BuilderList: ensure items always have an _id
agau-odoo Apr 3, 2025
c3a59b5
BuilderList: make use of owl's Component props validation
agau-odoo Apr 4, 2025
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
2 changes: 1 addition & 1 deletion addons/html_builder/static/src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { useSetupAction } from "@web/search/action_hook";
import { InvisibleElementsPanel } from "./sidebar/invisible_elements_panel";
import { BlockTab } from "./sidebar/block_tab";
import { CustomizeTab } from "./sidebar/customize_tab";
import { ThemeTab } from "./sidebar/theme_tab";
import { ThemeTab } from "@html_builder/website_builder/plugins/theme_tab";
import { CORE_PLUGINS } from "./core/core_plugins";
import { EDITOR_COLOR_CSS_VARIABLES, getCSSVariableValue } from "./utils/utils_css";

Expand Down
13 changes: 12 additions & 1 deletion addons/html_builder/static/src/builder.scss
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
}
}

%o_we_sublevel > div:first-of-type::before {
%o_we_sublevel > .hb-row-label::before {
content: "└"; // TODO The size and look of this depends on the
// browser default font, we should use a SVG instead.
display: inline-block;
Expand Down Expand Up @@ -208,3 +208,14 @@ div:hover>.o_we_shape_animated_label {
display: none;
}
}

// TODO Gray scale HUE slider
.o_we_slider_tint input[type="range"] {
appearance: none;
&::-webkit-slider-thumb {
appearance: auto !important;
}
&::-moz-range-thumb {
appearance: auto !important;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BuilderList } from "@html_builder/core/building_blocks/builder_list";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { BuilderButtonGroup } from "./building_blocks/builder_button_group";
import { Dropdown } from "@web/core/dropdown/dropdown";
Expand Down Expand Up @@ -42,6 +43,7 @@ export class BuilderComponentPlugin extends Plugin {
ModelMany2Many,
BuilderDateTimePicker,
BuilderUrlPicker,
BuilderList,
},
};

Expand Down
74 changes: 38 additions & 36 deletions addons/html_builder/static/src/core/builder_options_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ export class BuilderOptionsPlugin extends Plugin {
"builderOverlay",
"overlayButtons",
];
static shared = ["getContainers", "updateContainers"];
static shared = ["getContainers", "updateContainers", "getPageContainers"];
resources = {
step_added_handlers: () => this.updateContainers(),
clean_for_save_handlers: this.cleanForSave.bind(this),
post_undo_handlers: this.restoreContainer.bind(this),
post_redo_handlers: this.restoreContainer.bind(this),
on_add_element_handlers: ({ elementToAdd }) => this.setTarget(elementToAdd),
};

setup() {
Expand Down Expand Up @@ -71,11 +70,41 @@ export class BuilderOptionsPlugin extends Plugin {
return;
}

const newContainers = this.computeContainers(this.target);
// Do not update the containers if they did not change.
if (newContainers.length === this.lastContainers.length) {
const previousIds = this.lastContainers.map((c) => c.id);
const newIds = newContainers.map((c) => c.id);
const areSameElements = newIds.every((id, i) => id === previousIds[i]);
if (areSameElements) {
const previousOptions = this.lastContainers.flatMap((c) => [
...c.options,
...c.headerMiddleButtons,
]);
const newOptions = newContainers.flatMap((c) => [
...c.options,
...c.headerMiddleButtons,
]);
const areSameOptions =
newOptions.length === previousOptions.length &&
newOptions.every((option, i) => option.id === previousOptions[i].id);
if (areSameOptions) {
return;
}
}
}

this.lastContainers = newContainers;
this.dependencies.history.setStepExtra("optionSelection", this.target);
this.dispatchTo("change_current_options_containers_listeners", this.lastContainers);
}

computeContainers(target) {
const mapElementsToOptions = (options) => {
const map = new Map();
for (const option of options) {
const { selector, exclude, editableOnly } = option;
let elements = getClosestElements(this.target, selector);
let elements = getClosestElements(target, selector);
if (!elements.length) {
continue;
}
Expand All @@ -96,7 +125,7 @@ export class BuilderOptionsPlugin extends Plugin {

// Find the closest element with no options that should still have the
// overlay buttons.
let element = this.target;
let element = target;
while (element && !elementToOptions.has(element)) {
if (this.hasOverlayOptions(element)) {
elementToOptions.set(element, []);
Expand All @@ -106,7 +135,7 @@ export class BuilderOptionsPlugin extends Plugin {
}

const previousElementToIdMap = new Map(this.lastContainers.map((c) => [c.element, c.id]));
const newContainers = [...elementToOptions]
return [...elementToOptions]
.sort(([a], [b]) => (b.contains(a) ? 1 : -1))
.map(([element, options]) => ({
id: previousElementToIdMap.get(element) || uniqueId(),
Expand All @@ -118,37 +147,10 @@ export class BuilderOptionsPlugin extends Plugin {
isClonable: isClonable(element),
optionsContainerTopButtons: this.getOptionsContainerTopButtons(element),
}));

// Do not update the containers if they did not change.
if (newContainers.length === this.lastContainers.length) {
const previousIds = this.lastContainers.map((c) => c.id);
const newIds = newContainers.map((c) => c.id);
const areSameElements = newIds.every((id, i) => id === previousIds[i]);
if (areSameElements) {
const previousOptions = this.lastContainers.flatMap((c) => [
...c.options,
...c.headerMiddleButtons,
]);
const newOptions = newContainers.flatMap((c) => [
...c.options,
...c.headerMiddleButtons,
]);
const areSameOptions =
newOptions.length === previousOptions.length &&
newOptions.every((option, i) => option.id === previousOptions[i].id);
if (areSameOptions) {
return;
}
}
}

this.lastContainers = newContainers;
this.dependencies.history.setStepExtra("optionSelection", this.target);
this.dispatchTo("change_current_options_containers_listeners", this.lastContainers);
}

setTarget(target) {
this.target = target;
getPageContainers() {
return this.computeContainers(this.editable.querySelector("main"));
}

getContainers() {
Expand Down Expand Up @@ -194,8 +196,8 @@ export class BuilderOptionsPlugin extends Plugin {
}

restoreContainer(revertedStep) {
if (revertedStep && revertedStep.extra.optionSelection) {
this.updateContainers(revertedStep.extra.optionSelection);
if (revertedStep && revertedStep.extraStepInfos.optionSelection) {
this.updateContainers(revertedStep.extraStepInfos.optionSelection);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<t t-name="html_builder.BuilderButton">
<BuilderComponent>
<button type="button" class="btn" t-att-style="this.props.style ?? 'min-width: min-content;'"
<button type="button" class="btn" t-att-style="this.props.style"
t-att-data-action-id="info.actionId"
t-att-data-action-param="info.actionParam"
t-att-data-action-value="info.actionValue"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ export class BuilderColorPicker extends Component {
static props = {
...basicContainerBuilderComponentProps,
noTransparency: { type: Boolean, optional: true },
withGradient: { type: Boolean, optional: true },
enabledTabs: { type: Array, optional: true },
unit: { type: String, optional: true },
title: { type: String, optional: true },
getUsedCustomColors: { type: Function, optional: true },
selectedTab: { type: String, optional: true },
};
static defaultProps = {
getUsedCustomColors: () => [],
withGradient: true,
};
static components = {
ColorSelector: ColorSelector,
Expand All @@ -97,7 +97,7 @@ export class BuilderColorPicker extends Component {
const { state, onApply, onPreview, onPreviewRevert } = useColorPickerBuilderComponent();
this.colorButton = useRef("colorButton");
this.state = state;
this.state.defaultTab = "solid"; // TODO: select the correct tab based on the color
this.state.defaultTab = this.props.selectedTab || "solid"; // TODO: select the correct tab based on the color
useColorPicker(
"colorButton",
{
Expand All @@ -108,7 +108,7 @@ export class BuilderColorPicker extends Component {
getUsedCustomColors: this.props.getUsedCustomColors,
colorPrefix: "color-prefix-",
noTransparency: this.props.noTransparency,
withGradient: this.props.withGradient,
enabledTabs: this.props.enabledTabs,
},
{
onClose: onPreviewRevert,
Expand Down
155 changes: 155 additions & 0 deletions addons/html_builder/static/src/core/building_blocks/builder_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { BuilderComponent } from "@html_builder/core/building_blocks/builder_component";
import {
basicContainerBuilderComponentProps,
useBuilderComponent,
useInputBuilderComponent,
} from "@html_builder/core/utils";
import { Component, useRef } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { useSortable } from "@web/core/utils/sortable_owl";

export class BuilderList extends Component {
static template = "html_builder.BuilderList";
static props = {
...basicContainerBuilderComponentProps,
id: { type: String, optional: true },
addItemTitle: { type: String, optional: true },
entryShape: {
type: Object,
values: [{ value: "number" }, { value: "text" }],
validate: (value) =>
// is not empty object and doesn't include reserved fields
Object.keys(value).length > 0 && !Object.keys(value).includes("_id"),
optional: true,
},
defaultValue: { optional: true },
sortable: { optional: true },
};
static defaultProps = {
addItemTitle: _t("Add"),
entryShape: { value: "text" },
defaultValue: { value: _t("Item") },
sortable: true,
};
static components = { BuilderComponent };

setup() {
this.validateProps();

useBuilderComponent();
const { state, commit, preview } = useInputBuilderComponent({
id: this.props.id,
defaultValue: this.parseDisplayValue([this.makeDefaultItem()]),
parseDisplayValue: this.parseDisplayValue,
formatRawValue: this.formatRawValue,
});
this.state = state;
this.commit = commit;
this.preview = preview;

if (this.props.sortable) {
useSortable({
enable: () => this.props.sortable,
ref: useRef("table"),
elements: ".o_row_draggable",
handle: ".o_handle_cell",
cursor: "grabbing",
placeholderClasses: ["d-table-row"],
onDrop: (params) => {
const { element, previous } = params;
this.reorderItem(element.dataset.id, previous?.dataset.id);
},
});
}
}

validateProps() {
// keys match
const entryShapeKeys = Object.keys(this.props.entryShape);
const defaultValueKeys = Object.keys(this.props.defaultValue);
const allKeys = new Set([...entryShapeKeys, ...defaultValueKeys]);
if (allKeys.size !== entryShapeKeys.length) {
throw new Error("defaultValue properties don't match entryShape");
}
}

parseDisplayValue(displayValue) {
return JSON.stringify(displayValue);
}

formatRawValue(rawValue) {
const items = rawValue ? JSON.parse(rawValue) : [];
for (const item of items) {
if (!("_id" in item)) {
item._id = this.getNextAvailableEntryId(items);
}
}
return items;
}

addItem() {
const items = this.formatRawValue(this.state.value);
items.push(this.makeDefaultItem());
this.commit(items);
}

deleteItem(e) {
const itemId = e.target.dataset.id;
const items = this.formatRawValue(this.state.value);
this.commit(items.filter((item) => item._id !== itemId));
}

reorderItem(itemId, previousId) {
let items = this.formatRawValue(this.state.value);
const itemToReorder = items.find((item) => item._id === itemId);
items = items.filter((item) => item._id !== itemId);

const previousItem = items.find((item) => item._id === previousId);
const previousItems = items.slice(0, items.indexOf(previousItem) + 1);

const nextItems = items.slice(items.indexOf(previousItem) + 1, items.length);

const newItems = [...previousItems, itemToReorder, ...nextItems];
this.commit(newItems);
}

makeDefaultItem() {
return {
...this.props.defaultValue,
_id: this.getNextAvailableEntryId(),
};
}

getNextAvailableEntryId(items) {
items = items || this.formatRawValue(this.state?.value);
const biggestId = items
.map((item) => parseInt(item._id))
.reduce((acc, id) => (id > acc ? id : acc), -1);
const nextAvailableId = biggestId + 1;
return nextAvailableId.toString();
}

onInput(e) {
this.handleValueChange(e.target, false);
}

onChange(e) {
this.handleValueChange(e.target, true);
}

handleValueChange(targetInputEl, commitToHistory) {
const id = targetInputEl.dataset.id;
const propertyName = targetInputEl.name;
const value = targetInputEl.value;

const items = this.formatRawValue(this.state.value);
const item = items.find((item) => item._id === id);
item[propertyName] = value;

if (commitToHistory) {
this.commit(items);
} else {
this.preview(items);
}
}
}
Loading