Browse Source

feat(gui-v2): add hm, m2m, bt and rollup cells

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/2716/head
Pranav C 2 years ago
parent
commit
7ffdda936b
  1. 6
      packages/nc-gui-v2/assets/style/style.css
  2. 382
      packages/nc-gui-v2/components/cell/Attachment.vue
  3. 11
      packages/nc-gui-v2/components/cell/Boolean.vue
  4. 2
      packages/nc-gui-v2/components/cell/Checkbox.vue
  5. 19
      packages/nc-gui-v2/components/cell/Date.vue
  6. 37
      packages/nc-gui-v2/components/cell/DatePicker.vue
  7. 21
      packages/nc-gui-v2/components/cell/DateTime.vue
  8. 106
      packages/nc-gui-v2/components/cell/DateTimePicker.vue
  9. 113
      packages/nc-gui-v2/components/cell/Duration.vue
  10. 32
      packages/nc-gui-v2/components/cell/Enum.vue
  11. 9
      packages/nc-gui-v2/components/cell/Float.vue
  12. 9
      packages/nc-gui-v2/components/cell/Integer.vue
  13. 12
      packages/nc-gui-v2/components/cell/Json.vue
  14. 1
      packages/nc-gui-v2/components/cell/JsonEditableCell.vue
  15. 126
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  16. 47
      packages/nc-gui-v2/components/cell/Rating.vue
  17. 68
      packages/nc-gui-v2/components/cell/SetList.vue
  18. 57
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  19. 9
      packages/nc-gui-v2/components/cell/Text.vue
  20. 14
      packages/nc-gui-v2/components/cell/TextArea.vue
  21. 58
      packages/nc-gui-v2/components/cell/Time.vue
  22. 81
      packages/nc-gui-v2/components/cell/Url.vue
  23. 335
      packages/nc-gui-v2/components/editable-cell/Attachment.vue
  24. 138
      packages/nc-gui-v2/components/editable-cell/DurationCell.vue
  25. 79
      packages/nc-gui-v2/components/editable-cell/EditableUrlCell.vue
  26. 101
      packages/nc-gui-v2/components/editable-cell/MultiSelect.vue
  27. 122
      packages/nc-gui-v2/components/editable-cell/SetListEditableCell.vue
  28. 110
      packages/nc-gui-v2/components/editable-cell/TimePickerCell.vue
  29. 229
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  30. 216
      packages/nc-gui-v2/components/smartsheet/EditableCell.vue
  31. 9
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  32. 223
      packages/nc-gui-v2/components/smartsheet/VirtualCell.vue
  33. 2
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  34. 493
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  35. 93
      packages/nc-gui-v2/components/virtual-cell/FormulaCell.vue
  36. 603
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  37. 183
      packages/nc-gui-v2/components/virtual-cell/Lookup.vue
  38. 613
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  39. 9
      packages/nc-gui-v2/components/virtual-cell/Rollup.vue
  40. 48
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  41. 252
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  42. 126
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItemsModal.vue
  43. 248
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  44. 20
      packages/nc-gui-v2/composables/useBelongsTo.ts
  45. 2
      packages/nc-gui-v2/composables/useColumn.ts
  46. 20
      packages/nc-gui-v2/composables/useHasMany.ts
  47. 20
      packages/nc-gui-v2/composables/useManyToMany.ts
  48. 4
      packages/nc-gui-v2/composables/useProject.ts
  49. 32
      packages/nc-gui-v2/composables/useVirtualCell.ts
  50. 11
      packages/nc-gui-v2/package-lock.json
  51. 1
      packages/nc-gui-v2/package.json

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

@ -27,10 +27,6 @@ table .v-input__control {
border-bottom: 1px solid orange;
}
table .v-text-field > .v-input__control {
width: 25px;
}
table .v-text-field > .v-input__control > .v-input__slot:before {
border-style: none;
}
@ -368,4 +364,4 @@ html {
.menu-filter-dropdown {
max-height: 500px;
overflow-y: auto;
}
}

382
packages/nc-gui-v2/components/cell/Attachment.vue

@ -1,79 +1,312 @@
<script>
import { isImage } from '@/components/project/spreadsheet/helpers/imageExt'
export default {
name: 'AttachmentCell',
props: ['value', 'column'],
data: () => ({
dragOver: false,
}),
computed: {
localState() {
try {
return JSON.parse(this.value) || []
} catch (e) {
return []
}
},
},
methods: {
isImage,
},
<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> | null }>()
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 editEnabled = inject<boolean>('editEnabled')
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="d-flex align-center img-container d-100 h-100"
v-on="$listeners"
@dragover.prevent="dragOver = true"
@dragenter.prevent="dragOver = true"
@dragexit="dragOver = false"
@dragleave="dragOver = false"
@dragend="dragOver = false"
@drop.prevent
>
<div v-if="dragOver" class="drop-overlay">
<div>
<v-icon small> mdi-cloud-upload-outline </v-icon>
<span class="caption font-weight-bold">Drop here</span>
<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>
</div>
<template v-if="localState">
<div v-for="item in localState" :key="item.title" class="thumbnail d-flex align-center justify-center">
<v-lazy class="d-flex align-center justify-center">
<v-tooltip bottom>
<template #activator="{ on }">
<img v-if="isImage(item.title)" alt="#" :src="item.url" v-on="on" />
<v-icon v-else-if="item.icon" size="33" v-on="on">
{{ item.icon }}
</v-icon>
<v-icon v-else size="33" v-on="on"> mdi-file </v-icon>
</template>
<span>{{ item.title }}</span>
</v-tooltip>
</v-lazy>
<!-- 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>
</template>
<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>
.img-container {
<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;
position: relative;
}
.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: 29px;
width: 29px;
height: 99px;
width: 99px;
margin: 2px;
border-radius: 4px;
}
.thumbnail img {
max-height: 29px;
max-width: 29px;
!*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 {
@ -91,28 +324,13 @@ export default {
align-items: center;
pointer-events: none;
}
.expand-icon {
opacity: 0;
transition: 0.4s opacity;
}
.main:hover .expand-icon {
opacity: 1;
}*/
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

11
packages/nc-gui-v2/components/cell/Boolean.vue

@ -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>

2
packages/nc-gui-v2/components/editable-cell/Boolean.vue → packages/nc-gui-v2/components/cell/Checkbox.vue

@ -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'])

19
packages/nc-gui-v2/components/cell/Date.vue

@ -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>

37
packages/nc-gui-v2/components/editable-cell/DatePicker.vue → packages/nc-gui-v2/components/cell/DatePicker.vue

@ -1,5 +1,26 @@
<script>
<script setup lang="ts">
import dayjs from 'dayjs'
import { computed } from '@vue/reactivity'
const { modelValue } = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue'])
const localState = computed({
get() {
if (!modelValue || !dayjs(modelValue).isValid()) {
return undefined
}
return (/^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue)).format('YYYY-MM-DD')
},
set(val) {
if (dayjs(val).isValid()) {
emit('update:modelValue', val && dayjs(val).format('YYYY-MM-DD'))
}
},
})
/*
export default {
name: 'DatePickerCell',
@ -45,16 +66,16 @@ export default {
this.$el.$el.focus()
}
},
}
} */
</script>
<template>
<v-menu>
<template #activator="{ on }">
<input :value="date" class="value" v-on="on" />
</template>
<v-date-picker v-model="localState" flat @click.native.stop v-on="parentListeners" />
</v-menu>
<!-- <v-menu> -->
<!-- <template #activator="{ on }"> -->
<input v-model="localState" type="date" class="value" />
<!-- </template> -->
<!-- <v-date-picker v-model="localState" flat @click.native.stop v-on="parentListeners" /> -->
<!-- </v-menu> -->
</template>
<style scoped>

21
packages/nc-gui-v2/components/cell/DateTime.vue

@ -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>

106
packages/nc-gui-v2/components/editable-cell/DateTimePickerCell.vue → packages/nc-gui-v2/components/cell/DateTimePicker.vue

@ -1,5 +1,36 @@
<script>
<script setup lang="ts">
import dayjs from 'dayjs'
import { computed } from '@vue/reactivity'
import useProject from '~/composables/useProject'
const { modelValue } = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const showMessage = ref(false)
const localState = computed({
get() {
if (!modelValue) {
return modelValue
}
const d = /^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue)
if (d.isValid()) {
showMessage.value = false
return d.format('YYYY-MM-DD HH:mm')
} else {
showMessage.value = true
}
},
set(value) {
if (isMysql) {
emit('update:modelValue', value && dayjs(value).format('YYYY-MM-DD HH:mm:ss'))
} else {
emit('update:modelValue', value && dayjs(value).format('YYYY-MM-DD HH:mm:ssZ'))
}
},
})
/* import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
@ -63,49 +94,50 @@ export default {
this.$refs.picker.display = true
}
},
}
} */
</script>
<template>
<div>
<div v-show="!showMessage">
<v-datetime-picker
ref="picker"
v-model="localState"
class="caption xc-date-time-picker"
:text-field-props="{
class: 'caption mt-0 pt-0',
flat: true,
solo: true,
dense: true,
hideDetails: true,
}"
:time-picker-props="{
format: '24hr',
}"
v-on="parentListeners"
/>
</div>
<div v-show="showMessage" class="edit-warning" @dblclick="$refs.picker.display = true">
<!-- TODO: i18n -->
ERR: Couldn't parse {{ value }}
</div>
</div>
<input type="datetime-local" />
<!-- <div> -->
<!-- <div v-show="!showMessage"> -->
<!-- <v-datetime-picker -->
<!-- ref="picker" -->
<!-- v-model="localState" -->
<!-- class="caption xc-date-time-picker" -->
<!-- :text-field-props="{ -->
<!-- class: 'caption mt-0 pt-0', -->
<!-- flat: true, -->
<!-- solo: true, -->
<!-- dense: true, -->
<!-- hideDetails: true, -->
<!-- }" -->
<!-- :time-picker-props="{ -->
<!-- format: '24hr', -->
<!-- }" -->
<!-- v-on="parentListeners" -->
<!-- /> -->
<!-- </div> -->
<!-- <div v-show="showMessage" class="edit-warning" @dblclick="$refs.picker.display = true"> -->
<!-- &lt;!&ndash; TODO: i18n &ndash;&gt; -->
<!-- ERR: Couldn't parse {{ value }} -->
<!-- </div> -->
<!-- </div> -->
</template>
<style scoped>
:deep(.v-input),
:deep(.v-text-field) {
margin-top: 0 !important;
padding-top: 0 !important;
font-size: inherit !important;
}
/*:deep(.v-input),*/
/*:deep(.v-text-field) {*/
/* margin-top: 0 !important;*/
/* padding-top: 0 !important;*/
/* font-size: inherit !important;*/
/*}*/
.edit-warning {
padding: 10px;
text-align: left;
color: #e65100;
}
/*.edit-warning {*/
/* padding: 10px;*/
/* text-align: left;*/
/* color: #e65100;*/
/*}*/
</style>
<!--
/**

113
packages/nc-gui-v2/components/cell/Duration.vue

@ -1,42 +1,119 @@
<script>
import { convertMS2Duration, durationOptions } from '~/helpers/durationHelper'
<script setup lang="ts">
import { inject } from 'vue'
import { convertDurationToSeconds, convertMS2Duration, durationOptions } from '~/helpers/durationHelper'
const editEnabled = inject<boolean>('editEnabled')
/*
export default {
name: 'DurationCell',
props: {
column: Object,
value: [String, Number],
value: [Number, String],
readOnly: Boolean,
},
data: () => ({
// flag to determine to show warning message or not
showWarningMessage: false,
localValue: null,
// 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.column?.meta?.duration || 0].title
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
},
},
watch: {
'column.meta.duration': function (newValue, oldValue) {
if (oldValue !== newValue) {
this.localValue = convertMS2Duration(this.value, newValue)
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
}
},
'value': function (val, oldVal) {
this.localValue = convertMS2Duration(val !== oldVal && !val && val !== 0 ? oldVal : val, this.column?.meta?.duration || 0)
onBlur() {
if (this.isEdited) {
this.$emit('input', this.durationInMS)
}
this.isEdited = false
},
},
created() {
this.localValue = convertMS2Duration(this.value, this.column?.meta?.duration || 0)
},
}
} */
</script>
<template>
<input v-model="localValue" :placeholder="durationPlaceholder" readonly />
<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></style>
<style scoped>
.duration-cell-wrapper {
padding: 10px;
}
.duration-warning {
text-align: left;
margin-top: 10px;
color: #e65100;
}
</style>
<!--
/**

32
packages/nc-gui-v2/components/cell/Enum.vue

@ -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>

9
packages/nc-gui-v2/components/editable-cell/Float.vue → packages/nc-gui-v2/components/cell/Float.vue

@ -1,10 +1,11 @@
<script lang="ts" setup>
import { computed } from "@vue/reactivity";
import { onMounted } from "vue";
import { computed } from '@vue/reactivity'
import { inject, onMounted } from 'vue'
const { modelValue: value } = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject<boolean>('editEnabled')
const root = ref<HTMLInputElement>()
@ -20,11 +21,11 @@ const localState = computed({
onMounted(() => {
root.value?.focus()
})
</script>
<template>
<input ref="root" v-model="localState" type="number" />
<input v-if="editEnabled" ref="root" v-model="localState" type="number" />
<span v-else>{{ localState }}</span>
</template>
<style scoped>

9
packages/nc-gui-v2/components/editable-cell/Integer.vue → packages/nc-gui-v2/components/cell/Integer.vue

@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed } from "@vue/reactivity";
import { onMounted } from "vue";
import { computed } from '@vue/reactivity'
import { inject, onMounted } from 'vue'
const { modelValue: value } = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject<boolean>('editEnabled')
const root = ref<HTMLInputElement>()
@ -20,11 +21,11 @@ const localState = computed({
onMounted(() => {
root.value?.focus()
})
</script>
<template>
<input ref="root" v-model="localState" type="number" />
<input v-if="editEnabled" ref="root" v-model="localState" type="number" />
<span v-else>{{ localState }}</span>
</template>
<style scoped>

12
packages/nc-gui-v2/components/cell/Json.vue

@ -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
packages/nc-gui-v2/components/editable-cell/JsonEditableCell.vue → packages/nc-gui-v2/components/cell/JsonEditableCell.vue

@ -1,5 +1,6 @@
<script>
import MonacoJsonObjectEditor from '@/components/monaco/MonacoJsonObjectEditor'
const editEnabled = inject < boolean > 'editEnabled'
export default {
name: 'JsonEditableCell',

126
packages/nc-gui-v2/components/cell/MultiSelect.vue

@ -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>

47
packages/nc-gui-v2/components/editable-cell/RatingCell.vue → packages/nc-gui-v2/components/cell/Rating.vue

@ -1,4 +1,41 @@
<script>
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { computed, inject } from 'vue'
import MdiStarIcon from '~icons/mdi/star'
import MdiStarOutlineIcon from '~icons/mdi/star-outline'
const { modelValue: value } = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue'])
const column = inject<ColumnType & { meta?: any }>('column')
const isForm = inject<boolean>('isForm')
const ratingMeta = computed(() => {
return {
icon: {
full: 'mdi-star',
empty: 'mdi-star-outline',
},
color: '#fcb401',
max: 5,
// ...(column?.meta || {})
}
})
const localState = computed({
get() {
return value
},
set(val) {
emit('update:modelValue', val)
},
})
const toggle = () => {
localState.value = !localState.value
}
/* import { inject } from "vue";
const editEnabled = inject<boolean>("editEnabled");
export default {
name: 'RatingCell',
props: {
@ -33,7 +70,7 @@ export default {
}
},
},
}
} */
</script>
<template>
@ -41,10 +78,12 @@ export default {
<v-rating v-model="localState" :length="ratingMeta.max" dense x-small :readonly="readOnly" clearable>
<template #item="{ isFilled, click }">
<v-icon v-if="isFilled" :size="15" :color="ratingMeta.color" @click="click">
{{ fullIcon }}
<MdiStarIcon />
<!-- {{ fullIcon }} -->
</v-icon>
<v-icon v-else :color="ratingMeta.color" :size="15" class="nc-cell-hover-show" @click="click">
{{ emptyIcon }}
<!-- {{ emptyIcon }} -->
<MdiStarOutlineIcon />
</v-icon>
</template>
</v-rating>

68
packages/nc-gui-v2/components/cell/SetList.vue

@ -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/>.
*
*/
-->

57
packages/nc-gui-v2/components/editable-cell/SingleSelect.vue → packages/nc-gui-v2/components/cell/SingleSelect.vue

@ -1,31 +1,29 @@
<script lang="ts" setup>
import { computed } from '@vue/reactivity'
import type { ColumnType } from 'nocodb-sdk'
import { Ref, inject } from 'vue'
const { modelValue } = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue'])
// import {enumColor}from "~/utils/colorsUtils";
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 column = inject<ColumnType>('column')
const isForm = inject<boolean>('isForm')
const editEnabled = inject<boolean>('editEnabled')
const localState = computed({
get() {
return modelValue?.replace(/\\'/g, "'").replace(/^'|'$/g, "");
return modelValue?.replace(/\\'/g, "'").replace(/^'|'$/g, '')
},
set(val) {
emit("update:modelValue", val);
}
});
emit('update:modelValue', val)
},
})
const options = computed<string[]>(() => {
return column?.dtxp?.split(",").map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, "")) || [];
});
return column?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || []
})
/*import colors from '@/mixins/colors'
/* import colors from '@/mixins/colors'
export default {
name: 'EnumListEditableCell',
@ -50,23 +48,12 @@ export default {
return $listeners
},
},
}*/
} */
</script>
<template>
{{options}}
<v-select
v-model="localState"
solo
dense
flat
:items="options"
hide-details
class="mt-0"
:clearable="!column.rqd"
>
<!-- v-on="parentListeners"
<v-select v-model="localState" :items="options" hide-details :clearable="!column.rqd" variation="outlined">
<!-- v-on="parentListeners"
<template #selection="{ item }">
<div
class="d-100"
@ -86,12 +73,12 @@ export default {
</template>
<template #append>
<v-icon small class="mt-1"> mdi-menu-down</v-icon>
</template>-->
</template> -->
</v-select>
</template>
<style scoped lang="scss">
:deep {
/*:deep {
.v-select {
min-width: 150px;
}
@ -109,7 +96,7 @@ export default {
font-size: 13px !important;
}
}
}
}*/
</style>
<!--
/**

9
packages/nc-gui-v2/components/editable-cell/Text.vue → packages/nc-gui-v2/components/cell/Text.vue

@ -1,10 +1,12 @@
<script setup lang="ts">
import { computed } from '@vue/reactivity'
import { onMounted } from 'vue'
import { watchEffect } from '@vue/runtime-core'
import { inject, onMounted } from 'vue'
const { modelValue: value } = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject<boolean>('editEnabled')
const root = ref<HTMLInputElement>()
@ -17,7 +19,7 @@ const localState = computed({
},
})
onMounted(() => {
watchEffect(() => {
root.value?.focus()
})
@ -59,7 +61,8 @@ onMounted(() => {
</script>
<template>
<input ref="root" v-model="localState" />
<input v-if="editEnabled" ref="root" v-model="localState" />
<span v-else>{{ localState }}</span>
<!-- v-on="parentListeners" /> -->
</template>

14
packages/nc-gui-v2/components/editable-cell/TextArea.vue → packages/nc-gui-v2/components/cell/TextArea.vue

@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed } from '@vue/reactivity'
import { onMounted } from 'vue'
import { inject, onMounted } from 'vue'
const { modelValue: value } = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject<boolean>('editEnabled')
const root = ref<HTMLInputElement>()
@ -57,7 +58,16 @@ onMounted(() => {
</script>
<template>
<textarea ref="root" v-model="localState" rows="4" v-on="parentListeners" @keydown.alt.enter.stop @keydown.shift.enter.stop />
<textarea
v-if="editEnabled"
ref="root"
v-model="localState"
rows="4"
v-on="parentListeners"
@keydown.alt.enter.stop
@keydown.shift.enter.stop
/>
<span v-else>{{ localState }}</span>
</template>
<style scoped>

58
packages/nc-gui-v2/components/cell/Time.vue

@ -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/>.
*
*/
-->

81
packages/nc-gui-v2/components/cell/Url.vue

@ -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/>.
*
*/
-->

335
packages/nc-gui-v2/components/editable-cell/Attachment.vue

@ -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>

138
packages/nc-gui-v2/components/editable-cell/DurationCell.vue

@ -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/>.
*
*/
-->

79
packages/nc-gui-v2/components/editable-cell/EditableUrlCell.vue

@ -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/>.
*
*/
-->

101
packages/nc-gui-v2/components/editable-cell/MultiSelect.vue

@ -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>

122
packages/nc-gui-v2/components/editable-cell/SetListEditableCell.vue

@ -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/>.
*
*/
-->

110
packages/nc-gui-v2/components/editable-cell/TimePickerCell.vue

@ -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/>.
*
*/
-->

229
packages/nc-gui-v2/components/smartsheet/Cell.vue

@ -1,14 +1,29 @@
<script setup lang="ts">
import { computed } from '@vue/reactivity'
import { watchEffect } from '@vue/runtime-core'
import type { ColumnType } from 'nocodb-sdk'
import useColumn from '~/composables/useColumn'
const { column, value } = defineProps<{ column: ColumnType; value: any }>()
const { column, modelValue: value, editEnabled } = defineProps<{ column: ColumnType; modelValue: any; editEnabled: boolean }>()
const emit = defineEmits(['update:modelValue'])
provide('column', column)
provide('value', value)
provide(
'editEnabled',
computed(() => editEnabled),
)
const localState = computed({
get() {
return value
},
set(val) {
emit('update:modelValue', val)
},
})
const {
isSet,
isEnum,
isURL,
isEmail,
isJSON,
@ -21,24 +36,200 @@ const {
isCurrency,
isAttachment,
isTextArea,
isString,
isSingleSelect,
isMultiSelect,
} = useColumn(column)
</script>
<template>
<!-- <CellEditableAttachment v-if="isAttachment" /> -->
<CellSetList v-if="isSet" />
<CellEnum v-else-if="isEnum" />
<!-- <CellUrl v-else-if="isURL" /> -->
<!-- <CellEmail v-else-if="isEmail" /> -->
<!-- <CellJson v-else-if="isJSON" /> -->
<!-- <CellDate v-else-if="isDate" /> -->
<!-- <CellDateTime v-else-if="isDateTime" /> -->
<!-- <CellTime v-else-if="isTime" /> -->
<CellBoolean v-else-if="isBoolean" />
<!-- <CellDuration v-else-if="isDuration" /> -->
<!-- <CellRating v-else-if="isRating" /> -->
<!-- <CellCurrency v-else-if="isCurrency" /> -->
<span v-else :title="value">{{ value }}</span>
<div class="nc-cell" @keydown.stop.left @keydown.stop.right @keydown.stop.up @keydown.stop.down>
<!--
todo :
JSONCell
DurationCell
Currency
Url
Email
-->
<!-- <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" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :active="active" -->
<!-- :is-form="isForm" -->
<!-- :column="column" -->
<!-- :is-locked="isLocked" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <IntegerCell -->
<!-- v-else-if="isInt" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <FloatCell -->
<!-- v-else-if="isFloat" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <DatePickerCell -->
<!-- v-else-if="isDate" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <TimePickerCell -->
<!-- v-else-if="isTime" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- @save="$emit('save')" -->
<!-- />&ndash;&gt; -->
<!-- <DateTimePickerCell -->
<!-- v-else-if="isDateTime" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- ignore-focus -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <EnumCell -->
<!-- v-else-if="isEnum && ((!isForm && !active) || isLocked || (isPublic && !isForm))" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <EnumListCell -->
<!-- v-else-if="isEnum" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState"&ndash;&gt; -->
<!-- &lt;!&ndash; :is-form="isForm"&ndash;&gt; -->
<!-- &lt;!&ndash; :column="column"&ndash;&gt; -->
<!-- &lt;!&ndash; v-on="parentListeners"&ndash;&gt; -->
<!-- &lt;!&ndash; />&ndash;&gt; -->
<!-- <JsonEditableCell -->
<!-- v-else-if="isJSON" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :is-form="isForm" -->
<!-- v-on="parentListeners" -->
<!-- @input="$emit('save')" -->
<!-- />&ndash;&gt; -->
<!-- <SetListEditableCell -->
<!-- v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <SetListCell -->
<!-- v-else-if="isSet" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <UrlCell v-else-if="isURL" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" v-on="parentListeners" &ndash;&gt; -->
<!-- &lt;!&ndash; />&ndash;&gt; -->
<CellText v-if="isString" v-model="localState" />
<!-- v-on="parentListeners"
/>
-->
<CellTextArea v-else-if="isTextArea" v-model="localState" />
<!-- v-model="localState"
:is-form="isForm"
v-on="parentListeners"
/> -->
<CellBoolean v-else-if="isBoolean" v-model="localState" />
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- :is-form="isForm" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<CellAttachment v-else-if="isAttachment" v-model="localState" />
<CellSingleSelect v-else-if="isSingleSelect" v-model="localState" />
<CellMultiSelect v-else-if="isMultiSelect" v-model="localState" />
<CellDatePicker v-else-if="isDate" v-model="localState" />
<CellDateTimePicker v-else-if="isDateTime" v-model="localState" />
<CellDateTimePicker v-else-if="isTime" v-model="localState" />
<CellRating v-else-if="isRating" 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"
/> -->
<CellText 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></style>
<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>

216
packages/nc-gui-v2/components/smartsheet/EditableCell.vue

@ -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" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :active="active" -->
<!-- :is-form="isForm" -->
<!-- :column="column" -->
<!-- :is-locked="isLocked" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <IntegerCell -->
<!-- v-else-if="isInt" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <FloatCell -->
<!-- v-else-if="isFloat" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <DatePickerCell -->
<!-- v-else-if="isDate" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <TimePickerCell -->
<!-- v-else-if="isTime" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- @save="$emit('save')" -->
<!-- />&ndash;&gt; -->
<!-- <DateTimePickerCell -->
<!-- v-else-if="isDateTime" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- ignore-focus -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <EnumCell -->
<!-- v-else-if="isEnum && ((!isForm && !active) || isLocked || (isPublic && !isForm))" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <EnumListCell -->
<!-- v-else-if="isEnum" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState"&ndash;&gt; -->
<!-- &lt;!&ndash; :is-form="isForm"&ndash;&gt; -->
<!-- &lt;!&ndash; :column="column"&ndash;&gt; -->
<!-- &lt;!&ndash; v-on="parentListeners"&ndash;&gt; -->
<!-- &lt;!&ndash; />&ndash;&gt; -->
<!-- <JsonEditableCell -->
<!-- v-else-if="isJSON" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :is-form="isForm" -->
<!-- v-on="parentListeners" -->
<!-- @input="$emit('save')" -->
<!-- />&ndash;&gt; -->
<!-- <SetListEditableCell -->
<!-- v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <SetListCell -->
<!-- v-else-if="isSet" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <EditableUrlCell v-else-if="isURL" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" v-on="parentListeners" &ndash;&gt; -->
<!-- &lt;!&ndash; />&ndash;&gt; -->
<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" />
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- :is-form="isForm" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<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>

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

@ -124,15 +124,16 @@ onKeyStroke(['Enter'], (e) => {
<!-- @navigateToPrev="navigateToPrev" -->
<!-- /> -->
<span v-if="isVirtualCol(columnObj)" />
<SmartsheetVirtualCell v-if="isVirtualCol(columnObj)" v-model="row[columnObj.title]" :column="columnObj" />
<SmartsheetEditableCell
v-else-if="editEnabled && selected.col === colIndex && selected.row === rowIndex"
<SmartsheetCell
v-else
v-model="row[columnObj.title]"
:column="columnObj"
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
/>
<SmartsheetCell v-else :column="columnObj" :value="row[columnObj.title]" />
<!-- <SmartsheetCell v-else :column="columnObj" :value="row[columnObj.title]" /> -->
<!-- :selected="selected.col === col && selected.row === row" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->

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

@ -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>

2
packages/nc-gui-v2/components/tabs/Smartsheet.vue

@ -29,7 +29,7 @@ watch(
<template>
<div class="overflow-auto">
<!-- <v-toolbar height="32" dense class="nc-table-toolbar elevation-0 xc-toolbar xc-border-bottom mx-1" style="z-index: 7" />-->
<!-- <v-toolbar height="32" dense class="nc-table-toolbar elevation-0 xc-toolbar xc-border-bottom mx-1" style="z-index: 7" /> -->
<template v-if="meta && tabMeta">
<SmartsheetGrid />
</template>

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

@ -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/>.
*
*/
-->

93
packages/nc-gui-v2/components/virtual-cell/FormulaCell.vue

@ -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>

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

@ -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&#45;&#45;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/>.
*
*/
-->

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

@ -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/>.
*
*/
-->

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

@ -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"
/>
&lt;!&ndash; todo : move to list item component &ndash;&gt;
<v-dialog
v-if="selectedChild && !isPublic"
v-model="expandFormModal"
:overlay-opacity="0.8"
width="1000px"
max-width="100%"
class="mx-auto"
>
<component
:is="form"
v-if="selectedChild"
ref="expandedForm"
v-model="selectedChild"
:db-alias="nodes.dbAlias"
:has-many="childMeta.hasMany"
:belongs-to="childMeta.belongsTo"
v-model:is-new="isNewChild"
:table="childMeta.table_name"
:old-row="{ ...selectedChild }"
:meta="childMeta"
:primary-value-column="childPrimaryCol"
:available-columns="childAvailableColumns"
icon-color="warning"
:nodes="nodes"
:query-params="childQueryParams"
:breadcrumbs="breadcrumbs"
@cancel="
selectedChild = null
expandFormModal = false
"
@input="onChildSave"
/>
</v-dialog> -->
</div>
</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/>.
*
*/
-->

9
packages/nc-gui-v2/components/virtual-cell/Rollup.vue

@ -0,0 +1,9 @@
<script setup lang="ts">
const { value } = defineProps<{ value: any }>()
</script>
<template>
<span>
{{ value }}
</span>
</template>

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

@ -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>

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

@ -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>&nbsp; Link to '{{ meta.title }}'
</v-btn>
</v-card-title>
<v-card-text>
<div class="items-container pt-2 mb-n4" :class="{ 'mx-n2': isForm }">
<div v-if="!readOnly && (isPublic || _isUIAllowed('xcDatatableEditable'))" class="text-right mb-2 mt-n2 mx-2">
<v-btn v-if="isForm" x-small class="caption" color="primary" outlined @click="$emit('new-record')">
<v-icon x-small> mdi-link </v-icon>&nbsp; Link to '{{ meta.title }}'
</v-btn>
</div>
<template v-if="isDataAvail">
<v-card
v-for="(ch, i) in (data && data.list) || localState"
:key="i"
class="mx-2 mb-2 child-list-modal child-card"
outlined
@click="!readOnly && $emit('edit', ch)"
>
<div class="remove-child-icon d-flex align-center">
<x-icon
v-if="((isPublic && isForm) || (!isPublic && _isUIAllowed('xcDatatableEditable'))) && !readOnly"
:tooltip="`Unlink this '${meta.title}' from '${parentMeta.title}'`"
:color="['error', 'grey']"
small
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/>.
*
*/
-->

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

@ -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/>.
*
*/
-->

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

@ -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>&nbsp; New Record
</v-btn>
</v-card-title>
<v-card-text>
<div class="items-container">
<template v-if="data && data.list && data.list.length">
<v-card v-for="(ch, i) in data.list" :key="i" v-ripple class="ma-2 child-card" outlined @click="$emit('add', ch)">
<v-card-text class="primary-value textColor--text text--lighten-2 d-flex">
<span class="font-weight-bold"> {{ ch[primaryCol] || (ch && Object.values(ch).slice(0, 1).join()) }}&nbsp;</span>
<span v-if="primaryKey" class="grey--text caption primary-key">(Primary Key : {{ ch[primaryKey] }})</span>
<v-spacer />
<v-chip v-if="hm && ch[`${hm._rtn}Read`] && ch[`${hm._rtn}Read`][hmParentPrimaryValCol]" x-small>
{{ ch[`${hm._rtn}Read`][hmParentPrimaryValCol] }}
</v-chip>
</v-card-text>
</v-card>
</template>
<div v-else-if="data" class="text-center py-15 textLight--text">
<!-- No items found -->
{{ $t('placeholder.noItemsFound') }}
</div>
</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/>.
*
*/
-->

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

@ -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 }
}

2
packages/nc-gui-v2/composables/useColumn.ts

@ -69,6 +69,6 @@ export default (column: ColumnType) => {
isAutoSaved,
isManualSaved,
isSingleSelect,
isMultiSelect
isMultiSelect,
}
}

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

@ -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 }
}

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

@ -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 }
}

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

@ -19,5 +19,7 @@ export default () => {
project.value = await $api.project.read(projectId)
}
return { project, tables, loadProject, loadTables }
const isMysql = computed(() => ['mysql', 'mysql2'].includes(project.value?.bases?.[0]?.type || ''))
return { project, tables, loadProject, loadTables, isMysql }
}

32
packages/nc-gui-v2/composables/useVirtualCell.ts

@ -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,
}
}

11
packages/nc-gui-v2/package-lock.json generated

@ -6,6 +6,7 @@
"": {
"dependencies": {
"@vueuse/core": "^8.7.5",
"dayjs": "^1.11.3",
"nocodb-sdk": "file:../nocodb-sdk",
"socket.io-client": "^4.5.1",
"vue-i18n": "^9.1.10",
@ -4322,6 +4323,11 @@
"node": ">= 12"
}
},
"node_modules/dayjs": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
"integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -15626,6 +15632,11 @@
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
"dev": true
},
"dayjs": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
"integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

1
packages/nc-gui-v2/package.json

@ -9,6 +9,7 @@
},
"dependencies": {
"@vueuse/core": "^8.7.5",
"dayjs": "^1.11.3",
"nocodb-sdk": "file:../nocodb-sdk",
"socket.io-client": "^4.5.1",
"vue-i18n": "^9.1.10",

Loading…
Cancel
Save