@gisce/ooui 2.42.0 → 2.43.0-alpha.2
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 +101 -0
- package/dist/Kanban.d.ts.map +1 -0
- package/dist/KanbanCard.d.ts +13 -0
- package/dist/KanbanCard.d.ts.map +1 -0
- package/dist/SearchFilter.d.ts.map +1 -1
- package/dist/WidgetFactory.d.ts.map +1 -1
- package/dist/helpers/onChangeParser.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/ooui.es.js +1634 -1420
- package/dist/ooui.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Kanban.ts +315 -0
- package/src/KanbanCard.ts +84 -0
- package/src/SearchFilter.ts +3 -0
- package/src/WidgetFactory.ts +1 -0
- package/src/helpers/onChangeParser.ts +8 -1
- package/src/index.ts +4 -0
- package/src/spec/Kanban.spec.ts +402 -0
- package/src/spec/kanbanCard.spec.ts +217 -0
package/package.json
CHANGED
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;
|
package/src/SearchFilter.ts
CHANGED
|
@@ -81,6 +81,9 @@ class SearchFilter {
|
|
|
81
81
|
const { type, widget } = fieldAttributes;
|
|
82
82
|
let widgetType = widget ?? type;
|
|
83
83
|
if (SearchFieldTypes[widgetType] === undefined) {
|
|
84
|
+
console.error(
|
|
85
|
+
`[SearchFilter] Field "${searchField}" has unsupported type "${type}"`,
|
|
86
|
+
);
|
|
84
87
|
widgetType = type;
|
|
85
88
|
}
|
|
86
89
|
return widgetFactory.createWidget(widgetType, fieldAttributes);
|
package/src/WidgetFactory.ts
CHANGED
|
@@ -3,7 +3,14 @@ const parseOnChange = (onChangeString: string) => {
|
|
|
3
3
|
|
|
4
4
|
const method = splitted[0];
|
|
5
5
|
const argsGross = splitted[1];
|
|
6
|
-
|
|
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,
|