@fleetbase/ember-ui 0.3.10 → 0.3.12

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.
Files changed (76) hide show
  1. package/SCHEDULING_COMPONENTS.md +228 -0
  2. package/addon/components/availability-editor.js +81 -0
  3. package/addon/components/chart.js +10 -10
  4. package/addon/components/click-to-reveal.hbs +1 -1
  5. package/addon/components/click-to-reveal.js +5 -23
  6. package/addon/components/dashboard/create.hbs +5 -3
  7. package/addon/components/dashboard/widget-panel.hbs +17 -7
  8. package/addon/components/dashboard/widget-panel.js +61 -13
  9. package/addon/components/dashboard.js +0 -1
  10. package/addon/components/filter/multi-input.hbs +16 -0
  11. package/addon/components/filter/multi-input.js +68 -0
  12. package/addon/components/filter/range.hbs +52 -0
  13. package/addon/components/filter/range.js +74 -0
  14. package/addon/components/filters-picker.js +13 -6
  15. package/addon/components/layout/resource/tabular.hbs +2 -0
  16. package/addon/components/layout/resource/tabular.js +28 -0
  17. package/addon/components/layout/sidebar/item.hbs +2 -2
  18. package/addon/components/lazy-engine-component.hbs +8 -0
  19. package/addon/components/lazy-engine-component.js +136 -0
  20. package/addon/components/load-engine.hbs +1 -0
  21. package/addon/components/load-engine.js +82 -0
  22. package/addon/components/modals/query-builder-computed-column-editor.hbs +80 -0
  23. package/addon/components/modals/query-builder-computed-column-editor.js +251 -0
  24. package/addon/components/pill.js +0 -1
  25. package/addon/components/query-builder/computed-columns.hbs +59 -0
  26. package/addon/components/query-builder/computed-columns.js +104 -0
  27. package/addon/components/query-builder.hbs +10 -0
  28. package/addon/components/query-builder.js +5 -0
  29. package/addon/components/registry-yield.hbs +7 -1
  30. package/addon/components/registry-yield.js +107 -15
  31. package/addon/components/report-builder.hbs +1 -0
  32. package/addon/components/report-builder.js +2 -1
  33. package/addon/components/schedule-calendar.hbs +37 -0
  34. package/addon/components/schedule-calendar.js +166 -0
  35. package/addon/components/schedule-item-card.hbs +39 -0
  36. package/addon/components/schedule-item-card.js +74 -0
  37. package/addon/components/tab-navigation.hbs +78 -38
  38. package/addon/components/tab-navigation.js +22 -9
  39. package/addon/components/table/cell/dropdown.hbs +2 -0
  40. package/addon/components/table/cell/dropdown.js +11 -0
  41. package/addon/components/table/cell/resizer.js +1 -1
  42. package/addon/components/table/cell.hbs +15 -2
  43. package/addon/components/table/cell.js +24 -0
  44. package/addon/components/table/foot.js +1 -1
  45. package/addon/components/table/td.hbs +2 -2
  46. package/addon/components/table/td.js +66 -1
  47. package/addon/components/table/th.hbs +15 -1
  48. package/addon/components/table/th.js +84 -0
  49. package/addon/components/table.hbs +22 -15
  50. package/addon/components/table.js +288 -0
  51. package/addon/helpers/avatar-url.js +9 -0
  52. package/addon/helpers/lazy-engine-component.js +116 -0
  53. package/addon/helpers/point-coordinates.js +10 -0
  54. package/addon/helpers/point-to-coordinates.js +32 -0
  55. package/addon/helpers/resolve-component.js +18 -0
  56. package/addon/helpers/set-object-kv.js +9 -0
  57. package/addon/helpers/unwrap-coordinates.js +249 -0
  58. package/addon/services/dashboard.js +39 -17
  59. package/addon/styles/components/badge.css +8 -0
  60. package/addon/styles/components/input.css +8 -0
  61. package/addon/styles/components/table.css +183 -0
  62. package/addon/styles/layout/next.css +245 -28
  63. package/app/components/filter/multi-input.js +1 -0
  64. package/app/components/filter/range.js +1 -0
  65. package/app/components/lazy-engine-component.js +1 -0
  66. package/app/components/load-engine.js +1 -0
  67. package/app/components/modals/query-builder-computed-column-editor.js +1 -0
  68. package/app/components/query-builder/computed-columns.js +1 -0
  69. package/app/components/schedule-calendar.js +1 -0
  70. package/app/helpers/avatar-url.js +1 -0
  71. package/app/helpers/lazy-engine-component.js +1 -0
  72. package/app/helpers/point-coordinates.js +1 -0
  73. package/app/helpers/point-to-coordinates.js +1 -0
  74. package/app/helpers/set-object-kv.js +1 -0
  75. package/app/helpers/unwrap-coordinates.js +1 -0
  76. 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
+ }
@@ -1,6 +1,7 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { action } from '@ember/object';
4
+ import { debug } from '@ember/debug';
4
5
  import Chart, { _adapters } from 'chart.js/auto';
5
6
  import {
6
7
  parse,
@@ -46,13 +47,7 @@ import {
46
47
 
47
48
  export default class ChartComponent extends Component {
48
49
  @tracked chart;
49
- @tracked isLoading;
50
-
51
- constructor() {
52
- super(...arguments);
53
-
54
- this.isLoading = this.args.isLoading === true;
55
- }
50
+ @tracked isLoading = this.args.isLoading;
56
51
 
57
52
  @action async renderChart(ctx) {
58
53
  const options = {
@@ -65,9 +60,14 @@ export default class ChartComponent extends Component {
65
60
  };
66
61
 
67
62
  if (typeof options.data.datasets === 'function') {
68
- this.isLoading = true;
69
- options.data.datasets = await options.data.datasets();
70
- this.isLoading = false;
63
+ try {
64
+ this.isLoading = true;
65
+ options.data.datasets = await options.data.datasets();
66
+ } catch (err) {
67
+ debug('Error loading Chart dataset: ' + err.message);
68
+ } finally {
69
+ this.isLoading = false;
70
+ }
71
71
  }
72
72
 
73
73
  this.useDateFns();
@@ -1,4 +1,4 @@
1
- <div class="relative click-to-reveal {{if this.isVisible 'is-visible' 'is-blurred'}}" {{on "click" (fn this.copy @value)}} ...attributes>
1
+ <div class="relative click-to-reveal {{this.wrapperClass}} {{if this.isVisible 'is-visible' 'is-blurred'}}" {{on "click" (fn this.copy @value)}} ...attributes>
2
2
  <span class="click-to-reveal--hidden-value">
3
3
  {{#if (has-block)}}
4
4
  {{yield}}
@@ -4,33 +4,11 @@ import { action, computed } from '@ember/object';
4
4
  import { later } from '@ember/runloop';
5
5
 
6
6
  export default class ClickToRevealComponent extends ClickToCopyComponent {
7
- /**
8
- * The visiblity state of the value
9
- *
10
- * @var {Boolean}
11
- */
12
7
  @tracked isVisible = false;
13
-
14
- /**
15
- * The loading state of the reveal process
16
- *
17
- * @var {Boolean}
18
- */
19
8
  @tracked isLoading = false;
20
-
21
- /**
22
- * The loading timing
23
- *
24
- * @var {Integer}
25
- */
26
9
  @tracked timeout = 600;
27
-
28
- /**
29
- * If click to copy should be enabled.
30
- *
31
- * @var {Boolean}
32
- */
33
10
  @tracked clickToCopy = false;
11
+ @tracked wrapperClass = this.args.wrapperClass ?? '';
34
12
 
35
13
  /**
36
14
  * Setup the component
@@ -47,6 +25,10 @@ export default class ClickToRevealComponent extends ClickToCopyComponent {
47
25
  if (column && column.cellComponentArgs) {
48
26
  this.clickToCopy = column.cellComponentArgs.clickToCopy ?? false;
49
27
  }
28
+
29
+ if (column && column.cellComponentArgs) {
30
+ this.wrapperClass = column.cellComponentArgs.wrapperClass ?? '';
31
+ }
50
32
  }
51
33
 
52
34
  /**
@@ -1,10 +1,12 @@
1
1
  <div class="fleetbase-dashboard-grid" ...attributes>
2
2
  <GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
3
3
  {{#each @dashboard.widgets as |widget|}}
4
- {{#let (resolve-component widget.component) as |ResolvedComponent|}}
5
- {{#if ResolvedComponent}}
4
+ {{#let (resolve-component widget.component) as |componentDefinition|}}
5
+ {{#if componentDefinition}}
6
6
  <GridStackItem id={{widget.id}} @options={{spread-widget-options (hash id=widget.id options=widget.grid_options)}} class="relative">
7
- {{component ResolvedComponent widget=widget options=widget.options}}
7
+ <LazyEngineComponent @component={{componentDefinition}} as |resolvedComponent|>
8
+ {{component resolvedComponent widget=widget options=widget.options}}
9
+ </LazyEngineComponent>
8
10
 
9
11
  {{#if @isEdit}}
10
12
  <div class="absolute top-2 right-2">
@@ -1,25 +1,35 @@
1
1
  <Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
2
- <Overlay::Header @title={{t "component.dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
2
+ <Overlay::Header @title={{t "dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5 text-gray-800 dark:text-white">
3
3
  <:actions>
4
4
  <div class="flex flex-1 justify-end">
5
- <Button @type="default" @icon="times" @helpText={{t "component.dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
5
+ <Button @type="default" @icon="times" @helpText={{t "dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
6
6
  </div>
7
7
  </:actions>
8
8
  </Overlay::Header>
9
9
 
10
- <Overlay::Body @wrapperClass="new-service-rate-overlay-body px-4 space-y-4 pt-4">
10
+ <Overlay::Body @wrapperClass="new-service-rate-overlay-body px-1 space-y-4 pt-2">
11
+ <div>
12
+ <Input
13
+ @type="text"
14
+ @value={{this.searchQuery}}
15
+ placeholder={{or (t "dashboard-widget-panel.filter-widgets") "Filter widgets by keyword"}}
16
+ class="w-full form-input form-input-sm"
17
+ {{on "input" this.updateSearchQuery}}
18
+ />
19
+ </div>
20
+
11
21
  <div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
12
22
  {{#each this.availableWidgets as |widget|}}
13
23
  <div
14
24
  class="rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out shadow-md px-4 py-2 cursor-pointer"
15
- {{on "click" (fn this.addWidgetToDashboard widget)}}
25
+ {{on "click" (perform this.addWidgetToDashboard widget)}}
16
26
  >
17
- <div class="flex flex-row items-center leading-6 mb-2.5">
18
- <div class="w-8 flex items-center justify-start">
27
+ <div class="flex flex-row items-center leading-7 mb-2">
28
+ <div class="w-7 flex items-center justify-start">
19
29
  <FaIcon @icon={{widget.icon}} class="text-lg text-gray-600 dark:text-gray-300" />
20
30
  </div>
21
31
  <p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
22
- {{t "component.dashboard-widget-panel.widget-name" widgetName=widget.name}}
32
+ {{t "dashboard-widget-panel.widget-name" widgetName=widget.name}}
23
33
  </p>
24
34
  </div>
25
35
  <div>
@@ -2,23 +2,72 @@ 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 { task } from 'ember-concurrency';
6
+ import { ExtensionComponent } from '@fleetbase/ember-core/contracts';
5
7
 
6
8
  export default class DashboardWidgetPanelComponent extends Component {
7
- @service universe;
8
- @tracked availableWidgets = [];
9
+ @service('universe/widget-service') widgetService;
10
+ @service notifications;
11
+ @tracked defaultDashboardId = this.args.defaultDashboardId ?? 'dashboard';
9
12
  @tracked dashboard;
10
13
  @tracked isOpen = true;
11
- @service notifications;
14
+ @tracked searchQuery = '';
12
15
 
13
16
  /**
14
17
  * Constructs the component and applies initial state.
15
18
  */
16
19
  constructor(owner, { dashboard, defaultDashboardId = 'dashboard' }) {
17
20
  super(...arguments);
18
- this.availableWidgets = this.universe.getWidgets(defaultDashboardId);
21
+ this.defaultDashboardId = defaultDashboardId;
19
22
  this.dashboard = dashboard;
20
23
  }
21
24
 
25
+ /**
26
+ * Gets available widgets for the dashboard.
27
+ * Computed as a getter to ensure reactivity when widgets are registered.
28
+ * @returns {Array} Available widgets
29
+ */
30
+ get availableWidgets() {
31
+ const dashboardId = this.args.defaultDashboardId || this.defaultDashboardId || 'dashboard';
32
+ const widgets = this.widgetService.getWidgets(dashboardId);
33
+
34
+ // Filter widgets by search query
35
+ if (this.searchQuery && this.searchQuery.trim()) {
36
+ const query = this.searchQuery.toLowerCase().trim();
37
+ return widgets.filter((widget) => {
38
+ const name = (widget.name || '').toLowerCase();
39
+ const description = (widget.description || '').toLowerCase();
40
+ return name.includes(query) || description.includes(query);
41
+ });
42
+ }
43
+
44
+ return widgets;
45
+ }
46
+
47
+ /**
48
+ * Adds a new available widget to the current dashboard.
49
+ *
50
+ * @param {Component|Widget|string} widget
51
+ * @memberof DashboardWidgetPanelComponent
52
+ */
53
+ @task *addWidgetToDashboard(widget) {
54
+ // If widget is a component definition/class
55
+ if (typeof widget.component === 'function') {
56
+ widget.component = widget.component.name;
57
+ }
58
+
59
+ // If using extension component definition
60
+ if (widget.component instanceof ExtensionComponent) {
61
+ widget.component = widget.component.toString();
62
+ }
63
+
64
+ try {
65
+ yield this.dashboard.addWidget(widget);
66
+ } catch (err) {
67
+ this.notifications.serverError(err);
68
+ }
69
+ }
70
+
22
71
  /**
23
72
  * Sets the overlay context.
24
73
  *
@@ -33,15 +82,14 @@ export default class DashboardWidgetPanelComponent extends Component {
33
82
  }
34
83
  }
35
84
 
36
- @action addWidgetToDashboard(widget) {
37
- // If widget is a component definition/class
38
- if (typeof widget.component === 'function') {
39
- widget.component = widget.component.name;
40
- }
41
-
42
- this.args.dashboard.addWidget(widget).catch((error) => {
43
- this.notifications.serverError(error);
44
- });
85
+ /**
86
+ * Updates the search query
87
+ *
88
+ * @action
89
+ * @param {Event} event
90
+ */
91
+ @action updateSearchQuery(event) {
92
+ this.searchQuery = event.target.value;
45
93
  }
46
94
 
47
95
  /**
@@ -17,7 +17,6 @@ export default class DashboardComponent extends Component {
17
17
  @service modalsManager;
18
18
  @service fetch;
19
19
  @service dashboard;
20
- @service universe;
21
20
 
22
21
  /**
23
22
  * Creates an instance of DashboardComponent.
@@ -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
+ }