Browse Source

Merge branch 'develop' into feat/gui-v2-form-view

pull/3030/head
Wing-Kam Wong 2 years ago
parent
commit
0d5dace6d9
  1. 4
      packages/nc-gui-v2/assets/style-v2.scss
  2. 1
      packages/nc-gui-v2/components.d.ts
  3. 22
      packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue
  4. 1
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  5. 40
      packages/nc-gui-v2/components/smartsheet-column/PercentOptions.vue
  6. 25
      packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue
  7. 3
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  8. 40
      packages/nc-gui-v2/components/smartsheet/VirtualCell.vue
  9. 502
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  10. 4
      packages/nc-gui-v2/components/virtual-cell/Formula.vue
  11. 625
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  12. 6
      packages/nc-gui-v2/components/virtual-cell/Lookup.vue
  13. 622
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  14. 4
      packages/nc-gui-v2/components/virtual-cell/Rollup.vue
  15. 16
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  16. 310
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  17. 126
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItemsModal.vue
  18. 305
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  19. 4
      packages/nc-gui-v2/composables/index.ts
  20. 19
      packages/nc-gui-v2/composables/useBelongsTo.ts
  21. 19
      packages/nc-gui-v2/composables/useHasMany.ts
  22. 258
      packages/nc-gui-v2/composables/useLTARStore.ts
  23. 19
      packages/nc-gui-v2/composables/useManyToMany.ts
  24. 6
      packages/nc-gui-v2/context/index.ts
  25. 2
      packages/nc-gui/lang/fr.json

4
packages/nc-gui-v2/assets/style-v2.scss

@ -104,3 +104,7 @@ html {
.nc-active-btn > .ant-btn{
@apply bg-primary/20;
}
.ant-modal-wrap {
@apply !scrollbar-thin-primary;
}

1
packages/nc-gui-v2/components.d.ts vendored

@ -20,6 +20,7 @@ declare module '@vue/runtime-core' {
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']

22
packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue

@ -50,22 +50,36 @@ formState.value.meta = {
color: '#777',
...formState.value.meta,
}
// antdv doesn't support object as value
// use iconIdx as value and update back in watch
const iconIdx = iconList.findIndex(
(ele) => ele.checked === formState.value.meta.icon.checked && ele.unchecked === formState.value.meta.icon.unchecked,
)
formState.value.meta.iconIdx = iconIdx === -1 ? 0 : iconIdx
watch(
() => formState.value.meta.iconIdx,
(v) => {
formState.value.meta.icon = iconList[v]
},
)
</script>
<template>
<a-row>
<a-col :span="24">
<a-form-item label="Icon">
<a-select v-model:value="formState.meta.icon" size="small" class="w-52">
<!-- FIXME: antdv doesn't support object as value -->
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="icon">
<a-select v-model:value="formState.meta.iconIdx" size="small" class="w-52">
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<component
:is="getMdiIcon(icon.checked)"
class="mx-1"
:style="{
color: formState.meta.color,
}"
/>
{{ ' ' }}
<component
:is="getMdiIcon(icon.unchecked)"
:style="{

1
packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue

@ -96,6 +96,7 @@ watchEffect(() => {
<SmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" />
<SmartsheetColumnLinkedToAnotherRecordOptions v-if="formState.uidt === UITypes.LinkToAnotherRecord" />
<SmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" />
<div
v-if="!isVirtualCol(formState.uidt)"

40
packages/nc-gui-v2/components/smartsheet-column/PercentOptions.vue

@ -0,0 +1,40 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow } from '#imports'
import { precisions } from '@/utils/percentUtils'
const { formState } = $(useColumnCreateStoreOrThrow())
if (!formState.meta) formState.meta = {}
if (!formState.meta?.precision) formState.meta.precision = precisions[0].id
if (!formState.meta?.negative) formState.meta.negative = false
if (!formState.meta?.default) formState.meta.default = null
</script>
<template>
<div class="flex flex-col mt-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" label="Precision">
<a-select v-model:value="formState.meta.precision" size="small">
<a-select-option v-for="(precision, i) of precisions" :key="i" :value="precision.id">
<div class="flex flex-row items-center">
<div class="text-xs">
{{ precision.title }}
</div>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Default Number (%)">
<a-input v-model:value="formState.meta.default" size="small" name="default" type="number" />
</a-form-item>
</div>
<div class="flex flex-row mt-2">
<a-form-item>
<div class="flex flex-row space-x-2 items-center">
<a-switch v-model:checked="formState.meta.negative" name="negative" />
<div class="text-xs">Allow negative numbers</div>
</div>
</a-form-item>
</div>
</div>
</template>

25
packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue

@ -35,7 +35,8 @@ const picked = ref(formState.value.meta.color || enumColor.light[0])
// set default value
formState.value.meta = {
icons: {
iconIdx: 0,
icon: {
full: 'mdi-star',
empty: 'mdi-star-outline',
},
@ -43,22 +44,36 @@ formState.value.meta = {
max: 5,
...formState.value.meta,
}
// antdv doesn't support object as value
// use iconIdx as value and update back in watch
const iconIdx = iconList.findIndex(
(ele) => ele.full === formState.value.meta.icon.full && ele.empty === formState.value.meta.icon.empty,
)
formState.value.meta.iconIdx = iconIdx === -1 ? 0 : iconIdx
watch(
() => formState.value.meta.iconIdx,
(v) => {
formState.value.meta.icon = iconList[v]
},
)
</script>
<template>
<a-row>
<a-col :span="12">
<a-form-item label="Icon">
<a-select v-model:value="formState.meta.icon" size="small" class="w-52">
<!-- FIXME: antdv doesn't support object as value -->
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="icon">
<a-select v-model:value="formState.meta.iconIdx" size="small" class="w-52">
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<component
:is="getMdiIcon(icon.full)"
class="mx-1"
:style="{
color: formState.meta.color,
}"
/>
{{ ' ' }}
<component
:is="getMdiIcon(icon.empty)"
:style="{

3
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -13,6 +13,7 @@ import {
import {
ActiveViewInj,
ChangePageInj,
EditModeInj,
FieldsInj,
IsFormInj,
IsGridInj,
@ -278,6 +279,8 @@ const onNavigate = (dir: NavigateDir) => {
v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="selected.col === colIndex && selected.row === rowIndex"
:row="row"
@navigate="onNavigate"
/>

40
packages/nc-gui-v2/components/smartsheet/VirtualCell.vue

@ -1,27 +1,35 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { provide, useVirtualCell } from '#imports'
import { ColumnInj, ValueInj } from '~/context'
import { provide, toRef, useVirtualCell } from '#imports'
import type { Row } from '~/composables'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj } from '~/context'
import { NavigateDir } from '~/lib'
interface Props {
column: ColumnType
modelValue: any
row: Row
active?: boolean
}
const { column, modelValue: value } = defineProps<Props>()
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const { column, modelValue: value } = props
const active = toRef(props, 'active', false)
const row = toRef(props, 'row')
provide(ColumnInj, column)
provide(ValueInj, value)
provide(CellValueInj, value)
provide(ActiveCellInj, active)
provide(RowInj, row)
provide(CellValueInj, toRef(props, 'modelValue'))
const { isLookup, isBt, isRollup, isMm, isHm, isFormula, isCount } = useVirtualCell(column)
</script>
<template>
<div
class="nc-virtual-cell"
class="nc-virtual-cell w-full"
@keydown.stop.enter.exact="emit('navigate', NavigateDir.NEXT)"
@keydown.stop.shift.enter.exact="emit('navigate', NavigateDir.PREV)"
>
@ -34,23 +42,3 @@ const { isLookup, isBt, isRollup, isMm, isHm, isFormula, isCount } = useVirtualC
<VirtualCellLookup v-else-if="isLookup" />
</div>
</template>
<style scoped>
.nc-hint {
font-size: 0.61rem;
color: grey;
}
.nc-virtual-cell {
position: relative;
}
.nc-locked-overlay {
position: absolute;
z-index: 2;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
</style>

502
packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue

@ -1,490 +1,50 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import ItemChip from './components/ItemChip.vue'
import { ColumnInj, ValueInj } from '~/context'
import { useBelongsTo } from '#imports'
import ListItems from './components/ListItems.vue'
import { useProvideLTARStore } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiExpandIcon from '~icons/mdi/arrow-expand'
const column = inject(ColumnInj)
const value = inject(ValueInj)
const reloadTrigger = inject(ReloadViewDataHookInj)
const cellValue = inject(CellValueInj)
const row = inject(RowInj)
const active = false
const localState = null
const { parentMeta, loadParentMeta, primaryValueProp } = useBelongsTo(column as ColumnType)
await loadParentMeta()
// import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
/* import { RelationTypes, UITypes, isSystemColumn } from 'nocodb-sdk'
import ListItems from '~/components/project/spreadsheet/components/virtualCell/components/ListItems'
import ListChildItems from '~/components/project/spreadsheet/components/virtualCell/components/ListChildItems'
import ItemChip from '~/components/project/spreadsheet/components/virtualCell/components/ItemChip'
import { parseIfInteger } from '@/helpers'
export default {
name: 'BelongsToCell',
components: { ListChildItems, ItemChip, ListItems },
props: {
isLocked: Boolean,
breadcrumbs: {
type: Array,
default() {
return []
},
},
isForm: Boolean,
value: [Array, Object],
meta: [Object],
nodes: [Object],
row: [Object],
api: [Object, Function],
sqlUi: [Object, Function],
active: Boolean,
isNew: Boolean,
disabledColumns: Object,
isPublic: Boolean,
metas: Object,
password: String,
column: 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,
}),
computed: {
parentMeta() {
return this.metas
? this.metas[this.column.colOptions.fk_related_model_id]
: this.$store.state.meta.metas[this.column.colOptions.fk_related_model_id]
},
// todo : optimize
parentApi() {},
parentId() {
return (
this.pid ??
(this.value &&
this.parentMeta &&
this.parentMeta.columns
.filter((c) => c.pk)
.map((c) => this.value[c.title])
.join('___'))
)
},
rowId() {
return (
this.row &&
this.meta &&
this.meta.columns
.filter((c) => c.pk)
.map((c) => this.row[c.title])
.join('___')
)
},
parentPrimaryCol() {
return this.parentMeta && (this.parentMeta.columns.find((c) => c.pv) || {}).title
},
parentPrimaryKey() {
return this.parentMeta && (this.parentMeta.columns.find((c) => c.pk) || {}).title
},
parentReferenceKey() {
return (
this.parentMeta && (this.parentMeta.columns.find((c) => c.id === this.column.colOptions.fk_parent_column_id) || {}).title
)
},
btWhereClause() {
// if parent reference key is pk, then filter out the selected value
// else, filter out the selected value + empty values (as we can't set an empty value)
const prk = this.parentReferenceKey
const selectedValue =
this.meta && this.meta.columns
? this.meta.columns
.filter((c) => c.id === this.column.colOptions.fk_child_column_id)
.map((c) => this.row[c.title] || '')
.join('___')
: ''
return `(${prk},not,${selectedValue})~or(${prk},is,null)`
},
parentQueryParams() {
if (!this.parentMeta) {
return {}
}
// todo: use reduce
return {}
},
parentAvailableColumns() {
if (!this.parentMeta) {
return []
}
const columns = []
if (this.parentMeta.columns) {
columns.push(...this.parentMeta.columns.filter((c) => !isSystemColumn(c)))
}
return columns
},
// todo:
form() {
return this.selectedParent && !this.isPublic
? () => 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]
}
return null
},
},
watch: {
isNew(n, o) {
if (!n && o) {
this.localState = null
this.$emit('update:localState', this.localState)
}
},
},
async mounted() {
if (this.isNew && this.value) {
this.localState = this.value
}
if (this.isForm) {
await this.loadParentMeta()
}
},
created() {
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.parentMeta.columns.find(
(c) =>
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions &&
this.column.colOptions &&
c.colOptions.fk_child_column_id === this.column.colOptions.fk_child_column_id &&
c.colOptions.fk_parent_column_id === this.column.colOptions.fk_parent_column_id &&
c.colOptions.type === RelationTypes.HAS_MANY,
) || {}
).title]: [this.row],
}
this.expandFormModal = true
},
async unlink(parent) {
const column = this.meta.columns.find((c) => c.id === this.column.colOptions.fk_child_column_id)
const _cn = column.title
if (this.isNew) {
this.$emit('updateCol', this.row, _cn, null)
this.localState = null
this.$emit('update:localState', this.localState)
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.title])
.join('___')
// todo: audit
await this.$api.dbTableRow.nestedRemove(
NOCO,
this.projectName,
this.meta.title,
id,
'bt',
this.column.title,
parent[this.parentPrimaryKey],
)
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.title])
.join('___')
const _cn = this.parentMeta.columns.find((c) => c.column_name === this.hm.column_name).title
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.title])
.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,
id: this.column.colOptions.fk_related_model_id,
})
}
},
async showNewRecordModal() {
await this.loadParentMeta()
this.newRecordModal = true
},
async addChildToParent(parent) {
const pid = this._extractRowId(parent, this.parentMeta)
const id = this._extractRowId(this.row, this.meta)
const _cn = this.meta.columns.find((c) => c.id === this.column.colOptions.fk_child_column_id).title
if (this.isNew) {
const _rcn = this.parentMeta.columns.find((c) => c.id === this.column.colOptions.fk_parent_column_id).title
this.localState = parent
this.$emit('update:localState', this.localState)
this.$emit('updateCol', this.row, _cn, parent[_rcn])
this.newRecordModal = false
return
}
await this.$api.dbTableRow.nestedAdd(NOCO, this.projectName, this.meta.title, id, 'bt', this.column.title, pid)
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)
},
},
} */
const listItemsDlg = ref(false)
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Required<ColumnType>,
row,
() => reloadTrigger?.trigger(),
)
await loadRelatedTableMeta()
</script>
<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">
<ItemChip :active="active" :item="value" :value="value[primaryValueProp]" />
<!-- :readonly="isLocked || (isPublic && !isForm)"
@edit="editParent"
@unlink="unlink" -->
<div class="flex w-full chips-wrapper align-center" :class="{ active }">
<div class="chips d-flex align-center flex-grow">
<template v-if="cellValue || localState">
<ItemChip :item="cellValue" :value="cellValue[relatedTablePrimaryValueProp]" @unlink="unlink(cellValue || localState)" />
</template>
</div>
<!-- <div
v-if="!isLocked && _isUIAllowed('xcDatatableEditable') && (isForm || !isPublic)"
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> -->
<!-- <ListItems
v-if="newRecordModal"
:key="parentId"
v-model="newRecordModal"
:size="10"
:meta="parentMeta"
:column="column"
:primary-col="parentPrimaryCol"
:primary-key="parentPrimaryKey"
:parent-meta="meta"
:api="parentApi"
:query-params="{
...parentQueryParams,
where: isNew ? null : `${btWhereClause}`,
}"
:is-public="isPublic"
:tn="bt && bt.rtn"
:password="password"
:row-id="rowId"
@add-new-record="insertAndMapNewParentRecord"
@add="addChildToParent"
/>
<ListChildItems
v-if="parentMeta && isForm"
ref="childList"
:is-form="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})`,
}"
:bt="value"
:is-public="isPublic"
:row-id="parentId"
@new-record="showNewRecordModal"
@edit="editParent"
@unlink="unlink"
/>
<v-dialog
v-if="!isPublic && selectedParent"
v-model="expandFormModal"
:overlay-opacity="0.8"
width="1000px"
max-width="100%"
class="mx-auto"
>
<component
:is="form"
v-if="selectedParent"
ref="expandedForm"
v-model="selectedParent"
v-model:is-new="isNewParent"
:db-alias="nodes.dbAlias"
:has-many="parentMeta.hasMany"
:belongs-to="parentMeta.belongsTo"
:table="parentMeta.table_name"
:old-row="{ ...selectedParent }"
:meta="parentMeta"
:sql-ui="sqlUi"
:primary-value-column="parentPrimaryCol"
:api="parentApi"
:available-columns="parentAvailableColumns"
:nodes="nodes"
:query-params="parentQueryParams"
icon-color="warning"
:breadcrumbs="breadcrumbs"
@cancel="
selectedParent = null
expandFormModal = false
"
@input="onParentSave"
/>
</v-dialog> -->
<div class="flex-1 flex justify-end gap-1">
<MdiExpandIcon
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500)"
@click="listItemsDlg = true"
/>
</div>
<ListItems v-model="listItemsDlg" />
</div>
</template>
<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: 0.5em;
}
&:hover .primary-key {
display: inline;
}
<style scoped>
.nc-action-icon {
@apply hidden cursor-pointer;
}
.child-card {
cursor: pointer;
&:hover {
box-shadow: 0 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);
}
}
.chips-wrapper:hover .nc-action-icon {
@apply inline-block;
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Md Ishtiaque Zafar <ishtiaque.zafar92@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@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-v2/components/virtual-cell/Formula.vue

@ -1,12 +1,12 @@
<script lang="ts" setup>
import { computed, useProject } from '#imports'
import { ColumnInj, ValueInj } from '~/context'
import { CellValueInj, ColumnInj } from '~/context'
import { handleTZ } from '~/utils/dateTimeUtils'
import { replaceUrlsWithLink } from '~/utils/urlUtils'
const column = inject(ColumnInj)
const value = inject(ValueInj)
const value = inject(CellValueInj)
const { isPg } = useProject()

625
packages/nc-gui-v2/components/virtual-cell/HasMany.vue

@ -1,604 +1,55 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import ItemChip from './components/ItemChip.vue'
import { ColumnInj, ValueInj } from '~/context'
import { useHasMany } from '#imports'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
import { useProvideLTARStore } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiExpandIcon from '~icons/mdi/arrow-expand'
import MdiPlusIcon from '~icons/mdi/plus'
const column = inject(ColumnInj)
const value = inject(ValueInj)
const active = false
const { childMeta, loadChildMeta, primaryValueProp } = useHasMany(column as ColumnType)
await loadChildMeta()
/* // import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import { RelationTypes, UITypes, isSystemColumn } from 'nocodb-sdk'
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 ListChildItems from '~/components/project/spreadsheet/components/virtualCell/components/ListChildItems'
import listChildItemsModal from '~/components/project/spreadsheet/components/virtualCell/components/ListChildItemsModal'
import { parseIfInteger } from '@/helpers'
import ItemChip from '~/components/project/spreadsheet/components/virtualCell/components/ItemChip'
// todo: handling add new record for new row
export default {
name: 'HasManyCell',
components: {
ListChildItems,
ItemChip,
ListItems,
Pagination,
DlgLabelSubmitCancel,
ListChildItemsModal: listChildItemsModal,
},
props: {
isLocked: Boolean,
breadcrumbs: {
type: Array,
default() {
return []
},
},
value: [Object, Array],
meta: [Object],
nodes: [Object],
row: [Object],
active: Boolean,
isNew: Boolean,
isForm: Boolean,
required: Boolean,
isPublic: Boolean,
metas: Object,
password: String,
column: Object,
},
data: () => ({
newRecordModal: false,
childListModal: false,
// childMeta: null,
dialogShow: false,
confirmAction: null,
confirmMessage: '',
selectedChild: null,
expandFormModal: false,
isNewChild: false,
localState: [],
}),
computed: {
childMeta() {
return this.metas
? this.metas[this.column.colOptions.fk_related_model_id]
: this.$store.state.meta.metas[this.column.colOptions.fk_related_model_id]
},
// todo : optimize
childApi() {},
childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find((c) => c.pv) || {}).title
},
primaryCol() {
return this.meta && (this.meta.columns.find((c) => c.pv) || {}).title
},
childPrimaryKey() {
return this.childMeta && (this.childMeta.columns.find((c) => c.pk) || {}).title
},
childForeignKey() {
return (
this.childMeta && (this.childMeta.columns.find((c) => c.id === this.column.colOptions.fk_child_column_id) || {}).title
)
},
childForeignKeyVal() {
return this.meta && this.meta.columns
? this.meta.columns
.filter((c) => c.title === this.childForeignKey)
.map((c) => this.row[c.title] || '')
.join('___')
: ''
},
isVirtualRelation() {
return this.column && this.column.colOptions.virtual // (this.childMeta && (!!this.childMeta.columns.find(c => c.column_name === this.hm.column_name && this.hm.type === 'virtual'))) || false
},
isByPass() {
if (this.isVirtualRelation) {
return false
}
// if child fk references a column in parent which is not pk,
// then this column has to be filled
// if (((this.meta && this.meta.columns.find(c => !c.pk && c.id === this.hm.rcn)) || false)) {
// return this.childForeignKeyVal === ''
// }
if ((this.meta && this.meta.columns.find((c) => !c.pk && c.id === this.column.fk_parent_column_id)) || false) {
return this.childForeignKeyVal === ''
}
return false
},
disabledChildColumns() {
return { [this.childForeignKey]: true }
},
// todo:
form() {
return this.selectedChild && !this.isPublic
? () => import('~/components/project/spreadsheet/components/ExpandedForm')
: 'span'
},
childAvailableColumns() {
if (!this.childMeta) {
return []
}
const columns = []
if (this.childMeta.columns) {
columns.push(...this.childMeta.columns.filter((c) => !isSystemColumn(c)))
}
return columns
},
childQueryParams() {
if (!this.childMeta) {
return {}
}
// todo: use reduce
return {
hm:
(this.childMeta &&
this.childMeta.v &&
this.childMeta.v
.filter((v) => v.hm)
.map(({ hm }) => hm.table_name)
.join()) ||
'',
bt:
(this.childMeta &&
this.childMeta.v &&
this.childMeta.v
.filter((v) => v.bt)
.map(({ bt }) => bt.rtn)
.join()) ||
'',
mm:
(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.title === this.childForeignKey)
.map((c) => this.row[c.title] || '')
.join('___') ||
this.meta.columns
.filter((c) => c.pk)
.map((c) => this.row[c.title])
.join('___'))) ||
''
)
},
},
watch: {
isNew(n, o) {
if (!n && o) {
this.saveLocalState()
}
},
},
async mounted() {
await this.loadChildMeta()
if (this.isNew && this.value) {
this.localState = [...this.value]
}
},
created() {
this.loadChildMeta()
},
methods: {
onChildSave() {
if (this.isNew) {
this.addChildToParent(this.selectedChild)
} else {
this.$emit('loadTableData')
}
},
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.title])
.join('___')
try {
await this.$api.data.delete(this.childMeta.id, id)
this.dialogShow = false
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
this.$refs.childList.loadData()
}
} catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
}
}
},
async unlinkChild(child) {
if (this.isNew) {
this.localState.splice(this.localState.indexOf(child), 1)
this.$emit('update:localState', [...this.localState])
return
}
await this.loadChildMeta()
const column = this.childMeta.columns.find((c) => c.id === this.column.colOptions.fk_child_column_id)
if (column.rqd) {
this.$toast.info('Unlink is not possible, instead add to another record.').goAway(3000)
return
}
const id = this.childMeta.columns
.filter((c) => c.pk)
.map((c) => child[c.title])
.join('___')
await this.$api.dbTableRow.nestedRemove(
NOCO,
this.projectName,
this.meta.title,
this.parentId,
RelationTypes.HAS_MANY,
this.column.title,
id,
)
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,
id: this.column.colOptions.fk_related_model_id,
})
}
},
async showNewRecordModal() {
await this.loadChildMeta()
this.newRecordModal = true
},
async addChildToParent(child) {
if (this.isNew && this.localState.every((it) => it[this.childForeignKey] !== child[this.childPrimaryKey])) {
this.localState.push(child)
this.$emit('update:localState', [...this.localState])
this.$emit('saveRow')
this.newRecordModal = false
return
}
const id = this.childMeta.columns
.filter((c) => c.pk)
.map((c) => child[c.title])
.join('___')
this.newRecordModal = false
await this.$api.dbTableRow.nestedAdd(NOCO, this.projectName, this.meta.title, this.parentId, 'hm', this.column.title, id)
this.$emit('loadTableData')
if ((this.childListModal || this.isForm) && this.$refs.childList) {
await this.$refs.childList.loadData()
}
},
async editChild(child) {
await this.loadChildMeta()
this.isNewChild = false
this.expandFormModal = true
this.selectedChild = child
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.childMeta.columns.find(
(c) =>
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions &&
this.column.colOptions &&
c.colOptions.fk_child_column_id === this.column.colOptions.fk_child_column_id &&
c.colOptions.fk_parent_column_id === this.column.colOptions.fk_parent_column_id &&
c.colOptions.type === RelationTypes.BELONGS_TO,
) || {}
).title]: this.row,
}
this.expandFormModal = true
if (!this.isNew) {
setTimeout(() => {
this.$refs.expandedForm &&
this.$refs.expandedForm.$set(this.$refs.expandedForm.changedColumns, this.childForeignKey, true)
}, 500)
}
},
getCellValue(cellObj) {
if (cellObj) {
if (this.childMeta && this.childPrimaryCol) {
return cellObj[this.childPrimaryCol]
}
return Object.values(cellObj)[1]
}
},
async saveLocalState(row) {
let child
// eslint-disable-next-line no-cond-assign
while ((child = this.localState.pop())) {
if (row) {
const pid = this.meta.columns
.filter((c) => c.pk)
.map((c) => row[c.title])
.join('___')
const id = this.childMeta.columns
.filter((c) => c.pk)
.map((c) => child[c.title])
.join('___')
await this.$api.dbTableRow.nestedAdd(NOCO, this.projectName, this.meta.title, pid, 'hm', this.column.title, id)
} else {
await this.addChildToParent(child)
}
}
this.$emit('newRecordsSaved')
},
},
} */
const cellValue = inject(CellValueInj)
const row = inject(RowInj)
const reloadTrigger = inject(ReloadViewDataHookInj)
const listItemsDlg = ref(false)
const childListDlg = ref(false)
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Required<ColumnType>,
row,
() => reloadTrigger?.trigger(),
)
await loadRelatedTableMeta()
</script>
<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 flex-nowrap">
<template v-if="value || localState">
<ItemChip v-for="(ch, i) in value || localState" :key="i" :value="ch[primaryValueProp]" />
<!--
:active="active" :item="ch"
:value="getCellValue(ch)"
:readonly="isLocked || isPublic"
@edit="editChild"
@unlink="unlinkChild " -->
<!-- <span
v-if="!isLocked && value && value.length === 10"
class="caption pointer ml-1 grey&#45;&#45;text"
@click="showChildListModal"
>more...
</span> -->
<div class="flex align-center gap-1 w-full min-full chips-wrapper">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cellValue">
<ItemChip v-for="(ch, i) in cellValue" :key="i" :value="ch[relatedTablePrimaryValueProp]" @unlink="unlink(ch)" />
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template>
</div>
<!-- <div -->
<!-- v-if="!isLocked" -->
<!-- class="actions align-center justify-center px-1 flex-shrink-1" -->
<!-- :class="{ 'd-none': !active, 'd-flex': active }" -->
<!-- > -->
<!-- <x-icon -->
<!-- v-if="_isUIAllowed('xcDatatableEditable') && (isForm || !isPublic)" -->
<!-- small -->
<!-- :color="['primary', 'grey']" -->
<!-- @click="showNewRecordModal" -->
<!-- > -->
<!-- mdi-plus -->
<!-- </x-icon> -->
<!-- <x-icon x-small :color="['primary', 'grey']" class="ml-2" @click="showChildListModal"> mdi-arrow-expand </x-icon> -->
<!-- </div> -->
<!-- </template> -->
<!-- <ListItems
v-if="newRecordModal"
v-model="newRecordModal"
:size="10"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
:api="childApi"
:parent-meta="meta"
:column="column"
:query-params="{
...childQueryParams,
// check if it needs to bypass to
// avoid foreign key constraint violation in real relation
isByPass,
where:
// show all for new record
isNew
? null
: // filter out those selected items
`~not(${childForeignKey},eq,${parentId})` +
// allow the child with empty key
`~or(${childForeignKey},is,null)`,
}"
:is-public="isPublic"
:password="password"
:row-id="parentId"
@add-new-record="insertAndAddNewChildRecord"
@add="addChildToParent"
/>
<ListChildItems
:is="isForm ? 'list-child-items' : 'list-child-items-modal'"
v-if="childMeta && (childListModal || isForm)"
ref="childList"
v-model="childListModal"
v-model:local-state="localState"
:is-form="isForm"
:is-new="isNew"
:size="10"
:meta="childMeta"
:parent-meta="meta"
:password="password"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
:api="childApi"
:column="column"
:query-params="{
...childQueryParams,
where: `(${childForeignKey},eq,${parentId})`,
}"
:is-public="isPublic"
:row-id="parentId"
type="hm"
@new-record="showNewRecordModal"
@edit="editChild"
@unlink="unlinkChild"
@delete="deleteChild"
/>
<DlgLabelSubmitCancel
v-if="dialogShow"
type="primary"
:actions-mtd="confirmAction"
:dialog-show="dialogShow"
:heading="confirmMessage"
/>
<v-dialog v-model="expandFormModal" :overlay-opacity="0.8" width="1000px" max-width="100%" class="mx-auto">
<component
:is="form"
v-if="selectedChild"
ref="expandedForm"
v-model="selectedChild"
:db-alias="nodes.dbAlias"
:has-many="childMeta.hasMany"
:belongs-to="childMeta.belongsTo"
:table="childMeta.table_name"
v-model:is-new="isNewChild"
:old-row="{ ...selectedChild }"
:meta="childMeta"
:primary-value-column="childPrimaryCol"
:api="childApi"
:available-columns="childAvailableColumns"
icon-color="warning"
:nodes="nodes"
:query-params="childQueryParams"
:disabled-columns="disabledChildColumns"
:breadcrumbs="breadcrumbs"
@cancel="
selectedChild = null
expandFormModal = false
"
@input="onChildSave"
/>
</v-dialog>
-->
<div class="flex-grow flex justify-end gap-1">
<MdiExpandIcon
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="childListDlg = true"
/>
<MdiPlusIcon class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
</div>
<ListItems v-model="listItemsDlg" />
<ListChildItems v-model="childListDlg" @attach-record=";(childListDlg = false), (listItemsDlg = true)" />
</div>
</template>
<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: 0.5em;
}
&:hover .primary-key {
display: inline;
}
}
.child-card {
cursor: pointer;
&:hover {
box-shadow: 0 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;
}
}
<style scoped>
.nc-action-icon {
@apply hidden cursor-pointer;
}
.chips-wrapper {
.chips {
max-width: 100%;
}
&.active {
.chips {
max-width: calc(100% - 44px);
}
}
.chips-wrapper:hover .nc-action-icon {
@apply flex;
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@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/>.
*
*/
-->

6
packages/nc-gui-v2/components/virtual-cell/Lookup.vue

@ -2,7 +2,7 @@
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { useColumn } from '~/composables'
import { ColumnInj, MetaInj, ReadonlyInj, ValueInj } from '~/context'
import { CellValueInj, ColumnInj, MetaInj, ReadonlyInj } from '~/context'
const { metas, getMeta } = useMetas()
@ -10,8 +10,8 @@ provide(ReadonlyInj, true)
const column = inject(ColumnInj) as ColumnType & { colOptions: LookupType }
const meta = inject(MetaInj)
const value = inject(ValueInj)
const arrValue = Array.isArray(value) ? value : [value]
const value = inject(CellValueInj)
const arrValue = computed(() => (Array.isArray(value?.value) ? value?.value : [value?.value]))
const relationColumn = meta?.value.columns?.find((c) => c.id === column.colOptions.fk_relation_column_id) as ColumnType & {
colOptions: LinkToAnotherRecordType

622
packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue

@ -1,611 +1,55 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import ItemChip from './components/ItemChip.vue'
import { ColumnInj, ValueInj } from '~/context'
import { useManyToMany } from '#imports'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
import { useProvideLTARStore } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiExpandIcon from '~icons/mdi/arrow-expand'
import MdiPlusIcon from '~icons/mdi/plus'
const column = inject(ColumnInj)
const value = inject(ValueInj)
const active = false
const isLocked = false
const row = inject(RowInj)
const cellValue = inject(CellValueInj)
const reloadTrigger = inject(ReloadViewDataHookInj)
const { childMeta, loadChildMeta, primaryValueProp } = useManyToMany(column as ColumnType)
await loadChildMeta()
const listItemsDlg = ref(false)
const childListDlg = ref(false)
/* import { RelationTypes, UITypes, isSystemColumn } from 'nocodb-sdk'
import DlgLabelSubmitCancel from '~/components/utils/DlgLabelSubmitCancel'
import ListItems from '~/components/project/spreadsheet/components/virtualCell/components/ListItems'
import ListChildItems from '~/components/project/spreadsheet/components/virtualCell/components/ListChildItems'
import listChildItemsModal from '~/components/project/spreadsheet/components/virtualCell/components/ListChildItemsModal'
import { parseIfInteger } from '@/helpers'
import ItemChip from '~/components/project/spreadsheet/components/virtualCell/components/ItemChip'
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Required<ColumnType>,
row,
() => reloadTrigger?.trigger(),
)
export default {
name: 'ManyToManyCell',
components: { ListChildItems, ItemChip, ListItems, DlgLabelSubmitCancel, ListChildItemsModal: listChildItemsModal },
props: {
isLocked: Boolean,
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,
required: Boolean,
isPublic: Boolean,
metas: Object,
password: String,
column: Object,
},
data: () => ({
isNewChild: false,
newRecordModal: false,
childListModal: false,
// childMeta: null,
// assocMeta: null,
childList: null,
dialogShow: false,
confirmAction: null,
confirmMessage: '',
selectedChild: null,
expandFormModal: false,
localState: [],
}),
computed: {
getCellValue() {
return (cellObj) => {
if (cellObj) {
if (this.childPrimaryCol) {
return cellObj[this.childPrimaryCol]
}
return Object.values(cellObj)[1]
}
}
},
childMeta() {
return this.metas
? this.metas[this.column.colOptions.fk_related_model_id]
: this.$store.state.meta.metas[this.column.colOptions.fk_related_model_id]
},
assocMeta() {
return this.metas
? this.metas[this.column.colOptions.fk_mm_model_id]
: this.$store.state.meta.metas[this.column.colOptions.fk_mm_model_id]
},
// todo : optimize
childApi() {
// return this.childMeta && this.$ncApis.get({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// id: this.column.colOptions.fk_related_model_id
// })
//
// return this.childMeta && this.childMeta.title
// ? ApiFactory.create(
// this.$store.getters['project/GtrProjectType'],
// this.childMeta.title,
// this.childMeta.columns,
// this,
// this.childMeta
// )
// : null
},
// todo : optimize
assocApi() {
// return this.childMeta && this.$ncApis.get({
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias,
// id: this.column.colOptions.fk_mm_model_id
// })
// return this.assocMeta && this.assocMeta.title
// ? ApiFactory.create(
// this.$store.getters['project/GtrProjectType'],
// this.assocMeta.title,
// this.assocMeta.columns,
// this,
// this.assocMeta
// )
// : null
},
childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find((c) => c.pv) || {}).title
},
childPrimaryKey() {
return this.childMeta && (this.childMeta.columns.find((c) => c.pk) || {}).title
},
parentPrimaryKey() {
return this.meta && (this.meta.columns.find((c) => c.pk) || {}).title
},
childQueryParams() {
if (!this.childMeta) {
return {}
}
// todo: use reduce
return {
hm:
(this.childMeta &&
this.childMeta.v &&
this.childMeta.v
.filter((v) => v.hm)
.map(({ hm }) => hm.table_name)
.join()) ||
'',
bt:
(this.childMeta &&
this.childMeta.v &&
this.childMeta.v
.filter((v) => v.bt)
.map(({ bt }) => bt.rtn)
.join()) ||
'',
mm:
(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.table_name]: {
// relationType: 'hm',
// [this.assocMeta.columns.find(c => c.column_name === this.mm.vcn).column_name]: {
// eq: this.row[this.parentPrimaryKey]
// }
// }
// }
},
childAvailableColumns() {
if (!this.childMeta) {
return []
}
const columns = []
if (this.childMeta.columns) {
columns.push(...this.childMeta.columns.filter((c) => !isSystemColumn(c)))
}
return columns
},
// todo:
form() {
return this.selectedChild && !this.isPublic
? () => import('~/components/project/spreadsheet/components/ExpandedForm')
: 'span'
},
},
watch: {
async isNew(n, o) {
if (!n && o) {
await this.saveLocalState()
}
},
},
async mounted() {
if (this.isForm) {
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()])
}
if (this.isNew && this.value) {
this.localState = [...this.value]
}
},
created() {
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)
this.$emit('update:localState', [...this.localState])
return
}
await Promise.all([this.loadChildMeta(), this.loadAssociateTableMeta()])
const cid = this.childMeta.columns
.filter((c) => c.pk)
.map((c) => child[c.title])
.join('___')
const pid = this.meta.columns
.filter((c) => c.pk)
.map((c) => this.row[c.title])
.join('___')
await this.$api.dbTableRow.nestedRemove(NOCO, this.projectName, this.meta.title, pid, 'mm', this.column.title, cid)
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.title])
.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,
id: this.column.colOptions.fk_related_model_id,
})
// 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,
id: this.column.colOptions.fk_mm_model_id,
})
// 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.every((it) => it[this.childForeignKey] !== child[this.childPrimaryKey])) {
this.localState.push(child)
this.$emit('update:localState', [...this.localState])
this.$emit('saveRow')
this.newRecordModal = false
return
}
const cid = this.childMeta.columns
.filter((c) => c.pk)
.map((c) => child[c.title])
.join('___')
const pid = this.meta.columns
.filter((c) => c.pk)
.map((c) => this.row[c.title])
.join('___')
// const vcidCol = this.assocMeta.columns.find(c => c.id === this.column.colOptions.fk_mm_parent_column_id).title
// const vpidCol = this.assocMeta.columns.find(c => c.id === this.column.colOptions.fk_mm_child_column_id).title
await this.$api.dbTableRow.nestedAdd(NOCO, this.projectName, this.meta.title, pid, 'mm', this.column.title, cid)
try {
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.childMeta.columns.find(
(c) =>
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions &&
this.column.colOptions &&
c.colOptions.fk_child_column_id === this.column.colOptions.fk_parent_column_id &&
c.colOptions.fk_parent_column_id === this.column.colOptions.fk_child_column_id &&
c.colOptions.fk_mm_model_id === this.column.colOptions.fk_mm_model_id &&
c.colOptions.type === RelationTypes.MANY_TO_MANY,
) || {}
).title]: [this.row],
}
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
// eslint-disable-next-line no-cond-assign
while ((child = this.localState.pop())) {
if (row) {
const cid = this.childMeta.columns
.filter((c) => c.pk)
.map((c) => child[c.title])
.join('___')
const pid = this.meta.columns
.filter((c) => c.pk)
.map((c) => row[c.title])
.join('___')
await this.$api.dbTableRow.nestedAdd(NOCO, this.projectName, this.meta.title, pid, 'mm', this.column.title, cid)
} else {
await this.addChildToParent(child)
}
}
this.$emit('newRecordsSaved')
},
},
} */
await loadRelatedTableMeta()
</script>
<template>
<div class="d-flex d-100 chips-wrapper" :class="{ active }">
<div class="flex align-center gap-1 w-full h-full chips-wrapper">
<!-- <template v-if="!isForm"> -->
<div class="chips d-flex align-center img-container flex-grow-1 hm-items flex-nowrap">
<template v-if="value || localState">
<ItemChip v-for="(v, j) in value || localState" :key="j" :item="v" :value="v[primaryValueProp]" />
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cellValue">
<ItemChip v-for="(ch, i) in cellValue" :key="i" :value="ch[relatedTablePrimaryValueProp]" @unlink="unlink(ch)" />
<!-- :active="active"
:readonly="isLocked || isPublic"
@edit="editChild"
@unlink="unlinkChild" -->
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template>
<span v-if="!isLocked && value && value.length === 10" class="caption pointer ml-1 grey--text" @click="showChildListModal"
>more...</span
>
</div>
<!-- <div -->
<!-- v-if="!isLocked" -->
<!-- class="actions align-center justify-center px-1 flex-shrink-1" -->
<!-- :class="{ 'd-none': !active, 'd-flex': active }" -->
<!-- > -->
<!-- <x-icon -->
<!-- v-if="_isUIAllowed('xcDatatableEditable') && (isForm || !isPublic)" -->
<!-- small -->
<!-- :color="['primary', 'grey']" -->
<!-- @click="showNewRecordModal" -->
<!-- > -->
<!-- mdi-plus -->
<!-- </x-icon> -->
<!-- <x-icon x-small :color="['primary', 'grey']" class="ml-2" @click="showChildListModal"> mdi-arrow-expand </x-icon> -->
<!-- </div> -->
<!-- </template> -->
<!-- <ListItems
v-if="newRecordModal"
v-model="newRecordModal"
:hm="true"
:size="10"
:column="column"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
:parent-meta="meta"
:api="api"
:mm="mm"
:tn="mm && mm.rtn"
:parent-id="row && row[parentPrimaryKey]"
:is-public="isPublic"
:query-params="childQueryParams"
:password="password"
:row-id="row && row[parentPrimaryKey]"
@add-new-record="insertAndAddNewChildRecord"
@add="addChildToParent"
/>
<ListChildItems
:is="isForm ? 'list-child-items' : 'list-child-items-modal'"
v-if="childMeta && assocMeta && (isForm || childListModal)"
ref="childList"
v-model="childListModal"
:is-form="isForm"
: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"
:is-public="isPublic"
:row-id="row && row[parentPrimaryKey]"
:column="column"
type="mm"
:password="password"
@new-record="showNewRecordModal"
@edit="editChild"
@unlink="unlinkChild"
/>
<DlgLabelSubmitCancel
v-if="dialogShow"
type="primary"
:actions-mtd="confirmAction"
:dialog-show="dialogShow"
:heading="confirmMessage"
/>
&lt;!&ndash; todo : move to list item component &ndash;&gt;
<v-dialog
v-if="selectedChild && !isPublic"
v-model="expandFormModal"
:overlay-opacity="0.8"
width="1000px"
max-width="100%"
class="mx-auto"
>
<component
:is="form"
v-if="selectedChild"
ref="expandedForm"
v-model="selectedChild"
:db-alias="nodes.dbAlias"
:has-many="childMeta.hasMany"
:belongs-to="childMeta.belongsTo"
v-model:is-new="isNewChild"
:table="childMeta.table_name"
:old-row="{ ...selectedChild }"
:meta="childMeta"
:primary-value-column="childPrimaryCol"
:available-columns="childAvailableColumns"
icon-color="warning"
:nodes="nodes"
:query-params="childQueryParams"
:breadcrumbs="breadcrumbs"
@cancel="
selectedChild = null
expandFormModal = false
"
@input="onChildSave"
/>
</v-dialog> -->
<div class="flex-1 flex justify-end gap-1">
<MdiExpandIcon class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="childListDlg = true" />
<MdiPlusIcon class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
</div>
<ListItems v-model="listItemsDlg" />
<ListChildItems v-model="childListDlg" @attach-record=";(childListDlg = false), (listItemsDlg = true)" />
</div>
</template>
<style scoped lang="scss">
.items-container {
overflow-x: visible;
max-height: min(500px, 60vh);
overflow-y: auto;
<style scoped>
.nc-action-icon {
@apply hidden cursor-pointer;
}
.primary-value {
.primary-key {
display: none;
margin-left: 0.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 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);
}
}
.chips-wrapper:hover .nc-action-icon {
@apply inline-block;
}
</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-v2/components/virtual-cell/Rollup.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ValueInj } from '~/context'
import { CellValueInj } from '~/context'
const value = inject(ValueInj)
const value = inject(CellValueInj)
</script>
<template>

16
packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue

@ -1,25 +1,25 @@
<script setup lang="ts">
import { ReadonlyInj } from '~/context'
import { ActiveCellInj, ReadonlyInj } from '~/context'
import MdiCloseThickIcon from '~icons/mdi/close-thick'
interface Props {
value?: string | number | boolean
active?: boolean
item?: any
}
const { value, active, item } = defineProps<Props>()
const { value, item } = defineProps<Props>()
const emit = defineEmits(['unlink'])
const readonly = inject(ReadonlyInj, false)
const active = inject(ActiveCellInj, false)
</script>
<template>
<v-chip class="chip" :class="{ active }" small text-color="textColor">
<div class="group py-1 px-2 flex align-center gap-1 bg-gray-200/50 hover:bg-gray-200 rounded-[20px]" :class="{ active }">
<span class="name">{{ value }}</span>
<div v-show="active" v-if="!readonly" class="mr-n1 ml-2">
<MdiCloseThickIcon class="unlink-icon" />
<div v-show="active" v-if="!readonly" class="flex align-center">
<MdiCloseThickIcon class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click="emit('unlink')" />
</div>
</v-chip>
</div>
</template>
<style scoped lang="scss">

310
packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue

@ -1,254 +1,84 @@
<script>
import { RelationTypes } from 'nocodb-sdk'
import { NOCO } from '~/lib/constants'
import Pagination from '~/components/project/spreadsheet/components/Pagination'
<script lang="ts" setup>
import { useLTARStoreOrThrow, useVModel } from '#imports'
import MdiReloadIcon from '~icons/mdi/reload'
import MdiDeleteIcon from '~icons/mdi/delete-outline'
import MdiUnlinkIcon from '~icons/mdi/link-variant-remove'
export default {
name: 'ListChildItems',
components: { Pagination },
props: {
readOnly: Boolean,
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],
isPublic: Boolean,
rowId: [String, Number],
column: Object,
type: String,
password: String,
},
emits: ['input', 'edit', 'delete', 'unlink', 'newRecord'],
data: () => ({
RelationTypes,
data: null,
page: 1,
}),
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()
},
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
if (!this.isForm && this.isPublic && this.$route.params.id) {
if (this.column && this.column.colOptions && this.rowId) {
this.data = await this.$api.public.dataNestedList(
this.$route.params.id,
this.rowId,
this.column.colOptions.type,
this.column.fk_column_id || this.column.id,
{
limit: this.size,
offset: this.size * (this.page - 1),
},
{},
)
}
const props = defineProps<{ modelValue?: boolean }>()
const emit = defineEmits(['update:modelValue', 'attachRecord'])
return
}
const vModel = useVModel(props, 'modelValue', emit)
if (this.isNew) {
return
}
if (this.column && this.column.colOptions) {
this.data = await this.$api.dbTableRow.nestedList(
NOCO,
this.projectName,
this.parentMeta.title,
this.rowId,
this.column.colOptions.type,
this.column.title,
{
limit: this.size,
offset: this.size * (this.page - 1),
},
)
} else {
this.data = await this.$api.dbTableRow.list(NOCO, this.projectName, this.meta.title, {
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams,
})
}
},
},
const {
childrenList,
meta,
deleteRelatedRow,
loadChildrenList,
childrenListPagination,
relatedTablePrimaryValueProp,
unlink,
getRelatedTableRowId,
} = useLTARStoreOrThrow()
watch(vModel, () => {
if (vModel.value) {
loadChildrenList()
}
})
const unlinkRow = async (row: Record<string, any>) => {
await unlink(row)
await loadChildrenList()
}
</script>
<template>
<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.title : 'Children' }}</span>
<v-spacer />
<v-icon small class="mr-1" @click="loadData()"> mdi-reload </v-icon>
<v-btn
v-if="(isForm || !isPublic) && !readOnly && (isPublic || _isUIAllowed('xcDatatableEditable'))"
small
class="caption"
color="primary"
@click="$emit('newRecord')"
>
<v-icon small> mdi-link </v-icon>&nbsp; Link to '{{ meta.title }}'
</v-btn>
</v-card-title>
<v-card-text>
<div class="items-container pt-2 mb-n4" :class="{ 'mx-n2': isForm }">
<div v-if="!readOnly && (isPublic || _isUIAllowed('xcDatatableEditable'))" class="text-right mb-2 mt-n2 mx-2">
<v-btn v-if="isForm" x-small class="caption" color="primary" outlined @click="$emit('newRecord')">
<v-icon x-small> mdi-link </v-icon>&nbsp; Link to '{{ meta.title }}'
</v-btn>
</div>
<template v-if="isDataAvail">
<v-card
v-for="(ch, i) in (data && data.list) || localState"
:key="i"
class="mx-2 mb-2 child-list-modal child-card"
outlined
@click="!readOnly && $emit('edit', ch)"
>
<div class="remove-child-icon d-flex align-center">
<x-icon
v-if="((isPublic && isForm) || (!isPublic && _isUIAllowed('xcDatatableEditable'))) && !readOnly"
:tooltip="`Unlink this '${meta.title}' from '${parentMeta.title}'`"
:color="['error', 'grey']"
small
class="mr-1 mt-n1"
@click.stop="$emit('unlink', ch, i)"
>
mdi-link-variant-remove
</x-icon>
<x-icon
v-if="!isPublic && type === RelationTypes.HAS_MANY && !readOnly && _isUIAllowed('xcDatatableEditable')"
:tooltip="`Delete row in '${meta.title}'`"
:color="['error', 'grey']"
small
@click.stop="$emit('delete', ch, i)"
>
mdi-delete-outline
</x-icon>
<a-modal v-model:visible="vModel" :footer="null" title="Child list">
<div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col">
<div class="flex mb-4 align-center gap-2">
<!-- <a-input v-model:value="childrenListPagination.query" class="max-w-[200px]" size="small"></a-input> -->
<div class="flex-1" />
<MdiReloadIcon class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<a-button type="primary" size="small" @click="emit('attachRecord')">
<div class="flex align-center gap-1">
<MdiUnlinkIcon class="text-xs text-white" @click="unlinkRow(row)" />
Link to '{{ meta.title }}'
</div>
</a-button>
</div>
<template v-if="childrenList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0">
<a-card v-for="(row, i) of childrenList?.list ?? []" :key="i" class="ma-2 hover:(!bg-gray-200/50 shadow-md)">
<div class="flex align-center">
<div class="flex-grow overflow-hidden min-w-0">
{{ row[relatedTablePrimaryValueProp]
}}<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div>
<div class="flex-1"></div>
<div class="flex gap-2">
<MdiUnlinkIcon class="text-xs text-grey hover:(!text-red-500) cursor-pointer" @click="unlinkRow(row)" />
<MdiDeleteIcon class="text-xs text-grey hover:(!text-red-500) cursor-pointer" @click="deleteRelatedRow(row)" />
</div>
</div>
<v-card-title class="primary-value textColor--text text--lighten-2">
{{ ch[primaryCol] }}
<span v-if="primaryKey" class="grey--text caption primary-key ml-1"> (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.pageInfo && data.pageInfo.totalRows > 1"
v-model="page"
:size="size"
:count="data && data.pageInfo && data.pageInfo.totalRows"
@input="loadData"
/>
</a-card>
</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.pageInfo && data.pageInfo.totalRows > 1"
v-model="page"
:size="size"
:count="data && data.pageInfo && data.pageInfo.totalRows"
class="mb-3"
@input="loadData"
/>
</v-card-actions>
</v-card>
<!-- </v-dialog> -->
<a-pagination
v-if="childrenList?.pageInfo"
v-model:current="childrenListPagination.page"
v-model:page-size="childrenListPagination.size"
class="mt-2 mx-auto"
size="small"
:total="childrenList.pageInfo.totalRows"
show-less-items
/>
</template>
<a-empty v-else class="my-10" />
</div>
</a-modal>
</template>
<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;
:deep(.ant-pagination-item a) {
line-height: 21px !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/>.
*
*/
-->

126
packages/nc-gui-v2/components/virtual-cell/components/ListChildItemsModal.vue

@ -1,126 +0,0 @@
<script>
import ListChildItems from './ListChildItems.vue'
export default {
name: 'ListChildItemsModal',
components: { ListChildItems },
props: {
type: String,
readOnly: Boolean,
localState: Array,
isNew: Boolean,
password: String,
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],
isPublic: Boolean,
rowId: [String, Number],
column: Object,
},
emits: ['input'],
data: () => ({
data: null,
page: 1,
}),
computed: {
show: {
set(v) {
this.$emit('input', v)
},
get() {
return this.value
},
},
},
mounted() {},
methods: {
async loadData() {
if (this.$refs && this.$refs.child) {
await this.$refs.child.loadData()
}
},
},
}
</script>
<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>
<ListChildItems
v-if="show"
ref="child"
:type="type"
:row-id="rowId"
:local-state="localState"
:is-new="isNew"
:size="10"
:meta="meta"
:password="password"
:parent-meta="parentMeta"
:primary-col="primaryCol"
:primary-key="primaryKey"
:api="api"
:query-params="queryParams"
v-bind="$attrs"
:read-only="readOnly"
:is-public="isPublic"
:column="column"
/>
</v-dialog>
</template>
<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/>.
*
*/
-->

305
packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue

@ -1,250 +1,79 @@
<script>
import Pagination from '~/components/project/spreadsheet/components/Pagination'
import { NOCO } from '~/lib/constants'
export default {
name: 'ListItems',
components: { Pagination },
props: {
value: Boolean,
tn: String,
hm: [Object, Function, Boolean],
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],
isPublic: Boolean,
password: String,
column: Object,
rowId: [Number, String],
},
emits: ['input', 'add', 'addNewRecord'],
data: () => ({
data: null,
page: 1,
query: '',
}),
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).title
},
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
if (this.isPublic) {
this.data = await this.$api.public.dataRelationList(
this.$route.params.id,
this.column.id,
{},
{
headers: {
'xc-password': this.password,
},
query: {
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams,
},
},
)
} else {
const where = `(${this.primaryCol},like,%${this.query}%)`
<script lang="ts" setup>
import { useLTARStoreOrThrow, useVModel } from '#imports'
import MdiReloadIcon from '~icons/mdi/reload'
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emit)
const {
childrenExcludedList,
loadChildrenExcludedList,
childrenExcludedListPagination,
relatedTablePrimaryValueProp,
link,
getRelatedTableRowId,
} = useLTARStoreOrThrow()
watch(vModel, () => {
if (vModel.value) {
loadChildrenExcludedList()
}
})
if (this.column && this.column.colOptions && this.rowId) {
this.data = await this.$api.dbTableRow.nestedChildrenExcludedList(
NOCO,
this.projectName,
this.parentMeta.title,
this.rowId,
this.column.colOptions.type,
this.column.title,
{
limit: this.size,
offset: this.size * (this.page - 1),
where: this.query && `(${this.primaryCol},like,${this.query})`,
},
)
} else {
this.data = await this.$api.dbTableRow.list(NOCO, this.projectName, this.meta.title, {
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams,
where,
})
}
}
},
},
const linkRow = async (row: Record<string, any>) => {
await link(row)
vModel.value = false
// await loadChildrenExcludedList()
}
</script>
<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
v-model="query"
hide-details
dense
outlined
<a-modal v-model:visible="vModel" :footer="null" title="Link Record">
<div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col">
<div class="flex mb-4 align-center gap-2">
<a-input
v-model:value="childrenExcludedListPagination.query"
placeholder="Filter query"
class="caption search-field ml-2"
@keydown.enter="loadData"
>
<template #append>
<x-icon tooltip="Apply filter" small icon class="mt-1" @click="loadData"> mdi-keyboard-return </x-icon>
</template>
</v-text-field>
<v-spacer />
<v-icon small class="mr-1" @click="loadData()"> mdi-reload </v-icon>
<v-btn v-if="!isPublic" small class="caption mr-2" color="primary" @click="$emit('addNewRecord')">
<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" :key="i" v-ripple class="ma-2 child-card" outlined @click="$emit('add', ch)">
<v-card-text class="primary-value textColor--text text--lighten-2 d-flex">
<span class="font-weight-bold"> {{ ch[primaryCol] || (ch && Object.values(ch).slice(0, 1).join()) }}&nbsp;</span>
<span v-if="primaryKey" class="grey--text caption primary-key">(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 -->
{{ $t('placeholder.noItemsFound') }}
</div>
class="max-w-[200px]"
size="small"
></a-input>
<div class="flex-1" />
<MdiReloadIcon class="cursor-pointer text-gray-500" @click="loadChildrenExcludedList" />
<a-button type="primary" size="small" @click="emit('addNewRecord')">Add new record</a-button>
</div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0">
<a-card
v-for="(row, i) in childrenExcludedList?.list ?? []"
:key="i"
class="ma-2 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group"
@click="linkRow(row)"
>
{{ row[relatedTablePrimaryValueProp]
}}<span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1"
>(Primary key : {{ getRelatedTableRowId(row) }})</span
>
</a-card>
</div>
</v-card-text>
<v-card-actions class="justify-center py-2 flex-column">
<Pagination
v-if="data && data.list && data.list.length"
v-model="page"
:size="size"
:count="data && data.pageInfo && data.pageInfo.totalRows"
class="mb-3"
@input="loadData"
<a-pagination
v-if="childrenExcludedList?.pageInfo"
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
class="mt-2 mx-auto !text-xs"
size="small"
:total="childrenExcludedList.pageInfo.totalRows"
show-less-items
/>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<a-empty v-else class="my-10" />
</div>
</a-modal>
</template>
<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 0.2em var(--v-textColor-lighten5);
}
}
.primary-value {
.primary-key {
display: none;
margin-left: 0.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 scoped>
:deep(.ant-pagination-item a) {
line-height: 21px !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/>.
*
*/
-->

4
packages/nc-gui-v2/composables/index.ts

@ -3,12 +3,9 @@ export * from './useGlobal'
export * from './useInjectionState'
export * from './useUIPermission'
export * from './useAttachment'
export * from './useBelongsTo'
export * from './useColors'
export * from './useColumn'
export * from './useGridViewColumnWidth'
export * from './useHasMany'
export * from './useManyToMany'
export * from './useMetas'
export * from './useProject'
export * from './useTable'
@ -21,3 +18,4 @@ export * from './useViewSorts'
export * from './useVirtualCell'
export * from './useColumnCreateStore'
export * from './useSmartsheetStore'
export * from './useLTARStore'

19
packages/nc-gui-v2/composables/useBelongsTo.ts

@ -1,19 +0,0 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import { useMetas } from './useMetas'
export function useBelongsTo(column: ColumnType) {
const { metas, getMeta } = useMetas()
const parentMeta = computed<TableType>(() => {
return metas.value?.[(column.colOptions as any)?.fk_related_model_id as string]
})
const loadParentMeta = async () => {
await getMeta((column.colOptions as any)?.fk_related_model_id as string)
}
const primaryValueProp = computed(() => {
return (parentMeta?.value?.columns?.find((c) => c.pv) || parentMeta?.value?.columns?.[0])?.title
})
return { parentMeta, loadParentMeta, primaryValueProp }
}

19
packages/nc-gui-v2/composables/useHasMany.ts

@ -1,19 +0,0 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import { useMetas } from './useMetas'
export function useHasMany(column: ColumnType) {
const { metas, getMeta } = useMetas()
const childMeta = computed<TableType>(() => {
return metas.value?.[(column.colOptions as any)?.fk_related_model_id as string]
})
const loadChildMeta = async () => {
await getMeta((column.colOptions as any)?.fk_related_model_id as string)
}
const primaryValueProp = computed(() => {
return (childMeta?.value?.columns?.find((c) => c.pv) || childMeta?.value?.columns?.[0])?.title
})
return { childMeta, loadChildMeta, primaryValueProp }
}

258
packages/nc-gui-v2/composables/useLTARStore.ts

@ -0,0 +1,258 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { Modal, notification } from 'ant-design-vue'
import { useInjectionState, useMetas, useProject } from '#imports'
import { NOCO } from '~/lib'
import type { Row } from '~/composables'
import { extractSdkResponseErrorMsg } from '~/utils'
interface DataApiResponse {
list: Record<string, any>
pageInfo: PaginatedType
}
/** Store for managing Link to another cells */
const [useProvideLTARStore, useLTARStore] = useInjectionState(
(column: Required<ColumnType>, row?: Ref<Row>, reloadData = () => {}) => {
// state
const { metas, getMeta } = useMetas()
const { project } = useProject()
const { $api } = useNuxtApp()
const childrenExcludedList: Ref<DataApiResponse | undefined> = ref()
const childrenList: Ref<DataApiResponse | undefined> = ref()
const childrenExcludedListPagination = reactive({
page: 1,
query: '',
size: 10,
})
const childrenListPagination = reactive({
page: 1,
query: '',
size: 10,
})
const colOptions = column.colOptions as LinkToAnotherRecordType
// getters
const meta = computed(() => metas?.value?.[column.fk_model_id as string])
const relatedTableMeta = computed<TableType>(() => {
return metas.value?.[(column.colOptions as any)?.fk_related_model_id as string]
})
const rowId = computed(() =>
meta.value.columns
.filter((c: Required<ColumnType>) => c.pk)
.map((c: Required<ColumnType>) => row?.value?.row?.[c.title])
.join('___'),
)
// actions
const getRelatedTableRowId = (row: Record<string, any>) => {
return relatedTableMeta.value?.columns
?.filter((c) => c.pk)
.map((c) => row?.[c.title as string])
.join('___')
}
const loadRelatedTableMeta = async () => {
await getMeta(colOptions?.fk_related_model_id as string)
}
const relatedTablePrimaryValueProp = computed(() => {
return (relatedTableMeta?.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
})
const primaryValueProp = computed(() => {
return (meta?.value?.columns?.find((c: Required<ColumnType>) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
})
const loadChildrenExcludedList = async () => {
try {
childrenExcludedList.value = await $api.dbTableRow.nestedChildrenExcludedList(
NOCO,
project.value.id as string,
meta.value.id,
rowId.value,
(column.colOptions as LinkToAnotherRecordType).type as 'mm' | 'hm',
column.title,
// todo: swagger type correction
{
limit: childrenExcludedListPagination.size,
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
} as any,
)
} catch (e: any) {
notification.error({
message: 'Failed to load list',
description: await extractSdkResponseErrorMsg(e),
})
}
}
const loadChildrenList = async () => {
try {
childrenList.value = await $api.dbTableRow.nestedList(
NOCO,
project.value.id as string,
meta.value.id,
rowId.value,
colOptions.type as 'mm' | 'hm',
column.title,
// todo: swagger type correction
{
limit: childrenListPagination.size,
offset: childrenListPagination.size * (childrenListPagination.page - 1),
where: childrenListPagination.query && `(${relatedTablePrimaryValueProp.value},like,${childrenListPagination.query})`,
} as any,
)
} catch (e: any) {
notification.error({
message: 'Failed to load children list',
description: await extractSdkResponseErrorMsg(e),
})
}
}
const deleteRelatedRow = async (row: Record<string, any>) => {
Modal.confirm({
title: 'Do you want to delete the record?',
type: 'warning',
onOk: async () => {
const id = getRelatedTableRowId(row)
try {
$api.dbTableRow.delete(NOCO, project.value.id as string, relatedTableMeta.value.id as string, id as string)
reloadData?.()
await loadChildrenList()
} catch (e: any) {
notification.error({
message: 'Delete failed',
description: await extractSdkResponseErrorMsg(e),
})
}
},
})
}
const unlink = async (row: Record<string, any>) => {
// const column = meta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id);
// todo: handle if new record
// if (this.isNew) {
// this.$emit('updateCol', this.row, _cn, null);
// this.localState = null;
// this.$emit('update:localState', this.localState);
// return;
// }
// todo: handle bt column if required
// if (column.rqd) {
// this.$toast.info('Unlink is not possible, instead map to another parent.').goAway(3000);
// return;
// }
try {
// todo: audit
await $api.dbTableRow.nestedRemove(
NOCO,
project.value.title as string,
meta.value.title,
rowId.value,
colOptions.type as 'mm' | 'hm',
column.title,
getRelatedTableRowId(row) as string,
)
} catch (e) {
notification.error({
message: 'Unlink failed',
description: await extractSdkResponseErrorMsg(e),
})
}
reloadData?.()
// todo: reload table data and children list
// this.$emit('loadTableData');
// if (this.isForm && this.$refs.childList) {
// this.$refs.childList.loadData();
// }
}
const link = async (row: Record<string, any>) => {
// todo: handle new record
// const pid = this._extractRowId(parent, this.parentMeta);
// const id = this._extractRowId(this.row, this.meta);
// const _cn = this.meta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id).title;
//
// if (this.isNew) {
// const _rcn = this.parentMeta.columns.find(c => c.id === this.column.colOptions.fk_parent_column_id).title;
// this.localState = parent;
// this.$emit('update:localState', this.localState);
// this.$emit('updateCol', this.row, _cn, parent[_rcn]);
// this.newRecordModal = false;
// return;
// }
try {
await $api.dbTableRow.nestedAdd(
NOCO,
project.value.title as string,
meta.value.title as string,
rowId.value,
colOptions.type as 'mm' | 'hm',
column.title,
getRelatedTableRowId(row) as string,
)
} catch (e) {
notification.error({
message: 'Linking failed',
description: await extractSdkResponseErrorMsg(e),
})
}
// todo: reload table data and child list
// this.pid = pid;
//
// this.newRecordModal = false;
//
// this.$emit('loadTableData');
// if (this.isForm && this.$refs.childList) {
// this.$refs.childList.loadData();
// }
reloadData?.()
}
// watchers
watch(childrenExcludedListPagination, async () => {
await loadChildrenExcludedList()
})
watch(childrenListPagination, async () => {
await loadChildrenList()
})
return {
relatedTableMeta,
loadRelatedTableMeta,
relatedTablePrimaryValueProp,
childrenExcludedList,
childrenList,
rowId,
childrenExcludedListPagination,
childrenListPagination,
primaryValueProp,
meta,
unlink,
link,
loadChildrenExcludedList,
loadChildrenList,
row,
deleteRelatedRow,
getRelatedTableRowId,
}
},
'ltar-store',
)
export { useProvideLTARStore }
export function useLTARStoreOrThrow() {
const ltarStore = useLTARStore()
if (ltarStore == null) throw new Error('Please call `useLTARStore` on the appropriate parent component')
return ltarStore
}

19
packages/nc-gui-v2/composables/useManyToMany.ts

@ -1,19 +0,0 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import { useMetas } from './useMetas'
export function useManyToMany(column: ColumnType) {
const { metas, getMeta } = useMetas()
const childMeta = computed<TableType>(() => {
return metas.value?.[(column.colOptions as any)?.fk_related_model_id as string]
})
const loadChildMeta = async () => {
await getMeta((column.colOptions as any)?.fk_related_model_id as string)
}
const primaryValueProp = computed(() => {
return (childMeta?.value?.columns?.find((c) => c.pv) || childMeta?.value?.columns?.[0])?.title
})
return { childMeta, loadChildMeta, primaryValueProp }
}

6
packages/nc-gui-v2/context/index.ts

@ -2,8 +2,12 @@ import type { ColumnType, FormType, GalleryType, GridType, KanbanType, TableType
import type { ComputedRef, InjectionKey, Ref } from 'vue'
import type { EventHook } from '@vueuse/core'
import type { useViewData } from '#imports'
import type { Row } from '~/composables'
import type { TabItem } from '~/composables/useTabs'
export const EditEnabledInj: InjectionKey<boolean> = Symbol('edit-enabled')
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<ColumnType & { meta: any }> = Symbol('column-injection')
export const MetaInj: InjectionKey<ComputedRef<TableType>> = Symbol('meta-injection')
export const TabMetaInj: InjectionKey<ComputedRef<TabItem>> = Symbol('tab-meta-injection')
@ -13,7 +17,7 @@ export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changeP
export const IsFormInj: InjectionKey<boolean> = Symbol('is-form-injection')
export const IsGridInj: InjectionKey<boolean> = Symbol('is-grid-injection')
export const IsLockedInj: InjectionKey<boolean> = Symbol('is-locked-injection')
export const ValueInj: InjectionKey<any> = Symbol('value-injection')
export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection')
export const ActiveViewInj: InjectionKey<Ref<GridType | FormType | KanbanType | GalleryType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<any> = Symbol('readonly-injection')
export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection')

2
packages/nc-gui/lang/fr.json

@ -229,7 +229,7 @@
"bookDemo": "Planifier une démonstration gratuite",
"getAnswered": "Obtenir des réponses à vos questions",
"joinDiscord": "Rejoindre le serveur Discord",
"joinCommunity": "Join NocoDB Community",
"joinCommunity": "Rejoindre la communauté NocoDB",
"joinReddit": "Rejoindre /r/NocoDB",
"followNocodb": "Suivre NocoDB"
},

Loading…
Cancel
Save