@fleetbase/ember-ui 0.3.9 → 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.
Files changed (51) hide show
  1. package/SCHEDULING_COMPONENTS.md +228 -0
  2. package/addon/components/availability-editor.js +81 -0
  3. package/addon/components/custom-field/input.hbs +1 -1
  4. package/addon/components/custom-field/input.js +61 -24
  5. package/addon/components/custom-field/options-input.hbs +1 -1
  6. package/addon/components/file.hbs +52 -49
  7. package/addon/components/file.js +5 -36
  8. package/addon/components/filter/multi-input.hbs +16 -0
  9. package/addon/components/filter/multi-input.js +68 -0
  10. package/addon/components/filter/range.hbs +52 -0
  11. package/addon/components/filter/range.js +74 -0
  12. package/addon/components/filters-picker.js +13 -6
  13. package/addon/components/layout/resource/panel.js +22 -12
  14. package/addon/components/layout/resource/tabular.hbs +2 -0
  15. package/addon/components/layout/resource/tabular.js +28 -0
  16. package/addon/components/modals/query-builder-computed-column-editor.hbs +80 -0
  17. package/addon/components/modals/query-builder-computed-column-editor.js +156 -0
  18. package/addon/components/query-builder/computed-columns.hbs +59 -0
  19. package/addon/components/query-builder/computed-columns.js +106 -0
  20. package/addon/components/query-builder.hbs +10 -0
  21. package/addon/components/query-builder.js +5 -0
  22. package/addon/components/report-builder.hbs +1 -0
  23. package/addon/components/schedule-calendar.hbs +37 -0
  24. package/addon/components/schedule-calendar.js +166 -0
  25. package/addon/components/schedule-item-card.hbs +39 -0
  26. package/addon/components/schedule-item-card.js +74 -0
  27. package/addon/components/table/cell/dropdown.hbs +2 -0
  28. package/addon/components/table/cell/dropdown.js +11 -0
  29. package/addon/components/table/cell/resizer.js +1 -1
  30. package/addon/components/table/cell.hbs +15 -2
  31. package/addon/components/table/cell.js +24 -0
  32. package/addon/components/table/foot.js +1 -1
  33. package/addon/components/table/td.hbs +2 -2
  34. package/addon/components/table/td.js +66 -1
  35. package/addon/components/table/th.hbs +15 -1
  36. package/addon/components/table/th.js +84 -0
  37. package/addon/components/table.hbs +2 -2
  38. package/addon/components/table.js +288 -0
  39. package/addon/styles/components/badge.css +8 -0
  40. package/addon/styles/components/file.css +32 -18
  41. package/addon/styles/components/input.css +8 -0
  42. package/addon/styles/components/table.css +126 -0
  43. package/addon/styles/layout/next.css +249 -28
  44. package/addon/utils/is-image-file.js +101 -0
  45. package/app/components/filter/multi-input.js +1 -0
  46. package/app/components/filter/range.js +1 -0
  47. package/app/components/modals/query-builder-computed-column-editor.js +1 -0
  48. package/app/components/query-builder/computed-columns.js +1 -0
  49. package/app/components/schedule-calendar.js +1 -0
  50. package/app/utils/is-image-file.js +1 -0
  51. 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
+ }
@@ -74,7 +74,7 @@
74
74
  @onFileAdded={{this.onFileAddedHandler}}
75
75
  >
76
76
  <a tabindex={{0}} class="btn btn-default btn-xs cursor-pointer">
77
- <FaIcon @icon="upload" @size="sm" class="mr-2" />{{t "common.select-file"}}
77
+ <FaIcon @icon="upload" @size="sm" class="mr-2" />{{t "common.select-field" field=(t "common.file")}}
78
78
  </a>
79
79
  </FileUpload>
80
80
  {{#if this.file}}
@@ -16,27 +16,53 @@ export default class CustomFieldInputComponent extends Component {
16
16
  @tracked value;
17
17
  @tracked file;
18
18
  @tracked uploadedFile;
19
- acceptedFileTypes = [
20
- 'application/vnd.ms-excel',
21
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
22
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
23
- 'application/msword',
24
- 'application/pdf',
25
- 'application/x-pdf',
26
- 'image/jpeg',
27
- 'image/png',
28
- 'image/gif',
29
- 'image/webp',
30
- 'video/mp4',
31
- 'video/quicktime',
32
- 'video/x-msvideo',
33
- 'video/x-flv',
34
- 'video/x-ms-wmv',
35
- 'audio/mpeg',
36
- 'video/x-msvideo',
37
- 'application/zip',
38
- 'application/x-tar',
39
- ];
19
+
20
+ get acceptedFileTypes() {
21
+ return [
22
+ // Excel
23
+ 'application/vnd.ms-excel',
24
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
25
+ 'application/vnd.ms-excel.sheet.macroenabled.12',
26
+ // Word / PowerPoint
27
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
28
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
29
+ 'application/msword',
30
+ // PDF
31
+ 'application/pdf',
32
+ 'application/x-pdf',
33
+ // Images
34
+ 'image/jpeg',
35
+ 'image/png',
36
+ 'image/gif',
37
+ 'image/webp',
38
+ // Video
39
+ 'video/mp4',
40
+ 'video/quicktime',
41
+ 'video/x-msvideo',
42
+ 'video/x-flv',
43
+ 'video/x-ms-wmv',
44
+ // Audio
45
+ 'audio/mpeg',
46
+ // Archives
47
+ 'application/zip',
48
+ 'application/x-tar',
49
+ // Json
50
+ 'application/json',
51
+ 'text/json',
52
+ 'application/x-json',
53
+ // Text documents
54
+ 'text/plain',
55
+ 'text/markdown',
56
+ 'application/rtf',
57
+ 'text/csv',
58
+ 'text/tab-separated-values',
59
+ 'text/html',
60
+ 'application/xml',
61
+ 'text/xml',
62
+ 'application/x-yaml',
63
+ 'text/yaml',
64
+ ];
65
+ }
40
66
 
41
67
  /**
42
68
  * A map defining the available custom field types and their corresponding components.
@@ -65,7 +91,7 @@ export default class CustomFieldInputComponent extends Component {
65
91
  }
66
92
  }
67
93
 
68
- @action onFileAddedHandler(file) {
94
+ @action async onFileAddedHandler(file) {
69
95
  // since we have dropzone and upload button within dropzone validate the file state first
70
96
  // as this method can be called twice from both functions
71
97
  if (['queued', 'failed', 'timed_out', 'aborted'].indexOf(file.state) === -1) return;
@@ -73,12 +99,23 @@ export default class CustomFieldInputComponent extends Component {
73
99
  // set file for progress state
74
100
  this.file = file;
75
101
 
102
+ // resolve subject if necessary
103
+ const subject = await this.subject;
104
+
105
+ let path = `uploads/${this.extension ?? 'cf-files'}/${this.customField.id}`;
106
+ let type = `custom_field_file`;
107
+
108
+ if (subject) {
109
+ path = `uploads/${this.extension ?? 'cf-files'}/${getModelName(subject)}-cf-files`;
110
+ type = `${underscore(getModelName(subject))}_file`;
111
+ }
112
+
76
113
  // Queue and upload immediatley
77
114
  this.fetch.uploadFile.perform(
78
115
  file,
79
116
  {
80
- path: `uploads/${this.extension}/${getModelName(this.subject)}-cf-files`,
81
- type: `${underscore(getModelName(this.subject))}_file`,
117
+ path,
118
+ type,
82
119
  },
83
120
  (uploadedFile) => {
84
121
  this.file = undefined;
@@ -1,6 +1,6 @@
1
1
  <div class="rounded-lg border border-gray-200 dark:border-gray-700" ...attributes>
2
2
  <div class="flex flex-row items-center justify-between px-4 py-2">
3
- <div class="mr-2 text-sm">{{t "fleet-ops.component.custom-field-form-panel.field-options"}}</div>
3
+ <div class="mr-2 text-sm">Field Options</div>
4
4
  <Button @type="magic" @icon="plus" @text="Add new option" @size="xs" @onClick={{this.addOption}} />
5
5
  </div>
6
6
  {{#each-in this.options as |index option|}}
@@ -1,56 +1,59 @@
1
- <div class="x-fleetbase-file" ...attributes>
2
- <div class="x-fleetbase-file-wrapper">
3
- <div class="x-fleetbase-file-actions">
4
- <DropdownButton
5
- @dropdownId="x-fleetbase-file-actions-dropdown"
6
- @icon={{or @dropdownIcon "ellipsis"}}
7
- @iconSize="xs"
8
- @iconPrefix={{@dropdownButtonIconPrefix}}
9
- @text={{@dropdownButtonText}}
10
- @size="xs"
11
- @horizontalPosition="left"
12
- @calculatePosition={{@dropdownButtonCalculatePosition}}
13
- @renderInPlace={{or @dropdownButtonRenderInPlace true}}
14
- @wrapperClass={{concat @dropdownButtonWrapperClass " " "next-nav-item-dropdown-button"}}
15
- @triggerClass={{@dropdownButtonTriggerClass}}
16
- @registerAPI={{@registerAPI}}
17
- @onInsert={{this.onDropdownButtonInsert}}
18
- as |dd|
19
- >
20
- <div class="next-dd-menu mt-0i" role="menu" aria-orientation="vertical" aria-labelledby="user-menu">
21
- <div class="px-1">
22
- <div class="text-sm flex flex-row items-center px-3 py-1 rounded-md my-1 text-gray-800 dark:text-gray-300">
23
- {{t "component.file.dropdown-label"}}
24
- </div>
25
- </div>
26
- <div class="next-dd-menu-seperator"></div>
27
- <div role="group" class="px-1">
1
+ <div
2
+ class="group relative w-32 h-40 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400 hover:shadow-md transition-all"
3
+ ...attributes
4
+ >
5
+ <div class="relative w-full h-32 bg-gray-50 dark:bg-gray-900 flex items-center justify-center overflow-hidden rounded-t-lg">
6
+ {{#if this.isImage}}
7
+ <Image src={{@file.url}} alt={{@file.original_filename}} class="w-full h-full object-cover" />
8
+ {{else}}
9
+ <div class="flex items-center justify-center w-full h-full">
10
+ <FileIcon @file={{@file}} @hideExtension={{true}} @iconSize="3x" />
11
+ </div>
12
+ {{/if}}
13
+ </div>
28
14
 
29
- <a href="javascript:;" role="menuitem" class="next-dd-item text-danger" {{on "click" (fn this.onDropdownItemClick "onDelete" dd)}}>
30
- <span class="mr-1">
31
- <FaIcon @icon="trash" @prefix={{@dropdownButtonIconPrefix}} />
32
- </span>
33
- {{t "common.delete"}}
34
- </a>
15
+ <div class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
16
+ <DropdownButton
17
+ @dropdownId="file-actions-{{@file.id}}"
18
+ @icon={{or @dropdownIcon "ellipsis-v"}}
19
+ @iconSize="sm"
20
+ @iconPrefix={{@dropdownButtonIconPrefix}}
21
+ @text={{@dropdownButtonText}}
22
+ @size="xs"
23
+ @horizontalPosition="left"
24
+ @calculatePosition={{@dropdownButtonCalculatePosition}}
25
+ @renderInPlace={{or @dropdownButtonRenderInPlace true}}
26
+ @wrapperClass={{concat @dropdownButtonWrapperClass " " "next-nav-item-dropdown-button"}}
27
+ @triggerClass={{@dropdownButtonTriggerClass}}
28
+ @registerAPI={{@registerAPI}}
29
+ @onInsert={{this.onDropdownButtonInsert}}
30
+ as |dd|
31
+ >
32
+ <div class="next-dd-menu mt-0" role="menu">
33
+ <div class="px-1 pt-2">
34
+ <div class="text-xs px-2 text-gray-500 dark:text-gray-400 font-medium">
35
+ {{t "component.file.dropdown-label"}}
35
36
  </div>
36
37
  </div>
37
- </DropdownButton>
38
- </div>
39
- <div class="flex flex-1 flex-col justify-between items-center">
40
- <div class="flex-1">
41
- {{#if this.isImage}}
42
- <Image src={{@file.url}} alt={{@file.original_filename}} class="x-fleetbase-file-preview rounded-md shadow-sm" />
43
- {{else}}
44
- <div class="x-fleetbase-file-preview">
45
- <FileIcon @file={{@file}} @hideExtension={{true}} @iconSize="2xl" />
46
- </div>
47
- {{/if}}
48
- </div>
49
- <div class="flex-1 overflow-hidden flex flex-col items-center justify-end">
50
- <div class="x-fleetbase-file-name">
51
- {{truncate-filename @file.original_filename}}
38
+ <div class="next-dd-menu-seperator"></div>
39
+ <div role="group" class="px-1">
40
+ <a
41
+ href="javascript:;"
42
+ role="menuitem"
43
+ class="next-dd-item xs-text text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
44
+ {{on "click" (dropdown-fn dd @onDelete this.file)}}
45
+ >
46
+ <FaIcon @icon="trash" @size="sm" class="mr-2" @prefix={{@dropdownButtonIconPrefix}} />
47
+ {{t "common.delete"}}
48
+ </a>
52
49
  </div>
53
50
  </div>
54
- </div>
51
+ </DropdownButton>
52
+ </div>
53
+
54
+ <div class="flex items-center justify-between px-2 py-1.5 border-t border-gray-100 dark:border-gray-700">
55
+ <span class="text-xs text-gray-700 dark:text-gray-300 truncate font-medium">
56
+ {{truncate-filename @file.original_filename}}
57
+ </span>
55
58
  </div>
56
59
  </div>
@@ -1,43 +1,12 @@
1
1
  import Component from '@glimmer/component';
2
- import { tracked } from '@glimmer/tracking';
3
- import { action } from '@ember/object';
2
+ import isImageFile from '../utils/is-image-file';
4
3
 
5
4
  export default class FileComponent extends Component {
6
- @tracked file;
7
- @tracked isImage = false;
8
-
9
- constructor(owner, { file }) {
10
- super(...arguments);
11
-
12
- this.file = file;
13
- this.isImage = this.isImageFile(file);
14
- }
15
-
16
- @action onDropdownItemClick(action, dd) {
17
- if (typeof dd.actions === 'object' && typeof dd.actions.close === 'function') {
18
- dd.actions.close();
19
- }
20
-
21
- if (typeof this.args[action] === 'function') {
22
- this.args[action](this.file);
23
- }
5
+ get file() {
6
+ return this.args.file;
24
7
  }
25
8
 
26
- isImageFile(file) {
27
- if (!file || (!file.original_filename && !file.url && !file.path)) {
28
- return false;
29
- }
30
-
31
- const filename = file.original_filename || file.url || file.path;
32
- const extensionMatch = filename.match(/\.(.+)$/);
33
-
34
- if (!extensionMatch) {
35
- return false;
36
- }
37
-
38
- const extension = extensionMatch[1].toLowerCase();
39
- const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'];
40
-
41
- return imageExtensions.includes(extension);
9
+ get isImage() {
10
+ return isImageFile(this.file);
42
11
  }
43
12
  }
@@ -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
+ }