@gisce/ooui 2.41.0 → 2.43.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gisce/ooui",
3
- "version": "2.41.0",
3
+ "version": "2.43.0-alpha.1",
4
4
  "engines": {
5
5
  "node": "20.5.0"
6
6
  },
@@ -23,6 +23,16 @@ export class GraphIndicator extends Graph {
23
23
  return this._showPercent;
24
24
  }
25
25
 
26
+ _progressbar: boolean = false;
27
+ get progressbar(): boolean {
28
+ return this._progressbar;
29
+ }
30
+
31
+ _showTotal: boolean = true;
32
+ get showTotal(): boolean {
33
+ return this._showTotal;
34
+ }
35
+
26
36
  _suffix: string | null = null;
27
37
  get suffix(): string | null {
28
38
  return this._suffix;
@@ -43,5 +53,10 @@ export class GraphIndicator extends Graph {
43
53
  this._suffix = element.attributes.suffix || null;
44
54
  this._totalDomain = replaceEntities(element.attributes.totalDomain) || null;
45
55
  this._showPercent = parseBoolAttribute(element.attributes.showPercent);
56
+ this._progressbar = parseBoolAttribute(element.attributes.progressbar);
57
+ this._showTotal =
58
+ element.attributes.showTotal !== undefined
59
+ ? parseBoolAttribute(element.attributes.showTotal)
60
+ : !!this._totalDomain;
46
61
  }
47
62
  }
package/src/Kanban.ts ADDED
@@ -0,0 +1,315 @@
1
+ import WidgetFactory from "./WidgetFactory";
2
+ import Widget from "./Widget";
3
+ import Button from "./Button";
4
+ import { replaceEntities } from "./helpers/attributeParser";
5
+ import { parseBoolAttribute, ParsedNode } from "./helpers/nodeParser";
6
+ import * as txml from "txml";
7
+ import { parseContext } from "./helpers/contextParser";
8
+ import { parseOnChange } from "./helpers/onChangeParser";
9
+
10
+ export type KanbanField = Widget & {
11
+ sum?: string; // Aggregation label (e.g., "Total hours")
12
+ };
13
+
14
+ export type KanbanButton = Button & {
15
+ states?: string; // Comma-separated states where button should show
16
+ };
17
+
18
+ class Kanban {
19
+ /**
20
+ * Object containing fields specification of the kanban.
21
+ */
22
+ _fields: any;
23
+ get fields() {
24
+ return this._fields;
25
+ }
26
+
27
+ /**
28
+ * Array of field widgets to display in cards
29
+ */
30
+ _card_fields: KanbanField[] = [];
31
+ get card_fields(): KanbanField[] {
32
+ return this._card_fields;
33
+ }
34
+
35
+ /**
36
+ * Array of button widgets to display in cards
37
+ */
38
+ _buttons: KanbanButton[] = [];
39
+ get buttons(): KanbanButton[] {
40
+ return this._buttons;
41
+ }
42
+
43
+ _string: string | null = null;
44
+ get string(): string | null {
45
+ return this._string;
46
+ }
47
+
48
+ _status: string | null = null;
49
+ get status(): string | null {
50
+ return this._status;
51
+ }
52
+
53
+ /**
54
+ * Widget type
55
+ */
56
+ _type: string = "kanban";
57
+ get type(): string {
58
+ return this._type;
59
+ }
60
+
61
+ /**
62
+ * Field that defines the columns (e.g., "state")
63
+ */
64
+ _column_field: string = "state";
65
+ get column_field(): string {
66
+ return this._column_field;
67
+ }
68
+
69
+ /**
70
+ * Domain for filtering columns (for many2one fields)
71
+ * Example: "[('fold', '!=', True)]"
72
+ */
73
+ _column_domain: string | null = null;
74
+ get column_domain(): string | null {
75
+ return this._column_domain;
76
+ }
77
+
78
+ /**
79
+ * Enable dragging cards between columns
80
+ */
81
+ _drag: boolean = true;
82
+ get drag(): boolean {
83
+ return this._drag;
84
+ }
85
+
86
+ /**
87
+ * Field name to use for sorting cards within columns
88
+ */
89
+ _sort: string | undefined = undefined;
90
+ get sort(): string | undefined {
91
+ return this._sort;
92
+ }
93
+
94
+ /**
95
+ * Enable setting max cards per column (WIP limits)
96
+ */
97
+ _set_max_cards: boolean = false;
98
+ get set_max_cards(): boolean {
99
+ return this._set_max_cards;
100
+ }
101
+
102
+ /**
103
+ * Color expression value (e.g., "blue:state=='draft';green:state=='done'")
104
+ */
105
+ _colors: string | null = null;
106
+ get colors(): string | null {
107
+ return this._colors;
108
+ }
109
+
110
+ /**
111
+ * Custom function to call when a card moves between columns
112
+ * Example: "handle_state_change" or with args "handle_state_change(field, from, to)"
113
+ * If not defined, the frontend should use the default on_change_column method
114
+ */
115
+ _on_change_column: { method: string; args: string[] } | null = null;
116
+ get on_change_column(): { method: string; args: string[] } | null {
117
+ return this._on_change_column;
118
+ }
119
+
120
+ /**
121
+ * Context for each field in the kanban
122
+ */
123
+ _contextForFields: Record<string, any> = {};
124
+ get contextForFields(): Record<string, any> {
125
+ return this._contextForFields;
126
+ }
127
+
128
+ set contextForFields(value: Record<string, any>) {
129
+ this._contextForFields = value;
130
+ }
131
+
132
+ /**
133
+ * Map of fields that have sum aggregation
134
+ * Key: field name, Value: sum label (e.g., "Total hours")
135
+ */
136
+ _aggregations: Record<string, string> = {};
137
+ get aggregations(): Record<string, string> {
138
+ return this._aggregations;
139
+ }
140
+
141
+ constructor(fields: Object) {
142
+ this._fields = fields;
143
+ }
144
+
145
+ parse(xml: string) {
146
+ const view = txml
147
+ .parse(xml)
148
+ .filter((el: ParsedNode) => el.tagName === "kanban")[0];
149
+
150
+ // Parse kanban attributes
151
+ this._string = view.attributes.string || null;
152
+ if (this._string) {
153
+ this._string = replaceEntities(this._string);
154
+ }
155
+
156
+ this._column_field = view.attributes.column_field || "state";
157
+ this._column_domain = view.attributes.column_domain || null;
158
+
159
+ this._drag =
160
+ view.attributes.drag !== undefined
161
+ ? parseBoolAttribute(view.attributes.drag)
162
+ : true;
163
+ this._sort = view.attributes.sort || undefined;
164
+ this._set_max_cards =
165
+ view.attributes.set_max_cards !== undefined
166
+ ? parseBoolAttribute(view.attributes.set_max_cards)
167
+ : false;
168
+
169
+ this._colors = view.attributes.colors || null;
170
+ if (this._colors) {
171
+ this._colors = replaceEntities(this._colors);
172
+ }
173
+
174
+ this._status = view.attributes.status || null;
175
+ if (this._status) {
176
+ this._status = replaceEntities(this._status);
177
+ }
178
+
179
+ // Parse on_change_column attribute
180
+ if (view.attributes.on_change_column) {
181
+ this._on_change_column = parseOnChange(view.attributes.on_change_column);
182
+ }
183
+
184
+ const widgetFactory = new WidgetFactory();
185
+
186
+ // Parse children (fields and buttons)
187
+ view.children.forEach((element: ParsedNode) => {
188
+ const { tagName, attributes } = element;
189
+
190
+ if (tagName === "field") {
191
+ this._parseField(element, attributes, widgetFactory);
192
+ } else if (tagName === "button") {
193
+ this._parseButton(element, attributes);
194
+ }
195
+ });
196
+ }
197
+
198
+ private _parseField(
199
+ _element: ParsedNode,
200
+ attributes: any,
201
+ widgetFactory: WidgetFactory,
202
+ ) {
203
+ const { name, widget, sum } = attributes;
204
+
205
+ if (!name) {
206
+ return;
207
+ }
208
+
209
+ if (!this._fields[name]) {
210
+ throw new Error(`Field ${name} doesn't exist in fields definition`);
211
+ }
212
+
213
+ const fieldDef = this._fields[name];
214
+ let widgetType = fieldDef.type;
215
+
216
+ // Handle domain override
217
+ if (
218
+ ((Array.isArray(fieldDef?.domain) && fieldDef?.domain.length === 0) ||
219
+ fieldDef?.domain === false) &&
220
+ attributes.domain &&
221
+ attributes.domain.length > 0
222
+ ) {
223
+ delete fieldDef.domain;
224
+ }
225
+
226
+ // Parse context
227
+ const widgetContext = parseContext({
228
+ context: attributes.context || fieldDef.context,
229
+ values: {},
230
+ fields: this._fields,
231
+ });
232
+
233
+ const mergedAttrs = {
234
+ ...fieldDef,
235
+ ...attributes,
236
+ fieldsWidgetType: fieldDef?.type,
237
+ context: widgetContext,
238
+ };
239
+
240
+ this._contextForFields[name] = widgetContext;
241
+
242
+ // Override widget type if specified
243
+ if (widget) {
244
+ widgetType = widget;
245
+ }
246
+
247
+ // Create the widget
248
+ if (!mergedAttrs.invisible) {
249
+ const fieldWidget = widgetFactory.createWidget(
250
+ widgetType,
251
+ mergedAttrs,
252
+ ) as KanbanField;
253
+
254
+ // Handle aggregation (sum)
255
+ if (sum) {
256
+ fieldWidget.sum = replaceEntities(sum);
257
+ this._aggregations[name] = replaceEntities(sum);
258
+ }
259
+
260
+ this._card_fields.push(fieldWidget);
261
+ }
262
+ }
263
+
264
+ private _parseButton(_element: ParsedNode, attributes: any) {
265
+ const { name, type, string, states } = attributes;
266
+
267
+ if (!name) {
268
+ return;
269
+ }
270
+
271
+ const buttonProps = {
272
+ ...attributes,
273
+ name,
274
+ buttonType: type || "object",
275
+ string: string || "",
276
+ };
277
+
278
+ const button = new Button(buttonProps) as KanbanButton;
279
+
280
+ // Parse states attribute (e.g., "draft,open")
281
+ if (states) {
282
+ button.states = states;
283
+ }
284
+
285
+ this._buttons.push(button);
286
+ }
287
+
288
+ /**
289
+ * Find the widgets matching with param id
290
+ * @param {string} id id to find
291
+ */
292
+ findById(id: string): Widget | null {
293
+ const foundField = this._card_fields.find((item) => {
294
+ if (item.findById) {
295
+ return item.findById(id);
296
+ }
297
+ return false;
298
+ });
299
+
300
+ if (foundField) {
301
+ return foundField;
302
+ }
303
+
304
+ const foundButton = this._buttons.find((item) => {
305
+ if (item.findById) {
306
+ return item.findById(id);
307
+ }
308
+ return false;
309
+ });
310
+
311
+ return foundButton || null;
312
+ }
313
+ }
314
+
315
+ export default Kanban;
@@ -0,0 +1,84 @@
1
+ import WidgetFactory from "./WidgetFactory";
2
+ import Container from "./Container";
3
+ import Kanban from "./Kanban";
4
+ import { evaluateButtonStates } from "./helpers/stateParser";
5
+ import { evaluateAttributes } from "./helpers/attributeParser";
6
+ import Button from "./Button";
7
+
8
+ export type KanbanCardParseOptions = {
9
+ readOnly?: boolean;
10
+ };
11
+
12
+ class KanbanCard {
13
+ _kanbanDef: Kanban;
14
+ get kanbanDef(): Kanban {
15
+ return this._kanbanDef;
16
+ }
17
+
18
+ constructor(kanbanDef: Kanban) {
19
+ this._kanbanDef = kanbanDef;
20
+ }
21
+
22
+ parse(record: any, options?: KanbanCardParseOptions): Container {
23
+ const readOnly = options?.readOnly ?? false;
24
+ const container = new Container(1, 12, readOnly);
25
+ const widgetFactory = new WidgetFactory();
26
+ let keyIdx = 0;
27
+
28
+ this._kanbanDef.card_fields.forEach((field: any) => {
29
+ keyIdx++;
30
+
31
+ const evaluatedAttrs = evaluateAttributes({
32
+ tagAttributes: field.raw_props || {},
33
+ values: record,
34
+ fields: this._kanbanDef.fields,
35
+ widgetType: field.type || "field",
36
+ });
37
+
38
+ // Extract properties from the existing field widget
39
+ const widgetProps = {
40
+ ...(field.raw_props || {}),
41
+ ...evaluatedAttrs,
42
+ key: `field_${keyIdx}`,
43
+ };
44
+
45
+ if (readOnly) {
46
+ widgetProps.readonly = true;
47
+ }
48
+
49
+ // Determine widget type from the field
50
+ const widgetType = field.type || "field";
51
+ const widget = widgetFactory.createWidget(widgetType, widgetProps);
52
+ container.addWidget(widget, { addLabel: !field.nolabel });
53
+ });
54
+
55
+ // Add buttons to container
56
+ this._kanbanDef.buttons.forEach((button: any) => {
57
+ keyIdx++;
58
+
59
+ const evaluatedStates = button.states
60
+ ? evaluateButtonStates({
61
+ states: button.states,
62
+ values: record,
63
+ })
64
+ : {};
65
+
66
+ const buttonProps = {
67
+ ...(button.raw_props || {}),
68
+ ...evaluatedStates,
69
+ key: `button_${keyIdx}`,
70
+ };
71
+
72
+ if (readOnly) {
73
+ buttonProps.readonly = true;
74
+ }
75
+
76
+ const buttonWidget = new Button(buttonProps);
77
+ container.addWidget(buttonWidget, { addLabel: false });
78
+ });
79
+
80
+ return container;
81
+ }
82
+ }
83
+
84
+ export default KanbanCard;
@@ -90,6 +90,7 @@ class WidgetFactory {
90
90
  this._widgetClass = Selection;
91
91
  break;
92
92
  case "many2one":
93
+ case "many2one_lazy":
93
94
  this._widgetClass = Many2one;
94
95
  break;
95
96
  case "boolean":
@@ -3,7 +3,14 @@ const parseOnChange = (onChangeString: string) => {
3
3
 
4
4
  const method = splitted[0];
5
5
  const argsGross = splitted[1];
6
- const argsSplitted = argsGross.split(",").map((arg) => arg.trim());
6
+
7
+ // Handle case where there are no parentheses (no arguments)
8
+ const argsSplitted = argsGross
9
+ ? argsGross
10
+ .split(",")
11
+ .map((arg) => arg.trim())
12
+ .filter((arg) => arg.length > 0)
13
+ : [];
7
14
 
8
15
  return {
9
16
  method,
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import Avatar from "./Avatar";
2
2
  import Form from "./Form";
3
3
  import Tree from "./Tree";
4
+ import Kanban from "./Kanban";
5
+ import KanbanCard from "./KanbanCard";
4
6
  import Char from "./Char";
5
7
  import Container from "./Container";
6
8
  import ContainerWidget from "./ContainerWidget";
@@ -87,6 +89,8 @@ export {
87
89
  Widget,
88
90
  Form,
89
91
  Tree,
92
+ Kanban,
93
+ KanbanCard,
90
94
  NewLine,
91
95
  Boolean,
92
96
  One2many,
@@ -55,6 +55,66 @@ describe("A Graph", () => {
55
55
  expect(graph.field).toBe("potencia");
56
56
  expect(graph.operator).toBe("+");
57
57
  });
58
+ it("should parse progressbar, showPercent and showTotal attributes", () => {
59
+ const xml1 = `<?xml version="1.0"?>
60
+ <graph string="My indicator" type="indicator" progressbar="1" />
61
+ `;
62
+
63
+ const graph1 = parseGraph(xml1) as GraphIndicator;
64
+ expect(graph1.progressbar).toBe(true);
65
+ expect(graph1.showPercent).toBe(false);
66
+ expect(graph1.showTotal).toBe(false); // No totalDomain, should default to false
67
+
68
+ const xml2 = `<?xml version="1.0"?>
69
+ <graph string="My indicator" type="indicator" showPercent="1" />
70
+ `;
71
+
72
+ const graph2 = parseGraph(xml2) as GraphIndicator;
73
+ expect(graph2.showPercent).toBe(true);
74
+ expect(graph2.progressbar).toBe(false);
75
+ expect(graph2.showTotal).toBe(false); // No totalDomain, should default to false
76
+
77
+ const xml3 = `<?xml version="1.0"?>
78
+ <graph string="My indicator" type="indicator" />
79
+ `;
80
+
81
+ const graph3 = parseGraph(xml3) as GraphIndicator;
82
+ expect(graph3.showPercent).toBe(false);
83
+ expect(graph3.progressbar).toBe(false);
84
+ expect(graph3.showTotal).toBe(false); // No totalDomain, should default to false
85
+
86
+ const xml4 = `<?xml version="1.0"?>
87
+ <graph string="My indicator" type="indicator" showTotal="0" />
88
+ `;
89
+
90
+ const graph4 = parseGraph(xml4) as GraphIndicator;
91
+ expect(graph4.showTotal).toBe(false);
92
+ });
93
+ it("should set showTotal to true when totalDomain is defined", () => {
94
+ const xml1 = `<?xml version="1.0"?>
95
+ <graph string="My indicator" type="indicator" totalDomain="[('state','=','open')]" />
96
+ `;
97
+
98
+ const graph1 = parseGraph(xml1) as GraphIndicator;
99
+ expect(graph1.totalDomain).toBe("[('state','=','open')]");
100
+ expect(graph1.showTotal).toBe(true); // totalDomain is defined, should default to true
101
+
102
+ const xml2 = `<?xml version="1.0"?>
103
+ <graph string="My indicator" type="indicator" totalDomain="[('state','=','open')]" showTotal="0" />
104
+ `;
105
+
106
+ const graph2 = parseGraph(xml2) as GraphIndicator;
107
+ expect(graph2.totalDomain).toBe("[('state','=','open')]");
108
+ expect(graph2.showTotal).toBe(false); // Explicitly set to false
109
+
110
+ const xml3 = `<?xml version="1.0"?>
111
+ <graph string="My indicator" type="indicator" totalDomain="[('state','=','open')]" showTotal="1" />
112
+ `;
113
+
114
+ const graph3 = parseGraph(xml3) as GraphIndicator;
115
+ expect(graph3.totalDomain).toBe("[('state','=','open')]");
116
+ expect(graph3.showTotal).toBe(true); // Explicitly set to true
117
+ });
58
118
  it("should parse a graph with timerange parameter", () => {
59
119
  const xml = `<?xml version="1.0"?>
60
120
  <graph type="line" timerange="day">