From 24deaeab06b47de7d48cfef601f0059050571b85 Mon Sep 17 00:00:00 2001
From: Vinzenz Rosenkranz
Date: Fri, 6 Dec 2024 11:34:04 +0100
Subject: [PATCH] allow references on entity (w/o attribute)
Signed-off-by: Vinzenz Rosenkranz
---
CHANGELOG.md | 8 +-
app/Http/Controllers/ReferenceController.php | 36 ++--
app/Reference.php | 4 +-
...5_125736_enable_reference_for_entities.php | 38 +++++
resources/js/api.js | 12 +-
resources/js/bootstrap/stores/entity.js | 20 ++-
resources/js/components/MainView.vue | 126 ++++++++++----
.../components/bibliography/ReferenceForm.vue | 156 ++++++++++++++++++
.../js/components/modals/entity/Reference.vue | 123 +-------------
resources/js/i18n/de.json | 1 +
resources/js/i18n/en.json | 1 +
routes/api.php | 2 +-
12 files changed, 352 insertions(+), 175 deletions(-)
create mode 100644 database/migrations/2024_12_05_125736_enable_reference_for_entities.php
create mode 100644 resources/js/components/bibliography/ReferenceForm.vue
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc173a4f5..f6a93b24f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.
+## 0.11
+### Added
+- References can also be added to an Entity, not only to an Entity Attribute
+
## 0.10.1
### Added
- Option to display attributes in _Data Model Editor_ in groups
@@ -10,8 +14,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/Http/Controllers/ReferenceController.php b/app/Http/Controllers/ReferenceController.php
index e1532a678..157826456 100644
--- a/app/Http/Controllers/ReferenceController.php
+++ b/app/Http/Controllers/ReferenceController.php
@@ -40,7 +40,12 @@ public function getByEntity($id) {
$groupedReferences = [];
foreach($references as $r) {
- $key = $r->attribute->thesaurus_url;
+ if(isset($r->attribute)) {
+ $key = $r->attribute->thesaurus_url;
+ } else {
+ $key = 'on_entity';
+ }
+
if(!isset($groupedReferences[$key])) {
$groupedReferences[$key] = [];
}
@@ -53,7 +58,7 @@ public function getByEntity($id) {
// POST
- public function addReference(Request $request, $id, $aid) {
+ public function addReference(Request $request, int $id, ?int $aid = null) {
$user = auth()->user();
if(!$user->can('entity_data_create')) {
return response()->json([
@@ -69,18 +74,25 @@ public function addReference(Request $request, $id, $aid) {
'error' => __('This entity does not exist')
], 400);
}
- try {
- Attribute::findOrFail($aid);
- } catch(ModelNotFoundException $e) {
- return response()->json([
- 'error' => __('This attribute does not exist')
- ], 400);
- }
- $props = array_merge([
+ $data = [
'entity_id' => $id,
- 'attribute_id' => $aid
- ], $request->only(array_keys(Reference::rules)));
+ ];
+ if(isset($aid)) {
+ try {
+ Attribute::findOrFail($aid);
+ $data['attribute_id'] = $aid;
+ } catch(ModelNotFoundException $e) {
+ return response()->json([
+ 'error' => __('This attribute does not exist')
+ ], 400);
+ }
+ }
+
+ $props = array_merge(
+ $data,
+ $request->only(array_keys(Reference::rules))
+ );
$reference = Reference::add($props, $user);
return response()->json($reference, 201);
}
diff --git a/app/Reference.php b/app/Reference.php
index 08eea1409..95e658d95 100644
--- a/app/Reference.php
+++ b/app/Reference.php
@@ -26,11 +26,11 @@ class Reference extends Model
const rules = [
'bibliography_id' => 'required|integer|exists:bibliography,id',
- 'description' => 'string|nullable'
+ 'description' => 'required|string'
];
const patchRules = [
- 'description' => 'string|nullable'
+ 'description' => 'required|string'
];
public function getActivitylogOptions() : LogOptions
diff --git a/database/migrations/2024_12_05_125736_enable_reference_for_entities.php b/database/migrations/2024_12_05_125736_enable_reference_for_entities.php
new file mode 100644
index 000000000..9601b6136
--- /dev/null
+++ b/database/migrations/2024_12_05_125736_enable_reference_for_entities.php
@@ -0,0 +1,38 @@
+disableLogging();
+
+ Schema::table('references', function (Blueprint $table) {
+ $table->integer('attribute_id')->nullable()->change();
+ });
+
+ activity()->enableLogging();
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ activity()->disableLogging();
+
+ Reference::whereNull('attribute_id')->delete();
+ Schema::table('references', function (Blueprint $table) {
+ $table->integer('attribute_id')->nullable(false)->change();
+ });
+
+ activity()->enableLogging();
+ }
+};
diff --git a/resources/js/api.js b/resources/js/api.js
index 49c6d8eb6..0ed0b8432 100644
--- a/resources/js/api.js
+++ b/resources/js/api.js
@@ -491,9 +491,15 @@ export async function addEntityTypeAttribute(etid, aid, rank) {
}
export async function addReference(eid, aid, data) {
- return $httpQueue.add(
- () => http.post(`/entity/${eid}/reference/${aid}`, data).then(response => response.data)
- );
+ if(aid) {
+ return $httpQueue.add(
+ () => http.post(`/entity/${eid}/reference/${aid}`, data).then(response => response.data)
+ );
+ } else {
+ return $httpQueue.add(
+ () => http.post(`/entity/${eid}/reference`, data).then(response => response.data)
+ );
+ }
}
export async function getFilteredActivity(pageUrl, payload) {
diff --git a/resources/js/bootstrap/stores/entity.js b/resources/js/bootstrap/stores/entity.js
index e1e1f877d..7da773cf1 100644
--- a/resources/js/bootstrap/stores/entity.js
+++ b/resources/js/bootstrap/stores/entity.js
@@ -60,7 +60,7 @@ function updateSelectionTypeIdList(selection) {
const handleAddEntityType = (context, typeData, attributes = []) => {
context.entityTypeAttributes[typeData.id] = attributes.slice();
context.entityTypes[typeData.id] = typeData;
-}
+};
const handlePostDelete = (context, entityId) => {
const currentRoute = router.currentRoute.value;
@@ -87,7 +87,7 @@ const handlePostDelete = (context, entityId) => {
}
}
}
-}
+};
export const useEntityStore = defineStore('entity', {
state: _ => ({
@@ -180,7 +180,7 @@ export const useEntityStore = defineStore('entity', {
colors = state.entityTypeColors[id];
}
return colors;
- }
+ };
},
getEntityTypeName(state) {
return id => {
@@ -512,8 +512,8 @@ export const useEntityStore = defineStore('entity', {
}
}
- // Remove the data from the entity.
- // We need to do this as the 'replace', 'add' 'remove'
+ // Remove the data from the entity.
+ // We need to do this as the 'replace', 'add' 'remove'
// operations are calculated based on this value.
for(const attributeId in removedData) {
if(entity.data[attributeId]) {
@@ -544,12 +544,17 @@ export const useEntityStore = defineStore('entity', {
});
},
handleReference(entityId, attributeUrl, action, data) {
+ const entity = this.getEntity(entityId);
+ let references;
+ if(attributeUrl) {
+ references = entity?.references[attributeUrl] || [];
+ } else {
+ references = entity?.references.on_entity || [];
+ }
if(action == 'add') {
- const references = this.getEntity(entityId)?.references[attributeUrl] || [];
references.push(data);
return data;
} else if(action == 'update') {
- const references = this.getEntity(entityId)?.references[attributeUrl] || [];
const id = data.id;
const refData = data.data;
const updateData = data.updates;
@@ -561,7 +566,6 @@ export const useEntityStore = defineStore('entity', {
reference.updated_at = updateData.updated_at;
}
} else if(action == 'delete') {
- const references = this.getEntity(entityId)?.references[attributeUrl] || [];
const idx = references.findIndex(ref => ref.id == data.id);
if(idx > -1) {
references.splice(idx, 1);
diff --git a/resources/js/components/MainView.vue b/resources/js/components/MainView.vue
index 60fb01ceb..e5005703d 100644
--- a/resources/js/components/MainView.vue
+++ b/resources/js/components/MainView.vue
@@ -71,39 +71,70 @@
>
{{ t('main.entity.references.empty') }}
-
-
+ {{ t('main.entity.references.general') }}
+
+
+
+
+
+ {{ date(reference.updated_at) }}
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- {{ date(reference.updated_at) }}
-
-
-
+
+
+
+
+
+
+ {{ date(reference.updated_at) }}
+
+
+
+
-
+
@@ -129,6 +160,7 @@
useRoute,
} from 'vue-router';
+ import useBibliographyStore from '@/bootstrap/stores/bibliography.js';
import useEntityStore from '@/bootstrap/stores/entity.js';
import useSystemStore from '@/bootstrap/stores/system.js';
import router from '%router';
@@ -170,15 +202,18 @@
import { useToast } from '@/plugins/toast.js';
import Quotation from '@/components/bibliography/Quotation.vue';
+ import ReferenceForm from '@/components/bibliography/ReferenceForm.vue';
export default {
components: {
Quotation,
+ ReferenceForm,
},
setup(props, context) {
const { t } = useI18n();
const currentRoute = useRoute();
const toast = useToast();
+ const bibliographyStore = useBibliographyStore();
const entityStore = useEntityStore();
const systemStore = useSystemStore();
@@ -195,6 +230,9 @@
const isTab = id => {
return state.tab == id;
};
+ const addEntityReference = data => {
+ entityStore.addReference(state.entity.id, null, null, data);
+ };
const showMetadataForReferenceGroup = referenceGroup => {
if(!referenceGroup) return;
if(!state.entity) return;
@@ -238,13 +276,38 @@
}),
concepts: computed(_ => systemStore.concepts),
entity: computed(_ => entityStore.selectedEntity),
- hasReferences: computed(_ => {
+ hasEntityReferences: computed(_ => {
const isNotSet = !state.entity.references;
if(isNotSet) return false;
const isEmpty = !Object.keys(state.entity.references).length > 0;
if(isEmpty) return false;
- return Object.values(state.entity.references).some(v => v.length > 0);
+ return state.entity.references.on_entity?.length > 0;
+ }),
+ hasAttributeReferences: computed(_ => {
+ const isNotSet = !state.entity.references;
+ if(isNotSet) return false;
+
+ const {
+ on_entity,
+ ...refs
+ } = state.entity.references;
+ const isEmpty = !Object.keys(refs).length > 0;
+ if(isEmpty) return false;
+ return Object.values(refs).some(v => v.length > 0);
+ }),
+ hasReferences: computed(_ => state.hasEntityReferences || state.hasAttributeReferences),
+ entityReferences: computed(_ => state.hasEntityReferences ? state.entity.references.on_entity : []),
+ attributeReferences: computed(_ => {
+ if(state.hasAttributeReferences) {
+ const {
+ on_entity,
+ ...refs
+ } = state.entity.references;
+ return refs;
+ } else {
+ return {};
+ }
}),
entityTypes: computed(_ => entityStore.entityTypes),
columnPref: computed(_ => systemStore.getPreference('prefs.columns')),
@@ -289,6 +352,7 @@
// LOCAL
setTab,
isTab,
+ addEntityReference,
showMetadataForReferenceGroup,
openLiteratureInfo,
// STATE
diff --git a/resources/js/components/bibliography/ReferenceForm.vue b/resources/js/components/bibliography/ReferenceForm.vue
new file mode 100644
index 000000000..3f3ef257f
--- /dev/null
+++ b/resources/js/components/bibliography/ReferenceForm.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/components/modals/entity/Reference.vue b/resources/js/components/modals/entity/Reference.vue
index 90aeac6f5..dd1098ec4 100644
--- a/resources/js/components/modals/entity/Reference.vue
+++ b/resources/js/components/modals/entity/Reference.vue
@@ -161,73 +161,9 @@
{{ t('main.entity.references.bibliography.add') }}
-
+