Browse Source

wip: grid view migration

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/2716/head
Pranav C 2 years ago
parent
commit
0674f6837a
  1. 123
      packages/nc-gui-v2/components/cell/Attachment.vue
  2. 11
      packages/nc-gui-v2/components/cell/Boolean.vue
  3. 40
      packages/nc-gui-v2/components/cell/Currency.vue
  4. 21
      packages/nc-gui-v2/components/cell/Date.vue
  5. 21
      packages/nc-gui-v2/components/cell/DateTime.vue
  6. 69
      packages/nc-gui-v2/components/cell/Duration.vue
  7. 22
      packages/nc-gui-v2/components/cell/Email.vue
  8. 31
      packages/nc-gui-v2/components/cell/Enum.vue
  9. 14
      packages/nc-gui-v2/components/cell/Json.vue
  10. 68
      packages/nc-gui-v2/components/cell/SetList.vue
  11. 19
      packages/nc-gui-v2/components/cell/Time.vue
  12. 23
      packages/nc-gui-v2/components/cell/Url.vue
  13. 2
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  14. 85
      packages/nc-gui-v2/components/editable-cell/Boolean.vue
  15. 92
      packages/nc-gui-v2/components/editable-cell/DatePicker.vue
  16. 132
      packages/nc-gui-v2/components/editable-cell/DateTimePickerCell.vue
  17. 139
      packages/nc-gui-v2/components/editable-cell/DurationCell.vue
  18. 599
      packages/nc-gui-v2/components/editable-cell/EditableAttachmentCell.vue
  19. 80
      packages/nc-gui-v2/components/editable-cell/EditableUrlCell.vue
  20. 132
      packages/nc-gui-v2/components/editable-cell/EnumListEditableCell.vue
  21. 101
      packages/nc-gui-v2/components/editable-cell/EnumRadioEditableCell.vue
  22. 69
      packages/nc-gui-v2/components/editable-cell/FloatCell.vue
  23. 69
      packages/nc-gui-v2/components/editable-cell/IntegerCell.vue
  24. 138
      packages/nc-gui-v2/components/editable-cell/JsonEditableCell.vue
  25. 71
      packages/nc-gui-v2/components/editable-cell/RatingCell.vue
  26. 101
      packages/nc-gui-v2/components/editable-cell/SetListCheckboxCell.vue
  27. 129
      packages/nc-gui-v2/components/editable-cell/SetListEditableCell.vue
  28. 81
      packages/nc-gui-v2/components/editable-cell/TextAreaCell.vue
  29. 105
      packages/nc-gui-v2/components/editable-cell/TextAreaCellOld.vue
  30. 73
      packages/nc-gui-v2/components/editable-cell/TextCell.vue
  31. 111
      packages/nc-gui-v2/components/editable-cell/TimePickerCell.vue
  32. 48
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  33. 189
      packages/nc-gui-v2/components/smartsheet/EditableCell.vue
  34. 204
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  35. 16
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  36. 58
      packages/nc-gui-v2/composables/colors.ts
  37. 72
      packages/nc-gui-v2/composables/column.ts
  38. 6
      packages/nc-gui-v2/composables/project.ts
  39. 2
      packages/nc-gui-v2/composables/tabs.ts
  40. 18
      packages/nc-gui-v2/layouts/default.vue
  41. 9
      packages/nc-gui-v2/layouts/withoutSidebar.vue
  42. 22
      packages/nc-gui-v2/pages/dashboard/[projectId].vue
  43. 2
      packages/nc-gui-v2/pages/signin.vue
  44. 2
      packages/nc-gui-v2/pages/signup.vue

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

@ -0,0 +1,123 @@
<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>
</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>
</div>
</template>
</div>
</template>
<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>
<style scoped>
.img-container {
margin: 0 -2px;
position: relative;
}
.thumbnail {
height: 29px;
width: 29px;
margin: 2px;
border-radius: 4px;
}
.thumbnail img {
max-height: 29px;
max-width: 29px;
}
.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;
}
</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

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

40
packages/nc-gui-v2/components/cell/Currency.vue

@ -0,0 +1,40 @@
<template>
<a v-if="value">{{ currency }}</a>
<span v-else />
</template>
<script>
export default {
name: 'CurrencyCell',
props: {
column: Object,
value: [String, Number]
},
computed: {
currency() {
try {
return isNaN(this.value)
? this.value
: new Intl.NumberFormat(this.currencyMeta.currency_locale || 'en-US',
{ style: 'currency', currency: this.currencyMeta.currency_code || 'USD' })
.format(this.value)
} catch (e) {
return this.value
}
},
currencyMeta() {
return {
currency_locale: 'en-US',
currency_code: 'USD',
...(this.column && this.column.meta
? this.column.meta
: {})
}
}
}
}
</script>
<style scoped>
</style>

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

@ -0,0 +1,21 @@
<template>
<span>{{ date }}</span>
</template>
<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>
<style scoped>
</style>

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

@ -0,0 +1,21 @@
<template>
<span>{{ dateTime }}</span>
</template>
<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>
<style scoped>
</style>

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

@ -0,0 +1,69 @@
<template>
<input
v-model="localValue"
:placeholder="durationPlaceholder"
readonly
>
</template>
<script>
import { durationOptions, convertMS2Duration } from '~/helpers/durationHelper'
export default {
name: 'DurationCell',
props: {
column: Object,
value: [String, Number]
},
data: () => ({
showWarningMessage: false,
localValue: null
}),
computed: {
durationPlaceholder() {
return durationOptions[this.column?.meta?.duration || 0].title
}
},
watch: {
'column.meta.duration'(newValue, oldValue) {
if (oldValue !== newValue) {
this.localValue = convertMS2Duration(this.value, newValue)
}
},
value(val, oldVal) {
this.localValue = convertMS2Duration(val !== oldVal && (!val && val !== 0) ? oldVal : val, this.column?.meta?.duration || 0)
}
},
created() {
this.localValue = convertMS2Duration(this.value, this.column?.meta?.duration || 0)
}
}
</script>
<style scoped>
</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/>.
*
*/
-->

22
packages/nc-gui-v2/components/cell/Email.vue

@ -0,0 +1,22 @@
<template>
<a v-if="isEmail" :href="`mailto:${value}`" target="_blank">{{ value }}</a>
<span v-else>{{ value }}</span>
</template>
<script>
import { isEmail } from '~/helpers'
export default {
name: 'EmailCell',
props: ['value'],
computed: {
isEmail() {
return isEmail(this.value || '')
}
}
}
</script>
<style scoped>
</style>

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

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { inject } from 'vue'
import { enumColor } from '~/composables/colors'
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>

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

@ -0,0 +1,14 @@
<template>
<pre class="text-left caption">{{ value }}</pre>
</template>
<script>
export default {
name: 'JsonCell',
props: ['value']
}
</script>
<style scoped>
</style>

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

@ -0,0 +1,68 @@
<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/>.
*
*/
-->

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

@ -0,0 +1,19 @@
<template>
<span>{{ time }}</span>
</template>
<script>
export default {
name: 'TimeCell',
props: ['value'],
computed: {
time() {
return typeof this.value === 'string' ? this.value.replace(/(\d)T(?=\d)/, '$1 ') : this.value
}
}
}
</script>
<style scoped>
</style>

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

@ -0,0 +1,23 @@
<template>
<a v-if="isValid" :href="value" target="_blank">{{ value }}</a>
<span v-else>{{ value }}</span>
</template>
<script>
import { isValidURL } from '~/helpers'
export default {
name: 'UrlCell',
props: ['value'],
computed: {
isValid() {
return this.value && isValidURL(this.value)
}
}
}
</script>
<style scoped>
</style>

2
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -8,7 +8,7 @@ const { addTab } = useTabs()
<template>
<div>
<v-list density="medium">
<v-list>
<v-list-item
v-for="table in tables"
:key="table.id"

85
packages/nc-gui-v2/components/editable-cell/Boolean.vue

@ -0,0 +1,85 @@
<template>
<div class="d-flex align-center " :class="{'justify-center':!isForm,'nc-cell-hover-show': !localState}">
<v-icon small :color="checkboxMeta.color" @click="toggle">
{{ localState ? checkedIcon :uncheckedIcon }}
</v-icon>
</div>
</template>
<script>
export default {
name: 'BooleanCell',
props: {
column: Object,
value: [String, Number, Boolean],
isForm: Boolean,
readOnly: Boolean
},
computed: {
checkedIcon() {
return (this.checkboxMeta && this.checkboxMeta.icon && this.checkboxMeta.icon.checked) || 'mdi-check-bold'
},
uncheckedIcon() {
return (this.checkboxMeta && this.checkboxMeta.icon && this.checkboxMeta.icon.unchecked) || 'mdi-crop-square'
},
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
parentListeners() {
const $listeners = {}
return $listeners
},
checkboxMeta() {
return {
icon: {
checked: 'mdi-check-circle-outline',
unchecked: 'mdi-checkbox-blank-circle-outline'
},
color: 'primary',
...(this.column && this.column.meta
? this.column.meta
: {})
}
}
},
methods: {
toggle() {
this.localState = !this.localState
}
}
}
</script>
<style scoped>
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

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

@ -0,0 +1,92 @@
<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>
</template>
<script>
import dayjs from 'dayjs'
export default {
name: 'DatePickerCell',
props: {
value: [String, Date]
},
computed: {
localState: {
get() {
if (!this.value || !dayjs(this.value).isValid()) { return undefined }
return (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value)).format('YYYY-MM-DD')
},
set(val) {
if (dayjs(val).isValid()) {
this.$emit('input', val && dayjs(val).format('YYYY-MM-DD'))
}
}
},
date() {
if (!this.value || this.localState) {
return this.localState
}
return 'Invalid Date'
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
mounted() {
if (this.$el && this.$el.$el) {
this.$el.$el.focus()
}
}
}
</script>
<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/>.
*
*/
-->

132
packages/nc-gui-v2/components/editable-cell/DateTimePickerCell.vue

@ -0,0 +1,132 @@
<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 {{ this.value }}
</div>
</div>
</template>
<script>
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
export default {
name: 'DateTimePickerCell',
props: {
value: [String, Date, Number], ignoreFocus: Boolean
},
data: () => ({
showMessage: false
}),
computed: {
isMysql() {
return ['mysql', 'mysql2'].indexOf(this.$store.getters['project/GtrClientType'])
},
localState: {
get() {
if (!this.value) {
return this.value
}
const d = (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value))
if (d.isValid()) {
this.showMessage = false
return d.format('YYYY-MM-DD HH:mm')
} else {
this.showMessage = true
}
},
set(value) {
if (this.isMysql) {
this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ss'))
} else {
this.$emit('input', value && dayjs(value).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
}
return $listeners
}
},
mounted() {
// listen dialog click:outside event and save on close
if (this.$refs.picker && this.$refs.picker.$children && this.$refs.picker.$children[0]) {
this.$refs.picker.$children[0].$on('click:outside', () => {
this.$refs.picker.okHandler()
})
}
if (!this.ignoreFocus) {
this.$refs.picker.display = true
}
}
}
</script>
<style scoped>
/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;
}
</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/>.
*
*/
-->

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

@ -0,0 +1,139 @@
<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>
<script>
import { durationOptions, convertMS2Duration, convertDurationToSeconds } 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>
<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/>.
*
*/
-->

599
packages/nc-gui-v2/components/editable-cell/EditableAttachmentCell.vue

@ -0,0 +1,599 @@
<template>
<div
class="main h-100"
@dragover.prevent="dragOver = true"
@dragenter.prevent="dragOver = true"
@dragexit="dragOver = false"
@dragleave="dragOver = false"
@dragend="dragOver = false"
@drop.prevent.stop="onFileDrop"
>
<div v-show="(isForm || _isUIAllowed('tableAttachment')) && dragOver" class="drop-overlay">
<div>
<v-icon small>
mdi-cloud-upload-outline
</v-icon>
<span class="caption font-weight-bold">Drop here</span>
</div>
</div>
<div class="d-flex align-center img-container">
<div class="d-flex no-overflow">
<div
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
: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)"
>
<template #placeholder>
<v-skeleton-loader
type="image"
:height="active ? 33 : 22"
:width="active ? 33 : 22"
/>
</template>
</v-img>
<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
v-if="isForm || active && !isPublicGrid && !isLocked"
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="">
mdi-plus
</v-icon>
Attachment
</v-btn>
<v-icon
v-else-if="_isUIAllowed('tableAttachment')"
v-show="active"
small
color="primary nc-attachment-add-icon"
>
mdi-plus
</v-icon>
</div>
<v-spacer />
<v-icon class="expand-icon mr-1" x-small color="primary" @click.stop="dialog = true">
mdi-arrow-expand
</v-icon>
<input ref="file" type="file" multiple class="d-none" @change="onFileSelection">
</div>
<v-dialog
v-if="dialog"
v-model="dialog"
width="800"
>
<v-card class="h-100 images-modal">
<v-card-text class="h-100 backgroundColor">
<div class="d-flex mx-2">
<v-btn
v-if="(isForm || _isUIAllowed('tableAttachment')) && !isPublicGrid && !isLocked"
small
class="my-4 "
:loading="uploading"
@click="addFile"
>
<v-icon small class="mr-2">
mdi-link-variant
</v-icon>
<span class="caption">Attach File</span>
</v-btn>
<!-- <v-text-field v-model="urlString" @keypress.enter="uploadByUrl" />-->
</div>
<div class="d-flex flex-wrap h-100">
<v-container fluid style="max-height:calc(90vh - 80px);overflow-y: auto">
<draggable
v-model="localState"
class="row"
@update="onOrderUpdate"
>
<v-col v-for="(item,i) in (isPublicForm ? localFilesState : localState)" :key="i" cols="4">
<v-card
class="modal-thumbnail-card align-center justify-center d-flex"
height="200px"
style="position: relative"
>
<v-icon
v-if="_isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked"
small
class="remove-icon"
@click="removeItem(i)"
>
mdi-close-circle
</v-icon>
<v-icon color="grey" class="download-icon" @click.stop="downloadItem(item,i)">
mdi-download
</v-icon>
<div class="pa-2 d-flex align-center" style="height:200px">
<img
v-if="isImage(item.title, item.mimetype)"
style="max-height: 100%;max-width: 100%"
alt="#"
:src="item.url || item.data"
@click="selectImage(item.url,i)"
>
<v-icon v-else-if="item.icon" size="33" @click="openUrl(item.url || item.data,'_blank')">
{{
item.icon
}}
</v-icon>
<v-icon v-else size="33" @click="openUrl(item.url || item.data,'_blank')">
mdi-file
</v-icon>
</div>
</v-card>
<p class="caption mt-2 modal-title" :title="item.title">
{{ item.title }}
</p>
</v-col>
</draggable>
</v-container>
</div>
</v-card-text>
</v-card>
</v-dialog>
<v-overlay v-if="showImage" v-model="showImage" z-index="99999" opacity=".93">
<div v-click-outside="hideIfVisible" class="image-overlay-container">
<template v-if="showImage && selectedImage">
<v-carousel v-model="carousel" height="calc(100vh - 100px)" hide-delimiters>
<v-carousel-item
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
:key="i"
>
<div class="mx-auto d-flex flex-column justify-center align-center" style="min-height:100px">
<p class="title text-center">
{{ item.title }}
<v-icon class="ml-3" color="grey" @click.stop="downloadItem(item,i)">
mdi-download
</v-icon>
</p>
<div style="width:90vh;height:calc(100vh - 150px)" class="d-flex align-center justify-center">
<img
v-if="isImage(item.title, item.mimetype)"
style="max-width:90vh;max-height:calc(100vh - 100px)"
:src="item.url || item.data"
>
<v-icon v-else-if="item.icon" size="55">
{{ item.icon }}
</v-icon>
<v-icon v-else size="55">
mdi-file
</v-icon>
</div>
</div>
</v-carousel-item>
</v-carousel>
</template>
<v-sheet
v-if="showImage"
class="mx-auto align-center justify-center"
max-width="90vw"
height="80px"
style="background: transparent"
>
<v-slide-group
multiple
show-arrows
>
<v-slide-item
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
:key="i"
>
<v-card
:key="i"
class="ma-2 pa-2 d-flex align-center justify-center overlay-thumbnail"
:class="{active: carousel === i}"
width="48"
height="48"
@click="carousel = i"
>
<img
v-if="isImage(item.title, item.mimetype)"
style="max-width:100%;max-height:100%"
:src="item.url || item.data"
>
<v-icon v-else-if="item.icon" size="48">
{{ item.icon }}
</v-icon>
<v-icon v-else size="48">
mdi-file
</v-icon>
</v-card>
</v-slide-item>
</v-slide-group>
</v-sheet>
<v-icon x-large class="close-icon" @click="showImage=false">
mdi-close-circle
</v-icon>
</div>
</v-overlay>
</div>
</template>
<script>
import FileSaver from 'file-saver'
import draggable from 'vuedraggable'
import { isImage } from '@/components/project/spreadsheet/helpers/imageExt'
export default {
name: 'EditableAttachmentCell',
components: { draggable },
props: ['dbAlias', 'value', 'active', 'isLocked', 'meta', 'column', 'isPublicGrid', 'isForm', 'isPublicForm', 'viewId'],
data: () => ({
carousel: null,
uploading: false,
localState: '',
dialog: false,
showImage: false,
selectedImage: null,
dragOver: false,
localFilesState: [],
urlString: ''
}),
watch: {
value(val, prev) {
try {
this.localState = ((typeof val === 'string' && val !== prev ? JSON.parse(val) : val) || []).filter(Boolean)
} catch (e) {
this.localState = []
}
}
},
created() {
try {
this.localState = ((typeof this.value === 'string' ? JSON.parse(this.value) : this.value) || []).filter(Boolean)
} catch (e) {
this.localState = []
}
document.addEventListener('keydown', this.onArrowDown)
},
beforeDestroy() {
document.removeEventListener('keydown', this.onArrowDown)
},
mounted() {
},
methods: {
async uploadByUrl() {
const data = await this.$api.storage.uploadByUrl(
{
path: ['noco', this.projectName, this.meta.title, this.column.title].join('/')
},
[{
url: this.urlString
}]
)
this.localState.push(...data)
},
openUrl(url, target) {
window.open(url, target)
},
isImage,
hideIfVisible() {
if (this.showImage) {
this.showImage = false
}
},
selectImage(selectedImage, i) {
this.carousel = i
this.selectedImage = selectedImage
this.showImage = true
},
addFile() {
if (!this.isLocked) {
this.$refs.file.click()
}
},
async onFileSelection() {
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
}
this.uploading = true
for (const file of this.$refs.file.files) {
try {
const data = await this.$api.storage.upload(
{
path: ['noco', this.projectName, this.meta.title, this.column.title].join('/')
}, {
files: file,
json: '{}'
}
)
this.localState.push(...data)
} catch (e) {
this.$toast.error((e.message) || 'Some internal error occurred').goAway(3000)
this.uploading = false
return
}
}
this.uploading = false
this.$emit('input', JSON.stringify(this.localState))
this.$emit('update')
},
onOrderUpdate() {
this.$emit('input', JSON.stringify(this.localState))
this.$emit('update')
},
removeItem(i) {
if (this.isPublicForm) {
this.localFilesState.splice(i, 1)
this.$emit('input', this.localFilesState.map(f => f.file))
} else {
this.localState.splice(i, 1)
this.$emit('input', JSON.stringify(this.localState))
}
this.$emit('update')
},
downloadItem(item) {
FileSaver.saveAs(item.url || item.data, item.title)
},
onArrowDown(e) {
if (!this.showImage) {
return
}
e = e || window.event
// eslint-disable-next-line eqeqeq
if (e.keyCode == '37') {
this.carousel = (this.carousel || this.localState.length) - 1
// eslint-disable-next-line eqeqeq
} else if (e.keyCode == '39') {
this.carousel = ++this.carousel % this.localState.length
// eslint-disable-next-line eqeqeq
} else if (e.keyCode == '27') {
this.hideIfVisible()
}
},
async onFileDrop(e) {
this.dragOver = false
this.$refs.file.files = e.dataTransfer.files
await this.onFileSelection()
}
}
}
</script>
<style scoped lang="scss">
.img-container {
margin: 0 -2px;
}
.no-overflow {
overflow: hidden;
}
.add {
transition: .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: .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: .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: .4s transform, .4s opacity;
opacity: .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: .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: .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>
* @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/>.
*
*/
-->

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

@ -0,0 +1,80 @@
<template>
<input v-model="localState" v-on="parentListeners">
</template>
<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>
<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/>.
*
*/
-->

132
packages/nc-gui-v2/components/editable-cell/EnumListEditableCell.vue

@ -0,0 +1,132 @@
<template>
<v-select
v-model="localState"
solo
dense
flat
:items="enumValues"
hide-details
class="mt-0"
:clearable="!column.rqd"
v-on="parentListeners"
>
<template #selection="{item}">
<div
class="d-100"
:class="{
'text-center' : !isForm
}"
>
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]" class="ma-1">
{{ item }}
</v-chip>
</div>
</template>
<template #item="{item}">
<v-chip small :color="colors[enumValues.indexOf(item) % colors.length]">
{{ item }}
</v-chip>
</template>
<template #append>
<v-icon small class="mt-1">
mdi-menu-down
</v-icon>
</template>
</v-select>
</template>
<script>
import colors from '@/mixins/colors'
export default {
name: 'EnumListEditableCell',
mixins: [colors],
props: {
value: String,
column: Object,
isForm: Boolean
},
computed: {
localState: {
get() {
return this.value && this.value.replace(/\\'/g, '\'').replace(/^'|'$/g, '')
},
set(val) {
this.$emit('input', val)
}
},
enumValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp
.split(',')
.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>
<style scoped lang="scss">
::v-deep {
.v-select {
min-width: 150px;
}
.v-input__slot{
padding-right: 0 !important;
padding-left: 35px !important;
}
.v-input__icon.v-input__icon--clear {
width: 15px !important;
min-width: 13px !important;
.v-icon {
font-size: 13px !important;
}
}
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

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

@ -0,0 +1,101 @@
<template>
<div class="d-flex align-center">
<div>
<div v-for="(val,i) of enumValues" :key="val" class="item">
<input :id="`key-radio-${val}`" v-model="localState" type="radio" class="orange--text" :value="val">
<label
class="py-1 px-3 d-inline-block my-1 label"
:for="`key-radio-${val}`"
:style="{
background:colors[i % colors.length ]
}"
>{{ val }}</label>
</div>
</div>
</div>
</template>
<script>
import { enumColor as colors } from '@/components/project/spreadsheet/helpers/colors'
export default {
name: 'EnumRadioEditableCell',
props: {
value: String,
column: Object
},
computed: {
colors() {
return this.$store.state.settings.darkTheme ? colors.dark : colors.light
},
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
this.$emit('update')
}
},
enumValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map(v => v.replace(/^'|'$/g, ''))
}
return []
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
mounted() {
// this.$el.focus();
// let event;
// event = document.createEvent('MouseEvents');
// event.initMouseEvent('mousedown', true, true, window);
// this.$el.dispatchEvent(event);
}
}
</script>
<style scoped>
.label {
border-radius: 25px;
}
.item {
white-space: nowrap;
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

69
packages/nc-gui-v2/components/editable-cell/FloatCell.vue

@ -0,0 +1,69 @@
<template>
<input v-model="localState" type="number" v-on="parentListeners">
</template>
<script>
export default {
name: 'FloatCell',
props: {
value: [String, Number]
},
computed: {
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', +val)
}
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
mounted() {
this.$el.focus()
}
}
</script>
<style scoped>
input {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

69
packages/nc-gui-v2/components/editable-cell/IntegerCell.vue

@ -0,0 +1,69 @@
<template>
<input v-model="localState" type="number" v-on="parentListeners">
</template>
<script>
export default {
name: 'IntegerCell',
props: {
value: [String, Number]
},
computed: {
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', parseInt(val, 10))
}
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
mounted() {
this.$el.focus()
}
}
</script>
<style scoped>
input {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

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

@ -0,0 +1,138 @@
<template>
<v-dialog :is="expand ? 'v-dialog' : 'div'" v-model="expand" max-width="800px" class="cell-container" @keydown.stop.enter>
<div class="d-flex pa-1 " :class="{backgroundColor:expand}">
<v-spacer />
<v-icon small class="mr-2" @click="expand = !expand">
{{ expand ? 'mdi-arrow-collapse' : 'mdi-arrow-expand' }}
</v-icon>
<template v-if="!isForm">
<v-btn outlined x-small class="mr-1" @click="$emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn x-small color="primary" :disabled="!isValid" @click="save">
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
</template>
<v-btn v-else-if="expand" x-small @click="expand=false">
<!-- Close -->
{{ $t('general.close') }}
</v-btn>
</div>
<monaco-json-object-editor
v-if="expand"
v-model="localState"
class="text-left caption"
style="width: 300px;min-height:min(600px,80vh);min-width:100%; "
@validate="validate"
/>
<monaco-json-object-editor
v-else
v-model="localState"
class="text-left caption"
style="width: 300px;min-height:200px;min-width:100%;"
@validate="validate"
/>
<div v-show="error" class="px-2 py-1 text-left caption error--text">
{{ error }}
</div>
</v-dialog>
</template>
<script>
import MonacoJsonObjectEditor from '@/components/monaco/MonacoJsonObjectEditor'
export default {
name: 'JsonEditableCell',
components: { MonacoJsonObjectEditor },
props: {
value: [String, Object],
isForm: Boolean
},
data: () => ({
localState: '',
expand: false,
isValid: true,
error: undefined
}),
computed: {
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
watch: {
value(val) {
try {
this.localState = typeof val === 'string' ? JSON.parse(val) : val
} catch (e) {
// ignore parse error for invalid JSON
}
},
localState(val) {
if (this.isForm) {
this.$emit('input', JSON.stringify(val))
}
}
},
created() {
try {
this.localState = typeof this.value === 'string' ? JSON.parse(this.value) : this.value
} catch (e) {
// ignore parse error for invalid JSON
}
},
mounted() {
},
methods: {
save() {
this.expand = false
this.$emit('input', JSON.stringify(this.localState))
},
validate(n, e) {
this.isValid = n
this.error = e
}
}
}
</script>
<style scoped>
.cell-container {
width: 100%
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

71
packages/nc-gui-v2/components/editable-cell/RatingCell.vue

@ -0,0 +1,71 @@
<template>
<div class="d-100 h-100" :class="{'nc-cell-hover-show': localState == 0 || !localState}">
<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 }}
</v-icon>
<v-icon
v-else
:color="ratingMeta.color"
:size="15"
class="nc-cell-hover-show"
@click="click"
>
{{ emptyIcon }}
</v-icon>
</template>
</v-rating>
</div>
</template>
<script>
export default {
name: 'RatingCell',
props: {
column: Object,
value: [String, Number],
readOnly: Boolean
},
computed: {
fullIcon() {
return (this.ratingMeta && this.ratingMeta.icon && this.ratingMeta.icon.full) || 'mdi-star'
},
emptyIcon() {
return (this.ratingMeta && this.ratingMeta.icon && this.ratingMeta.icon.empty) || 'mdi-star-outline'
},
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
ratingMeta() {
return {
icon: {
full: 'mdi-star',
empty: 'mdi-star-outline'
},
color: '#fcb401',
max: 5,
...(this.column && this.column.meta
? this.column.meta
: {})
}
}
}
}
</script>
<style scoped>
</style>

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

@ -0,0 +1,101 @@
<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>
<script>
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>
<style scoped>
.label {
border-radius: 25px;
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

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

@ -0,0 +1,129 @@
<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>
<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>
<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/>.
*
*/
-->

81
packages/nc-gui-v2/components/editable-cell/TextAreaCell.vue

@ -0,0 +1,81 @@
<template>
<textarea
ref="textarea"
v-model="localState"
rows="4"
v-on="parentListeners"
@keydown.alt.enter.stop
@keydown.shift.enter.stop
/>
</template>
<script>
export default {
name: 'TextAreaCell',
props: {
value: String
},
computed: {
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
created() {
this.localState = this.value
},
mounted() {
this.$refs.textarea && this.$refs.textarea.focus()
}
}
</script>
<style scoped>
input, textarea {
width: 100%;
min-height: 60px;
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/>.
*
*/
-->

105
packages/nc-gui-v2/components/editable-cell/TextAreaCellOld.vue

@ -0,0 +1,105 @@
<template>
<div>
<div v-if="!isForm" class="d-flex ma-1">
<v-spacer />
<v-btn v-if="!isForm" outlined x-small class="mr-1" @click="$emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn v-if="!isForm" x-small color="primary" @click="save">
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
</div>
<textarea
ref="textarea"
v-model="localState"
rows="3"
v-on="parentListeners"
@input="isForm && save()"
@keydown.stop.enter
/>
</div>
</template>
<script>
export default {
name: 'TextAreaCell',
props: {
value: String,
isForm: Boolean
},
data: () => ({
localState: ''
}),
computed: {
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
}
},
watch: {
value(val) {
this.localState = val
},
localState(val) {
if (this.isForm) {
this.$emit('input', val)
}
}
},
created() {
this.localState = this.value
},
mounted() {
this.$refs.textarea && this.$refs.textarea.focus()
},
methods: {
save() {
this.$emit('input', this.localState)
}
}
}
</script>
<style scoped>
input, textarea {
width: 100%;
min-height:60px;
height: calc(100% - 28px);
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/>.
*
*/
-->

73
packages/nc-gui-v2/components/editable-cell/TextCell.vue

@ -0,0 +1,73 @@
<template>
<input v-model="localState" v-on="parentListeners">
</template>
<script>
export default {
name: 'TextCell',
props: {
value: [String, Object, Number, Boolean, Array]
},
computed: {
localState: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
if (this.$listeners.cancel) {
$listeners.cancel = this.$listeners.cancel
}
return $listeners
}
},
mounted() {
this.$el.focus()
}
}
</script>
<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/>.
*
*/
-->

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

@ -0,0 +1,111 @@
<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>
<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>
<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/>.
*
*/
-->

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

@ -0,0 +1,48 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { useColumn } from '~/composables/column'
const { column, value } = defineProps<{ column: ColumnType; value: any }>()
provide('column', column)
provide('value', value)
const {
isSet,
isEnum,
isURL,
isEmail,
isJSON,
isDate,
isDateTime,
isTime,
isBoolean,
isDuration,
isRating,
isCurrency,
isAttachment,
isTextArea,
} = 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="title"
>{{ value }}</span>
</template>
<style scoped>
</style>

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

@ -0,0 +1,189 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { useColumn } from '~/composables/column'
const { column, value } = defineProps<{ column: ColumnType; value: any }>()
provide('column', column)
provide('value', value)
const {
isSet,
isEnum,
isURL,
isEmail,
isJSON,
isDate,
isDateTime,
isTime,
isBoolean,
isDuration,
isRating,
isCurrency,
isAttachment,
isTextArea,
} = useColumn(column)
</script>
<template>
<div
class="nc-cell"
@keydown.stop.left
@keydown.stop.right
@keydown.stop.up
@keydown.stop.down
>
<EditableAttachmentCell
v-if="isAttachment"
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"
/>
<RatingCell
v-else-if="isRating"
v-model="localState"
:active="active"
:is-form="isForm"
:column="column"
:is-public-grid="isPublic && !isForm"
:is-public-form="isPublic && isForm"
:is-locked="isLocked"
v-on="$listeners"
/>
<DurationCell
v-else-if="isDuration"
v-model="localState"
:active="active"
:is-form="isForm"
:column="column"
:is-locked="isLocked"
v-on="parentListeners"
/>
<BooleanCell
v-else-if="isBoolean"
v-model="localState"
:column="column"
:is-form="isForm"
v-on="parentListeners"
/>
<IntegerCell
v-else-if="isInt"
v-model="localState"
v-on="parentListeners"
/>
<FloatCell
v-else-if="isFloat"
v-model="localState"
v-on="parentListeners"
/>
<DatePickerCell
v-else-if="isDate"
v-model="localState"
v-on="parentListeners"
/>
<TimePickerCell
v-else-if="isTime"
v-model="localState"
v-on="parentListeners"
@save="$emit('save')"
/>
<DateTimePickerCell
v-else-if="isDateTime"
v-model="localState"
ignore-focus
v-on="parentListeners"
/>
<EnumCell
v-else-if="isEnum && ((!isForm && !active) || isLocked || (isPublic && !isForm))"
v-model="localState"
:column="column"
v-on="parentListeners"
/>
<EnumListCell
v-else-if="isEnum"
v-model="localState"
:is-form="isForm"
:column="column"
v-on="parentListeners"
/>
<JsonEditableCell
v-else-if="isJSON"
v-model="localState"
:is-form="isForm"
v-on="parentListeners"
@input="$emit('save')"
/>
<SetListEditableCell
v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)"
v-model="localState"
:column="column"
v-on="parentListeners"
/>
<SetListCell
v-else-if="isSet"
v-model="localState"
:column="column"
v-on="parentListeners"
/>
<EditableUrlCell v-else-if="isURL" v-model="localState" v-on="parentListeners" />
<TextCell v-else-if="isString" v-model="localState" v-on="parentListeners" />
<TextAreaCell
v-else-if="isTextArea"
v-model="localState"
:is-form="isForm"
v-on="parentListeners"
/>
<TextCell 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>
div {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
}
.nc-hint {
font-size: .61rem;
color: grey;
}
.nc-cell {
position: relative;
}
.nc-locked-overlay {
position: absolute;
z-index: 2;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
</style>

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

@ -1,12 +1,12 @@
<script lang="ts" setup>
import { inject, ComputedRef } from 'vue'
import { isVirtualCol } from 'nocodb-sdk'
import type { TableType } from 'nocodb-sdk'
import { useNuxtApp } from '#app'
import type { TabItem } from '~/composables/tabs'
interface Props {
tabMeta: Record<string, any>
meta: Record<string, any>
}
const { tabMeta, meta } = defineProps<Props>()
const tabMeta = inject<TabItem>('tabMeta')
const meta = inject<ComputedRef<TableType>>('meta')
const { project } = useProject()
const rows = ref()
@ -17,44 +17,172 @@ const loadData = async () => {
const response = await $api.dbTableRow.list(
'noco',
project.value.id!,
meta.id,
{},
{
headers: {
'xc-auth': $state.value.token,
},
},
meta.id
)
rows.value = response.list
}
onMounted(async () => {
await loadData()
})
onMounted(loadData)
</script>
<template>
<div>
<div class="card">
<v-table>
<thead>
<tr>
<th v-for="(col, i) of meta.columns" :key="`${col.title}-${i}`">
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in rows" :key="i">
<th v-for="col in meta.columns" :key="col.title">
{{ row[col.title] }}
</th>
</tr>
</tbody>
</v-table>
</div>
</div>
<table
class="xc-row-table nc-grid backgroundColorDefault"
>
<thead>
<tr>
<th>#</th>
<th v-for="(col) in meta.columns" :key="col.title">
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, rowIndex) in rows"
:key="rowIndex"
class="nc-grid-row"
>
<td
key="row-index"
style="width: 65px"
class="caption nc-grid-cell"
>
<div class="d-flex align-center">
{{ rowIndex + 1 }}
</div>
</td>
<td
v-for="(columnObj) in meta.columns"
:key="rowIndex + columnObj.title"
class="cell pointer nc-grid-cell"
:class="{
// 'active':
// !isPublicView
// && selected.col === col
// && selected.row === row
// && isEditable,
// 'primary-column': primaryValueColumn === columnObj.title,
// 'text-center': isCentrallyAligned(columnObj),
// 'required': isRequired(columnObj, rowObj),
}"
:data-col="columnObj.title"
>
<!-- @dblclick="makeEditable(col, row, columnObj.ai, rowMeta)" -->
<!-- @click="makeSelected(col, row)" -->
<!-- @contextmenu=" -->
<!-- showRowContextMenu($event, rowObj, rowMeta, row, col, columnObj) -->
<!-- " -->
<!-- > -->
<!-- <virtual-cell -->
<!-- v-if="isVirtualCol(columnObj)" -->
<!-- :password="password" -->
<!-- :is-public="isPublicView" -->
<!-- :metas="metas" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->
<!-- :row="rowObj" -->
<!-- :nodes="nodes" -->
<!-- :meta="meta" -->
<!-- :api="api" -->
<!-- :active="selected.col === col && selected.row === row" -->
<!-- :sql-ui="sqlUi" -->
<!-- :is-new="rowMeta.new" -->
<!-- v-on="$listeners" -->
<!-- @updateCol=" -->
<!-- (...args) => -->
<!-- updateCol( -->
<!-- ...args, -->
<!-- columnObj.bt -->
<!-- && meta.columns.find( -->
<!-- (c) => c.column_name === columnObj.bt.column_name, -->
<!-- ), -->
<!-- col, -->
<!-- row, -->
<!-- ) -->
<!-- " -->
<!-- @saveRow="onCellValueChange(col, row, columnObj, true)" -->
<!-- /> -->
<!-- <editable-cell -->
<!-- v-else-if=" -->
<!-- ((isPkAvail || rowMeta.new) -->
<!-- && !isView -->
<!-- && !isLocked -->
<!-- && !isPublicView -->
<!-- && editEnabled.col === col -->
<!-- && editEnabled.row === row) -->
<!-- || enableEditable(columnObj) -->
<!-- " -->
<!-- v-model="rowObj[columnObj.title]" -->
<!-- :column="columnObj" -->
<!-- :meta="meta" -->
<!-- :active="selected.col === col && selected.row === row" -->
<!-- :sql-ui="sqlUi" -->
<!-- :db-alias="nodes.dbAlias" -->
<!-- :is-locked="isLocked" -->
<!-- :is-public="isPublicView" -->
<!-- :view-id="viewId" -->
<!-- @save="editEnabled = {}; onCellValueChange(col, row, columnObj, true);" -->
<!-- @cancel="editEnabled = {}" -->
<!-- @update="onCellValueChange(col, row, columnObj, false)" -->
<!-- @blur="onCellValueChange(col, row, columnObj, true)" -->
<!-- @input="unsaved = true" -->
<!-- @navigateToNext="navigateToNext" -->
<!-- @navigateToPrev="navigateToPrev" -->
<!-- /> -->
<span v-if="isVirtualCol(columnObj)" />
<SmartsheetCell
v-else
:class="{
// 'primary--text': primaryValueColumn === columnObj.title,
}"
:column="columnObj"
:value="row[columnObj.title]"
/>
<!-- :selected="selected.col === col && selected.row === row" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->
<!-- :meta="meta" -->
<!-- :db-alias="nodes.dbAlias" -->
<!-- :value="rowObj[columnObj.title]" -->
<!-- :sql-ui="sqlUi" -->
<!-- @enableedit=" -->
<!-- makeSelected(col, row); -->
<!-- makeEditable(col, row, columnObj.ai, rowMeta); -->
<!-- " -->
<!-- /> -->
</td>
</tr>
</tbody>
</table>
</template>
<style scoped></style>
<style scoped lang="scss">
td,
tr {
min-height: 31px !important;
position: relative;
padding: 0 5px !important;
min-width: 200px;
}
table,
td,
th {
border-right: 1px solid #7f828b33 !important;
border-left: 1px solid #7f828b33 !important;
border-bottom: 1px solid #7f828b33 !important;
border-top: 1px solid #7f828b33 !important;
border-collapse: collapse;
font-size:.8rem;
}
td{
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted, provide, watch } from 'vue'
import { useMetas } from '~/composables/metas'
const { tabMeta } = defineProps({
@ -16,6 +16,10 @@ onMounted(async () => {
await getMeta(tabMeta?.id)
})
provide('meta', meta)
provide('tabMeta', tabMeta)
watch(
() => tabMeta && tabMeta?.id,
async (newVal, oldVal) => {
@ -25,9 +29,15 @@ watch(
</script>
<template>
<div>
<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"
/>
<template v-if="meta && tabMeta">
<SmartsheetGrid :meta="meta" :tab-meta="tabMeta" />
<SmartsheetGrid />
</template>
</div>
</template>

58
packages/nc-gui-v2/composables/colors.ts

@ -0,0 +1,58 @@
export default {
light: [
'#ffdce5',
'#fee2d5',
'#ffeab6',
'#d1f7c4',
'#ede2fe',
'#eee',
'#cfdffe',
'#d0f1fd',
'#c2f5e8',
'#ffdaf6',
],
dark: [
'#f82b6099',
'#ff6f2c99',
'#fcb40099',
'#20c93399',
'#8b46ff99',
'#666',
'#2d7ff999',
'#18bfff99',
'#20d9d299',
'#ff08c299',
],
}
const enumColor = {
light: [
'#cfdffe',
'#d0f1fd',
'#c2f5e8',
'#ffdaf6',
'#ffdce5',
'#fee2d5',
'#ffeab6',
'#d1f7c4',
'#ede2fe',
'#eeeeee',
],
dark: [
'#2d7ff999',
'#18bfff99',
'#20d9d299',
'#ff08c299',
'#f82b6099',
'#ff6f2c99',
'#fcb40099',
'#20c93399',
'#8b46ff99',
'#666',
],
}
export {
enumColor,
}

72
packages/nc-gui-v2/composables/column.ts

@ -0,0 +1,72 @@
import { SqlUiFactory, UITypes, isVirtualCol } from 'nocodb-sdk'
import { useProject } from '~/composables/project'
export const useColumn = (column) => {
const { project } = useProject()
const uiDatatype = column && column.uidt
const abstractType = isVirtualCol(column) ? null : SqlUiFactory.create(project.value?.bases?.[0]?.config || { client: 'mysql2' }).getAbstractType(column)
const dataTypeLow = column && column.dt && column.dt.toLowerCase()
const isBoolean = abstractType === 'boolean'
const isString = abstractType === 'string'
const isTextArea = uiDatatype === UITypes.LongText
const isInt = abstractType === 'integer'
const isFloat = abstractType === 'float'
const isDate = abstractType === 'date' || uiDatatype === 'Date'
const isTime = abstractType === 'time' || uiDatatype === 'Time'
const isDateTime = abstractType === 'datetime' || uiDatatype === 'DateTime'
const isJSON = uiDatatype === 'JSON'
const isEnum = uiDatatype === 'SingleSelect'
const isSet = uiDatatype === 'MultiSelect'
const isURL = uiDatatype === 'URL'
const isEmail = uiDatatype === UITypes.Email
const isAttachment = uiDatatype === 'Attachment'
const isRating = uiDatatype === UITypes.Rating
const isCurrency = uiDatatype === 'Currency'
const isDuration = uiDatatype === UITypes.Duration
const isAutoSaved = [
UITypes.SingleLineText,
UITypes.LongText,
UITypes.PhoneNumber,
UITypes.Email,
UITypes.URL,
UITypes.Number,
UITypes.Decimal,
UITypes.Percent,
UITypes.Count,
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry,
].includes(uiDatatype)
const isManualSaved = [
UITypes.Currency,
UITypes.Year,
UITypes.Time,
UITypes.Duration,
].includes(uiDatatype)
return {
abstractType,
dataTypeLow,
isBoolean,
isString,
isTextArea,
isInt,
isFloat,
isDate,
isTime,
isDateTime,
isJSON,
isEnum,
isSet,
isURL,
isEmail,
isAttachment,
isRating,
isCurrency,
isDuration,
isAutoSaved,
isManualSaved,
}
}

6
packages/nc-gui-v2/composables/project.ts

@ -1,11 +1,11 @@
import type { TableType } from 'nocodb-sdk'
import type { ProjectType, TableType } from 'nocodb-sdk'
import { useNuxtApp } from '#app'
export const useProject = () => {
const { $api } = useNuxtApp()
const project = useState<{ id?: string; title?: string }>('project')
const tables = useState<TableType[]>('tables')
const project = useState<ProjectType>('project', null)
const tables = useState<Array<TableType>>('tables', null)
const loadTables = async () => {
if (project.value.id) {

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

@ -1,6 +1,6 @@
import { useState } from '#app'
interface TabItem {
export interface TabItem {
type: 'table' | 'view'
title: string
id: string

18
packages/nc-gui-v2/layouts/default.vue

@ -1,20 +1,6 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
const route = useRoute()
const openDiscord = () => {
// shell.openExternal('https://discord.gg/5RgZmkW')
}
const isDashboard = computed(() => {
return route.path && (route.path === '/nc' || route.path === '/nc/' || route.path.startsWith('/nc/'))
})
</script>
<script lang="ts">
<script>
export default {
name: 'General',
name: 'Default',
}
</script>

9
packages/nc-gui-v2/layouts/withoutSidebar.vue

@ -0,0 +1,9 @@
<script setup lang="ts">
</script>
<template>
<v-layout>
<v-app-bar color="" />
<slot />
</v-layout>
</template>

22
packages/nc-gui-v2/pages/dashboard/[projectId].vue

@ -3,6 +3,10 @@ import { watch } from 'vue'
import { useProject } from '~/composables/project'
import { useTabs } from '~/composables/tabs'
definePageMeta({
layout: 'without-sidebar',
})
const route = useRoute()
const { user } = useUser()
const { loadProject, loadTables } = useProject()
@ -26,16 +30,14 @@ watch(
</script>
<template>
<NuxtLayout>
<v-navigation-drawer color="" permanent>
<DashboardTreeView />
</v-navigation-drawer>
<v-main>
<v-container>
<DashboardTabView />
</v-container>
</v-main>
</NuxtLayout>
<v-navigation-drawer permanent>
<DashboardTreeView />
</v-navigation-drawer>
<v-main>
<v-container>
<DashboardTabView />
</v-container>
</v-main>
</template>
<style scoped lang="scss">

2
packages/nc-gui-v2/pages/signin.vue

@ -27,7 +27,7 @@ const signIn = async () => {
<!-- Enter your work email -->
<v-card class="pa-10 mx-auto mt-10" style="max-width: 500px">
<v-card-text>
<v-alert v-if="error" density="medium" class="mb-10" type="error">
<v-alert v-if="error" class="mb-10" type="error">
{{ error }}
</v-alert>

2
packages/nc-gui-v2/pages/signup.vue

@ -26,7 +26,7 @@ const signUp = async () => {
<!-- Enter your work email -->
<v-card class="pa-10 mx-auto mt-10" style="max-width: 500px">
<v-card-text>
<v-alert v-if="error" density="medium" class="mb-4" type="error">
<v-alert v-if="error" class="mb-4" type="error">
{{ error }}
</v-alert>

Loading…
Cancel
Save