Skip to content

Progress bar options #3687

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

Merged
merged 2 commits into from
Dec 18, 2024
Merged
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
12 changes: 11 additions & 1 deletion addons/html_builder/static/src/builder/builder_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export function useClickableWeWidget() {
if (!editingElements.length) {
return;
}
return getAllActions().every((o) => {
const areActionsActiveTabs = getAllActions().map((o) => {
const { actionId, actionParam, actionValue } = o;
// TODO isActive === first editing el or all ?
const editingElement = editingElements[0];
Expand All @@ -291,6 +291,16 @@ export function useClickableWeWidget() {
value: actionValue,
});
});
// If there is no `isActive` method for the widget return false
if (areActionsActiveTabs.every((el) => el === undefined)) {
return false;
}
// If `isActive` is explicitly false for an action return false
if (areActionsActiveTabs.some((el) => el === false)) {
return false;
}
// `isActive` is true for at least one action
return true;
}

return {
Expand Down
30 changes: 30 additions & 0 deletions addons/html_builder/static/src/builder/components/WeCheckbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Component } from "@odoo/owl";
import { CheckBox } from "@web/core/checkbox/checkbox";
import {
clickableWeWidgetProps,
useClickableWeWidget,
WeComponent,
useDependecyDefinition,
} from "../builder_helpers";

export class WeCheckbox extends Component {
static template = "html_builder.WeCheckbox";
static components = { WeComponent, CheckBox };
static props = {
...clickableWeWidgetProps,
id: { type: String, optional: true },
};

setup() {
const { state, operation, isActive } = useClickableWeWidget();
if (this.props.id) {
useDependecyDefinition({ id: this.props.id, isActive });
}
this.state = state;
this.onChange = operation.commit;
}

getClassName() {
return "o_field_boolean o_boolean_toggle form-switch" + (this.props.extraClassName || "");
}
}
10 changes: 10 additions & 0 deletions addons/html_builder/static/src/builder/components/WeCheckbox.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">

<t t-name="html_builder.WeCheckbox">
<WeComponent dependencies="props.dependencies">
<CheckBox className="getClassName()" onChange="onChange" value="state.isActive"/>
</WeComponent>
</t>

</templates>
1 change: 1 addition & 0 deletions addons/html_builder/static/src/builder/components/WeRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class WeRow extends Component {
dependencies: { type: [String, Array], optional: true },
tooltip: { type: String, optional: true },
slots: { type: Object, optional: true },
extraClassName: { type: String, optional: true },
};

setup() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<t t-name="html_builder.WeRow">
<WeComponent dependencies="props.dependencies">
<div class="d-flex p-1 px-2 hb-row" t-ref="root">
<div class="d-flex p-1 px-2 hb-row" t-att-class="props.extraClassName or ''" t-ref="root">
<div class="d-flex" style="flex-grow: 0.4; flex-basis: 0; min-width: 0;" t-att-data-tooltip="props.tooltip">
<span class="text-nowrap text-truncate" t-out="props.label"/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { WeSelect } from "./WeSelect";
import { WeSelectItem } from "./WeSelectItem";
import { WeColorpicker } from "./WeColorpicker";
import { WeTextInput } from "./WeTextInput";
import { WeCheckbox } from "./WeCheckbox";

export const defaultOptionComponents = {
WeRow,
Expand All @@ -22,4 +23,5 @@ export const defaultOptionComponents = {
WeColorpicker,
WeSelect,
WeSelectItem,
WeCheckbox,
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ class AlertOptionPlugin extends Plugin {
}
icon.classList.remove(className);
},

isActive: () => {
return true;
isActive: ({ editingElement, param }) => {
const iconEl = editingElement.querySelector(".s_alert_icon");
if (!iconEl) {
return;
}
return iconEl.classList.contains(param);
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { registry } from "@web/core/registry";
import { Plugin } from "@html_editor/plugin";
import { clamp } from "@web/core/utils/numbers";
class ProgressBarOptionPlugin extends Plugin {
static id = "ProgressBarOption";
selector = ".s_progress_bar";
resources = {
builder_options: {
template: "html_builder.ProgressBarOption",
selector: this.selector,
},
builder_actions: this.getActions(),
clean_for_save_handlers: this.cleanForSave.bind(this),
};

cleanForSave({ root }) {
const editingEls = root.querySelectorAll(this.selector);
for (const editingEl of editingEls) {
const progressBar = editingEl.querySelector(".progress-bar");
const progressLabel = editingEl.querySelector(".s_progress_bar_text");

if (!progressBar.classList.contains("progress-bar-striped")) {
progressBar.classList.remove("progress-bar-animated");
}

if (progressLabel && progressLabel.classList.contains("d-none")) {
progressLabel.remove();
}
}
}
getActions() {
return {
display: {
apply: ({ editingElement, param }) => {
// retro-compatibility
if (editingElement.classList.contains("progress")) {
editingElement.classList.remove("progress");
const progressBarEl = editingElement.querySelector(".progress-bar");
if (progressBarEl) {
const wrapperEl = document.createElement("div");
wrapperEl.classList.add("progress");
progressBarEl.parentNode.insertBefore(wrapperEl, progressBarEl);
wrapperEl.appendChild(progressBarEl);
editingElement
.querySelector(".progress-bar span")
.classList.add("s_progress_bar_text");
}
}

const progress = editingElement.querySelector(".progress");
const progressValue = progress.getAttribute("aria-valuenow");
let progressLabel = editingElement.querySelector(".s_progress_bar_text");

if (!progressLabel && param !== "none") {
progressLabel = document.createElement("span");
progressLabel.classList.add("s_progress_bar_text", "small");
progressLabel.textContent = progressValue + "%";
}

if (param === "inline") {
editingElement.querySelector(".progress-bar").appendChild(progressLabel);
} else if (["below", "after"].includes(param)) {
progress.insertAdjacentElement("afterend", progressLabel);
}

// Temporary hide the label. It's effectively removed in cleanForSave
// if the option is confirmed
progressLabel.classList.toggle("d-none", param === "none");
},
},
Copy link
Author

@loco-odoo loco-odoo Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To discuss: add isActive or not?

progressBarValue: {
apply: ({ editingElement, value }) => {
value = clamp(value, 0, 100);
const progressBarEl = editingElement.querySelector(".progress-bar");
const progressBarTextEl = editingElement.querySelector(".s_progress_bar_text");
const progressMainEl = editingElement.querySelector(".progress");
// Target precisely the XX% not only XX to not replace wrong element
// eg 'Since 1978 we have completed 45%' <- don't replace 1978
progressBarTextEl.innerText = progressBarTextEl.innerText.replace(
/[0-9]+%/,
value + "%"
);
progressMainEl.setAttribute("aria-valuenow", value);
progressBarEl.style.width = value + "%";
},
getValue: ({ editingElement }) =>
editingElement.querySelector(".progress").getAttribute("aria-valuenow"),
},
};
}
}
registry.category("website-plugins").add(ProgressBarOptionPlugin.id, ProgressBarOptionPlugin);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">

<t t-name="html_builder.ProgressBarOption">
<WeRow label.translate="Value">
<WeNumberInput action="'progressBarValue'" unit="'%'"/>
</WeRow>
<WeRow label.translate="Label">
<WeSelect>
<WeSelectItem action="'display'" actionParam="'inline'" classAction="'s_progress_bar_label_inline'">Display Inside</WeSelectItem>
<WeSelectItem action="'display'" actionParam="'below'" classAction="'s_progress_bar_label_below'">Display Below</WeSelectItem>
<WeSelectItem action="'display'" actionParam="'after'" classAction="'s_progress_bar_label_after'">Display After</WeSelectItem>
<WeSelectItem action="'display'" actionParam="'none'" classAction="'s_progress_bar_label_hidden'">Hide</WeSelectItem>
</WeSelect>
</WeRow>
<WeRow label.translate="Colors">
<WeColorpicker applyTo="'.progress-bar'" styleAction="'background-color'"/>
</WeRow>
<WeRow label.translate="Striped">
<WeCheckbox id="'striped_option'" classAction="'progress-bar-striped'" applyTo="'.progress-bar'"/>
</WeRow>
<WeRow label.translate="Animated" extraClassName="'o_we_sublevel'">
<WeCheckbox classAction="'progress-bar-animated'" dependencies="'striped_option'" applyTo="'.progress-bar'"/>
</WeRow>
</t>

</templates>
2 changes: 1 addition & 1 deletion addons/html_builder/static/src/builder/snippets_menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
}
}

.o_we_sublevel > div::before {
.o_we_sublevel > div:first-child::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
20 changes: 20 additions & 0 deletions addons/html_builder/static/tests/toolbox/misc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,23 @@ test("don't rerender the OptionsContainer every time you click on the same eleme
await contains(":iframe .sub-child-target").click();
expect.verifySteps([]);
});

test("no need to define 'isActive' method for custom action if the widget already has a generic action", async () => {
addOption({
selector: ".s_test",
template: xml`
<WeRow label.translate="Type">
<WeSelect>
<WeSelectItem classAction="'alert-info'" action="'alertIcon'" actionParam="'fa-info-circle'">Info</WeSelectItem>
</WeSelect>
</WeRow>
`,
});

await setupWebsiteBuilder(`
<div class="s_test alert-info">
a
</div>`);
await contains(":iframe .s_test").click();
expect(".options-container button").toHaveText("Info");
});
63 changes: 63 additions & 0 deletions addons/html_builder/static/tests/toolbox/widget.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ describe("WeRow", () => {
await contains("[data-class-action='my-custom-class']").click();
expect(queryAllTexts(selectorRowLabel)).toEqual(["Row 1", "Row 3"]);
});
test("extra classes on WeRow", async () => {
addOption({
selector: ".test-options-target",
template: xml`<WeRow label="'my label'" extraClassName="'extra-class'">row text</WeRow>`,
});
await setupWebsiteBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect(".hb-row").toHaveClass("extra-class");
});
});
describe("WeButton", () => {
test("call a specific action with some params and value", async () => {
Expand Down Expand Up @@ -883,6 +893,59 @@ describe("WeColorpicker", () => {
expect(".options-container .o_we_color_preview").toHaveCount(1);
});
});
describe("WeCheckbox", () => {
test("Click on checkbox", async () => {
addOption({
selector: ".test-options-target",
template: xml`<WeCheckbox classAction="'checkbox-action'"/>`,
});
const { getEditor } = await setupWebsiteBuilder(`<div class="test-options-target">b</div>`);
const editor = getEditor();

await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect(".o-checkbox .form-check-input:checked").toHaveCount(0);
expect(editor.editable).toHaveInnerHTML(`<div class="test-options-target">b</div>`);

await contains(".o-checkbox").click();
expect(".o-checkbox .form-check-input:checked").toHaveCount(1);
expect(editor.editable).toHaveInnerHTML(
`<div class="test-options-target checkbox-action">b</div>`
);

await contains(".o-checkbox").click();
expect(".o-checkbox .form-check-input:checked").toHaveCount(0);
expect(editor.editable).toHaveInnerHTML(`<div class="test-options-target">b</div>`);
});
test("hide/display base on applyTo", async () => {
addOption({
selector: ".parent-target",
template: xml`<WeButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addOption({
selector: ".parent-target",
template: xml`<WeCheckbox classAction="'checkbox-action'" applyTo="'.my-custom-class'"/>`,
});
const { getEditor } = await setupWebsiteBuilder(
`<div class="parent-target"><div class="child-target b">b</div></div>`
);
const editor = getEditor();

await contains(":iframe .parent-target").click();
expect(editor.editable).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target b">b</div></div>`
);
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
expect(".options-container .o-checkbox").toHaveCount(0);

await contains("[data-class-action='my-custom-class']").click();
expect(editor.editable).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target b my-custom-class">b</div></div>`
);
expect("[data-class-action='my-custom-class']").toHaveClass("active");
expect(".options-container .o-checkbox").toHaveCount(1);
});
});
describe("dependencies", () => {
test("a button should not be visible if its dependency isn't (with undo)", async () => {
addOption({
Expand Down