diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b0b124f..b269b1be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,24 @@ All notable changes to this project will be documented in this file. ## 0.11 ### Added - Filter and sort functionality on _Entity Type List_ - ### Fixed - Missing "Color Dots" of _Entity Type_ (colors are now handled on the Entity Type itself and do no longer rely on the _Map_ Plugin) - 'Jumping' behavior on floating quick access controls on the _Entity Type List_ (copy, duplicate, delete) - Attribute Dependency inverted +## 0.10.2 +### Fixed +- Date Range _importFrom_ did not return a JSON string +- Richtext editor did not trigger the dirty state on entity +- Certainty warning due to _undefined_ value +- Serial and SQL attributes now have the required value (_v_) and functions (_resetFieldState_, _undirtyField_) +### Changed +- Added tests to _ApiDataImporterTest_ to check if the same columns can be used in multiple attributes +- Row error now also sends the column value, instead of just sending the column name +- Dropdowns in the _Data Importer_ are now sorted alphabetically +- The ErrorList component of the Data Importer in the Frontend now preserves text inside `{{...}}` +- Adjusted colors of Markdown modal buttons + ## 0.10.1 ### Added - Option to display attributes in _Data Model Editor_ in groups @@ -19,8 +31,8 @@ All notable changes to this project will be documented in this file. - selects the first choice (if there is **only one choice** in the dropdown)* - selects the exact match (**case insensitive**; e.g. "apple" + `Tab` will select the available choice "apple", but also "Apple")* - nothing and focuses the next attribute (default) - - * Selected elements will be marked with a blue (Tab) badge -- Pressing `Delete` inside _Single Choice Dropdowns_ will clear the element + - * Selected elements will be marked with a blue (Tab) badge +- Pressing `Delete` inside _Single Choice Dropdowns_ will clear the element - Importer now automatically removes BOM if present - Better readable format for error message on validation - Renamed _fromImport_ to _parseImport_ on the attribute classses. The base class now by default imports the passed string, removing redundancies on the string-based classes. diff --git a/app/AttributeTypes/DaterangeAttribute.php b/app/AttributeTypes/DaterangeAttribute.php index b0236905a..788bfd643 100644 --- a/app/AttributeTypes/DaterangeAttribute.php +++ b/app/AttributeTypes/DaterangeAttribute.php @@ -29,10 +29,10 @@ public static function parseImport(int|float|bool|string $data) : mixed { throw InvalidDataException::requireBefore($start, $end); } - return [ - "start" => $start, - "end" => $end, - ]; + return json_encode([ + $start, + $end, + ]); } public static function unserialize(mixed $data) : mixed { diff --git a/app/Import/EntityImporter.php b/app/Import/EntityImporter.php index 56a927415..ad458abf0 100644 --- a/app/Import/EntityImporter.php +++ b/app/Import/EntityImporter.php @@ -25,7 +25,7 @@ class EntityImporter { private $metadata; private array $attributesMap; - private array $attributeAttributeMap = []; + private array $attributeIdToAttributeValue = []; private int $entityTypeId; private string $nameColumn; private ?string $parentColumn = null; @@ -147,7 +147,7 @@ private function verifyEntityType($entityTypeId): bool { private function verifyAttributeMapping($headers): bool { $nameErrors = []; $indexErrors = []; - foreach($this->attributesMap as $attribute => $column) { + foreach($this->attributesMap as $attributeId => $column) { $column = trim($column); if($column == "") { continue; @@ -157,27 +157,26 @@ private function verifyAttributeMapping($headers): bool { array_push($nameErrors, $column); } - $attr = Attribute::find($attribute); + $attr = Attribute::find($attributeId); if(!$attr) { - array_push($indexErrors, $attribute); + array_push($indexErrors, $attributeId); } else { - $this->attributeAttributeMap[$column] = $attr; + $this->attributeIdToAttributeValue[$attributeId] = $attr; } } + $valid = true; if(count($indexErrors) > 0) { $this->resolver->conflict(__("entity-importer.attribute-id-does-not-exist", ["attributes" => implode(", ", $indexErrors)])); + $valid = false; } if(count($nameErrors) > 0) { $this->resolver->conflict(__("entity-importer.attribute-column-does-not-exist", ["columns" => implode(", ", $nameErrors)])); + $valid = false; } - if(count($indexErrors) > 0 || count($nameErrors) > 0) { - return false; - } else { - return true; - } + return $valid; } private function validateName($row, $rowIndex): bool { @@ -233,18 +232,22 @@ private function validateLocation($row, $rowIndex): bool { private function validateAttributesInRow($row, $index): bool { $errors = []; - foreach($this->attributeAttributeMap as $column => $attribute) { + foreach($this->attributeIdToAttributeValue as $attributeId => $attribute) { try { + $column = $this->attributesMap[$attributeId]; $datatype = $attribute->datatype; $attrClass = AttributeBase::getMatchingClass($datatype); $attrClass::fromImport($row[$column]); } catch(Exception $e) { - array_push($errors, $column); + array_push($errors, ["column" => $column, "value" => $row[$column]]); } } if(count($errors) > 0) { - $this->rowConflict($index, "entity-importer.attribute-could-not-be-imported", ["attribute" => implode(", ", $errors)]); + $errorStrings = array_map(function ($error) { + return "{{" . $error['column'] . "}}" . " => " . "{{" . $error['value'] . "}}"; + }, $errors); + $this->rowConflict($index, "entity-importer.attribute-could-not-be-imported", ["attributeErrors" => implode(", ", $errorStrings)]); } return count($errors) == 0; } diff --git a/database/seeders/Demo/EntityAttributesTableSeeder.php b/database/seeders/Demo/EntityAttributesTableSeeder.php index ae6aac1b4..3f2e129b4 100644 --- a/database/seeders/Demo/EntityAttributesTableSeeder.php +++ b/database/seeders/Demo/EntityAttributesTableSeeder.php @@ -245,6 +245,16 @@ public function run() 'position' => 6, 'depends_on' => NULL, ), + 23 => + array ( + 'id' => 26, + 'entity_type_id' => 3, // Fundstelle + 'attribute_id' => 19, // Aufbewahrung [string] + 'created_at' => '2017-12-20 17:00:43', + 'updated_at' => '2017-12-20 17:01:58', + 'position' => 3, + 'depends_on' => NULL, + ), )); } } diff --git a/lang/de/entity-importer.php b/lang/de/entity-importer.php index 7fb38671c..84af00013 100644 --- a/lang/de/entity-importer.php +++ b/lang/de/entity-importer.php @@ -1,4 +1,3 @@ - 'Datei konnte nicht gelesen werden.', 'missing-data' => 'Benötigte Spalte fehlt: :column', 'invalid-data' => 'Ungültige Daten: [:column] => :value', + 'attribute-could-not-be-imported' => 'Attribut konnte nicht importiert werden: :attributeErrors', + 'attribute-id-does-not-exist' => 'Die Attribut-ID existiert nicht: :attributes', + 'attribute-column-does-not-exist' => 'Die Attribut-Spalten existieren nicht: :columns', 'name-column-does-not-exist' => 'Die Spalte für den Namen existiert nicht: :column', 'parent-column-does-non-exist' => 'Die Spalte für die Eltern-Entität existiert nicht: :column', 'parent-entity-does-not-exist' => 'Die Eltern-Entität existiert nicht: :entity', diff --git a/lang/en/entity-importer.php b/lang/en/entity-importer.php index 1cf3d9000..8eb861de7 100644 --- a/lang/en/entity-importer.php +++ b/lang/en/entity-importer.php @@ -5,7 +5,7 @@ 'file-not-found' => 'File could not be read.', 'missing-data' => 'Required column is missing: :column', 'invalid-data' => 'Invalid data: [:column] => :value', - 'attribute-could-not-be-imported' => 'Attribute could not be imported: :attribute', + 'attribute-could-not-be-imported' => 'Attribute could not be imported: :attributeErrors', 'attribute-id-does-not-exist' => 'The attribute id does not exist: :attributes', 'attribute-column-does-not-exist' => 'The attribute columns do not exist: :columns', 'name-column-does-not-exist' => 'The column for the name does not exist: :column', diff --git a/resources/js/components/AttributeList.vue b/resources/js/components/AttributeList.vue index 9ff187e53..99bca48ce 100644 --- a/resources/js/components/AttributeList.vue +++ b/resources/js/components/AttributeList.vue @@ -501,7 +501,7 @@ }; const certainty = attribute => { - return state.attributeValues?.[attribute.id]?.certainty; + return state.attributeValues?.[attribute.id]?.certainty ?? null; }; const hasComment = attribute => { diff --git a/resources/js/components/attribute/Attribute.vue b/resources/js/components/attribute/Attribute.vue index 6f7d5f50e..99ec6efe8 100644 --- a/resources/js/components/attribute/Attribute.vue +++ b/resources/js/components/attribute/Attribute.vue @@ -64,6 +64,7 @@ { + return !arr || arr.length === 2; + }, required: true, }, }, @@ -56,15 +58,15 @@ disabled, value, } = toRefs(props); - // FETCH - // FUNCTIONS - const strToDate = str => { - return new Date(str); + // FETCH + const fixValue = _ => { + return value.value?.map(dt => new Date(dt)); }; + const resetFieldState = _ => { v.resetField({ - value: value.value?.map(dt => strToDate(dt)), + value: fixValue(), }); }; const undirtyField = _ => { @@ -78,7 +80,7 @@ return new Date(date.getTime() - (date.getTimezoneOffset()*60*1000)); }); v.handleChange(correctValue); - } + }; // DATA const { @@ -87,7 +89,7 @@ meta, resetField, } = useField(`daterange_${name.value}`, yup.array(), { - initialValue: value.value?.map(dt => strToDate(dt)), + initialValue: fixValue(), }); const state = reactive({ @@ -99,7 +101,6 @@ resetField, }); - watch(_ => value, (newValue, oldValue) => { resetFieldState(); }); diff --git a/resources/js/components/attribute/Richtext.vue b/resources/js/components/attribute/Richtext.vue index c3577f9c4..d37be4a1e 100644 --- a/resources/js/components/attribute/Richtext.vue +++ b/resources/js/components/attribute/Richtext.vue @@ -5,10 +5,10 @@ @mouseleave="state.hovered = false" >
{ - current.value = text || ''; - meta.dirty = true; - meta.valid = true; + v.value = text || ''; + v.meta.dirty = true; + v.meta.valid = true; context.emit('change', { - dirty: meta.dirty, - valid: meta.valid, - value: current.value, + dirty: v.meta.dirty, + valid: v.meta.valid, + value: v.value, }); }; const resetFieldState = _ => { - current.value = initial.value || ''; + v.value = initial.value || ''; undirtyField(); }; const undirtyField = _ => { - meta.dirty = false; + v.meta.dirty = false; }; watch(initial, _ => { resetFieldState(); }); - const openMdEditor = _ => { - showMarkdownEditor(current.value, text => { + showMarkdownEditor(v.value, text => { handleInput(text); }); }; @@ -119,24 +117,18 @@ } }), }); - const current = ref(initial.value || ''); - const meta = reactive({ - dirty: false, - valid: true, - }); /** * v is required as the attr-list fetches * the values of the attributes via every * attribute's v.value. */ - const v = computed(_ => { - return { - value: current.value, - meta: { - ...meta - } - }; + const v = reactive({ + value: initial.value || '', + meta: { + dirty: false, + valid: true, + } }); // RETURN @@ -147,7 +139,6 @@ undirtyField, openMdEditor, // STATE - current, state, v, }; diff --git a/resources/js/components/attribute/Serial.vue b/resources/js/components/attribute/Serial.vue index b53817fab..00896d9a1 100644 --- a/resources/js/components/attribute/Serial.vue +++ b/resources/js/components/attribute/Serial.vue @@ -12,7 +12,6 @@ diff --git a/resources/js/components/attribute/Sql.vue b/resources/js/components/attribute/Sql.vue index 989987904..ed32ba9f6 100644 --- a/resources/js/components/attribute/Sql.vue +++ b/resources/js/components/attribute/Sql.vue @@ -63,28 +63,26 @@ }, }, setup(props, context) { - const { - name, - disabled, - value, - } = toRefs(props); - // FETCH - - // FUNCTIONS - - // DATA - const state = reactive({ - + const v = reactive({ + value: props.value, + meta: { + dirty: false, + valid: true, + }, + resetField: _ => true, }); + + const resetFieldState = _ => {}; + const undirtyField = _ => {}; // RETURN return { // HELPERS + v, isArray, translateConcept, - // LOCAL - // STATE - state, + resetFieldState, + undirtyField, }; }, }; diff --git a/resources/js/components/error/ErrorList.vue b/resources/js/components/error/ErrorList.vue index 5f8435ad2..987934d9e 100644 --- a/resources/js/components/error/ErrorList.vue +++ b/resources/js/components/error/ErrorList.vue @@ -35,29 +35,44 @@ required: true } }, - setup(props){ + setup(props) { const header = computed(_ => { return props.value.split(props.headerSeparator)[0].trim(); }); const items = computed(_ => { + // Replace text inside double curly braces with placeholders + const preservationRegex = RegExp('{{(.*?)}}', 'g'); + const variables = {}; + let counter = 1; + const preservationMatches = props.value.replace(preservationRegex, (match, p1 = '') => { + const key = `$${counter++}`; + variables[key] = p1; + return key; + }); - const headerParts = props.value.split(props.headerSeparator); - headerParts.shift(); + const [header, ...body] = preservationMatches.split(props.headerSeparator); - const parts = headerParts.join(props.separator); - return parts.split(props.separator); + const joinedBody = body.join(props.headerSeparator).trim(); + let lines = joinedBody.split(props.separator); + lines = lines.map((line, idx) => { + let result = line; + for(const [key, value] of Object.entries(variables)) { + result = result.replace(key, value); + } + return result; + }); + return lines; }); const hasItems = computed(_ => { - return items.value.length > 0 && items.value[0].trim() !== ''; + return items.value.length > 0 && items.value[0] && items.value[0].trim() !== ''; }); - return { hasItems, header, - items + items, }; } }; diff --git a/resources/js/components/modals/system/MarkdownEditor.vue b/resources/js/components/modals/system/MarkdownEditor.vue index ac23f10b8..9d8b39515 100644 --- a/resources/js/components/modals/system/MarkdownEditor.vue +++ b/resources/js/components/modals/system/MarkdownEditor.vue @@ -31,7 +31,7 @@ - diff --git a/resources/js/components/tools/importer/EntityImporterSettings.vue b/resources/js/components/tools/importer/EntityImporterSettings.vue index a15fd7d64..ed804c9f5 100644 --- a/resources/js/components/tools/importer/EntityImporterSettings.vue +++ b/resources/js/components/tools/importer/EntityImporterSettings.vue @@ -14,7 +14,7 @@ :hide-selected="true" :label="'thesaurus_url'" :object="true" - :options="availableEntityTypes" + :options="sortedAvailableEntityTypes" :placeholder="t('global.select.placeholder')" :searchable="true" :track-by="'id'" @@ -24,11 +24,11 @@ @change="value => $emit('update:entityType', value)" > @@ -53,7 +53,7 @@ :classes="multiselectResetClasslist" :disabled="disabled" :hide-selected="true" - :options="availableColumns" + :options="sortedAvailableColumns" :placeholder="t('global.select.placeholder')" :searchable="true" :value="entityName" @@ -79,7 +79,7 @@ :disabled="disabled" :hide-selected="true" :value="entityParent" - :options="availableColumns" + :options="sortedAvailableColumns" :placeholder="t('global.select.placeholder')" :searchable="true" :append-to-body="true" @@ -90,6 +90,7 @@