Skip to content

Commit

Permalink
Add preliminary specification editor
Browse files Browse the repository at this point in the history
  • Loading branch information
backspace committed Sep 29, 2024
1 parent 1ed1678 commit 90cd543
Show file tree
Hide file tree
Showing 4 changed files with 446 additions and 8 deletions.
33 changes: 25 additions & 8 deletions waydowntown_app/lib/tools/my_specifications_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:waydowntown/app.dart';
import 'package:waydowntown/models/specification.dart';
import 'package:waydowntown/routes/request_run_route.dart';
import 'package:waydowntown/widgets/edit_specification_widget.dart';

class MySpecificationsTable extends StatefulWidget {
final Dio dio;
Expand Down Expand Up @@ -137,16 +138,32 @@ class _MySpecificationsTableState extends State<MySpecificationsTable> {
DataCell(Text(spec.answers?.length.toString() ?? '0')),
DataCell(_truncatedText(spec.startDescription)),
DataCell(_truncatedText(spec.taskDescription)),
DataCell(IconButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => RequestRunRoute(
dio: widget.dio,
specificationId: spec.id,
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => RequestRunRoute(
dio: widget.dio,
specificationId: spec.id,
),
),
),
icon: const Icon(Icons.play_arrow),
),
),
icon: const Icon(Icons.play_arrow),
IconButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => EditSpecificationWidget(
dio: widget.dio,
specification: spec,
),
),
),
icon: const Icon(Icons.edit),
),
],
)),
],
)));
Expand Down
234 changes: 234 additions & 0 deletions waydowntown_app/lib/widgets/edit_specification_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:waydowntown/app.dart';
import 'package:waydowntown/models/specification.dart';
import 'package:yaml/yaml.dart';

class EditSpecificationWidget extends StatefulWidget {
final Dio dio;
final Specification specification;

const EditSpecificationWidget({
super.key,
required this.dio,
required this.specification,
});

@override
EditSpecificationWidgetState createState() => EditSpecificationWidgetState();
}

class EditSpecificationWidgetState extends State<EditSpecificationWidget> {
late TextEditingController _startDescriptionController;
late TextEditingController _taskDescriptionController;
late TextEditingController _durationController;
String? _selectedConcept;
Map<String, String> _fieldErrors = {};

@override
void initState() {
super.initState();
_startDescriptionController =
TextEditingController(text: widget.specification.startDescription);
_taskDescriptionController =
TextEditingController(text: widget.specification.taskDescription);
_durationController = TextEditingController(
text: widget.specification.duration?.toString() ?? '');
_selectedConcept = widget.specification.concept;
}

Future<dynamic> _loadConcepts(context) async {
final yamlString =
await DefaultAssetBundle.of(context).loadString('assets/concepts.yaml');
final yamlMap = loadYaml(yamlString);
return yamlMap;
}

@override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: _loadConcepts(context),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

return Scaffold(
appBar: AppBar(
title: const Text('Edit Specification'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildConceptDropdown(snapshot.data),
_buildTextField('Start Description',
_startDescriptionController, 'start_description'),
_buildTextField('Task Description',
_taskDescriptionController, 'task_description'),
_buildDurationField(),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _saveSpecification,
child: const Text('Save'),
),
],
),
],
),
),
),
);
});
}

Widget _buildConceptDropdown(dynamic concepts) {
Map<String, String> conceptKeyToName = {};

for (dynamic concept in concepts.keys) {
conceptKeyToName[concept.toString()] =
concepts[concept]['name'] ?? concept.toString();
}

return DropdownButtonFormField<String>(
value: _selectedConcept,
decoration: InputDecoration(
labelText: 'Concept',
errorText: _fieldErrors['concept'],
),
items: conceptKeyToName.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedConcept = newValue;
});
},
);
}

Widget _buildTextField(
String label, TextEditingController controller, String fieldName) {
return TextField(
controller: controller,
decoration: InputDecoration(
labelText: label,
errorText: _fieldErrors[fieldName],
),
);
}

Widget _buildDurationField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _durationController,
decoration: InputDecoration(
labelText: 'Duration (seconds)',
errorText: _fieldErrors['duration'],
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () => _setDuration(30),
child: const Text('30s'),
),
ElevatedButton(
onPressed: () => _setDuration(60),
child: const Text('1m'),
),
ElevatedButton(
onPressed: () => _setDuration(120),
child: const Text('2m'),
),
],
),
],
);
}

void _setDuration(int seconds) {
setState(() {
_durationController.text = seconds.toString();
});
}

Future<void> _saveSpecification() async {
try {
final prefs = await SharedPreferences.getInstance();
final authToken = prefs.getString('access_token');

if (authToken == null) {
throw Exception('Auth token not found');
}

final response = await widget.dio.patch(
'/waydowntown/specifications/${widget.specification.id}',
data: {
'data': {
'type': 'specifications',
'id': widget.specification.id,
'attributes': {
'concept': _selectedConcept,
'start_description': _startDescriptionController.text,
'task_description': _taskDescriptionController.text,
'duration': int.tryParse(_durationController.text),
},
},
},
options: Options(
headers: {'Authorization': authToken},
),
);

if (response.statusCode == 200 && mounted) {
Navigator.of(context).pop(true);
}
} on DioException catch (e) {
talker.error('Error updating specification: $e');
if (e.response?.statusCode == 422) {
final errors = e.response?.data['errors'] as List<dynamic>;
setState(() {
_fieldErrors = {};

for (var error in errors) {
final field = error['source']['pointer'].split('/').last;
_fieldErrors[field] = error['detail'];
}
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to update specification')),
);
}
}
}

@override
void dispose() {
_startDescriptionController.dispose();
_taskDescriptionController.dispose();
_durationController.dispose();
super.dispose();
}
}
1 change: 1 addition & 0 deletions waydowntown_app/test/test_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class TestHelpers {
placed: true,
concept: concept,
startDescription: start,
taskDescription: description,
answers: answers ??
[
const Answer(id: '1', label: 'Answer 1'),
Expand Down
Loading

0 comments on commit 90cd543

Please sign in to comment.