Browse Source

feat: Many to Many relation

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/341/head
Pranav C 3 years ago
parent
commit
edf6cb2f36
  1. 67
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue
  2. 31
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/item-chip.vue
  3. 95
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  4. 209
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue
  5. 7
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  6. 6
      packages/nocodb/src/lib/noco/rest/RestCtrl.ts

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

@ -2,21 +2,15 @@
<div class="d-flex"> <div class="d-flex">
<div class="d-flex align-center img-container flex-grow-1 hm-items"> <div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value"> <template v-if="value">
<v-chip small :color="colors[0]" <item-chip
@click="active && editParent(value)" :active="active"
>{{ :item="value"
Object.values(value)[1] :color="colors[i%colors.length]"
}} :value="Object.values(value)[1]"
<div v-show="active" class="mr-n1 ml-2 mt-n1"> :key="i"
<x-icon @edit="editParent"
:color="['text' , 'textLight']" @unlink="unlink"
x-small ></item-chip>
icon.class="unlink-icon"
@click.stop="unlink(ch)"
>mdi-close-thick
</x-icon>
</div>
</v-chip>
</template> </template>
</div> </div>
<div v-if="!isNew" class=" align-center justify-center px-1 flex-shrink-1" <div v-if="!isNew" class=" align-center justify-center px-1 flex-shrink-1"
@ -41,46 +35,6 @@
/> />
<!-- <v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600">-->
<!-- <v-card width="600" color="backgroundColor">-->
<!-- <v-card-title class="textColor&#45;&#45;text mx-2">Add Record</v-card-title>-->
<!-- <v-card-text>-->
<!-- <v-text-field-->
<!-- hide-details-->
<!-- dense-->
<!-- outlined-->
<!-- placeholder="Search record"-->
<!-- class="mb-2 mx-2 caption"-->
<!-- />-->
<!-- <div class="items-container">-->
<!-- <template v-if="list">-->
<!-- <v-card-->
<!-- v-for="(p,i) in list.list"-->
<!-- class="ma-2 child-card"-->
<!-- outlined-->
<!-- v-ripple-->
<!-- @click="addParentToChild(p)"-->
<!-- :key="i"-->
<!-- >-->
<!-- <v-card-title class="primary-value textColor&#45;&#45;text text&#45;&#45;lighten-2">{{ p[parentPrimaryCol] }}-->
<!-- <span class="grey&#45;&#45;text caption primary-key"-->
<!-- v-if="parentPrimaryKey">(Primary Key : {{ p[parentPrimaryKey] }})</span>-->
<!-- </v-card-title>-->
<!-- </v-card>-->
<!-- </template>-->
<!-- </div>-->
<!-- </v-card-text>-->
<!-- <v-card-actions class="justify-center pb-6 ">-->
<!-- <v-btn small outlined class="caption" color="primary">-->
<!-- <v-icon>mdi-plus</v-icon>-->
<!-- Add New Record-->
<!-- </v-btn>-->
<!-- </v-card-actions>-->
<!-- </v-card>-->
<!-- </v-dialog>-->
<v-dialog <v-dialog
:overlay-opacity="0.8" :overlay-opacity="0.8"
v-if="selectedParent" v-if="selectedParent"
@ -121,10 +75,11 @@
import colors from "@/mixins/colors"; import colors from "@/mixins/colors";
import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory"; import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems"; import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
import ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip";
export default { export default {
name: "belongs-to-cell", name: "belongs-to-cell",
components: {ListItems}, components: {ItemChip, ListItems},
mixins: [colors], mixins: [colors],
props: { props: {
value: [Object, Array], value: [Object, Array],

31
packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/item-chip.vue

@ -0,0 +1,31 @@
<template>
<v-chip small :color="color"
@click="active && $emit('edit',item)"
>{{value}}
<div v-show="active" class="mr-n1 ml-2 mt-n1">
<x-icon
:color="['text' , 'textLight']"
x-small
icon.class="unlink-icon"
@click.stop="$emit('unlink',item)"
>mdi-close-thick
</x-icon>
</div>
</v-chip>
</template>
<script>
export default {
props:{
color:String,
value:String,
active:Boolean,
item:Object
},
name: "item-chip"
}
</script>
<style scoped>
</style>

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

@ -2,25 +2,16 @@
<div class="d-flex"> <div class="d-flex">
<div class="d-flex align-center img-container flex-grow-1 hm-items"> <div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value"> <template v-if="value">
<v-chip <item-chip
small
v-for="(ch,i) in value" v-for="(ch,i) in value"
:key="i" :active="active"
:item="ch"
:color="colors[i%colors.length]" :color="colors[i%colors.length]"
@click="active && editChild(ch)" :value="Object.values(ch)[1]"
> :key="i"
{{ Object.values(ch)[1] }} @edit="editChild"
<div v-show="active" class="mr-n1 ml-2 mt-n1"> @unlink="unlinkChild"
<x-icon ></item-chip>
:color="['text' , 'textLight']"
x-small
icon.class="unlink-icon"
@click.stop="unlinkChild(ch)"
>mdi-close-thick
</x-icon>
</div>
</v-chip>
</template> </template>
</div> </div>
<div class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }"> <div class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
@ -45,74 +36,6 @@
}"/> }"/>
<!-- <v-dialog v-if="newRecordModal && false" v-model="newRecordModal" width="600">
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor&#45;&#45;text mx-2 justify-center">Link Record
</v-card-title>
<v-card-title>
<v-text-field
hide-details
dense
outlined
placeholder="Search records"
class=" caption search-field ml-2"
/>
<v-spacer></v-spacer>
<v-btn small class="caption mr-2" color="primary" @click="insertAndAddNewChildRecord">
<v-icon small>mdi-plus</v-icon>&nbsp;
New Record
</v-btn>
</v-card-title>
<v-card-text>
<div class="items-container">
<template v-if="list && list.list && list.list.length">
<v-card
v-for="(ch,i) in list.list"
class="ma-2 child-card"
outlined
v-ripple
@click="addChildToParent(ch)"
:key="i"
>
<v-card-text class="primary-value textColor&#45;&#45;text text&#45;&#45;lighten-2 d-flex">
<span class="font-weight-bold"> {{ ch[childPrimaryCol] }}</span>
<span class="grey&#45;&#45;text caption primary-key "
v-if="childPrimaryKey">(Primary Key : {{ ch[childPrimaryKey] }})</span>
<v-spacer/>
<v-chip v-if="ch[meta._tn]" x-small>
{{ ch[meta._tn][primaryCol] }}
</v-chip>
</v-card-text>
</v-card>
</template>
<div v-else class="text-center py-15 textLight&#45;&#45;text">
No items found
</div>
</div>
</v-card-text>
<v-card-actions class="justify-center py-2 flex-column">
<pagination
v-if="list && list.list && list.list.length"
:size="listPagination.size"
:count="list.count"
v-model="listPagination.page"
@input="showNewRecordModal"
class="mb-3"
></pagination>
</v-card-actions>
</v-card>
</v-dialog>
-->
<v-dialog v-if="childListModal" v-model="childListModal" width="600"> <v-dialog v-if="childListModal" v-model="childListModal" width="600">
<v-card width="600" color="backgroundColor"> <v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">{{ childMeta ? childMeta._tn : 'Children' }} <v-card-title class="textColor--text mx-2">{{ childMeta ? childMeta._tn : 'Children' }}
@ -227,10 +150,12 @@ import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel"; import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import Pagination from "@/components/project/spreadsheet/components/pagination"; import Pagination from "@/components/project/spreadsheet/components/pagination";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems"; import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
import ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip";
export default { export default {
name: "has-many-cell", name: "has-many-cell",
components: { components: {
ItemChip,
ListItems, ListItems,
Pagination, Pagination,
DlgLabelSubmitCancel DlgLabelSubmitCancel

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

@ -4,11 +4,16 @@
<div class="d-flex align-center img-container flex-grow-1 hm-items"> <div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value"> <template v-if="value">
<v-chip small v-for="(v,j) in value.map(v=>Object.values(v)[2])" <item-chip v-for="(v,j) in value"
:color="colors[j%colors.length]" :key="j">{{ :active="active"
v :item="v"
}} :color="colors[j%colors.length]"
</v-chip> :value="Object.values(v)[2]"
:key="j"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
</template> </template>
</div> </div>
<div class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }"> <div class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
@ -17,45 +22,18 @@
</div> </div>
<v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600"> <list-items
<v-card width="600" color="backgroundColor"> v-if="newRecordModal"
<v-card-title class="textColor--text mx-2">Add Record</v-card-title> :hm="true"
<v-card-text> :size="10"
<v-text-field :meta="childMeta"
hide-details :primary-col="childPrimaryCol"
dense :primary-key="childPrimaryKey"
outlined v-model="newRecordModal"
placeholder="Search record" :api="childApi"
class="mb-2 mx-2 caption" @add-new-record="insertAndAddNewChildRecord"
/> @add="addChildToParent"
:query-params="{...childQueryParams, conditionGraph }"/>
<div class="items-container">
<template v-if="list">
<v-card
v-for="(p,i) in list.list"
class="ma-2 child-card"
outlined
v-ripple
@click="addChildToParent(p)"
:key="i"
>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ p[childPrimaryCol] }}
<span class="grey--text caption primary-key"
v-if="childPrimaryKey">(Primary Key : {{ p[childPrimaryKey] }})</span>
</v-card-title>
</v-card>
</template>
</div>
</v-card-text>
<v-card-actions class="justify-center pb-6 ">
<v-btn small outlined class="caption" color="primary">
<v-icon>mdi-plus</v-icon>
Add New Record
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-if="childListModal" v-model="childListModal" width="600"> <v-dialog v-if="childListModal" v-model="childListModal" width="600">
<v-card width="600" color="backgroundColor"> <v-card width="600" color="backgroundColor">
@ -130,6 +108,41 @@
:heading="confirmMessage" :heading="confirmMessage"
> >
</dlg-label-submit-cancel> </dlg-label-submit-cancel>
<!-- todo : move to listitem component -->
<v-dialog
:overlay-opacity="0.8"
v-if="selectedChild"
width="1000px"
max-width="100%"
class=" mx-auto"
v-model="expandFormModal">
<component
v-if="selectedChild"
:is="form"
:db-alias="nodes.dbAlias"
:has-many="childMeta.hasMany"
:belongs-to="childMeta.belongsTo"
:table="childMeta.tn"
v-model="selectedChild"
:old-row="{...selectedChild}"
:meta="childMeta"
:sql-ui="sqlUi"
:primary-value-column="childPrimaryCol"
:api="childApi"
:available-columns="childAvailableColumns"
icon-color="warning"
:nodes="nodes"
:query-params="childQueryParams"
ref="expandedForm"
:is-new="isNewChild"
@cancel="selectedChild = null"
@input="onChildSave"
></component>
</v-dialog>
</div> </div>
</template> </template>
@ -137,11 +150,13 @@
import colors from "@/mixins/colors"; import colors from "@/mixins/colors";
import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory"; import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel"; import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
import ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip";
export default { export default {
name: "many-to-many-cell", name: "many-to-many-cell",
mixins: [colors], mixins: [colors],
components: {DlgLabelSubmitCancel}, components: {ItemChip, ListItems, DlgLabelSubmitCancel},
props: { props: {
value: [Object, Array], value: [Object, Array],
meta: [Object], meta: [Object],
@ -151,30 +166,51 @@ export default {
api: [Object, Function], api: [Object, Function],
sqlUi: [Object, Function], sqlUi: [Object, Function],
active: Boolean, active: Boolean,
isNew: Boolean isNew: Boolean,
}, },
data: () => ({ data: () => ({
isNewChild: false,
newRecordModal: false, newRecordModal: false,
childListModal: false, childListModal: false,
childMeta: null, childMeta: null,
assocMeta: null, assocMeta: null,
list: null, // list: null,
childList: null, childList: null,
dialogShow: false, dialogShow: false,
confirmAction: null, confirmAction: null,
confirmMessage: '', confirmMessage: '',
selectedChild:null selectedChild: null,
expandFormModal: false
}), }),
methods: { methods: {
async onChildSave(child) {
if (this.isNewChild) {
await this.addChildToParent(child)
} else {
this.$emit('loadTableData')
}
},
async showChildListModal() { async showChildListModal() {
this.childListModal = true; this.childListModal = true;
await this.getChildMeta(); await this.loadChildMeta();
const pid = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___'); const pid = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
const _cn = this.childMeta.columns.find(c => c.cn === this.hm.cn)._cn; const _cn = this.childMeta.columns.find(c => c.cn === this.hm.cn)._cn;
this.childList = await this.childApi.paginatedList({ this.childList = await this.childApi.paginatedList({
where: `(${_cn},eq,${pid})` where: `(${_cn},eq,${pid})`
}) })
}, async unlinkChild(child) {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
const _pcn = this.meta.columns.find(c => c.cn === this.mm.cn)._cn;
const _ccn = this.childMeta.columns.find(c => c.cn === this.mm.rcn)._cn;
const apcn = this.assocMeta.columns.find(c => c.cn === this.mm.vcn).cn;
const accn = this.assocMeta.columns.find(c => c.cn === this.mm.vrcn).cn;
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')
}, },
async removeChild(child) { async removeChild(child) {
this.dialogShow = true; this.dialogShow = true;
@ -192,7 +228,7 @@ export default {
} }
} }
}, },
async getChildMeta() { async loadChildMeta() {
// todo: optimize // todo: optimize
if (!this.childMeta) { if (!this.childMeta) {
const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
@ -204,7 +240,7 @@ export default {
this.childMeta = JSON.parse(parentTableData.meta) this.childMeta = JSON.parse(parentTableData.meta)
} }
}, },
async getAssociateTableMeta() { async loadAssociateTableMeta() {
// todo: optimize // todo: optimize
if (!this.childMeta) { if (!this.childMeta) {
const assocTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ const assocTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
@ -217,9 +253,9 @@ export default {
} }
}, },
async showNewRecordModal() { async showNewRecordModal() {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
this.newRecordModal = true; this.newRecordModal = true;
await Promise.all([this.getChildMeta(), this.getAssociateTableMeta()]); // this.list = await this.childApi.paginatedList({})
this.list = await this.childApi.paginatedList({})
}, },
async addChildToParent(child) { async addChildToParent(child) {
const cid = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___'); const cid = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
@ -227,15 +263,42 @@ export default {
const vcidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vrcn)._cn; const vcidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vrcn)._cn;
const vpidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vcn)._cn; const vpidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vcn)._cn;
try {
await this.assocApi.insert({ await this.assocApi.insert({
[vcidCol]: cid, [vcidCol]: cid,
[vpidCol]: pid [vpidCol]: pid
}); });
this.newRecordModal = false;
this.$emit('loadTableData') this.$emit('loadTableData')
} catch (e) {
// todo: handle
console.log(e)
} }
this.newRecordModal = false;
},
async insertAndAddNewChildRecord() {
this.newRecordModal = false;
await this.loadChildMeta();
this.isNewChild = true;
this.selectedChild = {
[this.childForeignKey]: this.parentId
};
this.expandFormModal = true;
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.$set(this.$refs.expandedForm.changedColumns, this.childForeignKey, true)
}, 500)
}, async editChild(child) {
await this.loadChildMeta();
this.isNewChild = false;
this.selectedChild = child;
this.expandFormModal = true;
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500)
},
}, },
computed: { computed: {
childApi() { childApi() {
@ -264,7 +327,43 @@ export default {
}, },
parentPrimaryKey() { parentPrimaryKey() {
return this.meta && (this.meta.columns.find(c => c.pk) || {})._cn return this.meta && (this.meta.columns.find(c => c.pk) || {})._cn
},
childQueryParams() {
if (!this.childMeta) return {}
return {
childs: (this.childMeta && this.childMeta.hasMany && this.childMeta.hasMany.map(hm => hm.tn).join()) || '',
parents: (this.childMeta && this.childMeta.belongsTo && this.childMeta.belongsTo.map(hm => hm.rtn).join()) || '',
many: (this.childMeta && this.childMeta.manyToMany && this.childMeta.manyToMany.map(mm => mm.rtn).join()) || ''
} }
},
conditionGraph() {
// if (!this.childMeta || !this.assocMeta) return null;
// return {
// [this.assocMeta.tn]: {
// "relationType": "hm",
// [this.assocMeta.columns.find(c => c.cn === this.mm.vcn).cn]: {
// "eq": this.row[this.parentPrimaryKey]
// }
// }
// }
},
childAvailableColumns() {
const hideCols = ['created_at', 'updated_at'];
if (!this.childMeta) return [];
const columns = [];
if (this.childMeta.columns) {
columns.push(...this.childMeta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn)))
}
if (this.childMeta.v) {
columns.push(...this.childMeta.v.map(v => ({...v, virtual: 1})));
}
return columns;
},
// todo:
form() {
return this.selectedChild ? () => import("@/components/project/spreadsheet/components/expandedForm") : 'span';
},
} }
} }
</script> </script>

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

@ -69,7 +69,7 @@
</tr> </tr>
</thead> </thead>
<tbody v-click-outside="() => {this.selected.col=null;this.selected.row=null}"> <tbody v-click-outside="onClickOutside">
<tr <tr
v-for="({row:rowObj, rowMeta, oldRow},row) in data" v-for="({row:rowObj, rowMeta, oldRow},row) in data"
:key="row" :key="row"
@ -413,6 +413,11 @@ export default {
} }
}, },
onClickOutside() {
if (this.meta.columns[this.selected.col].virtual) return
this.selected.col = null;
this.selected.row = null
},
onNewColCreation() { onNewColCreation() {
this.addNewColMenu = false; this.addNewColMenu = false;
this.addNewColModal = false; this.addNewColModal = false;

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

@ -135,8 +135,14 @@ export class RestCtrl extends RestBaseCtrl {
const data = await req.model.delb(req.body) const data = await req.model.delb(req.body)
res.json(data); res.json(data);
} }
public async nestedList(req: Request | any, res): Promise<void> { public async nestedList(req: Request | any, res): Promise<void> {
const startTime = process.hrtime(); const startTime = process.hrtime();
if (req.query.conditionGraph && typeof req.query.conditionGraph === 'string') {
req.query.conditionGraph = {models: this.models, condition: JSON.parse(req.query.conditionGraph)}
}
const data = await req.model.nestedList({ const data = await req.model.nestedList({
...req.query ...req.query
} as any); } as any);

Loading…
Cancel
Save