diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 787bbe9cbf..902a7c7218 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -18,7 +18,10 @@ on: - "packages/nc-gui/**" - "packages/nocodb/**" - ".github/workflows/ci-cd.yml" + - ".github/workflows/playwright-test-workflow.yml" - "tests/playwright/**" + # Triggered manually + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -89,6 +92,10 @@ jobs: ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- + - name: Set CI env + run: export CI=true + - name: Set NC Edition + run: export EE=true - name: setup pg working-directory: ./ run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d & diff --git a/.github/workflows/playwright-test-workflow.yml b/.github/workflows/playwright-test-workflow.yml index eb2ec03eca..60b62f90ff 100644 --- a/.github/workflows/playwright-test-workflow.yml +++ b/.github/workflows/playwright-test-workflow.yml @@ -13,21 +13,9 @@ on: jobs: playwright: - runs-on: ubuntu-20.04 - timeout-minutes: 40 + runs-on: [self-hosted, v2] + timeout-minutes: 100 steps: - # Reference: https://github.com/pierotofy/set-swap-space/blob/master/action.yml - - name: Set 5gb swap - shell: bash - # Delete the swap file, allocate a new one, and activate it - run: | - export SWAP_FILE=$(swapon --show=NAME | tail -n 1) - sudo swapoff $SWAP_FILE - sudo rm $SWAP_FILE - sudo fallocate -l 5G $SWAP_FILE - sudo chmod 600 $SWAP_FILE - sudo mkswap $SWAP_FILE - sudo swapon $SWAP_FILE - name: Setup Node uses: actions/setup-node@v3 with: @@ -42,57 +30,87 @@ jobs: with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-v2-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + ${{ runner.os }}-v2-build-${{ env.cache-name }}- + ${{ runner.os }}-v2-build- + ${{ runner.os }}-v2 + - name: setup pg + if: ${{ inputs.db == 'pg' || ( inputs.db == 'sqlite' && inputs.shard == '1' ) }} + working-directory: ./ + run: | + service postgresql start + cd /var/lib/postgresql/ && sudo -u postgres psql -c "SELECT 'dropdb '||datname||'' FROM pg_database WHERE datistemplate = false AND datallowconn = true And datname NOT IN ('postgres')" |grep ' dropdb ' | sudo -u postgres /bin/bash ; cd + sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'password';" + sudo -u postgres psql -c "ALTER USER postgres WITH SUPERUSER;" + service postgresql restart + - name: Set CI env + run: export CI=true + - name: Kill stale servers + run: | + # export NODE_OPTIONS=\"--max_old_space_size=16384\"; + kill -9 $(lsof -t -i:8080) || echo "no process running on 8080" + kill -9 $(lsof -t -i:3000) || echo "no process running on 3000" + - name: Set CI env + run: export CI=true + - name: Set NC Edition + run: export EE=true - name: install dependencies nocodb-sdk working-directory: ./packages/nocodb-sdk run: npm install - - name: build nocodb-sdk + - name: Build nocodb-sdk working-directory: ./packages/nocodb-sdk run: npm run build - - name: setup mysql + - name: Setup mysql if: ${{ inputs.db == 'mysql' }} - working-directory: ./ - run: docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d & - - name: setup pg - if: ${{ inputs.db == 'pg' }} - working-directory: ./ - run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d & - - name: setup pg for quick tests + working-directory: ./packages/nocodb/tests/mysql-sakila-db + run: | + # Get a list of non-system databases and construct the DROP DATABASE statement for each + service mysql start + mysql -u'root' -p'password' -e "SHOW DATABASES" --skip-column-names | grep -Ev "(information_schema|mysql|performance_schema|sys)" | while read db; do + mysql -u'root' -p'password' -e "DROP DATABASE IF EXISTS \`$db\`"; + done + # keep sql_mode default except remove "STRICT_TRANS_TABLES" + mysql -u'root' -p'password' -e "SET GLOBAL sql_mode = 'ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';" + # this is only needed for connecting to sakila db as its refeferred in multiple places in test code + mysql -u'root' -p'password' < 01-mysql-sakila-schema.sql + mysql -u'root' -p'password' < 02-mysql-sakila-insert-data.sql + - name: Setup pg for quick tests if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }} - working-directory: ./ - run: docker-compose -f ./tests/playwright/scripts/docker-compose-pg-pw-quick.yml up -d & + working-directory: ./packages/nocodb/tests/pg-cy-quick/ + run: | + sudo -u postgres psql -U postgres -f 01-cy-quick.sql - name: run frontend working-directory: ./packages/nc-gui run: npm run ci:run + timeout-minutes: 20 - name: Run backend if: ${{ inputs.db == 'sqlite' }} working-directory: ./packages/nocodb run: | npm install - npm run watch:run:playwright > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & + npm run watch:run:playwright &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & - name: Run backend:mysql if: ${{ inputs.db == 'mysql' }} working-directory: ./packages/nocodb run: | npm install - npm run watch:run:playwright:mysql > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & + npm run watch:run:playwright:mysql &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & - name: Run backend:pg if: ${{ inputs.db == 'pg' }} working-directory: ./packages/nocodb run: | npm install - npm run watch:run:playwright:pg > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & + npm run watch:run:playwright:pg &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & - name: Cache playwright npm modules uses: actions/cache@v3 id: playwright-cache with: path: | **/tests/playwright/node_modules - key: cache-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }} + key: cache-v2-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }} + restore-keys: | + cache-v2-nc-playwright- - name: Install dependencies if: steps.playwright-cache.outputs.cache-hit != 'true' working-directory: ./tests/playwright @@ -106,11 +124,11 @@ jobs: printf '.' sleep 2 done - - - name: Run Playwright tests + timeout-minutes: 2 + - name: Run Playwright Tests working-directory: ./tests/playwright run: E2E_DB_TYPE=${{ inputs.db }} npm run ci:test:shard:${{ inputs.shard }} - + timeout-minutes: 60 # Stress test added/modified tests - name: Fetch develop branch working-directory: ./tests/playwright @@ -119,28 +137,32 @@ jobs: working-directory: ./tests/playwright run: E2E_DB_TYPE=${{ inputs.db }} node ./scripts/stressTestNewlyAddedTest.js - # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1) - - name: Run quick server and tests (pg) - if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }} - working-directory: ./packages/nocodb - run: | - kill -9 $(lsof -t -i:8080) - npm run watch:run:playwright:pg:cyquick & - - name: Run quick server and tests (sqlite) - if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }} - working-directory: ./packages/nocodb - run: | - kill -9 $(lsof -t -i:8080) - npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log & - - name: Wait for backend & run quick tests - if: ${{ inputs.db == 'sqlite' }} - working-directory: ./tests/playwright - run: | - while ! curl --output /dev/null --silent --head --fail http://localhost:8080; do - printf '.' - sleep 2 - done - PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick +# # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1) +# - name: Run quick server and tests (pg) +# if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }} +# working-directory: ./packages/nocodb +# run: | +# kill -9 $(lsof -t -i:8080) +# npm run watch:run:playwright:pg:cyquick > quick_${{ inputs.shard }}_test_backend.log & +# - name: Run quick server and tests (sqlite) +# if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }} +# working-directory: ./packages/nocodb +# run: | +# kill -9 $(lsof -t -i:8080) +# npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log & +# - name: Wait for backend for sqlite-tests +# if: ${{ inputs.db == 'sqlite' }} +# working-directory: ./tests/playwright +# run: | +# while ! curl --output /dev/null --silent --head --fail http://localhost:8080; do +# printf '.' +# sleep 2 +# done +# timeout-minutes: 1 +# - name: Run quick tests +# if: ${{ inputs.db == 'sqlite' }} +# working-directory: ./tests/playwright +# run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick - uses: actions/upload-artifact@v3 if: ${{ inputs.db == 'sqlite' }} with: @@ -172,3 +194,9 @@ jobs: name: backend-logs-${{ inputs.db }}-${{ inputs.shard }} path: ./packages/nocodb/${{ inputs.db }}_${{ inputs.shard }}_test_backend.log retention-days: 2 + - name: stop database servers + if: always() + working-directory: ./packages/nocodb + run: | + service postgresql stop + service mysql stop \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2d333cbc36..94d7be5041 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ # =========== .DS_Store ehthumbs.db -Icon? Thumbs.db +Icon + + # Node and related ecosystem # ========================== @@ -92,3 +94,4 @@ test_noco.db # ngrok config httpbin +.run/test-debug.run.xml diff --git a/packages/nc-gui/app.vue b/packages/nc-gui/app.vue index dfd4c809a6..4c57910a95 100644 --- a/packages/nc-gui/app.vue +++ b/packages/nc-gui/app.vue @@ -1,12 +1,15 @@ + + diff --git a/packages/nc-gui/components/api-client/Params.vue b/packages/nc-gui/components/api-client/Params.vue index 35da74a0a5..61aa3f1f2c 100644 --- a/packages/nc-gui/components/api-client/Params.vue +++ b/packages/nc-gui/components/api-client/Params.vue @@ -11,69 +11,77 @@ const vModel = useVModel(props, 'modelValue', emits) const addParamRow = () => vModel.value.push({}) -const deleteParamRow = (i: number) => vModel.value.splice(i, 1) +const deleteParamRow = (i: number) => { + if (vModel.value.length === 1) return + + vModel.value.splice(i, 1) +} + + diff --git a/packages/nc-gui/components/cell/Checkbox.vue b/packages/nc-gui/components/cell/Checkbox.vue index 94a87c5a0c..c95b66cd1e 100644 --- a/packages/nc-gui/components/cell/Checkbox.vue +++ b/packages/nc-gui/components/cell/Checkbox.vue @@ -35,7 +35,7 @@ const isForm = inject(IsFormInj) const readOnly = inject(ReadonlyInj) -const checkboxMeta = $computed(() => { +const checkboxMeta = computed(() => { return { icon: { checked: 'mdi-check-circle-outline', @@ -46,7 +46,7 @@ const checkboxMeta = $computed(() => { } }) -let vModel = $computed({ +const vModel = computed({ get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0, set: (val: any) => emits('update:modelValue', isMssql(column?.value?.base_id) ? +val : val), }) @@ -59,7 +59,7 @@ function onClick(force?: boolean, event?: MouseEvent) { return } if (!readOnly?.value && (force || active.value)) { - vModel = !vModel + vModel.value = !vModel.value } } diff --git a/packages/nc-gui/components/cell/ClampedText.vue b/packages/nc-gui/components/cell/ClampedText.vue index 88b4ab046d..d01c6fb0f4 100644 --- a/packages/nc-gui/components/cell/ClampedText.vue +++ b/packages/nc-gui/components/cell/ClampedText.vue @@ -13,7 +13,7 @@ const props = defineProps<{ '-webkit-line-clamp': props.lines || 1, '-webkit-box-orient': 'vertical', 'overflow': 'hidden', - 'white-space': 'pre', + 'word-break': 'break-all', }" > {{ props.value || '' }} diff --git a/packages/nc-gui/components/cell/Currency.vue b/packages/nc-gui/components/cell/Currency.vue index 51aa023d4a..799e90592b 100644 --- a/packages/nc-gui/components/cell/Currency.vue +++ b/packages/nc-gui/components/cell/Currency.vue @@ -59,7 +59,7 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle const submitCurrency = () => { if (lastSaved.value !== vModel.value) { - lastSaved.value = vModel.value + vModel.value = lastSaved.value = vModel.value ?? null emit('save') } editEnabled.value = false diff --git a/packages/nc-gui/components/cell/DatePicker.vue b/packages/nc-gui/components/cell/DatePicker.vue index 5cde5ec01f..ca3dac123e 100644 --- a/packages/nc-gui/components/cell/DatePicker.vue +++ b/packages/nc-gui/components/cell/DatePicker.vue @@ -30,22 +30,24 @@ const columnMeta = inject(ColumnInj, null)! const readOnly = inject(ReadonlyInj, ref(false)) +const isLockedMode = inject(IsLockedInj, ref(false)) + const active = inject(ActiveCellInj, ref(false)) const editable = inject(EditModeInj, ref(false)) -let isDateInvalid = $ref(false) +const isDateInvalid = ref(false) -const dateFormat = $computed(() => parseProp(columnMeta?.value?.meta)?.date_format ?? 'YYYY-MM-DD') +const dateFormat = computed(() => parseProp(columnMeta?.value?.meta)?.date_format ?? 'YYYY-MM-DD') -let localState = $computed({ +const localState = computed({ get() { if (!modelValue) { return undefined } if (!dayjs(modelValue).isValid()) { - isDateInvalid = true + isDateInvalid.value = true return undefined } @@ -77,7 +79,7 @@ watch( { flush: 'post' }, ) -const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid ? 'Invalid date' : '')) +const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid.value ? 'Invalid date' : '')) useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { switch (e.key) { @@ -110,7 +112,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowLeft': - if (!localState) { + if (!localState.value) { ;(document.querySelector('.nc-picker-date.active .ant-picker-header-prev-btn') as HTMLButtonElement)?.click() } else { const prevEl = document.querySelector('.nc-picker-date.active .ant-picker-cell-selected') @@ -133,7 +135,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowRight': - if (!localState) { + if (!localState.value) { ;(document.querySelector('.nc-picker-date.active .ant-picker-header-next-btn') as HTMLButtonElement)?.click() } else { const nextEl = document.querySelector('.nc-picker-date.active .ant-picker-cell-selected') @@ -156,15 +158,15 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowUp': - if (!localState) + if (!localState.value) (document.querySelector('.nc-picker-date.active .ant-picker-header-super-prev-btn') as HTMLButtonElement)?.click() break case 'ArrowDown': - if (!localState) + if (!localState.value) (document.querySelector('.nc-picker-date.active .ant-picker-header-super-next-btn') as HTMLButtonElement)?.click() break case ';': - localState = dayjs(new Date()) + localState.value = dayjs(new Date()) break } }) @@ -206,7 +208,7 @@ const clickHandler = () => { :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" :dropdown-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''}`" - :open="(readOnly || (localState && isPk)) && !active && !editable ? false : open" + :open="((readOnly || (localState && isPk)) && !active && !editable) || isLockedMode ? false : open" @click="clickHandler" @update:open="updateOpen" > diff --git a/packages/nc-gui/components/cell/DateTimePicker.vue b/packages/nc-gui/components/cell/DateTimePicker.vue index 397ceda16f..38c0d1417d 100644 --- a/packages/nc-gui/components/cell/DateTimePicker.vue +++ b/packages/nc-gui/components/cell/DateTimePicker.vue @@ -36,11 +36,13 @@ const active = inject(ActiveCellInj, ref(false)) const editable = inject(EditModeInj, ref(false)) +const isLockedMode = inject(IsLockedInj, ref(false)) + const column = inject(ColumnInj)! -let isDateInvalid = $ref(false) +const isDateInvalid = ref(false) -const dateTimeFormat = $computed(() => { +const dateTimeFormat = computed(() => { const dateFormat = parseProp(column?.value?.meta)?.date_format ?? dateFormats[0] const timeFormat = parseProp(column?.value?.meta)?.time_format ?? timeFormats[0] return `${dateFormat} ${timeFormat}` @@ -48,14 +50,14 @@ const dateTimeFormat = $computed(() => { let localModelValue = modelValue ? dayjs(modelValue).utc().local() : undefined -let localState = $computed({ +const localState = computed({ get() { if (!modelValue) { return undefined } if (!dayjs(modelValue).isValid()) { - isDateInvalid = true + isDateInvalid.value = true return undefined } @@ -129,7 +131,7 @@ watch( { flush: 'post' }, ) -const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid ? 'Invalid date' : '')) +const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid.value ? 'Invalid date' : '')) useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { switch (e.key) { @@ -158,7 +160,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowLeft': - if (!localState) { + if (!localState.value) { ;(document.querySelector('.nc-picker-datetime.active .ant-picker-header-prev-btn') as HTMLButtonElement)?.click() } else { const prevEl = document.querySelector('.nc-picker-datetime.active .ant-picker-cell-selected') @@ -181,7 +183,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowRight': - if (!localState) { + if (!localState.value) { ;(document.querySelector('.nc-picker-datetime.active .ant-picker-header-next-btn') as HTMLButtonElement)?.click() } else { const nextEl = document.querySelector('.nc-picker-datetime.active .ant-picker-cell-selected') @@ -204,15 +206,15 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowUp': - if (!localState) + if (!localState.value) (document.querySelector('.nc-picker-datetime.active .ant-picker-header-super-prev-btn') as HTMLButtonElement)?.click() break case 'ArrowDown': - if (!localState) + if (!localState.value) (document.querySelector('.nc-picker-datetime.active .ant-picker-header-super-next-btn') as HTMLButtonElement)?.click() break case ';': - localState = dayjs(new Date()) + localState.value = dayjs(new Date()) break } }) @@ -248,7 +250,7 @@ const clickHandler = () => { :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" :dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`" - :open="readOnly || (localState && isPk) ? false : open && (active || editable)" + :open="readOnly || (localState && isPk) || isLockedMode ? false : open && (active || editable)" :disabled="readOnly || (localState && isPk)" @click="clickHandler" @ok="open = !open" diff --git a/packages/nc-gui/components/cell/Decimal.vue b/packages/nc-gui/components/cell/Decimal.vue index 562a4ac858..87b2170325 100644 --- a/packages/nc-gui/components/cell/Decimal.vue +++ b/packages/nc-gui/components/cell/Decimal.vue @@ -21,8 +21,24 @@ const { showNull } = useGlobal() const editEnabled = inject(EditModeInj) +const column = inject(ColumnInj, null)! + +const domRef = ref() + +const meta = computed(() => { + return typeof column?.value.meta === 'string' ? JSON.parse(column.value.meta) : column?.value.meta ?? {} +}) + const _vModel = useVModel(props, 'modelValue', emits) +const displayValue = computed(() => { + if (_vModel.value === null) return null + + if (isNaN(Number(_vModel.value))) return null + + return Number(_vModel.value).toFixed(meta.value.precision ?? 1) +}) + const vModel = computed({ get: () => _vModel.value, set: (value) => { @@ -36,9 +52,39 @@ const vModel = computed({ }, }) +const precision = computed(() => { + const meta = typeof column?.value.meta === 'string' ? JSON.parse(column.value.meta) : column?.value.meta ?? {} + const _precision = meta.precision ?? 1 + + return Number(0.1 ** _precision).toFixed(_precision) +}) + const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! +// Handle the arrow keys as its default behavior is to increment/decrement the value +const onKeyDown = (e: any) => { + if (e.key === 'ArrowDown') { + e.preventDefault() + // Move the cursor to the end of the input + e.target.type = 'text' + e.target?.setSelectionRange(e.target.value.length, e.target.value.length) + e.target.type = 'number' + } else if (e.key === 'ArrowUp') { + e.preventDefault() + + e.target.type = 'text' + e.target?.setSelectionRange(0, 0) + e.target.type = 'number' + } +} + const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() + +watch(isExpandedFormOpen, () => { + if (!isExpandedFormOpen.value) { + domRef.value?.focus() + } +}) diff --git a/packages/nc-gui/components/cell/Duration.vue b/packages/nc-gui/components/cell/Duration.vue index f9c9f80eb1..6c579311a9 100644 --- a/packages/nc-gui/components/cell/Duration.vue +++ b/packages/nc-gui/components/cell/Duration.vue @@ -15,7 +15,7 @@ import { interface Props { modelValue: number | string | null | undefined - showValidationError: boolean + showValidationError?: boolean } const { modelValue, showValidationError = true } = defineProps() diff --git a/packages/nc-gui/components/cell/Email.vue b/packages/nc-gui/components/cell/Email.vue index b15b6d4e52..2c4669d118 100644 --- a/packages/nc-gui/components/cell/Email.vue +++ b/packages/nc-gui/components/cell/Email.vue @@ -10,6 +10,8 @@ const { modelValue: value } = defineProps() const emit = defineEmits(['update:modelValue']) +const rowHeight = inject(RowHeightInj, ref(undefined)) + const { t } = useI18n() const { showNull } = useGlobal() @@ -73,8 +75,8 @@ watch( NULL - {{ vModel }} + - {{ vModel }} + diff --git a/packages/nc-gui/components/cell/GeoData.vue b/packages/nc-gui/components/cell/GeoData.vue index 68a3cdcf11..042f5bd838 100644 --- a/packages/nc-gui/components/cell/GeoData.vue +++ b/packages/nc-gui/components/cell/GeoData.vue @@ -16,17 +16,17 @@ const emits = defineEmits() const vModel = useVModel(props, 'modelValue', emits) -let isExpanded = $ref(false) +const isExpanded = ref(false) -let isLoading = $ref(false) +const isLoading = ref(false) -let isLocationSet = $ref(false) +const isLocationSet = ref(false) const [latitude, longitude] = (vModel.value || '').split(';') const latLongStr = computed(() => { const [latitude, longitude] = (vModel.value || '').split(';') - if (latitude) isLocationSet = true + if (latitude) isLocationSet.value = true return latitude && longitude ? `${latitude}; ${longitude}` : 'Set location' }) @@ -37,28 +37,28 @@ const formState = reactive({ const handleFinish = () => { vModel.value = latLongToJoinedString(parseFloat(formState.latitude), parseFloat(formState.longitude)) - isExpanded = false + isExpanded.value = false } const clear = () => { - isExpanded = false + isExpanded.value = false formState.latitude = latitude formState.longitude = longitude } const onClickSetCurrentLocation = () => { - isLoading = true + isLoading.value = true const onSuccess: PositionCallback = (position: GeolocationPosition) => { const crd = position.coords formState.latitude = `${crd.latitude}` formState.longitude = `${crd.longitude}` - isLoading = false + isLoading.value = false } const onError: PositionErrorCallback = (err: GeolocationPositionError) => { console.error(`ERROR(${err.code}): ${err.message}`) - isLoading = false + isLoading.value = false } const options = { diff --git a/packages/nc-gui/components/cell/Integer.vue b/packages/nc-gui/components/cell/Integer.vue index 0800c09823..793d9ed5c3 100644 --- a/packages/nc-gui/components/cell/Integer.vue +++ b/packages/nc-gui/components/cell/Integer.vue @@ -23,6 +23,14 @@ const editEnabled = inject(EditModeInj) const _vModel = useVModel(props, 'modelValue', emits) +const displayValue = computed(() => { + if (_vModel.value === null) return null + + if (isNaN(Number(_vModel.value))) return null + + return Number(_vModel.value) +}) + const vModel = computed({ get: () => _vModel.value, set: (value) => { @@ -40,17 +48,33 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() -function onKeyDown(evt: KeyboardEvent) { - const cmdOrCtrl = isMac() ? evt.metaKey : evt.ctrlKey - if (cmdOrCtrl && !evt.altKey) { - switch (evt.keyCode) { +function onKeyDown(e: any) { + const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey + if (cmdOrCtrl && !e.altKey) { + switch (e.keyCode) { case 90: { - evt.stopPropagation() + e.stopPropagation() break } } } - return evt.key === '.' && evt.preventDefault() + if (e.key === '.') { + return e.preventDefault() + } + + if (e.key === 'ArrowDown') { + e.preventDefault() + // Move the cursor to the end of the input + e.target.type = 'text' + e.target?.setSelectionRange(e.target.value.length, e.target.value.length) + e.target.type = 'number' + } else if (e.key === 'ArrowUp') { + e.preventDefault() + + e.target.type = 'text' + e.target?.setSelectionRange(0, 0) + e.target.type = 'number' + } } @@ -61,6 +85,7 @@ function onKeyDown(evt: KeyboardEvent) { v-model="vModel" class="outline-none p-0 border-none w-full h-full text-sm" type="number" + style="letter-spacing: 0.06rem" @blur="editEnabled = false" @keydown="onKeyDown" @keydown.down.stop @@ -72,11 +97,23 @@ function onKeyDown(evt: KeyboardEvent) { @mousedown.stop /> NULL - {{ vModel }} + {{ displayValue }} diff --git a/packages/nc-gui/components/cell/Json.vue b/packages/nc-gui/components/cell/Json.vue index 3042946ff4..1206b4b5b2 100644 --- a/packages/nc-gui/components/cell/Json.vue +++ b/packages/nc-gui/components/cell/Json.vue @@ -39,25 +39,25 @@ const vModel = useVModel(props, 'modelValue', emits) const localValueState = ref() -let error = $ref() +const error = ref() -let isExpanded = $ref(false) +const isExpanded = ref(false) const localValue = computed | undefined>({ get: () => localValueState.value, set: (val: undefined | string | Record) => { localValueState.value = typeof val === 'object' ? JSON.stringify(val, null, 2) : val /** if form and not expanded then sync directly */ - if (isForm.value && !isExpanded) { + if (isForm.value && !isExpanded.value) { vModel.value = val } }, }) const clear = () => { - error = undefined + error.value = undefined - isExpanded = false + isExpanded.value = false editEnabled.value = false @@ -66,44 +66,59 @@ const clear = () => { const formatJson = (json: string) => { try { - return JSON.stringify(JSON.parse(json), null, 2) + json = json + .trim() + .replace(/^\{\s*|\s*\}$/g, '') + .replace(/\n\s*/g, '') + json = `{${json}}` + + return json } catch (e) { + console.log(e) return json } } const onSave = () => { - isExpanded = false + isExpanded.value = false editEnabled.value = false - localValue.value = localValue ? formatJson(localValue.value as string) : localValue + vModel.value = localValue ? formatJson(localValue.value as string) : localValue +} - vModel.value = localValue.value +const setLocalValue = (val: any) => { + try { + localValue.value = typeof val === 'string' ? JSON.stringify(JSON.parse(val), null, 2) : val + } catch (e) { + localValue.value = val + } } watch( vModel, (val) => { - localValue.value = val + setLocalValue(val) }, { immediate: true }, ) -watch(localValue, (val) => { +watch([localValue, editEnabled], () => { try { - JSON.parse(val as string) + JSON.parse(localValue.value as string) - error = undefined + error.value = undefined } catch (e: any) { - error = e + if (localValue.value === undefined) return + + error.value = e } }) watch(editEnabled, () => { - isExpanded = false + isExpanded.value = false - localValue.value = vModel.value + setLocalValue(vModel.value) }) useSelectedCellKeyupListener(active, (e) => { diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue index 7a22788276..69d7628c6b 100644 --- a/packages/nc-gui/components/cell/MultiSelect.vue +++ b/packages/nc-gui/components/cell/MultiSelect.vue @@ -4,6 +4,7 @@ import { message } from 'ant-design-vue' import tinycolor from 'tinycolor2' import type { Select as AntSelect } from 'ant-design-vue' import type { SelectOptionType, SelectOptionsType } from 'nocodb-sdk' +import { WorkspaceUserRoles } from 'nocodb-sdk' import { ActiveCellInj, CellClickHookInj, @@ -46,6 +47,8 @@ const column = inject(ColumnInj)! const readOnly = inject(ReadonlyInj)! +const isLockedMode = inject(IsLockedInj, ref(false)) + const isEditable = inject(EditModeInj, ref(false)) const activeCell = inject(ActiveCellInj, ref(false)) @@ -99,7 +102,15 @@ const isOptionMissing = computed(() => { return (options.value ?? []).every((op) => op.title !== searchVal.value) }) -const hasEditRoles = computed(() => hasRole('owner', true) || hasRole('creator', true) || hasRole('editor', true)) +const hasEditRoles = computed( + () => + hasRole('owner', true) || + hasRole('creator', true) || + hasRole('editor', true) || + hasRole(WorkspaceUserRoles.OWNER, true) || + hasRole(WorkspaceUserRoles.CREATOR, true) || + hasRole(WorkspaceUserRoles.EDITOR, true), +) const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value) @@ -334,7 +345,11 @@ const selectedOpts = computed(() => {