mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add sort-by-device-type to the devices list
Wide layout makes the leading device-type icon column header tappable to sort; narrow layout gains a "Device Type" option in the sort sheet. The backend already whitelisted device_type as a sort column. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
863e37d06e
commit
3cb104e364
@@ -26,7 +26,7 @@
|
|||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
- [ ] Review the whole codebase for dead code, duplication and simplicity
|
- [ ] Review the whole codebase for dead code, duplication and simplicity
|
||||||
- [ ] In the devices list, add an order by type (in the wide version it should be over the icon on the left of the list)
|
- [x] In the devices list, add an order by type (in the wide version it should be over the icon on the left of the list)
|
||||||
- [ ] How expensive it is to get the total number of pages and implement a go to last page
|
- [ ] How expensive it is to get the total number of pages and implement a go to last page
|
||||||
- [x] Mobile - Device details - Chart buttons not fully visible, replace with combo only for narrow devices
|
- [x] Mobile - Device details - Chart buttons not fully visible, replace with combo only for narrow devices
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ class DeviceListHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: _iconWidth),
|
_IconHeaderCell(
|
||||||
|
active: sortColumn,
|
||||||
|
ascending: ascending,
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
for (final spec in _columnSpecs)
|
for (final spec in _columnSpecs)
|
||||||
_HeaderCell(
|
_HeaderCell(
|
||||||
flex: spec.flex,
|
flex: spec.flex,
|
||||||
@@ -152,6 +156,53 @@ class _HeaderCell extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The leading device-type column shows only an icon per row, so its header is a
|
||||||
|
// compact icon button that sorts by device type rather than a labelled cell.
|
||||||
|
class _IconHeaderCell extends StatelessWidget {
|
||||||
|
final DeviceSortColumn active;
|
||||||
|
final bool ascending;
|
||||||
|
final void Function(DeviceSortColumn column) onTap;
|
||||||
|
|
||||||
|
const _IconHeaderCell({
|
||||||
|
required this.active,
|
||||||
|
required this.ascending,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isActive = active == DeviceSortColumn.deviceType;
|
||||||
|
final color = isActive
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: theme.colorScheme.onSurfaceVariant;
|
||||||
|
return SizedBox(
|
||||||
|
width: _iconWidth,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => onTap(DeviceSortColumn.deviceType),
|
||||||
|
child: Tooltip(
|
||||||
|
message: 'Sort by device type',
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.devices, size: 18, color: color),
|
||||||
|
if (isActive)
|
||||||
|
Icon(
|
||||||
|
ascending ? Icons.arrow_upward : Icons.arrow_downward,
|
||||||
|
size: 14,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class DeviceRowWide extends StatelessWidget {
|
class DeviceRowWide extends StatelessWidget {
|
||||||
final Device device;
|
final Device device;
|
||||||
final FriendlyDateFormatter formatter;
|
final FriendlyDateFormatter formatter;
|
||||||
@@ -217,6 +268,9 @@ Widget _cellContent(
|
|||||||
FriendlyDateFormatter formatter,
|
FriendlyDateFormatter formatter,
|
||||||
) {
|
) {
|
||||||
switch (column) {
|
switch (column) {
|
||||||
|
case DeviceSortColumn.deviceType:
|
||||||
|
// The device type is shown via the leading icon column, never a text cell.
|
||||||
|
return const SizedBox.shrink();
|
||||||
case DeviceSortColumn.name:
|
case DeviceSortColumn.name:
|
||||||
return _OverflowTooltipText(
|
return _OverflowTooltipText(
|
||||||
text: _displayName(device),
|
text: _displayName(device),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
enum DeviceSortColumn {
|
enum DeviceSortColumn {
|
||||||
|
deviceType('Device Type', 'device_type'),
|
||||||
name('Name', 'name'),
|
name('Name', 'name'),
|
||||||
owner('Owner', 'owner'),
|
owner('Owner', 'owner'),
|
||||||
macAddress('MAC Address', 'mac_address'),
|
macAddress('MAC Address', 'mac_address'),
|
||||||
|
|||||||
@@ -101,6 +101,68 @@ void main() {
|
|||||||
await tearDownTree(tester);
|
await tearDownTree(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('tapping the device-type header sorts by device type', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Default ordering (last_seen, desc) returns two devices.
|
||||||
|
adapter.onGet(
|
||||||
|
'/devices',
|
||||||
|
(server) => server.reply(200, [
|
||||||
|
deviceJson(macAddress: '00:00:00:00:00:01'),
|
||||||
|
deviceJson(macAddress: '00:00:00:00:00:02'),
|
||||||
|
]),
|
||||||
|
queryParameters: {
|
||||||
|
'is_registered': false,
|
||||||
|
'sort_by': 'last_seen',
|
||||||
|
'sort_order': 'desc',
|
||||||
|
'page_offset': 0,
|
||||||
|
'page_limit': 11,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Sorting by device type (asc) returns a single, distinguishable device.
|
||||||
|
adapter.onGet(
|
||||||
|
'/devices',
|
||||||
|
(server) =>
|
||||||
|
server.reply(200, [deviceJson(macAddress: '00:00:00:00:00:03')]),
|
||||||
|
queryParameters: {
|
||||||
|
'is_registered': false,
|
||||||
|
'sort_by': 'device_type',
|
||||||
|
'sort_order': 'asc',
|
||||||
|
'page_offset': 0,
|
||||||
|
'page_limit': 11,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await pumpScreen(tester, const DeviceList());
|
||||||
|
await pumpUntilFound(tester, find.byType(DeviceRowWide));
|
||||||
|
expect(find.byType(DeviceRowWide), findsNWidgets(2));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Sort by device type'));
|
||||||
|
for (var i = 0;
|
||||||
|
i < 40 && find.byType(DeviceRowWide).evaluate().length != 1;
|
||||||
|
i++) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
expect(find.byType(DeviceRowWide), findsOneWidget);
|
||||||
|
|
||||||
|
await tearDownTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the sort sheet offers ordering by device type', (tester) async {
|
||||||
|
adapter.onGet('/devices', (server) => server.reply(200, [deviceJson()]));
|
||||||
|
|
||||||
|
// A phone-width viewport so the compact layout with the sort button shows.
|
||||||
|
await pumpScreen(tester, const DeviceList(), size: const Size(500, 900));
|
||||||
|
await pumpUntilFound(tester, find.byType(DeviceRowCompact));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Sort'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Device Type'), findsOneWidget);
|
||||||
|
|
||||||
|
await tearDownTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('pulling the list down refetches devices', (tester) async {
|
testWidgets('pulling the list down refetches devices', (tester) async {
|
||||||
// A single reply list whose contents are swapped before the pull, so the
|
// A single reply list whose contents are swapped before the pull, so the
|
||||||
// same route re-serializes fresh data on the refresh request.
|
// same route re-serializes fresh data on the refresh request.
|
||||||
|
|||||||
Reference in New Issue
Block a user