@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.
- package/SCHEDULING_COMPONENTS.md +228 -0
- package/addon/components/availability-editor.js +81 -0
- package/addon/components/custom-field/input.hbs +1 -1
- package/addon/components/custom-field/input.js +61 -24
- package/addon/components/custom-field/options-input.hbs +1 -1
- package/addon/components/file.hbs +52 -49
- package/addon/components/file.js +5 -36
- 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/panel.js +22 -12
- 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/file.css +32 -18
- package/addon/styles/components/input.css +8 -0
- package/addon/styles/components/table.css +126 -0
- package/addon/styles/layout/next.css +249 -28
- package/addon/utils/is-image-file.js +101 -0
- 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/app/utils/is-image-file.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
|
+
}
|
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
81
|
-
type
|
|
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">
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
</
|
|
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>
|
package/addon/components/file.js
CHANGED
|
@@ -1,43 +1,12 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
|
-
import
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
+
}
|