@fleetbase/ember-ui 0.3.10 → 0.3.11
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/SCHEDULING_COMPONENTS.md +228 -0
- package/addon/components/availability-editor.js +81 -0
- package/addon/components/filter/multi-input.hbs +16 -0
- package/addon/components/filter/multi-input.js +68 -0
- package/addon/components/filter/range.hbs +52 -0
- package/addon/components/filter/range.js +74 -0
- package/addon/components/filters-picker.js +13 -6
- package/addon/components/layout/resource/tabular.hbs +2 -0
- package/addon/components/layout/resource/tabular.js +28 -0
- package/addon/components/modals/query-builder-computed-column-editor.hbs +80 -0
- package/addon/components/modals/query-builder-computed-column-editor.js +156 -0
- package/addon/components/query-builder/computed-columns.hbs +59 -0
- package/addon/components/query-builder/computed-columns.js +106 -0
- package/addon/components/query-builder.hbs +10 -0
- package/addon/components/query-builder.js +5 -0
- package/addon/components/report-builder.hbs +1 -0
- package/addon/components/schedule-calendar.hbs +37 -0
- package/addon/components/schedule-calendar.js +166 -0
- package/addon/components/schedule-item-card.hbs +39 -0
- package/addon/components/schedule-item-card.js +74 -0
- package/addon/components/table/cell/dropdown.hbs +2 -0
- package/addon/components/table/cell/dropdown.js +11 -0
- package/addon/components/table/cell/resizer.js +1 -1
- package/addon/components/table/cell.hbs +15 -2
- package/addon/components/table/cell.js +24 -0
- package/addon/components/table/foot.js +1 -1
- package/addon/components/table/td.hbs +2 -2
- package/addon/components/table/td.js +66 -1
- package/addon/components/table/th.hbs +15 -1
- package/addon/components/table/th.js +84 -0
- package/addon/components/table.hbs +2 -2
- package/addon/components/table.js +288 -0
- package/addon/styles/components/badge.css +8 -0
- package/addon/styles/components/input.css +8 -0
- package/addon/styles/components/table.css +126 -0
- package/addon/styles/layout/next.css +245 -28
- package/app/components/filter/multi-input.js +1 -0
- package/app/components/filter/range.js +1 -0
- package/app/components/modals/query-builder-computed-column-editor.js +1 -0
- package/app/components/query-builder/computed-columns.js +1 -0
- package/app/components/schedule-calendar.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Scheduling Components for Ember UI
|
|
2
|
+
|
|
3
|
+
This package provides reusable scheduling components for the Fleetbase platform.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
### ScheduleCalendar
|
|
8
|
+
|
|
9
|
+
A full-featured calendar component for displaying and managing schedules.
|
|
10
|
+
|
|
11
|
+
**Usage:**
|
|
12
|
+
```handlebars
|
|
13
|
+
<ScheduleCalendar
|
|
14
|
+
@resources={{this.drivers}}
|
|
15
|
+
@items={{this.scheduleItems}}
|
|
16
|
+
@view="resourceTimeline"
|
|
17
|
+
@onItemClick={{this.handleItemClick}}
|
|
18
|
+
@onItemDrop={{this.handleItemDrop}}
|
|
19
|
+
@onDateClick={{this.handleDateClick}}
|
|
20
|
+
>
|
|
21
|
+
<:item as |item|>
|
|
22
|
+
<div class="custom-event-content">
|
|
23
|
+
{{item.title}}
|
|
24
|
+
</div>
|
|
25
|
+
</:item>
|
|
26
|
+
</ScheduleCalendar>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Arguments:**
|
|
30
|
+
- `@resources` - Array of resources (e.g., drivers, vehicles)
|
|
31
|
+
- `@items` - Array of schedule items to display
|
|
32
|
+
- `@view` - Calendar view type (default: 'resourceTimeline')
|
|
33
|
+
- `@editable` - Enable drag-and-drop editing (default: true)
|
|
34
|
+
- `@onItemClick` - Callback when an item is clicked
|
|
35
|
+
- `@onItemDrop` - Callback when an item is dragged and dropped
|
|
36
|
+
- `@onDateClick` - Callback when a date is clicked
|
|
37
|
+
|
|
38
|
+
**Named Blocks:**
|
|
39
|
+
- `:item` - Custom rendering for schedule items
|
|
40
|
+
- `:header` - Custom header content
|
|
41
|
+
- `:footer` - Custom footer content
|
|
42
|
+
|
|
43
|
+
### ScheduleItemCard
|
|
44
|
+
|
|
45
|
+
Displays a schedule item in a card format.
|
|
46
|
+
|
|
47
|
+
**Usage:**
|
|
48
|
+
```handlebars
|
|
49
|
+
<ScheduleItemCard @item={{this.scheduleItem}} @onClick={{this.handleClick}}>
|
|
50
|
+
<:content as |ctx|>
|
|
51
|
+
<div class="custom-content">
|
|
52
|
+
{{ctx.item.title}}
|
|
53
|
+
</div>
|
|
54
|
+
</:content>
|
|
55
|
+
<:actions as |ctx|>
|
|
56
|
+
<button {{on "click" (fn this.edit ctx.item)}}>Edit</button>
|
|
57
|
+
<button {{on "click" (fn this.delete ctx.item)}}>Delete</button>
|
|
58
|
+
</:actions>
|
|
59
|
+
</ScheduleItemCard>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Arguments:**
|
|
63
|
+
- `@item` - The schedule item to display
|
|
64
|
+
- `@onClick` - Callback when the card is clicked
|
|
65
|
+
|
|
66
|
+
**Named Blocks:**
|
|
67
|
+
- `:content` - Custom content rendering
|
|
68
|
+
- `:actions` - Custom action buttons
|
|
69
|
+
|
|
70
|
+
### AvailabilityEditor
|
|
71
|
+
|
|
72
|
+
Allows users to set and manage availability windows.
|
|
73
|
+
|
|
74
|
+
**Usage:**
|
|
75
|
+
```handlebars
|
|
76
|
+
<AvailabilityEditor
|
|
77
|
+
@subjectType="driver"
|
|
78
|
+
@subjectUuid={{@driver.id}}
|
|
79
|
+
@onSave={{this.handleAvailabilitySave}}
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Arguments:**
|
|
84
|
+
- `@subjectType` - Type of the subject (e.g., 'driver', 'vehicle')
|
|
85
|
+
- `@subjectUuid` - UUID of the subject
|
|
86
|
+
- `@onSave` - Callback when availability is saved
|
|
87
|
+
|
|
88
|
+
## Models
|
|
89
|
+
|
|
90
|
+
### Schedule
|
|
91
|
+
|
|
92
|
+
Represents a master schedule.
|
|
93
|
+
|
|
94
|
+
**Attributes:**
|
|
95
|
+
- `name` - Schedule name
|
|
96
|
+
- `description` - Schedule description
|
|
97
|
+
- `start_date` - Start date
|
|
98
|
+
- `end_date` - End date
|
|
99
|
+
- `timezone` - Timezone
|
|
100
|
+
- `status` - Status (draft, published, active, paused, archived)
|
|
101
|
+
- `subject_uuid` - UUID of the subject
|
|
102
|
+
- `subject_type` - Type of the subject
|
|
103
|
+
|
|
104
|
+
**Relationships:**
|
|
105
|
+
- `items` - hasMany schedule-item
|
|
106
|
+
- `company` - belongsTo company
|
|
107
|
+
|
|
108
|
+
### ScheduleItem
|
|
109
|
+
|
|
110
|
+
Represents an individual scheduled item.
|
|
111
|
+
|
|
112
|
+
**Attributes:**
|
|
113
|
+
- `start_at` - Start datetime
|
|
114
|
+
- `end_at` - End datetime
|
|
115
|
+
- `duration` - Duration in minutes
|
|
116
|
+
- `status` - Status (pending, confirmed, in_progress, completed, cancelled, no_show)
|
|
117
|
+
- `assignee_uuid` - UUID of the assignee
|
|
118
|
+
- `assignee_type` - Type of the assignee
|
|
119
|
+
- `resource_uuid` - UUID of the resource
|
|
120
|
+
- `resource_type` - Type of the resource
|
|
121
|
+
|
|
122
|
+
**Relationships:**
|
|
123
|
+
- `schedule` - belongsTo schedule
|
|
124
|
+
|
|
125
|
+
### ScheduleTemplate
|
|
126
|
+
|
|
127
|
+
Represents a reusable schedule template.
|
|
128
|
+
|
|
129
|
+
**Attributes:**
|
|
130
|
+
- `name` - Template name
|
|
131
|
+
- `description` - Template description
|
|
132
|
+
- `start_time` - Start time
|
|
133
|
+
- `end_time` - End time
|
|
134
|
+
- `duration` - Duration in minutes
|
|
135
|
+
- `break_duration` - Break duration in minutes
|
|
136
|
+
- `rrule` - RFC 5545 recurrence rule
|
|
137
|
+
|
|
138
|
+
### ScheduleAvailability
|
|
139
|
+
|
|
140
|
+
Represents availability windows for resources.
|
|
141
|
+
|
|
142
|
+
**Attributes:**
|
|
143
|
+
- `subject_uuid` - UUID of the subject
|
|
144
|
+
- `subject_type` - Type of the subject
|
|
145
|
+
- `start_at` - Start datetime
|
|
146
|
+
- `end_at` - End datetime
|
|
147
|
+
- `is_available` - Availability flag
|
|
148
|
+
- `preference_level` - Preference strength (1-5)
|
|
149
|
+
- `reason` - Reason for unavailability
|
|
150
|
+
- `notes` - Additional notes
|
|
151
|
+
- `rrule` - RFC 5545 recurrence rule
|
|
152
|
+
|
|
153
|
+
### ScheduleConstraint
|
|
154
|
+
|
|
155
|
+
Represents scheduling constraints.
|
|
156
|
+
|
|
157
|
+
**Attributes:**
|
|
158
|
+
- `name` - Constraint name
|
|
159
|
+
- `description` - Constraint description
|
|
160
|
+
- `type` - Constraint type (hos, labor, business, capacity)
|
|
161
|
+
- `category` - Constraint category (compliance, optimization)
|
|
162
|
+
- `constraint_key` - Constraint key
|
|
163
|
+
- `constraint_value` - Constraint value
|
|
164
|
+
- `jurisdiction` - Jurisdiction (e.g., US-Federal, US-CA)
|
|
165
|
+
- `priority` - Priority (higher = more important)
|
|
166
|
+
- `is_active` - Active flag
|
|
167
|
+
|
|
168
|
+
## Service
|
|
169
|
+
|
|
170
|
+
### Scheduling Service
|
|
171
|
+
|
|
172
|
+
Provides methods for interacting with the scheduling API.
|
|
173
|
+
|
|
174
|
+
**Methods:**
|
|
175
|
+
|
|
176
|
+
- `loadSchedule(scheduleId)` - Load a schedule by ID
|
|
177
|
+
- `createSchedule(data)` - Create a new schedule
|
|
178
|
+
- `createScheduleItem(data)` - Create a new schedule item
|
|
179
|
+
- `updateScheduleItem(item, data)` - Update a schedule item
|
|
180
|
+
- `deleteScheduleItem(item)` - Delete a schedule item
|
|
181
|
+
- `getScheduleItemsForAssignee(assigneeType, assigneeUuid, filters)` - Get items for an assignee
|
|
182
|
+
- `checkAvailability(subjectType, subjectUuid, startAt, endAt)` - Check availability
|
|
183
|
+
- `setAvailability(data)` - Set availability
|
|
184
|
+
- `loadConstraints(subjectType, subjectUuid)` - Load constraints
|
|
185
|
+
- `validateScheduleItem(item)` - Validate an item against constraints
|
|
186
|
+
|
|
187
|
+
**Usage:**
|
|
188
|
+
```javascript
|
|
189
|
+
import { inject as service } from '@ember/service';
|
|
190
|
+
|
|
191
|
+
export default class MyComponent extends Component {
|
|
192
|
+
@service scheduling;
|
|
193
|
+
|
|
194
|
+
async loadDriverSchedule(driverId) {
|
|
195
|
+
const items = await this.scheduling.getScheduleItemsForAssignee.perform(
|
|
196
|
+
'driver',
|
|
197
|
+
driverId,
|
|
198
|
+
{ start_at: '2025-11-15', end_at: '2025-11-22' }
|
|
199
|
+
);
|
|
200
|
+
return items;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Styling
|
|
206
|
+
|
|
207
|
+
All components follow Fleetbase UI styling standards:
|
|
208
|
+
- Minimal padding and spacing
|
|
209
|
+
- Tailwind CSS framework
|
|
210
|
+
- Dark mode support
|
|
211
|
+
- Consistent with existing ember-ui components
|
|
212
|
+
|
|
213
|
+
## Dependencies
|
|
214
|
+
|
|
215
|
+
The ScheduleCalendar component requires FullCalendar to be installed:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pnpm add @fullcalendar/core @fullcalendar/resource-timeline @fullcalendar/interaction
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Future Enhancements
|
|
222
|
+
|
|
223
|
+
- TimeOffForm component for time-off requests
|
|
224
|
+
- ScheduleTemplateBuilder component for creating templates
|
|
225
|
+
- Conflict detection UI
|
|
226
|
+
- RRULE editor for recurring patterns
|
|
227
|
+
- Multi-timezone support improvements
|
|
228
|
+
- Real-time updates via WebSockets
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
import { inject as service } from '@ember/service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AvailabilityEditor Component
|
|
8
|
+
*
|
|
9
|
+
* Allows users to set and manage availability windows for resources.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <AvailabilityEditor
|
|
13
|
+
* @subjectType="driver"
|
|
14
|
+
* @subjectUuid={{@driver.id}}
|
|
15
|
+
* @onSave={{this.handleAvailabilitySave}}
|
|
16
|
+
* />
|
|
17
|
+
*/
|
|
18
|
+
export default class AvailabilityEditorComponent extends Component {
|
|
19
|
+
@service scheduling;
|
|
20
|
+
@service notifications;
|
|
21
|
+
|
|
22
|
+
@tracked startAt = null;
|
|
23
|
+
@tracked endAt = null;
|
|
24
|
+
@tracked isAvailable = true;
|
|
25
|
+
@tracked preferenceLevel = 3;
|
|
26
|
+
@tracked reason = '';
|
|
27
|
+
@tracked notes = '';
|
|
28
|
+
@tracked rrule = '';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save availability
|
|
32
|
+
*/
|
|
33
|
+
@action
|
|
34
|
+
async saveAvailability() {
|
|
35
|
+
try {
|
|
36
|
+
const data = {
|
|
37
|
+
subject_type: this.args.subjectType,
|
|
38
|
+
subject_uuid: this.args.subjectUuid,
|
|
39
|
+
start_at: this.startAt,
|
|
40
|
+
end_at: this.endAt,
|
|
41
|
+
is_available: this.isAvailable,
|
|
42
|
+
preference_level: this.preferenceLevel,
|
|
43
|
+
reason: this.reason,
|
|
44
|
+
notes: this.notes,
|
|
45
|
+
rrule: this.rrule,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const availability = await this.scheduling.setAvailability.perform(data);
|
|
49
|
+
|
|
50
|
+
if (this.args.onSave) {
|
|
51
|
+
this.args.onSave(availability);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.resetForm();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Failed to save availability:', error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reset form
|
|
62
|
+
*/
|
|
63
|
+
@action
|
|
64
|
+
resetForm() {
|
|
65
|
+
this.startAt = null;
|
|
66
|
+
this.endAt = null;
|
|
67
|
+
this.isAvailable = true;
|
|
68
|
+
this.preferenceLevel = 3;
|
|
69
|
+
this.reason = '';
|
|
70
|
+
this.notes = '';
|
|
71
|
+
this.rrule = '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update field
|
|
76
|
+
*/
|
|
77
|
+
@action
|
|
78
|
+
updateField(field, value) {
|
|
79
|
+
this[field] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="filter-multi-input" ...attributes>
|
|
2
|
+
<TagInput
|
|
3
|
+
class="form-input form-input-sm flex-1"
|
|
4
|
+
@placeholder={{or @placeholder "Add values..."}}
|
|
5
|
+
@allowSpacesInTags={{or @allowSpacesInTags true}}
|
|
6
|
+
@tags={{this.tags}}
|
|
7
|
+
@addTag={{this.addTag}}
|
|
8
|
+
@removeTagAtIndex={{this.removeTag}}
|
|
9
|
+
as |tag|
|
|
10
|
+
>
|
|
11
|
+
{{tag}}
|
|
12
|
+
</TagInput>
|
|
13
|
+
<button type="button" class="clear-button" disabled={{not this.tags.length}} alt="Clear" {{on "click" this.clear}}>
|
|
14
|
+
<FaIcon @icon="times" />
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
import { isArray } from '@ember/array';
|
|
5
|
+
|
|
6
|
+
export default class FilterMultiInputComponent extends Component {
|
|
7
|
+
@tracked tags = [];
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.tags = this.parseValue(this.args.value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
parseValue(value) {
|
|
15
|
+
if (isArray(value)) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof value === 'string' && value.includes(',')) {
|
|
20
|
+
return value
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((v) => v.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (value) {
|
|
27
|
+
return [value];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
buildValue() {
|
|
34
|
+
return this.tags.join(',');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@action addTag(tag) {
|
|
38
|
+
const { onChange, filter } = this.args;
|
|
39
|
+
|
|
40
|
+
this.tags.pushObject(tag);
|
|
41
|
+
const value = this.buildValue();
|
|
42
|
+
|
|
43
|
+
if (typeof onChange === 'function') {
|
|
44
|
+
onChange(filter, value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@action removeTag(index) {
|
|
49
|
+
const { onChange, filter } = this.args;
|
|
50
|
+
|
|
51
|
+
this.tags.removeAt(index);
|
|
52
|
+
const value = this.buildValue();
|
|
53
|
+
|
|
54
|
+
if (typeof onChange === 'function') {
|
|
55
|
+
onChange(filter, value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@action clear() {
|
|
60
|
+
const { onClear, filter } = this.args;
|
|
61
|
+
|
|
62
|
+
this.tags = [];
|
|
63
|
+
|
|
64
|
+
if (typeof onClear === 'function') {
|
|
65
|
+
onClear(filter);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<div class="filter-range" ...attributes>
|
|
2
|
+
<div class="filter-range-inputs">
|
|
3
|
+
<div class="filter-range-input-group">
|
|
4
|
+
<label class="filter-range-label">{{or @filter.minLabel "Min"}}</label>
|
|
5
|
+
<Input
|
|
6
|
+
@type="number"
|
|
7
|
+
@value={{this.minValue}}
|
|
8
|
+
min={{or @filter.min 0}}
|
|
9
|
+
max={{or @filter.max 100}}
|
|
10
|
+
step={{or @filter.step 1}}
|
|
11
|
+
{{on "input" this.onMinChange}}
|
|
12
|
+
class="form-input form-input-sm"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="filter-range-separator">-</div>
|
|
16
|
+
<div class="filter-range-input-group">
|
|
17
|
+
<label class="filter-range-label">{{or @filter.maxLabel "Max"}}</label>
|
|
18
|
+
<Input
|
|
19
|
+
@type="number"
|
|
20
|
+
@value={{this.maxValue}}
|
|
21
|
+
min={{or @filter.min 0}}
|
|
22
|
+
max={{or @filter.max 100}}
|
|
23
|
+
step={{or @filter.step 1}}
|
|
24
|
+
{{on "input" this.onMaxChange}}
|
|
25
|
+
class="form-input form-input-sm"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="filter-range-sliders">
|
|
30
|
+
<input
|
|
31
|
+
type="range"
|
|
32
|
+
value={{this.minValue}}
|
|
33
|
+
min={{or @filter.min 0}}
|
|
34
|
+
max={{or @filter.max 100}}
|
|
35
|
+
step={{or @filter.step 1}}
|
|
36
|
+
{{on "input" this.onMinChange}}
|
|
37
|
+
class="filter-range-slider filter-range-slider-min"
|
|
38
|
+
/>
|
|
39
|
+
<input
|
|
40
|
+
type="range"
|
|
41
|
+
value={{this.maxValue}}
|
|
42
|
+
min={{or @filter.min 0}}
|
|
43
|
+
max={{or @filter.max 100}}
|
|
44
|
+
step={{or @filter.step 1}}
|
|
45
|
+
{{on "input" this.onMaxChange}}
|
|
46
|
+
class="filter-range-slider filter-range-slider-max"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
<button type="button" class="clear-button" alt="Clear" {{on "click" this.clear}}>
|
|
50
|
+
<FaIcon @icon="times" />
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
|
|
5
|
+
export default class FilterRangeComponent extends Component {
|
|
6
|
+
@tracked minValue;
|
|
7
|
+
@tracked maxValue;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.parseValue(this.args.value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
parseValue(value) {
|
|
15
|
+
const { filter } = this.args;
|
|
16
|
+
const { min = 0, max = 100 } = filter;
|
|
17
|
+
|
|
18
|
+
if (typeof value === 'string' && value.includes(',')) {
|
|
19
|
+
const [minVal, maxVal] = value.split(',').map((v) => parseFloat(v.trim()));
|
|
20
|
+
this.minValue = isNaN(minVal) ? min : minVal;
|
|
21
|
+
this.maxValue = isNaN(maxVal) ? max : maxVal;
|
|
22
|
+
} else {
|
|
23
|
+
this.minValue = min;
|
|
24
|
+
this.maxValue = max;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
buildValue() {
|
|
29
|
+
return `${this.minValue},${this.maxValue}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@action onMinChange(event) {
|
|
33
|
+
const { onChange, filter } = this.args;
|
|
34
|
+
const value = parseFloat(event.target.value);
|
|
35
|
+
|
|
36
|
+
this.minValue = value;
|
|
37
|
+
|
|
38
|
+
// Ensure min doesn't exceed max
|
|
39
|
+
if (this.minValue > this.maxValue) {
|
|
40
|
+
this.maxValue = this.minValue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof onChange === 'function') {
|
|
44
|
+
onChange(filter, this.buildValue());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@action onMaxChange(event) {
|
|
49
|
+
const { onChange, filter } = this.args;
|
|
50
|
+
const value = parseFloat(event.target.value);
|
|
51
|
+
|
|
52
|
+
this.maxValue = value;
|
|
53
|
+
|
|
54
|
+
// Ensure max doesn't go below min
|
|
55
|
+
if (this.maxValue < this.minValue) {
|
|
56
|
+
this.minValue = this.maxValue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof onChange === 'function') {
|
|
60
|
+
onChange(filter, this.buildValue());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@action clear() {
|
|
65
|
+
const { onClear, filter } = this.args;
|
|
66
|
+
|
|
67
|
+
this.minValue = filter.min ?? 0;
|
|
68
|
+
this.maxValue = filter.max ?? 100;
|
|
69
|
+
|
|
70
|
+
if (typeof onClear === 'function') {
|
|
71
|
+
onClear(filter);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -91,25 +91,32 @@ export default class FiltersPickerComponent extends Component {
|
|
|
91
91
|
this.args.onClear(...args);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// Build a
|
|
94
|
+
// Build a qp bag that explicitly clears the filter params
|
|
95
95
|
const qp = { ...this.hostRouter.currentRoute.queryParams };
|
|
96
|
+
|
|
96
97
|
(this.args.columns ?? [])
|
|
97
98
|
.filter((c) => c.filterable)
|
|
98
99
|
.forEach((c) => {
|
|
99
100
|
const key = c.filterParam ?? c.valuePath;
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
|
|
102
|
+
// Explicitly clear them instead of deleting
|
|
103
|
+
if (key in qp) {
|
|
104
|
+
qp[key] = null; // will remove from URL if default is null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const arrayKey = `${key}[]`;
|
|
108
|
+
if (arrayKey in qp) {
|
|
109
|
+
qp[arrayKey] = null;
|
|
110
|
+
}
|
|
102
111
|
});
|
|
103
112
|
|
|
104
|
-
// Transition – routeDidChange listener will rebuild afterwards
|
|
105
113
|
try {
|
|
106
114
|
await this.hostRouter.transitionTo(this.hostRouter.currentRouteName, {
|
|
107
115
|
queryParams: qp,
|
|
108
116
|
});
|
|
109
117
|
} catch (error) {
|
|
110
|
-
// Ignore only the "transition aborted" case
|
|
111
118
|
if (error?.name !== 'TransitionAborted') {
|
|
112
|
-
throw error;
|
|
119
|
+
throw error;
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
122
|
}
|
|
@@ -133,6 +133,8 @@
|
|
|
133
133
|
@onPageChange={{@onPageChange}}
|
|
134
134
|
@tfootVerticalOffset={{@tfootVerticalOffset}}
|
|
135
135
|
@tfootVerticalOffsetElements={{@tfootVerticalOffsetElements}}
|
|
136
|
+
@onSort={{this.handleSort}}
|
|
137
|
+
@checkboxSticky={{this.checkboxSticky}}
|
|
136
138
|
/>
|
|
137
139
|
{{/if}}
|
|
138
140
|
</Layout::Section::Body>
|
|
@@ -2,12 +2,20 @@ import Component from '@glimmer/component';
|
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
4
|
import { action } from '@ember/object';
|
|
5
|
+
import { isNone } from '@ember/utils';
|
|
5
6
|
|
|
6
7
|
export default class LayoutResourceTabularComponent extends Component {
|
|
7
8
|
@service filters;
|
|
8
9
|
@tracked table;
|
|
9
10
|
@tracked columns = [];
|
|
10
11
|
|
|
12
|
+
get checkboxSticky() {
|
|
13
|
+
if (!isNone(this.args.checkboxSticky)) return this.args.checkboxSticky;
|
|
14
|
+
|
|
15
|
+
const columns = this.args.columns ?? this.columns;
|
|
16
|
+
return columns.some((c) => !isNone(c?.sticky));
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
constructor(owner, { columns = [] }) {
|
|
12
20
|
super(...arguments);
|
|
13
21
|
this.columns = columns;
|
|
@@ -19,4 +27,24 @@ export default class LayoutResourceTabularComponent extends Component {
|
|
|
19
27
|
this.args.setupTable(table);
|
|
20
28
|
}
|
|
21
29
|
}
|
|
30
|
+
|
|
31
|
+
@action handleSort(sortString, sortColumns) {
|
|
32
|
+
// sortString is comma-delimited format: "created_at,-order_date,status"
|
|
33
|
+
// sortColumns is array format: [{ param: 'created_at', direction: 'asc' }, ...]
|
|
34
|
+
|
|
35
|
+
if (this.args.controller && this.args.controller.sort !== undefined) {
|
|
36
|
+
this.args.controller.sort = sortString;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof this.args.onSort === 'function') {
|
|
40
|
+
this.args.onSort({
|
|
41
|
+
sortString,
|
|
42
|
+
sortColumns,
|
|
43
|
+
// Legacy support for single column callbacks
|
|
44
|
+
sortBy: sortColumns.length > 0 ? sortColumns[0].param : null,
|
|
45
|
+
sortDirection: sortColumns.length > 0 ? sortColumns[0].direction : null,
|
|
46
|
+
sortValue: sortString,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
22
50
|
}
|