Skip to content

Commit e543bec

Browse files
agau-odoobso-odoo
authored andcommitted
add basic BuilderList
1 parent ab7cce6 commit e543bec

File tree

5 files changed

+459
-0
lines changed

5 files changed

+459
-0
lines changed

addons/html_builder/static/src/core/builder_component_plugin.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BuilderList } from "@html_builder/core/building_blocks/builder_list";
12
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
23
import { BuilderButtonGroup } from "./building_blocks/builder_button_group";
34
import { Dropdown } from "@web/core/dropdown/dropdown";
@@ -45,6 +46,7 @@ export class BuilderComponentPlugin extends Plugin {
4546
ModelMany2Many,
4647
BuilderDateTimePicker,
4748
BuilderUrlPicker,
49+
BuilderList,
4850
Img,
4951
},
5052
};
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { BuilderComponent } from "@html_builder/core/building_blocks/builder_component";
2+
import {
3+
basicContainerBuilderComponentProps,
4+
useBuilderComponent,
5+
useInputBuilderComponent,
6+
} from "@html_builder/core/utils";
7+
import { Component, useRef } from "@odoo/owl";
8+
import { _t } from "@web/core/l10n/translation";
9+
import { useSortable } from "@web/core/utils/sortable_owl";
10+
11+
export class BuilderList extends Component {
12+
static template = "html_builder.BuilderList";
13+
static props = {
14+
...basicContainerBuilderComponentProps,
15+
id: { type: String, optional: true },
16+
addItemTitle: { type: String, optional: true },
17+
itemShape: {
18+
type: Object,
19+
values: [{ value: "number" }, { value: "text" }],
20+
validate: (value) =>
21+
// is not empty object and doesn't include reserved fields
22+
Object.keys(value).length > 0 && !Object.keys(value).includes("_id"),
23+
optional: true,
24+
},
25+
default: { optional: true },
26+
sortable: { optional: true },
27+
hiddenProperties: { type: Array, optional: true },
28+
};
29+
static defaultProps = {
30+
addItemTitle: _t("Add"),
31+
itemShape: { value: "text" },
32+
default: { value: _t("Item") },
33+
sortable: true,
34+
hiddenProperties: [],
35+
};
36+
static components = { BuilderComponent };
37+
38+
setup() {
39+
this.validateProps();
40+
41+
useBuilderComponent();
42+
const { state, commit, preview } = useInputBuilderComponent({
43+
id: this.props.id,
44+
defaultValue: this.parseDisplayValue([this.makeDefaultItem()]),
45+
parseDisplayValue: this.parseDisplayValue,
46+
formatRawValue: this.formatRawValue,
47+
});
48+
this.state = state;
49+
this.commit = commit;
50+
this.preview = preview;
51+
52+
if (this.props.sortable) {
53+
useSortable({
54+
enable: () => this.props.sortable,
55+
ref: useRef("table"),
56+
elements: ".o_row_draggable",
57+
handle: ".o_handle_cell",
58+
cursor: "grabbing",
59+
placeholderClasses: ["d-table-row"],
60+
onDrop: (params) => {
61+
const { element, previous } = params;
62+
this.reorderItem(element.dataset.id, previous?.dataset.id);
63+
},
64+
});
65+
}
66+
}
67+
68+
validateProps() {
69+
// keys match
70+
const itemShapeKeys = Object.keys(this.props.itemShape);
71+
const defaultKeys = Object.keys(this.props.default);
72+
const allKeys = new Set([...itemShapeKeys, ...defaultKeys]);
73+
if (allKeys.size !== itemShapeKeys.length) {
74+
throw new Error("default properties don't match itemShape");
75+
}
76+
}
77+
78+
parseDisplayValue(displayValue) {
79+
return JSON.stringify(displayValue);
80+
}
81+
82+
formatRawValue(rawValue) {
83+
const items = rawValue ? JSON.parse(rawValue) : [];
84+
for (const item of items) {
85+
if (!("_id" in item)) {
86+
item._id = this.getNextAvailableItemId(items);
87+
}
88+
}
89+
return items;
90+
}
91+
92+
addItem() {
93+
const items = this.formatRawValue(this.state.value);
94+
items.push(this.makeDefaultItem());
95+
this.commit(items);
96+
}
97+
98+
deleteItem(e) {
99+
const itemId = e.target.dataset.id;
100+
const items = this.formatRawValue(this.state.value);
101+
this.commit(items.filter((item) => item._id !== itemId));
102+
}
103+
104+
reorderItem(itemId, previousId) {
105+
let items = this.formatRawValue(this.state.value);
106+
const itemToReorder = items.find((item) => item._id === itemId);
107+
items = items.filter((item) => item._id !== itemId);
108+
109+
const previousItem = items.find((item) => item._id === previousId);
110+
const previousItems = items.slice(0, items.indexOf(previousItem) + 1);
111+
112+
const nextItems = items.slice(items.indexOf(previousItem) + 1, items.length);
113+
114+
const newItems = [...previousItems, itemToReorder, ...nextItems];
115+
this.commit(newItems);
116+
}
117+
118+
makeDefaultItem() {
119+
return {
120+
...this.props.default,
121+
_id: this.getNextAvailableItemId(),
122+
};
123+
}
124+
125+
getNextAvailableItemId(items) {
126+
items = items || this.formatRawValue(this.state?.value);
127+
const biggestId = items
128+
.map((item) => parseInt(item._id))
129+
.reduce((acc, id) => (id > acc ? id : acc), -1);
130+
const nextAvailableId = biggestId + 1;
131+
return nextAvailableId.toString();
132+
}
133+
134+
onInput(e) {
135+
this.handleValueChange(e.target, false);
136+
}
137+
138+
onChange(e) {
139+
this.handleValueChange(e.target, true);
140+
}
141+
142+
handleValueChange(targetInputEl, commitToHistory) {
143+
const id = targetInputEl.dataset.id;
144+
const propertyName = targetInputEl.name;
145+
const value = targetInputEl.value;
146+
147+
const items = this.formatRawValue(this.state.value);
148+
const item = items.find((item) => item._id === id);
149+
item[propertyName] = value;
150+
151+
if (commitToHistory) {
152+
this.commit(items);
153+
} else {
154+
this.preview(items);
155+
}
156+
}
157+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<template xml:space="preserve">
3+
4+
<t t-name="html_builder.BuilderList">
5+
<BuilderComponent>
6+
<t t-if="state.value?.length > 2">
7+
<div class="o_we_table_wrapper">
8+
<table t-ref="table">
9+
<t t-foreach="formatRawValue(state.value)" t-as="item" t-key="item._id">
10+
<tr class="o_row_draggable" t-att-data-id="item._id">
11+
<td t-if="props.sortable" class="o_handle_cell">
12+
<button type="button" class="btn fa fa-fw fa-arrows"/>
13+
</td>
14+
<t t-foreach="Object.entries(props.itemShape).filter(([key,_]) => !props.hiddenProperties.includes(key))" t-as="entry" t-key="entry[0]">
15+
<td>
16+
<input
17+
t-att-type="entry[1]"
18+
t-att-name="entry[0]"
19+
t-att-value="item[entry[0]]"
20+
t-att-data-id="item._id"
21+
t-on-input="onInput"
22+
t-on-change="onChange"
23+
/>
24+
</td>
25+
</t>
26+
<td>
27+
<button type="button" class="btn o_we_text_danger builder_list_remove_item fa fa-fw fa-minus"
28+
t-on-click="deleteItem"
29+
t-att-data-id="item._id"/>
30+
</td>
31+
</tr>
32+
</t>
33+
</table>
34+
</div>
35+
</t>
36+
<button type="button" class="btn builder_list_add_item"
37+
t-on-click="addItem"><t t-out="props.addItemTitle"/></button>
38+
</BuilderComponent>
39+
</t>
40+
41+
</template>

addons/html_builder/static/src/sidebar/option_container.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
&>img {
1616
padding-bottom: 2px;
1717
}
18+
19+
&.o_we_text_danger {
20+
color: $o-we-color-danger;
21+
}
1822
}
1923

2024
.btn-primary {
@@ -48,6 +52,7 @@
4852
width: 2em;
4953
}
5054

55+
@include o-input-number-no-arrows();
5156
input {
5257
border: $o-we-sidebar-content-field-border-width solid $o-we-sidebar-content-field-border-color;
5358
border-radius: $o-we-sidebar-content-field-border-radius;
@@ -59,6 +64,12 @@
5964
border-color: $o-we-sidebar-content-field-input-border-color;
6065
}
6166
}
67+
68+
.o_we_table_wrapper {
69+
width: 100%;
70+
max-height: 200px;
71+
overflow-y: auto;
72+
}
6273
}
6374

6475
.o_we_img_animate:hover img {

0 commit comments

Comments
 (0)