mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
51 changed files with 4048 additions and 1514 deletions
@ -1,11 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { inject } from '@vue/runtime-core' |
||||
|
||||
const value = inject<boolean | number>('value') |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="d-flex align-center"> |
||||
<input v-model="value" type="checkbox" :disabled="true" /> |
||||
</div> |
||||
</template> |
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts"> |
||||
import type { ColumnType } from 'nocodb-sdk' |
||||
import { computed } from 'vue' |
||||
import { computed, inject } from 'vue' |
||||
|
||||
const { modelValue: value } = defineProps<{ modelValue: any }>() |
||||
const emit = defineEmits(['update:modelValue']) |
@ -1,19 +0,0 @@
|
||||
<script> |
||||
import dayjs from 'dayjs' |
||||
|
||||
export default { |
||||
name: 'DateCell', |
||||
props: ['value'], |
||||
computed: { |
||||
date() { |
||||
return (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value)).format('YYYY-MM-DD') |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<span>{{ date }}</span> |
||||
</template> |
||||
|
||||
<style scoped></style> |
@ -1,21 +0,0 @@
|
||||
<script> |
||||
import dayjs from 'dayjs' |
||||
|
||||
export default { |
||||
name: 'DateTimeCell', |
||||
props: ['value'], |
||||
computed: { |
||||
dateTime() { |
||||
return !this.value |
||||
? this.value |
||||
: (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value)).format('YYYY-MM-DD HH:mm') |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<span>{{ dateTime }}</span> |
||||
</template> |
||||
|
||||
<style scoped></style> |
@ -1,32 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import type { ColumnType } from 'nocodb-sdk' |
||||
import { inject } from 'vue' |
||||
import { enumColor } from '~/utils/colorsUtils' |
||||
|
||||
const colors = enumColor.light |
||||
|
||||
const value = inject('value') |
||||
const column = inject<ColumnType>('column') |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<span |
||||
v-for="v in [(value || '').replace(/\\'/g, '\'').replace(/^'|'$/g, '')]" |
||||
:key="v" |
||||
:style="{ |
||||
background: colors[v], |
||||
}" |
||||
class="set-item ma-1 py-1 px-3" |
||||
>{{ v }}</span |
||||
> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.set-item { |
||||
display: inline-block; |
||||
border-radius: 25px; |
||||
white-space: nowrap; |
||||
} |
||||
</style> |
@ -1,12 +0,0 @@
|
||||
<script> |
||||
export default { |
||||
name: 'JsonCell', |
||||
props: ['value'], |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<pre class="text-left caption">{{ value }}</pre> |
||||
</template> |
||||
|
||||
<style scoped></style> |
@ -1,5 +1,6 @@
|
||||
<script> |
||||
import MonacoJsonObjectEditor from '@/components/monaco/MonacoJsonObjectEditor' |
||||
const editEnabled = inject < boolean > 'editEnabled' |
||||
|
||||
export default { |
||||
name: 'JsonEditableCell', |
@ -0,0 +1,126 @@
|
||||
<script lang="ts" setup> |
||||
import { computed } from '@vue/reactivity' |
||||
import type { ColumnType } from 'nocodb-sdk' |
||||
import { Ref, inject } from 'vue' |
||||
import { enumColor } from '~/utils/colorsUtils' |
||||
|
||||
const { modelValue } = defineProps<{ modelValue: any }>() |
||||
const emit = defineEmits(['update:modelValue']) |
||||
const column = inject<ColumnType>('column') |
||||
const isForm = inject<boolean>('isForm') |
||||
const editEnabled = inject<boolean>('editEnabled') |
||||
|
||||
const options = computed<string[]>(() => { |
||||
return column?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [] |
||||
}) |
||||
|
||||
const localState = computed({ |
||||
get() { |
||||
return modelValue?.match(/(?:[^',]|\\')+(?='?(?:,|$))/g).map((v: string) => v.replace(/\\'/g, "'")) |
||||
}, |
||||
set(val) { |
||||
emit('update:modelValue', val.filter((v: string) => options.value.includes(v)).join(',')) |
||||
}, |
||||
}) |
||||
|
||||
/* import colors from '@/components/project/spreadsheet/helpers/colors' |
||||
|
||||
export default { |
||||
name: 'SetListCheckboxCell', |
||||
props: { |
||||
value: String, |
||||
column: Object, |
||||
values: Array, |
||||
}, |
||||
data() {}, |
||||
computed: { |
||||
colors() { |
||||
return this.$store.state.settings.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 this.values || [] |
||||
}, |
||||
parentListeners() { |
||||
const $listeners = {} |
||||
|
||||
if (this.$listeners.blur) { |
||||
$listeners.blur = this.$listeners.blur |
||||
} |
||||
if (this.$listeners.focus) { |
||||
$listeners.focus = this.$listeners.focus |
||||
} |
||||
|
||||
return $listeners |
||||
}, |
||||
}, |
||||
mounted() { |
||||
this.$el.focus() |
||||
const event = document.createEvent('MouseEvents') |
||||
event.initMouseEvent('mousedown', true, true, window) |
||||
this.$el.dispatchEvent(event) |
||||
}, |
||||
} */ |
||||
</script> |
||||
|
||||
<template> |
||||
<!-- |
||||
<v-select |
||||
v-model="localState" |
||||
:items="options" |
||||
hide-details |
||||
:clearable="!column.rqd" |
||||
variation="outlined" |
||||
multiple |
||||
/> |
||||
--> |
||||
|
||||
<v-combobox |
||||
v-model="localState" |
||||
:items="options" |
||||
multiple |
||||
chips |
||||
flat |
||||
dense |
||||
solo |
||||
hide-details |
||||
deletable-chips |
||||
class="text-center mt-0" |
||||
> |
||||
<!-- <template #selection="data"> --> |
||||
<!-- <v-chip --> |
||||
<!-- :key="data.item" --> |
||||
<!-- small --> |
||||
<!-- class="ma-1 " --> |
||||
<!-- :color="colors[setValues.indexOf(data.item) % colors.length]" --> |
||||
<!-- @click:close="data.parent.selectItem(data.item)" --> |
||||
<!-- > --> |
||||
<!-- {{ data.item }} --> |
||||
<!-- </v-chip> --> |
||||
<!-- </template> --> |
||||
|
||||
<!-- <template #item="{item}"> --> |
||||
<!-- <v-chip small :color="colors[setValues.indexOf(item) % colors.length]"> --> |
||||
<!-- {{ item }} --> |
||||
<!-- </v-chip> --> |
||||
<!-- </template> --> |
||||
<!-- <template #append> --> |
||||
<!-- <v-icon small class="mt-2"> --> |
||||
<!-- mdi-menu-down --> |
||||
<!-- </v-icon> --> |
||||
<!-- </template> --> |
||||
</v-combobox> |
||||
</template> |
||||
|
||||
<style scoped></style> |
@ -1,68 +0,0 @@
|
||||
<script> |
||||
// import colors from '@/mixins/colors' |
||||
|
||||
export default { |
||||
name: 'SetListCell', |
||||
// mixins: [colors], |
||||
props: ['value', 'column'], |
||||
computed: { |
||||
setValues() { |
||||
if (this.column && this.column.dtxp) |
||||
return this.column.dtxp.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) |
||||
|
||||
return [] |
||||
}, |
||||
selectedValues() { |
||||
return this.value ? this.value.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) : [] |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<v-chip |
||||
v-for="v in selectedValues" |
||||
v-show="v || setValues.includes(v)" |
||||
:key="v" |
||||
small |
||||
:color="colors[setValues.indexOf(v) % colors.length]" |
||||
class="set-item ma-1 py-1 px-3" |
||||
> |
||||
{{ v }} |
||||
</v-chip> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
/* |
||||
.set-item { |
||||
display: inline-block; |
||||
border-radius: 25px; |
||||
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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -1,17 +1,51 @@
|
||||
<script> |
||||
export default { |
||||
name: 'TimeCell', |
||||
props: ['value'], |
||||
computed: { |
||||
time() { |
||||
return typeof this.value === 'string' ? this.value.replace(/(\d)T(?=\d)/, '$1 ') : this.value |
||||
}, |
||||
}, |
||||
} |
||||
<script setup lang="ts"> |
||||
import { inject } from 'vue' |
||||
|
||||
const editEnabled = inject<boolean>('editEnabled') |
||||
</script> |
||||
|
||||
<template> |
||||
<span>{{ time }}</span> |
||||
<v-menu> |
||||
<template #activator="{ on }"> |
||||
<input v-model="localState" class="value" v-on="on" /> |
||||
</template> |
||||
<div class="d-flex flex-column justify-center" @click.stop> |
||||
<v-time-picker v-model="localState" v-on="parentListeners" /> |
||||
<v-btn small color="primary" @click="$emit('save')"> |
||||
<!-- Save --> |
||||
{{ $t('general.save') }} |
||||
</v-btn> |
||||
</div> |
||||
</v-menu> |
||||
</template> |
||||
|
||||
<style scoped></style> |
||||
<style scoped> |
||||
.value { |
||||
width: 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/>. |
||||
* |
||||
*/ |
||||
--> |
||||
|
@ -1,20 +1,83 @@
|
||||
<script> |
||||
import { isValidURL } from '~/helpers' |
||||
const editEnabled = inject < boolean > 'editEnabled' |
||||
|
||||
/* |
||||
import { isValidURL } from '@/helpers' |
||||
import { inject } from "vue"; |
||||
|
||||
export default { |
||||
name: 'UrlCell', |
||||
props: ['value'], |
||||
name: 'EditableUrlCell', |
||||
props: { |
||||
value: String, |
||||
column: Object, |
||||
}, |
||||
computed: { |
||||
isValid() { |
||||
return this.value && isValidURL(this.value) |
||||
localState: { |
||||
get() { |
||||
return this.value |
||||
}, |
||||
set(val) { |
||||
if (!(this.column && this.column.meta && this.column.meta.validate) || isValidURL(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 |
||||
} |
||||
|
||||
if (this.$listeners.cancel) { |
||||
$listeners.cancel = this.$listeners.cancel |
||||
} |
||||
|
||||
return $listeners |
||||
}, |
||||
}, |
||||
mounted() { |
||||
this.$el.focus() |
||||
}, |
||||
} */ |
||||
</script> |
||||
|
||||
<template> |
||||
<a v-if="isValid" :href="value" target="_blank">{{ value }}</a> |
||||
<span v-else>{{ value }}</span> |
||||
<input v-model="localState" v-on="parentListeners" /> |
||||
</template> |
||||
|
||||
<style scoped></style> |
||||
<style scoped> |
||||
input, |
||||
textarea { |
||||
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/>. |
||||
* |
||||
*/ |
||||
--> |
||||
|
@ -1,335 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { useNuxt } from '@nuxt/kit' |
||||
import { inject, watchEffect } from '@vue/runtime-core' |
||||
import type { ColumnType, TableType } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import { useToast } from 'vue-toastification' |
||||
import { useNuxtApp } from '#app' |
||||
import useProject from '~/composables/useProject' |
||||
// import FileSaver from "file-saver"; |
||||
import { isImage } from '~/utils/fileUtils' |
||||
import MaterialPlusIcon from '~icons/mdi/plus' |
||||
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand' |
||||
|
||||
const { modelValue } = defineProps<{ modelValue: string | Array<any> }>() |
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const isPublicForm = inject<boolean>('isPublicForm', false) |
||||
const isForm = inject<boolean>('isForm', false) |
||||
const meta = inject<Ref<TableType>>('meta') |
||||
const column = inject<ColumnType>('column') |
||||
|
||||
const localFilesState = reactive([]) |
||||
const attachments = ref([]) |
||||
const uploading = ref(false) |
||||
const fileInput = ref<HTMLInputElement>() |
||||
|
||||
const { $api } = useNuxtApp() |
||||
const { project } = useProject() |
||||
const toast = useToast() |
||||
|
||||
watchEffect(() => { |
||||
if (modelValue) { |
||||
attachments.value = ((typeof modelValue === 'string' ? JSON.parse(modelValue) : modelValue) || []).filter(Boolean) |
||||
} |
||||
}) |
||||
|
||||
const selectImage = (file: any, i) => { |
||||
// todo: implement |
||||
} |
||||
|
||||
const openUrl = (url: string, target = '_blank') => { |
||||
window.open(url, target) |
||||
} |
||||
|
||||
const addFile = () => { |
||||
fileInput.value?.click() |
||||
} |
||||
|
||||
const onFileSelection = async (e) => { |
||||
// if (this.isPublicGrid) { |
||||
// return |
||||
// } |
||||
// if (!this.$refs.file.files || !this.$refs.file.files.length) { |
||||
// return |
||||
// } |
||||
|
||||
// if (this.isPublicForm) { |
||||
// this.localFilesState.push(...Array.from(this.$refs.file.files).map((file) => { |
||||
// const res = { file, title: file.name } |
||||
// if (isImage(file.name, file.mimetype)) { |
||||
// const reader = new FileReader() |
||||
// reader.onload = (e) => { |
||||
// this.$set(res, 'data', e.target.result) |
||||
// } |
||||
// reader.readAsDataURL(file) |
||||
// } |
||||
// return res |
||||
// })) |
||||
// |
||||
// this.$emit('input', this.localFilesState.map(f => f.file)) |
||||
// return |
||||
// } |
||||
|
||||
// todo : move to com |
||||
uploading.value = true |
||||
const newAttachments = [] |
||||
for (const file of fileInput.value?.files ?? []) { |
||||
try { |
||||
const data = await $api.storage.upload( |
||||
{ |
||||
path: ['noco', project.value.title, meta?.value?.title, column?.title].join('/'), |
||||
}, |
||||
{ |
||||
files: file, |
||||
json: '{}', |
||||
}, |
||||
) |
||||
newAttachments.push(...data) |
||||
} catch (e: any) { |
||||
toast.error(e.message || 'Some internal error occurred') |
||||
uploading.value = false |
||||
return |
||||
} |
||||
} |
||||
uploading.value = false |
||||
emit('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments])) |
||||
|
||||
// this.$emit('input', JSON.stringify(this.localState)) |
||||
// this.$emit('update') |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="main h-100"> |
||||
<div class="d-flex align-center img-container"> |
||||
<div class="d-flex no-overflow"> |
||||
<div |
||||
v-for="(item, i) in isPublicForm ? localFilesState : attachments" |
||||
:key="item.url || item.title" |
||||
class="thumbnail align-center justify-center d-flex" |
||||
> |
||||
<!-- <v-tooltip bottom> --> |
||||
<!-- <template #activator="{ on }"> --> |
||||
<!-- <v-img |
||||
v-if="isImage(item.title, item.mimetype)" |
||||
lazy-src="https://via.placeholder.com/60.png?text=Loading..." |
||||
alt="#" |
||||
max-height="99px" |
||||
contain |
||||
:src="item.url || item.data" |
||||
v-on="on" |
||||
@click="selectImage(item.url || item.data, i)" |
||||
> --> |
||||
<img |
||||
v-if="isImage(item.title, item.mimetype)" |
||||
alt="#" |
||||
style="max-height: 30px; max-width: 30px" |
||||
:src="item.url || item.data" |
||||
@click="selectImage(item.url || item.data, i)" |
||||
/> |
||||
<!-- <template #placeholder> --> |
||||
<!-- <v-skeleton-loader type="image" :height="active ? 33 : 22" :width="active ? 33 : 22" /> --> |
||||
<!-- </template> --> |
||||
<v-icon v-else-if="item.icon" :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> |
||||
{{ item.icon }} |
||||
</v-icon> |
||||
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> mdi-file </v-icon> |
||||
<!-- </template> --> |
||||
<!-- <span>{{ item.title }}</span> --> |
||||
<!-- </v-tooltip> --> |
||||
</div> |
||||
</div> |
||||
<!-- todo: hide or toggle based on ancestor --> |
||||
<div class="add d-flex align-center justify-center px-1 nc-attachment-add" @click="addFile"> |
||||
<v-icon v-if="uploading" small color="primary" class="nc-attachment-add-spinner"> mdi-loading mdi-spin</v-icon> |
||||
<!-- <v-btn v-else-if="isForm" outlined x-small color="" text class="nc-attachment-add-btn"> |
||||
<v-icon x-small color="" icon="MaterialPlusIcon"> mdi-plus </v-icon> |
||||
Attachment |
||||
</v-btn> |
||||
<v-icon small color="primary nc-attachment-add-icon"> |
||||
mdi-plus |
||||
</v-icon> --> |
||||
<MaterialPlusIcon /> |
||||
</div> |
||||
|
||||
<v-spacer /> |
||||
|
||||
<MaterialArrowExpandIcon @click.stop="dialog = true" /> |
||||
<!-- <v-icon class="expand-icon mr-1" x-small color="primary" @click.stop="dialog = true"> mdi-arrow-expand </v-icon> --> |
||||
</div> |
||||
|
||||
<input ref="fileInput" type="file" multiple class="d-none" @change="onFileSelection" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.thumbnail { |
||||
height: 30px; |
||||
width: 30px; |
||||
margin: 2px; |
||||
border-radius: 4px; |
||||
|
||||
img { |
||||
max-height: 33px; |
||||
max-width: 33px; |
||||
} |
||||
} |
||||
|
||||
.expand-icon { |
||||
margin-left: 8px; |
||||
border-radius: 2px; |
||||
transition: 0.3s background-color; |
||||
} |
||||
|
||||
.expand-icon:hover { |
||||
background-color: var(--v-primary-lighten4); |
||||
} |
||||
|
||||
/*.img-container { |
||||
margin: 0 -2px; |
||||
} |
||||
|
||||
.no-overflow { |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.add { |
||||
transition: 0.2s background-color; |
||||
!*background-color: #666666ee;*! |
||||
border-radius: 4px; |
||||
height: 33px; |
||||
margin: 5px 2px; |
||||
} |
||||
|
||||
.add:hover { |
||||
!*background-color: #66666699;*! |
||||
} |
||||
|
||||
.thumbnail { |
||||
height: 99px; |
||||
width: 99px; |
||||
margin: 2px; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
.thumbnail img { |
||||
!*max-height: 33px;*! |
||||
max-width: 99px; |
||||
} |
||||
|
||||
.main { |
||||
min-height: 20px; |
||||
position: relative; |
||||
height: auto; |
||||
} |
||||
|
||||
.expand-icon { |
||||
margin-left: 8px; |
||||
border-radius: 2px; |
||||
!*opacity: 0;*! |
||||
transition: 0.3s background-color; |
||||
} |
||||
|
||||
.expand-icon:hover { |
||||
!*opacity: 1;*! |
||||
background-color: var(--v-primary-lighten4); |
||||
} |
||||
|
||||
.modal-thumbnail img { |
||||
height: 50px; |
||||
max-width: 100%; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
.modal-thumbnail { |
||||
position: relative; |
||||
margin: 10px 10px; |
||||
} |
||||
|
||||
.remove-icon { |
||||
position: absolute; |
||||
top: 5px; |
||||
right: 5px; |
||||
} |
||||
|
||||
.modal-thumbnail-card { |
||||
.download-icon { |
||||
position: absolute; |
||||
bottom: 5px; |
||||
right: 5px; |
||||
opacity: 0; |
||||
transition: 0.4s opacity; |
||||
} |
||||
|
||||
&:hover .download-icon { |
||||
opacity: 1; |
||||
} |
||||
} |
||||
|
||||
.image-overlay-container { |
||||
max-height: 100vh; |
||||
overflow-y: auto; |
||||
position: relative; |
||||
} |
||||
|
||||
.image-overlay-container .close-icon { |
||||
position: fixed; |
||||
top: 15px; |
||||
right: 15px; |
||||
} |
||||
|
||||
.overlay-thumbnail { |
||||
transition: 0.4s transform, 0.4s opacity; |
||||
opacity: 0.5; |
||||
} |
||||
|
||||
.overlay-thumbnail.active { |
||||
transform: scale(1.4); |
||||
opacity: 1; |
||||
} |
||||
|
||||
.overlay-thumbnail:hover { |
||||
opacity: 1; |
||||
} |
||||
|
||||
.modal-title { |
||||
text-overflow: ellipsis; |
||||
white-space: nowrap; |
||||
width: 100%; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.modal-thumbnail-card { |
||||
transition: 0.4s transform; |
||||
} |
||||
|
||||
.modal-thumbnail-card:hover { |
||||
transform: scale(1.05); |
||||
} |
||||
|
||||
.drop-overlay { |
||||
z-index: 5; |
||||
position: absolute; |
||||
width: 100%; |
||||
height: 100%; |
||||
left: 0; |
||||
right: 0; |
||||
top: 0; |
||||
bottom: 5px; |
||||
background: #aaaaaa44; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
pointer-events: none; |
||||
} |
||||
|
||||
.expand-icon { |
||||
opacity: 0; |
||||
transition: 0.4s opacity; |
||||
} |
||||
|
||||
.main:hover .expand-icon { |
||||
opacity: 1; |
||||
}*/ |
||||
</style> |
@ -1,138 +0,0 @@
|
||||
<script> |
||||
import { convertDurationToSeconds, convertMS2Duration, durationOptions } from '~/helpers/durationHelper' |
||||
|
||||
export default { |
||||
name: 'DurationCell', |
||||
props: { |
||||
column: Object, |
||||
value: [Number, String], |
||||
readOnly: Boolean, |
||||
}, |
||||
data: () => ({ |
||||
// flag to determine to show warning message or not |
||||
showWarningMessage: false, |
||||
// duration in milliseconds |
||||
durationInMS: null, |
||||
// check if the cell is edited or not |
||||
isEdited: false, |
||||
}), |
||||
computed: { |
||||
localState: { |
||||
get() { |
||||
return convertMS2Duration(this.value, this.durationType) |
||||
}, |
||||
set(val) { |
||||
this.isEdited = true |
||||
const res = convertDurationToSeconds(val, this.durationType) |
||||
if (res._isValid) { |
||||
this.durationInMS = res._sec |
||||
} |
||||
}, |
||||
}, |
||||
durationPlaceholder() { |
||||
return durationOptions[this.durationType].title |
||||
}, |
||||
durationType() { |
||||
return this.column?.meta?.duration || 0 |
||||
}, |
||||
parentListeners() { |
||||
const $listeners = {} |
||||
|
||||
if (this.$listeners.blur) { |
||||
$listeners.blur = this.$listeners.blur |
||||
} |
||||
if (this.$listeners.focus) { |
||||
$listeners.focus = this.$listeners.focus |
||||
} |
||||
|
||||
return $listeners |
||||
}, |
||||
}, |
||||
mounted() { |
||||
window.addEventListener('keypress', (_) => { |
||||
if (this.$refs.durationInput) { |
||||
this.$refs.durationInput.focus() |
||||
} |
||||
}) |
||||
}, |
||||
methods: { |
||||
checkDurationFormat(evt) { |
||||
evt = evt || window.event |
||||
const charCode = evt.which ? evt.which : evt.keyCode |
||||
// ref: http://www.columbia.edu/kermit/ascii.html |
||||
const PRINTABLE_CTL_RANGE = charCode > 31 |
||||
const NON_DIGIT = charCode < 48 || charCode > 57 |
||||
const NON_COLON = charCode !== 58 |
||||
const NON_PERIOD = charCode !== 46 |
||||
if (PRINTABLE_CTL_RANGE && NON_DIGIT && NON_COLON && NON_PERIOD) { |
||||
this.showWarningMessage = true |
||||
evt.preventDefault() |
||||
} else { |
||||
this.showWarningMessage = false |
||||
// only allow digits, '.' and ':' (without quotes) |
||||
return true |
||||
} |
||||
}, |
||||
onBlur() { |
||||
if (this.isEdited) { |
||||
this.$emit('input', this.durationInMS) |
||||
} |
||||
this.isEdited = false |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="duration-cell-wrapper"> |
||||
<input |
||||
ref="durationInput" |
||||
v-model="localState" |
||||
:placeholder="durationPlaceholder" |
||||
@blur="onBlur" |
||||
@keypress="checkDurationFormat($event)" |
||||
@keydown.enter="isEdited && $emit('input', durationInMS)" |
||||
v-on="parentListeners" |
||||
/> |
||||
<div v-if="showWarningMessage == true" class="duration-warning"> |
||||
<!-- TODO: i18n --> |
||||
Please enter a number |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.duration-cell-wrapper { |
||||
padding: 10px; |
||||
} |
||||
|
||||
.duration-warning { |
||||
text-align: left; |
||||
margin-top: 10px; |
||||
color: #e65100; |
||||
} |
||||
</style> |
||||
|
||||
<!-- |
||||
/** |
||||
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
||||
* |
||||
* @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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -1,79 +0,0 @@
|
||||
<script> |
||||
import { isValidURL } from '@/helpers' |
||||
|
||||
export default { |
||||
name: 'EditableUrlCell', |
||||
props: { |
||||
value: String, |
||||
column: Object, |
||||
}, |
||||
computed: { |
||||
localState: { |
||||
get() { |
||||
return this.value |
||||
}, |
||||
set(val) { |
||||
if (!(this.column && this.column.meta && this.column.meta.validate) || isValidURL(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 |
||||
} |
||||
|
||||
if (this.$listeners.cancel) { |
||||
$listeners.cancel = this.$listeners.cancel |
||||
} |
||||
|
||||
return $listeners |
||||
}, |
||||
}, |
||||
mounted() { |
||||
this.$el.focus() |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<input v-model="localState" v-on="parentListeners" /> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
input, |
||||
textarea { |
||||
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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -1,101 +0,0 @@
|
||||
<script> |
||||
|
||||
|
||||
import { computed } from "@vue/reactivity"; |
||||
import { ColumnType } from "nocodb-sdk"; |
||||
import { inject, Ref } from "vue"; |
||||
import {enumColor}from "~/utils/colorsUtils"; |
||||
|
||||
const column = inject<ColumnType>("column"); |
||||
const isForm = inject<boolean>("isForm"); |
||||
|
||||
const { modelValue } = defineProps<{ modelValue: any }>(); |
||||
const emit = defineEmits(["update:modelValue"]); |
||||
|
||||
const localState = computed({ |
||||
get() { |
||||
return modelValue?.replace(/\\'/g, "'").replace(/^'|'$/g, ""); |
||||
}, |
||||
set(val) { |
||||
emit("update:modelValue", val); |
||||
} |
||||
}); |
||||
|
||||
const options = computed<string[]>(() => { |
||||
return column?.dtxp?.split(",").map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, "")) || []; |
||||
}); |
||||
|
||||
/*import colors from '@/components/project/spreadsheet/helpers/colors' |
||||
|
||||
export default { |
||||
name: 'SetListCheckboxCell', |
||||
props: { |
||||
value: String, |
||||
column: Object, |
||||
values: Array, |
||||
}, |
||||
data() {}, |
||||
computed: { |
||||
colors() { |
||||
return this.$store.state.settings.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 this.values || [] |
||||
}, |
||||
parentListeners() { |
||||
const $listeners = {} |
||||
|
||||
if (this.$listeners.blur) { |
||||
$listeners.blur = this.$listeners.blur |
||||
} |
||||
if (this.$listeners.focus) { |
||||
$listeners.focus = this.$listeners.focus |
||||
} |
||||
|
||||
return $listeners |
||||
}, |
||||
}, |
||||
mounted() { |
||||
this.$el.focus() |
||||
const event = document.createEvent('MouseEvents') |
||||
event.initMouseEvent('mousedown', true, true, window) |
||||
this.$el.dispatchEvent(event) |
||||
}, |
||||
}*/ |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="d-flex align-center"> |
||||
<div> |
||||
<div v-for="(val, i) of setValues" :key="val" class=""> |
||||
<input :id="`key-check-box-${val}`" v-model="localState" type="checkbox" class="orange--text" :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> |
||||
|
||||
<style scoped> |
||||
.label { |
||||
border-radius: 25px; |
||||
} |
||||
</style> |
@ -1,122 +0,0 @@
|
||||
<script> |
||||
import colors from '@/mixins/colors' |
||||
|
||||
export default { |
||||
name: 'SetListEditableCell', |
||||
mixins: [colors], |
||||
props: { |
||||
value: String, |
||||
column: Object, |
||||
}, |
||||
computed: { |
||||
localState: { |
||||
get() { |
||||
return this.value && this.value.match(/(?:[^',]|\\')+(?='?(?:,|$))/g).map((v) => v.replace(/\\'/g, "'")) |
||||
}, |
||||
set(val) { |
||||
this.$emit('input', val.filter((v) => this.setValues.includes(v)).join(',')) |
||||
}, |
||||
}, |
||||
setValues() { |
||||
if (this.column && this.column.dtxp) { |
||||
return this.column.dtxp.match(/(?:[^']|\\')+(?='?(?:,|$))/g).map((v) => v.replace(/\\'/g, "'").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 |
||||
}, |
||||
}, |
||||
mounted() { |
||||
// this.$el.focus(); |
||||
// let event; |
||||
// event = document.createEvent('MouseEvents'); |
||||
// event.initMouseEvent('mousedown', true, true, window); |
||||
// this.$el.dispatchEvent(event); |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<v-combobox |
||||
v-model="localState" |
||||
:items="setValues" |
||||
multiple |
||||
chips |
||||
flat |
||||
dense |
||||
solo |
||||
hide-details |
||||
deletable-chips |
||||
class="text-center mt-0" |
||||
> |
||||
<template #selection="data"> |
||||
<v-chip |
||||
:key="data.item" |
||||
small |
||||
class="ma-1" |
||||
:color="colors[setValues.indexOf(data.item) % colors.length]" |
||||
@click:close="data.parent.selectItem(data.item)" |
||||
> |
||||
{{ data.item }} |
||||
</v-chip> |
||||
</template> |
||||
|
||||
<template #item="{ item }"> |
||||
<v-chip small :color="colors[setValues.indexOf(item) % colors.length]"> |
||||
{{ item }} |
||||
</v-chip> |
||||
</template> |
||||
<template #append> |
||||
<v-icon small class="mt-2"> mdi-menu-down </v-icon> |
||||
</template> |
||||
</v-combobox> |
||||
</div> |
||||
</template> |
||||
|
||||
<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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -1,110 +0,0 @@
|
||||
<script> |
||||
import dayjs from 'dayjs' |
||||
|
||||
export default { |
||||
name: 'TimePickerCell', |
||||
props: { |
||||
value: [String, Date], |
||||
}, |
||||
computed: { |
||||
isMysql() { |
||||
return ['mysql', 'mysql2'].indexOf(this.$store.getters['project/GtrClientType']) |
||||
}, |
||||
localState: { |
||||
get() { |
||||
if (!this.value) { |
||||
return this.value |
||||
} |
||||
let dateTime = dayjs(this.value) |
||||
if (!dateTime.isValid()) { |
||||
dateTime = dayjs(this.value, 'HH:mm:ss') |
||||
} |
||||
if (!dateTime.isValid()) { |
||||
dateTime = dayjs(`1999-01-01 ${this.value}`) |
||||
} |
||||
if (!dateTime.isValid()) { |
||||
return this.value |
||||
} |
||||
return dateTime.format('HH:mm:ss') |
||||
}, |
||||
set(val) { |
||||
const dateTime = dayjs(`1999-01-01 ${val}:00`) |
||||
if (dateTime.isValid()) { |
||||
if (this.isMysql) { |
||||
this.$emit('input', dateTime.format('YYYY-MM-DD HH:mm:ss')) |
||||
} else { |
||||
this.$emit('input', dateTime.format('YYYY-MM-DD HH:mm:ssZ')) |
||||
} |
||||
} |
||||
}, |
||||
}, |
||||
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 |
||||
}, |
||||
}, |
||||
mounted() { |
||||
if (this.$el && this.$el.$el) { |
||||
this.$el.$el.focus() |
||||
} |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<v-menu> |
||||
<template #activator="{ on }"> |
||||
<input v-model="localState" class="value" v-on="on" /> |
||||
</template> |
||||
<div class="d-flex flex-column justify-center" @click.stop> |
||||
<v-time-picker v-model="localState" v-on="parentListeners" /> |
||||
<v-btn small color="primary" @click="$emit('save')"> |
||||
<!-- Save --> |
||||
{{ $t('general.save') }} |
||||
</v-btn> |
||||
</div> |
||||
</v-menu> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.value { |
||||
width: 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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -1,216 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { computed } from '@vue/reactivity' |
||||
import type { ColumnType } from 'nocodb-sdk' |
||||
import useColumn from '~/composables/useColumn' |
||||
|
||||
const { column, modelValue: value } = defineProps<{ column: ColumnType; modelValue: any }>() |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
provide('column', column) |
||||
|
||||
const localState = computed({ |
||||
get() { |
||||
return value |
||||
}, |
||||
set(val) { |
||||
emit('update:modelValue', val) |
||||
}, |
||||
}) |
||||
|
||||
const { |
||||
isURL, |
||||
isEmail, |
||||
isJSON, |
||||
isDate, |
||||
isDateTime, |
||||
isTime, |
||||
isBoolean, |
||||
isDuration, |
||||
isRating, |
||||
isCurrency, |
||||
isAttachment, |
||||
isTextArea, |
||||
isString, |
||||
isSingleSelect, |
||||
isMultiSelect |
||||
} = useColumn(column) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="nc-cell" @keydown.stop.left @keydown.stop.right @keydown.stop.up @keydown.stop.down> |
||||
<!-- <RatingCell --> |
||||
<!-- v-if="isRating" --> |
||||
<!-- /> --> |
||||
<!-- v-model="localState" |
||||
:active="active" |
||||
:is-form="isForm" |
||||
:column="column" |
||||
:is-public-grid="isPublic && !isForm" |
||||
:is-public-form="isPublic && isForm" |
||||
:is-locked="isLocked" |
||||
v-on="$listeners" |
||||
/> --> |
||||
|
||||
<!-- <DurationCell --> |
||||
<!-- v-else-if="isDuration" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- :active="active" --> |
||||
<!-- :is-form="isForm" --> |
||||
<!-- :column="column" --> |
||||
<!-- :is-locked="isLocked" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
|
||||
<!-- <IntegerCell --> |
||||
<!-- v-else-if="isInt" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
|
||||
<!-- <FloatCell --> |
||||
<!-- v-else-if="isFloat" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
|
||||
<!-- <DatePickerCell --> |
||||
<!-- v-else-if="isDate" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
|
||||
<!-- <TimePickerCell --> |
||||
<!-- v-else-if="isTime" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- @save="$emit('save')" --> |
||||
<!-- />–> --> |
||||
|
||||
<!-- <DateTimePickerCell --> |
||||
<!-- v-else-if="isDateTime" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- ignore-focus --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
|
||||
<!-- <EnumCell --> |
||||
<!-- v-else-if="isEnum && ((!isForm && !active) || isLocked || (isPublic && !isForm))" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- :column="column" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
<!-- <EnumListCell --> |
||||
<!-- v-else-if="isEnum" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState"–> --> |
||||
<!-- <!– :is-form="isForm"–> --> |
||||
<!-- <!– :column="column"–> --> |
||||
<!-- <!– v-on="parentListeners"–> --> |
||||
<!-- <!– />–> --> |
||||
|
||||
<!-- <JsonEditableCell --> |
||||
<!-- v-else-if="isJSON" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- :is-form="isForm" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- @input="$emit('save')" --> |
||||
<!-- />–> --> |
||||
|
||||
<!-- <SetListEditableCell --> |
||||
<!-- v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- :column="column" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
<!-- <SetListCell --> |
||||
<!-- v-else-if="isSet" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- :column="column" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
|
||||
<!-- <EditableUrlCell v-else-if="isURL" --> |
||||
<!-- /> --> |
||||
<!-- <!– v-model="localState" v-on="parentListeners" –> --> |
||||
<!-- <!– />–> --> |
||||
|
||||
<EditableCellText v-if="isString" v-model="localState" /> |
||||
<!-- v-on="parentListeners" |
||||
/> |
||||
--> |
||||
|
||||
<EditableCellTextArea v-else-if="isTextArea" v-model="localState" /> |
||||
<!-- v-model="localState" |
||||
:is-form="isForm" |
||||
v-on="parentListeners" |
||||
/> --> |
||||
|
||||
<EditableCellBoolean v-else-if="isBoolean" v-model="localState" /> |
||||
<!-- <!– v-model="localState" --> |
||||
<!-- :column="column" --> |
||||
<!-- :is-form="isForm" --> |
||||
<!-- v-on="parentListeners" --> |
||||
<!-- />–> --> |
||||
|
||||
<EditableCellAttachment v-if="isAttachment" v-model="localState" /> |
||||
<EditableCellSingleSelect v-if="isSingleSelect" v-model="localState" /> |
||||
<!-- v-model="localState" |
||||
:active="active" |
||||
:db-alias="dbAlias" |
||||
:meta="meta" |
||||
:is-form="isForm" |
||||
:column="column" |
||||
:is-public-grid="isPublic && !isForm" |
||||
:is-public-form="isPublic && isForm" |
||||
:view-id="viewId" |
||||
:is-locked="isLocked" |
||||
v-on="$listeners" |
||||
/> --> |
||||
|
||||
<EditableCellText v-else v-model="localState" /> |
||||
<!-- v-on="$listeners" <span v-if="hint" class="nc-hint">{{ hint }}</span> --> |
||||
|
||||
<!-- <div v-if="(isLocked || (isPublic && !isForm)) && !isAttachment" class="nc-locked-overlay" /> --> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
textarea { |
||||
outline: none; |
||||
} |
||||
|
||||
div { |
||||
width: 100%; |
||||
height: 100%; |
||||
color: var(--v-textColor-base); |
||||
} |
||||
|
||||
.nc-hint { |
||||
font-size: 0.61rem; |
||||
color: grey; |
||||
} |
||||
|
||||
.nc-cell { |
||||
position: relative; |
||||
} |
||||
|
||||
.nc-locked-overlay { |
||||
position: absolute; |
||||
z-index: 2; |
||||
height: 100%; |
||||
width: 100%; |
||||
top: 0; |
||||
left: 0; |
||||
} |
||||
</style> |
@ -0,0 +1,223 @@
|
||||
<script setup lang="ts"> |
||||
import type { ColumnType } from 'nocodb-sdk' |
||||
import useVirtualCell from '~/composables/useVirtualCell' |
||||
|
||||
const { column, modelValue: value } = defineProps<{ column: ColumnType; modelValue: any; editEnabled: boolean }>() |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
provide('column', column) |
||||
provide('value', value) |
||||
|
||||
const { isLookup, isBt, isRollup, isMm, isHm, isFormula } = useVirtualCell(column) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="nc-virtual-cell"> |
||||
<VirtualCellHasMany v-if="isHm" /> |
||||
<VirtualCellManyToMany v-else-if="isMm" /> |
||||
<VirtualCellBelongsTo v-else-if="isBt" /> |
||||
<VirtualCellRollup v-else-if="isRollup" /> |
||||
<VirtualCellRollup v-else-if="isRollup" /> |
||||
|
||||
<!-- <v-lazy> --> |
||||
<!-- <has-many-cell |
||||
v-if="hm" |
||||
ref="cell" |
||||
:row="row" |
||||
:value="row[column.title]" |
||||
:meta="meta" |
||||
:hm="hm" |
||||
:nodes="nodes" |
||||
:active="active" |
||||
:sql-ui="sqlUi" |
||||
:is-new="isNew" |
||||
:is-form="isForm" |
||||
:breadcrumbs="breadcrumbs" |
||||
:is-locked="isLocked" |
||||
:required="required" |
||||
:is-public="isPublic" |
||||
:metas="metas" |
||||
:column="column" |
||||
:password="password" |
||||
v-on="$listeners" |
||||
/> |
||||
<many-to-many-cell |
||||
v-else-if="mm" |
||||
ref="cell" |
||||
:is-public="isPublic" |
||||
:row="row" |
||||
:value="row[column.title]" |
||||
:meta="meta" |
||||
:nodes="nodes" |
||||
:sql-ui="sqlUi" |
||||
:active="active" |
||||
:is-new="isNew" |
||||
:api="api" |
||||
:is-form="isForm" |
||||
:breadcrumbs="breadcrumbs" |
||||
:is-locked="isLocked" |
||||
:required="required" |
||||
:column="column" |
||||
:metas="metas" |
||||
:password="password" |
||||
v-on="$listeners" |
||||
/> |
||||
<belongs-to-cell |
||||
v-else-if="bt" |
||||
ref="cell" |
||||
:is-public="isPublic" |
||||
:disabled-columns="disabledColumns" |
||||
:active="active" |
||||
:row="row" |
||||
:value="row[column.title]" |
||||
:meta="meta" |
||||
:nodes="nodes" |
||||
:api="api" |
||||
:sql-ui="sqlUi" |
||||
:is-new="isNew" |
||||
:is-form="isForm" |
||||
:breadcrumbs="breadcrumbs" |
||||
:is-locked="isLocked" |
||||
:metas="metas" |
||||
:column="column" |
||||
:password="password" |
||||
v-on="$listeners" |
||||
/> |
||||
<lookup-cell |
||||
v-else-if="lookup" |
||||
:disabled-columns="disabledColumns" |
||||
:active="active" |
||||
:row="row" |
||||
:value="row[column.title]" |
||||
:meta="meta" |
||||
:metas="metas" |
||||
:nodes="nodes" |
||||
:api="api" |
||||
:sql-ui="sqlUi" |
||||
:is-new="isNew" |
||||
:is-form="isForm" |
||||
:column="column" |
||||
:is-locked="isLocked" |
||||
v-on="$listeners " |
||||
/> |
||||
<formula-cell |
||||
v-else-if="formula" |
||||
:row="row" |
||||
:column="column" |
||||
:client="nodes.dbConnection.client" |
||||
/> |
||||
<rollup-cell |
||||
v-else-if="rollup" |
||||
:row="row" |
||||
:column="column" |
||||
/> |
||||
</v-lazy> |
||||
<span v-if="hint" class="nc-hint">{{ hint }}</span> |
||||
<div v-if="isLocked" class="nc-locked-overlay" /> --> |
||||
</div> |
||||
</template> |
||||
|
||||
<!-- <script> |
||||
import { UITypes } from "nocodb-sdk"; |
||||
import RollupCell from "./virtualCell/RollupCell"; |
||||
import FormulaCell from "./virtualCell/FormulaCell"; |
||||
import hasManyCell from "./virtualCell/HasManyCell"; |
||||
import LookupCell from "./virtualCell/LookupCell"; |
||||
import manyToManyCell from "./virtualCell/ManyToManyCell"; |
||||
import belongsToCell from "./virtualCell/BelongsToCell"; |
||||
|
||||
// todo: optimize parent/child meta extraction |
||||
|
||||
export default { |
||||
name: "VirtualCell", |
||||
components: { |
||||
RollupCell, |
||||
FormulaCell, |
||||
LookupCell, |
||||
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, |
||||
hint: String, |
||||
isLocked: Boolean, |
||||
required: Boolean, |
||||
isPublic: Boolean, |
||||
metas: Object, |
||||
password: String |
||||
}, |
||||
computed: { |
||||
hm() { |
||||
return this.column && this.column.uidt === UITypes.LinkToAnotherRecord && this.column.colOptions.type === "hm"; |
||||
}, |
||||
bt() { |
||||
return this.column && (this.column.uidt === UITypes.ForeignKey || this.column.uidt === UITypes.LinkToAnotherRecord) && this.column.colOptions.type === "bt"; |
||||
}, |
||||
mm() { |
||||
return this.column && this.column.uidt === UITypes.LinkToAnotherRecord && this.column.colOptions.type === "mm"; |
||||
}, |
||||
lookup() { |
||||
return this.column && this.column.uidt === UITypes.Lookup; |
||||
}, |
||||
formula() { |
||||
return this.column && this.column.uidt === UITypes.Formula; |
||||
}, |
||||
rollup() { |
||||
return this.column && this.column.uidt === UITypes.Rollup; |
||||
} |
||||
}, |
||||
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> |
||||
.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> |
@ -0,0 +1,493 @@
|
||||
<script setup lang="ts"> |
||||
|
||||
import { ColumnType } from "nocodb-sdk"; |
||||
import useBelongsTo from "~/composables/useBelongsTo"; |
||||
import ItemChip from "./components/ItemChip.vue"; |
||||
|
||||
const column = inject<ColumnType>("column"); |
||||
const value = inject("value"); |
||||
|
||||
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) |
||||
}, |
||||
}, |
||||
}*/ |
||||
</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"--> |
||||
</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> |
||||
</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 { |
||||
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> |
||||
* @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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -0,0 +1,93 @@
|
||||
<script> |
||||
import dayjs from 'dayjs' |
||||
|
||||
export default { |
||||
name: 'FormulaCell', |
||||
props: { column: Object, row: Object, client: String }, |
||||
data: () => ({ |
||||
showEditFormulaWarning: false, |
||||
}), |
||||
computed: { |
||||
result() { |
||||
if (this.client === 'pg') { |
||||
return this.handleTZ(this.row[this.column.title]) |
||||
} |
||||
return this.row[this.column.title] |
||||
}, |
||||
urls() { |
||||
if (!this.row[this.column.title]) { |
||||
return |
||||
} |
||||
|
||||
const rawText = this.result.toString() |
||||
let found = false |
||||
const out = rawText.replace(/URI::\((.*?)\)/g, (_, url) => { |
||||
found = true |
||||
const a = document.createElement('a') |
||||
a.textContent = url |
||||
a.setAttribute('href', url) |
||||
a.setAttribute('target', '_blank') |
||||
return a.outerHTML |
||||
}) |
||||
|
||||
return found && out |
||||
}, |
||||
}, |
||||
methods: { |
||||
// handle date returned from PostgreSQL |
||||
handleTZ(val) { |
||||
if (!val) { |
||||
return |
||||
} |
||||
if (typeof val !== 'string') { |
||||
return val |
||||
} |
||||
return val.replace( |
||||
/((?:-?(?:[1-9][0-9]*)?[0-9]{4})-(?:1[0-2]|0[1-9])-(?:3[01]|0[1-9]|[12][0-9])T(?:2[0-3]|[01][0-9]):(?:[0-5][0-9]):(?:[0-5][0-9])(?:\.[0-9]+)?(?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9]))/g, |
||||
(i, v) => { |
||||
return dayjs(v).format('YYYY-MM-DD HH:mm') |
||||
}, |
||||
) |
||||
}, |
||||
showEditFormulaWarningMessage() { |
||||
this.showEditFormulaWarning = true |
||||
setTimeout(() => { |
||||
this.showEditFormulaWarning = false |
||||
}, 3000) |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<v-tooltip v-if="column && column.colOptions && column.colOptions.error" bottom color="error"> |
||||
<template #activator="{ on }"> |
||||
<span class="caption" v-on="on">ERR<span class="error--text">!</span></span> |
||||
</template> |
||||
<span class="font-weight-bold">{{ column.colOptions.error }}</span> |
||||
</v-tooltip> |
||||
<div class="formula-cell-wrapper" @dblclick="showEditFormulaWarningMessage"> |
||||
<div v-if="urls" v-html="urls" /> |
||||
<div v-else> |
||||
{{ result }} |
||||
</div> |
||||
<div v-if="showEditFormulaWarning == true" class="edit-warning"> |
||||
<!-- TODO: i18n --> |
||||
Warning: Formula fields should be configured in the field menu dropdown. |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.formula-cell-wrapper { |
||||
padding: 10px; |
||||
} |
||||
|
||||
.edit-warning { |
||||
text-align: left; |
||||
margin-top: 10px; |
||||
color: #e65100; |
||||
} |
||||
</style> |
@ -0,0 +1,603 @@
|
||||
<script setup lang="ts"> |
||||
import { computed } from '@vue/reactivity' |
||||
import type { ColumnType } from 'nocodb-sdk' |
||||
import ItemChip from './components/ItemChip.vue' |
||||
import useHasMany from '~/composables/useHasMany' |
||||
|
||||
const column = inject<ColumnType>('column') |
||||
const value = inject('value') |
||||
|
||||
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') |
||||
}, |
||||
}, |
||||
} */ |
||||
</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--text" |
||||
@click="showChildListModal" |
||||
>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> |
||||
</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; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.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> |
||||
* @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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -0,0 +1,183 @@
|
||||
<script> |
||||
/*import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' |
||||
import TableCell from '../Cell' |
||||
import ItemChip from '~/components/project/spreadsheet/components/virtualCell/components/ItemChip' |
||||
export default { |
||||
name: 'LookupCell', |
||||
components: { |
||||
TableCell, |
||||
// ListChildItemsModal, |
||||
ItemChip, |
||||
}, |
||||
props: { |
||||
meta: [Object], |
||||
metas: [Object], |
||||
column: [Object], |
||||
nodes: [Object], |
||||
row: [Object], |
||||
api: [Object, Function], |
||||
sqlUi: [Object, Function], |
||||
active: Boolean, |
||||
isNew: Boolean, |
||||
isForm: Boolean, |
||||
value: [Object, Array, String, Number], |
||||
}, |
||||
data: () => ({ |
||||
UITypes, |
||||
lookupListModal: false, |
||||
lookupTableMeta: null, |
||||
lookupColumnMeta: null, |
||||
isVirtualCol, |
||||
RelationTypes, |
||||
}), |
||||
computed: { |
||||
virtualCell() { |
||||
return this.lookupColumnMeta && isVirtualCol(this.lookupColumnMeta) |
||||
? () => import('~/components/project/spreadsheet/components/VirtualCell') |
||||
: 'div' |
||||
}, |
||||
// todo : optimize |
||||
lookupApi() { |
||||
// return this.column && this.$ncApis.get({ |
||||
// env: this.nodes.env, |
||||
// dbAlias: this.nodes.dbAlias, |
||||
// table: this.column.lk.ltn |
||||
// }) |
||||
}, |
||||
lookUpMeta() { |
||||
// return this.metas ? this.metas[this.column.lk.ltn] : this.$store.state.meta.metas[this.column.lk.ltn] |
||||
}, |
||||
|
||||
assocMeta() { |
||||
// return this.column.lk.type === 'mm' && (this.metas ? this.metas[this.column.lk.vtn] : this.$store.state.meta.metas[this.column.lk.vtn]) |
||||
}, |
||||
lookUpColumnAlias() { |
||||
if (!this.lookUpMeta || !this.column.lk.lcn) { |
||||
return |
||||
} |
||||
return (this.lookUpMeta.columns.find((cl) => cl.column_name === this.column.lk.lcn) || {}).title |
||||
}, |
||||
lookUpColumn() { |
||||
if (!this.lookUpMeta || !this.column.lk.lcn) { |
||||
return |
||||
} |
||||
return this.lookUpMeta.columns.find((cl) => cl.column_name === this.column.lk.lcn) || {} |
||||
}, |
||||
localValueObj() {}, |
||||
localValue() { |
||||
return this.value && (Array.isArray(this.value) ? this.value : [this.value]) |
||||
}, |
||||
queryParams() {}, |
||||
}, |
||||
created() { |
||||
this.loadLookupMeta() |
||||
this.loadLookupColumnMeta() |
||||
}, |
||||
methods: { |
||||
async loadLookupColumnMeta() { |
||||
const relationColumn = this.meta.columns.find((c) => c.id === this.column.colOptions.fk_relation_column_id) |
||||
this.lookupTableMeta = await this.$store.dispatch('meta/ActLoadMeta', { id: relationColumn.colOptions.fk_related_model_id }) |
||||
this.lookupColumnMeta = this.lookupTableMeta.columns.find((c) => c.id === this.column.colOptions.fk_lookup_column_id) |
||||
}, |
||||
|
||||
async loadLookupMeta() {}, |
||||
showLookupListModal() { |
||||
this.lookupListModal = true |
||||
}, |
||||
}, |
||||
}*/ |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="d-flex flex-wrap wrapper"> |
||||
<!-- <template v-if="lookupColumnMeta"> |
||||
<template v-if="isVirtualCol(lookupColumnMeta)"> |
||||
<template |
||||
:is="virtualCell" |
||||
v-if=" |
||||
lookupColumnMeta.uidt === UITypes.LinkToAnotherRecord && |
||||
lookupColumnMeta.colOptions.type === RelationTypes.BELONGS_TO && |
||||
Array.isArray(value) |
||||
" |
||||
> |
||||
<div |
||||
:is="virtualCell" |
||||
v-for="(v, i) in value" |
||||
:key="i" |
||||
:is-public="true" |
||||
:metas="metas" |
||||
:is-locked="true" |
||||
:column="lookupColumnMeta" |
||||
:row="{ [lookupColumnMeta.title]: v }" |
||||
:nodes="nodes" |
||||
:meta="lookupTableMeta" |
||||
:sql-ui="sqlUi" |
||||
/> |
||||
</template> |
||||
<div |
||||
:is="virtualCell" |
||||
v-else |
||||
:is-public="true" |
||||
:metas="metas" |
||||
:is-locked="true" |
||||
:column="lookupColumnMeta" |
||||
:row="{ [lookupColumnMeta.title]: value }" |
||||
:nodes="nodes" |
||||
:meta="lookupTableMeta" |
||||
:sql-ui="sqlUi" |
||||
/> |
||||
</template> |
||||
<template v-else> |
||||
<template v-if="localValue"> |
||||
<ItemChip |
||||
v-for="(value, i) in localValue" |
||||
:key="i" |
||||
style="margin: 1.5px" |
||||
:active="active" |
||||
:value="value" |
||||
:readonly="true" |
||||
> |
||||
<TableCell |
||||
:is-locked="true" |
||||
:column="lookupColumnMeta" |
||||
:meta="lookupTableMeta" |
||||
:db-alias="nodes.dbAlias" |
||||
:value="value" |
||||
:sql-ui="sqlUi" |
||||
/> |
||||
</ItemChip> |
||||
</template> |
||||
</template> |
||||
</template>--> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.wrapper { |
||||
flex-wrap: wrap; |
||||
} |
||||
</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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -0,0 +1,613 @@
|
||||
<script setup lang="ts"> |
||||
import type { ColumnType } from "nocodb-sdk"; |
||||
import ItemChip from "./components/ItemChip.vue"; |
||||
import useManyToMany from "~/composables/useManyToMany"; |
||||
|
||||
const column = inject<ColumnType>("column"); |
||||
const value = inject("value"); |
||||
|
||||
const { childMeta, loadChildMeta, primaryValueProp } = useManyToMany(column as ColumnType); |
||||
await loadChildMeta(); |
||||
|
||||
/* 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' |
||||
|
||||
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') |
||||
}, |
||||
}, |
||||
} */ |
||||
</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="(v, j) in value || localState" |
||||
:key="j" |
||||
:item="v" |
||||
:value="v[primaryValueProp]" |
||||
/> |
||||
|
||||
<!-- :active="active" |
||||
:readonly="isLocked || isPublic" |
||||
@edit="editChild" |
||||
@unlink="unlinkChild" --> |
||||
</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" |
||||
/> |
||||
|
||||
<!– todo : move to list item component –> |
||||
<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> |
||||
</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-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); |
||||
} |
||||
} |
||||
} |
||||
</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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -0,0 +1,9 @@
|
||||
<script setup lang="ts"> |
||||
const { value } = defineProps<{ value: any }>() |
||||
</script> |
||||
|
||||
<template> |
||||
<span> |
||||
{{ value }} |
||||
</span> |
||||
</template> |
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts"> |
||||
import MdiCloseThickIcon from '~icons/mdi/close-thick' |
||||
/* export default { |
||||
name: 'ItemChip', |
||||
props: { |
||||
value: [String, Number, Boolean], |
||||
active: Boolean, |
||||
item: Object, |
||||
readonly: Boolean, |
||||
}, |
||||
} */ |
||||
|
||||
const { value, active, item, readonly } = defineProps({ |
||||
value: [String, Number, Boolean], |
||||
active: Boolean, |
||||
item: Object, |
||||
readonly: Boolean, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<v-chip |
||||
class="chip" |
||||
:class="{ active }" |
||||
small |
||||
text-color="textColor" |
||||
:color="isDark ? '' : 'primary lighten-5'" |
||||
@click="!readonly && active && $emit('edit', item)" |
||||
> |
||||
<span class="name" :title="value">{{ value }}</span> |
||||
|
||||
<!-- && _isUIAllowed('xcDatatableEditable') --> |
||||
<div v-show="active" v-if="!readonly" class="mr-n1 ml-2"> |
||||
<MdiCloseThickIcon class="unlink-icon" @click.stop="$emit('unlink', item)"> </MdiCloseThickIcon> |
||||
</div> |
||||
</v-chip> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.chip { |
||||
max-width: max(100%, 60px); |
||||
|
||||
.name { |
||||
text-overflow: ellipsis; |
||||
overflow: hidden; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,252 @@
|
||||
<script> |
||||
import { RelationTypes } from 'nocodb-sdk' |
||||
import Pagination from '~/components/project/spreadsheet/components/Pagination' |
||||
|
||||
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, |
||||
}, |
||||
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), |
||||
}, |
||||
{}, |
||||
) |
||||
} |
||||
|
||||
return |
||||
} |
||||
|
||||
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, |
||||
}) |
||||
} |
||||
}, |
||||
}, |
||||
} |
||||
</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('new-record')" |
||||
> |
||||
<v-icon small> mdi-link </v-icon> 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('new-record')"> |
||||
<v-icon x-small> mdi-link </v-icon> 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 |
||||
icon.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> |
||||
</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" |
||||
/> |
||||
</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> --> |
||||
</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; |
||||
} |
||||
</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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -0,0 +1,126 @@
|
||||
<script> |
||||
import ListChildItems from '~/components/project/spreadsheet/components/virtualCell/components/ListChildItems' |
||||
|
||||
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, |
||||
}, |
||||
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) { |
||||
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-on="$listeners" |
||||
/> |
||||
</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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -0,0 +1,248 @@
|
||||
<script> |
||||
import Pagination from '~/components/project/spreadsheet/components/Pagination' |
||||
|
||||
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], |
||||
}, |
||||
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}%)` |
||||
|
||||
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, |
||||
}) |
||||
} |
||||
} |
||||
}, |
||||
}, |
||||
} |
||||
</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 |
||||
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('add-new-record')"> |
||||
<v-icon small> mdi-plus </v-icon> 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()) }} </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> |
||||
</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" |
||||
/> |
||||
</v-card-actions> |
||||
</v-card> |
||||
</v-dialog> |
||||
</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> |
||||
<!-- |
||||
/** |
||||
* @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/>. |
||||
* |
||||
*/ |
||||
--> |
@ -0,0 +1,20 @@
|
||||
import type { ColumnType, TableType } from 'nocodb-sdk' |
||||
import type LinkToAnotherRecordColumn from '../../nocodb/src/lib/models/LinkToAnotherRecordColumn' |
||||
import useMetas from '~/composables/useMetas' |
||||
|
||||
export default function (column: ColumnType) { |
||||
const { metas, getMeta } = useMetas() |
||||
const parentMeta = computed<TableType>(() => { |
||||
return metas.value?.[(column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string] |
||||
}) |
||||
|
||||
const loadParentMeta = async () => { |
||||
await getMeta((column.colOptions as LinkToAnotherRecordColumn)?.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 } |
||||
} |
@ -0,0 +1,20 @@
|
||||
import type { ColumnType, TableType } from 'nocodb-sdk' |
||||
import type LinkToAnotherRecordColumn from '../../nocodb/src/lib/models/LinkToAnotherRecordColumn' |
||||
import useMetas from '~/composables/useMetas' |
||||
|
||||
export default function (column: ColumnType) { |
||||
const { metas, getMeta } = useMetas() |
||||
const childMeta = computed<TableType>(() => { |
||||
return metas.value?.[(column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string] |
||||
}) |
||||
|
||||
const loadChildMeta = async () => { |
||||
await getMeta((column.colOptions as LinkToAnotherRecordColumn)?.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 } |
||||
} |
@ -0,0 +1,20 @@
|
||||
import type { ColumnType, TableType } from 'nocodb-sdk' |
||||
import type LinkToAnotherRecordColumn from '../../nocodb/src/lib/models/LinkToAnotherRecordColumn' |
||||
import useMetas from '~/composables/useMetas' |
||||
|
||||
export default function (column: ColumnType) { |
||||
const { metas, getMeta } = useMetas() |
||||
const childMeta = computed<TableType>(() => { |
||||
return metas.value?.[(column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string] |
||||
}) |
||||
|
||||
const loadChildMeta = async () => { |
||||
await getMeta((column.colOptions as LinkToAnotherRecordColumn)?.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 } |
||||
} |
@ -0,0 +1,32 @@
|
||||
import { computed } from '@vue/reactivity' |
||||
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' |
||||
import { RelationTypes, UITypes } from 'nocodb-sdk' |
||||
|
||||
export default function useVirtualCell(column: ColumnType) { |
||||
const isHm = computed( |
||||
() => |
||||
column.uidt === UITypes.LinkToAnotherRecord && (<LinkToAnotherRecordType>column.colOptions).type === RelationTypes.HAS_MANY, |
||||
) |
||||
const isMm = computed( |
||||
() => |
||||
column.uidt === UITypes.LinkToAnotherRecord && |
||||
(<LinkToAnotherRecordType>column.colOptions).type === RelationTypes.MANY_TO_MANY, |
||||
) |
||||
const isBt = computed( |
||||
() => |
||||
column.uidt === UITypes.LinkToAnotherRecord && |
||||
(<LinkToAnotherRecordType>column.colOptions).type === RelationTypes.BELONGS_TO, |
||||
) |
||||
const isLookup = computed(() => column.uidt === UITypes.Lookup) |
||||
const isRollup = computed(() => column.uidt === UITypes.Rollup) |
||||
const isFormula = computed(() => column.uidt === UITypes.Formula) |
||||
|
||||
return { |
||||
isHm, |
||||
isMm, |
||||
isBt, |
||||
isLookup, |
||||
isRollup, |
||||
isFormula, |
||||
} |
||||
} |
Loading…
Reference in new issue