Browse Source

feat: Has many & belongs to - new record creation

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/341/head
Pranav C 3 years ago
parent
commit
92f2a59b88
  1. 6
      packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue
  2. 2
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  3. 232
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue
  4. 90
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue
  5. 70
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  6. 136
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts

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

@ -76,7 +76,7 @@
<div <div
style="height:100%; width:100%" style="height:100%; width:100%"
class="caption xc-input" class="caption xc-input"
v-if="col.ai || (col.pk && !selectedRowMeta.new) || disabledColumns[col._cn]" v-else-if="col.ai || (col.pk && !isNew) || disabledColumns[col._cn]"
@click="col.ai && $toast.info('Auto Increment field is not editable').goAway(3000)" @click="col.ai && $toast.info('Auto Increment field is not editable').goAway(3000)"
> >
<input <input
@ -204,8 +204,8 @@ export default {
table: String, table: String,
primaryValueColumn: String, primaryValueColumn: String,
api: [Object], api: [Object],
hasMany: Object, hasMany: [Object, Array],
belongsTo: Object, belongsTo: [Object, Array],
isNew: Boolean, isNew: Boolean,
oldRow: Object, oldRow: Object,
iconColor: { iconColor: {

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

@ -48,6 +48,8 @@ import hasManyCell from "@/components/project/spreadsheet/components/virtualCell
import manyToManyCell from "@/components/project/spreadsheet/components/virtualCell/manyToManyCell"; import manyToManyCell from "@/components/project/spreadsheet/components/virtualCell/manyToManyCell";
import belongsToCell from "@/components/project/spreadsheet/components/virtualCell/belogsToCell"; import belongsToCell from "@/components/project/spreadsheet/components/virtualCell/belogsToCell";
// todo: optimize parent/child meta extraction
export default { export default {
name: "virtual-cell", name: "virtual-cell",
components: { components: {

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

@ -2,57 +2,115 @@
<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]">{{ <v-chip small :color="colors[0]"
@click="active && editParent(value)"
>{{
Object.values(value)[1] Object.values(value)[1]
}} }}
<div v-show="active" class="mr-n1 ml-2 mt-n1">
<x-icon
:color="['text' , 'textLight']"
x-small
icon.class="unlink-icon"
@click.stop="unlink(ch)"
>mdi-close-thick
</x-icon>
</div>
</v-chip> </v-chip>
</template> </template>
</div> </div>
<div v-if="!isNew" class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }"> <div v-if="!isNew" 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">{{ <x-icon small :color="['primary','grey']" @click="showNewRecordModal">{{
value ? 'mdi-pencil' : 'mdi-plus' value ? 'mdi-arrow-expand' : 'mdi-plus'
}} }}
</x-icon> </x-icon>
</div> </div>
<list-items
<v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600"> v-if="newRecordModal"
<v-card width="600" color="backgroundColor"> :size="10"
<v-card-title class="textColor--text mx-2">Add Record</v-card-title> :meta="parentMeta"
<v-card-text> :primary-col="parentPrimaryCol"
<v-text-field :primary-key="parentPrimaryKey"
hide-details v-model="newRecordModal"
dense :api="parentApi"
outlined @add-new-record="insertAndMapNewParentRecord"
placeholder="Search record" @add="addParentToChild"
class="mb-2 mx-2 caption" :query-params="parentQueryParams"
/> />
<div class="items-container">
<template v-if="list"> <!-- <v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600">-->
<v-card <!-- <v-card width="600" color="backgroundColor">-->
v-for="(p,i) in list.list" <!-- <v-card-title class="textColor&#45;&#45;text mx-2">Add Record</v-card-title>-->
class="ma-2 child-card" <!-- <v-card-text>-->
outlined <!-- <v-text-field-->
v-ripple <!-- hide-details-->
@click="addParentToChild(p)" <!-- dense-->
:key="i" <!-- outlined-->
> <!-- placeholder="Search record"-->
<v-card-title class="primary-value textColor--text text--lighten-2">{{ p[parentPrimaryCol] }} <!-- class="mb-2 mx-2 caption"-->
<span class="grey--text caption primary-key" <!-- />-->
v-if="parentPrimaryKey">(Primary Key : {{ p[parentPrimaryKey] }})</span>
</v-card-title> <!-- <div class="items-container">-->
</v-card> <!-- <template v-if="list">-->
</template> <!-- <v-card-->
</div> <!-- v-for="(p,i) in list.list"-->
</v-card-text> <!-- class="ma-2 child-card"-->
<v-card-actions class="justify-center pb-6 "> <!-- outlined-->
<v-btn small outlined class="caption" color="primary"> <!-- v-ripple-->
<v-icon>mdi-plus</v-icon> <!-- @click="addParentToChild(p)"-->
Add New Record <!-- :key="i"-->
</v-btn> <!-- >-->
</v-card-actions> <!-- <v-card-title class="primary-value textColor&#45;&#45;text text&#45;&#45;lighten-2">{{ p[parentPrimaryCol] }}-->
</v-card> <!-- <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
:overlay-opacity="0.8"
v-if="selectedParent"
width="1000px"
max-width="100%"
class=" mx-auto"
v-model="expandFormModal">
<component
v-if="selectedParent"
:is="form"
:db-alias="nodes.dbAlias"
:has-many="parentMeta.hasMany"
:belongs-to="parentMeta.belongsTo"
:table="parentMeta.tn"
:old-row="{...selectedParent}"
:meta="parentMeta"
:sql-ui="sqlUi"
:primary-value-column="parentPrimaryCol"
:api="parentApi"
:available-columns="parentAvailableColumns"
:nodes="nodes"
:query-params="parentQueryParams"
:is-new="isNewParent"
icon-color="warning"
ref="expandedForm"
v-model="selectedParent"
@cancel="selectedParent = null"
@input="onParentSave"
></component>
</v-dialog> </v-dialog>
@ -62,10 +120,11 @@
<script> <script>
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 ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
export default { export default {
name: "belongs-to-cell", name: "belongs-to-cell",
components: {ListItems},
mixins: [colors], mixins: [colors],
props: { props: {
value: [Object, Array], value: [Object, Array],
@ -87,13 +146,42 @@ export default {
childList: null, childList: null,
dialogShow: false, dialogShow: false,
confirmAction: null, confirmAction: null,
confirmMessage: '' confirmMessage: '',
selectedParent: null,
isNewParent: false,
expandFormModal: false,
}), }),
methods: { methods: {
async onParentSave(parent) {
if (this.isNewParent) {
await this.addParentToChild(parent)
} else {
this.$emit('loadTableData')
}
},
async insertAndMapNewParentRecord() {
await this.loadParentMeta();
this.newRecordModal = false;
this.isNewParent = true;
this.selectedParent = {};
this.expandFormModal = true;
},
async unlink() {
const column = this.meta.columns.find(c => c.cn === this.bt.cn);
if (column.rqd) {
this.$toast.info('Unlink is not possible, instead map to another parent.').goAway(3000)
return
}
const _cn = column._cn;
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')
},
async showParentListModal() { async showParentListModal() {
this.parentListModal = true; this.parentListModal = true;
await this.getParentMeta(); await this.loadParentMeta();
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.parentMeta.columns.find(c => c.cn === this.hm.cn)._cn; const _cn = this.parentMeta.columns.find(c => c.cn === this.hm.cn)._cn;
this.childList = await this.parentApi.paginatedList({ this.childList = await this.parentApi.paginatedList({
@ -116,7 +204,7 @@ export default {
} }
} }
}, },
async getParentMeta() { async loadParentMeta() {
// todo: optimize // todo: optimize
if (!this.parentMeta) { if (!this.parentMeta) {
const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
@ -129,9 +217,9 @@ export default {
} }
}, },
async showNewRecordModal() { async showNewRecordModal() {
await this.loadParentMeta();
this.newRecordModal = true; this.newRecordModal = true;
await this.getParentMeta(); // this.list = await this.parentApi.paginatedList({})
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 pid = this.parentMeta.columns.filter((c) => c.pk).map(c => parent[c._cn]).join('___');
@ -146,7 +234,16 @@ export default {
this.newRecordModal = false; this.newRecordModal = false;
this.$emit('loadTableData') this.$emit('loadTableData')
} },
async editParent(parent) {
await this.loadParentMeta();
this.isNewParent = false;
this.selectedParent = parent;
this.expandFormModal = true;
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500)
},
}, },
computed: { computed: {
parentApi() { parentApi() {
@ -159,7 +256,32 @@ export default {
}, },
parentPrimaryKey() { parentPrimaryKey() {
return this.parentMeta && (this.parentMeta.columns.find(c => c.pk) || {})._cn return this.parentMeta && (this.parentMeta.columns.find(c => c.pk) || {})._cn
},
parentQueryParams() {
if (!this.parentMeta) return {}
return {
childs: (this.parentMeta && this.parentMeta.hasMany && this.parentMeta.hasMany.map(hm => hm.tn).join()) || '',
parents: (this.parentMeta && this.parentMeta.belongsTo && this.parentMeta.belongsTo.map(hm => hm.rtn).join()) || '',
many: (this.parentMeta && this.parentMeta.manyToMany && this.parentMeta.manyToMany.map(mm => mm.rtn).join()) || ''
} }
},
parentAvailableColumns() {
const hideCols = ['created_at', 'updated_at'];
if (!this.parentMeta) return [];
const columns = [];
if (this.parentMeta.columns) {
columns.push(...this.parentMeta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn)))
}
if (this.parentMeta.v) {
columns.push(...this.parentMeta.v.map(v => ({...v, virtual: 1})));
}
return columns;
},
// todo:
form() {
return this.selectedParent ? () => import("@/components/project/spreadsheet/components/expandedForm") : 'span';
},
} }
} }
</script> </script>
@ -182,24 +304,6 @@ export default {
} }
} }
.child-list-modal {
position: relative;
.remove-child-icon {
position: absolute;
right: 10px;
top: 10px;
bottom: 10px;
opacity: 0;
}
&:hover .remove-child-icon {
opacity: 1;
}
}
.child-card { .child-card {
cursor: pointer; cursor: pointer;

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

@ -1,27 +1,29 @@
<template> <template>
<v-dialog v-if="value" v-model="show" width="600"> <v-dialog v-model="show" width="600">
<v-card width="600" color="backgroundColor"> <v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">Add Record <v-card-title class="textColor--text mx-2 justify-center">{{ title }}
<v-spacer>
</v-spacer>
<v-btn small class="caption" color="primary">
<v-icon small>mdi-plus</v-icon>&nbsp;
Add New Record
</v-btn>
</v-card-title> </v-card-title>
<v-card-text>
<v-card-title>
<v-text-field <v-text-field
hide-details hide-details
dense dense
outlined outlined
placeholder="Search record" placeholder="Search records"
class="mb-2 mx-2 caption" class=" caption search-field ml-2"
/> />
<v-spacer></v-spacer>
<v-btn small class="caption mr-2" color="primary" @click="$emit('add-new-record')">
<v-icon small>mdi-plus</v-icon>&nbsp;
New Record
</v-btn>
</v-card-title>
<v-card-text>
<div class="items-container"> <div class="items-container">
<template v-if="data"> <template v-if="data && data.list && data.list.length">
<v-card <v-card
v-for="(ch,i) in data.list" v-for="(ch,i) in data.list"
class="ma-2 child-card" class="ma-2 child-card"
@ -30,22 +32,30 @@
@click="$emit('add',ch)" @click="$emit('add',ch)"
:key="i" :key="i"
> >
<v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[primaryCol] }} <v-card-text class="primary-value textColor--text text--lighten-2 d-flex">
<span class="font-weight-bold"> {{ ch[primaryCol] }}&nbsp;</span>
<span class="grey--text caption primary-key " <span class="grey--text caption primary-key "
v-if="primaryKey">(Primary Key : {{ ch[primaryKey] }})</span> v-if="primaryKey">(Primary Key : {{ ch[primaryKey] }})</span>
</v-card-title> <v-spacer/>
<v-chip v-if="hm && ch[meta._tn]" x-small>
{{ ch[primaryCol] }}
</v-chip>
</v-card-text>
</v-card> </v-card>
</template> </template>
<div v-else class="text-center py-15 textLight--text">
No items found
</div>
</div> </div>
</v-card-text> </v-card-text>
<v-card-actions class="justify-center py-2 flex-column"> <v-card-actions class="justify-center py-2 flex-column">
<pagination <pagination
v-if="list" v-if="data && data.list && data.list.length"
:size="size" :size="size"
:count="count" :count="data.count"
v-model="page" v-model="page"
@input="loadData" @input="loadData"
class="mb-3" class="mb-3"
@ -54,16 +64,26 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<span v-else></span>
</template> </template>
<script> <script>
import Pagination from "@/components/project/spreadsheet/components/pagination";
export default { export default {
name: "listItems", name: "listItems",
components: {Pagination},
props: { props: {
value: Boolean, value: Boolean,
title: String, hm:Boolean,
title: {
type: String,
default: 'Link Record'
},
queryParams: {
type: Object,
default() {
return {};
}
},
primaryKey: String, primaryKey: String,
primaryCol: String, primaryCol: String,
meta: Object, meta: Object,
@ -75,14 +95,18 @@ export default {
page: 1 page: 1
}), }),
mounted() { mounted() {
this.loadData();
}, },
methods: { methods: {
async loadData() { async loadData() {
if (this.api) {
this.data = await this.api.paginatedList({ this.data = await this.api.paginatedList({
limit: this.size, limit: this.size,
offset: this.size * (this.page - 1), offset: this.size * (this.page - 1),
...this.queryParams
}) })
} }
}
}, },
computed: { computed: {
show: { show: {
@ -92,13 +116,6 @@ export default {
return this.value; return this.value;
} }
} }
},
watch: {
value(v) {
if (v) {
this.loadData();
}
}
} }
} }
</script> </script>
@ -129,4 +146,21 @@ export default {
} }
} }
.primary-value {
.primary-key {
display: none;
margin-left: .5em;
}
&:hover .primary-key {
display: inline;
}
}
.items-container {
overflow-x: visible;
max-height: min(500px, 60vh);
overflow-y: auto;
}
</style> </style>

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

@ -7,7 +7,7 @@
v-for="(ch,i) in value" v-for="(ch,i) in value"
:key="i" :key="i"
:color="colors[i%colors.length]" :color="colors[i%colors.length]"
@click="editChild(ch)" @click="active && editChild(ch)"
> >
{{ Object.values(ch)[1] }} {{ Object.values(ch)[1] }}
<div v-show="active" class="mr-n1 ml-2 mt-n1"> <div v-show="active" class="mr-n1 ml-2 mt-n1">
@ -28,17 +28,26 @@
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon> <x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div> </div>
<!-- <list-items <list-items
:count="10" v-if="newRecordModal"
:hm="true"
:size="10"
:meta="childMeta" :meta="childMeta"
:primary-col="childPrimaryCol" :primary-col="childPrimaryCol"
:primary-key="childPrimaryKey" :primary-key="childPrimaryKey"
v-model="newRecordModal" v-model="newRecordModal"
:api="childApi" :api="childApi"
></list-items>--> @add-new-record="insertAndAddNewChildRecord"
<v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600"> @add="addChildToParent"
:query-params="{
...childQueryParams,
where: `~not(${childForeignKey},eq,${parentId})~or(${childForeignKey},is,null)`,
}"/>
<!-- <v-dialog v-if="newRecordModal && false" v-model="newRecordModal" width="600">
<v-card width="600" color="backgroundColor"> <v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2 justify-center">Link Record <v-card-title class="textColor&#45;&#45;text mx-2 justify-center">Link Record
</v-card-title> </v-card-title>
@ -70,9 +79,9 @@
@click="addChildToParent(ch)" @click="addChildToParent(ch)"
:key="i" :key="i"
> >
<v-card-text class="primary-value textColor--text text--lighten-2 d-flex"> <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="font-weight-bold"> {{ ch[childPrimaryCol] }}</span>
<span class="grey--text caption primary-key " <span class="grey&#45;&#45;text caption primary-key "
v-if="childPrimaryKey">(Primary Key : {{ ch[childPrimaryKey] }})</span> v-if="childPrimaryKey">(Primary Key : {{ ch[childPrimaryKey] }})</span>
<v-spacer/> <v-spacer/>
<v-chip v-if="ch[meta._tn]" x-small> <v-chip v-if="ch[meta._tn]" x-small>
@ -84,7 +93,7 @@
</template> </template>
<div v-else class="text-center py-15 textLight--text"> <div v-else class="text-center py-15 textLight&#45;&#45;text">
No items found No items found
</div> </div>
</div> </div>
@ -102,7 +111,7 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </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">
@ -182,7 +191,7 @@
width="1000px" width="1000px"
max-width="100%" max-width="100%"
class=" mx-auto" class=" mx-auto"
v-model="showExpandModal"> v-model="expandFormModal">
<component <component
v-if="selectedChild" v-if="selectedChild"
:is="form" :is="form"
@ -206,7 +215,6 @@
:is-new="isNewChild" :is-new="isNewChild"
:disabled-columns="disabledChildColumns" :disabled-columns="disabledChildColumns"
></component> ></component>
{{ childQueryParams }}
</v-dialog> </v-dialog>
@ -218,10 +226,12 @@ 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 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";
export default { export default {
name: "has-many-cell", name: "has-many-cell",
components: { components: {
ListItems,
Pagination, Pagination,
DlgLabelSubmitCancel DlgLabelSubmitCancel
}, },
@ -240,17 +250,17 @@ export default {
newRecordModal: false, newRecordModal: false,
childListModal: false, childListModal: false,
childMeta: null, childMeta: null,
list: null, // list: null,
childList: null, childList: null,
dialogShow: false, dialogShow: false,
confirmAction: null, confirmAction: null,
confirmMessage: '', confirmMessage: '',
selectedChild: null, selectedChild: null,
showExpandModal: false, expandFormModal: false,
listPagination: { // listPagination: {
page: 1, // page: 1,
size: 10 // size: 10
}, // },
childListPagination: { childListPagination: {
page: 1, page: 1,
size: 10 size: 10
@ -302,7 +312,7 @@ export default {
await this.loadChildMeta(); await this.loadChildMeta();
const column = this.childMeta.columns.find(c => c.cn === this.hm.cn); const column = this.childMeta.columns.find(c => c.cn === this.hm.cn);
if (column.rqd) { if (column.rqd) {
this.$toast.info('Unlink is not possible, add to another record.').goAway(3000) this.$toast.info('Unlink is not possible, instead add to another record.').goAway(3000)
return return
} }
const _cn = column._cn; const _cn = column._cn;
@ -329,15 +339,15 @@ export default {
} }
}, },
async showNewRecordModal() { async showNewRecordModal() {
this.newRecordModal = true;
await this.loadChildMeta(); await this.loadChildMeta();
const _cn = this.childForeignKey; this.newRecordModal = true;
this.list = await this.childApi.paginatedList({ // const _cn = this.childForeignKey;
...this.childQueryParams, // this.list = await this.childApi.paginatedList({
limit: this.listPagination.size, // ...this.childQueryParams,
offset: this.listPagination.size * (this.listPagination.page - 1), // limit: this.listPagination.size,
where: `~not(${_cn},eq,${this.parentId})~or(${_cn},is,null)` // offset: this.listPagination.size * (this.listPagination.page - 1),
}) // where: `~not(${_cn},eq,${this.parentId})~or(${_cn},is,null)`
// })
}, },
async addChildToParent(child) { async addChildToParent(child) {
const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___'); const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
@ -359,7 +369,7 @@ export default {
await this.loadChildMeta(); await this.loadChildMeta();
this.isNewChild = false; this.isNewChild = false;
this.selectedChild = child; this.selectedChild = child;
this.showExpandModal = true; this.expandFormModal = true;
setTimeout(() => { setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.reload() this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500) }, 500)
@ -371,7 +381,7 @@ export default {
this.selectedChild = { this.selectedChild = {
[this.childForeignKey]: this.parentId [this.childForeignKey]: this.parentId
}; };
this.showExpandModal = true; this.expandFormModal = true;
setTimeout(() => { setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.$set(this.$refs.expandedForm.changedColumns, this.childForeignKey, true) this.$refs.expandedForm && this.$refs.expandedForm.$set(this.$refs.expandedForm.changedColumns, this.childForeignKey, true)
}, 500) }, 500)
@ -400,7 +410,7 @@ export default {
}, },
// todo: // todo:
form() { form() {
return () => import("@/components/project/spreadsheet/components/expandedForm") return this.selectedChild ? () => import("@/components/project/spreadsheet/components/expandedForm") : 'span';
}, },
childAvailableColumns() { childAvailableColumns() {
const hideCols = ['created_at', 'updated_at']; const hideCols = ['created_at', 'updated_at'];

136
packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts

@ -1107,7 +1107,7 @@ class BaseModelSql extends BaseModel {
* @private * @private
*/ */
async _getChildListInParent({parent, child}, rest = {}, index) { async _getChildListInParent({parent, child}, rest = {}, index) {
let {fields, where, limit, offset, sort} = this._getChildListArgs(rest, index); let {fields, where, limit, offset, sort} = this._getChildListArgs(rest, index, child);
const {cn} = this.hasManyRelations.find(({tn}) => tn === child) || {}; const {cn} = this.hasManyRelations.find(({tn}) => tn === child) || {};
const _cn = this.dbModels[child].columnToAlias?.[cn]; const _cn = this.dbModels[child].columnToAlias?.[cn];
@ -1136,6 +1136,49 @@ class BaseModelSql extends BaseModel {
}) })
} }
/**
* Get child list and map to input parent
*
* @param {Object[]} parent - parent list array
* @param {String} child - child table name
* @param {Object} rest - index suffixed fields, limit, offset, where and sort
* @param index - child table index
* @returns {Promise<void>}
* @private
*/
async _getManyToManyList({parent, child}, rest = {}, index) {
let {fields, where, limit, offset, sort} = this._getChildListArgs(rest, index, child);
const {tn, cn, vtn, vcn, vrcn, rtn, rcn} = this.manyToManyRelations.find(({rtn}) => rtn === child) || {};
const _cn = this.dbModels[tn].columnToAlias?.[cn];
if (fields !== '*' && fields.split(',').indexOf(cn) === -1) {
fields += ',' + cn;
}
const childs = await this._run(this.dbDriver.union(
parent.map(p => {
const query =
this
.dbDriver(child)
.join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`)
.where(`${vtn}.${vcn}`, p[this.columnToAlias?.[this.pks[0].cn] || this.pks[0].cn])
.xwhere(where, this.dbModels[child].selectQuery(''))
.select({[_cn]: `${vtn}.${cn}`, ...this.dbModels[child].selectQuery(fields)}) // ...fields.split(','));
this._paginateAndSort(query, {sort, limit, offset}, null, true);
return this.isSqlite() ? this.dbDriver.select().from(query) : query;
}), !this.isSqlite()
));
let gs = _.groupBy(childs, _cn);
parent.forEach(row => {
row[this.dbModels?.[child]?._tn || child] = gs[row[this.pks[0]._cn]] || [];
})
}
/** /**
* Gets child rows for a parent row in this table * Gets child rows for a parent row in this table
* *
@ -1192,7 +1235,7 @@ class BaseModelSql extends BaseModel {
*/ */
// todo : add conditionGraph // todo : add conditionGraph
async hasManyList({childs, where, fields, f, ...rest}) { async hasManyList({childs = '', where, fields, f, ...rest}) {
fields = fields || f || '*'; fields = fields || f || '*';
try { try {
@ -1202,7 +1245,7 @@ class BaseModelSql extends BaseModel {
const parent = await this.list({childs, where, fields, ...rest}); const parent = await this.list({childs, where, fields, ...rest});
if (parent && parent.length) { if (parent && parent.length) {
await Promise.all([...new Set(childs.split('.'))].map((child, index) => this._getChildListInParent({ await Promise.all([...new Set(childs.split('.'))].map((child, index) => child && this._getChildListInParent({
parent, parent,
child child
}, rest, index))); }, rest, index)));
@ -1214,6 +1257,75 @@ class BaseModelSql extends BaseModel {
} }
} }
/**
* Gets parent list along with children list and parent
*
* @param {Object} args
* @param {String} args.childs - comma separated child table names
* @param {String} [args.fields=*] - commas separated column names of this table
* @param {String} [args.fields*=*] - commas separated column names of child table(* is a natural number 'i' where i is index of child table in comma separated list)
* @param {String} [args.where] - where clause with conditions within ()
* @param {String} [args.where*] - where clause with conditions within ()(* is a natural number 'i' where i is index of child table in comma separated list)
* @param {String} [args.limit] - number of rows to be limited (has default,min,max values in config)
* @param {String} [args.limit*] - number of rows to be limited of child table(* is a natural number 'i' where i is index of child table in comma separated list)
* @param {String} [args.offset] - offset from which to get the number of rows
* @param {String} [args.offset*] - offset from which to get the number of rows of child table(* is a natural number 'i' where i is index of child table in comma separated list)
* @param {String} [args.sort] - comma separated column names where each column name is cn ascending and -cn is cn descending
* @param {String} [args.sort*] - comma separated column names where each column name is cn ascending and -cn is cn descending(* is a natural number 'i' where i is index of child table in comma separated list)
* @returns {Promise<Object[]>}
*/
// todo : add conditionGraph
async nestedList({childs = '', parents = '', many = '', where, fields: fields1, f, ...rest}) {
let fields = fields1 || f || '*';
try {
if (fields !== '*' && fields.split(',').indexOf(this.pks[0].cn) === -1) {
fields += ',' + this.pks[0].cn;
}
for (const parent of parents.split(',')) {
const {cn} = this.belongsToRelations.find(({rtn}) => rtn === parent) || {};
if (fields !== '*' && fields.split(',').indexOf(cn) === -1) {
fields += ',' + cn;
}
}
const items = await this.list({childs, where, fields, ...rest});
if (items && items.length) {
await Promise.all([...new Set(childs.split(','))].map((child, index) => child && this._getChildListInParent({
parent: items,
child
}, rest, index)));
}
await Promise.all(parents.split(',').map((parent, index): any => {
if (!parent) {
return;
}
const {cn, rcn} = this.belongsToRelations.find(({rtn}) => rtn === parent) || {};
const parentIds = [...new Set(items.map(c => c[cn] || c[this.columnToAlias[cn]]))];
return this._belongsTo({parent, rcn, parentIds, childs: items, cn, ...rest}, index);
}))
if (items && items.length) {
await Promise.all([...new Set(many.split(','))].map((child, index) => child && this._getManyToManyList({
parent: items,
child
}, rest, index)));
}
return items;
} catch (e) {
console.log(e);
throw e;
}
}
/** /**
* Gets child list along with its parent * Gets child list along with its parent
* *
@ -1269,7 +1381,7 @@ class BaseModelSql extends BaseModel {
* @private * @private
*/ */
async _belongsTo({parent, rcn, parentIds, childs, cn, ...rest}, index) { async _belongsTo({parent, rcn, parentIds, childs, cn, ...rest}, index) {
let {fields} = this._getChildListArgs(rest, index); let {fields} = this._getChildListArgs(rest, index, parent);
if (fields !== '*' && fields.split(',').indexOf(rcn) === -1) { if (fields !== '*' && fields.split(',').indexOf(rcn) === -1) {
fields += ',' + rcn; fields += ',' + rcn;
} }
@ -1284,7 +1396,7 @@ class BaseModelSql extends BaseModel {
const gs = _.groupBy(parents, this.dbModels[parent]?.columnToAlias?.[rcn] || rcn); const gs = _.groupBy(parents, this.dbModels[parent]?.columnToAlias?.[rcn] || rcn);
childs.forEach(row => { childs.forEach(row => {
row[this.dbModels?.[parent]?._tn || parent] = gs[row[this.dbModels[parent]?.columnToAlias?.[cn] || cn]]?.[0]; row[this.dbModels?.[parent]?._tn || parent] = gs[row[this.dbModels[parent]?.columnToAlias?.[cn] || cn]]?.[0] || gs[row[cn || gs[row[this.dbModels[parent]?.columnToAlias?.[cn] || cn]]?.[0]]]?.[0];
}) })
} }
@ -1465,17 +1577,23 @@ class BaseModelSql extends BaseModel {
* @returns {Object} consisting of fields*,where*,limit*,offset*,sort* * @returns {Object} consisting of fields*,where*,limit*,offset*,sort*
* @private * @private
*/ */
_getChildListArgs(args: any, index?: number) { _getChildListArgs(args: any, index?: number, child?: string) {
index++; index++;
let obj: XcFilter = {}; let obj: XcFilter = {};
obj.where = args[`where${index}`] || args[`w${index}`] || ''; obj.where = args[`where${index}`] || args[`w${index}`] || '';
obj.limit = Math.max(Math.min(args[`limit${index}`] || args[`l${index}`] || this.config.limitDefault, this.config.limitMax), this.config.limitMin); obj.limit = Math.max(Math.min(args[`limit${index}`] || args[`l${index}`] || this.config.limitDefault, this.config.limitMax), this.config.limitMin);
obj.offset = args[`offset${index}`] || args[`o${index}`] || 0; obj.offset = args[`offset${index}`] || args[`o${index}`] || 0;
obj.fields = args[`fields${index}`] || args[`f${index}`] || '*'; obj.fields = args[`fields${index}`] || args[`f${index}`] || this.getPKandPV(child);
obj.sort = args[`sort${index}`] || args[`s${index}`]; obj.sort = args[`sort${index}`] || args[`s${index}`];
return obj; return obj;
} }
private getPKandPV(child: string) {
return child ?
(this.dbModels[child]?.columns?.filter(col => col.pk || col.pv).map(col => col.cn) || ['*']).join(',')
: '*';
}
// @ts-ignore // @ts-ignore
public selectQuery(fields) { public selectQuery(fields) {
const fieldsArr = fields.split(','); const fieldsArr = fields.split(',');
@ -1484,8 +1602,8 @@ class BaseModelSql extends BaseModel {
!fields !fields
|| fieldsArr.includes('*') || fieldsArr.includes('*')
|| fieldsArr.includes(`${this.tn}.*`) || fieldsArr.includes(`${this.tn}.*`)
|| fields.includes(col._cn) || fieldsArr.includes(col._cn)
|| fields.includes(col.cn) || fieldsArr.includes(col.cn)
) { ) {
selectObj[col._cn] = `${this.tn}.${col.cn}`; selectObj[col._cn] = `${this.tn}.${col.cn}`;
} }

Loading…
Cancel
Save