Skip to content

Commit 17f8e8b

Browse files
committed
Give record context panels the same header, tabs and actions as their details routes
Radar opened records through each resource's panel.view, and most of those registered only an overview tab with no header, title or buttons, drew the default header rule above the tab list, and opened narrower than the route. Work order, maintenance, schedule, part, equipment, issue and device panels now carry their route's header component, tabs and action buttons; inspection submissions and forms gain panels. Header menus are shared between routes and panels so they cannot drift. Radar loads records by public id with a single-record query, as the details routes do, which stops Ember Data's RecordIdentifier type error. Closing an issue from Radar uses the close-issue modal, and selected inspection links can be revoked in bulk. Adds the missing issue timeline, panel header and card re-exports the host panel needs.
1 parent fe70c6e commit 17f8e8b

36 files changed

Lines changed: 612 additions & 311 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<ActivityLog @subjectId={{@resource.id}} class="p-3" ...attributes />
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<div class="schedule-work-orders-tab p-4" ...attributes>
2+
{{#if this.load.isRunning}}
3+
<div class="flex items-center justify-center py-12"><Spinner /></div>
4+
{{else if this.workOrders.length}}
5+
<div class="space-y-2">
6+
{{#each this.workOrders as |workOrder|}}
7+
<button
8+
type="button"
9+
class="flex w-full items-start justify-between rounded-lg border border-gray-200 bg-white px-2 py-2 text-left text-sm dark:border-gray-700 dark:bg-gray-800"
10+
{{on "click" (fn this.view workOrder)}}
11+
>
12+
<div class="flex items-center gap-3">
13+
<FaIcon @icon="wrench" class="text-gray-400" />
14+
<div>
15+
<div class="font-medium text-gray-900 dark:text-white">{{or workOrder.subject workOrder.code workOrder.public_id}}</div>
16+
<div class="text-xs text-gray-500 dark:text-gray-400">
17+
{{workOrder.public_id}}
18+
{{#if workOrder.opened_at}}
19+
&bull; Opened
20+
{{format-date workOrder.opened_at "dd MMM yyyy"}}
21+
{{/if}}
22+
</div>
23+
</div>
24+
</div>
25+
<div class="flex items-center gap-2">
26+
<Badge @status={{workOrder.status}} @label={{smart-humanize workOrder.status}} />
27+
<Badge @status={{workOrder.priority}} @label={{smart-humanize workOrder.priority}} />
28+
</div>
29+
</button>
30+
{{/each}}
31+
</div>
32+
{{else}}
33+
<div class="flex flex-col items-center justify-center py-12 text-center text-gray-500 dark:text-gray-400">
34+
<FaIcon @icon="clipboard-list" class="mb-3 text-3xl text-gray-300 dark:text-gray-600" />
35+
<p class="text-sm font-medium">No work orders yet</p>
36+
<p class="mt-1 text-xs">Work orders will appear here when this schedule triggers.</p>
37+
</div>
38+
{{/if}}
39+
</div>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Component from '@glimmer/component';
2+
import { inject as service } from '@ember/service';
3+
import { tracked } from '@glimmer/tracking';
4+
import { action } from '@ember/object';
5+
import { task } from 'ember-concurrency';
6+
7+
/**
8+
* The work orders a maintenance schedule has raised, for the schedule's
9+
* context panel. The details route loads the same list in its own route.
10+
*/
11+
export default class MaintenanceScheduleWorkOrdersComponent extends Component {
12+
@service store;
13+
@service workOrderActions;
14+
@tracked workOrders = [];
15+
16+
constructor() {
17+
super(...arguments);
18+
this.load.perform();
19+
}
20+
21+
get schedule() {
22+
return this.args.resource ?? this.args.model;
23+
}
24+
25+
@task *load() {
26+
if (!this.schedule?.id) {
27+
return;
28+
}
29+
30+
const workOrders = yield this.store.query('work-order', { schedule_uuid: this.schedule.id });
31+
this.workOrders = workOrders?.toArray?.() ?? [...(workOrders ?? [])];
32+
}
33+
34+
@action view(workOrder) {
35+
return this.workOrderActions.panel.view(workOrder);
36+
}
37+
}
Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
<div class="px-4 py-2">
2-
<div class="flex flex-1 flex-row items-start justify-between">
3-
<div class="flex flex-col">
4-
<div class="flex flex-row items-center space-x-2">
5-
<h1 class="text-gray-900 dark:text-white font-semibold text-lg">{{n-a @resource.summary}}</h1>
6-
<Badge @status={{@resource.status}}>{{smart-humanize @resource.status}}</Badge>
1+
<div class="px-4 py-2" ...attributes>
2+
<div class="flex flex-1 flex-row items-start justify-between gap-3">
3+
<div class="flex min-w-0 flex-1 flex-col">
4+
<div class="flex min-w-0 flex-row items-center gap-2">
5+
<h1 class="truncate text-lg font-semibold text-gray-900 dark:text-white" title={{@resource.summary}}>{{n-a @resource.summary}}</h1>
6+
<Badge @status={{@resource.status}} class="flex-shrink-0">{{smart-humanize @resource.status}}</Badge>
77
</div>
8-
<div class="flex flex-row space-x-3 mt-1">
9-
<div class="text-xs text-gray-400 dark:text-gray-500">{{smart-humanize @resource.type}}</div>
8+
<div class="mt-1 flex min-w-0 flex-row flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-gray-400 dark:text-gray-500">
9+
{{#if @resource.type}}
10+
<span class="truncate">{{smart-humanize @resource.type}}</span>
11+
{{/if}}
1012
{{#if @resource.priority}}
11-
<div class="text-xs text-gray-400 dark:text-gray-500">{{t "column.priority"}}: {{smart-humanize @resource.priority}}</div>
13+
<span class="truncate">{{t "column.priority"}}: {{smart-humanize @resource.priority}}</span>
1214
{{/if}}
1315
{{#if @resource.scheduled_at}}
14-
<div class="text-xs text-gray-400 dark:text-gray-500">{{t "column.scheduled-at"}}: {{format-date @resource.scheduled_at}}</div>
16+
<span class="truncate">{{t "column.scheduled-at"}}: {{format-date @resource.scheduled_at}}</span>
1517
{{/if}}
1618
</div>
1719
</div>
18-
<div class="next-view-header-right">
20+
<div class="next-view-header-right flex-shrink-0 whitespace-nowrap">
1921
<Layout::Resource::Panel::HeaderActions
2022
@resource={{@resource}}
2123
@saveTask={{@saveTask}}
@@ -28,4 +30,4 @@
2830
/>
2931
</div>
3032
</div>
31-
</div>
33+
</div>

‎addon/components/radar/bulk-bar.hbs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@
2222
</div>
2323
</DropdownButton>
2424
<Button @size="xs" @icon="user-plus" @text={{t "radar.actions.assign"}} @disabled={{@isBusy}} @onClick={{fn @onAct "assign"}} data-test-radar-bulk-action="assign" />
25+
{{#if this.revocableCount}}
26+
<Button
27+
@size="xs"
28+
@icon="link-slash"
29+
@type="danger"
30+
@text={{t "radar.actions.revoke-links" count=this.revocableCount}}
31+
@disabled={{@isBusy}}
32+
@onClick={{fn @onAct "revoke_link"}}
33+
data-test-radar-bulk-action="revoke_link"
34+
/>
35+
{{/if}}
2536
{{#if this.hasSnoozed}}
2637
<Button @size="xs" @icon="bell" @text={{t "radar.actions.wake"}} @disabled={{@isBusy}} @onClick={{fn @onAct "wake"}} data-test-radar-bulk-action="wake" />
2738
{{/if}}

‎addon/components/radar/bulk-bar.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export default class RadarBulkBarComponent extends Component {
1717
return [...counts.entries()].map(([label, count]) => `${count} ${label}`).join(' · ');
1818
}
1919

20+
/** Selected inspection links that can still be revoked. */
21+
get revocableCount() {
22+
return (this.args.items ?? []).filter((item) => item.actions?.includes('revoke_link')).length;
23+
}
24+
2025
get hasSnoozed() {
2126
return (this.args.items ?? []).some((item) => item.state?.status === 'snoozed');
2227
}

‎addon/components/work-order/panel-header.hbs‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
1-
<div class="px-4 py-2">
2-
<div class="flex flex-1 flex-row items-start justify-between">
3-
<div class="flex flex-col">
4-
<div class="flex flex-row items-center space-x-2">
5-
<h1 class="text-gray-900 dark:text-white font-semibold text-lg">{{or @resource.code @resource.subject}}</h1>
6-
<Badge @status={{@resource.status}}>{{smart-humanize @resource.status}}</Badge>
1+
<div class="px-4 py-2" ...attributes>
2+
<div class="flex flex-1 flex-row items-start justify-between gap-3">
3+
<div class="flex min-w-0 flex-1 flex-col">
4+
<div class="flex min-w-0 flex-row items-center gap-2">
5+
<h1 class="truncate text-lg font-semibold text-gray-900 dark:text-white" title={{or @resource.code @resource.subject}}>{{or @resource.code @resource.subject}}</h1>
6+
<Badge @status={{@resource.status}} class="flex-shrink-0">{{smart-humanize @resource.status}}</Badge>
77
</div>
8-
<div class="flex flex-row space-x-3 mt-1">
9-
<div class="text-xs text-gray-400 dark:text-gray-500">{{smart-humanize @resource.type}}</div>
8+
<div class="mt-1 flex min-w-0 flex-row flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-gray-400 dark:text-gray-500">
9+
{{#if @resource.type}}
10+
<span class="truncate">{{smart-humanize @resource.type}}</span>
11+
{{/if}}
1012
{{#if @resource.priority}}
11-
<div class="text-xs text-gray-400 dark:text-gray-500">{{t "column.priority"}}: {{smart-humanize @resource.priority}}</div>
13+
<span class="truncate">{{t "column.priority"}}: {{smart-humanize @resource.priority}}</span>
1214
{{/if}}
13-
{{#if (or @resource.assignee @resource.assignee_name)}}
14-
<div class="flex flex-row items-center text-xs text-gray-400 dark:text-gray-500">
15-
<span class="mr-1">{{t "column.assignee"}}:</span>
16-
<span>{{or @resource.assignee_name @resource.assignee.name}}</span>
17-
</div>
15+
{{#if (or @resource.assignee_name @resource.assignee.name)}}
16+
<span class="truncate">{{t "column.assignee"}}: {{or @resource.assignee_name @resource.assignee.name}}</span>
1817
{{/if}}
1918
</div>
2019
</div>
21-
<div class="next-view-header-right">
20+
<div class="next-view-header-right flex-shrink-0 whitespace-nowrap">
2221
<Layout::Resource::Panel::HeaderActions
2322
@resource={{@resource}}
2423
@saveTask={{@saveTask}}

‎addon/controllers/connectivity/devices/index/details.js‎

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,7 @@ export default class ConnectivityDevicesIndexDetailsController extends Controlle
4242
icon: 'ellipsis-h',
4343
iconPrefix: 'fas',
4444
renderInPlace: true,
45-
items: [
46-
{
47-
text: this.intl.t('device.actions.attach-to-asset'),
48-
icon: 'link',
49-
fn: () => this.deviceActions.attachToVehicle(this.model),
50-
permission: 'fleet-ops update device',
51-
},
52-
...(this.model.attachable_uuid || this.model.attached_to_name || this.model.attachable
53-
? [
54-
{
55-
text: this.intl.t('device.attachment.detach'),
56-
icon: 'unlink',
57-
fn: () => this.deviceActions.detachFromVehicle(this.model),
58-
permission: 'fleet-ops update device',
59-
},
60-
]
61-
: []),
62-
],
45+
items: this.deviceActions.attachmentItems(this.model),
6346
},
6447
];
6548
}
Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import Controller from '@ember/controller';
22
import { inject as service } from '@ember/service';
33
import { tracked } from '@glimmer/tracking';
4-
import { action } from '@ember/object';
54

65
export default class MaintenanceInspectionFormsIndexDetailsController extends Controller {
76
@service inspectionFormActions;
@@ -22,31 +21,9 @@ export default class MaintenanceInspectionFormsIndexDetailsController extends Co
2221
* The public link needs a published form, so it appears at the same moment.
2322
*/
2423
get actionButtons() {
25-
const isPublished = this.model?.is_published === true || this.model?.status === 'published';
26-
27-
return [
28-
...(isPublished ? [] : [{ icon: 'check', fn: this.publish, text: 'Publish', type: 'success', permission: 'fleet-ops publish inspection-form' }]),
29-
...(isPublished ? [{ icon: 'link', fn: this.generateLink, text: 'Generate Link', permission: 'fleet-ops view inspection-form' }] : []),
30-
{ icon: 'edit', fn: this.edit, permission: 'fleet-ops update inspection-form' },
31-
{ icon: 'trash', fn: this.delete, type: 'danger', permission: 'fleet-ops delete inspection-form' },
32-
];
33-
}
34-
35-
@action publish() {
36-
return this.inspectionFormActions.publish(this.model);
37-
}
38-
39-
@action generateLink() {
40-
return this.inspectionFormActions.generateLink(this.model);
41-
}
42-
43-
@action edit() {
44-
return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index.edit', this.model);
45-
}
46-
47-
@action delete() {
48-
return this.inspectionFormActions.delete(this.model, {
49-
onConfirm: () => this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'),
24+
return this.inspectionFormActions.headerActionButtons(this.model, {
25+
onEdit: () => this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index.edit', this.model),
26+
onDeleted: () => this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'),
5027
});
5128
}
5229
}

‎addon/controllers/maintenance/inspection-submissions/index/details.js‎

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -42,59 +42,12 @@ export default class MaintenanceInspectionSubmissionsIndexDetailsController exte
4242
* from the Follow Up panel instead of offered again here.
4343
*/
4444
get followUpItems() {
45-
const record = this.model;
46-
const items = [];
47-
48-
if (record?.has_failures && !record?.issue_uuid) {
49-
items.push({
50-
text: this.intl.t('inspection.record.create-issue'),
51-
icon: 'triangle-exclamation',
52-
fn: this.createIssue,
53-
permission: 'fleet-ops create-issue inspection-submission',
54-
});
55-
}
56-
57-
if (record?.has_failures && !record?.work_order_uuid) {
58-
items.push({
59-
text: this.intl.t('inspection.record.create-work-order'),
60-
icon: 'clipboard-list',
61-
fn: this.createWorkOrder,
62-
permission: 'fleet-ops create-work-order inspection-submission',
63-
});
64-
}
65-
66-
if (record?.status !== 'resolved') {
67-
items.push({ text: this.intl.t('inspection.record.resolve'), icon: 'check', fn: this.resolve, permission: 'fleet-ops resolve inspection-submission' });
68-
}
69-
70-
if (items.length) {
71-
items.push({ separator: true });
72-
}
73-
74-
items.push({ text: this.intl.t('common.delete'), icon: 'trash', class: 'text-red-500', fn: this.delete, permission: 'fleet-ops delete inspection-submission' });
75-
76-
return items;
77-
}
78-
79-
@action createIssue() {
80-
return this.inspectionSubmissionActions.createIssue(this.model);
81-
}
82-
83-
@action createWorkOrder() {
84-
return this.inspectionSubmissionActions.createWorkOrder(this.model);
85-
}
86-
87-
@action resolve() {
88-
return this.inspectionSubmissionActions.resolve(this.model);
45+
return this.inspectionSubmissionActions.followUpItems(this.model, {
46+
onDeleted: () => this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'),
47+
});
8948
}
9049

9150
@action edit() {
9251
return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index.edit', this.model);
9352
}
94-
95-
@action delete() {
96-
return this.inspectionSubmissionActions.delete(this.model, {
97-
onConfirm: () => this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'),
98-
});
99-
}
10053
}

0 commit comments

Comments
 (0)