@gisce/ooui 2.40.0-alpha.5 → 2.40.0-alpha.7
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/dist/Kanban.d.ts +9 -3
- package/dist/Kanban.d.ts.map +1 -1
- package/dist/KanbanCard.d.ts +13 -0
- package/dist/KanbanCard.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 +655 -604
- package/dist/ooui.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Kanban.ts +14 -7
- package/src/KanbanCard.ts +84 -0
- package/src/index.ts +2 -0
- package/src/spec/Kanban.spec.ts +8 -8
- package/src/spec/kanbanCard.spec.ts +217 -0
package/package.json
CHANGED
package/src/Kanban.ts
CHANGED
|
@@ -65,6 +65,15 @@ class Kanban {
|
|
|
65
65
|
return this._column_field;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Domain for filtering columns (for many2one fields)
|
|
70
|
+
* Example: "[('fold', '!=', True)]"
|
|
71
|
+
*/
|
|
72
|
+
_column_domain: string | null = null;
|
|
73
|
+
get column_domain(): string | null {
|
|
74
|
+
return this._column_domain;
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
/**
|
|
69
78
|
* Enable dragging cards between columns
|
|
70
79
|
*/
|
|
@@ -74,10 +83,10 @@ class Kanban {
|
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
/**
|
|
77
|
-
*
|
|
86
|
+
* Field name to use for sorting cards within columns
|
|
78
87
|
*/
|
|
79
|
-
_sort:
|
|
80
|
-
get sort():
|
|
88
|
+
_sort: string | undefined = undefined;
|
|
89
|
+
get sort(): string | undefined {
|
|
81
90
|
return this._sort;
|
|
82
91
|
}
|
|
83
92
|
|
|
@@ -134,15 +143,13 @@ class Kanban {
|
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
this._column_field = view.attributes.column_field || "state";
|
|
146
|
+
this._column_domain = view.attributes.column_domain || null;
|
|
137
147
|
|
|
138
148
|
this._drag =
|
|
139
149
|
view.attributes.drag !== undefined
|
|
140
150
|
? parseBoolAttribute(view.attributes.drag)
|
|
141
151
|
: true;
|
|
142
|
-
this._sort =
|
|
143
|
-
view.attributes.sort !== undefined
|
|
144
|
-
? parseBoolAttribute(view.attributes.sort)
|
|
145
|
-
: true;
|
|
152
|
+
this._sort = view.attributes.sort || undefined;
|
|
146
153
|
this._set_max_cards =
|
|
147
154
|
view.attributes.set_max_cards !== undefined
|
|
148
155
|
? parseBoolAttribute(view.attributes.set_max_cards)
|
|
@@ -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/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import Avatar from "./Avatar";
|
|
|
2
2
|
import Form from "./Form";
|
|
3
3
|
import Tree from "./Tree";
|
|
4
4
|
import Kanban from "./Kanban";
|
|
5
|
+
import KanbanCard from "./KanbanCard";
|
|
5
6
|
import Char from "./Char";
|
|
6
7
|
import Container from "./Container";
|
|
7
8
|
import ContainerWidget from "./ContainerWidget";
|
|
@@ -89,6 +90,7 @@ export {
|
|
|
89
90
|
Form,
|
|
90
91
|
Tree,
|
|
91
92
|
Kanban,
|
|
93
|
+
KanbanCard,
|
|
92
94
|
NewLine,
|
|
93
95
|
Boolean,
|
|
94
96
|
One2many,
|
package/src/spec/Kanban.spec.ts
CHANGED
|
@@ -4,7 +4,7 @@ import Field from "../Field";
|
|
|
4
4
|
import Button from "../Button";
|
|
5
5
|
|
|
6
6
|
const XML_VIEW_KANBAN = `<?xml version="1.0"?>
|
|
7
|
-
<kanban string="Tasks" column_field="state" drag="1" sort="
|
|
7
|
+
<kanban string="Tasks" column_field="state" drag="1" sort="sequence" set_max_cards="1" colors="blue:state=='draft';green:state=='done'">
|
|
8
8
|
<field name="name"/>
|
|
9
9
|
<field name="user_id" widget="avatar"/>
|
|
10
10
|
<field name="planned_hours" sum="Total hours" widget="float_time"/>
|
|
@@ -20,8 +20,8 @@ const XML_VIEW_KANBAN_MINIMAL = `<?xml version="1.0"?>
|
|
|
20
20
|
</kanban>
|
|
21
21
|
`;
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
<kanban column_field="state" drag="0"
|
|
23
|
+
const XML_VIEW_KANBAN_NO_DRAG = `<?xml version="1.0"?>
|
|
24
|
+
<kanban column_field="state" drag="0">
|
|
25
25
|
<field name="name"/>
|
|
26
26
|
<field name="priority"/>
|
|
27
27
|
</kanban>
|
|
@@ -97,7 +97,7 @@ describe("A Kanban", () => {
|
|
|
97
97
|
expect(tree.string).toBe("Tasks");
|
|
98
98
|
expect(tree.column_field).toBe("state");
|
|
99
99
|
expect(tree.drag).toBe(true);
|
|
100
|
-
expect(tree.sort).toBe(
|
|
100
|
+
expect(tree.sort).toBe("sequence");
|
|
101
101
|
expect(tree.set_max_cards).toBe(true);
|
|
102
102
|
expect(tree.colors).toBe("blue:state=='draft';green:state=='done'");
|
|
103
103
|
});
|
|
@@ -110,16 +110,16 @@ describe("A Kanban", () => {
|
|
|
110
110
|
expect(tree.string).toBe(null);
|
|
111
111
|
expect(tree.column_field).toBe("status");
|
|
112
112
|
expect(tree.drag).toBe(true); // Default value
|
|
113
|
-
expect(tree.sort).
|
|
113
|
+
expect(tree.sort).toBeUndefined(); // No sort field specified
|
|
114
114
|
expect(tree.set_max_cards).toBe(false); // Default value
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
it("should parse drag
|
|
117
|
+
it("should parse drag as false", () => {
|
|
118
118
|
const tree = new Kanban(FIELDS);
|
|
119
|
-
tree.parse(
|
|
119
|
+
tree.parse(XML_VIEW_KANBAN_NO_DRAG);
|
|
120
120
|
|
|
121
121
|
expect(tree.drag).toBe(false);
|
|
122
|
-
expect(tree.sort).
|
|
122
|
+
expect(tree.sort).toBeUndefined();
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
it("should fallback to 'state' when column_field is missing", () => {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { it, expect, describe } from "vitest";
|
|
2
|
+
import Kanban from "../Kanban";
|
|
3
|
+
import KanbanCard from "../KanbanCard";
|
|
4
|
+
import Button from "../Button";
|
|
5
|
+
import Field from "../Field";
|
|
6
|
+
|
|
7
|
+
const XML_VIEW_KANBAN = `<?xml version="1.0"?>
|
|
8
|
+
<kanban string="Tasks" column_field="state" drag="1">
|
|
9
|
+
<field name="name"/>
|
|
10
|
+
<field name="user_id" widget="avatar"/>
|
|
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
|
+
<button name="action_always" type="object" string="Always Visible"/>
|
|
15
|
+
</kanban>
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const FIELDS = {
|
|
19
|
+
name: {
|
|
20
|
+
required: true,
|
|
21
|
+
size: 128,
|
|
22
|
+
string: "Task Summary",
|
|
23
|
+
type: "char",
|
|
24
|
+
views: {},
|
|
25
|
+
},
|
|
26
|
+
user_id: {
|
|
27
|
+
context: "",
|
|
28
|
+
domain: [],
|
|
29
|
+
relation: "res.users",
|
|
30
|
+
size: 64,
|
|
31
|
+
string: "Assigned to",
|
|
32
|
+
type: "many2one",
|
|
33
|
+
views: {},
|
|
34
|
+
widget: "avatar",
|
|
35
|
+
},
|
|
36
|
+
state: {
|
|
37
|
+
required: true,
|
|
38
|
+
readonly: true,
|
|
39
|
+
selection: [
|
|
40
|
+
["draft", "Draft"],
|
|
41
|
+
["open", "In Progress"],
|
|
42
|
+
["pending", "Pending"],
|
|
43
|
+
["cancelled", "Cancelled"],
|
|
44
|
+
["done", "Done"],
|
|
45
|
+
],
|
|
46
|
+
string: "State",
|
|
47
|
+
type: "selection",
|
|
48
|
+
views: {},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
describe("A KanbanCard", () => {
|
|
53
|
+
it("should parse and create a Container", () => {
|
|
54
|
+
const kanbanDef = new Kanban(FIELDS);
|
|
55
|
+
kanbanDef.parse(XML_VIEW_KANBAN);
|
|
56
|
+
|
|
57
|
+
const record = {
|
|
58
|
+
id: 1,
|
|
59
|
+
name: "Test Task",
|
|
60
|
+
user_id: [1, "John Doe"],
|
|
61
|
+
state: "draft",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const container = new KanbanCard(kanbanDef).parse(record);
|
|
65
|
+
|
|
66
|
+
expect(container).toBeDefined();
|
|
67
|
+
expect(container.rows).toBeDefined();
|
|
68
|
+
expect(container.rows.length).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should show button when record state matches button states", () => {
|
|
72
|
+
const kanbanDef = new Kanban(FIELDS);
|
|
73
|
+
kanbanDef.parse(XML_VIEW_KANBAN);
|
|
74
|
+
|
|
75
|
+
const record = {
|
|
76
|
+
id: 1,
|
|
77
|
+
name: "Test Task",
|
|
78
|
+
state: "draft", // Matches states="draft" in do_open button
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const container = new KanbanCard(kanbanDef).parse(record);
|
|
82
|
+
|
|
83
|
+
// Find all buttons in the container
|
|
84
|
+
const buttons = container.rows.flat().filter((w) => w instanceof Button);
|
|
85
|
+
|
|
86
|
+
// Find the "do_open" button
|
|
87
|
+
const doOpenButton = buttons.find((b) => b.id === "do_open");
|
|
88
|
+
expect(doOpenButton).toBeDefined();
|
|
89
|
+
expect(doOpenButton!.invisible).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should hide button when record state doesn't match button states", () => {
|
|
93
|
+
const kanbanDef = new Kanban(FIELDS);
|
|
94
|
+
kanbanDef.parse(XML_VIEW_KANBAN);
|
|
95
|
+
|
|
96
|
+
const record = {
|
|
97
|
+
id: 1,
|
|
98
|
+
name: "Test Task",
|
|
99
|
+
state: "open", // Does NOT match states="draft" in do_open button
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const container = new KanbanCard(kanbanDef).parse(record);
|
|
103
|
+
|
|
104
|
+
// Find all buttons in the container
|
|
105
|
+
const buttons = container.rows.flat().filter((w) => w instanceof Button);
|
|
106
|
+
|
|
107
|
+
// Find the "do_open" button
|
|
108
|
+
const doOpenButton = buttons.find((b) => b.id === "do_open");
|
|
109
|
+
expect(doOpenButton).toBeDefined();
|
|
110
|
+
expect(doOpenButton!.invisible).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should always show button without states attribute", () => {
|
|
114
|
+
const kanbanDef = new Kanban(FIELDS);
|
|
115
|
+
kanbanDef.parse(XML_VIEW_KANBAN);
|
|
116
|
+
|
|
117
|
+
const record = {
|
|
118
|
+
id: 1,
|
|
119
|
+
name: "Test Task",
|
|
120
|
+
state: "done", // Any state
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const container = new KanbanCard(kanbanDef).parse(record);
|
|
124
|
+
|
|
125
|
+
// Find all buttons in the container
|
|
126
|
+
const buttons = container.rows.flat().filter((w) => w instanceof Button);
|
|
127
|
+
|
|
128
|
+
// Find the "action_always" button (has no states attribute)
|
|
129
|
+
const alwaysButton = buttons.find((b) => b.id === "action_always");
|
|
130
|
+
expect(alwaysButton).toBeDefined();
|
|
131
|
+
expect(alwaysButton!.invisible).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should show button when state matches one of multiple comma-separated states", () => {
|
|
135
|
+
const kanbanDef = new Kanban(FIELDS);
|
|
136
|
+
kanbanDef.parse(XML_VIEW_KANBAN);
|
|
137
|
+
|
|
138
|
+
const record = {
|
|
139
|
+
id: 1,
|
|
140
|
+
name: "Test Task",
|
|
141
|
+
state: "pending", // Matches one of states="open,pending" in do_done button
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const container = new KanbanCard(kanbanDef).parse(record);
|
|
145
|
+
|
|
146
|
+
const buttons = container.rows.flat().filter((w) => w instanceof Button);
|
|
147
|
+
const doDoneButton = buttons.find((b) => b.id === "do_done");
|
|
148
|
+
expect(doDoneButton).toBeDefined();
|
|
149
|
+
expect(doDoneButton!.invisible).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should add fields to container", () => {
|
|
153
|
+
const kanbanDef = new Kanban(FIELDS);
|
|
154
|
+
kanbanDef.parse(XML_VIEW_KANBAN);
|
|
155
|
+
|
|
156
|
+
const record = {
|
|
157
|
+
id: 1,
|
|
158
|
+
name: "Test Task",
|
|
159
|
+
user_id: [1, "John Doe"],
|
|
160
|
+
state: "draft",
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const container = new KanbanCard(kanbanDef).parse(record);
|
|
164
|
+
|
|
165
|
+
// Find all fields in the container
|
|
166
|
+
const fields = container.rows.flat().filter((w) => w instanceof Field);
|
|
167
|
+
|
|
168
|
+
expect(fields.length).toBeGreaterThan(0);
|
|
169
|
+
|
|
170
|
+
// Check that the name field exists
|
|
171
|
+
const nameField = fields.find((f) => f.id === "name");
|
|
172
|
+
expect(nameField).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should respect readOnly option", () => {
|
|
176
|
+
const kanbanDef = new Kanban(FIELDS);
|
|
177
|
+
kanbanDef.parse(XML_VIEW_KANBAN);
|
|
178
|
+
|
|
179
|
+
const record = {
|
|
180
|
+
id: 1,
|
|
181
|
+
name: "Test Task",
|
|
182
|
+
state: "draft",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const container = new KanbanCard(kanbanDef).parse(record, {
|
|
186
|
+
readOnly: true,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(container.readOnly).toBe(true);
|
|
190
|
+
|
|
191
|
+
// Check that buttons inherit readOnly
|
|
192
|
+
const buttons = container.rows.flat().filter((w) => w instanceof Button);
|
|
193
|
+
buttons.forEach((button) => {
|
|
194
|
+
expect(button.readOnly).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should handle record without state field gracefully", () => {
|
|
199
|
+
const kanbanDef = new Kanban(FIELDS);
|
|
200
|
+
kanbanDef.parse(XML_VIEW_KANBAN);
|
|
201
|
+
|
|
202
|
+
const record = {
|
|
203
|
+
id: 1,
|
|
204
|
+
name: "Test Task",
|
|
205
|
+
// state field is missing
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const container = new KanbanCard(kanbanDef).parse(record);
|
|
209
|
+
|
|
210
|
+
// Should not throw error and should create container
|
|
211
|
+
expect(container).toBeDefined();
|
|
212
|
+
|
|
213
|
+
// Buttons should handle undefined state gracefully
|
|
214
|
+
const buttons = container.rows.flat().filter((w) => w instanceof Button);
|
|
215
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
216
|
+
});
|
|
217
|
+
});
|