Browse Source

feat: relation column delete

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/341/head
Pranav C 3 years ago
parent
commit
f33fe5c514
  1. 4
      packages/nc-gui/components/project/spreadsheet/components/editableCell/dateTimePickerCell.vue
  2. 34
      packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue
  3. 9
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  4. 90
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue
  5. 135
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItems.vue
  6. 121
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal.vue
  7. 2
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue
  8. 78
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  9. 71
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue
  10. 80
      packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
  11. 13
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  12. 3
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  13. 1
      packages/nc-gui/store/sqlMgr.js
  14. 18
      packages/nocodb/src/lib/noco/NcProjectBuilder.ts
  15. 23
      packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts
  16. 126
      packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts
  17. 6
      packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts

4
packages/nc-gui/components/project/spreadsheet/components/editableCell/dateTimePickerCell.vue

@ -30,12 +30,14 @@ export default {
computed: {
localState: {
get() {
// todo : time value correction
if(/^\d{6,}$/.test(this.value)){
return new Date(+this.value);
}
return /\dT\d/.test(this.value) ? new Date(this.value.replace(/(\d)T(?=\d)/, '$1 ')) : new Date(this.value);
return /\dT\d/.test(this.value) ? new Date(this.value.replace(/(\d)T(?=\d)/, '$1 ')) : this.value;
},
set(val) {
// if(/^\d{6,}$/.test(this.value)){

34
packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue

@ -51,7 +51,6 @@
<div>
<label :for="`data-table-form-${col._cn}`" class="body-2 text-capitalize">
<span v-if="col.virtual">
{{ col._cn }}
</span>
<header-cell
v-else
@ -66,15 +65,16 @@
v-if="col.virtual"
:disabledColumns="disabledColumns"
:column="col"
:row="localState"
:row="value"
:nodes="nodes"
:meta="meta"
:api="api"
:active="true"
:sql-ui="sqlUi"
@loadTableData="reload"
:is-new="isNew"
:is-form="true"
@updateCol="updateCol"
@newRecordsSaved="$listeners.loadTableData"
></virtual-cell>
<div
@ -204,7 +204,6 @@ export default {
value: Object,
meta: Object,
sqlUi: [Object, Function],
selectedRowMeta: Object,
table: String,
primaryValueColumn: String,
api: [Object],
@ -236,7 +235,7 @@ export default {
localState: {},
changedColumns: {},
comment: null,
showSystemFields: false
showSystemFields: false,
}),
created() {
this.localState = {...this.value}
@ -278,8 +277,8 @@ export default {
&& (rowObj[columnObj._cn] === undefined || rowObj[columnObj._cn] === null)
&& !columnObj.default);
},
updateCol(row, _cn, pid) {
this.$set(row, _cn, pid)
updateCol(_row, _cn, pid) {
this.$set(this.localState, _cn, pid)
this.$set(this.changedColumns, _cn, true)
},
isYou(email) {
@ -331,7 +330,7 @@ export default {
const where = this.meta.columns.filter((c) => c.pk).map(c => `(${c._cn},eq,${this.localState[c._cn]})`).join('~and');
this.$set(this, 'changedColumns', {});
// this.localState = await this.api.read(id);
this.localState = (await this.api.list({...(this.queryParams || {}), where}) || [{}])[0];
this.localState = (await this.api.list({...(this.queryParams || {}), where}) || [{}])[0] || this.localState;
if (!this.isNew && this.toggleDrawer) {
this.getAuditsAndComments()
}
@ -359,6 +358,9 @@ export default {
}
},
computed: {
primaryKey() {
return this.isNew ? '' : this.meta.columns.filter((c) => c.pk).map(c => this.localState[c._cn]).join('___');
},
edited() {
return !!Object.keys(this.changedColumns).length;
},
@ -373,6 +375,9 @@ export default {
return (this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn))) || [];
}
},
isChanged() {
return Object.values(this.changedColumns).some(Boolean)
}
}
}
</script>
@ -482,13 +487,14 @@ h5 {
.comment-box.focus {
border: 1px solid #4185f4;
}
.required > div > label + *{
border:1px solid red;
.required > div > label + * {
border: 1px solid red;
border-radius: 4px;
min-height: 42px;
display: flex;
align-items: center;
justify-content: flex-end;
//min-height: 42px;
//display: flex;
//align-items: center;
//justify-content: flex-end;
background: var(--v-backgroundColorDefault-base);
}
</style>

9
packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue

@ -11,6 +11,7 @@
:active="active"
:sql-ui="sqlUi"
:is-new="isNew"
:is-form="isForm"
v-on="$listeners"
/>
<many-to-many-cell
@ -23,8 +24,9 @@
:sql-ui="sqlUi"
:active="active"
:is-new="isNew"
v-on="$listeners"
:api="api"
:is-form="isForm"
v-on="$listeners"
/>
<belongs-to-cell
:disabled-columns="disabledColumns"
@ -38,6 +40,7 @@
:api="api"
:sql-ui="sqlUi"
:is-new="isNew"
:is-form="isForm"
v-on="$listeners"
/>
</v-lazy>
@ -70,6 +73,10 @@ export default {
type: Boolean,
default: false
},
isForm: {
type: Boolean,
default: false
},
disabledColumns: Object
},
computed: {

90
packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue

@ -1,25 +1,26 @@
<template>
<div class="d-flex">
<div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value || localState">
<item-chip
:active="active"
:item="value"
:value="Object.values(value || localState)[1]"
:key="i"
@edit="editParent"
@unlink="unlink"
></item-chip>
</template>
</div>
<div class=" align-center justify-center px-1 flex-shrink-1"
:class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">{{
value ? 'mdi-arrow-expand' : 'mdi-plus'
}}
</x-icon>
</div>
<template v-if="!isForm">
<div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value || localState">
<item-chip
:active="active"
:item="value"
:value="Object.values(value || localState)[1]"
:key="i"
@edit="editParent"
@unlink="unlink"
></item-chip>
</template>
</div>
<div class=" align-center justify-center px-1 flex-shrink-1"
:class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">{{
value ? 'mdi-arrow-expand' : 'mdi-plus'
}}
</x-icon>
</div>
</template>
<list-items
v-if="newRecordModal"
:size="10"
@ -33,6 +34,26 @@
:query-params="parentQueryParams"
/>
<list-child-items
ref="childList"
v-if="parentMeta && isForm"
:local-state="localState? [localState] : []"
:is-new="isNew"
:size="10"
:parent-meta="parentMeta"
:meta="parentMeta"
:primary-col="parentPrimaryCol"
:primary-key="parentPrimaryKey"
:api="parentApi"
:query-params="{
...parentQueryParams,
where: `(${parentPrimaryKey},eq,${parentId})`
}"
@new-record="showNewRecordModal"
@edit="editParent"
@unlink="unlink"
:bt="true"
/>
<v-dialog
:overlay-opacity="0.8"
@ -74,11 +95,13 @@
import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
import ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip";
import ListChildItems from "@/components/project/spreadsheet/components/virtualCell/components/listChildItems";
export default {
name: "belongs-to-cell",
components: {ItemChip, ListItems},
components: {ListChildItems, ItemChip, ListItems},
props: {
isForm: Boolean,
value: [Object, Array],
meta: [Object],
bt: Object,
@ -104,7 +127,11 @@ export default {
expandFormModal: false,
localState: null,
}),
async mounted() {
if (this.isForm) {
await this.loadParentMeta()
}
},
methods: {
async onParentSave(parent) {
if (this.isNewParent) {
@ -136,6 +163,9 @@ export default {
const id = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
await this.api.update(id, {[_cn]: null}, this.row)
this.$emit('loadTableData')
if (this.isForm && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
async showParentListModal() {
this.parentListModal = true;
@ -156,9 +186,11 @@ export default {
} else {
const id = this.parentMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
await this.parentApi.delete(id)
this.showParentListModal();
this.dialogShow = false;
this.$emit('loadTableData')
if (this.isForm && this.$refs.childList) {
this.$refs.childList.loadData();
}
}
}
},
@ -177,9 +209,8 @@ export default {
async showNewRecordModal() {
await this.loadParentMeta();
this.newRecordModal = true;
// this.list = await this.parentApi.paginatedList({})
},
async addParentToChild(parent) {
async addParentToChild(parent) {
const pid = this.parentMeta.columns.filter((c) => c.pk).map(c => parent[c._cn]).join('___');
const id = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
@ -196,9 +227,13 @@ export default {
}, {
[_cn]: parent[this.parentPrimaryKey]
});
this.newRecordModal = false;
this.$emit('loadTableData')
if (this.isForm && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
async editParent(parent) {
await this.loadParentMeta();
@ -216,6 +251,9 @@ export default {
ApiFactory.create(this.$store.getters['project/GtrProjectType'],
this.parentMeta && this.parentMeta._tn, this.parentMeta && this.parentMeta.columns, this) : null;
},
parentId() {
return this.value && this.parentMeta && this.parentMeta.columns.filter((c) => c.pk).map(c => this.value[c._cn]).join('___')
},
parentPrimaryCol() {
return this.parentMeta && (this.parentMeta.columns.find(c => c.pv) || {})._cn
},
@ -285,8 +323,6 @@ export default {
}
.hm-items {
//min-width: 200px;
//max-width: 400px;
flex-wrap: wrap;
row-gap: 3px;
gap: 3px;

135
packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItems.vue

@ -1,65 +1,69 @@
<template>
<v-dialog v-model="show" width="600">
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">{{ meta ? meta._tn : 'Children' }}
<v-spacer>
</v-spacer>
<!-- <v-dialog v-model="show" width="600">-->
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">{{ meta ? meta._tn : 'Children' }}
<v-spacer>
</v-spacer>
<v-btn small class="caption" color="primary" @click="$emit('new-record')">
<v-icon small>mdi-plus</v-icon>&nbsp;
Add Record
</v-btn>
<v-btn small class="caption" color="primary" @click="$emit('new-record')">
<v-icon small>{{ bt ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>&nbsp;
{{ bt ? 'Select' : 'Add' }} Record
</v-btn>
</v-card-title>
<v-card-text>
<div class="items-container">
<template v-if="data && data.list">
<v-card
v-for="(ch,i) in data.list"
class="ma-2 child-list-modal child-card"
outlined
:key="i"
@click="$emit('edit',ch)"
>
<div class="remove-child-icon d-flex align-center">
<x-icon
:tooltip="`Unlink this '${meta._tn}' from '${parentMeta._tn}'`"
:color="['error','grey']"
small
@click.stop="$emit('unlink',ch,i)"
icon.class="mr-1 mt-n1"
>mdi-link-variant-remove
</x-icon>
<x-icon
v-if="!mm"
:tooltip="`Delete row in '${meta._tn}'`"
:color="['error','grey']"
small
@click.stop="$emit('delete',ch,i)"
>mdi-delete-outline
</x-icon>
</div>
</v-card-title>
<v-card-text>
<div class="items-container pt-2">
<template v-if="(data && data.list && data.list.length) || (localState && localState.length)">
<v-card
v-for="(ch,i) in ((data && data.list) || localState)"
class="ma-2 child-list-modal child-card"
outlined
:key="i"
@click="$emit('edit',ch)"
>
<div class="remove-child-icon d-flex align-center">
<x-icon
:tooltip="`Unlink this '${meta._tn}' from '${parentMeta._tn}'`"
:color="['error','grey']"
small
@click.stop="$emit('unlink',ch,i)"
icon.class="mr-1 mt-n1"
>mdi-link-variant-remove
</x-icon>
<x-icon
v-if="!mm && !bt"
:tooltip="`Delete row in '${meta._tn}'`"
:color="['error','grey']"
small
@click.stop="$emit('delete',ch,i)"
>mdi-delete-outline
</x-icon>
</div>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[primaryCol] }}
<span class="grey--text caption primary-key"
v-if="primaryKey">(Primary Key : {{ ch[primaryKey] }})</span>
</v-card-title>
</v-card>
</template>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[primaryCol] }}
<span class="grey--text caption primary-key"
v-if="primaryKey">(Primary Key : {{ ch[primaryKey] }})</span>
</v-card-title>
</v-card>
</template>
<div v-else-if="data" class="text-center pt-6 pb-4 textLight--text">
No item{{ bt ? '' : 's' }} found
</div>
</v-card-text>
<v-card-actions class="justify-center py-2 flex-column">
<pagination
v-if="data && data.list"
:size="size"
:count="data.count"
v-model="page"
@input="loadData"
class="mb-3"
></pagination>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</v-card-text>
<v-card-actions class="justify-center py-2 flex-column">
<pagination
v-if="!bt && data && data.list"
:size="size"
:count="data.count"
v-model="page"
@input="loadData"
class="mb-3"
></pagination>
</v-card-actions>
</v-card>
<!-- </v-dialog>-->
</template>
@ -70,6 +74,9 @@ export default {
name: "listChildItems",
components: {Pagination},
props: {
bt: Boolean,
localState: [Array],
isNew: Boolean,
value: Boolean,
title: {
type: String,
@ -87,20 +94,19 @@ export default {
parentMeta: Object,
size: Number,
api: [Object, Function],
mm:[Object, Boolean]
mm: [Object, Boolean]
},
data: () => ({
data: null,
page: 1
}),
mounted() {
this.loadData();
this.loadData();
},
methods: {
async loadData() {
if (!this.api) return;
this.data = await this.api.paginatedList({
if (!this.api || this.isNew) return;
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams
@ -115,6 +121,11 @@ export default {
return this.value;
}
}
},
watch: {
queryParams() {
this.loadData();
}
}
}
</script>

121
packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal.vue

@ -0,0 +1,121 @@
<template>
<v-dialog v-model="show" width="600">
<list-child-items
v-if="show"
ref="child"
:local-state="localState"
:is-new="isNew"
:size="10"
:meta="meta"
:parent-meta="meta"
:primary-col="primaryCol"
:primary-key="primaryKey"
:api="api"
:query-params="queryParams"
v-bind="$attrs"
v-on="$listeners"
/>
</v-dialog>
</template>
<script>
import Pagination from "@/components/project/spreadsheet/components/pagination";
import ListChildItems from "@/components/project/spreadsheet/components/virtualCell/components/listChildItems";
export default {
name: "listChildItemsModal",
components: {ListChildItems, Pagination},
props: {
localState: Array,
isNew: Boolean,
value: Boolean,
title: {
type: String,
default: 'Link Record'
},
queryParams: {
type: Object,
default() {
return {};
}
},
primaryKey: String,
primaryCol: String,
meta: Object,
parentMeta: Object,
size: Number,
api: [Object, Function],
mm: [Object, Boolean]
},
data: () => ({
data: null,
page: 1
}),
mounted() {
},
methods: {
async loadData() {
if (this.$refs && this.$refs.child) {
this.$refs.child.loadData();
}
}
},
computed: {
show: {
set(v) {
this.$emit('input', v)
}, get() {
return this.value;
}
}
}
}
</script>
<style scoped lang="scss">
.child-list-modal {
position: relative;
.remove-child-icon {
position: absolute;
right: 10px;
top: 10px;
bottom: 10px;
opacity: 0;
}
&:hover .remove-child-icon {
opacity: 1;
}
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

2
packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue

@ -46,7 +46,7 @@
</template>
<div v-else class="text-center py-15 textLight--text">
<div v-else-if="data" class="text-center py-15 textLight--text">
No items found
</div>
</div>

78
packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue

@ -1,22 +1,24 @@
<template>
<div class="d-flex">
<div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value||localState">
<item-chip
v-for="(ch,i) in (value|| localState)"
:active="active"
:item="ch"
:value="Object.values(ch)[1]"
:key="i"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
</template>
</div>
<div class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">mdi-plus</x-icon>
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
<template v-if="!isForm">
<div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value||localState">
<item-chip
v-for="(ch,i) in (value|| localState)"
:active="active"
:item="ch"
:value="Object.values(ch)[1]"
:key="i"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
</template>
</div>
<div class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">mdi-plus</x-icon>
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
</template>
<list-items
v-if="newRecordModal"
@ -35,9 +37,12 @@
}"/>
<list-child-items
:is="isForm ? 'list-child-items' : 'list-child-items-modal'"
ref="childList"
v-if="childListModal"
v-if="childMeta && (childListModal || isForm)"
v-model="childListModal"
:local-state.sync="localState"
:is-new="isNew"
:size="10"
:meta="childMeta"
:parent-meta="meta"
@ -106,6 +111,8 @@ import Pagination from "@/components/project/spreadsheet/components/pagination";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
import ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip";
import ListChildItems from "@/components/project/spreadsheet/components/virtualCell/components/listChildItems";
import listChildItemsModal
from "@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal";
export default {
name: "has-many-cell",
@ -114,7 +121,8 @@ export default {
ItemChip,
ListItems,
Pagination,
DlgLabelSubmitCancel
DlgLabelSubmitCancel,
listChildItemsModal
},
props: {
value: [Object, Array],
@ -124,7 +132,8 @@ export default {
row: [Object],
sqlUi: [Object, Function],
active: Boolean,
isNew: Boolean
isNew: Boolean,
isForm: Boolean,
},
data: () => ({
newRecordModal: false,
@ -138,7 +147,11 @@ export default {
isNewChild: false,
localState: []
}),
async mounted() {
if (this.isForm) {
await this.loadChildMeta()
}
},
methods: {
async showChildListModal() {
await this.loadChildMeta();
@ -157,7 +170,7 @@ export default {
await this.childApi.delete(id)
this.dialogShow = false;
this.$emit('loadTableData')
if (this.childListModal && this.$refs.childList) {
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
} catch (e) {
@ -167,6 +180,11 @@ export default {
}
},
async unlinkChild(child) {
if (this.isNew) {
this.localState.splice(this.localState.indexOf(child), 1)
return;
}
await this.loadChildMeta();
const column = this.childMeta.columns.find(c => c.cn === this.hm.cn);
if (column.rqd) {
@ -177,7 +195,7 @@ export default {
const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
await this.childApi.update(id, {[_cn]: null}, child)
this.$emit('loadTableData')
if (this.childListModal && this.$refs.childList) {
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
// }
@ -214,12 +232,12 @@ export default {
await this.childApi.update(id, {
[_cn]: this.parentId
}, {
[_cn]: child[this.childPrimaryKey]
[_cn]: child[this.childForeignKey]
});
this.$emit('loadTableData')
if (this.childListModal) {
await this.showChildListModal()
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
async editChild(child) {
@ -295,12 +313,16 @@ export default {
}
},
watch: {
isNew(n, o) {
isNew(n, o) {
debugger
if (!n && o) {
debugger
let child;
debugger
while (child = this.localState.pop()) {
this.addChildToParent(child)
this.addChildToParent(child)
}
this.$emit('newRecordsSaved')
}
}
}

71
packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue

@ -1,24 +1,24 @@
<template>
<div class="d-flex">
<div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="(value || localState)">
<item-chip v-for="(v,j) in (value || localState)"
:active="active"
:item="v"
:value="Object.values(v)[2]"
:key="j"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
</template>
</div>
<div class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">mdi-plus</x-icon>
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
<template v-if="!isForm">
<div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="(value || localState)">
<item-chip v-for="(v,j) in (value || localState)"
:active="active"
:item="v"
:value="Object.values(v)[2]"
:key="j"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
</template>
</div>
<div class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">mdi-plus</x-icon>
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
</template>
<list-items
@ -37,9 +37,11 @@
:query-params="childQueryParams"/>
<list-child-items
:is="isForm ? 'list-child-items' : 'list-child-items-modal'"
ref="childList"
v-if="childListModal"
v-if="childMeta && assocMeta && (isForm || childListModal)"
v-model="childListModal"
:is-new="isNew"
:size="10"
:meta="childMeta"
:parent-meta="meta"
@ -49,6 +51,7 @@
:mm="mm"
:parent-id="row && row[parentPrimaryKey]"
:query-params="{...childQueryParams, conditionGraph }"
:local-state="localState"
@new-record="showNewRecordModal"
@edit="editChild"
@unlink="unlinkChild"
@ -144,6 +147,7 @@ export default {
sqlUi: [Object, Function],
active: Boolean,
isNew: Boolean,
isForm: Boolean,
},
data: () => ({
isNewChild: false,
@ -159,7 +163,11 @@ export default {
expandFormModal: false,
localState: []
}),
async mounted() {
if (this.isForm) {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
}
},
methods: {
async onChildSave(child) {
if (this.isNewChild) {
@ -172,6 +180,10 @@ export default {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
this.childListModal = true;
}, async unlinkChild(child) {
if (this.isNew) {
this.localState.splice(this.localState.indexOf(child), 1)
return;
}
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
const _pcn = this.meta.columns.find(c => c.cn === this.mm.cn)._cn;
@ -183,9 +195,8 @@ export default {
const id = this.assocMeta.columns.filter((c) => c.cn === apcn || c.cn === accn).map(c => c.cn === apcn ? this.row[_pcn] : child[_ccn]).join('___');
await this.assocApi.delete(id)
this.$emit('loadTableData')
if (this.childListModal && this.$refs.childList) {
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
// this.showChildListModal()
}
},
async removeChild(child) {
@ -198,9 +209,11 @@ export default {
} else {
const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
await this.childApi.delete(id)
this.showChildListModal();
this.dialogShow = false;
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
}
}
},
@ -231,7 +244,7 @@ export default {
async showNewRecordModal() {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
this.newRecordModal = true;
// this.list = await this.childApi.paginatedList({})
// this.list = await this.c hildApi.paginatedList({})
},
async addChildToParent(child) {
if (this.isNew) {
@ -257,6 +270,9 @@ export default {
console.log(e)
}
this.newRecordModal = false;
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
@ -348,12 +364,13 @@ export default {
},
},
watch: {
isNew(n, o) {
async isNew(n, o) {
if (!n && o) {
let child;
while (child = this.localState.pop()) {
this.addChildToParent(child)
await this.addChildToParent(child)
}
this.$emit('newRecordsSaved')
}
}
}

80
packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue

@ -33,23 +33,66 @@
</template>
<span class="caption font-weight-bold">Primary value will be shown in place of primary key</span>
</v-tooltip>
</v-list-item>
<v-list-item @click="columnDeleteDialog = true">
<x-icon small class="mr-1" color="error">mdi-delete-outline</x-icon>
<span class="caption">Delete</span>
</v-list-item>-->
</v-list-item> -->
<v-list-item @click="columnDeleteDialog = true">
<x-icon small class="mr-1" color="error">mdi-delete-outline</x-icon>
<span class="caption">Delete</span>
</v-list-item>
</v-list>
</v-menu>
<v-dialog v-model="columnDeleteDialog" max-width="500"
persistent>
<v-card>
<v-card-title class="grey darken-2 subheading white--text">Confirm</v-card-title>
<v-divider></v-divider>
<v-card-text class="mt-4 title">Do you want to delete <span class="font-weight-bold">'{{
column.cn
}}'</span> column ?
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="d-flex pa-4">
<v-spacer></v-spacer>
<v-btn small @click="columnDeleteDialog = false">Cancel</v-btn>
<v-btn small color="error" @click="deleteColumn">Confirm</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
props: ['column'],
props: ['column', 'nodes'],
name: "virtualHeaderCell",
data: () => ({}),
data: () => ({
columnDeleteDialog: false
}),
computed: {
type() {
if (this.column.bt) return 'bt'
if (this.column.hm) return 'hm'
if (this.column.mm) return 'mm'
},
childColumn() {
if (this.column.bt) return this.column.bt.cn
if (this.column.hm) return this.column.hm.cn
if (this.column.mm) return this.column.mm.rcn
},
childTable() {
if (this.column.bt) return this.column.bt.tn
if (this.column.hm) return this.column.hm.tn
if (this.column.mm) return this.column.mm.rtn
},
parentTable() {
if (this.column.bt) return this.column.bt.rtn
if (this.column.hm) return this.column.hm.rtn
if (this.column.mm) return this.column.mm.tn
},
parentColumn() {
if (this.column.bt) return this.column.bt.rcn
if (this.column.hm) return this.column.hm.rcn
if (this.column.mm) return this.column.mm.cn
},
tooltipMsg() {
if (!this.column) return '';
if (this.column.hm) {
@ -60,6 +103,27 @@ export default {
return `'${this.column.bt._tn}' belongs to '${this.column.bt._rtn}'`
}
}
}, methods: {
async deleteColumn() {
try {
const column = {...this.column, cno: this.column.cn};
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, "xcRelationColumnDelete", {
type: this.type,
childColumn: this.childColumn,
childTable: this.childTable,
parentTable: this.parentTable,
parentColumn: this.parentColumn,
assocTable: this.column.mm && this.column.mm.vtn
}]);
this.$emit('saved');
this.columnDeleteDialog = false;
} catch (e) {
console.log(e)
}
}
}
}
</script>

13
packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue

@ -399,12 +399,11 @@
v-model="showExpandModal">
<expanded-form
:key="selectedExpandRowIndex"
:db-alias="nodes.dbAlias"
:has-many="hasMany"
:belongs-to="belongsTo"
v-if="selectedExpandRowIndex != null && data[selectedExpandRowIndex]"
@cancel="showExpandModal = false;"
@input="showExpandModal = false; (data[selectedExpandRowIndex] && data[selectedExpandRowIndex].rowMeta && delete data[selectedExpandRowIndex].rowMeta.new)"
:table="table"
v-model="data[selectedExpandRowIndex].row"
:oldRow.sync="data[selectedExpandRowIndex].oldRow"
@ -414,10 +413,13 @@
:sql-ui="sqlUi"
:primary-value-column="primaryValueColumn"
:api="api"
@commented="reloadComments"
:availableColumns="availableColumns"
:nodes="nodes"
:query-params="queryParams"
@cancel="showExpandModal = false;"
@input="showExpandModal = false; (data[selectedExpandRowIndex] && data[selectedExpandRowIndex].rowMeta && delete data[selectedExpandRowIndex].rowMeta.new) ; loadTableData()"
@commented="reloadComments"
@loadTableData="loadTableData"
></expanded-form>
</v-dialog>
@ -820,7 +822,7 @@ export default {
const {rowMeta} = this.data[this.data.length - 1];
this.expandRow(this.data.length - 1, rowMeta)
}
this.save()
// this.save()
},
@ -859,7 +861,7 @@ export default {
},
loadTableDataDeb: debounce(async function (self) {
await self.loadTableDataFn()
}, 100),
}, 200),
loadTableData() {
this.loadTableDataDeb(this)
},
@ -918,7 +920,6 @@ export default {
return this.meta && this.meta._tn ? ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.meta && this.meta._tn, this.meta && this.meta.columns, this) : null;
}
},
}
</script>

3
packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue

@ -27,6 +27,7 @@
<virtual-header-cell v-if="col.virtual"
:column="col"
:nodes="nodes"
/>
@ -132,8 +133,8 @@
:api="api"
:active="selected.col === col && selected.row === row"
:sql-ui="sqlUi"
v-on="$listeners"
:is-new="rowMeta.new"
v-on="$listeners"
@updateCol="(...args) => updateCol(...args, columnObj.bt && meta.columns.find( c => c.cn === columnObj.bt.cn), col, row)"
></virtual-cell>

1
packages/nc-gui/store/sqlMgr.js

@ -263,6 +263,7 @@ function translateUiToLibCall(args, op, opArgs) {
case 'relationDelete':
case 'xcVirtualRelationDelete':
case 'xcRelationColumnDelete':
data.type = "Relation delete";
data.title = '';
data.module = "";

18
packages/nocodb/src/lib/noco/NcProjectBuilder.ts

@ -128,13 +128,23 @@ export default class NcProjectBuilder {
});
console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
break;
case 'xcVirtualRelationDelete':
await curBuilder.onRelationDelete(data.req.args.parentTable, data.req.args.childTable, {
case 'xcRelationColumnDelete':
await curBuilder.onRelationCreate(data.req.args.parentTable, data.req.args.childTable, {
...data.req.args,
virtual: true
});
console.log(`Deleted relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
break;
case 'xcVirtualRelationDelete':
if(data.req.args?.type === 'mm'){
curBuilder.onManyToManyRelationDelete(data.req.args.parentTable, data.req.args.childTable)
}
// await curBuilder.onRelationDelete(data.req.args.parentTable, data.req.args.childTable, {
// ...data.req.args,
// virtual: true
// });
// console.log(`Deleted relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
break;

23
packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts

@ -366,7 +366,7 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
this.baseLog(`onTableUpdate : Generating new model meta for '%s' table`, tn)
/* create models from table */
const newMeta:any = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getObject();
const newMeta: any = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getObject();
/* get ACL row */
@ -750,6 +750,27 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
return this.getManyToManyRelations({parent, child})
}
public async onManyToManyRelationDelete(parent: string, child: string, _args?: any) {
const parentMeta = this.metas[parent];
const childMeta = this.metas[child];
parentMeta.manyToMany = parentMeta.manyToMany.filter(mm => !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === child))
childMeta.manyToMany = childMeta.manyToMany.filter(mm => !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === child))
parentMeta.v = parentMeta.v.filter(({mm}) => !mm || !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === child))
childMeta.v = childMeta.v.filter(({mm}) => !mm || !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === child))
for (const meta of [parentMeta, childMeta]) {
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
mm: 1,
}, {title: meta.tn})
XcCache.del([this.projectId, this.dbAlias, 'table', meta.tn].join('::'));
this.models[meta.tn] = this.getBaseModel(meta)
}
}
protected async loadCommon(): Promise<any> {
this.baseLog(`loadCommon :`);

126
packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts

@ -1470,6 +1470,10 @@ export default class NcMetaMgr {
result = await this.xcM2MRelationCreate(args, req);
break;
case 'xcRelationColumnDelete':
result = await this.xcRelationColumnDelete(args, req);
break;
case 'xcVirtualRelationDelete':
result = await this.xcVirtualRelationDelete(args, req);
break;
@ -2448,6 +2452,128 @@ export default class NcMetaMgr {
}
}
// todo : transaction
protected async xcRelationColumnDelete(args: any, req): Promise<any> {
const dbAlias = this.getDbAlias(args);
const projectId = this.getProjectId(args);
// const parent = await this.xcMeta.metaGet(projectId, dbAlias, 'nc_models', {
// title: args.args.parentTable
// });
// // @ts-ignore
// const parentMeta = JSON.parse(parent.meta);
// @ts-ignore
// todo: compare column
switch (args.args.type) {
case 'bt':
case 'hm':
const child = await this.xcMeta.metaGet(projectId, dbAlias, 'nc_models', {
title: args.args.childTable
});
const childMeta = JSON.parse(child.meta);
const relation = childMeta.belongsTo.find(bt => bt.rtn === args.args.parentTable);
{
const opArgs = {
...args,
args: {
childColumn: relation.cn,
childTable: relation.tn,
parentTable: relation.rtn,
parentColumn: relation.rcn
},
api: 'relationDelete'
};
const out = await this.projectMgr.getSqlMgr({id: projectId}).handleRequest('relationDelete', opArgs);
if (this.listener) {
await this.listener({
req: opArgs,
res: out,
user: req.user,
ctx: {req}
});
}
}
{
const originalColumns = childMeta.columns;
const columns = childMeta.columns.map(c => ({...c, ...(relation.cn === c.cn ? {altered: 4} : {})}))
const opArgs = {
...args,
args: {
columns,
originalColumns,
tn: childMeta.tn,
sqlOpPlus: true
},
api: 'tableUpdate'
}
const out = await this.projectMgr.getSqlMgr({id: projectId}).handleRequest('tableUpdate', opArgs);
if (this.listener) {
await this.listener({
req: opArgs,
res: out,
user: req.user,
ctx: {req}
});
}
}
break;
case 'mm': {
const assoc = await this.xcMeta.metaGet(projectId, dbAlias, 'nc_models', {
title: args.args.assocTable
});
const assocMeta = JSON.parse(assoc.meta);
const rel1 = assocMeta.belongsTo.find(bt => bt.rtn === args.args.parentTable)
const rel2 = assocMeta.belongsTo.find(bt => bt.rtn === args.args.parentTable)
await this.xcRelationColumnDelete({
...args,
args: {
parentTable: rel1.rtn,
parentColumn: rel1.rcn,
childTable: rel1.tn,
childColumn: rel1.cn,
}
},req)
await this.xcRelationColumnDelete({
...args,
args: {
parentTable: rel2.rtn,
parentColumn: rel2.rcn,
childTable: rel2.tn,
childColumn: rel2.cn,
}
},req);
const opArgs = {
...args,
args: assocMeta,
api: 'tableDelete'
};
const out = await this.projectMgr.getSqlMgr({id: projectId}).handleRequest('tableDelete', opArgs);
if (this.listener) {
await this.listener({
req: opArgs,
res: out,
user: req.user,
ctx: {req}
});
}
}
break;
}
}
protected async xcVirtualRelationDelete(args: any, req): Promise<any> {

6
packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts

@ -1028,7 +1028,6 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
});
/* Add new has many relation to virtual columns */
oldMeta.v = oldMeta.v || [];
oldMeta.v.push({
@ -1211,8 +1210,10 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
const oldMeta = JSON.parse(existingModel.meta);
Object.assign(oldMeta, {
hasMany: meta.hasMany,
v: oldMeta.v.filter(({hm}) => !hm || hm.rtn !== tnp || hm.tn !== tnc)
});
// todo: delete from query_params
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
title: tnp,
meta: JSON.stringify(oldMeta),
@ -1244,8 +1245,9 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
const oldMeta = JSON.parse(existingModel.meta);
Object.assign(oldMeta, {
belongsTo: meta.belongsTo,
v: oldMeta.v.filter(({bt}) => !bt || bt.rtn !== tnp || bt.tn !== tnc)
});
// todo: delete from query_params
await this.xcMeta.metaUpdate(this.projectId,
this.dbAlias,
'nc_models', {

Loading…
Cancel
Save