Browse Source

feat: duration

pull/2389/head
Wing-Kam Wong 2 years ago
parent
commit
4bf6fd8c5b
  1. 4
      packages/nc-gui/components/project/spreadsheet/components/Cell.vue
  2. 51
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  3. 17
      packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue
  4. 69
      packages/nc-gui/components/project/spreadsheet/components/cell/DurationCell.vue
  5. 126
      packages/nc-gui/components/project/spreadsheet/components/editColumn/DurationOptions.vue
  6. 126
      packages/nc-gui/components/project/spreadsheet/components/editableCell/DurationCell.vue
  7. 5
      packages/nc-gui/components/project/spreadsheet/mixins/cell.js
  8. 192
      packages/nc-gui/helpers/durationHelper.js
  9. 2
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  10. 2
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  11. 2
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  12. 2
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  13. 6
      packages/nocodb/src/lib/meta/api/columnApis.ts
  14. 275
      scripts/cypress/integration/common/3e_duration_column.js
  15. 3
      scripts/cypress/integration/test/pg-restTableOps.js
  16. 3
      scripts/cypress/integration/test/restTableOps.js
  17. 3
      scripts/cypress/integration/test/xcdb-restTableOps.js

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

@ -17,6 +17,7 @@
<date-time-cell v-else-if="isDateTime" :value="value" />
<time-cell v-else-if="isTime" :value="value" />
<boolean-cell v-else-if="isBoolean" :value="value" read-only />
<duration-cell v-else-if="isDuration" :column="column" :value="value" read-only />
<rating-cell v-else-if="isRating" :value="value" read-only />
<currency-cell v-else-if="isCurrency" :value="value" :column="column" />
@ -37,10 +38,11 @@ import BooleanCell from '~/components/project/spreadsheet/components/cell/Boolea
import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
import CurrencyCell from '@/components/project/spreadsheet/components/cell/CurrencyCell'
import DurationCell from '@/components/project/spreadsheet/components/cell/DurationCell'
export default {
name: 'TableCell',
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell, CurrencyCell },
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell, CurrencyCell, DurationCell },
mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'],
computed: {

51
packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue

@ -172,7 +172,6 @@
:column="newColumn"
:meta="meta"
/>
<v-col
v-if="accordion"
cols="12"
@ -210,7 +209,31 @@
/>
</v-col>
<template v-if="newColumn.uidt !== 'Formula'">
<template v-if="newColumn.uidt === 'Formula'">
<v-col cols="12">
<formula-options
ref="formula"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.column_name"
:is-m-s-s-q-l="isMSSQL"
:sql-ui="sqlUi"
v-on="$listeners"
/>
</v-col>
</template>
<template v-else-if="newColumn.uidt === 'Duration'">
<v-col cols="12">
<duration-options
v-model="newColumn.meta"
:column="newColumn"
:meta="meta"
/>
</v-col>
</template>
<template v-else>
<v-col v-if="isLookup" cols="12">
<lookup-options
ref="lookup"
@ -497,21 +520,6 @@
</v-col>
</template>
</template>
<template v-else>
<v-col cols="12">
<formula-options
ref="formula"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.column_name"
:is-m-s-s-q-l="isMSSQL"
:sql-ui="sqlUi"
v-on="$listeners"
/>
</v-col>
</template>
</v-row>
</v-col>
</template>
@ -572,6 +580,7 @@ import { validateColumnName } from '~/helpers'
import RatingOptions from '~/components/project/spreadsheet/components/editColumn/RatingOptions'
import CheckboxOptions from '~/components/project/spreadsheet/components/editColumn/CheckboxOptions'
import CurrencyOptions from '@/components/project/spreadsheet/components/editColumn/CurrencyOptions'
import DurationOptions from '@/components/project/spreadsheet/components/editColumn/DurationOptions'
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
@ -587,7 +596,8 @@ export default {
DlgLabelSubmitCancel,
RelationOptions,
CustomSelectOptions,
CurrencyOptions
CurrencyOptions,
DurationOptions
},
props: {
nodes: Object,
@ -617,7 +627,8 @@ export default {
UITypes.Lookup,
UITypes.Rollup,
UITypes.SpecificDBType,
UITypes.Formula
UITypes.Formula,
UITypes.Duration
].includes(this.newColumn && this.newColumn.uidt)
},
uiTypes() {
@ -631,7 +642,7 @@ export default {
]
},
isEditDisabled() {
return this.editColumn && this.sqlUi === SqliteUi
return this.editColumn && this.sqlUi === SqliteUi && this.column.uidt !== UITypes.Duration
},
isSQLite() {
return this.sqlUi === SqliteUi

17
packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue

@ -35,6 +35,15 @@
v-on="$listeners"
/>
<duration-cell
v-else-if="isDuration"
v-model="localState"
:active="active"
:is-form="isForm"
:column="column"
:is-locked="isLocked"
/>
<boolean-cell
v-else-if="isBoolean"
v-model="localState"
@ -148,6 +157,7 @@ import EnumCell from '~/components/project/spreadsheet/components/cell/EnumCell'
import SetListEditableCell from '~/components/project/spreadsheet/components/editableCell/SetListEditableCell'
import SetListCell from '~/components/project/spreadsheet/components/cell/SetListCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
import DurationCell from '~/components/project/spreadsheet/components/editableCell/DurationCell'
export default {
name: 'EditableCell',
@ -167,7 +177,8 @@ export default {
TextAreaCell,
DateTimePickerCell,
TextCell,
DatePickerCell
DatePickerCell,
DurationCell
},
mixins: [cell],
props: {
@ -199,7 +210,7 @@ export default {
if (val !== this.value) {
this.changed = true
this.$emit('input', val)
if (this.isAttachment || this.isBoolean || this.isRating || this.isTime || this.isDateTime || this.isDate) {
if (this.isAttachment || this.isBoolean || this.isRating || this.isTime || this.isDateTime || this.isDate || this.isDuration) {
this.syncData()
} else if (!this.isCurrency && !this.isEnum && !this.isSet) {
this.syncDataDebounce(this)
@ -230,7 +241,7 @@ export default {
// this.$refs.input.focus();
},
beforeDestroy() {
if (this.changed && !(this.isAttachment || this.isBoolean || this.isRating || this.isTime || this.isDateTime)) {
if (this.changed && !(this.isAttachment || this.isBoolean || this.isRating || this.isTime || this.isDateTime || this.isDuration)) {
this.changed = false
this.$emit('change')
}

69
packages/nc-gui/components/project/spreadsheet/components/cell/DurationCell.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/>.
*
*/
-->

126
packages/nc-gui/components/project/spreadsheet/components/editColumn/DurationOptions.vue

@ -0,0 +1,126 @@
<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)"
>
<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
}
},
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/>.
*
*/
-->

126
packages/nc-gui/components/project/spreadsheet/components/editableCell/DurationCell.vue

@ -0,0 +1,126 @@
<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)"
>
<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
}
},
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/>.
*
*/
-->

5
packages/nc-gui/components/project/spreadsheet/mixins/cell.js

@ -67,8 +67,10 @@ export default {
},
isCurrency() {
return this.uiDatatype === 'Currency'
},
isDuration() {
return this.uiDatatype === UITypes.Duration
}
}
}
/**
@ -76,6 +78,7 @@ export default {
*
* @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
*

192
packages/nc-gui/helpers/durationHelper.js

@ -0,0 +1,192 @@
export const durationOptions = [
{
id: 0,
title: 'h:mm',
example: '(e.g. 1:23)',
regex: /(\d+)(?::(\d+))?/
}, {
id: 1,
title: 'h:mm:ss',
example: '(e.g. 3:45, 1:23:40)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?/
}, {
id: 2,
title: 'h:mm:ss.s',
example: '(e.g. 3:34.6, 1:23:40.0)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}, {
id: 3,
title: 'h:mm:ss.ss',
example: '(e.g. 3.45.67, 1:23:40.00)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}, {
id: 4,
title: 'h:mm:ss.sss',
example: '(e.g. 3.45.678, 1:23:40.000)',
regex: /(\d+)?(?::(\d+))?(?::(\d+))?(?:.(\d{0,4})?)?/
}
]
// pad zero
// mm && ss
// e.g. 3 -> 03
// e.g. 12 -> 12
// sss
// e.g. 1 -> 001
// e.g. 10 -> 010
const padZero = (val, isSSS = false) => {
return (val + '').padStart(isSSS ? 3 : 2, '0')
}
export const convertMS2Duration = (val, durationType) => {
if (val === null || val === undefined) { return val }
// 600.000 s --> 10:00 (10 mins)
const milliseconds = Math.round((val % 1) * 1000)
const centiseconds = Math.round(milliseconds / 10)
const deciseconds = Math.round(centiseconds / 10)
const hours = Math.floor(parseInt(val, 10) / (60 * 60))
const minutes = Math.floor((parseInt(val, 10) - (hours * 60 * 60)) / 60)
const seconds = parseInt(val, 10) - (hours * 60 * 60) - (minutes * 60)
if (durationType === 0) {
// h:mm
return `${padZero(hours)}:${padZero(minutes + (seconds >= 30))}`
} else if (durationType === 1) {
// h:mm:ss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}`
} else if (durationType === 2) {
// h:mm:ss.s
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${deciseconds}`
} else if (durationType === 3) {
// h:mm:ss.ss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${padZero(centiseconds)}`
} else if (durationType === 4) {
// h:mm:ss.sss
return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}.${padZero(milliseconds, true)}`
}
return val
}
export const convertDurationToSeconds = (val, durationType) => {
// 10:00 (10 mins) -> 600.000 s
const res = {
_ms: null,
_isValid: true
}
const durationRegex = durationOptions[durationType].regex
if (durationRegex.test(val)) {
let h, mm, ss
const groups = val.match(durationRegex)
if (groups[0] && groups[1] && !groups[2] && !groups[3] && !groups[4]) {
const val = parseInt(groups[1], 10)
if (groups.input.slice(-1) === ':') {
// e.g. 30:
h = groups[1]
mm = 0
ss = 0
} else if (durationType === 0) {
// consider it as minutes
// e.g. 360 -> 06:00
h = Math.floor(val / 60)
mm = Math.floor((val - ((h * 3600)) / 60))
ss = 0
} else {
// consider it as seconds
// e.g. 3600 -> 01:00:00
h = Math.floor(groups[1] / 3600)
mm = Math.floor(groups[1] / 60) % 60
ss = val % 60
}
} else if (durationType !== 0 && groups[1] && groups[2] && !groups[3]) {
// 10:10 means mm:ss instead of h:mm
// 10:10:10 means h:mm:ss
h = 0
mm = groups[1]
ss = groups[2]
} else {
h = groups[1] || 0
mm = groups[2] || 0
ss = groups[3] || 0
}
if (durationType === 0) {
// h:mm
res._sec = h * 3600 + mm * 60
} else if (durationType === 1) {
// h:mm:ss
res._sec = h * 3600 + mm * 60 + ss * 1
} else if (durationType === 2) {
// h:mm:ss.s (deciseconds)
const ds = groups[4] || 0
const len = Math.log(ds) * Math.LOG10E + 1 | 0
const ms = (
// e.g. len = 4: 1234 -> 1, 1456 -> 1
// e.g. len = 3: 123 -> 1, 191 -> 2
// e.g. len = 2: 12 -> 1 , 16 -> 2
len === 4
? Math.round(ds / 1000)
: len === 3
? Math.round(ds / 100)
: len === 2
? Math.round(ds / 10)
// take whatever it is
: ds
) * 100
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
} else if (durationType === 3) {
// h:mm:ss.ss (centiseconds)
const cs = groups[4] || 0
const len = Math.log(cs) * Math.LOG10E + 1 | 0
const ms = (
// e.g. len = 4: 1234 -> 12, 1285 -> 13
// e.g. len = 3: 123 -> 12, 128 -> 13
// check the third digit
len === 4
? Math.round(cs / 100)
: len === 3
? Math.round(cs / 10)
// take whatever it is
: cs
) * 10
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
} else if (durationType === 4) {
// h:mm:ss.sss (milliseconds)
let ms = groups[4] || 0
const len = Math.log(ms) * Math.LOG10E + 1 | 0
ms = (
// e.g. 1235 -> 124
// e.g. 1234 -> 123
len === 4
? Math.round(ms / 10)
// take whatever it is
: ms
) * 1
res._sec = h * 3600 + mm * 60 + ss * 1 + ms / 1000
}
} else {
res._isValid = false
}
return res
}
/**
* @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/>.
*
*/

2
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -1144,7 +1144,7 @@ export class MssqlUi {
colProp.dt = 'double';
break;
case 'Duration':
colProp.dt = 'int';
colProp.dt = 'decimal';
break;
case 'Rating':
colProp.dt = 'int';

2
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -1036,7 +1036,7 @@ export class MysqlUi {
colProp.dt = 'double';
break;
case 'Duration':
colProp.dt = 'int';
colProp.dt = 'decimal';
break;
case 'Rating':
colProp.dt = 'int';

2
packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts

@ -1660,7 +1660,7 @@ export class PgUi {
colProp.dt = 'double precision';
break;
case 'Duration':
colProp.dt = 'int8';
colProp.dt = 'decimal';
break;
case 'Rating':
colProp.dt = 'smallint';

2
packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts

@ -853,7 +853,7 @@ export class SqliteUi {
colProp.dt = 'double';
break;
case 'Duration':
colProp.dt = 'integer';
colProp.dt = 'decimal';
break;
case 'Rating':
colProp.dt = 'integer';

6
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -503,6 +503,12 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
default:
{
colBody = getColumnPropsFromUIDT(colBody, base);
if (colBody.uidt === UITypes.Duration) {
colBody.dtxp = '20';
// by default, colBody.dtxs is 2
// Duration column needs more that that
colBody.dtxs = '4';
}
const tableUpdateBody = {
...table,
tn: table.table_name,

275
scripts/cypress/integration/common/3e_duration_column.js

@ -0,0 +1,275 @@
import { mainPage } from "../../support/page_objects/mainPage";
import {
isTestSuiteActive,
} from "../../support/page_objects/projectConstants";
export const genTest = (apiType, dbType) => {
if (!isTestSuiteActive(apiType, dbType)) return;
describe(`${apiType.toUpperCase()} api - DURATION`, () => {
const tableName = "DurationTable";
// to retrieve few v-input nodes from their label
//
const fetchParentFromLabel = (label) => {
cy.get("label").contains(label).parents(".v-input").click();
};
// Run once before test- create table
//
before(() => {
mainPage.tabReset();
cy.createTable(tableName);
});
after(() => {
cy.deleteTable(tableName);
});
// Routine to create a new look up column
//
const addDurationColumn = (columnName, durationFormat) => {
// (+) icon at end of column header (to add a new column)
// opens up a pop up window
//
cy.get(".new-column-header").click();
// Column name
cy.get(".nc-column-name-input input").clear().type(`${columnName}`);
// Column data type
cy.get(".nc-ui-dt-dropdown").click();
cy.getActiveMenu().contains("Duration").click();
// Configure Child table & column names
fetchParentFromLabel("Duration Format");
cy.getActiveMenu().contains(durationFormat).click();
// click on Save
cy.get(".nc-col-create-or-edit-card").contains("Save").click();
// Verify if column exists.
//
cy.get(`th:contains(${columnName})`).should("exist");
};
// routine to delete column
//
const deleteColumnByName = (columnName) => {
// verify if column exists before delete
cy.get(`th:contains(${columnName})`).should("exist");
// delete opiton visible on mouse-over
cy.get(`th:contains(${columnName}) .mdi-menu-down`)
.trigger("mouseover")
.click();
// delete/ confirm on pop-up
cy.get(".nc-column-delete").click();
cy.getActiveModal().find("button:contains(Confirm)").click();
// validate if deleted (column shouldnt exist)
cy.get(`th:contains(${columnName})`).should("not.exist");
};
// routine to edit column
//
const editColumnByName = (oldName, newName, newDurationFormat) => {
// verify if column exists before delete
cy.get(`th:contains(${oldName})`).should("exist");
// delete opiton visible on mouse-over
cy.get(`th:contains(${oldName}) .mdi-menu-down`)
.trigger("mouseover")
.click();
// edit/ save on pop-up
cy.get(".nc-column-edit").click();
cy.get(".nc-column-name-input input").clear().type(newName);
// Configure Child table & column names
fetchParentFromLabel("Duration Format");
cy.getActiveMenu().contains(newDurationFormat).click();
cy.get(".nc-col-create-or-edit-card")
.contains("Save")
.click({ force: true });
cy.toastWait("Duration column updated successfully");
// validate if deleted (column shouldnt exist)
cy.get(`th:contains(${oldName})`).should("not.exist");
cy.get(`th:contains(${newName})`).should("exist");
};
const addDurationData = (colName, index, cellValue, expectedValue, isNewRow = false) => {
if (isNewRow) {
cy.get(".nc-add-new-row-btn:visible").should("exist");
cy.wait(500)
cy.get(".nc-add-new-row-btn").click({ force: true });
} else {
mainPage.getRow(index).find(".nc-row-expand-icon").click({ force: true });
}
cy.get(".duration-cell-wrapper > input").first().should('exist').type(cellValue);
cy.getActiveModal().find("button").contains("Save row").click({ force: true });
cy.toastWait("Row updated successfully");
mainPage.getCell(colName, index).find('input').then(($e) => {
expect($e[0].value).to.equal(expectedValue)
})
}
///////////////////////////////////////////////////
// Test case
{
// Duration: h:mm
it("Duration: h:mm", () => {
addDurationColumn("NC_DURATION_0", "h:mm (e.g. 1:23)");
addDurationData("NC_DURATION_0", 1, "1:30", "01:30", true);
addDurationData("NC_DURATION_0", 2, "30", "00:30", true);
addDurationData("NC_DURATION_0", 3, "60", "01:00", true);
addDurationData("NC_DURATION_0", 4, "80", "01:20", true);
addDurationData("NC_DURATION_0", 5, "12:34", "12:34", true);
addDurationData("NC_DURATION_0", 6, "15:130", "17:10", true);
addDurationData("NC_DURATION_0", 7, "123123", "2052:03", true);
});
it("Duration: Edit Column NC_DURATION_0", () => {
editColumnByName(
"NC_DURATION_0",
"NC_DURATION_EDITED_0",
"h:mm:ss (e.g. 3:45, 1:23:40)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_0");
});
}
{
// Duration: h:mm:ss
it("Duration: h:mm:ss", () => {
addDurationColumn("NC_DURATION_1", "h:mm:ss (e.g. 3:45, 1:23:40)");
addDurationData("NC_DURATION_1", 1, "11:22:33", "11:22:33");
addDurationData("NC_DURATION_1", 2, "1234", "00:20:34");
addDurationData("NC_DURATION_1", 3, "50", "00:00:50");
addDurationData("NC_DURATION_1", 4, "1:1111", "00:19:31");
addDurationData("NC_DURATION_1", 5, "1:11:1111", "01:29:31");
addDurationData("NC_DURATION_1", 6, "15:130", "00:17:10");
addDurationData("NC_DURATION_1", 7, "123123", "34:12:03");
});
it("Duration: Edit Column NC_DURATION_1", () => {
editColumnByName(
"NC_DURATION_1",
"NC_DURATION_EDITED_1",
"h:mm:ss.s (e.g. 3:34.6, 1:23:40.0)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_1");
});
}
{
// h:mm:ss.s
it("Duration: h:mm:ss.s", () => {
addDurationColumn("NC_DURATION_2", "h:mm:ss.s (e.g. 3:34.6, 1:23:40.0)");
addDurationData("NC_DURATION_2", 1, "1234", "00:20:34.0");
addDurationData("NC_DURATION_2", 2, "12:34", "00:12:34.0");
addDurationData("NC_DURATION_2", 3, "12:34:56", "12:34:56.0");
addDurationData("NC_DURATION_2", 4, "12:34:999", "12:50:39.0");
addDurationData("NC_DURATION_2", 5, "12:999:56", "28:39:56.0");
addDurationData("NC_DURATION_2", 6, "12:34:56.12", "12:34:56.1");
addDurationData("NC_DURATION_2", 7, "12:34:56.199", "12:34:56.2");
});
it("Duration: Edit Column NC_DURATION_2", () => {
editColumnByName(
"NC_DURATION_2",
"NC_DURATION_EDITED_2",
"h:mm:ss (e.g. 3:45, 1:23:40)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_2");
});
}
{
// h:mm:ss.ss
it("Duration: h:mm:ss.ss", () => {
addDurationColumn("NC_DURATION_3", "h:mm:ss.ss (e.g. 3.45.67, 1:23:40.00)");
addDurationData("NC_DURATION_3", 1, "1234", "00:20:34.00");
addDurationData("NC_DURATION_3", 2, "12:34", "00:12:34.00");
addDurationData("NC_DURATION_3", 3, "12:34:56", "12:34:56.00");
addDurationData("NC_DURATION_3", 4, "12:34:999", "12:50:39.00");
addDurationData("NC_DURATION_3", 5, "12:999:56", "28:39:56.00");
addDurationData("NC_DURATION_3", 6, "12:34:56.12", "12:34:56.12");
addDurationData("NC_DURATION_3", 7, "12:34:56.199", "12:34:56.20");
});
it("Duration: Edit Column NC_DURATION_3", () => {
editColumnByName(
"NC_DURATION_3",
"NC_DURATION_EDITED_3",
"h:mm:ss.ss (e.g. 3.45.67, 1:23:40.00)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_3");
});
}
{
// h:mm:ss.sss
it("Duration: h:mm:ss.sss", () => {
addDurationColumn("NC_DURATION_4", "h:mm:ss.sss (e.g. 3.45.678, 1:23:40.000)");
addDurationData("NC_DURATION_4", 1, "1234", "00:20:34.000");
addDurationData("NC_DURATION_4", 2, "12:34", "00:12:34.000");
addDurationData("NC_DURATION_4", 3, "12:34:56", "12:34:56.000");
addDurationData("NC_DURATION_4", 4, "12:34:999", "12:50:39.000");
addDurationData("NC_DURATION_4", 5, "12:999:56", "28:39:56.000");
addDurationData("NC_DURATION_4", 6, "12:34:56.12", "12:34:56.012");
addDurationData("NC_DURATION_4", 7, "12:34:56.199", "12:34:56.199");
});
it("Duration: Edit Column NC_DURATION_4", () => {
editColumnByName(
"NC_DURATION_4",
"NC_DURATION_EDITED_4",
"h:mm (e.g. 1:23)"
);
});
it("Duration: Delete column", () => {
deleteColumnByName("NC_DURATION_EDITED_4");
});
}
});
};
/**
* @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/>.
*
*/

3
scripts/cypress/integration/test/pg-restTableOps.js

@ -11,6 +11,7 @@ let t3a = require("../common/3a_filter_sort_fields_operations");
let t3b = require("../common/3b_formula_column");
let t3c = require("../common/3c_lookup_column");
let t3d = require("../common/3d_rollup_column");
let t3e = require("../common/3e_duration_column");
const {
setCurrentMode,
} = require("../../support/page_objects/projectConstants");
@ -38,6 +39,7 @@ const nocoTestSuite = (apiType, dbType) => {
// t3b.genTest(apiType, dbType);
t3c.genTest(apiType, dbType);
t3d.genTest(apiType, dbType);
t3e.genTest(apiType, dbType);
};
nocoTestSuite("rest", "postgres");
@ -46,6 +48,7 @@ nocoTestSuite("rest", "postgres");
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Raju Udava <sivadstala@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

3
scripts/cypress/integration/test/restTableOps.js

@ -11,6 +11,7 @@ let t3a = require("../common/3a_filter_sort_fields_operations");
let t3b = require("../common/3b_formula_column");
let t3c = require("../common/3c_lookup_column");
let t3d = require("../common/3d_rollup_column");
let t3e = require("../common/3e_duration_column");
const {
setCurrentMode,
} = require("../../support/page_objects/projectConstants");
@ -38,6 +39,7 @@ const nocoTestSuite = (apiType, dbType) => {
t3b.genTest(apiType, dbType);
t3c.genTest(apiType, dbType);
t3d.genTest(apiType, dbType);
t3e.genTest(apiType, dbType);
};
nocoTestSuite("rest", "mysql");
@ -46,6 +48,7 @@ nocoTestSuite("rest", "mysql");
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Raju Udava <sivadstala@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

3
scripts/cypress/integration/test/xcdb-restTableOps.js

@ -11,6 +11,7 @@ let t3a = require("../common/3a_filter_sort_fields_operations");
let t3b = require("../common/3b_formula_column");
let t3c = require("../common/3c_lookup_column");
let t3d = require("../common/3d_rollup_column");
let t3e = require("../common/3e_duration_column");
const {
setCurrentMode,
} = require("../../support/page_objects/projectConstants");
@ -38,6 +39,7 @@ const nocoTestSuite = (apiType, dbType) => {
t3b.genTest(apiType, dbType);
t3c.genTest(apiType, dbType);
t3d.genTest(apiType, dbType);
t3e.genTest(apiType, dbType);
};
nocoTestSuite("rest", "xcdb");
@ -46,6 +48,7 @@ nocoTestSuite("rest", "xcdb");
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Raju Udava <sivadstala@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

Loading…
Cancel
Save