Browse Source

feat: many to many - child listing

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/341/head
Pranav C 3 years ago
parent
commit
7b0a39bccc
  1. 14
      packages/nc-gui/components/project/spreadsheet/apis/restApi.js
  2. 25
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  3. 165
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItems.vue
  4. 59
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue
  5. 48
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  6. 63
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue
  7. 58
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts
  8. 24
      packages/nocodb/src/lib/noco/rest/RestCtrl.ts
  9. 11
      packages/nocodb/src/lib/sqlMgr/code/routes/xc-ts/ExpressXcTsRoutes.ts

14
packages/nc-gui/components/project/spreadsheet/apis/restApi.js

@ -48,7 +48,19 @@ export default class RestApi {
async paginatedList(params) {
// const list = await this.list(params);
// const count = (await this.count({where: params.where || ''})).count;
const [list, {count}] = await Promise.all([this.list(params), this.count({where: params.where || ''})]);
const [list, {count}] = await Promise.all([this.list(params), this.count({
where: params.where || '',
conditionGraph: params.conditionGraph
})]);
return {list, count};
}
async paginatedM2mNotChildrenList(params, assoc, pid) {
///api/v1/Film/m2mNotChildren/film_actor/44
// const list = await this.list(params);
// const count = (await this.count({where: params.where || ''})).count;
const {list, info: {count}} = (await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}/m2mNotChildren/${assoc}/${pid}`, params)).data
debugger
return {list, count};
}

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

@ -24,6 +24,7 @@
:active="active"
:is-new="isNew"
v-on="$listeners"
:api="api"
/>
<belongs-to-cell
:disabled-columns="disabledColumns"
@ -88,3 +89,27 @@ export default {
<style scoped>
</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/>.
*
*/
-->

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

@ -0,0 +1,165 @@
<template>
<v-dialog v-model="value" 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-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
: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>
</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>
</template>
<script>
import Pagination from "@/components/project/spreadsheet/components/pagination";
export default {
name: "listChildItems",
components: {Pagination},
props: {
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],
},
data: () => ({
data: null,
page: 1
}),
mounted() {
this.loadData();
},
methods: {
async loadData() {
if (!this.api) return;
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams
})
}
},
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/>.
*
*/
-->

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

@ -52,14 +52,14 @@
</div>
</v-card-text>
<v-card-actions class="justify-center py-2 flex-column">
<pagination
v-if="data && data.list && data.list.length"
:size="size"
:count="data.count"
v-model="page"
@input="loadData"
class="mb-3"
></pagination>
<pagination
v-if="data && data.list && data.list.length"
:size="size"
:count="data.count"
v-model="page"
@input="loadData"
class="mb-3"
></pagination>
</v-card-actions>
</v-card>
</v-dialog>
@ -68,12 +68,13 @@
<script>
import Pagination from "@/components/project/spreadsheet/components/pagination";
export default {
name: "listItems",
components: {Pagination},
props: {
value: Boolean,
hm:Boolean,
hm: Boolean,
title: {
type: String,
default: 'Link Record'
@ -88,7 +89,9 @@ export default {
primaryCol: String,
meta: Object,
size: Number,
api: [Object, Function]
api: [Object, Function],
mm: [Object, Function],
parentId: [String, Number]
},
data: () => ({
data: null,
@ -99,7 +102,16 @@ export default {
},
methods: {
async loadData() {
if (this.api) {
if (!this.api) return;
if (this.mm) {
this.data = await this.api.paginatedM2mNotChildrenList({
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams
}, this.mm.vtn,this.parentId)
} else {
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
@ -157,6 +169,7 @@ export default {
display: inline;
}
}
.items-container {
overflow-x: visible;
max-height: min(500px, 60vh);
@ -164,3 +177,27 @@ export default {
}
</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/>.
*
*/
-->

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

@ -4,13 +4,13 @@
<template v-if="value">
<item-chip
v-for="(ch,i) in value"
:active="active"
:item="ch"
:color="colors[i%colors.length]"
:value="Object.values(ch)[1]"
:key="i"
@edit="editChild"
@unlink="unlinkChild"
:active="active"
:item="ch"
:color="colors[i%colors.length]"
:value="Object.values(ch)[1]"
:key="i"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
</template>
</div>
@ -35,10 +35,25 @@
where: `~not(${childForeignKey},eq,${parentId})~or(${childForeignKey},is,null)`,
}"/>
<!-- <list-child-items
v-if="childListModal"
v-model="childListModal"
:size="10"
:meta="childMeta"
:parent-meta="meta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
:api="childApi"
:query-params="childQueryParams"
@new-record="showNewRecordModal"
@edit="editChild"
@unlink="unlinkChild"
/>-->
<v-dialog v-if="childListModal" v-model="childListModal" width="600">
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">{{ childMeta ? childMeta._tn : 'Children' }}
<v-card-title class="textColor&#45;&#45;text mx-2">{{ childMeta ? childMeta._tn : 'Children' }}
<v-spacer>
</v-spacer>
@ -77,8 +92,9 @@
</x-icon>
</div>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[childPrimaryCol] }}
<span class="grey--text caption primary-key"
<v-card-title class="primary-value textColor&#45;&#45;text text&#45;&#45;lighten-2">
{{ ch[childPrimaryCol] }}
<span class="grey&#45;&#45;text caption primary-key"
v-if="childPrimaryKey">(Primary Key : {{ ch[childPrimaryKey] }})</span>
</v-card-title>
</v-card>
@ -151,10 +167,12 @@ import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
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";
export default {
name: "has-many-cell",
components: {
ListChildItems,
ItemChip,
ListItems,
Pagination,
@ -266,13 +284,6 @@ export default {
async showNewRecordModal() {
await this.loadChildMeta();
this.newRecordModal = true;
// const _cn = this.childForeignKey;
// this.list = await this.childApi.paginatedList({
// ...this.childQueryParams,
// limit: this.listPagination.size,
// offset: this.listPagination.size * (this.listPagination.page - 1),
// where: `~not(${_cn},eq,${this.parentId})~or(${_cn},is,null)`
// })
},
async addChildToParent(child) {
const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
@ -383,6 +394,7 @@ export default {
}
}
/*
.child-list-modal {
position: relative;
@ -400,6 +412,7 @@ export default {
}
}
*/
.child-card {
cursor: pointer;
@ -409,6 +422,7 @@ export default {
}
}
.hm-items {
//min-width: 200px;
//max-width: 400px;

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

@ -18,7 +18,7 @@
</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>-->
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
@ -30,14 +30,33 @@
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
v-model="newRecordModal"
:api="childApi"
:api="api"
:mm="mm"
:parent-id="row && row[parentPrimaryKey]"
@add-new-record="insertAndAddNewChildRecord"
@add="addChildToParent"
:query-params="{...childQueryParams, conditionGraph }"/>
:query-params="childQueryParams"/>
<v-dialog v-if="childListModal" v-model="childListModal" width="600">
<list-child-items
v-if="childListModal"
v-model="childListModal"
:size="10"
:meta="childMeta"
:parent-meta="meta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
:api="childApi"
:mm="mm"
:parent-id="row && row[parentPrimaryKey]"
:query-params="{...childQueryParams, conditionGraph }"
@new-record="showNewRecordModal"
@edit="editChild"
@unlink="unlinkChild"
/>
<!--<v-dialog v-if="childListModal" v-model="childListModal" width="600">
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">{{ childMeta ? childMeta._tn : 'Children' }}</v-card-title>
<v-card-title class="textColor&#45;&#45;text mx-2">{{ childMeta ? childMeta._tn : 'Children' }}</v-card-title>
<v-card-text>
<div class="items-container">
@ -57,8 +76,8 @@
>mdi-delete-outline
</x-icon>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[childPrimaryCol] }}
<span class="grey--text caption primary-key"
<v-card-title class="primary-value textColor&#45;&#45;text text&#45;&#45;lighten-2">{{ ch[childPrimaryCol] }}
<span class="grey&#45;&#45;text caption primary-key"
v-if="childPrimaryKey">(Primary Key : {{ ch[childPrimaryKey] }})</span>
</v-card-title>
</v-card>
@ -73,7 +92,7 @@
</v-card-actions>
</v-card>
</v-dialog>
-->
<v-dialog
:overlay-opacity="0.8"
@ -152,11 +171,12 @@ import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
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";
import ListChildItems from "@/components/project/spreadsheet/components/virtualCell/components/listChildItems";
export default {
name: "many-to-many-cell",
mixins: [colors],
components: {ItemChip, ListItems, DlgLabelSubmitCancel},
components: {ListChildItems, ItemChip, ListItems, DlgLabelSubmitCancel},
props: {
value: [Object, Array],
meta: [Object],
@ -192,13 +212,8 @@ export default {
}
},
async showChildListModal() {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
this.childListModal = true;
await this.loadChildMeta();
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;
this.childList = await this.childApi.paginatedList({
where: `(${_cn},eq,${pid})`
})
}, async unlinkChild(child) {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
@ -337,15 +352,15 @@ export default {
}
},
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]
// }
// }
// }
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'];

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

@ -707,7 +707,7 @@ class BaseModelSql extends BaseModel {
try {
let {fields, where, limit, offset, sort, condition, conditionGraph = null} = this._getListArgs(args);
let {fields, where, limit, offset, sort, condition, conditionGraph = null} = this._getListArgs(args);
// if (fields === '*') {
// fields = `${this.tn}.*`;
@ -1326,6 +1326,62 @@ class BaseModelSql extends BaseModel {
}
}
// todo: naming
public m2mNotChildren({pid = null, assoc = null, ...args}): Promise<any> {
if (pid === null || assoc === null) {
return null;
}
// @ts-ignore
const {tn, cn, vtn, vcn, vrcn, rtn, rcn} = this.manyToManyRelations.find(({vtn}) => assoc === vtn) || {};
const childModel = this.dbModels[rtn];
const {fields, where, limit, offset, sort, condition, conditionGraph = null} = childModel._getListArgs(args);
const query = childModel.$db
.select(childModel.selectQuery(fields))
.xwhere(where, childModel.selectQuery(''))
.condition(condition, childModel.selectQuery(''))
.conditionGraph(conditionGraph)
.whereNotIn(rcn,
childModel.dbDriver(rtn)
.select(`${rtn}.${rcn}`)
.join(vtn, `${rtn}.${rcn}`, `${vtn}.${vrcn}`)
.where(`${vtn}.${vcn}`, pid)
);
childModel._paginateAndSort(query, {limit, offset, sort});
return this._run(query);
}
// todo: naming
public m2mNotChildrenCount({pid = null, assoc = null, ...args}): Promise<any> {
if (pid === null || assoc === null) {
return null;
}
// @ts-ignore
const {tn, cn, vtn, vcn, vrcn, rtn, rcn} = this.manyToManyRelations.find(({vtn}) => assoc === vtn) || {};
const childModel = this.dbModels[rtn];
const {where, condition, conditionGraph = null} = childModel._getListArgs(args);
const query = childModel.$db
.count(`${rcn} as count`)
.xwhere(where, childModel.selectQuery(''))
.condition(condition, childModel.selectQuery(''))
.conditionGraph(conditionGraph)
.whereNotIn(rcn,
childModel.dbDriver(rtn)
.select(`${rtn}.${rcn}`)
.join(vtn, `${rtn}.${rcn}`, `${vtn}.${vrcn}`)
.where(`${vtn}.${vcn}`, pid)
).first();
return this._run(query);
}
/**
* Gets child list along with its parent
*

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

@ -100,6 +100,9 @@ export class RestCtrl extends RestBaseCtrl {
}
public async count(req: Request | any, res: Response): Promise<void> {
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.countByPk({
...req.query
} as any);
@ -151,6 +154,27 @@ export class RestCtrl extends RestBaseCtrl {
res.xcJson(data);
}
public async m2mNotChildren(req: Request | any, res): Promise<void> {
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 list = await req.model.m2mNotChildren({
...req.query,
...req.params
} as any);
const count = await req.model.m2mNotChildrenCount({
...req.query,
...req.params
} as any);
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.xcJson({list, info: count});
}
protected async middleware(req: Request | any, res: Response, next: NextFunction): Promise<any> {
req.model = this.model;

11
packages/nocodb/src/lib/sqlMgr/code/routes/xc-ts/ExpressXcTsRoutes.ts

@ -81,6 +81,17 @@ async function(req, res){
res.json(data);
}
`]
},, {
path: `/api/${this.ctx.routeVersionLetter}/${ejsData._tn}/m2mNotChildren/:assoc/:pid`,
type: 'get',
handler: ['m2mNotChildren'],
acl: {
admin: true,
user: true,
guest: true
},
functions: [`
`]
}, {
path: `/api/${this.ctx.routeVersionLetter}/${ejsData._tn}/groupby/:column_name`,
type: 'get',

Loading…
Cancel
Save