Skip to content

Commit

Permalink
Fix issues around species creation & assignment (#139)
Browse files Browse the repository at this point in the history
* Remove buggy flag causing species field to not reload

* Fix detecting duplicates as the parent key was invalid

* Ask user if they want to create a species before
  • Loading branch information
adzialocha authored Jun 4, 2024
1 parent f5a744a commit 24b4859
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 55 deletions.
4 changes: 4 additions & 0 deletions packages/app/lib/locales/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
"sightingScreenTitle": "Sighting",
"sightingUnspecified": "Unknown species",
"speciesCardTitle": "Species",
"speciesCreateAbortButton": "Cancel",
"speciesCreateConfirmButton": "Create",
"speciesCreateDialogTitle": "Create new species?",
"speciesCreateMessage": "This action will create a new species \"{taxon}\" in the database",
"speciesDeleteAlertBody": "Are you sure you want to delete this species?",
"speciesDeleteAlertCancel": "Cancel",
"speciesDeleteAlertConfirm": "Delete",
Expand Down
4 changes: 4 additions & 0 deletions packages/app/lib/locales/app_pt.arb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
"sightingScreenTitle": "Observação",
"sightingUnspecified": "Espécie desconhecida",
"speciesCardTitle": "Espécie",
"speciesCreateAbortButton": "Cancelar",
"speciesCreateConfirmButton": "Criar",
"speciesCreateDialogTitle": "Criar nova espécie?",
"speciesCreateMessage": "Esta ação criará uma nova espécie \"{taxon}\" no banco de dados",
"speciesDeleteAlertBody": "Tem certeza de que deseja excluir esta espécie?",
"speciesDeleteAlertCancel": "Cancelar",
"speciesDeleteAlertConfirm": "Excluir",
Expand Down
55 changes: 21 additions & 34 deletions packages/app/lib/models/taxonomy_species.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:p2panda/p2panda.dart';

import 'package:app/io/graphql/graphql.dart';
import 'package:app/io/graphql/queries.dart';
import 'package:app/io/p2panda/publish.dart' as publish;
import 'package:app/io/p2panda/schemas.dart';
import 'package:app/models/base.dart';
Expand Down Expand Up @@ -238,9 +237,11 @@ String getTaxonomy(int rank, publish.DocumentId documentId) {
}

String searchTaxon(SchemaId schemaId, String query,
{bool strict = false, publish.DocumentId? parentId}) {
{bool strict = false, publish.DocumentId? parentId, String? parentField}) {
final op = strict ? "eq" : "contains";
final parentStr = parentId != null ? "parent: { eq: \"$parentId\" }," : "";

final parentStr =
parentId != null ? "$parentField: { eq: \"$parentId\" }," : "";

return '''
query SearchTaxon {
Expand All @@ -261,48 +262,34 @@ String searchTaxon(SchemaId schemaId, String query,
''';
}

Future<publish.DocumentViewId> createTaxon(SchemaId schemaId,
Future<TaxonomySpecies?> checkIfDuplicateTaxonExists(SchemaId schemaId,
{required String name, publish.DocumentId? parentId}) async {
List<(String, OperationValue)> fields = [
("name", OperationValue.string(name)),
];

if (parentId != null) {
final rank = RANKS.firstWhere((element) => element['schemaId'] == schemaId);
if (rank['parent'] != null) {
fields.add((rank['parent']!, OperationValue.relation(parentId)));
}
}

return await publish.create(schemaId, fields);
}

/// Safely create a new taxon instance if we're not aware yet of one with
/// the same "name" and "parent" value.
Future<publish.DocumentViewId> createDeduplicatedTaxon(SchemaId schemaId,
{required String name, publish.DocumentId? parentId}) async {
// Check if duplicate with same name and parent exists
// All ranks have a parent, except the last one
publish.DocumentId? parentIdFilter;
String? parentField;
if (parentId != null) {
final rank = RANKS.firstWhere((element) => element['schemaId'] == schemaId);
if (rank['parent'] != null) {
parentIdFilter = parentId;
parentField = rank['parent'];
}
}

final response = await client.query(QueryOptions(
document: gql(searchTaxon(schemaId, name,
strict: true, parentId: parentIdFilter))));

if (!response.hasException) {
final documents =
response.data![DEFAULT_RESULTS_KEY]['documents'] as List<dynamic>;
// Check if duplicate with same name and parent exists
final taxonQuery = searchTaxon(schemaId, name,
strict: true, parentId: parentIdFilter, parentField: parentField);
final response = await query(query: taxonQuery);
final documents = response[DEFAULT_RESULTS_KEY]['documents'] as List<dynamic>;

if (documents.isNotEmpty) {
return documents[0]['meta']['viewId'] as String;
}
if (documents.isNotEmpty) {
return TaxonomySpecies.fromJson(documents[0] as Map<String, dynamic>);
}

return null;
}

Future<publish.DocumentViewId> createTaxon(SchemaId schemaId,
{required String name, publish.DocumentId? parentId}) async {
// Create Taxon if duplicate was not found
List<(String, OperationValue)> fields = [
("name", OperationValue.string(name)),
Expand Down
82 changes: 61 additions & 21 deletions packages/app/lib/ui/widgets/species_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:app/ui/colors.dart';
import 'package:app/ui/widgets/action_buttons.dart';
import 'package:app/ui/widgets/alert_dialog.dart';
import 'package:app/ui/widgets/autocomplete.dart';
import 'package:app/ui/widgets/confirm_dialog.dart';
import 'package:app/ui/widgets/editable_card.dart';
import 'package:app/ui/widgets/read_only_value.dart';
import 'package:app/ui/widgets/taxonomy_autocomplete.dart';
Expand Down Expand Up @@ -64,10 +65,6 @@ class _SpeciesFieldState extends State<SpeciesField> {
}).toList(growable: false);
}

/// Do we need to to request data from the node? Needs to be true for initial
/// load.
bool _dirty = true;

/// Flag indicating if we're currently editing or not.
bool _isEditMode = false;

Expand Down Expand Up @@ -150,6 +147,28 @@ class _SpeciesFieldState extends State<SpeciesField> {
}

Future<void> _submit() async {
final t = AppLocalizations.of(context)!;

Future<bool> askUserToConfirm(String taxon) async {
final value = await showDialog<bool>(
context: context,
builder: (BuildContext context) => ConfirmDialog(
title: t.speciesCreateDialogTitle,
message: t.speciesCreateMessage(taxon),
labelAbort: t.speciesCreateAbortButton,
labelConfirm: t.speciesCreateConfirmButton,
onConfirm: () {
Navigator.pop(context, true);
},
onAbort: () {
Navigator.pop(context, false);
},
),
);

return value ?? false;
}

if (widget.current == null) {
if (_taxonomy[0] == null || _taxonomy[0]!.value == '') {
// Nothing has changed, therefore do nothing
Expand All @@ -167,17 +186,49 @@ class _SpeciesFieldState extends State<SpeciesField> {
}
}

// Create all taxons which do not exist yet
// Check if this action will create a new "Species" and ask the user if they
// want to proceed with this!
if (_taxonomy.first?.documentId == null) {
final duplicateSpeciesTaxon = await checkIfDuplicateTaxonExists(
_taxonomySettings[0]['schemaId']!,
name: _taxonomy.first!.value);

if (duplicateSpeciesTaxon == null) {
final userConfirmed = await askUserToConfirm(_taxonomy.first!.value);
if (!userConfirmed) {
_reset();
return;
}
}
}

// Create all taxons which do not exist yet after checking for duplicates
AutocompleteItem? parent;
for (final (index, item) in _taxonomy.reversed.indexed) {
final rank = _taxonomySettings[_taxonomy.length - index - 1];

// Autocomplete could not find an already existing item
if (item!.documentId == null) {
// This method checks if a duplicate with similar values already exists
final id = await createDeduplicatedTaxon(rank['schemaId']!,
name: item.value, parentId: parent?.documentId);
parent =
AutocompleteItem(value: item.value, documentId: id, viewId: id);
// .. to be sure we're checking again if the node knows about a duplicate
final duplicateTaxon = await checkIfDuplicateTaxonExists(
rank['schemaId']!,
name: item.value,
parentId: parent?.documentId);

if (duplicateTaxon != null) {
parent = AutocompleteItem(
value: duplicateTaxon.name,
documentId: duplicateTaxon.id,
viewId: duplicateTaxon.viewId);
} else {
// Create new taxon if duplicate has not been found
final viewId = await createTaxon(rank['schemaId']!,
name: item.value, parentId: parent?.documentId);
parent = AutocompleteItem(
value: item.value, documentId: viewId, viewId: viewId);
}
} else {
// Use taxon document directly suggested by autocomplete
parent = item;
}
}
Expand Down Expand Up @@ -247,13 +298,6 @@ class _SpeciesFieldState extends State<SpeciesField> {
}

void _reset() {
// Do not reload everything if nothing has been changed
if (!_dirty) {
return;
}

_dirty = false;

setState(() {
_taxonomy = List.filled(9, null, growable: false);
_showUpToRank = 1;
Expand Down Expand Up @@ -298,17 +342,13 @@ class _SpeciesFieldState extends State<SpeciesField> {
rank['schemaId']!,
_taxonomy[index],
onSubmit: () {
_dirty = true;

// Automatically submit final value if there's nothing more to
// fill out
if (_showUpToRank - 1 == index) {
_handleSubmit();
}
},
onChanged: (AutocompleteItem value) {
_dirty = true;

// Set current state to the edited value
if (value.value == '') {
_taxonomy[index] = null;
Expand Down

0 comments on commit 24b4859

Please sign in to comment.