Skip to content

Commit ee39c64

Browse files
authored
Merge pull request #58 from pvdthings/linked-things
Linked Things
2 parents 0509139 + 6571989 commit ee39c64

File tree

13 files changed

+369
-74
lines changed

13 files changed

+369
-74
lines changed

apps/api/apps/librarian/routes/things.js

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { fetchCategories, fetchThings, fetchThing, createThing, updateThing, deleteThingImage, updateThingCategories, deleteThing } = require('../../../services/things');
1+
const { fetchCategories, fetchThings, fetchThing, createThing, updateThing, deleteThingImage, deleteThing } = require('../../../services/things');
22

33
const express = require('express');
44
const router = express.Router();
@@ -43,23 +43,10 @@ router.put('/', async (req, res) => {
4343

4444
router.patch('/:id', async (req, res) => {
4545
const { id } = req.params;
46-
const { name, spanishName, eyeProtection, hidden, image } = req.body;
47-
48-
try {
49-
await updateThing(id, { name, spanishName, eyeProtection, hidden, image });
50-
res.status(204).send();
51-
} catch (error) {
52-
console.error(error);
53-
res.status(error.status || 500).send({ errors: [error] });
54-
}
55-
});
56-
57-
router.patch('/:id/categories', async (req, res) => {
58-
const { id } = req.params;
59-
const { categories } = req.body;
46+
const data = req.body;
6047

6148
try {
62-
await updateThingCategories(id, { categories });
49+
await updateThing(id, data);
6350
res.status(204).send();
6451
} catch (error) {
6552
console.error(error);

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pvdthings-api",
3-
"version": "1.18.3",
3+
"version": "1.19.0",
44
"description": "",
55
"main": "server.js",
66
"scripts": {

apps/api/services/things/mapThingDetails.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
function mapThingDetails(record, items) {
1+
const { mapItem } = require("../inventory");
2+
3+
function mapThingDetails(record, items = [], linkedThings = []) {
24
return {
35
id: record.id,
46
name: record.get('Name'),
@@ -8,14 +10,26 @@ function mapThingDetails(record, items) {
810
hidden: Boolean(record.get('Hidden')),
911
categories: record.get('Category') || [],
1012
eyeProtection: Boolean(record.get('Eye Protection')),
11-
images: record.get('Image')?.map(i => ({
12-
id: i.id,
13-
url: i.url,
14-
width: i.width,
15-
height: i.height,
16-
type: i.type
17-
})) || [],
18-
items
13+
images: record.get('Image')?.map(mapImage) || [],
14+
items: items.map(mapItem),
15+
linkedThings: linkedThings.map(mapLinkedThing)
16+
};
17+
}
18+
19+
function mapImage(image) {
20+
return {
21+
id: image.id,
22+
url: image.thumbnails.large.url,
23+
width: image.thumbnails.large.width,
24+
height: image.thumbnails.large.height,
25+
type: image.type
26+
};
27+
}
28+
29+
function mapLinkedThing(thing) {
30+
return {
31+
id: thing.id,
32+
name: thing.get('Name')
1933
};
2034
}
2135

apps/api/services/things/service.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const { base, Table, ThingCategories } = require('../../db');
22
const mapThing = require('./mapThing');
33
const mapThingDetails = require('./mapThingDetails');
4-
const { mapItem } = require('../inventory');
54
const things = base(Table.Things);
65
const inventory = base(Table.Inventory);
76

@@ -22,13 +21,15 @@ const fetchThings = async ({ byPopularity } = {}) => {
2221
const fetchThing = async (id) => {
2322
const record = await things.find(id);
2423

25-
const itemPromises = record.get('Inventory')?.map(id => {
26-
return inventory.find(id)
27-
});
24+
const itemPromises = record?.get('Inventory')?.map(id => inventory.find(id)) || [];
25+
const linkedThingPromises = record?.get('Linked Things')?.map(id => things.find(id)) || [];
2826

29-
const items = itemPromises ? (await Promise.all(itemPromises)).map(mapItem) : [];
27+
const [itemRecords, linkedThingRecords] = await Promise.all([
28+
Promise.all(itemPromises),
29+
Promise.all(linkedThingPromises)
30+
]);
3031

31-
return record ? mapThingDetails(record, items) : null;
32+
return record ? mapThingDetails(record, itemRecords, linkedThingRecords) : null;
3233
}
3334

3435
const createThing = async ({ name, spanishName, hidden, image, eyeProtection }) => {
@@ -40,10 +41,10 @@ const createThing = async ({ name, spanishName, hidden, image, eyeProtection })
4041
'Image': image?.url ? [{ url: image.url }] : []
4142
});
4243

43-
return record ? mapThingDetails(record, []) : null;
44+
return record ? mapThingDetails(record) : null;
4445
}
4546

46-
const updateThing = async (id, { name, spanishName, hidden, image, eyeProtection }) => {
47+
const updateThing = async (id, { name, spanishName, categories, hidden, image, eyeProtection, linkedThings }) => {
4748
let updatedFields = {};
4849

4950
if (name) {
@@ -54,6 +55,10 @@ const updateThing = async (id, { name, spanishName, hidden, image, eyeProtection
5455
updatedFields['name_es'] = spanishName;
5556
}
5657

58+
if (categories !== null) {
59+
updatedFields['Category'] = categories;
60+
}
61+
5762
if (hidden !== null) {
5863
updatedFields['Hidden'] = hidden;
5964
}
@@ -66,17 +71,17 @@ const updateThing = async (id, { name, spanishName, hidden, image, eyeProtection
6671
updatedFields['Eye Protection'] = eyeProtection;
6772
}
6873

74+
if (linkedThings !== null) {
75+
updatedFields['Linked Things'] = linkedThings;
76+
}
77+
6978
await things.update(id, updatedFields);
7079
}
7180

7281
const deleteThing = async (id) => {
7382
await things.destroy(id);
7483
}
7584

76-
const updateThingCategories = async (id, { categories }) => {
77-
await things.update(id, { 'Category': categories });
78-
}
79-
8085
const deleteThingImage = async (id) => {
8186
const record = await things.update(id, { 'Image': [] });
8287
return mapThingDetails(record);
@@ -88,7 +93,6 @@ module.exports = {
8893
fetchThing,
8994
createThing,
9095
updateThing,
91-
updateThingCategories,
9296
deleteThingImage,
9397
deleteThing,
9498
};

apps/librarian/lib/core/api/models/detailed_thing_model.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class DetailedThingModel {
77
required this.id,
88
required this.name,
99
required this.categories,
10+
required this.linkedThings,
1011
required this.images,
1112
required this.items,
1213
required this.hidden,
@@ -24,6 +25,7 @@ class DetailedThingModel {
2425
final int stock;
2526
final int available;
2627
final List<String> categories;
28+
final List<LinkedThing> linkedThings;
2729
final List<ImageModel> images;
2830
final List<ItemModel> items;
2931

@@ -37,9 +39,26 @@ class DetailedThingModel {
3739
stock: json['stock'] as int,
3840
available: json['available'] as int,
3941
categories: (json['categories'] as List).cast<String>(),
42+
linkedThings: (json['linkedThings'] as List)
43+
.map((e) => LinkedThing.fromJson(e))
44+
.toList(),
4045
images:
4146
(json['images'] as List).map((e) => ImageModel.fromJson(e)).toList(),
4247
items: (json['items'] as List).map((e) => ItemModel.fromJson(e)).toList(),
4348
);
4449
}
4550
}
51+
52+
class LinkedThing {
53+
const LinkedThing({required this.id, required this.name});
54+
55+
final String id;
56+
final String name;
57+
58+
factory LinkedThing.fromJson(Map<String, dynamic> json) {
59+
return LinkedThing(
60+
id: json['id'] as String,
61+
name: json['name'] as String,
62+
);
63+
}
64+
}

apps/librarian/lib/core/api/things.dart

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ Future<Response> updateThing(
2626
String thingId, {
2727
String? name,
2828
String? spanishName,
29+
List<String>? categories,
30+
List<String>? linkedThings,
2931
bool? hidden,
3032
bool? eyeProtection,
3133
ImageDTO? image,
3234
}) async {
3335
return await DioClient.instance.patch('/things/$thingId', data: {
3436
'name': name,
3537
'spanishName': spanishName,
38+
'categories': categories,
39+
'linkedThings': linkedThings,
3640
'hidden': hidden,
3741
'eyeProtection': eyeProtection,
3842
'image': image != null ? {'url': image.url} : null,
@@ -43,15 +47,6 @@ Future<Response> deleteThing(String id) async {
4347
return await DioClient.instance.delete('/things/$id');
4448
}
4549

46-
Future<Response> updateThingCategories(
47-
String id, {
48-
required List<String> categories,
49-
}) async {
50-
return await DioClient.instance.patch('/things/$id/categories', data: {
51-
'categories': categories,
52-
});
53-
}
54-
5550
Future<Response> deleteThingImage(String thingId) async {
5651
return await DioClient.instance.delete('/things/$thingId/image');
5752
}

apps/librarian/lib/core/data/inventory_repository.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,19 @@ class InventoryRepository extends Notifier<Future<List<ThingModel>>> {
6969
bool? hidden,
7070
bool? eyeProtection,
7171
List<String>? categories,
72+
List<LinkedThing>? linkedThings,
7273
UpdatedImageModel? image,
7374
}) async {
7475
if (image != null && image.bytes == null) {
7576
await deleteThingImage(thingId: thingId);
7677
}
7778

78-
if (categories != null) {
79-
await api.updateThingCategories(thingId, categories: categories);
80-
}
81-
8279
await api.updateThing(
8380
thingId,
8481
name: name,
8582
spanishName: spanishName,
83+
categories: categories,
84+
linkedThings: linkedThings?.map((t) => t.id).toList(),
8685
hidden: hidden,
8786
eyeProtection: eyeProtection,
8887
image: await _convert(image),

apps/librarian/lib/modules/things/details/inventory_details.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'package:librarian_app/modules/things/details/image/thing_image_card.dart
1818
import 'package:librarian_app/utils/media_query.dart';
1919

2020
import 'inventory/item_details/item_details_controller.dart';
21+
import 'linked_things/card.dart';
2122

2223
class InventoryDetails extends ConsumerWidget {
2324
const InventoryDetails({super.key});
@@ -69,7 +70,14 @@ class InventoryDetails extends ConsumerWidget {
6970
],
7071
),
7172
const SizedBox(height: 32),
72-
const CategoriesCard(),
73+
const Wrap(
74+
spacing: 16,
75+
runSpacing: 32,
76+
children: [
77+
CategoriesCard(),
78+
LinkedThingsCard(),
79+
],
80+
),
7381
const SizedBox(height: 32),
7482
ItemsCard(
7583
items: details.items,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:librarian_app/core/api/models/detailed_thing_model.dart';
4+
import 'package:librarian_app/core/api/models/thing_model.dart';
5+
import 'package:librarian_app/modules/things/providers/edited_thing_details_providers.dart';
6+
import 'package:librarian_app/modules/things/providers/thing_details_provider.dart';
7+
import 'package:librarian_app/widgets/details_card/card_body.dart';
8+
import 'package:librarian_app/widgets/details_card/card_header.dart';
9+
import 'package:librarian_app/widgets/details_card/details_card.dart';
10+
import 'package:librarian_app/widgets/hint_text.dart';
11+
12+
import 'dialog.dart';
13+
14+
class LinkedThingsCard extends ConsumerWidget {
15+
const LinkedThingsCard({super.key});
16+
17+
Future<List<LinkedThing>?> _chooseThings(context) async {
18+
final things = await showDialog<List<ThingModel>?>(
19+
context: context,
20+
builder: (context) => const ChooseThingsDialog(),
21+
);
22+
23+
return things?.map((t) => LinkedThing(id: t.id, name: t.name)).toList();
24+
}
25+
26+
@override
27+
Widget build(BuildContext context, WidgetRef ref) {
28+
return FutureBuilder(
29+
future: ref.watch(thingDetailsProvider),
30+
builder: (context, snapshot) {
31+
final List<LinkedThing> linkedThings = snapshot.connectionState ==
32+
ConnectionState.waiting
33+
? []
34+
: ref.watch(linkedThingsProvider) ?? snapshot.data!.linkedThings;
35+
36+
return DetailsCard(
37+
header: CardHeader(
38+
title: 'Linked Things',
39+
trailing: TextButton.icon(
40+
onPressed: () async {
41+
final chosen = await _chooseThings(context);
42+
ref.read(linkedThingsProvider.notifier).state = chosen;
43+
},
44+
label: const Text('Link things'),
45+
icon: const Icon(Icons.add),
46+
),
47+
),
48+
body: linkedThings.isEmpty
49+
? const CardBody(
50+
child: HintText('No linked things.'),
51+
)
52+
: CardBody(
53+
child: Wrap(
54+
spacing: 8,
55+
runSpacing: 8,
56+
children: linkedThings
57+
.map((t) => Chip(
58+
label: Text(t.name),
59+
onDeleted: () {
60+
ref.read(linkedThingsProvider.notifier).state =
61+
linkedThings
62+
.where((lt) => lt.id != t.id)
63+
.toList();
64+
},
65+
))
66+
.toList(),
67+
),
68+
),
69+
);
70+
},
71+
);
72+
}
73+
}

0 commit comments

Comments
 (0)