Browse Source

feat: has many relation creation

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/341/head
Pranav C 3 years ago
parent
commit
2e86e8f448
  1. 12
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  2. 55
      packages/nc-gui/components/project/spreadsheet/components/pagination.vue
  3. 132
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue
  4. 161
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  5. 68
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  6. 16
      packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts

12
packages/nc-gui/components/project/spreadsheet/components/editColumn.vue

@ -88,6 +88,7 @@
ref="relation" ref="relation"
:column="newColumn" :column="newColumn"
:nodes="nodes" :nodes="nodes"
:meta="meta"
@onColumnSelect="onRelColumnSelect" @onColumnSelect="onRelColumnSelect"
></linked-to-another-options> ></linked-to-another-options>
</v-col> </v-col>
@ -319,6 +320,7 @@ export default {
value: Boolean value: Boolean
}, },
data: () => ({ data: () => ({
valid: false,
relationDeleteDlg: false, relationDeleteDlg: false,
newColumn: {}, newColumn: {},
uiTypes, uiTypes,
@ -368,6 +370,11 @@ export default {
return this.$toast.info('Coming Soon...').goAway(3000) return this.$toast.info('Coming Soon...').goAway(3000)
} }
if (this.isLinkToAnotherRecord && this.$refs.relation) {
await this.$refs.relation.saveRelation();
return this.$emit('saved');
}
this.newColumn.tn = this.nodes.tn; this.newColumn.tn = this.nodes.tn;
this.newColumn._cn = this.newColumn.cn; this.newColumn._cn = this.newColumn.cn;
@ -494,7 +501,10 @@ export default {
}, },
computed: { computed: {
isEditDisabled() { isEditDisabled() {
return this.editColumn && this.sqlUi === SqliteUi; return this.editColumn && this.sqlUi === SqliteUi;
},
isSQLite() {
return this.sqlUi === SqliteUi;
}, },
dataTypes() { dataTypes() {
return this.sqlUi.getDataTypeListForUiType(this.newColumn) return this.sqlUi.getDataTypeListForUiType(this.newColumn)

55
packages/nc-gui/components/project/spreadsheet/components/pagination.vue

@ -0,0 +1,55 @@
<template>
<v-pagination
v-if="count !== Infinity"
style="max-width: 100%"
v-model="page"
:length="Math.ceil(count / size)"
:total-visible="8"
@input="$emit('input',page)"
color="primary lighten-2"
></v-pagination>
<div v-else class="mx-auto d-flex align-center mt-n1 " style="max-width:250px">
<span class="caption" style="white-space: nowrap"> Change page:</span>
<v-text-field
class="ml-1 caption"
:full-width="false"
outlined
dense
hide-details
v-model="page"
@keydown.enter="$emit('input',page)"
type="number"
>
<template #append>
<x-icon tooltip="Change page" small icon.class="mt-1" @click="$emit('input',page)">mdi-keyboard-return
</x-icon>
</template>
</v-text-field>
</div>
</template>
<script>
export default {
props: {
count: Number,
value: Number,
size: Number,
},
data: () => ({
page: 1
}),
mounted() {
this.page = this.value;
},
watch: {
value(v) {
this.page = v;
}
},
name: "pagination"
}
</script>
<style scoped>
</style>

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

@ -0,0 +1,132 @@
<template>
<v-dialog v-if="value" v-model="show" width="600">
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">Add Record
<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-text>
<v-text-field
hide-details
dense
outlined
placeholder="Search record"
class="mb-2 mx-2 caption"
/>
<div class="items-container">
<template v-if="data">
<v-card
v-for="(ch,i) in data.list"
class="ma-2 child-card"
outlined
v-ripple
@click="$emit('add',ch)"
:key="i"
>
<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="list"
:size="size"
:count="count"
v-model="page"
@input="loadData"
class="mb-3"
></pagination>
</v-card-actions>
</v-card>
</v-dialog>
<span v-else></span>
</template>
<script>
export default {
name: "listItems",
props: {
value: Boolean,
title: String,
primaryKey: String,
primaryCol: String,
meta: Object,
size: Number,
api: [Object, Function]
},
data: () => ({
data: null,
page: 1
}),
mounted() {
},
methods: {
async loadData() {
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
})
}
},
computed: {
show: {
set(v) {
this.$emit('input', v)
}, get() {
return this.value;
}
}
},
watch: {
value(v) {
if (v) {
this.loadData();
}
}
}
}
</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;
}
}
.child-card {
cursor: pointer;
&:hover {
box-shadow: 0 0 .2em var(--v-textColor-lighten5)
}
}
</style>

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

@ -15,10 +15,25 @@
<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
:count="10"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
v-model="newRecordModal"
:api="childApi"
></list-items>-->
<v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600"> <v-dialog v-if="newRecordModal" 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">Add Record</v-card-title> <v-card-title class="textColor--text mx-2">Add Record
<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-text> <v-card-text>
<v-text-field <v-text-field
hide-details hide-details
@ -43,14 +58,21 @@
v-if="childPrimaryKey">(Primary Key : {{ ch[childPrimaryKey] }})</span> v-if="childPrimaryKey">(Primary Key : {{ ch[childPrimaryKey] }})</span>
</v-card-title> </v-card-title>
</v-card> </v-card>
</template> </template>
</div> </div>
</v-card-text> </v-card-text>
<v-card-actions class="justify-center pb-6 "> <v-card-actions class="justify-center py-2 flex-column">
<v-btn small outlined class="caption" color="primary">
<v-icon>mdi-plus</v-icon> <pagination
Add New Record v-if="list"
</v-btn> :size="listPagination.size"
:count="list.count"
v-model="listPagination.page"
@input="showNewRecordModal"
class="mb-3"
></pagination>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -58,7 +80,16 @@
<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> <v-card-title class="textColor--text mx-2">{{ childMeta ? childMeta._tn : 'Children' }}
<v-spacer>
</v-spacer>
<v-btn small class="caption" color="primary" @click="showNewRecordModal">
<v-icon small>mdi-plus</v-icon>&nbsp;
Add Record
</v-btn>
</v-card-title>
<v-card-text> <v-card-text>
<div class="items-container"> <div class="items-container">
@ -70,13 +101,23 @@
:key="i" :key="i"
@click="editChild(ch)" @click="editChild(ch)"
> >
<x-icon <div class="remove-child-icon d-flex align-center">
class="remove-child-icon" <x-icon
:color="['error','grey']" :tooltip="`Unlink this '${childMeta._tn}' from '${meta._tn}'`"
small :color="['error','grey']"
@click.stop="removeChild(ch,i)" small
>mdi-delete-outline @click.stop="unlinkChild(ch,i)"
</x-icon> icon.class="mr-1 mt-n1"
>mdi-link-variant-remove
</x-icon>
<x-icon
:tooltip="`Delete row in '${childMeta._tn}'`"
:color="['error','grey']"
small
@click.stop="deleteChild(ch,i)"
>mdi-delete-outline
</x-icon>
</div>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[childPrimaryCol] }} <v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[childPrimaryCol] }}
<span class="grey--text caption primary-key" <span class="grey--text caption primary-key"
@ -86,11 +127,15 @@
</template> </template>
</div> </div>
</v-card-text> </v-card-text>
<v-card-actions class="justify-center pb-6"> <v-card-actions class="justify-center py-2 flex-column">
<v-btn small outlined class="caption" color="primary" @click="showNewRecordModal"> <pagination
<v-icon>mdi-plus</v-icon> v-if="childList"
Add Record :size="childListPagination.size"
</v-btn> :count="childList.count"
v-model="childListPagination.page"
@input="showChildListModal"
class="mb-3"
></pagination>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -127,8 +172,13 @@
:sql-ui="sqlUi" :sql-ui="sqlUi"
:primary-value-column="childPrimaryCol" :primary-value-column="childPrimaryCol"
:api="childApi" :api="childApi"
:available-columns="childAvailableColumns"
icon-color="warning" icon-color="warning"
:nodes="nodes"
:query-params="childQueryParams"
></component> ></component>
{{childQueryParams}}
</v-dialog> </v-dialog>
</div> </div>
@ -138,10 +188,12 @@
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 Pagination from "@/components/project/spreadsheet/components/pagination";
export default { export default {
name: "has-many-cell", name: "has-many-cell",
components: { components: {
Pagination,
DlgLabelSubmitCancel DlgLabelSubmitCancel
}, },
mixins: [colors], mixins: [colors],
@ -165,6 +217,14 @@ export default {
confirmMessage: '', confirmMessage: '',
selectedChild: null, selectedChild: null,
showExpandModal: false, showExpandModal: false,
listPagination: {
page: 1,
size: 10
},
childListPagination: {
page: 1,
size: 10
}
}), }),
methods: { methods: {
@ -174,10 +234,13 @@ export default {
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})`,
limit: this.childListPagination.size,
offset: this.childListPagination.size * (this.childListPagination.page - 1),
...this.childQueryParams
}) })
}, },
async removeChild(child) { async deleteChild(child) {
this.dialogShow = true; this.dialogShow = true;
this.confirmMessage = this.confirmMessage =
'Do you want to delete the record?'; 'Do you want to delete the record?';
@ -193,6 +256,23 @@ export default {
} }
} }
}, },
async unlinkChild(child) {
// todo:
// this.dialogShow = true;
// this.confirmMessage =
// 'Do you want to delete the record?';
// this.confirmAction = async act => {
// if (act === 'hideDialog') {
// this.dialogShow = false;
// } 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')
// }
// }
},
async getChildMeta() { async getChildMeta() {
// todo: optimize // todo: optimize
if (!this.childMeta) { if (!this.childMeta) {
@ -202,13 +282,17 @@ export default {
}, 'tableXcModelGet', { }, 'tableXcModelGet', {
tn: this.hm.tn tn: this.hm.tn
}]); }]);
this.childMeta = JSON.parse(childTableData.meta) this.childMeta = JSON.parse(childTableData.meta);
// this.childQueryParams = JSON.parse(childTableData.query_params);
} }
}, },
async showNewRecordModal() { async showNewRecordModal() {
this.newRecordModal = true; this.newRecordModal = true;
await this.getChildMeta(); await this.getChildMeta();
this.list = await this.childApi.paginatedList({}) this.list = await this.childApi.paginatedList({
limit: this.listPagination.size,
offset: this.listPagination.size * (this.listPagination.page - 1)
})
}, },
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('___');
@ -223,8 +307,11 @@ export default {
}); });
this.$emit('loadTableData') this.$emit('loadTableData')
if(this.childListModal){
await this.showChildListModal()
}
}, },
async editChild(child) { async editChild(child) {
this.selectedChild = child; this.selectedChild = child;
this.showExpandModal = true; this.showExpandModal = true;
} }
@ -242,10 +329,30 @@ export default {
return this.childMeta && (this.childMeta.columns.find(c => c.pk) || {})._cn return this.childMeta && (this.childMeta.columns.find(c => c.pk) || {})._cn
}, },
// todo: // todo:
form(){ form() {
return () => import("@/components/project/spreadsheet/components/expandedForm") return () => import("@/components/project/spreadsheet/components/expandedForm")
} },
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;
},
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()) || ''
}
}
} }
} }
</script> </script>

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

@ -70,11 +70,11 @@
<fields <fields
v-model="showFields" v-model="showFields"
:field-list="fieldList" :field-list="fieldList"
:meta="meta" :meta="meta"
:is-locked="isLocked" :is-locked="isLocked"
:fieldsOrder.sync="fieldsOrder" :fieldsOrder.sync="fieldsOrder"
:sqlUi="sqlUi" :sqlUi="sqlUi"
:showSystemFields.sync="showSystemFields" :showSystemFields.sync="showSystemFields"
/> />
<sort-list <sort-list
@ -220,33 +220,39 @@
</div> </div>
<template v-if="data"> <template v-if="data">
<v-pagination <pagination
v-if="count !== Infinity" :count="count"
style="max-width: 100%" :size="size"
v-model="page" v-model="page"
:length="Math.ceil(count / size)"
:total-visible="8"
@input="loadTableData" @input="loadTableData"
color="primary lighten-2" />
></v-pagination> <!-- <v-pagination
<div v-else class="mx-auto d-flex align-center mt-n1 " style="max-width:250px"> v-if="count !== Infinity"
<span class="caption" style="white-space: nowrap"> Change page:</span> style="max-width: 100%"
<v-text-field
class="ml-1 caption"
:full-width="false"
outlined
dense
hide-details
v-model="page" v-model="page"
@keydown.enter="loadTableData" :length="Math.ceil(count / size)"
type="number" :total-visible="8"
> @input="loadTableData"
<template #append> color="primary lighten-2"
<x-icon tooltip="Change page" small icon.class="mt-1" @click="loadTableData">mdi-keyboard-return ></v-pagination>
</x-icon> <div v-else class="mx-auto d-flex align-center mt-n1 " style="max-width:250px">
</template> <span class="caption" style="white-space: nowrap"> Change page:</span>
</v-text-field> <v-text-field
</div> class="ml-1 caption"
:full-width="false"
outlined
dense
hide-details
v-model="page"
@keydown.enter="loadTableData"
type="number"
>
<template #append>
<x-icon tooltip="Change page" small icon.class="mt-1" @click="loadTableData">mdi-keyboard-return
</x-icon>
</template>
</v-text-field>
</div>-->
</template> </template>
<!-- <div v-else class="d-flex justify-center py-4">--> <!-- <div v-else class="d-flex justify-center py-4">-->
<!-- <v-alert type="info" dense class="ma-1 flex-shrink-1">Table is empty</v-alert>--> <!-- <v-alert type="info" dense class="ma-1 flex-shrink-1">Table is empty</v-alert>-->
@ -411,7 +417,7 @@
@commented="reloadComments" @commented="reloadComments"
:availableColumns="availableColumns" :availableColumns="availableColumns"
:nodes="nodes" :nodes="nodes"
:queryParams="queryParams" :query-params="queryParams"
></expanded-form> ></expanded-form>
</v-dialog> </v-dialog>
@ -452,11 +458,13 @@ import SpreadsheetNavDrawer from "@/components/project/spreadsheet/components/sp
import spreadsheet from "@/components/project/spreadsheet/mixins/spreadsheet"; import spreadsheet from "@/components/project/spreadsheet/mixins/spreadsheet";
import LockMenu from "@/components/project/spreadsheet/components/lockMenu"; import LockMenu from "@/components/project/spreadsheet/components/lockMenu";
import ExpandedForm from "@/components/project/spreadsheet/components/expandedForm"; import ExpandedForm from "@/components/project/spreadsheet/components/expandedForm";
import Pagination from "@/components/project/spreadsheet/components/pagination";
export default { export default {
mixins: [spreadsheet], mixins: [spreadsheet],
name: "rows-xc-data-table", name: "rows-xc-data-table",
components: { components: {
Pagination,
ExpandedForm, ExpandedForm,
LockMenu, LockMenu,
SpreadsheetNavDrawer, SpreadsheetNavDrawer,

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

@ -1020,6 +1020,13 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
hasMany: meta.hasMany, hasMany: meta.hasMany,
}); });
/* Add new has many relation to virtual columns */
oldMeta.v = oldMeta.v || [];
oldMeta.v.push({
hm: meta.hasMany.find(hm => hm.rtn === tnp && hm.tn === tnc),
_cn: `${this.getTableNameAlias(tnp)} => ${this.getTableNameAlias(tnc)}`
})
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', { await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
title: tnp, title: tnp,
meta: JSON.stringify(oldMeta), meta: JSON.stringify(oldMeta),
@ -1093,6 +1100,14 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
Object.assign(oldMeta, { Object.assign(oldMeta, {
belongsTo: meta.belongsTo, belongsTo: meta.belongsTo,
}); });
/* Add new belongs to relation to virtual columns */
oldMeta.v = oldMeta.v || [];
oldMeta.v.push({
bt: meta.belongsTo.find(hm => hm.rtn === tnp && hm.tn === tnc),
_cn: `${this.getTableNameAlias(tnp)} <= ${this.getTableNameAlias(tnc)}`
})
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', { await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
title: tnc, title: tnc,
meta: JSON.stringify(oldMeta) meta: JSON.stringify(oldMeta)
@ -1661,4 +1676,3 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */

Loading…
Cancel
Save