Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nc feat/percent field v2 #7590

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
86 changes: 72 additions & 14 deletions packages/nc-gui/components/cell/Percent.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, inject, useVModel } from '#imports'
import {
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj,
inject,
useVModel,
getPercentStep,
isValidPercent,
renderPercent,
IsGroupByLabelInj,
} from '#imports'

interface Props {
modelValue?: number | string | null
Expand All @@ -20,17 +32,37 @@ const isEditColumn = inject(EditColumnInj, ref(false))

const readOnly = inject(ReadonlyInj, ref(false))

const isGroupByLabel = inject(IsGroupByLabelInj, ref(false))

const _vModel = useVModel(props, 'modelValue', emits)

const wrapperRef = ref<HTMLElement>()

const percentMeta = computed(() => {
return {
is_progress: false,
precision: 2,
...parseProp(column.value?.meta),
}
})

const percentStep = computed(() => getPercentStep(percentMeta.value.precision))

const displayValue = computed(() => {
if (_vModel.value === null || _vModel.value === undefined) return null

return renderPercent(_vModel.value, percentMeta.value.precision)
})

const vModel = computed({
get: () => _vModel.value,
get: () => {
return renderPercent(_vModel.value, percentMeta.value.precision, false, true)
},
set: (value) => {
if (value === '') {
_vModel.value = null
} else {
_vModel.value = value
} else if (isValidPercent(value, percentMeta.value?.negative)) {
_vModel.value = +value / 100
}
},
})
Expand All @@ -46,17 +78,11 @@ const cellFocused = ref(false)

const expandedEditEnabled = ref(false)

const percentMeta = computed(() => {
return {
is_progress: false,
...parseProp(column.value?.meta),
}
})

const onBlur = () => {
if (editEnabled) {
editEnabled.value = false
}

cellFocused.value = false
expandedEditEnabled.value = false
}
Expand Down Expand Up @@ -115,6 +141,34 @@ const onTabPress = (e: KeyboardEvent) => {
}
}
}

function onKeyDown(evt: KeyboardEvent) {
const keysToPrevent = ['e', 'E', '+']
if (!percentMeta.value?.negative) keysToPrevent.push('-')
return keysToPrevent.includes(evt.key) && evt.preventDefault()
}

/*
* The vModel value is formatted using toFixed to a specific precision.
* When the cursor is at the second position after the decimal and the backspace key is pressed,
* it removes the last decimal and the decimal point.
* To prevent the cursor from moving to the first position,
* we remove the value in the backspace event and update the vModel value.
*/
function onBackspace(evt: KeyboardEvent) {
const input = evt.target as HTMLInputElement

// Check if the cursor is after a decimal point
if (evt.key === 'Backspace' && input.value[input.value.length - 2] === '.') {
evt.preventDefault()

// Remove the decimal point and the value after it
const newValue = input.value.slice(0, -2)

// update vModel value
vModel.value = +newValue
}
}
</script>

<template>
Expand All @@ -131,9 +185,11 @@ const onTabPress = (e: KeyboardEvent) => {
v-if="!readOnly && editEnabled && (isExpandedFormOpen ? expandedEditEnabled : true)"
:ref="focus"
v-model="vModel"
class="nc-cell-field w-full !text-sm !border-none !outline-none focus:ring-0 text-base py-1"
class="nc-cell-field w-full !text-sm !border-none !outline-none focus:ring-0 py-1 px-0 invalid:text-red-500"
style="letter-spacing: 0.06rem"
type="number"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:step="percentStep"
@blur="onBlur"
@focus="onFocus"
@keydown.down.stop
Expand All @@ -142,11 +198,13 @@ const onTabPress = (e: KeyboardEvent) => {
@keydown.up.stop
@keydown.delete.stop
@keydown.tab="onTabPress"
@keydown="onKeyDown"
@keydown.backspace="onBackspace"
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-cell-field nc-null uppercase">{{ $t('general.null') }}</span>
<div v-else-if="percentMeta.is_progress === true && vModel !== null && vModel !== undefined" class="px-2">
<div v-else-if="percentMeta.is_progress === true && vModel !== null && vModel !== undefined && !isGroupByLabel" class="px-2">
<a-progress
:percent="Number(parseFloat(vModel.toString()).toFixed(2))"
size="small"
Expand All @@ -157,7 +215,7 @@ const onTabPress = (e: KeyboardEvent) => {
/>
</div>
<!-- nbsp to keep height even if vModel is zero length -->
<span v-else class="nc-cell-field">{{ vModel }}&nbsp;</span>
<span v-else class="nc-cell-field text-sm">{{ displayValue }}&nbsp;</span>
</div>
</template>

Expand Down
31 changes: 27 additions & 4 deletions packages/nc-gui/components/smartsheet/column/PercentOptions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,39 @@ setAdditionalValidations({
// set default value
vModel.value.meta = {
is_progress: false,
negative: false,
precision: precisions[2].id,
...vModel.value.meta,
}
</script>

<template>
<div class="flex flex-col">
<div>
<a-checkbox v-if="vModel.meta" v-model:checked="vModel.meta.is_progress" class="ml-1 mb-1">
<span class="text-[10px] text-gray-600">Display as progress</span>
</a-checkbox>
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-full" :label="$t('placeholder.precision')">
<a-select v-model:value="vModel.meta.precision" dropdown-class-name="nc-dropdown-precision">
<a-select-option v-for="(precision, i) of precisions" :key="i" :value="precision.id">
<div class="flex flex-row items-center">
<div class="text-xs">
{{ precision.title }}
</div>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
<div class="w-full flex flex-row justify-between items-center mt-2">
<a-form-item>
<a-checkbox v-if="vModel.meta" v-model:checked="vModel.meta.is_progress" class="ml-1 mb-1">
<span class="text-[10px] text-gray-600">Display as progress</span>
</a-checkbox>
</a-form-item>
<a-form-item class="h-full">
<div class="h-full flex flex-row space-x-2 items-center">
<a-switch v-model:checked="vModel.meta.negative" :name="$t('labels.negative')" size="small" />
<div class="text-[10px] text-gray-600">{{ $t('placeholder.allowNegativeNumbers') }}</div>
</div>
</a-form-item>
</div>
</div>
</template>
1 change: 1 addition & 0 deletions packages/nc-gui/components/smartsheet/grid/GroupBy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ const shouldRenderCell = (column) =>
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Percent
].includes(column?.uidt)
</script>

Expand Down
19 changes: 16 additions & 3 deletions packages/nc-gui/utils/percentUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,22 @@ export const precisions = [
{ id: 8, title: '1.00000000' },
]

export function renderPercent(value: any, precision: number, withPercentSymbol = true) {
if (!value) return value
value = (Number(value) * 100).toFixed(precision)
export function renderPercent(value: any, precision?: number, withPercentSymbol: boolean = true, isInputField = false) {
if (typeof value !== 'number' && !value) return value
if (isNaN(Number(value))) return null

value = Number(value) * 100

if (precision !== undefined) {
if (isInputField) {
value = value % 1 !== 0 ? value.toFixed(precision).replace(/\.?0+$/, '') : value.toString()
} else {
value = value.toFixed(precision)
}
} else {
value = value.toString()
}

if (withPercentSymbol) return padPercentSymbol(value)
return value
}
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright/quickTests/commonTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const recordCells = {
URL: 'www.a.com',
Number: '1',
Value: '$1.00',
Percent: '0.01',
Percent: '1.00%',
};

// links/ computed fields
Expand Down
5 changes: 5 additions & 0 deletions tests/playwright/setup/demoTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ async function createDemoTable({
Year: rowMixedValue(columns.numberBased[7], i),
Time: rowMixedValue(columns.numberBased[8], i, context.dbType),
};

// For percent field user will type `cellValue=33` but while storing it in backend we convert it to `cellValue/100`
if (row.Percent !== null) {
row.Percent = (row.Percent as number) / 100;
}
rowAttributes.push(row);
}
break;
Expand Down
6 changes: 6 additions & 0 deletions tests/playwright/tests/db/features/filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ test.describe('Filter Tests: Numerical', () => {
});
}

// percent field value is stored in db is cellValue/100 e.g: cellValue = 25% and db stored value is 0.25
if (dataType === 'Percent') {
eqStringDerived = parseInt(eqString) / 100;
isLikeStringDerived = parseInt(isLikeString) / 100;
}

const filterList = [
{
op: '=',
Expand Down
5 changes: 3 additions & 2 deletions tests/playwright/tests/db/features/verticalFillHandle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ test.describe('Fill Handle', () => {
{ title: 'Number', value: 33, type: 'text' },
{ title: 'Decimal', value: 33.3, type: 'text' },
{ title: 'Currency', value: 33.3, type: 'text' },
{ title: 'Percent', value: 33, type: 'text' },
{ title: 'Percent', value: '33.00%', type: 'text' },
{ title: 'Duration', value: '00:01', type: 'text' },
{ title: 'Rating', value: 3, type: 'rating' },
{ title: 'Year', value: '2023', type: 'year' },
Expand Down Expand Up @@ -138,7 +138,8 @@ test.describe('Fill Handle', () => {

// verify api response
// duration in seconds
const APIResponse = [33, 33.3, 33.3, 33, 60, 3, 2023, '02:02:00'];
// percent field value is stored in db is cellValue/100 e.g: cellValue = 25% and db stored value is 0.25
const APIResponse = [33, 33.3, 33.3, 0.33, 60, 3, 2023, '02:02:00'];
const updatedRecords = (await p.api.dbTableRow.list('noco', p.context.base.id, p.table.id, { limit: 4 })).list;
for (let i = 0; i < updatedRecords.length; i++) {
for (let j = 0; j < fields.length; j++) {
Expand Down