Skip to content

Commit

Permalink
Entity Bulk Add (copy Attributes) (#196) (#201)
Browse files Browse the repository at this point in the history
* New tab in edit entity - Add Entity (copy Attributes)
* Transformed "Add Entity" (copy Attributes) to "Bulk Add" (copy Attributes) via new component EntityBulkAdd
* Refactored ReferencedEntitySelect: 
  * Removed selected watcher because of a race condition and result of duplication
  * Replace activated with mounted, so references can load again after save
  • Loading branch information
TeodoraPavlova authored Apr 23, 2024
1 parent 54e14da commit df11fa3
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 15 deletions.
20 changes: 15 additions & 5 deletions frontend/src/components/Entity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import EntityForm from "@/components/inputs/EntityForm";
import Changes from "@/components/change_review/Changes";
import Tabbing from "@/components/layout/Tabbing";
import PermissionList from "@/components/auth/PermissionList";
import EntityBulkAdd from "@/components/EntityBulkAdd";
export default {
name: "Entity",
Expand All @@ -42,6 +43,12 @@ export default {
icon: "mode_edit",
tooltip: "Edit/show entity details"
},
{
name: "Bulk Add (copy Attributes)",
component: markRaw(EntityBulkAdd),
icon: "add_circle",
tooltip: "Copy over entity attributes to new entities"
},
{
name: "Permissions",
component: markRaw(PermissionList),
Expand All @@ -63,14 +70,15 @@ export default {
},
bindArgs() {
return [
{ schema: this.activeSchema, entity: this.entity },
{ schema: this.activeSchema, entity: this.entity },
{ objectType: "Entity", objectId: this.entity?.id },
{ schema: this.activeSchema, entitySlug: this.$route.params.entitySlug },
]
},
},
methods: {
async updateEntity() {
async getEntity() {
if (this.$route.params.entitySlug && this.$route.params.schemaSlug) {
const params = {
schemaSlug: this.$route.params.schemaSlug,
Expand All @@ -81,12 +89,14 @@ export default {
this.entity = null;
}
},
async onUpdate() {
await this.updateEntity();
async onUpdate(entity) {
if (entity) {
this.entity = entity;
}
}
},
async activated() {
await this.updateEntity();
await this.getEntity();
},
watch: {
entity(newValue) {
Expand All @@ -95,7 +105,7 @@ export default {
}
},
$route: {
handler: "updateEntity",
handler: "getEntity",
immediate: true
},
}
Expand Down
122 changes: 122 additions & 0 deletions frontend/src/components/EntityBulkAdd.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<template v-for="(form, index) in entityForms" :key="form.ref">
<div :id="`form-${index}`">
<div :class="`${entityForms.length < 2 && 'd-none'} ${index > 0 && 'mt-5 border-top border-light'} row`">
<div class="col">
<button type="button" :class="`btn-close float-end ${index > 0 ? 'my-3': 'mb-3'}`"
@click="closeForm(`form-${index}`, form.ref)"/>
</div>
</div>
<component :is="form.component" @save-all="saveAll" v-bind="form.props"
:ref="el => { entityFormRefs[form.ref] = el }"/>
</div>
</template>
<div class="container mt-2">
<button class="btn btn-outline-secondary w-100" @click="addNewItem">
<i class='eos-icons'>add_circle</i>
Add more
</button>
</div>
</template>

<script setup>
import {markRaw, ref} from "vue";
import EntityForm from "@/components/inputs/EntityForm.vue";
import _cloneDeep from "lodash/cloneDeep";
const props = defineProps({
schema: {
type: Object,
required: true
},
entity: {
type: Object,
required: true,
}
});
const entityData = ref(prepEntity());
const entityForms = ref([generateEntityForm(0)]);
const entityFormRefs = ref([]);
function generateEntityForm(id) {
return {
component: markRaw(EntityForm),
ref: `entity-form-${id}`,
props: {
entity: entityData,
schema: props.schema,
batchMode: true,
showAttributeCheckboxes: true,
}
}
}
async function saveAll() {
let successfullySaved = [];
const promises = Object.entries(entityFormRefs.value).map(ref => saveSingle(ref));
Promise.all(promises).then(responses => {
responses.forEach(resp => {
if (resp?.id) {
const ref = Object.entries(entityFormRefs.value)
.find(ref => ref[1].editEntity.slug === resp.slug);
successfullySaved.push(ref[0]);
clearEntityForm(ref);
}
});
entityFormRefs.value = [];
const forms = entityForms.value.filter(e => !successfullySaved.includes(e.ref));
entityForms.value = forms.length ? forms : [generateEntityForm(0)];
});
}
async function saveSingle(entityFormComponentRef) {
const entityForm = entityFormComponentRef[1];
if (entityForm) {
return await entityForm.createEntity();
}
}
function clearEntityForm(entityFormComponentRef) {
const entityForm = entityFormComponentRef[1];
if (entityForm) {
entityForm.editEntity.name = null;
entityForm.editEntity.slug = null;
}
}
async function addEntityForm() {
entityForms.value.push(generateEntityForm(entityForms.value.length));
}
function addNewItem() {
addEntityForm().then(() => {
document.getElementById(`form-${entityForms.value.length - 1}`).scrollIntoView();
});
}
function closeForm(formId, refName) {
const form = document.getElementById(formId);
form.style.transition = "0.5s linear all";
form.style.opacity = "0";
setTimeout(() => {
entityForms.value = entityForms.value.filter(e => refName !== e.ref);
entityFormRefs.value = entityFormRefs.value.filter(ref => ref[0] !== refName);
}, 500);
}
function prepEntity() {
let entityCopy = _cloneDeep(props.entity);
entityCopy.name = null;
entityCopy.slug = null;
delete entityCopy.id;
delete entityCopy.deleted;
return entityCopy;
}
</script>
38 changes: 33 additions & 5 deletions frontend/src/components/inputs/EntityForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,26 @@
:required="true"/>
<TextInput label="Slug" v-model="editEntity.slug" :args="{ id: 'slug', maxlength: 128 }"
:required="true"/>
<h3 class="mt-3">Attributes</h3>
<div class="d-flex justify-content-between align-items-end mt-3">
<h3 class="align-self-start">Attributes</h3>
<div v-if="showAttributeCheckboxes" class="ps-3 d-flex flex-column">
<span class="fw-bold me-2">Values to copy:</span>
<div>
<div v-for="attr in schema?.attributes || []" :key="`${attr.name}-${attr.id}`"
class="form-check form-check-inline">
<label :class="{'cursor-pointer': entity[attr.name], 'form-check-label': true}">
{{ attr.name }}
<sup v-if="requiredAttrs.includes(attr.name)" class="text-danger me-1"
data-bs-toggle="tooltip" title="This value is required">*</sup>
<input type="checkbox" @change="onAttributeCopyChange($event.target.checked, attr.name)"
:checked="editEntity[attr.name]"
:disabled="!entity[attr.name]"
:class="`${entity[attr.name] ? 'cursor-pointer' : ''} form-check-input`" />
</label>
</div>
</div>
</div>
</div>
<template v-for="attr in schema?.attributes || []" :key="attr.name">
<!-- ATTRIBUTE IS REFERENCE -->
<template v-if="attr.type === 'FK'">
Expand Down Expand Up @@ -110,6 +129,10 @@ export default {
batchMode: {
type: Boolean,
default: false
},
showAttributeCheckboxes: {
type: Boolean,
default: false,
}
},
inject: ["updatePendingRequests"],
Expand Down Expand Up @@ -181,7 +204,7 @@ export default {
if (this.entity?.schema_id && this.entity?.schema_id !== this.schema.id) {
await this.updateSchemaMeta();
}
if (this.entity && this.editEntity?.id !== this.entity?.id) {
if (this.entity && this.editEntity?.slug !== this.entity?.slug) {
this.editEntity = _cloneDeep(this.entity);
} else if (!this.entity) {
this.prepEntity();
Expand Down Expand Up @@ -232,7 +255,7 @@ export default {
async saveEntity() {
this.loading = true;
let response;
if (this.$route.params.entitySlug) {
if (this.entity) {
// UPDATE EXISTING ENTITY
response = this.updateEntity();
} else {
Expand All @@ -242,7 +265,7 @@ export default {
if (response) {
this.updatePendingRequests();
}
this.$emit("update");
this.$emit("update", this.entity ? this.editEntity : null);
},
async deleteEntity() {
if (this.entity?.id) {
Expand All @@ -261,11 +284,16 @@ export default {
});
this.$emit("update");
}
},
onAttributeCopyChange(isChecked, attrName) {
this.editEntity[attrName] = isChecked ? this.entity[attrName] : null;
}
},
}
</script>
<style scoped>
.cursor-pointer {
cursor: pointer;
}
</style>
7 changes: 2 additions & 5 deletions frontend/src/components/inputs/ReferencedEntitySelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default {
currentEntitySlug: this.$route.params.entitySlug,
}
},
activated() {
mounted() {
this.getSelected();
},
computed: {
Expand All @@ -77,6 +77,7 @@ export default {
methods: {
getSelected() {
if (!this.modelValue) {
this.selected.length = 0;
return;
}
if (this.currentEntitySlug !== this.$route.params.entitySlug) {
Expand Down Expand Up @@ -133,10 +134,6 @@ export default {
modelValue() {
this.getSelected();
},
selected: {
handler: "getSelected",
immediate: true
}
}
};
</script>

0 comments on commit df11fa3

Please sign in to comment.