@gisce/ooui 2.39.0 → 2.40.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/README.md +2 -0
- package/dist/Kanban.d.ts +80 -0
- package/dist/Kanban.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/ooui.es.js +1167 -1018
- package/dist/ooui.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Kanban.ts +285 -0
- package/src/index.ts +2 -0
- package/src/spec/Kanban.spec.ts +364 -0
package/package.json
CHANGED
package/src/Kanban.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
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
|
+
|
|
9
|
+
export type KanbanField = Widget & {
|
|
10
|
+
sum?: string; // Aggregation label (e.g., "Total hours")
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type KanbanButton = Button & {
|
|
14
|
+
states?: string; // Comma-separated states where button should show
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class Kanban {
|
|
18
|
+
/**
|
|
19
|
+
* Object containing fields specification of the kanban.
|
|
20
|
+
*/
|
|
21
|
+
_fields: any;
|
|
22
|
+
get fields() {
|
|
23
|
+
return this._fields;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Array of field widgets to display in cards
|
|
28
|
+
*/
|
|
29
|
+
_card_fields: KanbanField[] = [];
|
|
30
|
+
get card_fields(): KanbanField[] {
|
|
31
|
+
return this._card_fields;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Array of button widgets to display in cards
|
|
36
|
+
*/
|
|
37
|
+
_buttons: KanbanButton[] = [];
|
|
38
|
+
get buttons(): KanbanButton[] {
|
|
39
|
+
return this._buttons;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_string: string | null = null;
|
|
43
|
+
get string(): string | null {
|
|
44
|
+
return this._string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Widget type
|
|
49
|
+
*/
|
|
50
|
+
_type: string = "kanban";
|
|
51
|
+
get type(): string {
|
|
52
|
+
return this._type;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Field that defines the columns (e.g., "state")
|
|
57
|
+
*/
|
|
58
|
+
_column_field: string | null = null;
|
|
59
|
+
get column_field(): string | null {
|
|
60
|
+
return this._column_field;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Enable dragging cards between columns
|
|
65
|
+
*/
|
|
66
|
+
_drag: boolean = true;
|
|
67
|
+
get drag(): boolean {
|
|
68
|
+
return this._drag;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Enable sorting cards within columns
|
|
73
|
+
*/
|
|
74
|
+
_sort: boolean = true;
|
|
75
|
+
get sort(): boolean {
|
|
76
|
+
return this._sort;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enable setting max cards per column (WIP limits)
|
|
81
|
+
*/
|
|
82
|
+
_set_max_cards: boolean = false;
|
|
83
|
+
get set_max_cards(): boolean {
|
|
84
|
+
return this._set_max_cards;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Color expression value (e.g., "blue:state=='draft';green:state=='done'")
|
|
89
|
+
*/
|
|
90
|
+
_colors: string | null = null;
|
|
91
|
+
get colors(): string | null {
|
|
92
|
+
return this._colors;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Context for each field in the kanban
|
|
97
|
+
*/
|
|
98
|
+
_contextForFields: Record<string, any> = {};
|
|
99
|
+
get contextForFields(): Record<string, any> {
|
|
100
|
+
return this._contextForFields;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
set contextForFields(value: Record<string, any>) {
|
|
104
|
+
this._contextForFields = value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Map of fields that have sum aggregation
|
|
109
|
+
* Key: field name, Value: sum label (e.g., "Total hours")
|
|
110
|
+
*/
|
|
111
|
+
_aggregations: Record<string, string> = {};
|
|
112
|
+
get aggregations(): Record<string, string> {
|
|
113
|
+
return this._aggregations;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
constructor(fields: Object) {
|
|
117
|
+
this._fields = fields;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
parse(xml: string) {
|
|
121
|
+
const view = txml
|
|
122
|
+
.parse(xml)
|
|
123
|
+
.filter((el: ParsedNode) => el.tagName === "kanban")[0];
|
|
124
|
+
|
|
125
|
+
// Parse kanban attributes
|
|
126
|
+
this._string = view.attributes.string || null;
|
|
127
|
+
if (this._string) {
|
|
128
|
+
this._string = replaceEntities(this._string);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this._column_field = view.attributes.column_field || null;
|
|
132
|
+
if (!this._column_field) {
|
|
133
|
+
throw new Error("Kanban view must have a column_field attribute");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._drag =
|
|
137
|
+
view.attributes.drag !== undefined
|
|
138
|
+
? parseBoolAttribute(view.attributes.drag)
|
|
139
|
+
: true;
|
|
140
|
+
this._sort =
|
|
141
|
+
view.attributes.sort !== undefined
|
|
142
|
+
? parseBoolAttribute(view.attributes.sort)
|
|
143
|
+
: true;
|
|
144
|
+
this._set_max_cards =
|
|
145
|
+
view.attributes.set_max_cards !== undefined
|
|
146
|
+
? parseBoolAttribute(view.attributes.set_max_cards)
|
|
147
|
+
: false;
|
|
148
|
+
|
|
149
|
+
this._colors = view.attributes.colors || null;
|
|
150
|
+
if (this._colors) {
|
|
151
|
+
this._colors = replaceEntities(this._colors);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const widgetFactory = new WidgetFactory();
|
|
155
|
+
|
|
156
|
+
// Parse children (fields and buttons)
|
|
157
|
+
view.children.forEach((element: ParsedNode) => {
|
|
158
|
+
const { tagName, attributes } = element;
|
|
159
|
+
|
|
160
|
+
if (tagName === "field") {
|
|
161
|
+
this._parseField(element, attributes, widgetFactory);
|
|
162
|
+
} else if (tagName === "button") {
|
|
163
|
+
this._parseButton(element, attributes);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private _parseField(
|
|
169
|
+
_element: ParsedNode,
|
|
170
|
+
attributes: any,
|
|
171
|
+
widgetFactory: WidgetFactory,
|
|
172
|
+
) {
|
|
173
|
+
const { name, widget, sum } = attributes;
|
|
174
|
+
|
|
175
|
+
if (!name) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!this._fields[name]) {
|
|
180
|
+
throw new Error(`Field ${name} doesn't exist in fields definition`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const fieldDef = this._fields[name];
|
|
184
|
+
let widgetType = fieldDef.type;
|
|
185
|
+
|
|
186
|
+
// Handle domain override
|
|
187
|
+
if (
|
|
188
|
+
((Array.isArray(fieldDef?.domain) && fieldDef?.domain.length === 0) ||
|
|
189
|
+
fieldDef?.domain === false) &&
|
|
190
|
+
attributes.domain &&
|
|
191
|
+
attributes.domain.length > 0
|
|
192
|
+
) {
|
|
193
|
+
delete fieldDef.domain;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Parse context
|
|
197
|
+
const widgetContext = parseContext({
|
|
198
|
+
context: attributes.context || fieldDef.context,
|
|
199
|
+
values: {},
|
|
200
|
+
fields: this._fields,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const mergedAttrs = {
|
|
204
|
+
...fieldDef,
|
|
205
|
+
...attributes,
|
|
206
|
+
fieldsWidgetType: fieldDef?.type,
|
|
207
|
+
context: widgetContext,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
this._contextForFields[name] = widgetContext;
|
|
211
|
+
|
|
212
|
+
// Override widget type if specified
|
|
213
|
+
if (widget) {
|
|
214
|
+
widgetType = widget;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Create the widget
|
|
218
|
+
if (!mergedAttrs.invisible) {
|
|
219
|
+
const fieldWidget = widgetFactory.createWidget(
|
|
220
|
+
widgetType,
|
|
221
|
+
mergedAttrs,
|
|
222
|
+
) as KanbanField;
|
|
223
|
+
|
|
224
|
+
// Handle aggregation (sum)
|
|
225
|
+
if (sum) {
|
|
226
|
+
fieldWidget.sum = replaceEntities(sum);
|
|
227
|
+
this._aggregations[name] = replaceEntities(sum);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this._card_fields.push(fieldWidget);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private _parseButton(_element: ParsedNode, attributes: any) {
|
|
235
|
+
const { name, type, string, states } = attributes;
|
|
236
|
+
|
|
237
|
+
if (!name) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const buttonProps = {
|
|
242
|
+
...attributes,
|
|
243
|
+
name,
|
|
244
|
+
buttonType: type || "object",
|
|
245
|
+
string: string || "",
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const button = new Button(buttonProps) as KanbanButton;
|
|
249
|
+
|
|
250
|
+
// Parse states attribute (e.g., "draft,open")
|
|
251
|
+
if (states) {
|
|
252
|
+
button.states = states;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this._buttons.push(button);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Find the widgets matching with param id
|
|
260
|
+
* @param {string} id id to find
|
|
261
|
+
*/
|
|
262
|
+
findById(id: string): Widget | null {
|
|
263
|
+
const foundField = this._card_fields.find((item) => {
|
|
264
|
+
if (item.findById) {
|
|
265
|
+
return item.findById(id);
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (foundField) {
|
|
271
|
+
return foundField;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const foundButton = this._buttons.find((item) => {
|
|
275
|
+
if (item.findById) {
|
|
276
|
+
return item.findById(id);
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return foundButton || null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export default Kanban;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Avatar from "./Avatar";
|
|
2
2
|
import Form from "./Form";
|
|
3
3
|
import Tree from "./Tree";
|
|
4
|
+
import Kanban from "./Kanban";
|
|
4
5
|
import Char from "./Char";
|
|
5
6
|
import Container from "./Container";
|
|
6
7
|
import ContainerWidget from "./ContainerWidget";
|
|
@@ -86,6 +87,7 @@ export {
|
|
|
86
87
|
Widget,
|
|
87
88
|
Form,
|
|
88
89
|
Tree,
|
|
90
|
+
Kanban,
|
|
89
91
|
NewLine,
|
|
90
92
|
Boolean,
|
|
91
93
|
One2many,
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { it, expect, describe } from "vitest";
|
|
2
|
+
import Kanban from "../Kanban";
|
|
3
|
+
import Field from "../Field";
|
|
4
|
+
import Button from "../Button";
|
|
5
|
+
|
|
6
|
+
const XML_VIEW_KANBAN = `<?xml version="1.0"?>
|
|
7
|
+
<kanban string="Tasks" column_field="state" drag="1" sort="1" set_max_cards="1" colors="blue:state=='draft';green:state=='done'">
|
|
8
|
+
<field name="name"/>
|
|
9
|
+
<field name="user_id" widget="avatar"/>
|
|
10
|
+
<field name="planned_hours" sum="Total hours" widget="float_time"/>
|
|
11
|
+
<field name="state"/>
|
|
12
|
+
<button name="do_open" type="object" string="Start" states="draft"/>
|
|
13
|
+
<button name="do_done" type="object" string="Complete" states="open,pending"/>
|
|
14
|
+
</kanban>
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const XML_VIEW_KANBAN_MINIMAL = `<?xml version="1.0"?>
|
|
18
|
+
<kanban column_field="status">
|
|
19
|
+
<field name="name"/>
|
|
20
|
+
</kanban>
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const XML_VIEW_KANBAN_NO_DRAG_SORT = `<?xml version="1.0"?>
|
|
24
|
+
<kanban column_field="state" drag="0" sort="0">
|
|
25
|
+
<field name="name"/>
|
|
26
|
+
<field name="priority"/>
|
|
27
|
+
</kanban>
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const FIELDS = {
|
|
31
|
+
name: {
|
|
32
|
+
required: true,
|
|
33
|
+
size: 128,
|
|
34
|
+
string: "Task Summary",
|
|
35
|
+
type: "char",
|
|
36
|
+
views: {},
|
|
37
|
+
},
|
|
38
|
+
user_id: {
|
|
39
|
+
context: "",
|
|
40
|
+
domain: [],
|
|
41
|
+
relation: "res.users",
|
|
42
|
+
size: 64,
|
|
43
|
+
string: "Assigned to",
|
|
44
|
+
type: "many2one",
|
|
45
|
+
views: {},
|
|
46
|
+
widget: "avatar",
|
|
47
|
+
},
|
|
48
|
+
planned_hours: {
|
|
49
|
+
help: "Estimated time to complete the task",
|
|
50
|
+
string: "Planned Hours",
|
|
51
|
+
type: "float",
|
|
52
|
+
views: {},
|
|
53
|
+
widget: "float_time",
|
|
54
|
+
},
|
|
55
|
+
state: {
|
|
56
|
+
required: true,
|
|
57
|
+
readonly: true,
|
|
58
|
+
selection: [
|
|
59
|
+
["draft", "Draft"],
|
|
60
|
+
["open", "In Progress"],
|
|
61
|
+
["pending", "Pending"],
|
|
62
|
+
["cancelled", "Cancelled"],
|
|
63
|
+
["done", "Done"],
|
|
64
|
+
],
|
|
65
|
+
string: "State",
|
|
66
|
+
type: "selection",
|
|
67
|
+
views: {},
|
|
68
|
+
},
|
|
69
|
+
status: {
|
|
70
|
+
selection: [
|
|
71
|
+
["new", "New"],
|
|
72
|
+
["active", "Active"],
|
|
73
|
+
["completed", "Completed"],
|
|
74
|
+
],
|
|
75
|
+
string: "Status",
|
|
76
|
+
type: "selection",
|
|
77
|
+
views: {},
|
|
78
|
+
},
|
|
79
|
+
priority: {
|
|
80
|
+
selection: [
|
|
81
|
+
["0", "Low"],
|
|
82
|
+
["1", "Normal"],
|
|
83
|
+
["2", "High"],
|
|
84
|
+
],
|
|
85
|
+
string: "Priority",
|
|
86
|
+
type: "selection",
|
|
87
|
+
views: {},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe("A Kanban", () => {
|
|
92
|
+
it("should parse xml with all attributes", () => {
|
|
93
|
+
const tree = new Kanban(FIELDS);
|
|
94
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
95
|
+
|
|
96
|
+
expect(tree.type).toBe("kanban");
|
|
97
|
+
expect(tree.string).toBe("Tasks");
|
|
98
|
+
expect(tree.column_field).toBe("state");
|
|
99
|
+
expect(tree.drag).toBe(true);
|
|
100
|
+
expect(tree.sort).toBe(true);
|
|
101
|
+
expect(tree.set_max_cards).toBe(true);
|
|
102
|
+
expect(tree.colors).toBe("blue:state=='draft';green:state=='done'");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should parse minimal kanban xml", () => {
|
|
106
|
+
const tree = new Kanban(FIELDS);
|
|
107
|
+
tree.parse(XML_VIEW_KANBAN_MINIMAL);
|
|
108
|
+
|
|
109
|
+
expect(tree.type).toBe("kanban");
|
|
110
|
+
expect(tree.string).toBe(null);
|
|
111
|
+
expect(tree.column_field).toBe("status");
|
|
112
|
+
expect(tree.drag).toBe(true); // Default value
|
|
113
|
+
expect(tree.sort).toBe(true); // Default value
|
|
114
|
+
expect(tree.set_max_cards).toBe(false); // Default value
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should parse drag and sort as false", () => {
|
|
118
|
+
const tree = new Kanban(FIELDS);
|
|
119
|
+
tree.parse(XML_VIEW_KANBAN_NO_DRAG_SORT);
|
|
120
|
+
|
|
121
|
+
expect(tree.drag).toBe(false);
|
|
122
|
+
expect(tree.sort).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should throw error if column_field is missing", () => {
|
|
126
|
+
const tree = new Kanban(FIELDS);
|
|
127
|
+
const invalidXml = `<?xml version="1.0"?>
|
|
128
|
+
<kanban string="Invalid">
|
|
129
|
+
<field name="name"/>
|
|
130
|
+
</kanban>
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
expect(() => tree.parse(invalidXml)).toThrow(
|
|
134
|
+
"Kanban view must have a column_field attribute",
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should parse fields correctly", () => {
|
|
139
|
+
const tree = new Kanban(FIELDS);
|
|
140
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
141
|
+
|
|
142
|
+
expect(tree.card_fields).toHaveLength(4);
|
|
143
|
+
expect(tree.card_fields[0].id).toBe("name");
|
|
144
|
+
expect(tree.card_fields[1].id).toBe("user_id");
|
|
145
|
+
expect(tree.card_fields[2].id).toBe("planned_hours");
|
|
146
|
+
expect(tree.card_fields[3].id).toBe("state");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should parse field with sum aggregation", () => {
|
|
150
|
+
const tree = new Kanban(FIELDS);
|
|
151
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
152
|
+
|
|
153
|
+
const hoursField = tree.card_fields.find((f) => f.id === "planned_hours");
|
|
154
|
+
expect(hoursField).toBeDefined();
|
|
155
|
+
expect((hoursField as any).sum).toBe("Total hours");
|
|
156
|
+
|
|
157
|
+
expect(tree.aggregations).toHaveProperty("planned_hours");
|
|
158
|
+
expect(tree.aggregations.planned_hours).toBe("Total hours");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should preserve field widget attribute", () => {
|
|
162
|
+
const tree = new Kanban(FIELDS);
|
|
163
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
164
|
+
|
|
165
|
+
const avatarField = tree.card_fields.find((f) => f.id === "user_id");
|
|
166
|
+
expect(avatarField).toBeDefined();
|
|
167
|
+
expect((avatarField as any).raw_props?.widget).toBe("avatar");
|
|
168
|
+
|
|
169
|
+
const hoursField = tree.card_fields.find((f) => f.id === "planned_hours");
|
|
170
|
+
expect(hoursField).toBeDefined();
|
|
171
|
+
expect((hoursField as any).raw_props?.widget).toBe("float_time");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should parse buttons correctly", () => {
|
|
175
|
+
const tree = new Kanban(FIELDS);
|
|
176
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
177
|
+
|
|
178
|
+
expect(tree.buttons).toHaveLength(2);
|
|
179
|
+
|
|
180
|
+
const startButton = tree.buttons[0];
|
|
181
|
+
expect(startButton.id).toBe("do_open");
|
|
182
|
+
expect(startButton.buttonType).toBe("object");
|
|
183
|
+
expect(startButton.caption).toBe("Start");
|
|
184
|
+
expect((startButton as any).states).toBe("draft");
|
|
185
|
+
|
|
186
|
+
const completeButton = tree.buttons[1];
|
|
187
|
+
expect(completeButton.id).toBe("do_done");
|
|
188
|
+
expect(completeButton.buttonType).toBe("object");
|
|
189
|
+
expect(completeButton.caption).toBe("Complete");
|
|
190
|
+
expect((completeButton as any).states).toBe("open,pending");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should handle button without states attribute", () => {
|
|
194
|
+
const xmlWithButton = `<?xml version="1.0"?>
|
|
195
|
+
<kanban column_field="state">
|
|
196
|
+
<field name="name"/>
|
|
197
|
+
<button name="action_test" type="object" string="Test"/>
|
|
198
|
+
</kanban>
|
|
199
|
+
`;
|
|
200
|
+
const tree = new Kanban(FIELDS);
|
|
201
|
+
tree.parse(xmlWithButton);
|
|
202
|
+
|
|
203
|
+
expect(tree.buttons).toHaveLength(1);
|
|
204
|
+
const button = tree.buttons[0];
|
|
205
|
+
expect(button.id).toBe("action_test");
|
|
206
|
+
expect((button as any).states).toBeUndefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should merge field attributes correctly", () => {
|
|
210
|
+
const tree = new Kanban(FIELDS);
|
|
211
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
212
|
+
|
|
213
|
+
const nameField = tree.card_fields.find((f) => f.id === "name");
|
|
214
|
+
expect(nameField).toBeDefined();
|
|
215
|
+
expect(nameField?.type).toBe("char");
|
|
216
|
+
expect((nameField as any).required).toBe(true);
|
|
217
|
+
expect((nameField as any).size).toBe(128);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should handle context for fields", () => {
|
|
221
|
+
const xmlWithContext = `<?xml version="1.0"?>
|
|
222
|
+
<kanban column_field="state">
|
|
223
|
+
<field name="user_id" context="{'show_all': True}"/>
|
|
224
|
+
</kanban>
|
|
225
|
+
`;
|
|
226
|
+
const tree = new Kanban(FIELDS);
|
|
227
|
+
tree.parse(xmlWithContext);
|
|
228
|
+
|
|
229
|
+
expect(tree.contextForFields).toHaveProperty("user_id");
|
|
230
|
+
expect(tree.contextForFields.user_id).toBeDefined();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should throw error for non-existent field", () => {
|
|
234
|
+
const xmlWithInvalidField = `<?xml version="1.0"?>
|
|
235
|
+
<kanban column_field="state">
|
|
236
|
+
<field name="nonexistent_field"/>
|
|
237
|
+
</kanban>
|
|
238
|
+
`;
|
|
239
|
+
const tree = new Kanban(FIELDS);
|
|
240
|
+
|
|
241
|
+
expect(() => tree.parse(xmlWithInvalidField)).toThrow(
|
|
242
|
+
"Field nonexistent_field doesn't exist in fields definition",
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should find field by id", () => {
|
|
247
|
+
const tree = new Kanban(FIELDS);
|
|
248
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
249
|
+
|
|
250
|
+
const nameField = tree.findById("name");
|
|
251
|
+
expect(nameField).toBeDefined();
|
|
252
|
+
expect(nameField?.id).toBe("name");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should find button by id", () => {
|
|
256
|
+
const tree = new Kanban(FIELDS);
|
|
257
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
258
|
+
|
|
259
|
+
const button = tree.findById("do_open");
|
|
260
|
+
expect(button).toBeDefined();
|
|
261
|
+
expect(button?.id).toBe("do_open");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should return null for non-existent id", () => {
|
|
265
|
+
const tree = new Kanban(FIELDS);
|
|
266
|
+
tree.parse(XML_VIEW_KANBAN);
|
|
267
|
+
|
|
268
|
+
const result = tree.findById("nonexistent");
|
|
269
|
+
expect(result).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should handle empty kanban (only column_field)", () => {
|
|
273
|
+
const emptyXml = `<?xml version="1.0"?>
|
|
274
|
+
<kanban column_field="state"></kanban>
|
|
275
|
+
`;
|
|
276
|
+
const tree = new Kanban(FIELDS);
|
|
277
|
+
tree.parse(emptyXml);
|
|
278
|
+
|
|
279
|
+
expect(tree.card_fields).toHaveLength(0);
|
|
280
|
+
expect(tree.buttons).toHaveLength(0);
|
|
281
|
+
expect(tree.column_field).toBe("state");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should handle HTML entities in string attribute", () => {
|
|
285
|
+
const xmlWithEntities = `<?xml version="1.0"?>
|
|
286
|
+
<kanban string="Tasks & Projects" column_field="state">
|
|
287
|
+
<field name="name"/>
|
|
288
|
+
</kanban>
|
|
289
|
+
`;
|
|
290
|
+
const tree = new Kanban(FIELDS);
|
|
291
|
+
tree.parse(xmlWithEntities);
|
|
292
|
+
|
|
293
|
+
expect(tree.string).toBe("Tasks & Projects");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should handle HTML entities in colors attribute", () => {
|
|
297
|
+
const xmlWithEntities = `<?xml version="1.0"?>
|
|
298
|
+
<kanban column_field="state" colors="red:priority>1;blue:priority<1">
|
|
299
|
+
<field name="name"/>
|
|
300
|
+
<field name="priority"/>
|
|
301
|
+
</kanban>
|
|
302
|
+
`;
|
|
303
|
+
const tree = new Kanban(FIELDS);
|
|
304
|
+
tree.parse(xmlWithEntities);
|
|
305
|
+
|
|
306
|
+
expect(tree.colors).toBe("red:priority>1;blue:priority<1");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should skip invisible fields", () => {
|
|
310
|
+
const xmlWithInvisible = `<?xml version="1.0"?>
|
|
311
|
+
<kanban column_field="state">
|
|
312
|
+
<field name="name"/>
|
|
313
|
+
<field name="priority" invisible="1"/>
|
|
314
|
+
</kanban>
|
|
315
|
+
`;
|
|
316
|
+
const tree = new Kanban(FIELDS);
|
|
317
|
+
tree.parse(xmlWithInvisible);
|
|
318
|
+
|
|
319
|
+
expect(tree.card_fields).toHaveLength(1);
|
|
320
|
+
expect(tree.card_fields[0].id).toBe("name");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("should handle buttons without name (skip them)", () => {
|
|
324
|
+
const xmlWithUnnamedButton = `<?xml version="1.0"?>
|
|
325
|
+
<kanban column_field="state">
|
|
326
|
+
<field name="name"/>
|
|
327
|
+
<button type="object" string="Test"/>
|
|
328
|
+
</kanban>
|
|
329
|
+
`;
|
|
330
|
+
const tree = new Kanban(FIELDS);
|
|
331
|
+
tree.parse(xmlWithUnnamedButton);
|
|
332
|
+
|
|
333
|
+
expect(tree.buttons).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should handle fields without name (skip them)", () => {
|
|
337
|
+
const xmlWithUnnamedField = `<?xml version="1.0"?>
|
|
338
|
+
<kanban column_field="state">
|
|
339
|
+
<field name="name"/>
|
|
340
|
+
<field widget="custom"/>
|
|
341
|
+
</kanban>
|
|
342
|
+
`;
|
|
343
|
+
const tree = new Kanban(FIELDS);
|
|
344
|
+
tree.parse(xmlWithUnnamedField);
|
|
345
|
+
|
|
346
|
+
expect(tree.card_fields).toHaveLength(1);
|
|
347
|
+
expect(tree.card_fields[0].id).toBe("name");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should store multiple aggregations", () => {
|
|
351
|
+
const xmlWithMultipleAggs = `<?xml version="1.0"?>
|
|
352
|
+
<kanban column_field="state">
|
|
353
|
+
<field name="planned_hours" sum="Total Planned"/>
|
|
354
|
+
<field name="priority" sum="Total Priority"/>
|
|
355
|
+
</kanban>
|
|
356
|
+
`;
|
|
357
|
+
const tree = new Kanban(FIELDS);
|
|
358
|
+
tree.parse(xmlWithMultipleAggs);
|
|
359
|
+
|
|
360
|
+
expect(Object.keys(tree.aggregations)).toHaveLength(2);
|
|
361
|
+
expect(tree.aggregations.planned_hours).toBe("Total Planned");
|
|
362
|
+
expect(tree.aggregations.priority).toBe("Total Priority");
|
|
363
|
+
});
|
|
364
|
+
});
|