Browse Source

Merge branch 'has-many'

# Conflicts:
#	packages/nocodb/package-lock.json
#	packages/nocodb/package.json
pull/341/head
Pranav C 3 years ago
parent
commit
b2436d5355
  1. 27
      README.md
  2. 6
      packages/nc-gui/assets/style.css
  3. 2
      packages/nc-gui/assets/style/style.css
  4. 8
      packages/nc-gui/components/ProjectTreeView.vue
  5. 2
      packages/nc-gui/components/ProjectTreeViewOld.vue
  6. 2
      packages/nc-gui/components/auth/userManagement.vue
  7. 2
      packages/nc-gui/components/createOrEditProject.vue
  8. 4
      packages/nc-gui/components/project/appStore/FormInput.vue
  9. 76
      packages/nc-gui/components/project/appStore/inputs/datePickerCell.vue
  10. 93
      packages/nc-gui/components/project/appStore/inputs/dateTimePickerCell.vue
  11. 89
      packages/nc-gui/components/project/appStore/inputs/enumListEditableCell.vue
  12. 100
      packages/nc-gui/components/project/appStore/inputs/setListCheckboxCell.vue
  13. 91
      packages/nc-gui/components/project/appStore/inputs/setListEditableCell.vue
  14. 78
      packages/nc-gui/components/project/appStore/inputs/timePickerCell.vue
  15. 4
      packages/nc-gui/components/project/spreadsheet/apis/apiFactory.js
  16. 109
      packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js
  17. 18
      packages/nc-gui/components/project/spreadsheet/apis/restApi.js
  18. 30
      packages/nc-gui/components/project/spreadsheet/components/cell.vue
  19. 0
      packages/nc-gui/components/project/spreadsheet/components/cell/attachmentCell.vue
  20. 0
      packages/nc-gui/components/project/spreadsheet/components/cell/enumCell.vue
  21. 33
      packages/nc-gui/components/project/spreadsheet/components/cell/setListCell.vue
  22. 4
      packages/nc-gui/components/project/spreadsheet/components/columnFilterMenu.vue
  23. 35
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  24. 0
      packages/nc-gui/components/project/spreadsheet/components/editColumn/customSelectOptions.vue
  25. 302
      packages/nc-gui/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions.vue
  26. 29
      packages/nc-gui/components/project/spreadsheet/components/editColumn/relationOptions.vue
  27. 131
      packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue
  28. 55
      packages/nc-gui/components/project/spreadsheet/components/editableCell.vue
  29. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/booleanCell.vue
  30. 6
      packages/nc-gui/components/project/spreadsheet/components/editableCell/datePickerCell.vue
  31. 20
      packages/nc-gui/components/project/spreadsheet/components/editableCell/dateTimePickerCell.vue
  32. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue
  33. 6
      packages/nc-gui/components/project/spreadsheet/components/editableCell/enumListEditableCell.vue
  34. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/enumRadioEditableCell.vue
  35. 2
      packages/nc-gui/components/project/spreadsheet/components/editableCell/floatCell.vue
  36. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/integerCell.vue
  37. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/jsonCell.vue
  38. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/setListCheckboxCell.vue
  39. 5
      packages/nc-gui/components/project/spreadsheet/components/editableCell/setListEditableCell.vue
  40. 31
      packages/nc-gui/components/project/spreadsheet/components/editableCell/textAreaCell.vue
  41. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/textAreaCellOld.vue
  42. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/textCell.vue
  43. 0
      packages/nc-gui/components/project/spreadsheet/components/editableCell/timePickerCell.vue
  44. 177
      packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue
  45. 62
      packages/nc-gui/components/project/spreadsheet/components/headerCell.vue
  46. 54
      packages/nc-gui/components/project/spreadsheet/components/pagination.vue
  47. 2
      packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue
  48. 144
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  49. 399
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue
  50. 43
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/item-chip.vue
  51. 220
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItems.vue
  52. 118
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal.vue
  53. 225
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue
  54. 467
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  55. 505
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue
  56. 181
      packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
  57. 203
      packages/nc-gui/components/project/spreadsheet/editColumn/linkedToAnotherOptions.vue
  58. 101
      packages/nc-gui/components/project/spreadsheet/editableCell/enumRadioEditableCell.vue
  59. 69
      packages/nc-gui/components/project/spreadsheet/editableCell/integerCell.vue
  60. 97
      packages/nc-gui/components/project/spreadsheet/editableCell/jsonCell.vue
  61. 8
      packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js
  62. 33
      packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js
  63. 207
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  64. 359
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  65. 2
      packages/nc-gui/components/project/spreadsheet/xcTable.vue
  66. 82
      packages/nc-gui/components/project/table.vue
  67. 26
      packages/nc-gui/components/project/tableTabs/columns.vue
  68. 2
      packages/nc-gui/components/project/tableTabs/customAcl.vue
  69. 2
      packages/nc-gui/components/project/viewTabs/viewSpreadsheet.vue
  70. 4
      packages/nc-gui/components/project/xcInfo.vue
  71. 2
      packages/nc-gui/components/sponsorMini.vue
  72. 2
      packages/nc-gui/components/sponsorOverlay.vue
  73. 2
      packages/nc-gui/components/utils/dlgProjectCreate.vue
  74. 6
      packages/nc-gui/config/vuetify.options.js
  75. 1
      packages/nc-gui/helpers/index.js
  76. 12
      packages/nc-gui/helpers/treeViewIcons.js
  77. 4
      packages/nc-gui/helpers/treeViewIconsColors.js
  78. 26
      packages/nc-gui/layouts/default.vue
  79. 7
      packages/nc-gui/mixins/device.js
  80. 5
      packages/nc-gui/nuxt.config.js
  81. 2
      packages/nc-gui/pages/project/id.vue
  82. 2
      packages/nc-gui/pages/project/name.vue
  83. 2
      packages/nc-gui/pages/project/xcdb.vue
  84. 2
      packages/nc-gui/pages/projects/index.vue
  85. 4
      packages/nc-gui/plugins/globalMixin.js
  86. 73
      packages/nc-gui/store/meta.js
  87. 1
      packages/nc-gui/store/project.js
  88. 35
      packages/nc-gui/store/sqlMgr.js
  89. 6
      packages/nc-gui/store/windows.js
  90. 1
      packages/nocodb/package.json
  91. 2
      packages/nocodb/src/example/docker.ts
  92. 1
      packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts
  93. 217
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts
  94. 5
      packages/nocodb/src/lib/migrator/SqlMigrator/lib/KnexMigrator.ts
  95. 2
      packages/nocodb/src/lib/migrator/util/file.help.ts
  96. 21
      packages/nocodb/src/lib/noco/NcProjectBuilder.ts
  97. 314
      packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts
  98. 5
      packages/nocodb/src/lib/noco/common/XcMigrationSource.ts
  99. 558
      packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts
  100. 53
      packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts
  101. Some files were not shown because too many files have changed in this diff Show More

27
README.md

@ -238,3 +238,30 @@ Our mission is to provide the most powerful no-code interface for databases whic
- - - - - - -
### Has Many - Schema
- Create
* Create a new relation
- Create foreign key in child table
- Update metadata
- GUI
-
* Existing
- We need to show existing relation
- Update metadata
- Delete
* Delete virtual field
* Delete virtual field and relation
- HasMany Lookup column
* Rendering
* Adding new child
- Associate an existing child
- Create and add
* Pagination
* Remove child

6
packages/nc-gui/assets/style.css

@ -274,10 +274,14 @@ tbody tr:nth-of-type(odd) {
overflow: auto;
}
.table-tabs > .v-tabs-items {
.table-tabs:not(.hidden-tab) > .v-tabs-items {
height: calc(100% - 30px);
overflow: auto;
}
.table-tabs.hidden-tab > .v-tabs-items {
height: 100%;
overflow: auto;
}
.table-tabs > .v-tabs-items > .v-window__container {
height: 100%;

2
packages/nc-gui/assets/style/style.css

@ -336,7 +336,7 @@ html {
min-height: 40px !important;
}
.api-treeview .vtl-node-content{
max-width: calc(100% - 60px);
max-width: calc(100% - 44px);
}
/* for expansion panel content wrapper */
.expansion-wrap-0 .v-expansion-panel-content__wrap{

8
packages/nc-gui/components/ProjectTreeView.vue

@ -448,7 +448,7 @@
<!-- <v-tooltip bottom>-->
<!-- <template v-slot:activator="{on}">-->
<!-- <v-list-item dense v-on="on" @click="openLink('https://github.com/sponsors/xgenecloud')" class="body-2">-->
<!-- <v-list-item dense v-on="on" @click="openLink('https://github.com/sponsors/nocodb')" class="body-2">-->
<!-- <v-list-item-icon>-->
<!-- <v-icon color="red" class=" heart-anim" small> mdi-heart</v-icon>-->
<!-- </v-list-item-icon>-->
@ -515,21 +515,21 @@
<v-icon color="red" class=" heart-anim" small> mdi-heart</v-icon>
</v-list-item-title>
</v-list-item>
<v-list-item dense v-on="on" @click="openLink('https://github.com/sponsors/xgenecloud')" class="body-2">
<v-list-item dense v-on="on" @click="openLink('https://github.com/sponsors/nocodb')" class="body-2">
<v-list-item-icon>
</v-list-item-icon>
<v-list-item-title><span class="font-weight-regular caption">Sponsor Us</span></v-list-item-title>
</v-list-item>
<v-list-item dense v-on="on" @click="openLink('https://github.com/sponsors/xgenecloud')" class="body-2">
<v-list-item dense v-on="on" @click="openLink('https://github.com/sponsors/nocodb')" class="body-2">
<v-list-item-icon>
<v-icon color="red" class=" heart-anim" small> mdi-heart</v-icon>
</v-list-item-icon>
<v-list-item-title><span class="font-weight-regular caption">Sponsor Us</span></v-list-item-title>
</v-list-item>
<v-list-item dense v-on="on" @click="openLink('https://github.com/sponsors/xgenecloud')" class="body-2">
<v-list-item dense v-on="on" @click="openLink('https://github.com/sponsors/nocodb')" class="body-2">
<v-list-item-icon>
<v-icon color="red" class=" heart-anim" small> mdi-heart</v-icon>
</v-list-item-icon>

2
packages/nc-gui/components/ProjectTreeViewOld.vue

@ -153,7 +153,7 @@
<v-tooltip bottom>
<template v-slot:activator="{on}">
<div class="d-100 d-flex" v-on="on">
<v-icon color="orange" class="mr-1" small>mdi-settings</v-icon>
<v-icon color="orange" class="mr-1" small>mdi-cog</v-icon>
<span>Settings</span>
</div>
</template>

2
packages/nc-gui/components/auth/userManagement.vue

@ -450,7 +450,7 @@
</template>
<script>
import SetListCheckboxCell from "@/components/project/spreadsheet/editableCell/setListCheckboxCell";
import SetListCheckboxCell from "@/components/project/spreadsheet/components/editableCell/setListCheckboxCell";
import {enumColor as colors, enumColor} from "@/components/project/spreadsheet/helpers/colors";
import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import {isEmail} from "@/helpers";

2
packages/nc-gui/components/createOrEditProject.vue

@ -1043,7 +1043,7 @@ export default {
{ text: 'Disabled', value: 'none' },
],
projectTypes: [
{ text: 'REST APIs', value: 'rest', icon: 'mdi-json', iconColor: 'green' },
{ text: 'REST APIs', value: 'rest', icon: 'mdi-code-json', iconColor: 'green' },
{ text: 'GRAPHQL APIs', value: 'graphql', icon: 'mdi-graphql', iconColor: 'pink' },
// {
// text: 'Automatic gRPC APIs on database',

4
packages/nc-gui/components/project/appStore/FormInput.vue

@ -12,9 +12,9 @@
<script>
import TextField from "@/components/project/appStore/inputs/textField";
import EditableAttachmentCell from "@/components/project/spreadsheet/editableCell/editableAttachmentCell";
import EditableAttachmentCell from "@/components/project/spreadsheet/components/editableCell/editableAttachmentCell";
import Attachment from "@/components/project/appStore/inputs/attachment";
import TextAreaCell from "~/components/project/spreadsheet/editableCell/textAreaCell";
import TextAreaCell from "~/components/project/spreadsheet/components/editableCell/textAreaCell";
import PasswordField from "@/components/project/appStore/inputs/passwordField";
export default {

76
packages/nc-gui/components/project/appStore/inputs/datePickerCell.vue

@ -1,76 +0,0 @@
<template>
<v-menu>
<template v-slot:activator="{on}">
<div class="value" v-on="on">{{ localState }}</div>
</template>
<v-date-picker v-on="parentListeners" v-model="localState"></v-date-picker>
</v-menu>
</template>
<script>
export default {
name: "date-picker-cell", props: {
value: [String, Date]
},
mounted() {
if (this.$el && this.$el.$el) {
this.$el.$el.focus();
}
},
computed: {
localState: {
get() {
return typeof this.value === 'string' ? this.value.replace(/(\d)T(?=\d)/, '$1 ') : this.value;
},
set(val) {
this.$emit('input', new Date(val).toJSON().slice(0, 10));
}
},
parentListeners() {
const $listeners = {};
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur;
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
}
}
</script>
<style scoped>
.value {
width: 100%;
height: 100%;
min-height:20px;
}
</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/>.
*
*/
-->

93
packages/nc-gui/components/project/appStore/inputs/dateTimePickerCell.vue

@ -1,93 +0,0 @@
<template>
<v-datetime-picker
v-on="parentListeners"
class="caption xc-date-time-picker"
ref="picker"
:text-field-props="{
class:'caption mt-n1 pt-0'
}"
:time-picker-props="{
format:'24hr'
}"
v-model="localState"
></v-datetime-picker>
</template>
<script>
export default {
name: "date-time-picker-cell",
props: ['value', 'ignoreFocus'],
mounted() {
if (!this.ignoreFocus) {
this.$refs.picker.display = true;
}
},
computed: {
localState: {
get() {
if(/^\d{6,}$/.test(this.value)){
return new Date(+this.value);
}
return /\dT\d/.test(this.value) ? new Date(this.value.replace(/(\d)T(?=\d)/, '$1 ')) : this.value;
},
set(val) {
// if(/^\d{6,}$/.test(this.value)){
// return this.$emit('input', new Date(this.value).getTime());
// }
const uVal = new Date(val).toISOString().slice(0, 19).replace('T', ' ').replace(/(\d{1,2}:\d{1,2}):\d{1,2}$/,'$1');
console.log(val, uVal)
this.$emit('input', uVal);
}
},
parentListeners(){
const $listeners = {};
if(this.$listeners.blur){
$listeners.blur = this.$listeners.blur;
}
if(this.$listeners.focus){
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
}
}
</script>
<style scoped>
/deep/ .v-input, /deep/ .v-text-field {
margin-top: 0 !important;
padding-top: 0 !important;
font-size: inherit !important;
}
</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/>.
*
*/
-->

89
packages/nc-gui/components/project/appStore/inputs/enumListEditableCell.vue

@ -1,89 +0,0 @@
<template>
<select v-on="parentListeners" v-model="localState">
<option v-for="eVal of enumValues" :key="eVal" :value="eVal">{{ eVal }}</option>
</select>
</template>
<script>
export default {
name: "enum-list-cell",
props: {
value: String,
column: Object
},
mounted() {
this.$el.focus();
let event;
event = document.createEvent('MouseEvents');
event.initMouseEvent('mousedown', true, true, window);
this.$el.dispatchEvent(event);
},
computed: {
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val);
this.$emit('update');
}
},
enumValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/^'|'$/g, ''))
}
return [];
},
parentListeners(){
const $listeners = {};
if(this.$listeners.blur){
$listeners.blur = this.$listeners.blur;
}
if(this.$listeners.focus){
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
}
}
</script>
<style scoped>
select {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
-webkit-appearance: menulist;
/*webkit browsers */
-moz-appearance: menulist;
/*Firefox */
appearance: menulist;
}
</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/>.
*
*/
-->

100
packages/nc-gui/components/project/appStore/inputs/setListCheckboxCell.vue

@ -1,100 +0,0 @@
<template>
<div class="d-flex align-center">
<div>
<div class="" v-for="(val,i) of setValues" :key="val">
<input type="checkbox" :id="`key-check-box-${val}`" class="orange--text" v-model="localState" :value="val">
<label class="py-1 px-3 d-inline-block my-1 label" :for="`key-check-box-${val}`"
:style="{
background:colors[i % colors.length ]
}"
>{{ val }}</label>
</div>
</div>
</div>
</template>
<script>
import colors from "@/components/project/spreadsheet/helpers/colors";
export default {
name: "set-list-checkbox-cell",
props: {
value: String,
column: Object
},
data() {
},
mounted() {
this.$el.focus();
let event;
event = document.createEvent('MouseEvents');
event.initMouseEvent('mousedown', true, true, window);
this.$el.dispatchEvent(event);
},
computed: {
colors() {
return this.$store.state.windows.darkTheme ? colors.dark : colors.light;
},
localState: {
get() {
return this.value && this.value.split(',')
},
set(val) {
this.$emit('input', val.join(','));
this.$emit('update');
}
},
setValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/^'|'$/g, ''))
}
return [];
},
parentListeners() {
const $listeners = {};
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur;
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
}
}
</script>
<style scoped>
.label {
border-radius: 25px;
}
</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/>.
*
*/
-->

91
packages/nc-gui/components/project/appStore/inputs/setListEditableCell.vue

@ -1,91 +0,0 @@
<template>
<div>
<select v-on="parentListeners" v-model="localState" multiple>
<option v-for="val of setValues" :key="val" :value="val">{{ val }}</option>
</select>
</div>
</template>
<script>
export default {
name: "set-list-cell",
props: {
value: String,
column: Object
},
mounted() {
this.$el.focus();
let event;
event = document.createEvent('MouseEvents');
event.initMouseEvent('mousedown', true, true, window);
this.$el.dispatchEvent(event);
},
computed: {
localState: {
get() {
return this.value && this.value.split(',')
},
set(val) {
this.$emit('input', val.join(','));
this.$emit('update');
}
},
setValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/^'|'$/g, ''))
}
return [];
},
parentListeners(){
const $listeners = {};
if(this.$listeners.blur){
$listeners.blur = this.$listeners.blur;
}
if(this.$listeners.focus){
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
}
}
</script>
<style scoped>
select {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
-webkit-appearance: menulist;
/*webkit browsers */
-moz-appearance: menulist;
/*Firefox */
appearance: menulist;
}
</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/>.
*
*/
-->

78
packages/nc-gui/components/project/appStore/inputs/timePickerCell.vue

@ -1,78 +0,0 @@
<template>
<v-menu>
<template v-slot:activator="{on}">
<div class="value" v-on="on">{{ localState }}</div>
</template>
<v-time-picker v-on="parentListeners" v-model="localState"></v-time-picker>
</v-menu>
</template>
<script>
export default {
name: "time-picker-cell",
props: {
value: [String, Date]
},
mounted() {
if (this.$el && this.$el.$el) {
this.$el.$el.focus();
}
},
computed: {
localState: {
get() {
return typeof this.value === 'string' ? this.value.replace(/(\d)T(?=\d)/, '$1 ') : this.value;
},
set(val) {
this.$emit('input', (new Date(val).toJSON() || '').slice(0, 10));
}
},
parentListeners() {
const $listeners = {};
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur;
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus;
}
if (this.$listeners.cancel) {
$listeners.cancel = this.$listeners.cancel;
}
return $listeners;
},
}
}
</script>
<style scoped>
.value {
min-height: 20px;
}
</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/>.
*
*/
-->

4
packages/nc-gui/components/project/spreadsheet/apis/apiFactory.js

@ -3,9 +3,9 @@ import GqlApi from "@/components/project/spreadsheet/apis/gqlApi";
import GrpcApi from "@/components/project/spreadsheet/apis/grpcApi";
export default class ApiFactory {
static create(type, table, columns, ctx) {
static create(type, table, columns, ctx, meta) {
if (type === 'graphql') {
return new GqlApi(table, columns, ctx);
return new GqlApi(table, columns, meta, ctx,);
} else if (type === 'grpc') {
return new GrpcApi(table, ctx)
} else if (type === 'rest') {

109
packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js

@ -3,16 +3,17 @@ import inflection from 'inflection';
export default class GqlApi {
constructor(table, columns, $ctx) {
constructor(table, columns, meta, $ctx) {
// this.table = table;
this.columns = columns;
this.meta = meta;
this.$ctx = $ctx;
}
// todo: - get version letter and use table alias
async list(params) {
const data = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: this.gqlQuery(params),
query: await this.gqlQuery(params),
variables: null
});
return data.data.data[this.gqlQueryListName];
@ -51,15 +52,21 @@ export default class GqlApi {
if ('sort' in params) {
res.push(`sort: ${JSON.stringify(params.sort)}`)
}
if (params.condition) {
res.push(`condition: ${JSON.stringify(params.condition)}`)
}
if (params.conditionGraph) {
res.push(`conditionGraph: ${JSON.stringify(JSON.stringify(params.conditionGraph))}`)
}
return `(${res.join(',')})`;
}
gqlQuery(params) {
return `{${this.gqlQueryListName}${this.generateQueryParams(params)}${this.gqlReqBody}}`
async gqlQuery(params) {
return `{${this.gqlQueryListName}${this.generateQueryParams(params)}{${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}}`
}
gqlReadQuery(id) {
return `{${this.gqlQueryReadName}(id:"${id}")${this.gqlReqBody}}`
return `{${this.gqlQueryReadName}(id:"${id}"){${this.gqlReqBody}}}`
}
gqlCountQuery(params) {
@ -67,19 +74,63 @@ export default class GqlApi {
}
get gqlQueryListName() {
return `${this.table.replace(/(?:^|_)(.)/g, (_, m) => m.toUpperCase())}List`;
return `${this.meta._tn}List`;
}
get gqlQueryReadName() {
return `${this.table.replace(/(?:^|_)(.)/g, (_, m) => m.toUpperCase())}Read`;
return `${this.meta._tn}Read`;
}
get tableCamelized() {
return `${this.table.replace(/(?:^|_)(.)/g, (_, m) => m.toUpperCase())}`;
return `${this.meta._tn}`;
}
get gqlReqBody() {
return `{\n${this.columns.map(c => c._cn).join('\n')}\n}`
return `\n${this.columns.map(c => c._cn).join('\n')}\n`
}
async gqlRelationReqBody(params) {
let str = '';
if (params.childs) {
for (const child of params.childs.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.env,
tn: child
})
const meta = this.$ctx.$store.state.meta.metas[child];
if (meta) {
str += `\n${meta._tn}List{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
}
}
}
if (params.parents) {
for (const parent of params.parents.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.env,
tn: parent
})
const meta = this.$ctx.$store.state.meta.metas[parent];
if (meta) {
str += `\n${meta._tn}Read{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
}
}
}
if (params.many) {
for (const mm of params.many.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.env,
tn: mm
})
const meta = this.$ctx.$store.state.meta.metas[mm];
if (meta) {
str += `\n${meta._tn}MMList{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
}
}
}
return str;
}
get gqlQueryCountName() {
@ -102,18 +153,24 @@ export default class GqlApi {
async paginatedList(params) {
// const list = await this.list(params);
// const count = (await this.count({where: params.where || ''}));
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,
condition: params.condition
})
]);
return {list, count};
}
async update(id, data,oldData) {
async update(id, data, oldData) {
const data1 = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: `mutation update($id:String!, $data:${this.tableCamelized}Input){
${this.gqlMutationUpdateName}(id: $id, data: $data)
}`,
variables: {
id, data
id: id, data
}
});
@ -130,10 +187,9 @@ export default class GqlApi {
}
async insert(data) {
const data1 = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: `mutation create($data:${this.tableCamelized}Input){
${this.gqlMutationCreateName}(data: $data)${this.gqlReqBody}
${this.gqlMutationCreateName}(data: $data){${this.gqlReqBody}}
}`,
variables: {
data
@ -170,9 +226,32 @@ export default class GqlApi {
}
get table() {
return this.$ctx && this.$ctx.meta && this.$ctx.meta._tn && inflection.camelize(this.$ctx.meta._tn);
return this.meta && this.meta._tn && inflection.camelize(this.meta._tn);
}
async paginatedM2mNotChildrenList(params, assoc, pid) {
const list = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: `query m2mNotChildren($pid: String!,$assoc:String!,$parent:String!, $limit:Int, $offset:Int){
m2mNotChildren(pid: $pid,assoc:$assoc,parent:$parent,limit:$limit, offset:$offset)
}`,
variables: {
parent: this.meta.tn, assoc, pid: pid + '', ...params
}
});
const count = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: `query m2mNotChildrenCount($pid: String!,$assoc:String!,$parent:String!){
m2mNotChildrenCount(pid: $pid,assoc:$assoc,parent:$parent)
}`,
variables: {
parent: this.meta.tn, assoc, pid: pid + ''
}
});
return {list: list.data.data.m2mNotChildren, count: count.data.data.m2mNotChildrenCount.count};
}
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

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

@ -7,9 +7,8 @@ export default class RestApi {
// todo: - get version letter and use table alias
async list(params) {
const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}`, params);
// data.headers['xc-db-response'];
// const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}`, params)
const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}/nestedList`, params)
return data.data;
}
@ -49,7 +48,18 @@ 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
return {list, count};
}

30
packages/nc-gui/components/project/spreadsheet/editableCell/tableCell.vue → packages/nc-gui/components/project/spreadsheet/components/cell.vue

@ -1,26 +1,28 @@
<template>
<editable-attachment-cell
:isLocked="isLocked"
:db-alias="dbAlias"
@click.stop="$emit('enableedit')" v-if="isAttachment" :value="value" :column="column"></editable-attachment-cell>
<set-list-cell @click.stop="$emit('enableedit')" v-else-if="isSet" :value="value" :column="column"></set-list-cell>
<!-- <enum-list-editable-cell @click.stop="$emit('enableedit')" v-else-if="isEnum && selected" :value="value" :column="column"></enum-list-editable-cell>-->
<enum-cell @click.stop="$emit('enableedit')" v-else-if="isEnum" :value="value" :column="column"></enum-cell>
<span v-else>{{ value }}</span>
<v-lazy>
<editable-attachment-cell
:isLocked="isLocked"
:db-alias="dbAlias"
@click.stop="$emit('enableedit')" v-if="isAttachment" :value="value" :column="column"></editable-attachment-cell>
<set-list-cell @click.stop="$emit('enableedit')" v-else-if="isSet" :value="value" :column="column"></set-list-cell>
<!-- <enum-list-editable-cell @click.stop="$emit('enableedit')" v-else-if="isEnum && selected" :value="value" :column="column"></enum-list-editable-cell>-->
<enum-cell @click.stop="$emit('enableedit')" v-else-if="isEnum" :value="value" :column="column"></enum-cell>
<span v-else>{{ value }}</span>
</v-lazy>
</template>
<script>
import cell from "@/components/project/spreadsheet/mixins/cell";
import SetListCell from "@/components/project/spreadsheet/cell/setListCell";
import EnumCell from "@/components/project/spreadsheet/cell/enumCell";
import AttachmentCell from "@/components/project/spreadsheet/cell/attachmentCell";
import EditableAttachmentCell from "@/components/project/spreadsheet/editableCell/editableAttachmentCell";
import EnumListEditableCell from "@/components/project/spreadsheet/editableCell/enumListEditableCell";
import SetListCell from "@/components/project/spreadsheet/components/cell/setListCell";
import EnumCell from "@/components/project/spreadsheet/components/cell/enumCell";
import AttachmentCell from "@/components/project/spreadsheet/components/cell/attachmentCell";
import EditableAttachmentCell from "@/components/project/spreadsheet/components/editableCell/editableAttachmentCell";
import EnumListEditableCell from "@/components/project/spreadsheet/components/editableCell/enumListEditableCell";
export default {
name: "tableCell",
components: {EnumListEditableCell, EditableAttachmentCell, AttachmentCell, EnumCell, SetListCell},
props: ['value','dbAlias','isLocked','selected'],
props: ['value', 'dbAlias', 'isLocked', 'selected'],
mixins: [cell],
computed: {}
}

0
packages/nc-gui/components/project/spreadsheet/cell/attachmentCell.vue → packages/nc-gui/components/project/spreadsheet/components/cell/attachmentCell.vue

0
packages/nc-gui/components/project/spreadsheet/cell/enumCell.vue → packages/nc-gui/components/project/spreadsheet/components/cell/enumCell.vue

33
packages/nc-gui/components/project/spreadsheet/cell/setListCell.vue → packages/nc-gui/components/project/spreadsheet/components/cell/setListCell.vue

@ -1,38 +1,43 @@
<template>
<div>
<span v-for="v in (value || '').split(',')" :key="v" :style="{
background:colors[v]
}" class="set-item ma-1 py-1 px-3">{{ v }}</span>
<v-chip
small
v-for="v in (value || '').split(',')"
:key="v"
:color="colors[setValues.indexOf(v) % colors.length]"
class="set-item ma-1 py-1 px-3"
>
{{ v }}
</v-chip>
</div>
</template>
<script>
import colors from "@/components/project/spreadsheet/helpers/colors";
import colors from "@/mixins/colors";
export default {
props: ['value', 'column'],
name: "setListCell",
mixins: [colors],
computed: {
colors() {
const col = this.$store.state.windows.darkTheme ? colors.dark : colors.light;
setValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/^'|'$/g, '')).reduce((obj, v, i) => ({
...obj,
[v]: col[i]
}), {})
return this.column.dtxp.split(',').map(v => v.replace(/^'|'$/g, ''))
}
return {};
}
return [];
},
}
}
</script>
<style scoped>
/*
.set-item {
display: inline-block;
border-radius: 25px;
white-space: nowrap;
}
}*/
</style>
<!--
/**

4
packages/nc-gui/components/project/spreadsheet/components/columnFilterMenu.vue

@ -19,6 +19,7 @@
<column-filter v-model="filters" :field-list="fieldList">
<div class="d-flex align-center mx-2" @click.stop>
<v-checkbox
class="col-filter-checkbox"
hide-details
dense
id="col-filter-checkbox"
@ -85,4 +86,7 @@ export default {
</script>
<style scoped>
/deep/ .col-filter-checkbox .v-input--selection-controls__input {
transform: scale(.7);
}
</style>

35
packages/nc-gui/components/project/spreadsheet/editColumn/editColumn.vue → packages/nc-gui/components/project/spreadsheet/components/editColumn.vue

@ -1,7 +1,7 @@
<template>
<v-card min-width="300px" max-width="400px" max-height="95vh" style="overflow: auto"
class="elevation-0 card">
<v-form v-model="valid">
<v-form ref="form" v-model="valid">
<v-container fluid @click.stop.prevent>
<v-row>
<v-col cols="12" class="d-flex pb-0">
@ -13,10 +13,14 @@
<v-col cols="12">
<v-text-field
ref="column"
hide-details
hide-details="auto"
color="primary"
v-model="newColumn.cn"
@input="newColumn.altered = newColumn.altered || 8"
:rules="[
v => !!v || 'Required',
v => !meta || !meta.columns || meta.columns.every(c => column && c.cn === column.cn || v !== c.cn ) && meta.v.every(c => v !== c._cn ) || 'Duplicate column name'
]"
class="caption"
label="Column name"
dense outlined></v-text-field>
@ -88,6 +92,10 @@
ref="relation"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:isSQLite="isSQLite"
:alias="newColumn.cn"
:isMSSQL="isMSSQL"
@onColumnSelect="onRelColumnSelect"
></linked-to-another-options>
</v-col>
@ -97,6 +105,7 @@
ref="relation"
:column="newColumn"
:nodes="nodes"
:isMSSQL="isMSSQL"
@onColumnSelect="onRelColumnSelect"
:isSQLite="isSQLite"
></relation-options>
@ -300,11 +309,12 @@
<script>
import {uiTypes} from "@/components/project/spreadsheet/helpers/uiTypes";
import CustomSelectOptions from "@/components/project/spreadsheet/editColumn/customSelectOptions";
import RelationOptions from "@/components/project/spreadsheet/editColumn/relationOptions";
import CustomSelectOptions from "@/components/project/spreadsheet/components/editColumn/customSelectOptions";
import RelationOptions from "@/components/project/spreadsheet/components/editColumn/relationOptions";
import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import LinkedToAnotherOptions from "@/components/project/spreadsheet/editColumn/linkedToAnotherOptions";
import LinkedToAnotherOptions from "@/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions";
import {SqliteUi} from "@/helpers/SqliteUi";
import {MssqlUi} from "@/helpers/MssqlUi";
export default {
name: "editColumn",
@ -363,12 +373,20 @@ export default {
this.newColumn = {};
},
async save() {
if (!this.$refs.form.validate()) {
return;
}
try {
if (this.newColumn.uidt === 'Formula') {
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._cn = this.newColumn.cn;
@ -495,10 +513,13 @@ export default {
},
computed: {
isEditDisabled() {
return this.editColumn && this.isSQLite && !this.relation;
return this.editColumn && this.sqlUi === SqliteUi;
},
isSQLite() {
return this.sqlUi === SqliteUi
return this.sqlUi === SqliteUi;
},
isMSSQL() {
return this.sqlUi === MssqlUi;
},
dataTypes() {
return this.sqlUi.getDataTypeListForUiType(this.newColumn)

0
packages/nc-gui/components/project/spreadsheet/editColumn/customSelectOptions.vue → packages/nc-gui/components/project/spreadsheet/components/editColumn/customSelectOptions.vue

302
packages/nc-gui/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions.vue

@ -0,0 +1,302 @@
<template>
<div>
<v-container fluid class="wrapper mb-3">
<v-row>
<v-col>
<v-radio-group row hide-details dense v-model="type" @change="$refs.input.validate()" class="pt-0 mt-0">
<v-radio value="hm" label="Has Many"></v-radio>
<v-radio value="mm" label="Many To Many"></v-radio>
<v-radio disabled value="oo" label="One To One"></v-radio>
</v-radio-group>
</v-col>
</v-row>
</v-container>
<v-container fluid class="wrapper">
<v-row>
<v-col cols="12">
<v-autocomplete
ref="input"
outlined
class="caption"
hide-details="auto"
:loading="isRefTablesLoading"
label="Child Table"
:full-width="false"
v-model="relation.childTable"
:items="refTables"
item-text="_tn"
item-value="tn"
required
dense
:rules="tableRules"
></v-autocomplete>
</v-col
>
<!-- <v-col cols="6">
<v-text-field
outlined
class="caption"
hide-details
label="Child Column"
:full-width="false"
v-model="relation.childColumn"
required
dense
ref="childColumnRef"
@change="onColumnSelect"
></v-text-field>
</v-col
>-->
</v-row>
<template v-if="!isSQLite">
<v-row>
<v-col cols="6">
<v-autocomplete
outlined
class="caption"
hide-details
label="On Update"
:full-width="false"
v-model="relation.onUpdate"
:items="onUpdateDeleteOptions"
required
dense
:disabled="relation.type !== 'real'"
></v-autocomplete>
</v-col>
<v-col cols="6">
<v-autocomplete
outlined
class="caption"
hide-details
label="On Delete"
:full-width="false"
v-model="relation.onDelete"
:items="onUpdateDeleteOptions"
required
dense
:disabled="relation.type !== 'real'"
></v-autocomplete>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox
false-value="real"
true-value="virtual"
label="Virtual Relation"
:full-width="false"
v-model="relation.type"
required
class="mt-0"
dense
></v-checkbox>
</v-col>
</v-row>
</template>
</v-container>
</div>
</template>
<script>
export default {
name: "linked-to-another-options",
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'],
data: () => ({
type: 'hm',
refTables: [],
refColumns: [],
relation: {},
isRefTablesLoading: false,
isRefColumnsLoading: false,
}),
async created() {
await this.loadTablesList();
this.relation = {
childColumn: `${this.meta.tn}_id`,
childTable: this.nodes.tn,
parentTable: this.column.rtn || "",
parentColumn: this.column.rcn || "",
onDelete: "NO ACTION",
onUpdate: "NO ACTION",
updateRelation: this.column.rtn ? true : false,
type: 'real'
}
},
computed: {
onUpdateDeleteOptions() {
if (this.isMSSQL) {
return ["NO ACTION"]
}
return [
"NO ACTION",
"CASCADE",
"RESTRICT",
"SET NULL",
"SET DEFAULT"
];
},
tableRules() {
return [
v => !!v || 'Required',
v => {
if (this.type === 'mm')
return !(this.meta.manyToMany || [])
.some(mm => mm.tn === v && mm.rtn === this.meta.tn || mm.rtn === v && mm.tn === this.meta.tn)
|| 'Duplicate many to many relation is not allowed at the moment';
if (this.type === 'hm')
return !(this.meta.hasMany || [])
.some(hm => hm.tn === v)
|| 'Duplicate has many relation is not allowed at the moment';
},
]
}
},
methods: {
async loadColumnList() {
this.isRefColumnsLoading = true;
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'columnList', {tn: this.meta.tn}])
const columns = result.data.list;
this.refColumns = JSON.parse(JSON.stringify(columns));
if (this.relation.updateRelation && !this.relationColumnChanged) {
//only first time when editing add defaault value to this field
this.relation.parentColumn = this.column.rcn;
this.relationColumnChanged = true;
} else {
//find pk column and assign to parentColumn
const pkKeyColumns = this.refColumns.filter(el => el.pk);
this.relation.parentColumn = (pkKeyColumns[0] || {}).cn || "";
}
this.onColumnSelect();
this.isRefColumnsLoading = false;
},
async loadTablesList() {
this.isRefTablesLoading = true;
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableList']);
this.refTables = result.data.list.map(({tn, _tn}) => ({tn, _tn}))
this.isRefTablesLoading = false;
},
async saveManyToMany() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [
{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
},
'xcM2MRelationCreate',
{
_cn: this.alias,
...this.relation,
type: this.isSQLite || this.relation.type === 'virtual' ? 'virtual' : 'real',
parentTable: this.meta.tn,
updateRelation: this.column.rtn ? true : false
}
]);
} catch (e) {
throw e
}
},
async saveRelation() {
if (this.type === 'mm') {
await this.saveManyToMany();
return;
}
try {
const parentPK = this.meta.columns.find(c => c.pk);
const childTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableXcModelGet', {
tn: this.relation.childTable
}]);
const childMeta = JSON.parse(childTableData.meta)
const newChildColumn = {};
Object.assign(newChildColumn, {
cn: this.relation.childColumn,
_cn: this.relation.childColumn,
rqd: false,
pk: false,
ai: false,
cdf: null,
dt: parentPK.dt,
dtxp: parentPK.dtxp,
dtxs: parentPK.dtxs,
un: parentPK.un,
altered: 1
});
const columns = [...childMeta.columns, newChildColumn];
let result = await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, "tableUpdate", {
tn: childMeta.tn,
_tn: childMeta._tn,
originalColumns: childMeta.columns,
columns
}]);
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [
{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
},
this.relation.type === 'real' && !this.isSQLite ? "relationCreate" : 'xcVirtualRelationCreate',
{
...this.relation,
parentTable: this.meta.tn,
parentColumn: parentPK.cn,
updateRelation: this.column.rtn ? true : false,
type: 'real'
}
]);
} catch (e) {
throw e
}
},
onColumnSelect() {
const col = this.refColumns.find(c => this.relation.parentColumn === c.cn);
this.$emit('onColumnSelect', col)
}
},
}
</script>
<style scoped>
.wrapper {
border: solid 2px #7f828b33;
border-radius: 4px;
}
/deep/ .v-input__append-inner {
margin-top: 4px !important;
}
</style>

29
packages/nc-gui/components/project/spreadsheet/editColumn/relationOptions.vue → packages/nc-gui/components/project/spreadsheet/components/editColumn/relationOptions.vue

@ -97,20 +97,13 @@
<script>
export default {
name: "relationOptions",
props: ['nodes', 'column', 'isSQLite'],
props: ['nodes', 'column', 'isSQLite', 'isMSSQL'],
data: () => ({
refTables: [],
refColumns: [],
relation: {},
isRefTablesLoading: false,
isRefColumnsLoading: false,
onUpdateDeleteOptions: [
"NO ACTION",
"CASCADE",
"RESTRICT",
"SET NULL",
"SET DEFAULT"
],
}),
async created() {
await this.loadTablesList();
@ -119,12 +112,28 @@ export default {
childTable: this.nodes.tn,
parentTable: this.column.rtn || "",
parentColumn: this.column.rcn || "",
onDelete: "CASCADE",
onUpdate: "CASCADE",
onDelete: "NO ACTION",
onUpdate: "NO ACTION",
updateRelation: this.column.rtn ? true : false,
type: this.isSQLite ? 'virtual' : 'real'
}
},
computed: {
onUpdateDeleteOptions() {
if (this.isMSSQL) {
return [
"NO ACTION", x]
}
return [
"NO ACTION",
"CASCADE",
"RESTRICT",
"SET NULL",
"SET DEFAULT"
];
}
},
methods: {
async loadColumnList() {
if (!this.relation.parentTable) return;

131
packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue

@ -0,0 +1,131 @@
<template>
<v-card min-width="300px" max-width="400px" max-height="95vh" style="overflow: auto"
class="elevation-0 card">
<v-form v-model="valid">
<v-container fluid @click.stop.prevent>
<v-row>
<v-col cols="12" class="d-flex pb-0">
<v-spacer></v-spacer>
<v-btn x-small outlined @click="close">Cancel</v-btn>
<v-btn x-small color="primary" @click="save" :disabled="!valid">Save</v-btn>
</v-col>
<v-col cols="12">
<v-text-field
ref="column"
hide-details="auto"
color="primary"
v-model="newColumn._cn"
class="caption"
label="Column name"
:rules="[
v => !!v || 'Required',
v => !meta || !meta.columns || !column ||meta.columns.every(c => v !== c.cn ) && meta.v.every(c => column && c._cn === column._cn || v !== c._cn ) || 'Duplicate column name'
]"
dense outlined></v-text-field>
</v-col>
</v-row>
</v-container>
</v-form>
</v-card>
</template>
<script>
export default {
name: "editVirtualColumn",
components: {},
props: {
nodes: Object,
meta: Object,
value: Boolean,
column: Object
},
data: () => ({
valid: false,
newColumn: {}
}),
async created() {
},
methods: {
close() {
this.$emit('input', false);
this.newColumn = {};
},
async save() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcUpdateVirtualKeyAlias', {
tn: this.nodes.tn,
oldAlias: this.column._cn,
newAlias: this.newColumn._cn,
}]);
this.$toast.success('Successfully updated alias').goAway(3000);
} catch (e) {
console.log(e)
this.$toast.error('Failed to update column alias').goAway(3000);
}
this.$emit('saved');
this.$emit('input', false);
},
focusInput() {
setTimeout(() => {
if (this.$refs.column && this.$refs.column.$el) {
this.$refs.column.$el.querySelector('input').focus()
}
}, 100);
},
}, mounted() {
this.newColumn = {...this.column}
}, watch: {
column(c) {
this.newColumn = {...c}
}
}
}
</script>
<style scoped lang="scss">
::v-deep {
.v-input__slot {
min-height: auto !important;
}
.v-input:not(.v-input--is-focused) fieldset {
border-color: #7f828b33 !important;
}
.ui-type input {
height: 24px;
}
.v-input--selection-controls__input > i {
transform: scale(.83);
}
label {
font-size: 0.75rem !important
}
.v-text-field--outlined.v-input--dense .v-label:not(.v-label--active) {
top: 6px;
}
}
.card {
border: solid 2px #7f828b33;
}
</style>

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

@ -1,4 +1,5 @@
<template>
<v-lazy>
<div
@keydown.stop.left
@keydown.stop.right
@ -13,7 +14,6 @@
v-model="localState"></editable-attachment-cell>
<boolean-cell :isForm="isForm" v-else-if="isBoolean" v-on="parentListeners"
v-model="localState" @input="$emit('change');"></boolean-cell>
@ -57,7 +57,8 @@
<text-area-cell
:is-form="isForm"
v-else-if="isTextArea" @input="$emit('save')" v-model="localState"
v-else-if="isTextArea"
v-model="localState"
v-on="parentListeners"
></text-area-cell>
<!--<set-list-checkbox-cell :column="column" v-else-if="isSet" v-model="localState"
@ -65,26 +66,28 @@
<text-cell v-else v-model="localState" v-on="$listeners"></text-cell>
</div>
</v-lazy>
</template>
<script>
import DatePickerCell from "@/components/project/spreadsheet/editableCell/datePickerCell";
import TextCell from "@/components/project/spreadsheet/editableCell/textCell";
import DateTimePickerCell from "@/components/project/spreadsheet/editableCell/dateTimePickerCell";
import TextAreaCell from "@/components/project/spreadsheet/editableCell/textAreaCell";
import EnumListCell from "@/components/project/spreadsheet/editableCell/enumListEditableCell";
import JsonCell from "@/components/project/spreadsheet/editableCell/jsonCell";
import IntegerCell from "@/components/project/spreadsheet/editableCell/integerCell";
import FloatCell from "@/components/project/spreadsheet/editableCell/floatCell";
import TimePickerCell from "@/components/project/spreadsheet/editableCell/timePickerCell";
import BooleanCell from "@/components/project/spreadsheet/editableCell/booleanCell";
import SetListCheckboxCell from "@/components/project/spreadsheet/editableCell/setListCheckboxCell";
import DatePickerCell from "@/components/project/spreadsheet/components/editableCell/datePickerCell";
import TextCell from "@/components/project/spreadsheet/components/editableCell/textCell";
import DateTimePickerCell from "@/components/project/spreadsheet/components/editableCell/dateTimePickerCell";
import TextAreaCell from "@/components/project/spreadsheet/components/editableCell/textAreaCell";
import EnumListCell from "@/components/project/spreadsheet/components/editableCell/enumListEditableCell";
import JsonCell from "@/components/project/spreadsheet/components/editableCell/jsonCell";
import IntegerCell from "@/components/project/spreadsheet/components/editableCell/integerCell";
import FloatCell from "@/components/project/spreadsheet/components/editableCell/floatCell";
import TimePickerCell from "@/components/project/spreadsheet/components/editableCell/timePickerCell";
import BooleanCell from "@/components/project/spreadsheet/components/editableCell/booleanCell";
import SetListCheckboxCell from "@/components/project/spreadsheet/components/editableCell/setListCheckboxCell";
import cell from "@/components/project/spreadsheet/mixins/cell";
import EnumRadioEditableCell from "@/components/project/spreadsheet/editableCell/enumRadioEditableCell";
import EditableAttachmentCell from "@/components/project/spreadsheet/editableCell/editableAttachmentCell";
import EnumCell from "@/components/project/spreadsheet/cell/enumCell";
import SetListEditableCell from "@/components/project/spreadsheet/editableCell/setListEditableCell";
import SetListCell from "@/components/project/spreadsheet/cell/setListCell";
import EnumRadioEditableCell from "@/components/project/spreadsheet/components/editableCell/enumRadioEditableCell";
import EditableAttachmentCell from "@/components/project/spreadsheet/components/editableCell/editableAttachmentCell";
import EnumCell from "@/components/project/spreadsheet/components/cell/enumCell";
import SetListEditableCell from "@/components/project/spreadsheet/components/editableCell/setListEditableCell";
import SetListCell from "@/components/project/spreadsheet/components/cell/setListCell";
import debounce from "debounce";
export default {
name: "editable-cell",
@ -107,11 +110,13 @@ export default {
meta: Object,
ignoreFocus: Boolean,
isForm: Boolean,
active: Boolean,
active: Boolean
},
data: () => ({
changed: false,
destroyed: false
}),
mounted() {
// this.$refs.input.focus();
},
@ -119,6 +124,17 @@ export default {
if (this.changed && !(this.isAttachment || this.isEnum || this.isBoolean || this.isSet || this.isTime)) {
this.$emit('change');
}
this.destroyed = true;
},
methods: {
syncDataDebounce: debounce(async function (self) {
await self.syncData()
}, 1000),
syncData() {
if (!this.destroyed) {
this.$emit('update')
}
}
},
computed: {
localState: {
@ -128,6 +144,7 @@ export default {
set(val) {
this.changed = true;
this.$emit('input', val);
this.syncDataDebounce(this);
}
},
parentListeners() {

0
packages/nc-gui/components/project/appStore/inputs/booleanCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/booleanCell.vue

6
packages/nc-gui/components/project/spreadsheet/editableCell/datePickerCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/datePickerCell.vue

@ -23,10 +23,10 @@ export default {
computed: {
localState: {
get() {
return typeof this.value === 'string' ? this.value.replace(/(\d)T(?=\d)/, '$1 ') : this.value;
return typeof this.value === 'string' ? this.value.replace(/(\d)T(?=\d)/, '$1 ') : (this.value && new Date(this.value));
},
set(val) {
this.$emit('input', new Date(val).toJSON().slice(0, 10));
this.$emit('input', val && new Date(val).toJSON().slice(0, 10));
}
},
parentListeners() {
@ -48,7 +48,7 @@ export default {
<style scoped>
.value {
width: 100%;
min-height:20px;
min-height: 20px;
}
</style>

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

@ -30,31 +30,27 @@ export default {
computed: {
localState: {
get() {
if(/^\d{6,}$/.test(this.value)){
// todo : time value correction
if (/^\d{6,}$/.test(this.value)) {
return new Date(+this.value);
}
return /\dT\d/.test(this.value) ? new Date(this.value.replace(/(\d)T(?=\d)/, '$1 ')) : this.value;
return /\dT\d/.test(this.value) ? new Date(this.value.replace(/(\d)T(?=\d)/, '$1 ')) : (this.value && new Date(this.value));
},
set(val) {
// if(/^\d{6,}$/.test(this.value)){
// return this.$emit('input', new Date(this.value).getTime());
// }
const uVal = new Date(val).toISOString().slice(0, 19).replace('T', ' ').replace(/(\d{1,2}:\d{1,2}):\d{1,2}$/,'$1');
console.log(val, uVal)
const uVal = val && new Date(val).toISOString().slice(0, 19).replace('T', ' ').replace(/(\d{1,2}:\d{1,2}):\d{1,2}$/, '$1');
this.$emit('input', uVal);
}
},
parentListeners(){
parentListeners() {
const $listeners = {};
if(this.$listeners.blur){
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur;
}
if(this.$listeners.focus){
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus;
}

0
packages/nc-gui/components/project/spreadsheet/editableCell/editableAttachmentCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue

6
packages/nc-gui/components/project/spreadsheet/editableCell/enumListEditableCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/enumListEditableCell.vue

@ -1,11 +1,11 @@
<template>
<v-select v-on="parentListeners" v-model="localState" dense flat :items="enumValues" hide-details class="mt-0" :clearable="!column.rqd">
<v-select solo v-on="parentListeners" v-model="localState" dense flat :items="enumValues" hide-details class="mt-0" :clearable="!column.rqd">
<!-- <option v-for="eVal of enumValues" :key="eVal" :value="eVal">{{ eVal }}</option>-->
<template v-slot:selection="{item}">
<div class="d-100 pl-4" :class="{
<div class="d-100" :class="{
'text-center' : !isForm
}">
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]">{{ item }}</v-chip>
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]" class="ma-1">{{ item }}</v-chip>
</div>
</template>
<template v-slot:item="{item}">

0
packages/nc-gui/components/project/appStore/inputs/enumRadioEditableCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/enumRadioEditableCell.vue

2
packages/nc-gui/components/project/spreadsheet/editableCell/floatCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/floatCell.vue

@ -17,7 +17,7 @@ export default {
return this.value
},
set(val) {
this.$emit('input', val);
this.$emit('input', +val);
}
},
parentListeners(){

0
packages/nc-gui/components/project/appStore/inputs/integerCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/integerCell.vue

0
packages/nc-gui/components/project/appStore/inputs/jsonCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/jsonCell.vue

0
packages/nc-gui/components/project/spreadsheet/editableCell/setListCheckboxCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/setListCheckboxCell.vue

5
packages/nc-gui/components/project/spreadsheet/editableCell/setListEditableCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/setListEditableCell.vue

@ -11,13 +11,14 @@
chips
flat
dense
solo
hide-details
deletable-chips
class="text-center mt-0"
class="text-center mt-0 "
>
<template v-slot:selection="data">
<v-chip
small
small class="ma-1 "
:key="data"
:color="colors[setValues.indexOf(data.item) % colors.length]"
@click:close="data.parent.selectItem(data.item)"

31
packages/nc-gui/components/project/spreadsheet/editableCell/booleanCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/textAreaCell.vue

@ -1,30 +1,36 @@
<template>
<div class="d-flex align-center " :class="{'justify-center':!isForm}">
<input v-on="parentListeners" type="checkbox" v-model="localState">
</div>
<textarea v-on="parentListeners"
rows="4"
ref="textarea"
v-model="localState"
@keydown.alt.enter.stop
@keydown.shift.enter.stop
></textarea>
</template>
<script>
export default {
name: "boolean-cell",
name: "textAreaCell",
props: {
value: [String,Number, Boolean],
isForm: Boolean
value: String
},
created() {
this.localState = this.value;
},
mounted() {
this.$el.focus();
this.$refs.textarea && this.$refs.textarea.focus();
},
computed: {
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val);
// this.$emit('update');
}
},
parentListeners() {
const $listeners = {};
@ -42,7 +48,12 @@ export default {
</script>
<style scoped>
input, textarea {
width: 100%;
min-height: 60px;
height: 100%;
color: var(--v-textColor-base);
}
</style>
<!--
/**

0
packages/nc-gui/components/project/spreadsheet/editableCell/textAreaCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/textAreaCellOld.vue

0
packages/nc-gui/components/project/spreadsheet/editableCell/textCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/textCell.vue

0
packages/nc-gui/components/project/spreadsheet/editableCell/timePickerCell.vue → packages/nc-gui/components/project/spreadsheet/components/editableCell/timePickerCell.vue

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

@ -2,7 +2,10 @@
<v-card width="1000" max-width="100%">
<v-toolbar height="55" class="elevation-1">
<div class="d-100 d-flex ">
<h5 class="title text-center">{{ table }} : {{ localState[primaryValueColumn] }}</h5>
<h5 class="title text-center">
<v-icon :color="iconColor">mdi-table-arrow-right</v-icon>
{{ table }} : {{ localState[primaryValueColumn] }}
</h5>
<v-spacer>
</v-spacer>
<v-btn small text @click="reload">
@ -30,23 +33,38 @@
</div>
</v-toolbar>
<div class="form-container ">
<v-card-text class=" py-0 px-0 " :class="{
'px-10' : isNew || !toggleDrawer,
}">
<v-breadcrumbs class="caption pt-0 pb-2 justify-center d-100"
v-if="localBreadcrumbs && localBreadcrumbs.length"
:items="localBreadcrumbs.map(text => ({text}))"/>
<v-container fluid style="height:70vh" class="py-0">
<v-row class="h-100">
<v-col class="h-100 px-10" style="overflow-y: auto" cols="8" :offset="isNew || !toggleDrawer ? 2 : 0">
<div :class="{
'active-row' : active === col._cn
}" v-for="(col,i) in fields"
:key="i" class="row-col my-4">
<div
v-for="(col,i) in fields"
:class="{
'active-row' : active === col._cn,
required: isRequired(col, localState)
}"
:key="i" class="row-col my-4">
<div>
<label :for="`data-table-form-${col._cn}`" class="body-2 text-capitalize">
<!-- {{ col.cn }}-->
<virtual-header-cell
v-if="col.virtual"
:column="col"
:nodes="nodes"
:is-form="true"
:meta="meta"
>
</virtual-header-cell>
<header-cell
v-else
:is-form="true"
:is-foreign-key="col.cn in belongsTo || col.cn in hasMany"
:value="col._cn"
@ -54,11 +72,28 @@
:sql-ui="sqlUi"></header-cell>
</label>
<virtual-cell
ref="virtual"
v-if="col.virtual"
:disabledColumns="disabledColumns"
:column="col"
:row="localState"
:nodes="nodes"
:meta="meta"
:api="api"
:active="true"
:sql-ui="sqlUi"
:is-new="isNew"
:is-form="true"
:breadcrumbs="localBreadcrumbs"
@updateCol="updateCol"
@newRecordsSaved="$listeners.loadTableData|| reload"
></virtual-cell>
<div
style="height:100%; width:100%"
class="caption xc-input"
v-if="col.ai || (col.pk && !selectedRowMeta.new)"
v-else-if="col.ai || (col.pk && !isNew) || disabledColumns[col._cn]"
@click="col.ai && $toast.info('Auto Increment field is not editable').goAway(3000)"
>
<input
@ -142,6 +177,8 @@
</v-text-field>
</div>
</v-col>
</v-row>
</v-container>
</v-card-text>
@ -166,29 +203,49 @@ import HeaderCell from "@/components/project/spreadsheet/components/headerCell";
import EditableCell from "@/components/project/spreadsheet/components/editableCell";
import dayjs from 'dayjs';
import colors from "@/mixins/colors";
import VirtualCell from "@/components/project/spreadsheet/components/virtualCell";
import VirtualHeaderCell from "@/components/project/spreadsheet/components/virtualHeaderCell";
const relativeTime = require('dayjs/plugin/relativeTime')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
dayjs.extend(relativeTime)
export default {
components: {EditableCell, HeaderCell},
components: {VirtualHeaderCell, VirtualCell, EditableCell, HeaderCell},
mixins: [colors],
props: {
breadcrumbs: {
type: Array,
default() {
return [];
}
},
dbAlias: String,
value: Object,
meta: Object,
sqlUi: [Object, Function],
selectedRowMeta: Object,
table: String,
primaryValueColumn: String,
api: [Object],
hasMany: Object,
belongsTo: Object,
hasMany: [Object, Array],
belongsTo: [Object, Array],
isNew: Boolean,
oldRow: Object
oldRow: Object,
iconColor: {
type: String,
default: 'primary'
},
availableColumns: [Object, Array],
nodes: [Object],
queryParams: Object,
disabledColumns: {
type: Object,
default() {
return {}
}
}
},
name: "expandedForm",
name: "expanded-form",
data: () => ({
showborder: false,
loadingLogs: true,
@ -198,7 +255,7 @@ export default {
localState: {},
changedColumns: {},
comment: null,
showSystemFields: false
showSystemFields: false,
}),
created() {
this.localState = {...this.value}
@ -230,6 +287,20 @@ export default {
},
},
methods: {
isRequired(_columnObj, rowObj) {
let columnObj = _columnObj;
if (columnObj.bt) {
columnObj = this.meta.columns.find(c => c.cn === columnObj.bt.cn);
}
return (columnObj.rqd
&& (rowObj[columnObj._cn] === undefined || rowObj[columnObj._cn] === null)
&& !columnObj.default);
},
updateCol(_row, _cn, pid) {
this.$set(this.localState, _cn, pid)
this.$set(this.changedColumns, _cn, true)
},
isYou(email) {
return this.$store.state.users.user && this.$store.state.users.user.email === email;
},
@ -240,13 +311,6 @@ export default {
model_name: this.meta._tn
}])
this.logs = data.list;
// this.$nextTick(() => {
// const objDiv = this.$refs.commentsList.$el;
// if (objDiv) {
// objDiv.scrollTop = objDiv.scrollHeight;
// }
// })
this.loadingLogs = false;
},
async save() {
@ -257,9 +321,19 @@ export default {
obj[col] = this.localState[col];
return obj;
}, {});
if (this.isNew) {
const data = await this.api.insert(updatedObj);
Object.assign(this.localState, data)
this.localState = {...this.localState, ...data};
// save hasmany and manytomany relations from local state
if (this.$refs.virtual && Array.isArray(this.$refs.virtual)) {
for (const vcell of this.$refs.virtual) {
if (vcell.save) await vcell.save(this.localState);
}
}
await this.reload();
} else {
if (Object.keys(updatedObj).length) {
await this.api.update(id, updatedObj, this.oldRow);
@ -268,9 +342,11 @@ export default {
}
}
this.$emit('update:oldRow', {...this.localState})
this.changedColumns = {};
this.$emit('input', this.localState);
this.$emit('update:isNew', false);
this.$toast.success(`${this.localState[this.primaryValueColumn]} updated successfully.`, {
position: 'bottom-right'
@ -280,9 +356,12 @@ export default {
}
},
async reload() {
const id = this.meta.columns.filter((c) => c.pk).map(c => this.localState[c._cn]).join('___');
// const id = this.meta.columns.filter((c) => c.pk).map(c => this.localState[c._cn]).join('___');
const where = this.meta.columns.filter((c) => c.pk).map(c => `(${c._cn},eq,${this.localState[c._cn]})`).join('~and');
this.$set(this, 'changedColumns', {});
this.localState = await this.api.read(id);
// this.localState = await this.api.read(id);
const data = await this.api.list({...(this.queryParams || {}), where}) || [{}];
this.localState = data[0] || this.localState;
if (!this.isNew && this.toggleDrawer) {
this.getAuditsAndComments()
}
@ -310,18 +389,30 @@ export default {
}
},
computed: {
primaryKey() {
return this.isNew ? '' : this.meta.columns.filter((c) => c.pk).map(c => this.localState[c._cn]).join('___');
},
edited() {
return !!Object.keys(this.changedColumns).length;
},
fields() {
if (this.availableColumns) return this.availableColumns;
const hideCols = ['created_at', 'updated_at'];
if (this.showSystemFields) {
return this.meta.columns || [];
} else {
return (this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn))) || [];
return this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn)
&& !((this.meta.v || []).some(v => v.bt && v.bt.cn === c.cn))
) || [];
}
},
isChanged() {
return Object.values(this.changedColumns).some(Boolean)
},
localBreadcrumbs() {
return [...this.breadcrumbs, `${this.table} (${this.localState && this.localState[this.primaryValueColumn]})`]
}
}
}
@ -339,6 +430,17 @@ export default {
::v-deep {
.v-breadcrumbs__item:nth-child(odd) {
font-size: .72rem;
color: grey;
}
.v-breadcrumbs li:nth-child(even) {
padding: 0 6px;
font-size: .72rem;
color: var(--v-textColor-base);
}
position: relative;
.comment-icon {
@ -347,12 +449,15 @@ export default {
bottom: 60px;
}
/* todo: refactor */
.row-col {
& > div > input,
//& > div div > input,
& > div > .xc-input > input,
& > div > .xc-input > div > input,
& > div > select,
& > div > .xc-input > select,
& > div textarea {
& > div textarea:not(.inputarea) {
border: 1px solid #7f828b33;
padding: 1px 5px;
font-size: .8rem;
@ -379,11 +484,13 @@ export default {
background: #363636;
.row-col {
//& > div div > input,
& > div > input,
& > div > .xc-input > input,
& > div > .xc-input > div > input,
& > div > select,
& > div > .xc-input > select,
& > div textarea {
& > div textarea:not(.inputarea) {
background: #1e1e1e;
}
}
@ -394,10 +501,12 @@ export default {
.row-col {
& > div > input,
//& > div div > input,
& > div > .xc-input > input,
& > div > .xc-input > div > input,
& > div > select,
& > div > .xc-input > select,
& > div textarea {
& > div textarea:not(.inputarea) {
background: white;
}
}
@ -429,6 +538,16 @@ h5 {
.comment-box.focus {
border: 1px solid #4185f4;
}
.required > div > label + * {
border: 1px solid red;
border-radius: 4px;
//min-height: 42px;
//display: flex;
//align-items: center;
//justify-content: flex-end;
background: var(--v-backgroundColorDefault-base);
}
</style>
<!--
/**

62
packages/nc-gui/components/project/spreadsheet/components/headerCell.vue

@ -1,7 +1,5 @@
<template>
<div class="d-flex align-center">
<div class="d-flex align-center d-100">
<v-icon v-if="column.pk" color="warning" x-small class="mr-1">mdi-key-variant</v-icon>
@ -20,7 +18,9 @@
<v-icon color="grey" class="" v-else-if="isString">mdi-alpha-a</v-icon>
<v-icon color="grey" small class="mr-1" v-else-if="isTextArea">mdi-card-text-outline</v-icon>
{{ value }}
<span class="name" :title="value">{{ value }}</span>
<span v-if="column.rqd" class="error--text text--lighten-1">&nbsp;*</span>
<v-spacer>
</v-spacer>
@ -31,12 +31,21 @@
<v-icon v-on="on" small v-if="!isVirtual">mdi-menu-down</v-icon>
</template>
<v-list dense>
<v-list-item @click="editColumnMenu = true">
<v-icon small class="mr-1">mdi-pencil</v-icon>
<v-list-item dense @click="editColumnMenu = true">
<x-icon small class="mr-1" color="primary">mdi-pencil</x-icon>
<span class="caption">Edit</span>
</v-list-item>
<v-list-item dense @click="setAsPrimaryValue">
<x-icon small class="mr-1" color="primary">mdi-key-star</x-icon>
<v-tooltip bottom>
<template v-slot:activator="{on}">
<span class="caption" v-on="on">Set as Primary value</span>
</template>
<span class="caption font-weight-bold">Primary value will be shown in place of primary key</span>
</v-tooltip>
</v-list-item>
<v-list-item @click="columnDeleteDialog = true">
<v-icon small class="mr-1">mdi-delete-outline</v-icon>
<x-icon small class="mr-1" color="error">mdi-delete-outline</x-icon>
<span class="caption">Delete</span>
</v-list-item>
</v-list>
@ -85,11 +94,11 @@
<script>
import cell from "@/components/project/spreadsheet/mixins/cell";
import EditColumn from "@/components/project/spreadsheet/editColumn/editColumn";
import EditColumn from "@/components/project/spreadsheet/components/editColumn";
export default {
components: {EditColumn},
props: ['value', 'column', 'isForeignKey', 'meta', 'nodes', 'columnIndex', 'isForm', 'isPublicView','isVirtual'],
props: ['value', 'column', 'isForeignKey', 'meta', 'nodes', 'columnIndex', 'isForm', 'isPublicView', 'isVirtual'],
name: "headerCell",
mixins: [cell],
data: () => ({
@ -116,13 +125,46 @@ export default {
} catch (e) {
console.log(e)
}
}, async setAsPrimaryValue() {
// todo: pass only updated fields
try {
const meta = JSON.parse(JSON.stringify(this.meta));
for (const col of meta.columns) {
if (col.pv) {
delete col.pv;
}
if (col.cn === this.column.cn) {
col.pv = true;
}
}
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}]);
this.$toast.success('Successfully updated as primary column').goAway(3000);
} catch (e) {
console.log(e)
this.$toast.error('Failed to update primary column').goAway(3000);
}
this.$emit('saved');
this.columnDeleteDialog = false;
}
}
}
</script>
<style scoped>
.name{
max-width: calc(100% - 40px);
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<!--
/**

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

@ -0,0 +1,54 @@
<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>

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

@ -264,7 +264,7 @@
<v-hover >
<template v-slot:default="{hover}">
<v-btn
:color="hover ?'primary' : 'grey'" class="mb-2" small outlined href="https://github.com/sponsors/xgenecloud"
:color="hover ?'primary' : 'grey'" class="mb-2" small outlined href="https://github.com/sponsors/nocodb"
target="_blank">
<v-icon small color="red" class="mr-2">mdi-heart-outline</v-icon>
Sponsor Us

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

@ -0,0 +1,144 @@
<template>
<div>
<v-lazy>
<has-many-cell
ref="cell"
v-if="hm"
:row="row"
:value="row[`${hm._tn}List`]"
:meta="meta"
:hm="hm"
:nodes="nodes"
:active="active"
:sql-ui="sqlUi"
:is-new="isNew"
:is-form="isForm"
:breadcrumbs="breadcrumbs"
v-on="$listeners"
/>
<many-to-many-cell
ref="cell"
v-else-if="mm"
:row="row"
:value="row[`${mm._rtn}MMList`]"
:meta="meta"
:mm="mm"
:nodes="nodes"
:sql-ui="sqlUi"
:active="active"
:is-new="isNew"
:api="api"
:is-form="isForm"
:breadcrumbs="breadcrumbs"
v-on="$listeners"
/>
<belongs-to-cell
ref="cell"
:disabled-columns="disabledColumns"
v-else-if="bt"
:active="active"
:row="row"
:value="row[`${bt._rtn}Read`]"
:meta="meta"
:bt="bt"
:nodes="nodes"
:api="api"
:sql-ui="sqlUi"
:is-new="isNew"
:is-form="isForm"
:breadcrumbs="breadcrumbs"
v-on="$listeners"
/>
</v-lazy>
</div>
</template>
<script>
import hasManyCell from "@/components/project/spreadsheet/components/virtualCell/hasManyCell";
import manyToManyCell from "@/components/project/spreadsheet/components/virtualCell/manyToManyCell";
import belongsToCell from "@/components/project/spreadsheet/components/virtualCell/belogsToCell";
// todo: optimize parent/child meta extraction
export default {
name: "virtual-cell",
components: {
belongsToCell,
manyToManyCell,
hasManyCell
},
props: {
breadcrumbs: {
type: Array,
default() {
return [];
}
},
column: [Object],
row: [Object],
nodes: [Object],
meta: [Object],
api: [Object, Function],
active: Boolean,
sqlUi: [Object, Function],
isNew: {
type: Boolean,
default: false
},
isForm: {
type: Boolean,
default: false
},
disabledColumns: Object
},
computed: {
hm() {
return this.column && this.column.hm;
},
bt() {
return this.column && this.column.bt;
},
mm() {
return this.column && this.column.mm;
}
},
methods: {
async save(row) {
if (row && this.$refs.cell && this.$refs.cell.saveLocalState) {
try {
await this.$refs.cell.saveLocalState(row);
} catch (e) {
}
}
}
}
}
</script>
<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/>.
*
*/
-->

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

@ -0,0 +1,399 @@
<template>
<div class="d-flex d-100 chips-wrapper" :class="{active}">
<template v-if="!isForm">
<div class="chips d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value || localState">
<item-chip
:active="active"
:item="value"
:value="cellValue"
@edit="editParent"
@unlink="unlink"
></item-chip>
</template>
</div>
<div class="action align-center justify-center px-1 flex-shrink-1"
:class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">{{
value ? 'mdi-arrow-expand' : 'mdi-plus'
}}
</x-icon>
</div>
</template>
<list-items
:key="parentId"
v-if="newRecordModal"
:size="10"
:meta="parentMeta"
:primary-col="parentPrimaryCol"
:primary-key="parentPrimaryKey"
v-model="newRecordModal"
:api="parentApi"
@add-new-record="insertAndMapNewParentRecord"
@add="addChildToParent"
:query-params="parentQueryParams"
/>
<list-child-items
ref="childList"
v-if="parentMeta && isForm"
:isForm="isForm"
:local-state="localState? [localState] : []"
:is-new="isNew"
:size="10"
:parent-meta="parentMeta"
:meta="parentMeta"
:primary-col="parentPrimaryCol"
:primary-key="parentPrimaryKey"
:api="parentApi"
:query-params="{
...parentQueryParams,
where: `(${parentPrimaryKey},eq,${parentId})`
}"
@new-record="showNewRecordModal"
@edit="editParent"
@unlink="unlink"
:bt="value"
/>
<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.sync="isNewParent"
icon-color="warning"
ref="expandedForm"
v-model="selectedParent"
@cancel="selectedParent = null"
@input="onParentSave"
:breadcrumbs="breadcrumbs"
></component>
</v-dialog>
</div>
</template>
<script>
import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
import ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip";
import ListChildItems from "@/components/project/spreadsheet/components/virtualCell/components/listChildItems";
export default {
name: "belongs-to-cell",
components: {ListChildItems, ItemChip, ListItems},
props: {
breadcrumbs: {
type: Array,
default() {
return [];
}
},
isForm: Boolean,
value: [Object, Array],
meta: [Object],
bt: Object,
nodes: [Object],
row: [Object],
api: [Object, Function],
sqlUi: [Object, Function],
active: Boolean,
isNew: Boolean,
disabledColumns: Object
},
data: () => ({
newRecordModal: false,
parentListModal: false,
// parentMeta: null,
list: null,
childList: null,
dialogShow: false,
confirmAction: null,
confirmMessage: '',
selectedParent: null,
isNewParent: false,
expandFormModal: false,
localState: null,
pid: null
}),
async mounted() {
if (this.isForm) {
await this.loadParentMeta()
}
},
methods: {
async onParentSave(parent) {
if (this.isNewParent) {
await this.addChildToParent(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);
const _cn = column._cn;
if (this.isNew) {
this.$emit('updateCol', this.row, _cn, null);
this.localState = null;
return
}
if (column.rqd) {
this.$toast.info('Unlink is not possible, instead map to another parent.').goAway(3000)
return
}
const id = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
await this.api.update(id, {[_cn]: null}, this.row)
this.$emit('loadTableData')
if (this.isForm && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
async showParentListModal() {
this.parentListModal = true;
await this.loadParentMeta();
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;
this.childList = await this.parentApi.paginatedList({
where: `(${_cn},eq,${pid})`
})
},
async removeChild(child) {
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.parentMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
await this.parentApi.delete(id)
this.pid = null;
this.dialogShow = false;
this.$emit('loadTableData')
if (this.isForm && this.$refs.childList) {
this.$refs.childList.loadData();
}
}
}
},
async loadParentMeta() {
// todo: optimize
if (!this.parentMeta) {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.bt.rtn
})
// const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.bt.rtn
// }]);
// this.parentMeta = JSON.parse(parentTableData.meta)
}
},
async showNewRecordModal() {
await this.loadParentMeta();
this.newRecordModal = true;
},
async addChildToParent(parent) {
const pid = this.parentMeta.columns.filter((c) => c.pk).map(c => parent[c._cn]).join('___');
const id = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
const _cn = this.meta.columns.find(c => c.cn === this.bt.cn)._cn;
if (this.isNew) {
this.localState = parent;
this.$emit('updateCol', this.row, _cn, +pid || pid)
this.newRecordModal = false;
return
}
await this.api.update(id, {
[_cn]: +pid
}, {
[_cn]: this.value && this.value[this.parentPrimaryKey]
});
this.pid = pid;
this.newRecordModal = false;
this.$emit('loadTableData')
if (this.isForm && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
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: {
parentMeta() {
return this.$store.state.meta.metas[this.bt.rtn];
},
parentApi() {
return this.parentMeta && this.parentMeta._tn ?
ApiFactory.create(this.$store.getters['project/GtrProjectType'],
this.parentMeta && this.parentMeta._tn, this.parentMeta && this.parentMeta.columns, this, this.parentMeta) : null;
},
parentId() {
return this.pid ?? (this.value && this.parentMeta && this.parentMeta.columns.filter((c) => c.pk).map(c => this.value[c._cn]).join('___'))
},
parentPrimaryCol() {
return this.parentMeta && (this.parentMeta.columns.find(c => c.pv) || {})._cn
},
parentPrimaryKey() {
return this.parentMeta && (this.parentMeta.columns.find(c => c.pk) || {})._cn
},
parentQueryParams() {
if (!this.parentMeta) return {}
return {
childs: (this.parentMeta && this.parentMeta.v && this.parentMeta.v.filter(v => v.hm).map(({hm}) => hm.tn).join()) || '',
parents: (this.parentMeta && this.parentMeta.v && this.parentMeta.v.filter(v => v.bt).map(({bt}) => bt.rtn).join()) || '',
many: (this.parentMeta && this.parentMeta.v && this.parentMeta.v.filter(v => v.mm).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) && !((this.parentMeta.v || []).some(v => v.bt && v.bt.cn === 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';
},
cellValue() {
if (this.value || this.localState) {
if (this.parentMeta && this.parentPrimaryCol) {
return (this.value || this.localState)[this.parentPrimaryCol]
}
return Object.values(this.value || this.localState)[1]
}
}
},
watch: {
isNew(n, o) {
if (!n && o) {
this.localState = null
}
}
},
created() {
this.loadParentMeta();
}
}
</script>
<style scoped lang="scss">
.items-container {
overflow-x: visible;
max-height: min(500px, 60vh);
overflow-y: auto;
}
.primary-value {
.primary-key {
display: none;
margin-left: .5em;
}
&:hover .primary-key {
display: inline;
}
}
.child-card {
cursor: pointer;
&:hover {
box-shadow: 0 0 .2em var(--v-textColor-lighten5)
}
}
.hm-items {
flex-wrap: wrap;
row-gap: 3px;
gap: 3px;
margin: 3px auto;
}
.chips-wrapper {
.chips {
max-width: 100%;
}
&.active {
.chips {
max-width: calc(100% - 22px);
}
}
}
</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/>.
*
*/
-->

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

@ -0,0 +1,43 @@
<template>
<v-chip
class="chip"
:class="{active}"
small
text-color="textColor"
:color="isDark ? '' : 'primary lighten-5'"
@click="active && $emit('edit',item)"
>
<span class="name" :title="value">{{ value }}</span>
<div v-show="active" class="mr-n1 ml-2">
<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: {
value: String,
active: Boolean,
item: Object
},
name: "item-chip"
}
</script>
<style scoped lang="scss">
.chip {
max-width: max(100%, 60px);
.name {
text-overflow: ellipsis;
overflow: hidden;
}
}
</style>

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

@ -0,0 +1,220 @@
<template>
<!-- <v-dialog v-model="show" width="600">-->
<v-card width="600" color="">
<v-card-title v-if="!isForm" class="textColor--text mx-2" :class="{'py-2':isForm}">
<span v-if="!isForm">{{ meta ? meta._tn : 'Children' }}</span>
<v-spacer>
</v-spacer>
<v-icon small class="mr-1" @click="loadData()">mdi-reload</v-icon>
<v-btn
small
class="caption"
color="primary"
@click="$emit('new-record')"
>
<v-icon
small>mdi-link
</v-icon>&nbsp;
Link to '{{ meta._tn }}'
</v-btn>
</v-card-title>
<v-card-text>
<div class="items-container pt-2 mb-n4" :class="{'mx-n2' : isForm}">
<div class="text-right mb-2 mt-n2 mx-2">
<v-btn
v-if="isForm"
x-small
class="caption"
color="primary"
outlined
@click="$emit('new-record')"
>
<v-icon
x-small>mdi-link
</v-icon>&nbsp;
Link to '{{ meta._tn }}'
</v-btn>
</div>
<template v-if="isDataAvail">
<v-card
v-for="(ch,i) in ((data && data.list) || localState)"
class="mx-2 mb-2 child-list-modal child-card"
outlined
:key="i"
@click="$emit('edit',ch)"
>
<div class="remove-child-icon d-flex align-center">
<x-icon
:tooltip="`Unlink this '${meta._tn}' from '${parentMeta._tn}'`"
:color="['error','grey']"
small
@click.stop="$emit('unlink',ch,i)"
icon.class="mr-1 mt-n1"
>mdi-link-variant-remove
</x-icon>
<x-icon
v-if="!mm && !bt"
:tooltip="`Delete row in '${meta._tn}'`"
:color="['error','grey']"
small
@click.stop="$emit('delete',ch,i)"
>mdi-delete-outline
</x-icon>
</div>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[primaryCol] }}
<span class="grey--text caption primary-key ml-1"
v-if="primaryKey"> (Primary Key : {{ ch[primaryKey] }})</span>
</v-card-title>
</v-card>
</template>
<div v-else-if="data || localState" class="text-center textLight--text"
:class="{'pt-6 pb-4' : !isForm , 'pt-4 pb-3':isForm}">
No item{{ bt ? '' : 's' }} found
</div>
<div v-if="isForm" class="mb-2 d-flex align-center justify-center">
<pagination
v-if="!bt && data && data.count > 1"
:size="size"
:count="data && data.count"
v-model="page"
@input="loadData"
></pagination>
</div>
</div>
</v-card-text>
<v-card-actions v-if="!isForm" class="justify-center flex-column" :class="{'py-0':isForm}">
<pagination
v-if="!bt && data && data.count > 1"
:size="size"
:count="data && 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: {
isForm: Boolean,
bt: Object,
localState: [Array],
isNew: Boolean,
value: Boolean,
title: {
type: String,
default: 'Link Record'
},
queryParams: {
type: Object,
default() {
return {};
}
},
primaryKey: String,
primaryCol: String,
meta: Object,
parentMeta: Object,
size: Number,
api: [Object, Function],
mm: [Object, Boolean]
},
data: () => ({
data: null,
page: 1
}),
mounted() {
this.loadData();
},
methods: {
async loadData() {
if (!this.api || this.isNew) return;
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams
})
}
},
computed: {
isDataAvail() {
return (this.data && this.data.list && this.data.list.length) || (this.localState && this.localState.length);
},
show: {
set(v) {
this.$emit('input', v)
}, get() {
return this.value;
}
}
},
watch: {
queryParams() {
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;
}
}
.items-container {
overflow-x: visible;
max-height: min(500px, 60vh);
overflow-y: auto;
}
</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/>.
*
*/
-->

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

@ -0,0 +1,118 @@
<template>
<v-dialog v-model="show" width="600" content-class="dialog">
<v-icon small class="close-icon" @click="$emit('input',false)">mdi-close</v-icon>
<list-child-items
v-if="show"
ref="child"
:local-state="localState"
:is-new="isNew"
:size="10"
:meta="meta"
:parent-meta="meta"
:primary-col="primaryCol"
:primary-key="primaryKey"
:api="api"
:query-params="queryParams"
v-bind="$attrs"
v-on="$listeners"
/>
</v-dialog>
</template>
<script>
import Pagination from "@/components/project/spreadsheet/components/pagination";
import ListChildItems from "@/components/project/spreadsheet/components/virtualCell/components/listChildItems";
export default {
name: "listChildItemsModal",
components: {ListChildItems, Pagination},
props: {
localState: Array,
isNew: Boolean,
value: Boolean,
title: {
type: String,
default: 'Link Record'
},
queryParams: {
type: Object,
default() {
return {};
}
},
primaryKey: String,
primaryCol: String,
meta: Object,
parentMeta: Object,
size: Number,
api: [Object, Function],
mm: [Object, Boolean]
},
data: () => ({
data: null,
page: 1
}),
mounted() {
},
methods: {
async loadData() {
if (this.$refs && this.$refs.child) {
this.$refs.child.loadData();
}
}
},
computed: {
show: {
set(v) {
this.$emit('input', v)
}, get() {
return this.value;
}
}
}
}
</script>
<style scoped lang="scss">
::v-deep {
.dialog {
position: relative;
.close-icon {
width: auto;
position: absolute;
right: 10px;
top: 10px;
z-index: 9;
}
}
}
</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/>.
*
*/
-->

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

@ -0,0 +1,225 @@
<template>
<v-dialog v-model="show" width="600" content-class="dialog">
<v-icon small class="close-icon" @click="$emit('input',false)">mdi-close</v-icon>
<v-card width="600" >
<v-card-title class="textColor--text mx-2 justify-center">{{ title }}
</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-icon small class="mr-1" @click="loadData()">mdi-reload</v-icon>
<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">
<template v-if="data && data.list && data.list.length">
<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-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 "
v-if="primaryKey">(Primary Key : {{ ch[primaryKey] }})</span>
<v-spacer/>
<v-chip v-if="hm && ch[`${hm._rtn}Read`] && ch[`${hm._rtn}Read`][hmParentPrimaryValCol]" x-small>
{{ ch[`${hm._rtn}Read`][hmParentPrimaryValCol] }}
</v-chip>
</v-card-text>
</v-card>
</template>
<div v-else-if="data" class="text-center py-15 textLight--text">
No items found
</div>
</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>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import Pagination from "@/components/project/spreadsheet/components/pagination";
export default {
name: "listItems",
components: {Pagination},
props: {
value: Boolean,
hm: [Object, Function],
title: {
type: String,
default: 'Link Record'
},
queryParams: {
type: Object,
default() {
return {};
}
},
primaryKey: String,
primaryCol: String,
meta: Object,
size: Number,
api: [Object, Function],
mm: [Object, Function],
parentId: [String, Number],
parentMeta:[Object]
},
data: () => ({
data: null,
page: 1
}),
mounted() {
this.loadData();
},
methods: {
async loadData() {
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),
...this.queryParams
})
}
}
},
computed: {
show: {
set(v) {
this.$emit('input', v)
}, get() {
return this.value;
}
},
hmParentPrimaryValCol(){
return this.hm &&
this.parentMeta &&
this.parentMeta.columns.find(v => v.pv)._cn
}
}
}
</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)
}
}
.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;
}
::v-deep {
.dialog {
position: relative;
.close-icon {
width: auto;
position: absolute;
right: 10px;
top: 10px;
z-index: 9;
}
}
}
</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/>.
*
*/
-->

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

@ -0,0 +1,467 @@
<template>
<div class="d-flex d-100 chips-wrapper" :class="{active}">
<template v-if="!isForm">
<div class="chips d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value||localState">
<item-chip
v-for="(ch,i) in (value|| localState)"
:active="active"
:item="ch"
:value="getCellValue(ch)"
:key="i"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
<span v-if="value && value.length === 10" class="caption pointer ml-1 grey--text"
@click="showChildListModal">more...
</span>
</template>
</div>
<div class="actions align-center justify-center px-1 flex-shrink-1"
:class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">mdi-plus</x-icon>
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
</template>
<list-items
v-if="newRecordModal"
:hm="hm"
:size="10"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
v-model="newRecordModal"
:api="childApi"
:parent-meta="meta"
@add-new-record="insertAndAddNewChildRecord"
@add="addChildToParent"
:query-params="{
...childQueryParams,
where: isNew ? null :`~not(${childForeignKey},eq,${parentId})~or(${childForeignKey},is,null)`,
}"/>
<list-child-items
:is="isForm ? 'list-child-items' : 'list-child-items-modal'"
:isForm="isForm"
ref="childList"
v-if="childMeta && (childListModal || isForm)"
v-model="childListModal"
:local-state.sync="localState"
:is-new="isNew"
:size="10"
:meta="childMeta"
:parent-meta="meta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
:api="childApi"
:query-params="{
...childQueryParams,
where: `(${childForeignKey},eq,${parentId})`
}"
@new-record="showNewRecordModal"
@edit="editChild"
@unlink="unlinkChild"
@delete="deleteChild"
/>
<dlg-label-submit-cancel
type="primary"
v-if="dialogShow"
:actionsMtd="confirmAction"
:dialogShow="dialogShow"
:heading="confirmMessage"
>
</dlg-label-submit-cancel>
<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"
@cancel="selectedChild = null"
@input="$emit('loadTableData')"
: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.sync="isNewChild"
:disabled-columns="disabledChildColumns"
:breadcrumbs="breadcrumbs"
></component>
</v-dialog>
</div>
</template>
<script>
import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
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";
import listChildItemsModal
from "@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal";
import {parseIfInteger} from "@/helpers";
export default {
name: "has-many-cell",
components: {
ListChildItems,
ItemChip,
ListItems,
Pagination,
DlgLabelSubmitCancel,
listChildItemsModal
},
props: {
breadcrumbs: {
type: Array,
default() {
return [];
}
},
value: [Object, Array],
meta: [Object],
hm: Object,
nodes: [Object],
row: [Object],
sqlUi: [Object, Function],
active: Boolean,
isNew: Boolean,
isForm: Boolean,
},
data: () => ({
newRecordModal: false,
childListModal: false,
// childMeta: null,
dialogShow: false,
confirmAction: null,
confirmMessage: '',
selectedChild: null,
expandFormModal: false,
isNewChild: false,
localState: []
}),
async mounted() {
await this.loadChildMeta()
},
methods: {
async showChildListModal() {
await this.loadChildMeta();
this.childListModal = true;
},
async deleteChild(child) {
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('___');
try {
await this.childApi.delete(id)
this.dialogShow = false;
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
} catch (e) {
this.$toast.error(e.message)
}
}
}
},
async unlinkChild(child) {
if (this.isNew) {
this.localState.splice(this.localState.indexOf(child), 1)
return;
}
await this.loadChildMeta();
const column = this.childMeta.columns.find(c => c.cn === this.hm.cn);
if (column.rqd) {
this.$toast.info('Unlink is not possible, instead add to another record.').goAway(3000)
return
}
const _cn = column._cn;
const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
await this.childApi.update(id, {[_cn]: null}, child)
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
// }
// }
},
async loadChildMeta() {
// todo: optimize
if (!this.childMeta) {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.hm.tn
})
// const childTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.hm.tn
// }]);
// this.childMeta = JSON.parse(childTableData.meta);
// this.childQueryParams = JSON.parse(childTableData.query_params);
}
},
async showNewRecordModal() {
await this.loadChildMeta();
this.newRecordModal = true;
},
async addChildToParent(child) {
if (this.isNew) {
this.localState.push(child);
this.newRecordModal = false;
return;
}
const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
const _cn = this.childForeignKey;
this.newRecordModal = false;
await this.childApi.update(id, {
[_cn]: parseIfInteger(this.parentId)
}, {
[_cn]: child[this.childForeignKey]
});
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
async editChild(child) {
await this.loadChildMeta();
this.isNewChild = false;
this.selectedChild = child;
this.expandFormModal = true;
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500)
},
async insertAndAddNewChildRecord() {
this.newRecordModal = false;
await this.loadChildMeta();
this.isNewChild = true;
this.selectedChild = {
[this.childForeignKey]: parseIfInteger(this.parentId)
};
this.expandFormModal = true;
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.$set(this.$refs.expandedForm.changedColumns, this.childForeignKey, true)
}, 500)
},
getCellValue(cellObj) {
if (cellObj) {
if (this.parentMeta && this.childPrimaryCol) {
return cellObj[this.childPrimaryCol]
}
return Object.values(cellObj)[1]
}
},
async saveLocalState(row) {
let child;
while (child = this.localState.pop()) {
if (row) {
// todo: use common method
const pid = this.meta.columns.filter((c) => c.pk).map(c => row[c._cn]).join('___')
const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
const _cn = this.childForeignKey;
await this.childApi.update(id, {
[_cn]: parseIfInteger(pid)
}, {
[_cn]: child[this.childForeignKey]
});
} else {
await this.addChildToParent(child)
}
}
this.$emit('newRecordsSaved');
}
},
computed: {
childMeta() {
return this.$store.state.meta.metas[this.hm.tn]
},
childApi() {
return this.childMeta && this.childMeta._tn ?
ApiFactory.create(this.$store.getters['project/GtrProjectType'],
this.childMeta && this.childMeta._tn, this.childMeta && this.childMeta.columns, this, this.childMeta) : null;
},
childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn
},
primaryCol() {
return this.meta && (this.meta.columns.find(c => c.pv) || {})._cn
},
childPrimaryKey() {
return this.childMeta && (this.childMeta.columns.find(c => c.pk) || {})._cn
},
childForeignKey() {
return this.childMeta && (this.childMeta.columns.find(c => c.cn === this.hm.cn) || {})._cn
},
disabledChildColumns() {
return {[this.childForeignKey]: true}
},
// todo:
form() {
return this.selectedChild ? () => import("@/components/project/spreadsheet/components/expandedForm") : 'span';
},
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) && !((this.childMeta.v || []).some(v => v.bt && v.bt.cn === 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.v && this.childMeta.v.filter(v => v.hm).map(({hm}) => hm.tn).join()) || '',
parents: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.bt).map(({bt}) => bt.rtn).join()) || '',
many: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.mm).map(({mm}) => mm.rtn).join()) || ''
}
},
parentId() {
return this.meta && this.meta.columns ? this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___') : '';
}
},
watch: {
isNew(n, o) {
if (!n && o) {
this.saveLocalState();
}
}
},
created() {
this.loadChildMeta();
}
}
</script>
<style scoped lang="scss">
.items-container {
overflow-x: visible;
max-height: min(500px, 60vh);
overflow-y: auto;
}
.primary-value {
.primary-key {
display: none;
margin-left: .5em;
}
&:hover .primary-key {
display: inline;
}
}
.child-card {
cursor: pointer;
&:hover {
box-shadow: 0 0 .2em var(--v-textColor-lighten5)
}
}
.hm-items {
//min-width: 200px;
//max-width: 400px;
flex-wrap: wrap;
row-gap: 3px;
gap: 3px;
margin: 3px auto;
}
::v-deep {
.unlink-icon {
padding: 0px 1px 2px 1px;
margin-top: 2px;
margin-right: -2px;
}
.search-field {
input {
max-height: 28px !important;
}
.v-input__slot {
min-height: auto !important;
}
}
}
.chips-wrapper {
.chips {
max-width: 100%;
}
&.active {
.chips {
max-width: calc(100% - 44px);
}
}
}
</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/>.
*
*/
-->

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

@ -0,0 +1,505 @@
<template>
<div class="d-flex d-100 chips-wrapper" :class="{active}">
<template v-if="!isForm">
<div class="chips d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="(value || localState)">
<item-chip v-for="(v,j) in (value || localState)"
:active="active"
:item="v"
:value="getCellValue(v)"
:key="j"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
</template>
<span v-if="value && value.length === 10" class="caption pointer ml-1 grey--text" @click="showChildListModal">more...</span>
</div>
<div class="actions align-center justify-center px-1 flex-shrink-1"
:class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">mdi-plus</x-icon>
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
</template>
<list-items
v-if="newRecordModal"
:hm="true"
:size="10"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
v-model="newRecordModal"
:api="api"
:mm="mm"
:parent-id="row && row[parentPrimaryKey]"
@add-new-record="insertAndAddNewChildRecord"
@add="addChildToParent"
:query-params="childQueryParams"/>
<list-child-items
:is="isForm ? 'list-child-items' : 'list-child-items-modal'"
:isForm="isForm"
ref="childList"
v-if="childMeta && assocMeta && (isForm || childListModal)"
v-model="childListModal"
:is-new="isNew"
: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 }"
:local-state="localState"
@new-record="showNewRecordModal"
@edit="editChild"
@unlink="unlinkChild"
/>
<dlg-label-submit-cancel
type="primary"
v-if="dialogShow"
:actionsMtd="confirmAction"
:dialogShow="dialogShow"
:heading="confirmMessage"
>
</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"
: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.sync="isNewChild"
v-model="selectedChild"
:breadcrumbs="breadcrumbs"
@cancel="selectedChild = null"
@input="onChildSave"
></component>
</v-dialog>
</div>
</template>
<script>
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";
import listChildItemsModal
from "@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal";
import {parseIfInteger} from "@/helpers";
export default {
name: "many-to-many-cell",
components: {ListChildItems, ItemChip, ListItems, DlgLabelSubmitCancel, listChildItemsModal},
props: {
breadcrumbs: {
type: Array,
default() {
return [];
}
},
value: [Object, Array],
meta: [Object],
mm: Object,
nodes: [Object],
row: [Object],
api: [Object, Function],
sqlUi: [Object, Function],
active: Boolean,
isNew: Boolean,
isForm: Boolean,
},
data: () => ({
isNewChild: false,
newRecordModal: false,
childListModal: false,
// childMeta: null,
// assocMeta: null,
childList: null,
dialogShow: false,
confirmAction: null,
confirmMessage: '',
selectedChild: null,
expandFormModal: false,
localState: []
}),
async mounted() {
if (this.isForm) {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
}
},
methods: {
async onChildSave(child) {
if (this.isNewChild) {
this.isNewChild = false;
await this.addChildToParent(child);
} else {
this.$emit('loadTableData')
}
},
async showChildListModal() {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
this.childListModal = true;
}, async unlinkChild(child) {
if (this.isNew) {
this.localState.splice(this.localState.indexOf(child), 1)
return;
}
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
const _pcn = this.meta.columns.find(c => c.cn === this.mm.cn)._cn;
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')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
async removeChild(child) {
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.dialogShow = false;
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
}
}
},
async loadChildMeta() {
// todo: optimize
if (!this.childMeta) {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.mm.rtn
})
// const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.mm.rtn
// }]);
// this.childMeta = JSON.parse(parentTableData.meta)
}
},
async loadAssociateTableMeta() {
// todo: optimize
if (!this.assocMeta) {
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn: this.mm.vtn
})
// const assocTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.mm.vtn
// }]);
// this.assocMeta = JSON.parse(assocTableData.meta)
}
},
async showNewRecordModal() {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()]);
this.newRecordModal = true;
// this.list = await this.c hildApi.paginatedList({})
},
async addChildToParent(child) {
if (this.isNew) {
this.localState.push(child)
this.newRecordModal = false;
return
}
const cid = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
const pid = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
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;
try {
await this.assocApi.insert({
[vcidCol]: parseIfInteger(cid),
[vpidCol]: parseIfInteger(pid)
});
this.$emit('loadTableData')
} catch (e) {
// todo: handle
console.log(e)
}
this.newRecordModal = false;
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData();
}
},
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)
},
async saveLocalState(row) {
let child;
while (child = this.localState.pop()) {
if (row) {
// todo: use common method
const cid = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
const pid = this.meta.columns.filter((c) => c.pk).map(c => row[c._cn]).join('___');
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;
await this.assocApi.insert({
[vcidCol]: parseIfInteger(cid),
[vpidCol]: parseIfInteger(pid)
});
} else {
await this.addChildToParent(child)
}
}
this.$emit('newRecordsSaved');
}
},
computed: {
getCellValue() {
return cellObj => {
if (cellObj) {
if (this.childPrimaryCol) {
return cellObj[this.childPrimaryCol]
}
return Object.values(cellObj)[1]
}
}
},
childMeta() {
return this.$store.state.meta.metas[this.mm.rtn]
},
assocMeta() {
return this.$store.state.meta.metas[this.mm.vtn]
},
childApi() {
return this.childMeta && this.childMeta._tn ?
ApiFactory.create(
this.$store.getters['project/GtrProjectType'],
this.childMeta._tn,
this.childMeta.columns,
this,
this.childMeta
) : null;
},
assocApi() {
return this.assocMeta && this.assocMeta._tn ?
ApiFactory.create(
this.$store.getters['project/GtrProjectType'],
this.assocMeta._tn,
this.assocMeta.columns,
this,
this.assocMeta
) : null;
},
childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn
},
childPrimaryKey() {
return this.childMeta && (this.childMeta.columns.find(c => c.pk) || {})._cn
},
parentPrimaryKey() {
return this.meta && (this.meta.columns.find(c => c.pk) || {})._cn
},
childQueryParams() {
if (!this.childMeta) return {}
return {
childs: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.hm).map(({hm}) => hm.tn).join()) || '',
parents: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.bt).map(({bt}) => bt.rtn).join()) || '',
many: (this.childMeta && this.childMeta.v && this.childMeta.v.filter(v => v.mm).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) && !((this.childMeta.v || []).some(v => v.bt && v.bt.cn === 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';
},
},
watch: {
async isNew(n, o) {
if (!n && o) {
await this.saveLocalState();
}
}
},
created() {
this.loadChildMeta();
this.loadAssociateTableMeta()
}
}
</script>
<style scoped lang="scss">
.items-container {
overflow-x: visible;
max-height: min(500px, 60vh);
overflow-y: auto;
}
.primary-value {
.primary-key {
display: none;
margin-left: .5em;
}
&:hover .primary-key {
display: inline;
}
}
.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)
}
}
.hm-items {
//min-width: 200px;
//max-width: 400px;
flex-wrap: wrap;
row-gap: 3px;
gap: 3px;
margin: 3px auto;
}
.chips-wrapper {
.chips {
max-width: 100%;
}
&.active {
.chips {
max-width: calc(100% - 44px);
}
}
}
</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/>.
*
*/
-->

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

@ -0,0 +1,181 @@
<template>
<div class="d-flex align-center">
<v-tooltip bottom>
<template #activator="{on}">
<v-icon v-if="column.hm" color="warning" x-small class="mr-1" v-on="on">mdi-table-arrow-right</v-icon>
<v-icon v-else-if="column.bt" color="info" x-small class="mr-1" v-on="on">mdi-table-arrow-left</v-icon>
<v-icon v-else-if="column.mm" color="pink" x-small class="mr-1" v-on="on">mdi-table-network</v-icon>
<span v-on="on" class="name flex-grow-1" :title="column._cn">{{ column._cn }}</span>
<span v-if="column.rqd" v-on="on" class="error--text text--lighten-1">&nbsp;*</span>
</template>
<span class="caption" v-html="tooltipMsg"></span>
</v-tooltip>
<v-spacer>
</v-spacer>
<v-menu offset-y open-on-hover left>
<template v-slot:activator="{on}">
<v-icon v-if="!isForm" v-on="on" small>mdi-menu-down</v-icon>
</template>
<v-list dense>
<v-list-item dense @click="editColumnMenu = true">
<x-icon small class="mr-1" color="primary">mdi-pencil</x-icon>
<span class="caption">Edit</span>
</v-list-item>
<!-- <v-list-item dense @click="setAsPrimaryValue">
<x-icon small class="mr-1" color="primary">mdi-key-star</x-icon>
<v-tooltip bottom>
<template v-slot:activator="{on}">
<span class="caption" v-on="on">Set as Primary value</span>
</template>
<span class="caption font-weight-bold">Primary value will be shown in place of primary key</span>
</v-tooltip>
</v-list-item> -->
<v-list-item @click="columnDeleteDialog = true">
<x-icon small class="mr-1" color="error">mdi-delete-outline</x-icon>
<span class="caption">Delete</span>
</v-list-item>
</v-list>
</v-menu>
<v-dialog v-model="columnDeleteDialog" max-width="500"
persistent>
<v-card>
<v-card-title class="grey darken-2 subheading white--text">Confirm</v-card-title>
<v-divider></v-divider>
<v-card-text class="mt-4 title">Do you want to delete <span class="font-weight-bold">'{{
column.cn
}}'</span> column ?
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="d-flex pa-4">
<v-spacer></v-spacer>
<v-btn small @click="columnDeleteDialog = false">Cancel</v-btn>
<v-btn small color="error" @click="deleteColumn">Confirm</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-menu offset-y v-model="editColumnMenu" content-class="elevation-0" left>
<template v-slot:activator="{on}">
<span v-on="on"></span>
</template>
<edit-virtual-column
v-if="editColumnMenu"
v-model="editColumnMenu"
:nodes="nodes"
:edit-column="true"
:column="column"
:meta="meta"
v-on="$listeners"
></edit-virtual-column>
</v-menu>
</div>
</template>
<script>
import EditVirtualColumn from "@/components/project/spreadsheet/components/editVirtualColumn";
export default {
components: {EditVirtualColumn},
props: ['column', 'nodes', 'meta', 'isForm'],
name: "virtualHeaderCell",
data: () => ({
columnDeleteDialog: false,
editColumnMenu: false
}),
computed: {
type() {
if (this.column.bt) return 'bt'
if (this.column.hm) return 'hm'
if (this.column.mm) return 'mm'
},
childColumn() {
if (this.column.bt) return this.column.bt.cn
if (this.column.hm) return this.column.hm.cn
if (this.column.mm) return this.column.mm.rcn
},
childTable() {
if (this.column.bt) return this.column.bt.tn
if (this.column.hm) return this.column.hm.tn
if (this.column.mm) return this.column.mm.rtn
},
parentTable() {
if (this.column.bt) return this.column.bt.rtn
if (this.column.hm) return this.column.hm.rtn
if (this.column.mm) return this.column.mm.tn
},
parentColumn() {
if (this.column.bt) return this.column.bt.rcn
if (this.column.hm) return this.column.hm.rcn
if (this.column.mm) return this.column.mm.cn
},
tooltipMsg() {
if (!this.column) return '';
if (this.column.hm) {
return `'${this.column.hm._rtn}' has many '${this.column.hm._tn}'`
} else if (this.column.mm) {
return `'${this.column.mm._tn}' & '${this.column.mm._rtn}' have <br>many to many relation`
} else if (this.column.bt) {
return `'${this.column.bt._tn}' belongs to '${this.column.bt._rtn}'`
}
}
}, methods: {
async deleteColumn() {
try {
const column = {...this.column, cno: this.column.cn};
await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, "xcRelationColumnDelete", {
type: this.type,
childColumn: this.childColumn,
childTable: this.childTable,
parentTable: this.parentTable,
parentColumn: this.parentColumn,
assocTable: this.column.mm && this.column.mm.vtn
}]);
this.$emit('saved');
this.columnDeleteDialog = false;
} catch (e) {
console.log(e)
}
}
}
}
</script>
<style scoped>
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</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/>.
*
*/
-->

203
packages/nc-gui/components/project/spreadsheet/editColumn/linkedToAnotherOptions.vue

@ -1,203 +0,0 @@
<template>
<v-container fluid class="wrapper">
<v-row>
<v-col cols="6">
<v-autocomplete
outlined
class="caption"
hide-details
:loading="isRefTablesLoading"
label="Reference Table"
:full-width="false"
v-model="relation.parentTable"
:items="refTables"
item-text="_tn"
item-value="tn"
required
dense
@change="loadColumnList"
></v-autocomplete>
</v-col
>
<v-col cols="6">
<v-autocomplete
outlined
class="caption"
hide-details
:loading="isRefColumnsLoading"
label="Reference Column"
:full-width="false"
v-model="relation.parentColumn"
:items="refColumns"
item-text="_cn"
item-value="cn"
required
dense
ref="parentColumnRef"
@change="onColumnSelect"
></v-autocomplete>
</v-col
>
</v-row>
<v-row>
<v-col cols="6">
<v-autocomplete
outlined
class="caption"
hide-details
label="On Update"
:full-width="false"
v-model="relation.onUpdate"
:items="onUpdateDeleteOptions"
required
dense
:disabled="relation.type !== 'real'"
></v-autocomplete>
</v-col>
<v-col cols="6">
<v-autocomplete
outlined
class="caption"
hide-details
label="On Delete"
:full-width="false"
v-model="relation.onDelete"
:items="onUpdateDeleteOptions"
required
dense
:disabled="relation.type !== 'real'"
></v-autocomplete>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox
false-value="real"
true-value="virtual"
label="Virtual Relation"
:full-width="false"
v-model="relation.type"
required
class="mt-0"
dense
></v-checkbox>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
name: "linked-to-another-options",
props: ['nodes', 'column'],
data: () => ({
refTables: [],
refColumns: [],
relation: {},
isRefTablesLoading: false,
isRefColumnsLoading: false,
onUpdateDeleteOptions: [
"NO ACTION",
"CASCADE",
"RESTRICT",
"SET NULL",
"SET DEFAULT"
],
}),
async created() {
await this.loadTablesList();
this.relation = {
childColumn: this.column.cn,
childTable: this.nodes.tn,
parentTable: this.column.rtn || "",
parentColumn: this.column.rcn || "",
onDelete: "CASCADE",
onUpdate: "CASCADE",
updateRelation: this.column.rtn ? true : false,
type: 'real'
}
},
methods: {
async loadColumnList() {
if (!this.relation.parentTable) return;
this.isRefColumnsLoading = true;
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'columnList', {tn: this.relation.parentTable}])
const columns = result.data.list;
this.refColumns = JSON.parse(JSON.stringify(columns));
if (this.relation.updateRelation && !this.relationColumnChanged) {
//only first time when editing add defaault value to this field
this.relation.parentColumn = this.column.rcn;
this.relationColumnChanged = true;
} else {
//find pk column and assign to parentColumn
const pkKeyColumns = this.refColumns.filter(el => el.pk);
this.relation.parentColumn = (pkKeyColumns[0] || {}).cn || "";
}
this.onColumnSelect();
this.isRefColumnsLoading = false;
},
async loadTablesList() {
this.isRefTablesLoading = true;
const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableList']);
this.refTables = result.data.list.map(({tn,_tn}) => ({tn,_tn}))
this.isRefTablesLoading = false;
},
async saveRelation() {
try {
let result = await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [
{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
},
this.relation.type === 'real' ? "relationCreate" : 'xcVirtualRelationCreate',
this.relation
]);
} catch (e) {
throw e;
}
},
onColumnSelect() {
const col = this.refColumns.find(c => this.relation.parentColumn === c.cn);
this.$emit('onColumnSelect', col)
}
},
watch: {
'column.cn': (c) => {
this.$set(this.relation, 'childColumn', c);
}
}
}
</script>
<style scoped>
.wrapper {
border: solid 2px #7f828b33;
border-radius: 4px;
}
/deep/ .v-input__append-inner {
margin-top: 4px !important;
}
</style>

101
packages/nc-gui/components/project/spreadsheet/editableCell/enumRadioEditableCell.vue

@ -1,101 +0,0 @@
<template>
<div class="d-flex align-center">
<div>
<div class="item" v-for="(val,i) of enumValues" :key="val">
<input type="radio" :id="`key-radio-${val}`" class="orange--text" v-model="localState" :value="val">
<label class="py-1 px-3 d-inline-block my-1 label" :for="`key-radio-${val}`"
:style="{
background:colors[i % colors.length ]
}"
>{{ val }}</label>
</div>
</div>
</div>
</template>
<script>
import {enumColor as colors} from "@/components/project/spreadsheet/helpers/colors";
export default {
name: "enum-radio-editable-cell",
props: {
value: String,
column: Object
},
mounted() {
// this.$el.focus();
// let event;
// event = document.createEvent('MouseEvents');
// event.initMouseEvent('mousedown', true, true, window);
// this.$el.dispatchEvent(event);
},
computed: {
colors() {
return this.$store.state.windows.darkTheme ? colors.dark : colors.light;
},
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val);
this.$emit('update');
}
},
enumValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/^'|'$/g, ''))
}
return [];
},
parentListeners() {
const $listeners = {};
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur;
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
}
}
</script>
<style scoped>
.label {
border-radius: 25px;
}
.item {
white-space: nowrap;
}
</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/>.
*
*/
-->

69
packages/nc-gui/components/project/spreadsheet/editableCell/integerCell.vue

@ -1,69 +0,0 @@
<template>
<input v-on="parentListeners" v-model="localState" type="number">
</template>
<script>
export default {
name: "integerCell",
props: {
value: [String, Number]
},
mounted() {
this.$el.focus();
},
computed: {
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', parseInt(val, 10));
}
},
parentListeners(){
const $listeners = {};
if(this.$listeners.blur){
$listeners.blur = this.$listeners.blur;
}
if(this.$listeners.focus){
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
}
}
</script>
<style scoped>
input {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
}
</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/>.
*
*/
-->

97
packages/nc-gui/components/project/spreadsheet/editableCell/jsonCell.vue

@ -1,97 +0,0 @@
<template>
<div @keydown.stop.enter class="cell-container">
<div class="d-flex ma-1" v-if="!isForm">
<v-spacer>
</v-spacer>
<v-btn outlined x-small class="mr-1" @click="$emit('cancel')">Cancel</v-btn>
<v-btn x-small color="primary" @click="save">Save</v-btn>
</div>
<monaco-json-object-editor v-model="localState"
style="width: 300px;min-height: 200px;min-width:100%"></monaco-json-object-editor>
</div>
</template>
<script>
import MonacoJsonEditor from "@/components/monaco/MonacoJsonEditor";
import MonacoJsonObjectEditor from "@/components/monaco/MonacoJsonObjectEditor";
export default {
name: "json-cell",
components: {MonacoJsonObjectEditor, MonacoJsonEditor},
props: {
value: String,
isForm:Boolean
},
data: () => ({
localState: ''
}),
created() {
this.localState = typeof this.value === 'string' ? JSON.parse(this.value) : this.value;
},
mounted() {
}, watch: {
value(val) {
this.localState = typeof val === 'string' ? JSON.parse(val) : val;
},
localState(val){
if(this.isForm){
this.$emit('input', JSON.stringify(val))
}
}
},
methods: {
save() {
this.$emit('input', JSON.stringify(this.localState))
}
},
computed:{
parentListeners(){
const $listeners = {};
if(this.$listeners.blur){
$listeners.blur = this.$listeners.blur;
}
if(this.$listeners.focus){
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
}
}
</script>
<style scoped>
.cell-container {
/*margin: 0 -5px;*/
/*position: relative;*/
width: 100%
}
</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/>.
*
*/
-->

8
packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js

@ -43,10 +43,10 @@ const uiTypes = [
name: 'ForeignKey',
icon: 'mdi-link-variant',
},
// {
// name: 'LinkToAnotherRecord',
// icon: 'mdi-link-variant',
// },
{
name: 'LinkToAnotherRecord',
icon: 'mdi-link-variant',
},
{
name: 'SingleLineText',
icon: 'mdi-format-color-text',

33
packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js

@ -8,7 +8,7 @@ export default {
sortList: [],
showFields: {},
// fieldList: [],
meta: {},
// meta: {},
data: [],
}),
methods: {
@ -57,21 +57,36 @@ export default {
return c._cn;
})
},
realFieldList() {
return this.availableRealColumns.map(c => {
return c._cn;
})
},
availableRealColumns() {
return this.availableColumns && this.availableColumns.filter(c => !c.virtual)
},
availableColumns() {
let columns = [];
// todo: generate hideCols based on default values
const hideCols = ['created_at', 'updated_at'];
if (this.showSystemFields) {
columns = this.meta.columns || [];
} else if (this.data && this.data.length) {
// c._cn in this.data[0].row &&
columns = (this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn))) || [];
columns = (this.meta.columns.filter(c => !(c.pk && c.ai)
&& !((this.meta.v || []).some(v => v.bt && v.bt.cn === c.cn))
&& !hideCols.includes(c.cn))) || [];
} else {
columns = (this.meta && this.meta.columns && this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn))) || [];
}
if (this.meta && this.meta.v) {
columns = [...columns, ...this.meta.v.map(v => ({...v, virtual: 1}))];
}
if (this.fieldsOrder.length) {
return [...columns].sort((c1, c2) => {
const i1 = this.fieldsOrder.indexOf(c1._cn);
@ -102,8 +117,12 @@ export default {
return {
limit: this.size,
offset: this.size * (this.page - 1),
// condition: this.condition,
where: this.concatenatedXWhere,
sort: this.sort
sort: this.sort,
childs: (this.meta && this.meta.v && this.meta.v.filter(v => v.hm).map(({hm}) => hm.tn).join()) || '',
parents: (this.meta && this.meta.v && this.meta.v.filter(v => v.bt).map(({bt}) => bt.rtn).join()) || '',
many: (this.meta && this.meta.v && this.meta.v.filter(v => v.mm).map(({mm}) => mm.rtn).join()) || ''
}
}, colLength() {
return (this.availableColumns && this.availableColumns.length) || 0
@ -130,7 +149,7 @@ export default {
},
belongsTo() {
return this.meta && this.meta.belongsTo ? this.meta.belongsTo.reduce((bt, o) => {
const _cn = (this.meta.columns.find(c => c.cn === o.cn)||{})._cn
const _cn = (this.meta.columns.find(c => c.cn === o.cn) || {})._cn
bt[_cn] = o;
return bt;
}, {}) : {};
@ -146,8 +165,8 @@ export default {
return this.nodes.tn || this.nodes.view_name
},
primaryValueColumn() {
if (!this.meta || !this.availableColumns) return '';
return this.availableColumns.length ? this.availableColumns[0]._cn : '';
if (!this.meta || !this.availableColumns || !this.availableColumns.length) return '';
return (this.availableColumns.find(col => col.pv) || {_cn: ''})._cn;
},
},
watch: {

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

@ -18,7 +18,7 @@
</div>
</template>
<v-list dense>
<v-list-item v-for="col in availableColumns" :key="col.cn"
<v-list-item v-for="col in availableRealColumns" :key="col.cn"
@click="searchField = col._cn">
<span class="caption">{{ col._cn }}</span>
</v-list-item>
@ -53,8 +53,10 @@
<v-spacer></v-spacer>
<lock-menu v-if="_isUIAllowed('view-type')" v-model="viewStatus.type"></lock-menu>
<x-btn tooltip="Reload view data" outlined small text @click="loadTableData">
<x-btn tooltip="Reload view data" outlined small text @click="reload">
<v-icon small class="mr-1" color="grey darken-3">mdi-reload</v-icon>
</x-btn>
<x-btn tooltip="Add new row" v-if="relationType !== 'bt'" :disabled="isLocked" outlined small text
@ -66,28 +68,30 @@
Save
</x-btn>
<fields v-model="showFields" :field-list="fieldList"
:meta="meta"
:is-locked="isLocked"
:fieldsOrder.sync="fieldsOrder"
:sqlUi="sqlUi"
:showSystemFields.sync="showSystemFields"></fields>
<fields
v-model="showFields"
:field-list="fieldList"
:meta="meta"
:is-locked="isLocked"
:fieldsOrder.sync="fieldsOrder"
:sqlUi="sqlUi"
:showSystemFields.sync="showSystemFields"
/>
<sort-list
:is-locked="isLocked"
:field-list="fieldList"
:field-list="realFieldList"
v-model="sortList"
></sort-list>
<column-filter
:is-locked="isLocked"
:field-list="fieldList"
:field-list="realFieldList"
v-model="filters"
dense>
</column-filter>
<v-tooltip bottom>
<template v-slot:activator="{on}">
<v-btn :disabled="isLocked" v-on="on" small @click="deleteTable('showDialog')" outlined text>
<v-btn :disabled="isLocked" v-on="on" small @click="checkAndDeleteTable" outlined text>
<x-icon small color="red grey">mdi-delete-outline</x-icon>
</v-btn>
</template>
@ -129,19 +133,10 @@
color="grey darken-3"
>{{ toggleDrawer ? 'mdi-door-closed' : 'mdi-door-open' }}
</v-icon>
</x-btn>
<!-- <v-spacer></v-spacer>-->
<!-- <v-text-field outlined dense hide-details class="elevation-0" append-icon="mdi-magnify"></v-text-field>-->
</v-toolbar>
<div :class="`cell-height-${cellHeight}`"
style=" height:calc(100% - 32px);overflow:auto;transition: width 500ms "
style=" height:calc(100% - 32px);overflow:auto;transition: width 100ms "
class="d-flex"
>
<div class="flex-grow-1 h-100" style="overflow-y: auto">
@ -150,6 +145,7 @@
<v-skeleton-loader v-if="!dataLoaded && (loadingData || loadingMeta)" type="table"></v-skeleton-loader>
<template v-else-if="selectedView && (selectedView.type === 'table' || selectedView.show_as === 'grid' )">
<xc-grid-view
:key="key"
ref="ncgridview"
:relationType="relationType"
:columns-width.sync="columnsWidth"
@ -167,6 +163,7 @@
:visibleColLength="visibleColLength"
:meta="meta"
:isVirtual="selectedView.type === 'vtable'"
:api="api"
@onNewColCreation="onNewColCreation"
@onCellValueChange="onCellValueChange"
@insertNewRow="insertNewRow"
@ -174,6 +171,8 @@
@addNewRelationTab="addNewRelationTab"
@expandRow="expandRow"
@onRelationDelete="loadMeta"
@loadTableData="loadTableData"
@loadMeta="loadMeta"
></xc-grid-view>
</template>
<template v-else-if="selectedView && selectedView.show_as === 'gallery' ">
@ -215,33 +214,39 @@
</div>
<template v-if="data">
<v-pagination
v-if="count !== Infinity"
style="max-width: 100%"
<pagination
:count="count"
:size="size"
v-model="page"
:length="Math.ceil(count / size)"
:total-visible="8"
@input="loadTableData"
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-pagination
v-if="count !== Infinity"
style="max-width: 100%"
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>
:length="Math.ceil(count / size)"
:total-visible="8"
@input="loadTableData"
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="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>
<!-- <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>-->
@ -388,12 +393,11 @@
v-model="showExpandModal">
<expanded-form
:key="selectedExpandRowIndex"
:db-alias="nodes.dbAlias"
:has-many="hasMany"
:belongs-to="belongsTo"
v-if="selectedExpandRowIndex != null && data[selectedExpandRowIndex]"
@cancel="showExpandModal = false;"
@input="showExpandModal = false; (data[selectedExpandRowIndex] && data[selectedExpandRowIndex].rowMeta && delete data[selectedExpandRowIndex].rowMeta.new)"
:table="table"
v-model="data[selectedExpandRowIndex].row"
:oldRow.sync="data[selectedExpandRowIndex].oldRow"
@ -403,7 +407,13 @@
:sql-ui="sqlUi"
:primary-value-column="primaryValueColumn"
:api="api"
:availableColumns="availableColumns"
:nodes="nodes"
:query-params="queryParams"
@cancel="showExpandModal = false;"
@input="showExpandModal = false; (data[selectedExpandRowIndex] && data[selectedExpandRowIndex].rowMeta && delete data[selectedExpandRowIndex].rowMeta.new) ; loadTableData()"
@commented="reloadComments"
@loadTableData="loadTableData"
></expanded-form>
</v-dialog>
@ -428,7 +438,7 @@ import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
import Table from "@/components/project/table";
import {SqlUI} from "@/helpers/SqlUiFactory";
import NewColumn from "@/components/project/spreadsheet/editColumn/editColumn";
import NewColumn from "@/components/project/spreadsheet/components/editColumn";
import {mapActions} from "vuex";
import AdditionalFeatures from "@/components/project/spreadsheet/overlay/additinalFeatures";
import ColumnFilter from "~/components/project/spreadsheet/components/columnFilterMenu";
@ -444,11 +454,13 @@ import SpreadsheetNavDrawer from "@/components/project/spreadsheet/components/sp
import spreadsheet from "@/components/project/spreadsheet/mixins/spreadsheet";
import LockMenu from "@/components/project/spreadsheet/components/lockMenu";
import ExpandedForm from "@/components/project/spreadsheet/components/expandedForm";
import Pagination from "@/components/project/spreadsheet/components/pagination";
export default {
mixins: [spreadsheet],
name: "rows-xc-data-table",
components: {
Pagination,
ExpandedForm,
LockMenu,
SpreadsheetNavDrawer,
@ -477,6 +489,7 @@ export default {
showTabs: [Boolean, Number]
},
data: () => ({
key: 1,
dataLoaded: false,
searchQueryVal: '',
columnsWidth: null,
@ -574,7 +587,6 @@ export default {
console.log(e)
}
this.searchField = this.primaryValueColumn;
this.dataLoaded = true;
// await this.loadViews();
@ -583,6 +595,22 @@ export default {
...mapActions({
loadTablesFromChildTreeNode: "project/loadTablesFromChildTreeNode"
}),
checkAndDeleteTable() {
if (
!this.meta &&
this.meta.hasMany && this.meta.hasMany.length ||
this.meta.manyToMany && this.meta.manyToMany.length ||
this.meta.belongsTo && this.meta.belongsTo.length
) {
return this.$toast.info('Please delete relations before deleting table.').goAway(3000)
}
this.deleteTable('showDialog')
},
async reload() {
this.$store.commit('meta/MutClear');
await this.loadTableData();
this.key = Math.random();
},
reloadComments() {
if (this.$refs.ncgridview) {
this.$refs.ncgridview.xcAuditModelCommentsCount();
@ -672,27 +700,48 @@ export default {
}
},
async save() {
for (let row = 0; row < this.rowLength; row++) {
const {row: rowObj, rowMeta} = this.data[row];
if (rowMeta.new) {
try {
const pks = this.availableColumns.filter((col) => {
const pks = this.meta.columns.filter((col) => {
return col.pk;
});
if (this.availableColumns.every((col) => {
if (this.meta.columns.every((col) => {
return !col.ai;
}) && pks.length && pks.every(col => !rowObj[col._cn])) {
return this.$toast.info('Primary column is empty please provide some value').goAway(3000);
}
if (this.meta.columns.some((col) => {
return !col.ai && col.rqd && (rowObj[col._cn] === undefined || rowObj[col._cn] === null) && !col.default
})) {
return;
}
const insertObj = this.availableColumns.reduce((o, col) => {
const insertObj = this.meta.columns.reduce((o, col) => {
if (!col.ai && (rowObj && rowObj[col._cn]) !== null) {
o[col._cn] = rowObj && rowObj[col._cn];
}
return o;
}, {});
const insertedData = await this.api.insert(insertObj);
let insertedData = await this.api.insert(insertObj);
// todo: optimize
if (this.meta.v && this.meta.v.length) {
try {
const where = this.meta.columns.filter((c) => c.pk).map(c => `(${c._cn},eq,${insertedData[c._cn]})`).join('~and');
if (where) {
const {childs, parents, many} = this.queryParams;
const data = (await this.api.list({where, childs, parents, many}) || [insertedData]);
insertedData = data.length ? data[0] : insertedData;
}
} catch (e) {
// ignore
}
}
this.data.splice(row, 1, {
row: insertedData,
rowMeta: {},
@ -713,7 +762,15 @@ export default {
}
}
},
async onCellValueChange(col, row, column) {
onCellValueChangeDebounce: debounce(async function (col, row, column, self) {
await self.onCellValueChangeFn(col, row, column)
}, 300),
onCellValueChange(col, row, column) {
this.onCellValueChangeDebounce(col, row, column, this)
},
async onCellValueChangeFn(col, row, column) {
if (!this.data[row]) return;
const {row: rowObj, rowMeta, oldRow} = this.data[row];
if (rowMeta.new) {
@ -791,6 +848,7 @@ export default {
const {rowMeta} = this.data[this.data.length - 1];
this.expandRow(this.data.length - 1, rowMeta)
}
// this.save()
},
@ -809,20 +867,33 @@ export default {
break;
}
},
async loadMeta() {
async loadMeta(updateShowFields = true) {
this.loadingMeta = true;
const tableMeta = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// const tableMeta = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.table
// }]);
// this.meta = JSON.parse(tableMeta.meta);
const tableMeta = await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableXcModelGet', {
tn: this.table
}]);
this.meta = JSON.parse(tableMeta.meta);
dbAlias: this.nodes.dbAlias,
tn: this.table,
force: true
});
this.loadingMeta = false;
if (updateShowFields) {
try {
const qp = JSON.parse(tableMeta.query_params)
this.showFields = qp.showFields ? qp.showFields : this.showFields;
} catch (e) {
}
}
},
loadTableDataDeb: debounce(async function (self) {
await self.loadTableDataFn()
}, 100),
}, 200),
loadTableData() {
this.loadTableDataDeb(this)
},
@ -860,14 +931,17 @@ export default {
this.selectedExpandRowMeta = rowMeta;
},
async onNewColCreation() {
await this.loadMeta();
await this.loadMeta(true);
this.$nextTick(async () => {
await this.loadTableData();
this.mapFieldsAndShowFields();
// this.mapFieldsAndShowFields();
});
}
},
computed: {
meta() {
return this.$store.state.meta.metas[this.table];
},
currentApiUrl() {
return this.api && `${this.api.apiUrl}?` + Object.entries(this.queryParams).filter(p => p[1]).map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&')
},
@ -878,10 +952,9 @@ export default {
return SqlUI.create(this.nodes.dbConnection);
},
api() {
return this.meta && this.meta._tn ? ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.meta && this.meta._tn, this.meta && this.meta.columns, this) : null;
return this.meta && this.meta._tn ? ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.meta && this.meta._tn, this.meta && this.meta.columns, this, this.meta) : null;
}
},
}
</script>

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

@ -14,16 +14,26 @@
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
v-xc-ver-resize
v-for="(col,i) in availableColumns"
:key="col.cn"
:key=" col._cn"
v-show="showFields[col._cn]"
@xcresize="onresize(col._cn,$event)"
@xcresizing="resizingCol = col._cn"
@xcresizing="onXcResizing(col._cn,$event)"
@xcresized="resizingCol = null"
:data-col="col._cn"
>
<!-- :style="columnsWidth[col._cn] ? `min-width:${columnsWidth[col._cn]}; max-width:${columnsWidth[col._cn]}` : ''"
-->
<virtual-header-cell v-if="col.virtual"
:column="col"
:nodes="nodes"
:meta="meta"
@saved="onNewColCreation"
/>
<header-cell
v-else
:isPublicView="isPublicView"
@onRelationDelete="$emit('onRelationDelete')"
:nodes="nodes"
@ -37,7 +47,6 @@
@saved="onNewColCreation"
></header-cell>
<!--{{ col.cn }}-->
</th>
<th
@ -63,9 +72,10 @@
</v-menu>
</th>
</tr>
</thead>
<tbody>
<tbody v-click-outside="onClickOutside">
<tr
v-for="({row:rowObj, rowMeta, oldRow},row) in data"
:key="row"
@ -101,156 +111,171 @@
</div>
</td>
<td
class="cell pointer"
v-for="(columnObj,col) in availableColumns"
:key="columnObj._cn"
:key="row + columnObj._cn"
:class="{
active : !isPublicView && selected.col === col && selected.row === row && isEditable ,
'active' : !isPublicView && selected.col === col && selected.row === row && isEditable ,
'primary-column' : primaryValueColumn === columnObj._cn,
'text-center': isCentrallyAligned(columnObj)
'text-center': isCentrallyAligned(columnObj),
'required': isRequired(columnObj,rowObj)
}"
@dblclick="makeEditable(col,row,columnObj.ai)"
@click="makeSelected(col,row);"
v-show="showFields[columnObj._cn]"
:data-col="columnObj._cn"
>
<virtual-cell
v-if="columnObj.virtual"
:column="columnObj"
:row="rowObj"
:nodes="nodes"
:meta="meta"
:api="api"
:active="selected.col === col && selected.row === row"
:sql-ui="sqlUi"
:is-new="rowMeta.new"
v-on="$listeners"
@updateCol="(...args) => updateCol(...args, columnObj.bt && meta.columns.find( c => c.cn === columnObj.bt.cn), col, row)"
></virtual-cell>
<editable-cell
v-if="!isLocked
&& !isPublicView
&& (
editEnabled.col === col
&& editEnabled.row === row
)
|| enableEditable(columnObj)"
v-else-if="
!isLocked
&& !isPublicView
&& (editEnabled.col === col && editEnabled.row === row)
|| enableEditable(columnObj)
"
:column="columnObj"
:meta="meta"
:active="(selected.col === col && selected.row === row)"
:active="selected.col === col && selected.row === row"
v-model="rowObj[columnObj._cn]"
@save="editEnabled = {}"
@cancel="editEnabled = {}"
@update="onCellValueChange(col, row, columnObj)"
@blur="onCellValueChange(col, row, columnObj,'blur')"
@change="onCellValueChange(col, row, columnObj)"
:sql-ui="sqlUi"
:db-alias="nodes.dbAlias"
/>
<!-- @change="changed(col,row)"-->
<!-- />-->
<div v-else-if="columnObj.cn in hasMany" class="hasmany-col d-flex ">
{{ rowObj[columnObj._cn] }}
<v-spacer></v-spacer>
<v-menu open-on-hover>
<template v-slot:activator="{on}">
<v-icon v-on="on" class=" hasmany-col-menu-icon">mdi-menu-down</v-icon>
</template>
<v-list dense>
<v-list-item v-for="(rel,i) in hasMany[columnObj.cn]"
@click="addNewRelationTab(
rel,
table,
meta._tn,
rel.tn,
rel._tn,
rowObj[columnObj._cn],
'hm',
rowObj,
rowObj[primaryValueColumn]
)"
:key="i"
>
<v-chip small :color="colors[i % colors.length]">
<span class="caption text-capitalize"> {{ rel._tn }}</span>
</v-chip>
</v-list-item>
</v-list>
</v-menu>
</div>
<span v-else-if="columnObj._cn in belongsTo"
@click="addNewRelationTab(
belongsTo[columnObj._cn],
table,
meta._tn,
belongsTo[columnObj._cn].rtn,
belongsTo[columnObj._cn]._rtn,
rowObj[columnObj._cn],
'bt',
rowObj,
rowObj[primaryValueColumn]
)"
class="belongsto-col">{{ rowObj[columnObj._cn] }}</span>
<template v-else-if="primaryValueColumn === columnObj._cn">
<v-menu open-on-hover offset-y bottom>
<template v-slot:activator="{on}">
<!-- <v-chip v-on="on"
small
class="caption xc-bt-chip"
outlined
color="success">
{{ rowObj[columnObj.cn] }}
<v-icon v-on="on" class="hasmany-col-menu-icon pv">mdi-menu-down</v-icon>
</v-chip> -->
<span v-on="on"
class="caption xc-bt-chip primary--text">
<!--
<div v-else-if="columnObj.cn in hasMany" class="hasmany-col d-flex ">
{{ rowObj[columnObj._cn] }}
<v-icon v-on="on" class="hasmany-col-menu-icon pv">mdi-menu-down</v-icon>
</span>
</template>
<v-list dense>
<v-list-item dense v-if="haveHasManyrelation"><span class="grey--text caption text-center mt-n2">Has Many</span>
</v-list-item>
<template v-if="haveHasManyrelation">
<template v-for="(hm,idCol) in hasMany">
<template v-for="(rel,i) in hm">
<v-divider
:key="i + idCol + '_div'"></v-divider>
<v-list-item
class="py-1"
@click="addNewRelationTab(
rel,
table,
meta._tn,
rel.tn,
rel._tn,
rowObj[idCol],
'hm',
rowObj,
rowObj[primaryValueColumn]
)"
:key="i + idCol"
dense
>
<v-list-item-icon class="mx-1">
<v-icon class="has-many-icon mr-1" small :color="textColors[i % colors.length]">
mdi-source-fork
</v-icon>
</v-list-item-icon>
<!-- <v-chip small >-->
<!-- <v-list-item-title> -->
<span class="caption text-capitalize"> {{ rel._tn }}</span>
<!-- </v-list-item-title>-->
<!-- </v-chip>-->
</v-list-item>
<v-spacer></v-spacer>
<v-menu open-on-hover>
<template v-slot:activator="{on}">
<v-icon v-on="on" class=" hasmany-col-menu-icon">mdi-menu-down</v-icon>
</template>
<v-list dense>
<v-list-item v-for="(rel,i) in hasMany[columnObj.cn]"
@click="addNewRelationTab(
rel,
table,
meta._tn,
rel.tn,
rel._tn,
rowObj[columnObj._cn],
'hm',
rowObj,
rowObj[primaryValueColumn]
)"
:key="i"
>
<v-chip small :color="colors[i % colors.length]">
<span class="caption text-capitalize"> {{ rel._tn }}</span>
</v-chip>
</v-list-item>
</v-list>
</v-menu>
</div>
<span v-else-if="columnObj._cn in belongsTo"
@click="addNewRelationTab(
belongsTo[columnObj._cn],
table,
meta._tn,
belongsTo[columnObj._cn].rtn,
belongsTo[columnObj._cn]._rtn,
rowObj[columnObj._cn],
'bt',
rowObj,
rowObj[primaryValueColumn]
)"
class="belongsto-col">{{ rowObj[columnObj._cn] }}</span>
<template v-else-if="primaryValueColumn === columnObj._cn">
<v-menu open-on-hover offset-y bottom>
<template v-slot:activator="{on}">
&lt;!&ndash; <v-chip v-on="on"
small
class="caption xc-bt-chip"
outlined
color="success">
{{ rowObj[columnObj.cn] }}
<v-icon v-on="on" class="hasmany-col-menu-icon pv">mdi-menu-down</v-icon>
</v-chip> &ndash;&gt;
<span v-on="on"
class="caption xc-bt-chip primary&#45;&#45;text">
{{ rowObj[columnObj._cn] }}
<v-icon v-on="on" class="hasmany-col-menu-icon pv">mdi-menu-down</v-icon>
</span>
</template>
<v-list dense>
<v-list-item dense v-if="haveHasManyrelation"><span class="grey&#45;&#45;text caption text-center mt-n2">Has Many</span>
</v-list-item>
<template v-if="haveHasManyrelation">
<template v-for="(hm,idCol) in hasMany">
<template v-for="(rel,i) in hm">
<v-divider
:key="i + '_' + idCol + '_div'"></v-divider>
<v-list-item
class="py-1"
@click="addNewRelationTab(
rel,
table,
meta._tn,
rel.tn,
rel._tn,
rowObj[idCol],
'hm',
rowObj,
rowObj[primaryValueColumn]
)"
:key="i + '_' + idCol"
dense
>
<v-list-item-icon class="mx-1">
<v-icon class="has-many-icon mr-1" small :color="textColors[i % colors.length]">
mdi-source-fork
</v-icon>
</v-list-item-icon>
&lt;!&ndash; <v-chip small >&ndash;&gt;
&lt;!&ndash; <v-list-item-title> &ndash;&gt;
<span class="caption text-capitalize"> {{ rel._tn }}</span>
&lt;!&ndash; </v-list-item-title>&ndash;&gt;
&lt;!&ndash; </v-chip>&ndash;&gt;
</v-list-item>
</template>
</template>
</template>
<v-list-item v-else>
<span class="caption text-capitalize grey&#45;&#45;text font-weight-light"> No relation found</span>
</v-list-item>
</v-list>
</v-menu>
</template>
</template>
</template>
<v-list-item v-else>
<span class="caption text-capitalize grey--text font-weight-light"> No relation found</span>
</v-list-item>
</v-list>
</v-menu>
</template>
-->
<table-cell v-else
:class="{'primary--text' : primaryValueColumn === columnObj._cn}"
:selected="selected.col === col && selected.row === row"
:isLocked="isLocked"
@enableedit="makeSelected(col,row);makeEditable(col,row,columnObj.ai)"
@ -284,19 +309,28 @@
<script>
import HeaderCell from "@/components/project/spreadsheet/components/headerCell";
import EditableCell from "@/components/project/spreadsheet/components/editableCell";
import EditColumn from "@/components/project/spreadsheet/editColumn/editColumn";
import TableCell from "@/components/project/spreadsheet/editableCell/tableCell";
import EditColumn from "@/components/project/spreadsheet/components/editColumn";
import TableCell from "@/components/project/spreadsheet/components/cell";
import colors from "@/mixins/colors";
import columnStyling from "@/components/project/spreadsheet/helpers/columnStyling";
import HasManyCell from "@/components/project/spreadsheet/components/virtualCell/hasManyCell";
import BelongsToCell from "@/components/project/spreadsheet/components/virtualCell/belogsToCell";
import ManyToMany from "@/components/project/spreadsheet/components/virtualCell/manyToManyCell";
import VirtualCell from "@/components/project/spreadsheet/components/virtualCell";
import VirtualHeaderCell from "@/components/project/spreadsheet/components/virtualHeaderCell";
export default {
components: {TableCell, EditColumn, EditableCell, HeaderCell},
components: {
VirtualHeaderCell,
VirtualCell, ManyToMany, BelongsToCell, HasManyCell, TableCell, EditColumn, EditableCell, HeaderCell
},
mixins: [colors],
props: {
relationType: String,
availableColumns: [Object, Array],
showFields: Object,
sqlUi: [Object, Function],
api: [Object, Function],
isEditable: Boolean,
nodes: Object,
primaryValueColumn: String,
@ -315,12 +349,29 @@ export default {
this.calculateColumnWidth();
},
methods: {
isRequired(_columnObj, rowObj) {
let columnObj = _columnObj;
if (columnObj.bt) {
columnObj = this.meta.columns.find(c => c.cn === columnObj.bt.cn);
}
return columnObj && (columnObj.rqd
&& (rowObj[columnObj._cn] === undefined || rowObj[columnObj._cn] === null)
&& !columnObj.default);
},
updateCol(row, column, value, columnObj, colIndex, rowIndex) {
this.$set(row, column, value);
this.onCellValueChange(colIndex, rowIndex, columnObj)
},
calculateColumnWidth() {
setTimeout(() => {
const obj = {};
this.meta && this.meta.columns && this.meta.columns.forEach(c => {
obj[c._cn] = columnStyling[c.uidt] && columnStyling[c.uidt].w || undefined;
})
this.meta && this.meta.v && this.meta.v.forEach(v => {
obj[v._cn] = v.bt ? '100px' : '200px';
})
Array.from(this.$el.querySelectorAll('th')).forEach(el => {
const width = el.getBoundingClientRect().width;
obj[el.dataset.col] = obj[el.dataset.col] || ((width < 100 ? 100 : width) + 'px');
@ -377,8 +428,26 @@ export default {
case 13:
this.makeEditable(this.selected.col, this.selected.row)
break;
default: {
if (this.editEnabled.col != null && this.editEnabled.row != null) {
return;
}
if (e.key && e.key.length === 1) {
this.$set(this.data[this.selected.row].row, this.availableColumns[this.selected.col]._cn, e.key)
this.editEnabled = {...this.selected}
}
}
}
},
onClickOutside() {
if (
this.meta.columns
&& this.meta.columns[this.selected.col]
&& this.meta.columns[this.selected.col].virtual
) return
this.selected.col = null;
this.selected.row = null
},
onNewColCreation() {
this.addNewColMenu = false;
this.addNewColModal = false;
@ -390,8 +459,8 @@ export default {
showRowContextMenu($event, rowObj, rowMeta, row) {
this.$emit('showRowContextMenu', $event, rowObj, rowMeta, row)
},
onCellValueChange(col, row, column) {
this.$emit('onCellValueChange', col, row, column)
onCellValueChange(col, row, column, ev) {
this.$emit('onCellValueChange', col, row, column, ev);
},
addNewRelationTab(...args) {
this.$emit('addNewRelationTab', ...args)
@ -428,6 +497,10 @@ export default {
},
onresize(col, size) {
this.$emit('update:columnsWidth', {...this.columnsWidth, [col]: size});
},
onXcResizing(_cn, width) {
this.resizingCol = _cn;
this.resizingColWidth = width;
}
},
computed: {
@ -455,7 +528,7 @@ export default {
style() {
let style = '';
for (const [key, val] of Object.entries(this.columnsWidth || {})) {
if (val && key !== this.resizingCol)
if (val && key !== this.resizingCol) {
style += `
[data-col="${key}"]{
min-width: ${val};
@ -463,13 +536,23 @@ export default {
width: ${val};
}
`;
} else if (key === this.resizingCol) {
style += `
[data-col="${key}"]{
min-width: ${this.resizingColWidth};
max-width: ${this.resizingColWidth};
width: ${this.resizingColWidth};
}
`;
}
}
return style;
}
},
},
data: () => ({
resizingCol: null,
resizingColWidth: null,
selectedExpandRowIndex: null,
selectedExpandRowMeta: null,
addNewColMenu: false,
@ -722,6 +805,10 @@ tbody tr:hover {
.cell {
font-size: 13px;
&.required {
box-shadow: inset 0 0 0 1px red;
}
}
th::before {
@ -799,8 +886,8 @@ th:first-child, td:first-child {
transform: rotate(90deg);
}
th{
min-width:100px;
th {
min-width: 100px;
}

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

@ -468,7 +468,7 @@ export default {
}
},
api() {
return ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.table, this.meta && this.meta.columns, this);
return ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.table, this.meta && this.meta.columns, this, this.meta);
},
colLength() {
return (this.meta && this.meta.columns && this.meta.columns.length) || 0

82
packages/nc-gui/components/project/table.vue

@ -9,6 +9,7 @@
v-model="active"
:height="relationTabs && relationTabs.length ?38:0"
class="table-tabs"
:class="{'hidden-tab':!relationTabs || !relationTabs.length}"
@change="onTabChange"
color="pink"
>
@ -17,7 +18,7 @@
<v-icon small>mdi-table-eye</v-icon>&nbsp;
<span class="caption text-capitalize font-weight-bold"> Model</span></v-tab>
<v-tab-item
style="height:100%">
style="height:100%">
<v-tabs
color="pink"
height="38"
@ -32,7 +33,7 @@
<v-tab-item
style="height:100%">
style="height:100%">
<columnList
ref="tabs0"
@ -68,7 +69,7 @@
<span class="caption font-weight-bold text-capitalize"> Triggers</span>
</v-tab>
<v-tab-item
style="height:100%">
style="height:100%">
<triggerList
ref="tabs2"
v-if="loadTriggerList"
@ -96,22 +97,22 @@
<!-- </v-tab-item>-->
<!-- </v-tabs>-->
<!-- </v-tabs>-->
<!-- </v-tab-item>
</template>
<template v-if="_isUIAllowed('api')">
<v-tab class="">
<v-icon small>mdi-code-braces</v-icon>&nbsp;
<span class="caption text-capitalize font-weight-bold"> APIs</span></v-tab>
<v-tab-item>-->
<!-- <v-tabs-->
<!-- height="38"-->
<!-- class="table-tabs"-->
<!-- ma-0-->
<!-- pa-0-->
<!-- style="height:100%">-->
<!-- </v-tab-item>
</template>
<template v-if="_isUIAllowed('api')">
<v-tab class="">
<v-icon small>mdi-code-braces</v-icon>&nbsp;
<span class="caption text-capitalize font-weight-bold"> APIs</span></v-tab>
<v-tab-item>-->
<!-- <v-tabs-->
<!-- height="38"-->
<!-- class="table-tabs"-->
<!-- ma-0-->
<!-- pa-0-->
<!-- style="height:100%">-->
<template v-if="!isMvc && !isMetaTable">
@ -310,22 +311,22 @@
</template>
<template v-if="_isUIAllowed('airTable')">
<v-tab v-show="relationTabs && relationTabs.length" class="" >
<v-tab v-show="relationTabs && relationTabs.length" class="">
<v-icon small>mdi-table-edit</v-icon>&nbsp;<span
class="caption text-capitalize font-weight-bold"> {{nodes._tn}}</span></v-tab>
class="caption text-capitalize font-weight-bold"> {{ nodes._tn }}</span></v-tab>
<v-tab-item
style="height:100%">
<rows-xc-data-table
ref="tabs7"
:show-tabs="relationTabs && relationTabs.length"
:table="nodes.tn"
:nodes="nodes"
:newTable="newTableCopy"
:mtdNewTableUpdate="mtdNewTableUpdate"
:deleteTable="deleteTable"
:is-meta-table="isMetaTable"
:addNewRelationTab="addNewRelationTab"
/>
style="height:100%">
<rows-xc-data-table
ref="tabs7"
:show-tabs="relationTabs && relationTabs.length"
:table="nodes.tn"
:nodes="nodes"
:newTable="newTableCopy"
:mtdNewTableUpdate="mtdNewTableUpdate"
:deleteTable="deleteTable"
:is-meta-table="isMetaTable"
:addNewRelationTab="addNewRelationTab"
/>
</v-tab-item>
</template>
<!-- Closable tabs : START -->
@ -344,11 +345,11 @@
<v-tooltip bottom nudge-bottom="">
<template v-slot:activator="{on}">
<div v-on="on">
<!-- <span class="rel-row-parent"> {{ refTable }} - {{ primaryValue }} </span>-->
<!-- <span class="rel-row-parent"> {{ refTable }} - {{ primaryValue }} </span>-->
<v-icon small>mdi-table-arrow-{{ relationType === 'hm' ? 'right' : 'left' }}</v-icon>&nbsp;
<span
class="caption font-weight-bold text-capitalize">
{{ refTableAlias }} ({{ ((primaryValue || '') + '').slice(0,13) }}) ->
{{ refTableAlias }} ({{ ((primaryValue || '') + '').slice(0, 13) }}) ->
{{ tableAlias }}
</span>
<v-icon icon @click="removeRelationTab(i)" x-small class="ml-2">mdi-close</v-icon>
@ -469,8 +470,18 @@ export default {
};
},
methods: {
addNewRelationTab(relation, refTable,refTableAlias, table,tableAlias, relationIdValue, relationType, relationRow, primaryValue) {
this.relationTabs.push({relation, refTable, table, relationIdValue, relationType, relationRow, primaryValue,refTableAlias,tableAlias});
addNewRelationTab(relation, refTable, refTableAlias, table, tableAlias, relationIdValue, relationType, relationRow, primaryValue) {
this.relationTabs.push({
relation,
refTable,
table,
relationIdValue,
relationType,
relationRow,
primaryValue,
refTableAlias,
tableAlias
});
this.active = 'relRow' + (this.relationTabs.length - 1);
},
removeRelationTab(i) {
@ -641,6 +652,7 @@ export default {
.table-tabs, /deep/ .table-tabs > .v-windows {
height: 100%;
}
/deep/ .v-window-item {
height: 100%
}

26
packages/nc-gui/components/project/tableTabs/columns.vue

@ -122,7 +122,7 @@
v-ge="['columns','save-and-scaffold']">
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Save & Scaffold Module
</v-list-item-title>
@ -132,7 +132,7 @@
v-ge="['columns','save-and-scaffold']"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Save & Scaffold GQL Model
</v-list-item-title>
@ -144,7 +144,7 @@
v-ge="['columns','scaffold']"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Scaffold GQL Model
</v-list-item-title>
@ -154,7 +154,7 @@
v-ge="['columns','scaffold']"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Scaffold GQL Resolver
</v-list-item-title>
@ -164,7 +164,7 @@
v-ge="['columns','scaffold']"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Scaffold GQL Service
</v-list-item-title>
@ -174,7 +174,7 @@
v-ge="['columns','scaffold']"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Scaffold GQL Relations
</v-list-item-title>
@ -191,7 +191,7 @@
v-ge="['columns','save-and-scaffold']"
@click="saveAndScaffold()">
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Save & Scaffold Module
</v-list-item-title>
@ -201,7 +201,7 @@
v-ge="['columns','save-and-scaffold']"
@click="saveAndScaffoldModel()">
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Save & Scaffold Model
</v-list-item-title>
@ -211,7 +211,7 @@
v-ge="['columns','json-to-column']"
@click="scaffold({model:true})">
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Scaffold Model
</v-list-item-title>
@ -221,7 +221,7 @@
@click="scaffold({router:true})"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Scaffold Router
</v-list-item-title>
@ -231,7 +231,7 @@
@click="scaffold({service:true})"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Scaffold Service
</v-list-item-title>
@ -241,7 +241,7 @@
@click="scaffold({relations:true})"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;Scaffold Relations
</v-list-item-title>
@ -255,7 +255,7 @@
@click="showJsonToColumDlg = true"
>
<v-list-item-title class="font-weight-bold pa-1" v-ge="['columns','json-to-columns']">
<v-icon color="primary" small>mdi-json</v-icon>
<v-icon color="primary" small>mdi-code-json</v-icon>
&nbsp;
JSON To Columns
</v-list-item-title>

2
packages/nc-gui/components/project/tableTabs/customAcl.vue

@ -1,6 +1,6 @@
<template>
<v-skeleton-loader v-if="loading" type="text@3"></v-skeleton-loader>
<div class="caption" v-else>{{ Array.isArray(value) ? '[' : '{' }}
<div class="caption text-left" v-else>{{ Array.isArray(value) ? '[' : '{' }}
<ul>
<template v-if="Array.isArray(value)">

2
packages/nc-gui/components/project/viewTabs/viewSpreadsheet.vue

@ -476,7 +476,7 @@ export default {
return SqlUI.create(this.nodes.dbConnection);
},
api() {
return ApiFactory.create(this.$store.getters['project/GtrProjectType'], (this.meta && this.meta._tn) || this.table, this.meta && this.meta.columns, this);
return ApiFactory.create(this.$store.getters['project/GtrProjectType'], (this.meta && this.meta._tn) || this.table, this.meta && this.meta.columns, this, this.meta);
}, edited() {
return this.data && this.data.some(r => r.rowMeta && (r.rowMeta.new || r.rowMeta.changed))
},

4
packages/nc-gui/components/project/xcInfo.vue

@ -187,7 +187,7 @@
<v-card-text class="pb-0 font-weight-bold" v-else-if="item.type==='graphql'">Graphql Endpoint
</v-card-text>
<v-card-text class="title white--text ">
<v-icon color="success" class=" mr-1">mdi-json</v-icon>
<v-icon color="success" class=" mr-1">mdi-code-json</v-icon>
<a :href="`${origin}${item.apiEndpoint}`" target="_blank">{{ `${origin}${item.apiEndpoint}` }}</a>
</v-card-text>
@ -224,7 +224,7 @@ export default {
info: null,
aggregatedInfo: null,
apiTypeIcon: {
'rest': {icon: 'mdi-json', iconColor: 'green'},
'rest': {icon: 'mdi-code-json', iconColor: 'green'},
'graphql': {icon: 'mdi-graphql', iconColor: 'pink'},
'grpc': {src: 'grpc-icon-color.png', type: 'img'},
},

2
packages/nc-gui/components/sponsorMini.vue

@ -1,5 +1,5 @@
<template>
<v-card max-width="300" min-height="" class=" pb-3" href="https://github.com/sponsors/xgenecloud"
<v-card max-width="300" min-height="" class=" pb-3" href="https://github.com/sponsors/nocodb"
target="_blank">

2
packages/nc-gui/components/sponsorOverlay.vue

@ -1,6 +1,6 @@
<template>
<v-overlay opacity=".98">
<v-card light width="450" min-height="500" class="" href="https://github.com/sponsors/xgenecloud"
<v-card light width="450" min-height="500" class="" href="https://github.com/sponsors/nocodb"
target="_blank">

2
packages/nc-gui/components/utils/dlgProjectCreate.vue

@ -76,7 +76,7 @@ export default {
loading: false,
projectType: 'rest',
projectTypes: [
{text: 'Automatic REST APIs on database', value: 'rest', icon: 'mdi-json', iconColor: 'green'},
{text: 'Automatic REST APIs on database', value: 'rest', icon: 'mdi-code-json', iconColor: 'green'},
{text: 'Automatic GRAPHQL APIs on database', value: 'graphql', icon: 'mdi-graphql', iconColor: 'pink'},
/* {
text: 'Automatic gRPC APIs on database',

6
packages/nc-gui/config/vuetify.options.js

@ -18,13 +18,17 @@ export default function ({app}) {
primary: '#0989ff',
'x-active': '#e91e63',
textColor: '#ffffff',
backgroundColor: '#363636',
text: '#ffffff',
textLight: '#b3b3b3',
backgroundColor: '#565656',
backgroundColorDefault: '#1f1f1f'
},
light: {
primary: '#0989ff',
'x-active': '#e91e63',
textColor: '#333333',
text: '#333333',
textLight: '#929292',
backgroundColor: '#f7f7f7',
backgroundColorDefault: '#ffffff',
}

1
packages/nc-gui/helpers/index.js

@ -1,5 +1,6 @@
export const isEmail = v => /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i.test(v)
export const parseIfInteger = v => /^\d+$/.test(v) ? +v : v;
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

12
packages/nc-gui/helpers/treeViewIcons.js

@ -73,17 +73,17 @@ export default {
},
apiClientDir: {
class: '',
icon: "mdi-json",
icon: "mdi-code-json",
color: "warning",
openColor: "",
openIcon: "mdi-json"
openIcon: "mdi-code-json"
},
apiClientSwaggerDir: {
class: '',
icon: "mdi-json",
icon: "mdi-code-json",
color: "warning",
openColor: "",
openIcon: "mdi-json"
openIcon: "mdi-code-json"
},
sqlClientDir: {
class: '',
@ -199,10 +199,10 @@ export default {
},
projectSettings: {
class: '',
icon: "mdi-settings",
icon: "mdi-cog",
color: "",
openColor: "",
openIcon: "mdi-settings"
openIcon: "mdi-cog"
},
cronJobs: {
class: '',

4
packages/nc-gui/helpers/treeViewIconsColors.js

@ -56,10 +56,10 @@ export default {
openIcon: "mdi-alpha-a-circle-outline"
},
apiClientDir: {
icon: "mdi-json",
icon: "mdi-code-json",
color: "",
openColor: "green",
openIcon: "mdi-json"
openIcon: "mdi-code-json"
},
sqlClientDir: {
icon: "mdi-alpha-s-circle-outline",

26
packages/nc-gui/layouts/default.vue

@ -34,6 +34,7 @@
</template>
</v-toolbar-title>
<span class="caption grey--text ml-3" v-show="$nuxt.$loading.show">Loading <v-icon small color="grey">mdi-spin mdi-loading</v-icon></span>
<!-- <gh-btns-star-->
<!-- @click.native="githubClickHandler"-->
@ -345,7 +346,7 @@
<!-- <v-icon tooltip="GraphQL Client (^⇧G)"-->
<!-- v-if="isRest"-->
<!-- @click="swaggerClientTabAdd()" class="ml-3 " size="20">-->
<!-- mdi-json-->
<!-- mdi-code-json-->
<!-- </v-icon>-->
<!-- &nbsp;-->
<!-- GraphiQL-->
@ -376,7 +377,7 @@
<v-icon class="ml-3 "
v-if="!$store.state.windows.nc" size="20" @click="apiClientSwaggerTabAdd()"
tooltip="API Client (^⇧A)">mdi-json
tooltip="API Client (^⇧A)">mdi-code-json
</v-icon>
<!-- &nbsp; REST Client-->
<!-- </x-btn>-->
@ -471,16 +472,16 @@
<template v-else>
<!-- <x-icon iconClass="mr-4" @click="apiClientSwaggerOpen()" v-if="!$store.state.windows.isComp"-->
<!-- tooltip="API Client (^⇧A)">mdi-json-->
<!-- tooltip="API Client (^⇧A)">mdi-code-json-->
<!-- </x-icon>-->
<!-- <x-icon iconClass="mr-4" tooltip="Feed (^⇧F)" @click="feedDialog = true">mdi-glasses</x-icon>-->
<!-- <span v-shortkey="['ctrl','shift','f']" @shortkey="feedDialog = true"></span>-->
<!-- <x-icon iconClass="mr-4" @click="settingsTabAdd" size="20" tooltip="Tool Settings (^⇧C)">mdi-settings-->
<!-- <x-icon iconClass="mr-4" @click="settingsTabAdd" size="20" tooltip="Tool Settings (^⇧C)">mdi-cog-->
<!-- </x-icon>-->
<!-- <x-icon iconClass="mr-4" @click="settingsDialog = true" tooltip="Tool Settings (^⇧C)">mdi-settings-->
<!-- <x-icon iconClass="mr-4" @click="settingsDialog = true" tooltip="Tool Settings (^⇧C)">mdi-cog-->
<!-- </x-icon>-->
<!-- <span v-shortkey="[ 'ctrl','shift', 'c']"-->
<!-- @shortkey="settingsDialog = true"></span> -->
@ -545,10 +546,10 @@
@shortkey="terminalTabAdd()" tooltip="Terminal"></span>
<!-- <x-icon key="settings-dash" iconClass="mr-1 ml-4" @click="settingsDialog = true" tooltip="Tool Settings (^⇧C)">-->
<!-- mdi-settings-->
<!-- mdi-cog-->
<!-- </x-icon>-->
<!-- <x-icon key="settings-dash" iconClass="mr-1 ml-4" size="20" @click="settingsTabAdd" tooltip="Tool Settings (^⇧C)">-->
<!-- mdi-settings-->
<!-- mdi-cog-->
<!-- </x-icon>-->
<notification class="mx-2"></notification>
@ -569,7 +570,7 @@
<v-list dense>
<v-list-item dense to="/user/settings" v-if="userAuthIsEmail">
<v-list-item-title v-ge="['Settings','']">
<v-icon small>mdi-settings</v-icon> &nbsp; <span class="font-weight-regular">Settings</span>
<v-icon small>mdi-cog</v-icon> &nbsp; <span class="font-weight-regular">Settings</span>
</v-list-item-title>
</v-list-item>
<v-divider v-if="userAuthIsEmail"></v-divider>
@ -624,11 +625,12 @@
</v-list-item-title>
</v-list-item>-->
<v-list-item v-if="swaggerOrGraphiqlUrl" dense @click.stop="openUrl(`${$axios.defaults.baseURL}${swaggerOrGraphiqlUrl}`)">
<v-list-item v-if="swaggerOrGraphiqlUrl" dense
@click.stop="openUrl(`${$axios.defaults.baseURL}${swaggerOrGraphiqlUrl}`)">
<v-list-item-title>
<v-icon small key="terminal-dash">
{{ isGql ? 'mdi-graphql' : 'mdi-json' }}
{{ isGql ? 'mdi-graphql' : 'mdi-code-json' }}
</v-icon>&nbsp;
<span class="font-weight-regular">
{{ isGql ? 'GraphQL APIs' : 'Swagger APIs Doc' }}</span>
@ -640,7 +642,7 @@
<v-list-item-title>
<v-icon small key="terminal-dash">
mdi-settings
mdi-cog
</v-icon>&nbsp;
<span class="font-weight-regular">Themes</span>
@ -746,7 +748,7 @@
<!-- to="/client"-->
<!-- >-->
<!-- <v-list-item-icon>-->
<!-- <v-icon>mdi-json</v-icon>-->
<!-- <v-icon>mdi-code-json</v-icon>-->
<!-- </v-list-item-icon>-->
<!-- <v-list-item-content>-->

7
packages/nc-gui/mixins/device.js

@ -11,6 +11,12 @@ export default {
}
},
computed: {
isDark() {
return this.$vuetify && this.$vuetify.theme && this.$vuetify.theme.dark;
},
isLight() {
return this.$vuetify && this.$vuetify.theme && this.$vuetify.theme.light;
},
language() {
// const dummy = new Date();
@ -33,6 +39,7 @@ export default {
&& this.$route.path
&& (this.$route.path === '/nc' || this.$route.path === '/nc/' || this.$route.path.startsWith('/nc/'));
},
_meta() {
return this._isMac ? '⌘' : '^';
},

5
packages/nc-gui/nuxt.config.js

@ -127,6 +127,9 @@ export default {
publicPath: process.env.NODE_ENV === 'production' ? `./_nuxt/` : undefined,
extend(config, {isDev, isClient}) {
if (isDev) {
config.devtool = isClient ? 'source-map' : 'inline-source-map'
}
config.externals = config.externals || {};
config.externals ['@microsoft/typescript-etw'] = 'FakeModule';
@ -194,7 +197,7 @@ export default {
},
loading: {
color: '#13f4ef',
height: '2px',
height: '0px',
continuous: true,
duration: 3000
},

2
packages/nc-gui/pages/project/id.vue

@ -590,7 +590,7 @@ export default {
{text: "Disabled", value: "none"},
],
projectTypes: [
{text: 'Automatic REST APIs on database', value: 'rest', icon: 'mdi-json', iconColor: 'green'},
{text: 'Automatic REST APIs on database', value: 'rest', icon: 'mdi-code-json', iconColor: 'green'},
{text: 'Automatic GRAPHQL APIs on database', value: 'graphql', icon: 'mdi-graphql', iconColor: 'pink'},
{text: 'Automatic gRPC APIs on database', value: 'grpc', icon: 'grpc-icon-color.png', type: 'img'},
// {

2
packages/nc-gui/pages/project/name.vue

@ -66,7 +66,7 @@ export default {
loading: false,
projectType: 'rest',
projectTypes: [
{text: 'Automatic REST APIs on database', value: 'rest', icon: 'mdi-json', iconColor: 'green'},
{text: 'Automatic REST APIs on database', value: 'rest', icon: 'mdi-code-json', iconColor: 'green'},
{text: 'Automatic GRAPHQL APIs on database', value: 'graphql', icon: 'mdi-graphql', iconColor: 'pink'},
{
text: 'Automatic gRPC APIs on database',

2
packages/nc-gui/pages/project/xcdb.vue

@ -112,7 +112,7 @@ export default {
loading: false,
projectType: 'rest',
projectTypes: [
{text: 'REST APIs', value: 'rest', icon: 'mdi-json', iconColor: 'green'},
{text: 'REST APIs', value: 'rest', icon: 'mdi-code-json', iconColor: 'green'},
{text: 'GRAPHQL APIs', value: 'graphql', icon: 'mdi-graphql', iconColor: 'pink'},
/* {
text: 'Automatic gRPC APIs on database',

2
packages/nc-gui/pages/projects/index.vue

@ -241,7 +241,7 @@
{{
props.item.projectType === 'graphql'
? 'mdi-graphql'
: 'mdi-json'
: 'mdi-code-json'
}}
</x-icon>

4
packages/nc-gui/plugins/globalMixin.js

@ -49,6 +49,8 @@ export default async ({store}) => {
el.appendChild(resizer);
resizer.addEventListener('mousedown', initDrag, false);
el.style.transition = '20ms width0';
let startX, startY, startWidth;
function initDrag(e) {
@ -75,7 +77,7 @@ export default async ({store}) => {
function doDrag(e) {
width = (startWidth + e.clientX - startX) + 'px';
el.style.maxWidth = el.style.minWidth = el.style.width = width;
emit(vnode, 'xcresizing');
emit(vnode, 'xcresizing', width);
//
// p.style.height = (startHeight + e.clientY - startY) + 'px';
}

73
packages/nc-gui/store/meta.js

@ -0,0 +1,73 @@
export const state = () => ({
metas: {},
loading: {}
});
export const mutations = {
MutMeta(state, {key, value}) {
state.metas = {...state.metas, [key]: value};
},
MutLoading(state, {key, value}) {
state.loading = {...state.loading, [key]: value};
},
MutClear(state) {
state.metas = {};
}
};
export const actions = {
async ActLoadMeta({state, commit, dispatch}, {tn, env, dbAlias, force}) {
if (!force && state.loading[tn]) {
return await new Promise(resolve => {
const unsubscribe = this.app.store.subscribe(s => {
if (s.type === 'meta/MutLoading' && s.payload.key === tn && !s.payload.value) {
unsubscribe();
resolve(state.metas[tn])
}
})
})
}
if (!force && state.metas[tn]) {
return state.metas[tn];
}
commit('MutLoading', {
key: tn,
value: true
})
const model = await dispatch('sqlMgr/ActSqlOp', [{env, dbAlias}, 'tableXcModelGet', {tn}], {root: true});
const meta = JSON.parse(model.meta);
commit('MutMeta', {
key: tn,
value: meta
})
commit('MutLoading', {
key: tn,
value: undefined
})
return force ? model : meta;
}
}
/**
* @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/>.
*
*/

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

@ -266,6 +266,7 @@ export const actions = {
}
const data = await this.dispatch('sqlMgr/ActSqlOp', [null, 'PROJECT_READ_BY_WEB']); // unsearialized data
commit("list", data.data.list);
commit("meta/MutClear", null, {root: true});
} catch (e) {
this.$toast.error(e).goAway(3000);
this.$router.push('/projects')

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

@ -253,6 +253,7 @@ function translateUiToLibCall(args, op, opArgs) {
break;
case 'relationCreate':
case 'xcM2MRelationCreate':
case 'xcVirtualRelationCreate':
data.type = "Relation create";
data.title = '';
@ -262,6 +263,7 @@ function translateUiToLibCall(args, op, opArgs) {
case 'relationDelete':
case 'xcVirtualRelationDelete':
case 'xcRelationColumnDelete':
data.type = "Relation delete";
data.title = '';
data.module = "";
@ -374,7 +376,7 @@ export const actions = {
if (cusHeaders) {
Object.assign(headers, cusHeaders)
}
return (await this.$axios({
const data = (await this.$axios({
url: '?q=sqlOp_' + op,
baseURL: `${this.$axios.defaults.baseURL}/dashboard`,
data: {api: op, ...args, ...params, args: opArgs},
@ -384,6 +386,37 @@ export const actions = {
...(cusAxiosOptions || {}),
})).data;
/* // clear meta cache on relation create/delete
// todo: clear only necessary metas
// todo: include missing operations
if ([
'xcModelSet',
'relationCreate',
'xcM2MRelationCreate',
'xcVirtualRelationCreate',
'relationDelete',
'xcVirtualRelationDelete',
'xcRelationColumnDelete'
].includes(op)) {
commit('meta/MutClear', null, {root: true})
}
if (op === 'tableXcModelGet') {
try {
const meta = JSON.parse(data.meta);
commit('meta/MutMeta', {
key: meta.tn,
value: meta
}, {root: true})
} catch (e) {
// ignore
}
}*/
return data;
} catch (e) {
const err = new Error(e.response.data.msg);
err.response = e.response;

6
packages/nc-gui/store/windows.js

@ -45,10 +45,14 @@ export const state = () => ({
nc: true,
miniSponsorCard: 0,
screensaver: true,
autoApplyFilter: true
autoApplyFilter: true,
apiLoading: false
});
export const mutations = {
MutApiLoading(state, status) {
state.apiLoading = status
},
MutAutoApplyFilter(state, v) {
state.autoApplyFilter = v;
},

1
packages/nocodb/package.json

@ -122,6 +122,7 @@
"glob": "^7.1.6",
"graphql": "^15.3.0",
"graphql-depth-limit": "^1.1.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.6",
"import-fresh": "^3.2.1",
"inflection": "^1.12.0",

2
packages/nocodb/src/example/docker.ts

@ -1,5 +1,5 @@
import Noco from "../lib/noco/Noco";
process.env.NC_VERSION = '0009044';
import express from 'express';
import cors from 'cors';

1
packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts

@ -60,6 +60,7 @@ abstract class BaseModel {
protected pks: any[];
protected hasManyRelations: any;
protected belongsToRelations: any;
protected manyToManyRelations: any;
protected config: any;
protected clientType: string;
public readonly type: string;

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

@ -39,6 +39,7 @@ class BaseModelSql extends BaseModel {
columns,
hasMany = [],
belongsTo = [],
manyToMany = [],
type,
dbModels
}: {
@ -63,6 +64,7 @@ class BaseModelSql extends BaseModel {
this.pks = columns.filter(c => c.pk === true);
this.hasManyRelations = hasMany;
this.belongsToRelations = belongsTo;
this.manyToManyRelations = manyToMany;
this.config = {
limitDefault: process.env.DB_QUERY_LIMIT_DEFAULT || 10,
limitMax: process.env.DB_QUERY_LIMIT_MAX || 500,
@ -1105,15 +1107,16 @@ class BaseModelSql extends BaseModel {
* @private
*/
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.dbModels[child].columnToAlias?.[cn];
if (fields !== '*' && fields.split(',').indexOf(cn) === -1) {
fields += ',' + cn;
}
let childs = await this._run(this.dbDriver.union(
const childs = await this._run(this.dbDriver.union(
parent.map(p => {
const query =
this
@ -1122,16 +1125,68 @@ class BaseModelSql extends BaseModel {
.xwhere(where, this.dbModels[child].selectQuery(''))
.select(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);
const gs = _.groupBy(childs, _cn);
parent.forEach(row => {
row[this.dbModels?.[child]?._tn || child] = gs[row[this.pks[0].cn]] || [];
row[`${this.dbModels?.[child]?._tn || child}List`] = gs[row[this.pks[0]._cn]] || [];
})
}
/**
* 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) {
const gs = await this._getGroupedManyToManyList({
rest,
index,
child,
parentIds: parent.map(p => p[this.columnToAlias?.[this.pks[0].cn] || this.pks[0].cn])
});
parent.forEach((row, i) => {
row[`${this.dbModels?.[child]?._tn || child}MMList`] = gs[i] || [];
})
}
public async _getGroupedManyToManyList({rest = {}, index = 0, child, parentIds}) {
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) || {};
// @ts-ignore
// const _cn = this.dbModels[tn].columnToAlias?.[cn];
if (fields !== '*' && fields.split(',').indexOf(cn) === -1) {
fields += ',' + cn;
}
const childs = await this._run(this.dbDriver.union(
parentIds.map(id => {
const query =
this
.dbDriver(child)
.join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`)
.where(`${vtn}.${vcn}`, id) // p[this.columnToAlias?.[this.pks[0].cn] || this.pks[0].cn])
.xwhere(where, this.dbModels[child].selectQuery(''))
.select({[`${tn}_${vcn}`]: `${vtn}.${vcn}`, ...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()
));
const gs = _.groupBy(childs, `${tn}_${vcn}`);
return parentIds.map(id => gs[id] || [])
}
/**
@ -1190,7 +1245,7 @@ class BaseModelSql extends BaseModel {
*/
// todo : add conditionGraph
async hasManyList({childs, where, fields, f, ...rest}) {
async hasManyList({childs = '', where, fields, f, ...rest}) {
fields = fields || f || '*';
try {
@ -1200,7 +1255,7 @@ class BaseModelSql extends BaseModel {
const parent = await this.list({childs, where, fields, ...rest});
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,
child
}, rest, index)));
@ -1212,6 +1267,132 @@ class BaseModelSql extends BaseModel {
}
}
/**
* Gets parent list along with children list and parent
*
* @param {Object} args
* @param {String} args.childs - comma separated child table naes
* @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
// todo : implement nestedread
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;
}
}
// 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
*
@ -1267,7 +1448,7 @@ class BaseModelSql extends BaseModel {
* @private
*/
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) {
fields += ',' + rcn;
}
@ -1282,7 +1463,7 @@ class BaseModelSql extends BaseModel {
const gs = _.groupBy(parents, this.dbModels[parent]?.columnToAlias?.[rcn] || rcn);
childs.forEach(row => {
row[this.dbModels?.[parent]?._tn || parent] = gs[row[this.dbModels[parent]?.columnToAlias?.[cn] || cn]]?.[0];
row[`${this.dbModels?.[parent]?._tn || parent}Read`] = gs[row[this?.columnToAlias?.[cn] || cn]]?.[0];
})
}
@ -1323,7 +1504,7 @@ class BaseModelSql extends BaseModel {
this._paginateAndSort(query, {limit, offset}, child);
return this.isSqlite() ? this.dbDriver.select().from(query) : query;
}), !this.isSqlite()
), {sort} as any, child));
), {sort,limit:1000} as any, child));
// return _.groupBy(childs, cn);
@ -1463,18 +1644,24 @@ class BaseModelSql extends BaseModel {
* @returns {Object} consisting of fields*,where*,limit*,offset*,sort*
* @private
*/
_getChildListArgs(args: any, index?: number) {
_getChildListArgs(args: any, index?: number, child?: string) {
index++;
let obj: XcFilter = {};
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.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}`];
return obj;
}
// @ts-ignore
private getPKandPV(child: string) {
return child ?
(this.dbModels[child]?.columns?.filter(col => col.pk || col.pv).map(col => col.cn) || ['*']).join(',')
: '*';
}
// @ts-ignore
public selectQuery(fields) {
const fieldsArr = fields.split(',');
return this.columns?.reduce((selectObj, col) => {
@ -1482,8 +1669,8 @@ class BaseModelSql extends BaseModel {
!fields
|| fieldsArr.includes('*')
|| fieldsArr.includes(`${this.tn}.*`)
|| fields.includes(col._cn)
|| fields.includes(col.cn)
|| fieldsArr.includes(col._cn)
|| fieldsArr.includes(col.cn)
) {
selectObj[col._cn] = `${this.tn}.${col.cn}`;
}

5
packages/nocodb/src/lib/migrator/SqlMigrator/lib/KnexMigrator.ts

@ -484,7 +484,7 @@ export default class KnexMigrator extends SqlMigrator {
filesDown = files = await this.metaDb('nc_migrations').where({
project_id: this.project_id,
db_alias: args.dbAlias
}).orderBy('title', 'asc')
}).orderBy('id', 'asc')
} else {
files = await promisify(glob)(args.upFilesPattern);
filesDown = await promisify(glob)(args.downFilesPattern);
@ -1065,7 +1065,7 @@ export default class KnexMigrator extends SqlMigrator {
* and only filenames are migrated to _evolution table
* @memberof KnexMigrator
*/
async migrationsUp(args: any = {}) {
async migrationsUp(args: any = {}) {
const func = this.migrationsUp.name;
// const result = new Result();
@ -1077,7 +1077,6 @@ export default class KnexMigrator extends SqlMigrator {
// if (NcConfigFactory.hasDbUrl()) {
// this.project = NcConfigFactory.make();
// }
// console.log(this.project);
return await this._migrationsUp({

2
packages/nocodb/src/lib/migrator/util/file.help.ts

@ -3,7 +3,7 @@ import dayjs from 'dayjs';
const getUniqFilenamePrefix = function () {
return dayjs().format('YYYYMMDD_HHmmss')
return dayjs().format('YYYYMMDD_HHmmssSSS')
};

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

@ -60,10 +60,10 @@ export default class NcProjectBuilder {
let routeInfo;
if (meta instanceof RestApiBuilder) {
console.log(`Creating REST APIs ${meta.getDbType()} - > ${meta.getDbName()}`);
routeInfo = await (meta as RestApiBuilder).loadRoutes(null);
routeInfo = await (meta as RestApiBuilder).init();
} else if (meta instanceof GqlApiBuilder) {
console.log(`Creating GraphQL APIs ${meta.getDbType()} - > ${meta.getDbName()}`);
routeInfo = await (meta as GqlApiBuilder).loadResolvers(null);
routeInfo = await (meta as GqlApiBuilder).init();
}
allRoutesInfo.push(routeInfo);
this.progress(routeInfo, allRoutesInfo, isFirstTime);
@ -91,6 +91,9 @@ export default class NcProjectBuilder {
type: 'AUTH_MIDDLEWARE'
});
break;
case 'xcM2MRelationCreate':
await curBuilder.onManyToManyRelationCreate(data.req.args.parentTable, data.req.args.childTable, data.req.args);
break;
case 'relationCreate':
await curBuilder.onRelationCreate(data.req.args.parentTable, data.req.args.childTable, data.req.args);
@ -126,13 +129,19 @@ export default class NcProjectBuilder {
});
console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
break;
case 'xcVirtualRelationDelete':
await curBuilder.onRelationDelete(data.req.args.parentTable, data.req.args.childTable, {
...data.req.args,
virtual: true
});
console.log(`Deleted relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
break;
case 'xcRelationColumnDelete':
if (data.req.args?.type === 'mm') {
await curBuilder.onManyToManyRelationDelete(data.req.args.parentTable, data.req.args.childTable)
}
break;
@ -216,6 +225,10 @@ export default class NcProjectBuilder {
await curBuilder.onValidationUpdate(data.req.args.tn);
console.log(`Updated validations for table : ${data.req.args.tn}`)
break;
case 'xcUpdateVirtualKeyAlias':
await curBuilder.onVirtualColumnAliasUpdate(data.req.args.tn);
console.log(`Updated validations for table : ${data.req.args.tn}`)
break;
case 'xcModelSchemaSet':
await curBuilder.onGqlSchemaUpdate(data.req.args.tn, data.req.args.schema);

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

@ -94,6 +94,10 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
return this.models;
}
public get client() {
return this.connectionConfig?.client;
}
public readonly app: T;
public hooks: {
@ -228,6 +232,16 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
dr: onDelete,
ur: onUpdate,
})
} else {
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_relations', {
_tn: this.getTableNameAlias(tnc),
_rtn: this.getTableNameAlias(tnp),
}, {
tn: tnc,
cn: childColumn,
rtn: tnp,
rcn: parentColumn,
})
}
}
@ -308,7 +322,8 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
this.models[modelRow.title] = this.getBaseModel(metaObj);
// todo: check tableAlias changed or not
await this.onTableRename(tn, tn)
// todo:
// await this.onTableRename(tn, tn)
}
@ -322,7 +337,14 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
const oldModelRow = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {
title: tn
});
})
let queryParams;
try {
queryParams = JSON.parse(oldModelRow.query_params);
} catch (e) {
}
if (!oldModelRow) {
return;
@ -359,7 +381,7 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
this.baseLog(`onTableUpdate : Generating new model meta for '%s' table`, tn)
/* create models from table */
const newMeta = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getObject();
const newMeta: any = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getObject();
/* get ACL row */
@ -367,6 +389,11 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
const acl = JSON.parse(aclRow.acl);
const oldMeta = JSON.parse(oldModelRow.meta);
// copy virtual columns and many to many relations from old meta to new
newMeta.v = oldMeta.v;
newMeta.manyToMany = oldMeta.manyToMany;
const aclOper = [];
this.baseLog(`onTableUpdate : Comparing and updating new metadata of '%s' table`, tn)
@ -519,6 +546,12 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
aclObj.columns[column.cn] = true;
}
}
if (queryParams?.showFields) {
queryParams.showFields[column.cno] = true;
}
} else {
oldCol = oldMeta.columns.find(c => c.cn === column.cn);
newCol = newMeta.columns.find(c => c.cn === column.cn);
@ -535,7 +568,8 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
}
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
meta: JSON.stringify(newMeta)
meta: JSON.stringify(newMeta),
...(queryParams ? {query_params: JSON.stringify(queryParams)} : {})
}, {
title: tn
});
@ -644,44 +678,59 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
const NC_VERSIONS = [
// {name: '0.6', handler: null},
// {name: '0.7', handler: this.xcUpZeroPointSeven},
// {name: '0.8', handler: this.xcUpZeroPointEight},
// {name: '0.9', handler: this.xcUpZeroPointNine},
{name: '0009000', handler: null},
{name: '0009044', handler: this.ncUpManyToMany.bind(this)}
]
const knex = this.getDbDriver();
if (!await knex.schema.hasTable('nc_store')) {
if (!await this.xcMeta?.knex?.schema?.hasTable?.('nc_store')) {
return;
}
this.baseLog(`xcUpgrade : Getting configuration from meta database`,)
const config = await knex('nc_store').where({key: 'NC_CONFIG'}).first();
const config = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_store', {key: 'NC_CONFIG'});
if (config) {
const configObj: NcConfig = JSON.parse(config.value);
if (configObj.version !== process.env.NC_VERSION) {
let start = false;
for (const version of NC_VERSIONS) {
if (start) {
await version.handler()
} else if (version.name === configObj.version) {
start = true;
// todo: take backup of current version
// compare current version and old version
if (version.name > configObj.version) {
this.baseLog(`xcUpgrade : Upgrading '%s' => '%s'`, configObj.version, version.name)
await version?.handler?.();
// update version in meta after each upgrade
config.version = version.name;
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_store', {
value: JSON.stringify(config)
}, {
key: 'NC_CONFIG',
});
// todo: backup data
}
if (version.name === process.env.NC_VERSION) {
break;
// todo:
}
}
config.version = process.env.NC_VERSION;
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_store', {
key: 'NC_CONFIG',
value: JSON.stringify(config)
});
}
} else {
this.baseLog(`xcUpgrade : Inserting config to meta database`,)
const configObj: NcConfig = JSON.parse(JSON.stringify(this.config));
delete configObj.envs;
const isOld = (await this.xcMeta.metaList(this.projectId, this.dbAlias, 'nc_models'))?.length;
configObj.version = isOld ? '0009000' : process.env.NC_VERSION;
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_store', {
key: 'NC_CONFIG',
value: JSON.stringify(configObj)
});
if (isOld) {
await this.xcUpgrade();
}
}
}
@ -737,6 +786,45 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
}
public async onManyToManyRelationCreate(parent: string, child: string, _args?: any) {
return this.getManyToManyRelations({parent, child})
}
public async onManyToManyRelationDelete(parent: string, child: string, _args?: any) {
const parentMeta = this.metas[parent];
const childMeta = this.metas[child];
parentMeta.manyToMany = parentMeta.manyToMany.filter(mm => !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent))
childMeta.manyToMany = childMeta.manyToMany.filter(mm => !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent))
parentMeta.v = parentMeta.v.filter(({mm}) => !mm || !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent))
childMeta.v = childMeta.v.filter(({mm}) => !mm || !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent))
for (const meta of [parentMeta, childMeta]) {
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
meta: JSON.stringify(meta)
}, {title: meta.tn})
XcCache.del([this.projectId, this.dbAlias, 'table', meta.tn].join('::'));
this.models[meta.tn] = this.getBaseModel(meta)
}
}
public getProjectId(): string {
return this.projectId;
}
public async init(): Promise<void> {
await this.xcUpgrade();
}
public async onVirtualColumnAliasUpdate(tableName: string): Promise<void> {
const model = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {title: tableName});
const meta = JSON.parse(model.meta);
this.models[tableName] = this.getBaseModel(meta);
}
protected async loadCommon(): Promise<any> {
this.baseLog(`loadCommon :`);
@ -747,8 +835,6 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
protected initDbDriver(): void {
if (!this.dbDriver) {
if (this.connectionConfig?.connection?.ssl && typeof this.connectionConfig?.connection?.ssl === 'object') {
if (this.connectionConfig.connection.ssl.caFilePath) {
this.connectionConfig.connection.ssl.ca = fs
@ -809,7 +895,7 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
protected async getColumnList(tn: string): Promise<any[]> {
this.baseLog(`getColumnList : '%s'`, tn);
let columns = await this.sqlClient.columnList({tn: tn});
let columns = await this.sqlClient.columnList({tn});
columns = columns.data.list;
return columns;
}
@ -845,20 +931,22 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
}
protected generateContextForTable(tn: string, columns: any[], relations, hasMany: any[], belongsTo: any[], type = 'table', table_name_alias?: string): any {
protected generateContextForTable(tn: string, columns: any[], relations, hasMany: any[], belongsTo: any[], type = 'table', tableNameAlias?: string): any {
this.baseLog(`generateContextForTable : '%s' %s`, tn, type);
for (const col of columns) {
col._cn = this.getColumnNameAlias(col);
col._cn = col._cn || this.getColumnNameAlias(col);
}
const tableNameAlias = table_name_alias || this.getTableNameAlias(tn);
// tslint:disable-next-line:variable-name
const _tn = tableNameAlias || this.getTableNameAlias(tn)
const ctx = {
dbType: this.connectionConfig.client,
tn,
_tn: tableNameAlias,
tn_camelize: inflection.camelize(tableNameAlias),
tn_camelize_low: inflection.camelize(tableNameAlias, true),
_tn,
tn_camelize: inflection.camelize(_tn),
tn_camelize_low: inflection.camelize(_tn, true),
columns,
relations,
hasMany,
@ -871,11 +959,6 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
return ctx;
}
private getColumnNameAlias(col, tableName?: string) {
return this.metas?.[tableName]?.columns?.find(c => c.cn === col.cn)?._cn || col._cn || this.getInflectedName(col.cn, this.connectionConfig?.meta?.inflection?.cn);
}
protected getTableNameAlias(tn: string) {
if (this.metas?.[tn]?._tn) {
return this.metas?.[tn]?._tn;
@ -890,7 +973,7 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
...ctx,
_tn: this.metas[ctx.tn]._tn,
_ctn: this.metas[tnc]._tn,
tnc: tnc,
tnc,
project_id: this.projectId
};
}
@ -946,6 +1029,7 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
protected getBaseModel(meta): BaseModelSql {
this.baseLog(`getBaseModel : '%s'`);
this.metas[meta.tn] = meta;
return new BaseModel({
dbDriver: this.dbDriver,
...meta,
@ -1118,6 +1202,160 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
}, {rtn: oldTableName});
}
protected async getManyToManyRelations({parent = null, child = null, localMetas = null} = {}) {
const metas = new Set<any>();
const assocMetas = new Set<any>();
if (localMetas) {
for (const meta of localMetas) {
this.metas[meta.tn] = meta;
}
}
for (const meta of Object.values(this.metas)) {
// check if table is a Bridge table(or Associative Table) by checking
// number of foreign keys and columns
if (meta.belongsTo?.length === 2 && meta.columns.length < 5) {
if (parent && child && !([parent, child].includes(meta.belongsTo[0].rtn) && [parent, child].includes(meta.belongsTo[1].rtn))) {
continue;
}
const tableMetaA = this.metas[meta.belongsTo[0].rtn];
const tableMetaB = this.metas[meta.belongsTo[1].rtn];
/* // remove hasmany relation with associate table from tables
tableMetaA.hasMany.splice(tableMetaA.hasMany.findIndex(hm => hm.tn === meta.tn), 1)
tableMetaB.hasMany.splice(tableMetaB.hasMany.findIndex(hm => hm.tn === meta.tn), 1)*/
// add manytomany data under metadata of both linked tables
tableMetaA.manyToMany = tableMetaA.manyToMany || [];
if (tableMetaA.manyToMany.every(mm => mm.vtn !== meta.vtn)) {
tableMetaA.manyToMany.push({
"tn": tableMetaA.tn,
"cn": meta.belongsTo[0].rcn,
"vtn": meta.tn,
"vcn": meta.belongsTo[0].cn,
"vrcn": meta.belongsTo[1].cn,
"rtn": meta.belongsTo[1].rtn,
"rcn": meta.belongsTo[1].rcn,
"_tn": tableMetaA._tn,
"_cn": meta.belongsTo[0]._rcn,
"_rtn": meta.belongsTo[1]._rtn,
"_rcn": meta.belongsTo[1]._rcn
})
metas.add(tableMetaA)
}
tableMetaB.manyToMany = tableMetaB.manyToMany || [];
if (tableMetaB.manyToMany.every(mm => mm.vtn !== meta.vtn)) {
tableMetaB.manyToMany.push({
"tn": tableMetaB.tn,
"cn": meta.belongsTo[1].rcn,
"vtn": meta.tn,
"vcn": meta.belongsTo[1].cn,
"vrcn": meta.belongsTo[0].cn,
"rtn": meta.belongsTo[0].rtn,
"rcn": meta.belongsTo[0].rcn,
"_tn": tableMetaB._tn,
"_cn": meta.belongsTo[1]._rcn,
"_rtn": meta.belongsTo[0]._rtn,
"_rcn": meta.belongsTo[0]._rcn
})
metas.add(tableMetaB)
}
assocMetas.add(meta)
}
}
// Update metadata of tables which have manytomany relation
// and recreate basemodel with new meta information
for (const meta of metas) {
let queryParams;
// update showfields on new many to many relation create
if (parent && child) {
try {
queryParams = JSON.parse((await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {title: meta.tn})).query_params)
} catch (e) {
// ignore
}
}
meta.v = [
...meta.v.filter(vc => !(vc.hm && meta.manyToMany.some(mm => vc.hm.tn === mm.vtn))),
// todo: ignore duplicate m2m relations
...meta.manyToMany.map(mm => {
if (queryParams?.showFields && !(`${mm._tn} <=> ${mm._rtn}` in queryParams.showFields)) {
queryParams.showFields[`${mm._tn} <=> ${mm._rtn}`] = true;
}
return {
mm,
_cn: `${mm._tn} <=> ${mm._rtn}`
}
})]
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
meta: JSON.stringify(meta),
...(queryParams ? {query_params: JSON.stringify(queryParams)} : {})
}, {title: meta.tn})
XcCache.del([this.projectId, this.dbAlias, 'table', meta.tn].join('::'));
if (!localMetas) {
this.models[meta.tn] = this.getBaseModel(meta)
}
}
// Update metadata of associative table
for (const meta of assocMetas) {
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
mm: 1,
}, {title: meta.tn})
if (!localMetas) {
XcCache.del([this.projectId, this.dbAlias, 'table', meta.tn].join('::'));
this.models[meta.tn] = this.getBaseModel(meta)
}
}
}
protected async ncUpManyToMany(): Promise<any> {
const models = await this.xcMeta.metaList(this.projectId, this.dbAlias, 'nc_models', {
fields: ['meta']
});
if (!models.length) {
return
}
const metas = [];
// add virtual columns for relations
for (const metaObj of models) {
const meta = JSON.parse(metaObj.meta);
metas.push(meta);
const ctx = this.generateContextForTable(meta.tn, meta.columns, [], meta.hasMany, meta.belongsTo, meta.type, meta._tn);
// generate virtual columns
meta.v = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getVitualColumns();
// set default primary values
ModelXcMetaFactory.create(this.connectionConfig, {}).mapDefaultPrimaryValue(meta.columns);
// update meta
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
meta: JSON.stringify(meta)
}, {title: meta.tn})
}
// generate many to many relations an columns
await this.getManyToManyRelations({localMetas: metas});
return metas;
}
private getColumnNameAlias(col, tableName?: string) {
return this.metas?.[tableName]?.columns?.find(c => c.cn === col.cn)?._cn || col._cn || this.getInflectedName(col.cn, this.connectionConfig?.meta?.inflection?.cn);
}
private baseLog(str, ...args): void {
log(`${this.dbAlias} : ${str}`, ...args);
}
@ -1499,14 +1737,6 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
}
await this.loadXcAcl()
}
public get client() {
return this.connectionConfig?.client;
}
public getProjectId() {
return this.projectId;
}
}
export {IGNORE_TABLES};

5
packages/nocodb/src/lib/noco/common/XcMigrationSource.ts

@ -1,4 +1,5 @@
import * as project from '../migrations/nc_001_init';
import * as m2m from '../migrations/nc_002_add_m2m';
// Create a custom migration source class
export default class XcMigrationSource{
@ -7,7 +8,7 @@ export default class XcMigrationSource{
// arguments to getMigrationName and getMigration
public getMigrations(): Promise<any> {
// In this example we are just returning migration names
return Promise.resolve(['project'])
return Promise.resolve(['project','m2m'])
}
public getMigrationName(migration): string {
@ -18,6 +19,8 @@ export default class XcMigrationSource{
switch (migration) {
case 'project':
return project;
case 'm2m':
return m2m;
}
}
}

558
packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts

@ -1,13 +1,4 @@
import GqlResolver from "./GqlResolver";
import inflection from 'inflection';
// import {
// ExpressXcTsPolicyGql,
// GqlXcSchemaFactory,
// ModelXcMetaFactory
// } from "nc-help";
import {BaseType} from 'xc-core-ts';
import {DbConfig, NcConfig} from "../../../interface/config";
import Noco from "../Noco";
@ -31,6 +22,9 @@ import GqlXcSchemaFactory from "../../sqlMgr/code/gql-schema/xc-ts/GqlXcSchemaFa
import ModelXcMetaFactory from "../../sqlMgr/code/models/xc/ModelXcMetaFactory";
import ExpressXcTsPolicyGql from "../../sqlMgr/code/gql-policies/xc-ts/ExpressXcTsPolicyGql";
import {GraphQLJSON} from 'graphql-type-json';
import {m2mNotChildren, m2mNotChildrenCount} from "./GqlCommonResolvers";
const log = debug('nc:api:gql');
@ -94,6 +88,12 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
this.xcMeta = xcMeta;
}
public async init(): Promise<void> {
await super.init();
await this.loadResolvers(null);
}
public async onToggleModelRelation(relationInModels: any): Promise<void> {
this.log(`onToggleModelRelation: Within ToggleModelRelation event handler`)
@ -147,7 +147,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
public async onTableCreate(tn: string, args): Promise<void> {
this.log(`onTableCreate : '%s' `, tn)
const columns = {
[tn]: args.columns.map(({altered: _al, ...rest}) => rest)
[tn]: args?.columns?.map(({altered: _al, ...rest}) => rest)
}
@ -176,6 +176,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
await this.reInitializeGraphqlEndpoint();
}
// todo: m2m
public async onTableRename(oldTableName: string, newTableName: string): Promise<void> {
this.log(`onTableRename : '%s' => '%s' `, oldTableName, newTableName)
@ -315,8 +316,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const colNameAlias = self.models[hm.rtn]?.columnToAlias[hm.rcn];
const middlewareBody = middlewaresArr.find(({title}) => title === hm.tn)?.functions?.[0];
const countPropName = `${inflection.camelize(hm._tn, false)}Count`;
const listPropName = `${inflection.camelize(hm._tn, false)}List`;
const countPropName = `${hm._tn}Count`;
const listPropName = `${hm._tn}List`;
if (listPropName in this.types[tn].prototype) {
continue;
@ -335,6 +336,39 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
this.addHmCountResolverMethodToType(hm, mw, tn, loaderFunctionsObj, countPropName, colNameAlias);
}
}
for (const mm of schema.manyToMany || []) {
if (!enabledModels.includes(mm.rtn)) {
continue;
}
// todo: handle enable/disable
// if (!mm.enabled) {
// continue;
// }
const middlewareBody = middlewaresArr.find(({title}) => title === mm.rtn)?.functions?.[0];
// const countPropName = `${mm._rtn}Count`;
const listPropName = `${mm._rtn}MMList`;
if (listPropName in this.types[tn].prototype) {
continue;
}
const mw = new GqlMiddleware(this.acls, mm.tn, middlewareBody, this.models);
/* has many relation list loader with middleware */
this.addMMListResolverMethodToType(tn, mm, mw, {}, listPropName, this.metas[mm.tn].columns.find(c => c.pk)._cn);
// todo: count
// if (countPropName in this.types[tn].prototype) {
// continue;
// }
// {
// const mw = new GqlMiddleware(this.acls, hm.tn, middlewareBody, this.models);
//
// // create count loader with middleware
// this.addHmCountResolverMethodToType(hm, mw, tn, loaderFunctionsObj, countPropName, colNameAlias);
// }
}
for (const bt of schema.belongsTo) {
@ -349,7 +383,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const colNameAlias = self.models[bt.tn]?.columnToAlias[bt.cn];
const rcolNameAlias = self.models[bt.rtn]?.columnToAlias[bt.rcn];
const middlewareBody = middlewaresArr.find(({title}) => title === bt.rtn)?.functions?.[0];
const propName = `${inflection.camelize(bt._rtn, false)}Read`;
const propName = `${bt._rtn}Read`;
if (propName in this.types[tn].prototype) {
continue;
}
@ -395,6 +429,35 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}
}
private addMMListResolverMethodToType(tn: string, mm, mw: GqlMiddleware, _loaderFunctionsObj, listPropName: string, colNameAlias) {
{
const self = this;
this.log(`xcTablesRead : Creating loader for '%s'`, `${tn}Mm${mm.rtn}List`)
const listLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async parentIds => {
return (await this.models[tn]._getGroupedManyToManyList({
parentIds,
child: mm.rtn,
rest: {
fields1: '*'
}
}))?.map(child => child.map(c => new self.types[mm.rtn](c)));
},
[mw.postLoaderMiddleware]
));
/* defining HasMany list method within GQL Type class */
Object.defineProperty(this.types[tn].prototype, listPropName, {
async value(args: any, context: any, info: any): Promise<any> {
return listLoader.load([this[colNameAlias], args, context, info]);
},
configurable: true
})
}
}
private addHmCountResolverMethodToType(hm, mw, tn: string, loaderFunctionsObj, countPropName: string, colNameAlias) {
{
this.log(`xcTablesRead : Creating loader for '%s'`, `${tn}Hm${hm.tn}Count`)
@ -454,7 +517,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}>;
type?: 'table' | 'view',
columns?: {
[tn: string]: any
[key: string]: any
}
}): Promise<any> {
this.log('xcTablesPopulate : names - %o , type - %s', args?.tableNames, args?.type)
@ -464,8 +527,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
// set table name alias
relations.forEach(r => {
r._rtn = this.getTableNameAlias(r.rtn);
r._tn = this.getTableNameAlias(r.tn);
r._rtn = args?.tableNames?.find(t => t.tn === r.rtn)?._tn || this.getTableNameAlias(r.rtn);
r._tn = args?.tableNames?.find(t => t.tn === r.tn)?._tn || this.getTableNameAlias(r.tn);
r.enabled = true;
})
@ -571,7 +634,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
this.log(`xcTablesPopulate : Generating schema of '%s' %s`, table.tn, table.type);
/**************** prepare GQL: schemas, types, resolvers ****************/
this.schemas[table.tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getString();
// this.schemas[table.tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getString();
// tslint:disable-next-line:max-classes-per-file
this.types[table.tn] = class extends XCType {
@ -594,7 +657,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
title: table.tn,
type: table.type || 'table',
meta: JSON.stringify(this.metas[table.tn]),
schema: this.schemas[table.tn],
// schema: this.schemas[table.tn],
alias: this.metas[table.tn]._tn,
})
}
@ -620,12 +683,10 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
await NcHelp.executeOperations(insertResolvers, this.connectionConfig.client);
}
}
});
await NcHelp.executeOperations(tableResolvers, this.connectionConfig.client);
await Promise.all(Object.entries(this.metas).map(async ([tn, schema]) => {
for (const hm of schema.hasMany) {
@ -633,8 +694,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const colNameAlias = self.models[hm.rtn]?.columnToAlias[hm.rcn];
const countPropName = `${inflection.camelize(hm._tn, false)}Count`;
const listPropName = `${inflection.camelize(hm._tn, false)}List`;
const countPropName = `${hm._tn}Count`;
const listPropName = `${hm._tn}List`;
this.log(`xcTablesPopulate : Populating '%s' and '%s' loaders`, listPropName, countPropName);
@ -645,55 +706,16 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
/* has many relation list loader with middleware */
const mw = new GqlMiddleware(this.acls, hm.tn, '', this.models);
const listLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async ids => {
const data = await this.models[tn].hasManyListGQL({
ids,
child: hm.tn
})
return ids.map(id => data[id] ? data[id].map(c => new self.types[hm.tn](c)) : []);
},
[mw.postLoaderMiddleware]
));
/* defining HasMany list method within GQL Type class */
Object.defineProperty(this.types[tn].prototype, `${listPropName}`, {
async value(args, context, info): Promise<any> {
return listLoader.load([this[colNameAlias], args, context, info]);
},
configurable: true
})
/* has many relation list loader with middleware */
this.addHmListResolverMethodToType(tn, hm, mw, {}, listPropName, colNameAlias);
if (countPropName in this.types[tn].prototype) {
continue;
}
// create count loader with middleware
{
const mw = new GqlMiddleware(this.acls, hm.tn, '', this.models);
const countLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async ids => {
const data = await this.models[tn].hasManyListCount({
ids,
child: hm.tn
})
return data;
},
[mw.postLoaderMiddleware]
));
const mw = new GqlMiddleware(this.acls, hm.tn, null, this.models);
// defining HasMany count method within GQL Type class
Object.defineProperty(this.types[tn].prototype, `${countPropName}`, {
async value(args, context, info): Promise<any> {
return countLoader.load([this[colNameAlias], args, context, info]);
},
configurable: true
})
// create count loader with middleware
this.addHmCountResolverMethodToType(hm, mw, tn, {}, countPropName, colNameAlias);
}
this.log(`xcTablesPopulate : Inserting loader metadata of '%s' and '%s' loaders`, listPropName, countPropName);
@ -717,34 +739,22 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
for (const bt of schema.belongsTo) {
const colNameAlias = self.models[bt.tn]?.columnToAlias[bt.cn];
const propName = `${inflection.camelize(bt._rtn, false)}Read`;
const propName = `${bt._rtn}Read`;
if (propName in this.types[tn].prototype) {
continue;
}
this.log(`xcTablesPopulate : Populating '%s' loader`, propName);
// create read loader with middleware
const mw = new GqlMiddleware(this.acls, bt.rtn, '', this.models);
const readLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async ids => {
const data = await self.models[bt.rtn].list({
where: `(${bt.rcn},in,${ids.join(',')})`,
limit: ids.length
})
const gs = _.groupBy(data, bt.rcn);
return ids.map(async id => gs?.[id]?.[0] && new self.types[bt.rtn](gs[id][0]))
},
[mw.postLoaderMiddleware]
));
{
const mw = new GqlMiddleware(this.acls, bt.rtn, null, this.models);
this.log(`xcTablesRead : Creating loader for '%s'`, `${tn}Bt${bt.rtn}`);
this.adBtResolverMethodToType(propName, mw,
tn, bt, colNameAlias, colNameAlias, null);
}
// defining BelongsTo read method within GQL Type class
Object.defineProperty(this.types[tn].prototype, `${propName}`, {
async value(args, context, info): Promise<any> {
return readLoader.load([this[colNameAlias], args, context, info]);
},
configurable: true
});
this.log(`xcTablesPopulate : Inserting loader metadata of '%s' loader`, propName);
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
@ -757,6 +767,85 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}
}));
await this.getManyToManyRelations();
// generate schema of models
for (const meta of Object.values(this.metas)) {
/**************** prepare GQL: schemas, types, resolvers ****************/
this.schemas[meta.tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(
{
...this.generateContextForTable(
meta.tn,
meta.columns,
relations,
meta.hasMany,
meta.belongsTo,
meta.type,
meta._tn,
),
manyToMany: meta.manyToMany
})).getString();
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
schema: this.schemas[meta.tn],
}, {
title: meta.tn
})
}
// add property in type class for many to many relations
await Promise.all(Object.entries(this.metas).map(async ([tn, meta]) => {
if (!meta.manyToMany) {
return;
}
for (const mm of meta.manyToMany) {
const countPropName = `${mm._rtn}Count`;
const listPropName = `${mm._rtn}MMList`;
this.log(`xcTablesPopulate : Populating '%s' and '%s' many to many loaders`, listPropName, countPropName);
if (listPropName in this.types[tn].prototype) {
continue;
}
/* has many relation list loader with middleware */
const mw = new GqlMiddleware(this.acls, mm.rtn, '', this.models);
/* has many relation list loader with middleware */
this.addMMListResolverMethodToType(tn, mm, mw, {}, listPropName, meta.columns.find(c => c.pk)._cn);
// if (countPropName in this.types[tn].prototype) {
// continue;
// }
// {
// const mw = new GqlMiddleware(this.acls, hm.tn, null, this.models);
//
// // create count loader with middleware
// this.addHmCountResolverMethodToType(hm, mw, tn, {}, countPropName, colNameAlias);
// }
//
// this.log(`xcTablesPopulate : Inserting loader metadata of '%s' and '%s' loaders`, listPropName, countPropName);
//
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
title: `${tn}Mm${mm.rtn}List`,
parent: tn,
child: mm.rtn,
relation: 'mm',
resolver: 'mmlist',
});
// await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
// title: `${tn}Mm${hm.tn}Count`,
// parent: mm.tn,
// child: mm.rtn,
// relation: 'hm',
// resolver: 'list',
// });
}
}));
}
}
@ -795,6 +884,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
public async xcTableRename(oldTablename: string, newTablename: string): Promise<any> {
this.log(`xcTableRename : '%s' => '%s'`, oldTablename, newTablename);
//todo: verify the update queries
// const metaArr = await (this.sqlClient.knex as XKnex)('nc_models').select();
@ -1020,7 +1110,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
public async onRelationCreate(tnp: string, tnc: string, args): Promise<void> {
await super.onRelationCreate(tnp, tnc, args)
this.log(`onRelationCreate : Within relation create event handler`);
const self = this;
// const self = this;
const relations = await this.getXcRelationList();
// set table name alias
@ -1032,65 +1122,95 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
/* update parent table meta and resolvers */
{
const columns = await this.getColumnList(tnp);
const columns = this.metas[tnp]?.columns;
const hasMany = this.extractHasManyRelationsOfTable(relations, tnp);
const belongsTo = this.extractBelongsToRelationsOfTable(relations, tnp);
const ctx = this.generateContextForTable(tnp, columns, relations, hasMany, belongsTo);
ctx.manyToMany = this.metas?.[tnp]?.manyToMany;
const meta = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getObject();
this.metas[tnp] = meta;
// this.metas[tnp] = meta;
this.schemas[tnp] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getString();
// update old model meta with new details
this.log(`onRelationCreate : Generating and updating model meta for parent table '%s'`, tnp);
const existingModel = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {'title': tnp});
let queryParams;
try {
queryParams = JSON.parse(existingModel.query_params);
} catch (e) { /* */
}
if (existingModel) {
// todo: persisting old table_alias and columnAlias
// todo: get enable state of other relations
const oldMeta = JSON.parse(existingModel.meta);
meta.hasMany.forEach(hm => {
hm.enabled = true;
})
Object.assign(oldMeta, {
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)}`
})
if (queryParams?.showFields) {
queryParams.showFields[`${this.getTableNameAlias(tnp)} => ${this.getTableNameAlias(tnc)}`] = true;
}
this.models[tnp] = this.getBaseModel(oldMeta);
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
title: tnp,
meta: JSON.stringify(meta),
schema: this.schemas[tnp]
meta: JSON.stringify(oldMeta),
schema: this.schemas[tnp],
...(queryParams ? {query_params: JSON.stringify(queryParams)} : {})
}, {'title': tnp})
}
const countPropName = `${inflection.camelize(this.getTableNameAlias(tnc), false)}Count`;
const listPropName = `${inflection.camelize(this.getTableNameAlias(tnc), false)}List`;
const countPropName = `${this.getTableNameAlias(tnc)}Count`;
const listPropName = `${this.getTableNameAlias(tnc)}List`;
this.log(`onRelationCreate : Generating and inserting '%s' and '%s' loaders`, countPropName, listPropName);
/* has many relation list loader with middleware */
const mw = new GqlMiddleware(this.acls, tnc, '', this.models);
const listLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async ids => {
const data = await this.models[tnp].hasManyListGQL({
ids,
child: tnc
})
return ids.map(id => data[id] ? data[id].map(c => new self.types[tnc](c)) : []);
},
[mw.postLoaderMiddleware]
));
const hm = hasMany.find(rel => rel.tn === tnc)
{
/* has many relation list loader with middleware */
const mw = new GqlMiddleware(this.acls, tnc, '', this.models);
this.addHmListResolverMethodToType(tnp, hm, mw, {}, listPropName, this.models[hm.rtn]?.columnToAlias[hm.rcn]);
}
/* const listLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async ids => {
const data = await this.models[tnp].hasManyListGQL({
ids,
child: tnc
})
return ids.map(id => data[id] ? data[id].map(c => new self.types[tnc](c)) : []);
},
[mw.postLoaderMiddleware]
));
const currentRelation = hasMany.find(rel => rel.tn === tnc)
/* defining HasMany list method within GQL Type class */
Object.defineProperty(this.types[tnp].prototype, `${listPropName}`, {
async value(args, context, info): Promise<any> {
return listLoader.load([this[currentRelation.rcn], args, context, info]);
},
configurable: true
})
/!* defining HasMany list method within GQL Type class *!/
Object.defineProperty(this.types[tnp].prototype, `${listPropName}`, {
async value(args, context, info): Promise<any> {
return listLoader.load([this[hm.rcn], args, context, info]);
},
configurable: true
})*/
// create count loader with middleware
{
const mw = new GqlMiddleware(this.acls, tnc, '', this.models);
const countLoader = new DataLoader(
this.addHmListResolverMethodToType(tnp, hm, mw, {}, countPropName, this.models[hm.rtn]?.columnToAlias[hm.rcn]);
/*const countLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async ids => {
@ -1106,10 +1226,10 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
// defining HasMany count method within GQL Type class
Object.defineProperty(this.types[tnp].prototype, `${countPropName}`, {
async value(args, context, info): Promise<any> {
return countLoader.load([this[currentRelation.rcn], args, context, info]);
return countLoader.load([this[hm.rcn], args, context, info]);
},
configurable: true
})
})*/
}
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
@ -1131,40 +1251,67 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
/* update child table meta and resolvers */
{
const columns = await this.getColumnList(tnc);
const columns = this.metas[tnc]?.columns;
const belongsTo = this.extractBelongsToRelationsOfTable(relations, tnc);
const hasMany = this.extractHasManyRelationsOfTable(relations, tnc);
const ctx = this.generateContextForTable(tnc, columns, relations, hasMany, belongsTo);
ctx.manyToMany = this.metas?.[tnc]?.manyToMany;
const meta = ModelXcMetaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getObject();
this.metas[tnc] = meta;
// this.metas[tnc] = meta;
this.schemas[tnc] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getString();
this.log(`onRelationCreate : Generating and updating model meta for child table '%s'`, tnc);
// update old model meta with new details
const existingModel = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {'title': tnc});
let queryParams;
try {
queryParams = JSON.parse(existingModel.query_params);
} catch (e) { /* */
}
if (existingModel) {
// todo: persisting old table_alias and columnAlias
const oldMeta = JSON.parse(existingModel.meta);
Object.assign(oldMeta, {
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)}`
})
if (queryParams?.showFields) {
queryParams.showFields[`${this.getTableNameAlias(tnp)} <= ${this.getTableNameAlias(tnc)}`] = true;
}
this.models[tnc] = this.getBaseModel(oldMeta);
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
title: tnc,
meta: JSON.stringify(meta),
schema: this.schemas[tnc]
meta: JSON.stringify(oldMeta),
schema: this.schemas[tnc],
...(queryParams ? {query_params: JSON.stringify(queryParams)} : {})
}, {'title': tnc})
}
const propName = `${inflection.camelize(this.getTableNameAlias(tnp), false)}Read`;
const propName = `${this.getTableNameAlias(tnp)}Read`;
this.log(`onRelationCreate : Generating and inserting'%s' loader`, propName);
const currentRelation = belongsTo.find(rel => rel.rtn === tnp)
// create read loader with middleware
const mw = new GqlMiddleware(this.acls, tnp, '', this.models);
const readLoader = new DataLoader(
this.adBtResolverMethodToType(
propName,
mw,
tnc,
currentRelation,
this.models[currentRelation.rtn]?.columnToAlias[currentRelation.rcn],
this.models[currentRelation.tn]?.columnToAlias[currentRelation?.cn]
);
/*const readLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async ids => {
@ -1184,7 +1331,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
return readLoader.load([this[currentRelation.cn], args, context, info]);
},
configurable: true
})
})*/
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
title: `${tnc}Bt${tnp}`,
@ -1195,8 +1342,6 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
});
}
this.models[tnc] = this.getBaseModel(this.metas[tnc]);
this.models[tnp] = this.getBaseModel(this.metas[tnp]);
await this.reInitializeGraphqlEndpoint();
}
@ -1216,7 +1361,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
/* update parent table meta and resolvers */
{
const columns = await this.getColumnList(tnp);
const columns = this.metas[tnp]?.columns;//await this.getColumnList(tnp);
const hasMany = this.extractHasManyRelationsOfTable(relations, tnp);
const belongsTo = this.extractBelongsToRelationsOfTable(relations, tnp);
const ctx = this.generateContextForTable(tnp, columns, relations, hasMany, belongsTo);
@ -1232,18 +1377,20 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const oldMeta = JSON.parse(existingModel.meta);
Object.assign(oldMeta, {
hasMany: meta.hasMany,
schema: this.schemas[tnp]
v: oldMeta.v.filter(({hm}) => !hm || hm.rtn !== tnp || hm.tn !== tnc)
});
// todo: backup schema
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
title: tnp,
meta: JSON.stringify(oldMeta)
meta: JSON.stringify(oldMeta),
schema: this.schemas[tnp]
}, {'title': tnp})
this.metas[tnp] = oldMeta;
this.models[tnp] = this.getBaseModel(oldMeta);
}
const countPropName = `${inflection.camelize(this.getTableNameAlias(tnc), false)}Count`;
const listPropName = `${inflection.camelize(this.getTableNameAlias(tnc), false)}List`;
const countPropName = `${this.getTableNameAlias(tnc)}Count`;
const listPropName = `${this.getTableNameAlias(tnc)}List`;
this.log(`onRelationDelete : Deleting '%s' and '%s' loaders`, countPropName, listPropName);
/* defining HasMany list method within GQL Type class */
@ -1279,15 +1426,17 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const oldMeta = JSON.parse(existingModel.meta);
Object.assign(oldMeta, {
belongsTo: meta.belongsTo,
v: oldMeta.v.filter(({bt}) => !bt || bt.rtn !== tnp || bt.tn !== tnc)
});
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
title: tnc,
meta: JSON.stringify(oldMeta)
meta: JSON.stringify(oldMeta),
schema: this.schemas[tnc]
}, {'title': tnc});
this.metas[tnc] = oldMeta;
this.models[tnc] = this.getBaseModel(oldMeta);
}
const propName = `${inflection.camelize(this.getTableNameAlias(tnp), false)}Read`;
const propName = `${this.getTableNameAlias(tnp)}Read`;
this.log(`onRelationDelete : Deleting '%s' loader`, propName);
@ -1590,7 +1739,10 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const rootValue = mergeResolvers([{
nocodb_health() {
return 'Coming soon'
}
},
m2mNotChildren: m2mNotChildren({models: this.models}),
m2mNotChildrenCount: m2mNotChildrenCount({models: this.models}),
JSON: GraphQLJSON,
}, ...Object.values(this.resolvers).map(r => r.mapResolvers(this.customResolver))]);
this.log(`initGraphqlRoute : Building graphql schema`);
@ -1613,7 +1765,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
},
rootValue,
schema,
validationRules: [depthLimit(this.connectionConfig?.meta?.api?.graphqlDepthLimit ?? 10),
validationRules: [
depthLimit(this.connectionConfig?.meta?.api?.graphqlDepthLimit ?? 10),
],
customExecuteFn: async (args) => {
const data = await execute(args);
@ -1659,6 +1812,127 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
private log(str, ...args): void {
log(`${this.dbAlias} : ${str}`, ...args);
}
public async onManyToManyRelationCreate(parent: string, child: string, args?: any) {
await super.onManyToManyRelationCreate(parent, child, args);
for (const tn of [parent, child]) {
const meta = this.metas[tn];
const {columns, hasMany, belongsTo, manyToMany} = meta;
const ctx = this.generateContextForTable(tn, columns, [...hasMany, ...belongsTo], hasMany, belongsTo);
ctx.manyToMany = manyToMany;
this.schemas[tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getString();
// todo: update schema history
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
schema: this.schemas[tn]
}, {
title: tn
})
}
{
const listPropName = `${this.metas[child]._tn}MMList`;
this.log(`onRelationCreate : Generating and inserting '%s' loaders`, listPropName);
/* has many relation list loader with middleware */
const mw = new GqlMiddleware(this.acls, parent, '', this.models);
this.addMMListResolverMethodToType(parent, {rtn: child}, mw, {}, listPropName, this.metas[parent].columns.find(c => c.pk)._cn)
}
{
const listPropName = `${this.metas[parent]._tn}MMList`;
this.log(`onRelationCreate : Generating and inserting '%s' loaders`, listPropName);
/* has many relation list loader with middleware */
const mw = new GqlMiddleware(this.acls, child, '', this.models);
this.addMMListResolverMethodToType(child, {rtn: parent}, mw, {}, listPropName, this.metas[child].columns.find(c => c.pk)._cn)
}
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
title: `${parent}Mm${child}List`,
parent,
child,
relation: 'mm',
resolver: 'list',
});
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
title: `${child}Mm${parent}List`,
parent: child,
child: parent,
relation: 'mm',
resolver: 'list',
});
await this.reInitializeGraphqlEndpoint();
}
public async onManyToManyRelationDelete(parent: string, child: string, args?: any) {
await super.onManyToManyRelationDelete(parent, child, args)
for (const tn of [parent, child]) {
const meta = this.metas[tn];
const {columns, hasMany, belongsTo, manyToMany} = meta;
const ctx = this.generateContextForTable(tn, columns, [...hasMany, ...belongsTo], hasMany, belongsTo);
this.schemas[tn] = GqlXcSchemaFactory.create(this.connectionConfig, {
...this.generateRendererArgs(ctx),
manyToMany
}).getString();
// todo: update schema history
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
schema: this.schemas[tn]
}, {
title: tn
})
}
await this.reInitializeGraphqlEndpoint();
}
protected async ncUpManyToMany(): Promise<any> {
const metas = await super.ncUpManyToMany();
if (!metas) {
return;
}
for (const meta of metas) {
const ctx = this.generateContextForTable(meta.tn, meta.columns, [], meta.hasMany, meta.belongsTo, meta.type, meta._tn);
/* generate gql schema of the table */
const schema = GqlXcSchemaFactory.create(this.connectionConfig, {
dir: '',
ctx: {
...ctx,
manyToMany: meta.manyToMany
},
filename: ''
}).getString();
/* update schema in metadb */
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
schema
}, {
title: meta.tn
})
// todo : add loaders
if (meta.manyToMany) {
for (const mm of meta.manyToMany) {
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
title: `${mm.tn}Mm${mm.rtn}List`,
parent: mm.tn,
child: mm.rtn,
relation: 'mm',
resolver: 'mmlist',
});
}
}
}
}
}
/**

53
packages/nc-gui/components/project/appStore/inputs/floatCell.vue → packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts

@ -1,49 +1,17 @@
<template>
<input v-on="parentListeners" v-model="localState" type="number">
</template>
import {BaseModelSql} from "../../dataMapper";
<script>
export default {
name: "floatCell",
props: {
value: [String,Number]
},
mounted() {
this.$el.focus();
},
computed: {
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val);
}
},
parentListeners(){
const $listeners = {};
if(this.$listeners.blur){
$listeners.blur = this.$listeners.blur;
}
if(this.$listeners.focus){
$listeners.focus = this.$listeners.focus;
}
return $listeners;
},
export const m2mNotChildren = ({models = {}}: { models: { [key: string]: BaseModelSql } }) => {
return async (args) => {
return models[args?.parent]?.m2mNotChildren(args);
}
}
</script>
<style scoped>
input {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
export const m2mNotChildrenCount = ({models = {}}: { models: { [key: string]: BaseModelSql } }) => {
return async (args) => {
return models[args?.parent]?.m2mNotChildrenCount(args);
}
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
@ -66,4 +34,3 @@ input {
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save