Browse Source

chore: resolve conflicts

pull/2424/head
Wing-Kam Wong 2 years ago
parent
commit
9097737eb2
  1. 213
      .github/workflows/release-executables.yml
  2. 2
      .github/workflows/release-nightly-dev.yml
  3. 15
      .github/workflows/release-nocodb.yml
  4. 10
      .github/workflows/release-pr.yml
  5. 158
      .github/workflows/release-timely-executables.yml
  6. 13
      packages/nc-gui/components/CreateOrEditProject.vue
  7. 4
      packages/nc-gui/components/project/spreadsheet/components/Cell.vue
  8. 51
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  9. 31
      packages/nc-gui/components/project/spreadsheet/components/EditableCell.vue
  10. 69
      packages/nc-gui/components/project/spreadsheet/components/cell/DurationCell.vue
  11. 72
      packages/nc-gui/components/project/spreadsheet/components/editColumn/DurationOptions.vue
  12. 139
      packages/nc-gui/components/project/spreadsheet/components/editableCell/DurationCell.vue
  13. 3
      packages/nc-gui/components/project/spreadsheet/components/editableCell/TimePickerCell.vue
  14. 29
      packages/nc-gui/components/project/spreadsheet/mixins/cell.js
  15. 192
      packages/nc-gui/helpers/durationHelper.js
  16. 21
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  17. 42
      packages/noco-docs/content/en/engineering/timely-build.md
  18. 8
      packages/noco-docs/content/en/getting-started/installation.md
  19. 44
      packages/noco-docs/content/en/setup-and-usages/primary-key.md
  20. 2
      packages/noco-docs/content/en/setup-and-usages/primary-value.md
  21. 36
      packages/nocodb-sdk/src/lib/Api.ts
  22. 2
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  23. 2
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  24. 2
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  25. 2
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  26. 27
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  27. 1
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts
  28. 2
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/getAst.ts
  29. 4
      packages/nocodb/src/lib/jobs/RedisJobsMgr.ts
  30. 6
      packages/nocodb/src/lib/meta/api/columnApis.ts
  31. 7
      packages/nocodb/src/lib/meta/api/projectApis.ts
  32. 21
      packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts
  33. 2
      packages/nocodb/src/lib/meta/api/userApi/ui/auth/emailVerify.ts
  34. 40
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  35. 1
      packages/nocodb/src/lib/models/Filter.ts
  36. 275
      scripts/cypress/integration/common/3e_duration_column.js
  37. 3
      scripts/cypress/integration/test/pg-restTableOps.js
  38. 3
      scripts/cypress/integration/test/restTableOps.js
  39. 3
      scripts/cypress/integration/test/xcdb-restTableOps.js
  40. BIN
      scripts/pkg-executable/binaries/binding/napi-v3-darwin-arm64/node_sqlite3.node
  41. BIN
      scripts/pkg-executable/binaries/binding/napi-v3-darwin-x64/node_sqlite3.node
  42. BIN
      scripts/pkg-executable/binaries/binding/napi-v3-linux-x64/node_sqlite3.node
  43. BIN
      scripts/pkg-executable/binaries/binding/napi-v3-win32-ia32/node_sqlite3.node
  44. BIN
      scripts/pkg-executable/binaries/binding/napi-v3-win32-x64/node_sqlite3.node
  45. 12
      scripts/pkg-executable/index.js
  46. 33
      scripts/pkg-executable/package.json
  47. 18
      scripts/sdk/swagger.json

213
.github/workflows/release-executables.yml

@ -5,13 +5,13 @@ on:
workflow_dispatch:
inputs:
tag:
description: "Timely version"
description: "Tag name"
required: true
# Triggered by release-nightly-dev.yml / release-pr.yml
# Triggered by release-nocodb.yml
workflow_call:
inputs:
tag:
description: "Timely version"
description: "Tag name"
required: true
type: string
secrets:
@ -21,10 +21,15 @@ jobs:
build-executables:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Get the latest draft release for asset upload url
- uses: cardinalby/git-get-release-action@v1
id: get_release
env:
GITHUB_TOKEN: ${{ secrets.NC_GITHUB_TOKEN }}
with:
token: ${{ secrets.NC_GITHUB_TOKEN }}
repository: 'nocodb/nocodb-timely'
latest: 1
draft: true
- uses: actions/checkout@v3
- name: Cache node modules
id: cache-npm
uses: actions/cache@v3
@ -38,6 +43,7 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache pkg modules
id: cache-pkg
uses: actions/cache@v3
@ -51,8 +57,11 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
# for building images for all platforms these libraries are required in Linux
- name: Install QEMU and ldid
run: |
sudo apt update
# Install qemu
sudo apt install qemu binfmt-support qemu-user-static
# install ldid
@ -61,26 +70,17 @@ jobs:
./make.sh
sudo cp ./ldid /usr/local/bin
- name: Update nocodb-timely
env:
TAG: ${{ github.event.inputs.tag || inputs.tag }}
run: |
npm i -E nocodb-daily@$TAG
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git commit package.json -m "Update to $TAG"
git tag $TAG
git push --tags
- uses: actions/setup-node@v3
with:
node-version: 16
- name : Install dependencies and build executables
- name : Install nocodb, other dependencies and build executables
run: |
cd ./scripts/pkg-executable
# Install nocodb version based on provided tag name
npm i -E nocodb@$TAG
# install npm dependendencies
npm i
@ -88,37 +88,66 @@ jobs:
rsync -rvzhP ./binaries/binding/ ./node_modules/sqlite3/lib/binding/
# clean up code to optimize size
npx modclean --patterns="default:*" --ignore="nc-lib-gui-daily/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
npx modclean --patterns="default:*" --ignore="nc-lib-gui/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
# build executables
npm run build
ls ./dist
# Move macOS executables for signing
mkdir ./mac-dist
mv ./dist/Noco-macos-arm64 ./mac-dist/
mv ./dist/Noco-macos-x64 ./mac-dist/
# Compress executables
GZIP=-9 tar -czvf ./dist/Noco-linux-x64.tar.gz ./dist/Noco-linux-x64
GZIP=-9 tar -czvf ./dist/Noco-win-x64.tar.gz ./dist/Noco-win-x64.exe
GZIP=-9 tar -czvf ./dist/Noco-linux-arm64.tar.gz ./dist/Noco-linux-arm64
GZIP=-9 tar -czvf ./dist/Noco-win-arm64.tar.gz ./dist/Noco-win-arm64.exe
- name: Upload win-arm64 build to asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/dist/Noco-win-arm64.exe
asset_name: Noco-win-arm64
asset_content_type: application/octet-stream
- name: Upload win-x64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/dist/Noco-win-x64.exe
asset_name: Noco-win-x64
asset_content_type: application/octet-stream
- name: Upload executables(except mac executables) to release
uses: svenstaro/upload-release-action@v2
- name: Upload linux-arm64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
repo_token: ${{ secrets.NC_GITHUB_TOKEN }}
file: dist/**
tag: ${{ github.event.inputs.tag || inputs.tag }}
overwrite: true
file_glob: true
repo_name: nocodb/nocodb-timely
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/dist/Noco-linux-arm64
asset_name: Noco-linux-arm64
asset_content_type: application/octet-stream
- name: Upload linux-x64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/dist/Noco-linux-x64
asset_name: Noco-linux-x64
asset_content_type: application/octet-stream
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
path: scripts/pkg-executable/mac-dist
retention-days: 1
outputs:
upload_url: ${{ steps.get_release.outputs.upload_url }}
sign-mac-executables:
runs-on: macos-latest
needs: build-executables
@ -127,41 +156,111 @@ jobs:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
path: scripts/pkg-executable/mac-dist
- name: Sign macOS executables
run: |
/usr/bin/codesign --force -s - ./mac-dist/Noco-macos-arm64 -v
/usr/bin/codesign --force -s - ./mac-dist/Noco-macos-x64 -v
/usr/bin/codesign --force -s - ./scripts/pkg-executable/mac-dist/Noco-macos-arm64 -v
/usr/bin/codesign --force -s - ./scripts/pkg-executable/mac-dist/Noco-macos-x64 -v
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
path: scripts/pkg-executable/mac-dist
retention-days: 1
publish-mac-executables:
needs: sign-mac-executables
publish-mac-executables-and-homebrew:
needs: [sign-mac-executables,build-executables]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
- name: Compress files
path: scripts/pkg-executable/mac-dist
- uses: actions/checkout@v3
with:
path: 'homebrew-nocodb'
token: ${{ secrets.NC_GITHUB_TOKEN }}
repository: 'nocodb/homebrew-nocodb'
fetch-depth: 0
- name: Compress files and calculate checksum
run: |
cd ./scripts/pkg-executable
cp ./mac-dist/Noco-macos-x64 ./mac-dist/nocodb
tar -czf ./mac-dist/nocodb.tar.gz ./mac-dist/nocodb
rm ./mac-dist/nocodb
echo "::set-output name=CHECKSUM::$(shasum -a 256 ./mac-dist/nocodb.tar.gz | awk '{print $1}')"
id: compress
- name: Upload macos-x64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build-executables.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/mac-dist/Noco-macos-x64
asset_name: Noco-macos-x64
asset_content_type: application/octet-stream
- name: Upload macos-arm64 build to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build-executables.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/mac-dist/Noco-macos-arm64
asset_name: Noco-macos-arm64
asset_content_type: application/octet-stream
- name: Upload macos compressed build(for homebrew) to asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build-executables.outputs.upload_url }}
asset_path: ./scripts/pkg-executable/mac-dist/nocodb.tar.gz
asset_name: nocodb.tar.gz
asset_content_type: application/octet-stream
- name: Generate Homebrew Formula class and push
run: |
GZIP=-9 tar -czvf ./mac-dist/Noco-macos-x64.tar.gz ./mac-dist/Noco-macos-x64
GZIP=-9 tar -czvf ./mac-dist/Noco-macos-arm64.tar.gz ./mac-dist/Noco-macos-arm64
- name: Upload mac executables to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.NC_GITHUB_TOKEN }}
file: mac-dist/**
tag: ${{ github.event.inputs.tag || inputs.tag }}
overwrite: true
file_glob: true
repo_name: nocodb/nocodb-timely
FORMULA_CLASS_STR=$(cat << EOF
class Nocodb < Formula
desc "Get Human Readable file size information. - CLI"
homepage "https://github.com/nocodb/nocodb"
url "https://github.com/nocodb/nocodb/releases/download/${{ github.event.inputs.tag || inputs.tag }}/nocodb.tar.gz"
sha256 "${{ steps.compress.outputs.CHECKSUM }}"
license "MIT"
version "${{ github.event.inputs.tag || inputs.tag }}"
def install
bin.install "nocodb"
end
end
EOF
)
cd ./homebrew-nocodb
printf "$FORMULA_CLASS_STR" > ./Formula/nocodb.rb
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git commit ./Formula/nocodb.rb -m "Automatic publish"
git push

2
.github/workflows/release-nightly-dev.yml

@ -51,7 +51,7 @@ jobs:
# Build executables and publish to GitHub
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-executables.yml
uses: ./.github/workflows/release-timely-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
secrets:

15
.github/workflows/release-nocodb.yml

@ -44,14 +44,14 @@ jobs:
# bump the version from PREV_TAG
TARGET_TAG=$(echo ${PREV_TAG} | awk -F. -v OFS=. '{$NF += 1 ; print}')
fi
echo target version: ${TARGET_TAG}
echo previous version: ${PREV_TAG}
echo "::set-output name=target_tag::${TARGET_TAG}"
echo "::set-output name=prev_tag::${PREV_TAG}"
- name: Verify
run : |
echo ${{ steps.process-input.outputs.target_tag }}
echo ${{ steps.process-input.outputs.target_tag }}
# Merge develop to master
pr-to-master:
@ -94,6 +94,15 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"
# Build executables and publish to GitHub
release-executables:
needs: [release-draft-note, process-input]
uses: ./.github/workflows/release-executables.yml
with:
tag: ${{ needs.process-input.outputs.target_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Close all issues with target tags 'Fixed' & 'Resolved'
close-fixed-issues:
needs: [release-docker, process-input]
@ -101,7 +110,7 @@ jobs:
with:
issue_label: 'Status: Fixed'
version: ${{ needs.process-input.outputs.target_tag }}
close-resolved-issues:
needs: [close-fixed-issues, process-input]
uses: ./.github/workflows/release-close-issue.yml

10
.github/workflows/release-pr.yml

@ -80,7 +80,7 @@ jobs:
# Build executables and publish to GitHub
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-executables.yml
uses: ./.github/workflows/release-timely-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
secrets:
@ -119,8 +119,7 @@ jobs:
#### MacOS
```bash
mkdir -p ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} \
&& cd ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} \
mkdir -p ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} && cd "$_" \
&& curl http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-macos-arm64 -o noco -L \
&& chmod +x noco \
&& ./noco
@ -128,13 +127,12 @@ jobs:
#### Linux
```bash
mkdir -p ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} \
&& cd ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} \
mkdir -p ./${{ needs.set-tag.outputs.current_version }}/${{ needs.set-tag.outputs.target_tag }} && cd "$_" \
&& curl http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-linux-x64 -o noco -L \
&& chmod +x noco \
&& ./noco
```
#### windows
#### Windows
```bash
iwp http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-win-arm64.exe

158
.github/workflows/release-timely-executables.yml

@ -0,0 +1,158 @@
name: "Release : Timely Executables"
on:
# Triggered manually
workflow_dispatch:
inputs:
tag:
description: "Timely version"
required: true
# Triggered by release-nightly-dev.yml / release-pr.yml
workflow_call:
inputs:
tag:
description: "Timely version"
required: true
type: string
secrets:
NC_GITHUB_TOKEN:
required: true
jobs:
build-executables:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.NC_GITHUB_TOKEN }}
repository: 'nocodb/nocodb-timely'
- name: Cache node modules
id: cache-npm
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache pkg modules
id: cache-pkg
uses: actions/cache@v3
env:
cache-name: cache-pkg
with:
# pkg cache files are stored in `~/.pkg-cache`
path: ~/.pkg-cache
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install QEMU and ldid
run: |
sudo apt update
# Install qemu
sudo apt install qemu binfmt-support qemu-user-static
# install ldid
git clone https://github.com/daeken/ldid.git
cd ./ldid
./make.sh
sudo cp ./ldid /usr/local/bin
- name: Update nocodb-timely
env:
TAG: ${{ github.event.inputs.tag || inputs.tag }}
run: |
npm i -E nocodb-daily@$TAG
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git commit package.json -m "Update to $TAG"
git tag $TAG
git push --tags
- uses: actions/setup-node@v3
with:
node-version: 16
- name : Install dependencies and build executables
run: |
# install npm dependendencies
npm i
# Copy sqlite binaries
rsync -rvzhP ./binaries/binding/ ./node_modules/sqlite3/lib/binding/
# clean up code to optimize size
npx modclean --patterns="default:*" --ignore="nc-lib-gui-daily/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
# build executables
npm run build
mkdir ./mac-dist
mv ./dist/Noco-macos-arm64 ./mac-dist/
mv ./dist/Noco-macos-x64 ./mac-dist/
- name: Upload executables(except mac executables) to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.NC_GITHUB_TOKEN }}
file: dist/**
tag: ${{ github.event.inputs.tag || inputs.tag }}
overwrite: true
file_glob: true
repo_name: nocodb/nocodb-timely
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
retention-days: 1
sign-mac-executables:
runs-on: macos-latest
needs: build-executables
steps:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
- name: Sign macOS executables
run: |
/usr/bin/codesign --force -s - ./mac-dist/Noco-macos-arm64 -v
/usr/bin/codesign --force -s - ./mac-dist/Noco-macos-x64 -v
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
retention-days: 1
publish-mac-executables:
needs: sign-mac-executables
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: mac-dist
- name: Upload mac executables to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.NC_GITHUB_TOKEN }}
file: mac-dist/**
tag: ${{ github.event.inputs.tag || inputs.tag }}
overwrite: true
file_glob: true
repo_name: nocodb/nocodb-timely

13
packages/nc-gui/components/CreateOrEditProject.vue

@ -453,14 +453,14 @@
"
/>
</v-col>
<!-- todo : Schema name -->
<!-- Schema name -->
<v-col
v-if="db.client === 'mssql' || db.client === 'pg'"
cols="4"
class="py-0"
>
<v-text-field
v-model="schema"
v-model="db.searchPath[0]"
:disabled="edit && enableDbEdit < 2"
class="body-2 database-field"
:rules="form.requiredRule"
@ -891,7 +891,6 @@ export default {
layout: 'empty',
data() {
return {
schema: 'public',
testSuccess: false,
projectCreated: false,
allSchemas: false,
@ -1542,9 +1541,7 @@ export default {
this.projectReloading = true
const con = projectJson.envs._noco.db[0]
if (con.client === 'pg' || con.client === 'mssql') {
con.searchPath = [this.schema]
} else if ('searchPath' in con) {
if (con.client !== 'pg' && con.client !== 'mssql' && 'searchPath' in con) {
delete con.searchPath
}
@ -1895,9 +1892,9 @@ export default {
},
onDatabaseTypeChanged(client, db1, index, env) {
if (this.databaseNames[client] === 'mssql') {
this.schema = 'dbo'
this.project.envs[env].db[index].searchPath[0] = 'dbo'
} else if (this.databaseNames[client] === 'pg') {
this.schema = 'public'
this.project.envs[env].db[index].searchPath[0] = 'public'
}
for (const env in this.project.envs) {

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

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

@ -35,6 +35,16 @@
v-on="$listeners"
/>
<duration-cell
v-else-if="isDuration"
v-model="localState"
:active="active"
:is-form="isForm"
:column="column"
:is-locked="isLocked"
v-on="parentListeners"
/>
<boolean-cell
v-else-if="isBoolean"
v-model="localState"
@ -65,6 +75,7 @@
v-else-if="isTime"
v-model="localState"
v-on="parentListeners"
@save="$emit('save')"
/>
<date-time-picker-cell
@ -86,7 +97,6 @@
:is-form="isForm"
:column="column"
v-on="parentListeners"
@input="$emit('save')"
/>
<json-editable-cell
@ -102,7 +112,6 @@
v-model="localState"
:column="column"
v-on="parentListeners"
@input="$emit('save')"
/>
<set-list-cell
v-else-if="isSet"
@ -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,10 +210,10 @@ 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) {
this.syncData()
} else if (!this.isCurrency && !this.isEnum && !this.isSet) {
if (this.isAutoSaved) {
this.syncDataDebounce(this)
} else if (!this.isManualSaved) {
this.saveData()
}
}
}
@ -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.isAutoSaved) {
this.changed = false
this.$emit('change')
}
@ -242,6 +253,12 @@ export default {
this.changed = false
this.$emit('update')
}
},
saveData() {
if (this.changed && !this.destroyed) {
this.changed = false
this.$emit('save')
}
}
}
}

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

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

@ -0,0 +1,72 @@
<template>
<v-row class="duration-wrapper">
<div class="caption">
A duration of time in minutes or seconds (e.g. 1:23).
</div>
<!-- TODO: i18n -->
<v-autocomplete
v-model="colMeta.duration"
hide-details
class="caption ui-type nc-ui-dt-dropdown"
label="Duration Format"
dense
outlined
item-value="id"
item-text="title"
:items="durationOptionList"
>
<template #selection="{ item }">
<div>
<span class="caption grey--text text--darken-4">
{{ item.title }}
</span>
</div>
</template>
<template #item="{ item }">
<div class="caption">
{{ item.title }}
</div>
</template>
</v-autocomplete>
</v-row>
</template>
<script>
import { durationOptions } from '~/helpers/durationHelper'
export default {
name: 'DuractionOptions',
props: ['column', 'meta', 'value'],
data: () => ({
durationOptionList: durationOptions.map(o => ({
...o,
// h:mm:ss (e.g. 3:45, 1:23:40)
title: `${o.title} ${o.example}`
})),
colMeta: {
duration: 0
}
}),
watch: {
value() {
this.colMeta = this.value || {}
},
colMeta(v) {
this.$emit('input', v)
}
},
created() {
this.colMeta = this.value ? { ...this.value } : { ...this.colMeta }
}
}
</script>
<style scoped>
.duration-wrapper {
margin: 0;
}
.duration-wrapper .caption:first-child {
margin: -10px 0px 10px 5px;
}
</style>

139
packages/nc-gui/components/project/spreadsheet/components/editableCell/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/>.
*
*/
-->

3
packages/nc-gui/components/project/spreadsheet/components/editableCell/TimePickerCell.vue

@ -5,7 +5,7 @@
</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('update')">
<v-btn small color="primary" @click="$emit('save')">
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
@ -15,7 +15,6 @@
<script>
import dayjs from 'dayjs'
import { MysqlUi } from 'nocodb-sdk'
export default {
name: 'TimePickerCell',

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

@ -67,8 +67,34 @@ export default {
},
isCurrency() {
return this.uiDatatype === 'Currency'
},
isDuration() {
return this.uiDatatype === UITypes.Duration
},
isAutoSaved() {
return [
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(this.uiDatatype)
},
isManualSaved() {
return [
UITypes.Currency,
UITypes.Year,
UITypes.Time,
UITypes.Duration
].includes(this.uiDatatype)
}
}
}
/**
@ -76,6 +102,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 === "" || 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/>.
*
*/

21
packages/noco-docs/content/en/developer-resources/rest-apis.md

@ -21,15 +21,15 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Category | Method | Tag | Function Name | Path |
|---|---|---|---|---|
| Auth | Post | auth | signup | /api/v1/db/auth/user/signup |
| Auth | Post | auth | signin | /api/v1/db/auth/user/signin |
| Auth | Get | auth | me | /api/v1/db/auth/user/me |
| Auth | Post | auth | passwordForgot | /api/v1/db/auth/password/forgot |
| Auth | Post | auth | passwordChange | /api/v1/db/auth/password/change |
| Auth | Post | auth | passwordReset | /api/v1/db/auth/password/reset/{token} |
| Auth | Post | auth | tokenRefresh | /api/v1/db/auth/token/refresh |
| Auth | Post | auth | passwordResetTokenValidate | /api/v1/db/auth/token/validate/{token} |
| Auth | Post | auth | emailValidate | /api/v1/db/auth/email/validate/{email} |
| Auth | Post | auth | signup | /api/v1/auth/user/signup |
| Auth | Post | auth | signin | /api/v1/auth/user/signin |
| Auth | Get | auth | me | /api/v1/auth/user/me |
| Auth | Post | auth | passwordForgot | /api/v1/auth/password/forgot |
| Auth | Post | auth | passwordChange | /api/v1/auth/password/change |
| Auth | Post | auth | passwordReset | /api/v1/auth/password/reset/{token} |
| Auth | Post | auth | tokenRefresh | /api/v1/auth/token/refresh |
| Auth | Post | auth | passwordResetTokenValidate | /api/v1/auth/token/validate/{token} |
| Auth | Post | auth | emailValidate | /api/v1/auth/email/validate/{email} |
### Public APIs
@ -193,7 +193,8 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Operation | Meaning | Example |
|---|---|---|
| eq | equal | (colName,eq,colValue) |
| not | not equal | (colName,not,colValue) |
| neq | not equal | (colName,neq,colValue) |
| not | not equal (alias of neq) | (colName,not,colValue) |
| gt | greater than | (colName,gt,colValue) |
| ge | greater or equal | (colName,ge,colValue) |
| lt | less than | (colName,lt,colValue) |

42
packages/noco-docs/content/en/engineering/timely-build.md

@ -0,0 +1,42 @@
---
title: "Timely Build"
description: "Timely Build"
position: 5000
category: "Engineering"
menuTitle: "Timely Build"
---
NocoDB provides timely build versions on Docker and Executables by compiling our source code and packaging as a deliverable so that it can
- reduce pull request cycle time
- allow issue reporters / reviewers to verify the fix without setting up their local machines
## Docker
When a non-draft Pull Request is created, reopened or synchronized, a timely build for Docker would be triggered for the changes only included in the following paths.
- `packages/nocodb-sdk/**`
- `packages/nc-gui/**`
- `packages/nc-plugin/**`
- `packages/nocodb/**`
The docker images will be built and pushed to Docker Hub (See [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely/tags) for the full list). Once the image is ready, Github bot will add a comment with the command in the pull request. The tag would be `<NOCODB_CURRENT_VERSION>-pr-<PR_NUMBER>-<YYYYMMDD>-<HHMM>`.
![image](https://user-images.githubusercontent.com/35857179/175012097-240dab05-da93-4c4e-87c1-1c36fb1350bd.png)
## Executables
Similarly, we provide a timely build for executables for non-docker users. The source code will be built, packaged as binary files, and pushed to Github (See [nocodb/nocodb-timely](https://github.com/nocodb/nocodb-timely/releases) for the full list).
Currently, we only support the following targets:
- `node16-linux-arm64`
- `node16-macos-arm64`
- `node16-win-arm64`
- `node16-linux-x64`
- `node16-macos-x64`
- `node16-win-x64`
Once the executables are ready, Github bot will add a comment with the commands in the pull request.
![image](https://user-images.githubusercontent.com/35857179/175012070-f5f3e7b8-6dc5-4d1c-9f7e-654bc5039521.png)

8
packages/noco-docs/content/en/getting-started/installation.md

@ -57,6 +57,14 @@ npm install
npm start
```
### Homebrew
```bash
brew tap nocodb/nocodb
brew install nocodb
nocodb
```
### Docker
If you are a Docker user, you may try this way!

44
packages/noco-docs/content/en/setup-and-usages/primary-key.md

@ -0,0 +1,44 @@
---
title: "Primary Key"
description: "Primary Key"
position: 575
category: "Product"
menuTitle: "Primary Key"
---
## What is a Primary Key ?
- A primary key is a special database table column designated to uniquely identify each table record.
## What is the use of Primary Key ?
- As it uniquely identifies an individual record of a table, it is used internally by NocoDB for all operations associated with a record
## Primary Key in NocoDB
- Primary Key that gets defined / used in NocoDB depends on how underlying table was created. Summary is captured below
1. From UI, Create new table / Import from Excel / Import from CSV
1. An `ID` [datatype: Integer] system field created by default during table creation is used as primary key
2. Additional system fields `created-at`, `updated-at` are inserted by default & can be omitted optionally; these fields can be deleted after table creation
2. Connect to existing external database
1. Existing `primary key` field defined for a table is retained as is; NocoDB doesn't insert a new ID field
2. Additional system fields `created-at`, `updated-at` are not inserted by default
3. Import from Airtable
1. Airtable record ID is marked as primary key for imported records, and is mapped to field `ncRecordId` [datatype: varchar]
2. If a new record is inserted after migration & if ncRecordId field was omitted during record insertion - auto generated string will be inserted by NocoDB
3. Computed hash value for the entire record is stored in system field `ncRecordHash`
4. Additional system fields `created-at`, `updated-at` are not inserted by default
4. Create new table using SDK / API
1. No default primary key field is introduced by NocoDB. It has to be explicitly specified during table creation (using attribute `pk: true`)
## What if Primary Key was missing?
It is possible to have a table without any primary key.
- External database table can be created without primary key configuration.
- New table can be created using SDK / API without primary key
In such scenario's, new records can be created in NocoDB for this table, but records can't be updated or deleted [as there is now way for NocoDB to uniquely identify these records]
#### Example : Primary Key & optional system fields during new table creation
![Screenshot 2022-06-16 at 12 15 43 PM](https://user-images.githubusercontent.com/86527202/174010350-8610b9c1-a761-4bff-a53d-dc728df47e1b.png)
#### Example : Show System Fields
![Screenshot 2022-06-16 at 12 16 07 PM](https://user-images.githubusercontent.com/86527202/174010379-9e300d42-ad89-4653-afa2-f70fca407ca8.png)
## Can I change the Primary Key to another column within tables ?
- You can't update Primary Key from NocoDB UI. You can reconfigure it at database level directly & trigger `metasync` explicitly

2
packages/noco-docs/content/en/setup-and-usages/primary-value.md

@ -1,7 +1,7 @@
---
title: "Primary value"
description: "Primary value"
position: 575
position: 580
category: "Product"
menuTitle: "Primary value"
---

36
packages/nocodb-sdk/src/lib/Api.ts

@ -805,7 +805,7 @@ export class Api<
* @tags Auth
* @name Signup
* @summary Signup
* @request POST:/api/v1/db/auth/user/signup
* @request POST:/api/v1/auth/user/signup
* @response `200` `{ token?: string }` OK
* @response `400` `{ msg?: string }` Bad Request
* @response `401` `void` Unauthorized
@ -816,7 +816,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<{ token?: string }, { msg?: string } | void>({
path: `/api/v1/db/auth/user/signup`,
path: `/api/v1/auth/user/signup`,
method: 'POST',
body: data,
format: 'json',
@ -829,7 +829,7 @@ export class Api<
* @tags Auth
* @name Signin
* @summary Signin
* @request POST:/api/v1/db/auth/user/signin
* @request POST:/api/v1/auth/user/signin
* @response `200` `{ token?: string }` OK
* @response `400` `{ msg?: string }` Bad Request
*/
@ -838,7 +838,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<{ token?: string }, { msg?: string }>({
path: `/api/v1/db/auth/user/signin`,
path: `/api/v1/auth/user/signin`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -852,12 +852,12 @@ export class Api<
* @tags Auth
* @name Me
* @summary User info
* @request GET:/api/v1/db/auth/user/me
* @request GET:/api/v1/auth/user/me
* @response `200` `UserInfoType` OK
*/
me: (query?: { project_id?: string }, params: RequestParams = {}) =>
this.request<UserInfoType, any>({
path: `/api/v1/db/auth/user/me`,
path: `/api/v1/auth/user/me`,
method: 'GET',
query: query,
format: 'json',
@ -870,13 +870,13 @@ export class Api<
* @tags Auth
* @name PasswordForgot
* @summary Password forgot
* @request POST:/api/v1/db/auth/password/forgot
* @request POST:/api/v1/auth/password/forgot
* @response `200` `void` OK
* @response `401` `void` Unauthorized
*/
passwordForgot: (data: { email?: string }, params: RequestParams = {}) =>
this.request<void, void>({
path: `/api/v1/db/auth/password/forgot`,
path: `/api/v1/auth/password/forgot`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -889,7 +889,7 @@ export class Api<
* @tags Auth
* @name PasswordChange
* @summary Password change
* @request POST:/api/v1/db/auth/password/change
* @request POST:/api/v1/auth/password/change
* @response `200` `{ msg?: string }` OK
* @response `400` `{ msg?: string }` Bad request
*/
@ -898,7 +898,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<{ msg?: string }, { msg?: string }>({
path: `/api/v1/db/auth/password/change`,
path: `/api/v1/auth/password/change`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -912,12 +912,12 @@ export class Api<
* @tags Auth
* @name PasswordResetTokenValidate
* @summary Reset token verify
* @request POST:/api/v1/db/auth/token/validate/{token}
* @request POST:/api/v1/auth/token/validate/{token}
* @response `200` `void` OK
*/
passwordResetTokenValidate: (token: string, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/auth/token/validate/${token}`,
path: `/api/v1/auth/token/validate/${token}`,
method: 'POST',
...params,
}),
@ -928,12 +928,12 @@ export class Api<
* @tags Auth
* @name EmailValidate
* @summary Verify email
* @request POST:/api/v1/db/auth/email/validate/{token}
* @request POST:/api/v1/auth/email/validate/{token}
* @response `200` `void` OK
*/
emailValidate: (token: string, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/auth/email/validate/${token}`,
path: `/api/v1/auth/email/validate/${token}`,
method: 'POST',
...params,
}),
@ -944,7 +944,7 @@ export class Api<
* @tags Auth
* @name PasswordReset
* @summary Password reset
* @request POST:/api/v1/db/auth/password/reset/{token}
* @request POST:/api/v1/auth/password/reset/{token}
* @response `200` `void` OK
*/
passwordReset: (
@ -953,7 +953,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<void, any>({
path: `/api/v1/db/auth/password/reset/${token}`,
path: `/api/v1/auth/password/reset/${token}`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -966,12 +966,12 @@ export class Api<
* @tags Auth
* @name TokenRefresh
* @summary Refresh token
* @request POST:/api/v1/db/auth/token/refresh
* @request POST:/api/v1/auth/token/refresh
* @response `200` `void` OK
*/
tokenRefresh: (params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/auth/token/refresh`,
path: `/api/v1/auth/token/refresh`,
method: 'POST',
...params,
}),

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

@ -1147,7 +1147,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';

27
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -91,7 +91,7 @@ class BaseModelSqlv2 {
}
public async readByPk(id?: any): Promise<any> {
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
@ -107,7 +107,7 @@ class BaseModelSqlv2 {
}
public async exist(id?: any): Promise<any> {
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
const pks = this.model.primaryKeys;
if ((id + '').split('___').length != pks.length) {
@ -120,12 +120,14 @@ class BaseModelSqlv2 {
args: {
where?: string;
filterArr?: Filter[];
sort?: string | string[];
} = {}
): Promise<any> {
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
const aliasColObjMap = await this.model.getAliasColObjMap();
const sorts = extractSortsObject(args?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(args?.where, aliasColObjMap);
await conditionV2(
@ -146,6 +148,12 @@ class BaseModelSqlv2 {
this.dbDriver
);
if (Array.isArray(sorts) && sorts?.length) {
await sortV2(sorts, qb, this.dbDriver);
} else if (this.model.primaryKey) {
qb.orderBy(this.model.primaryKey.column_name);
}
const data = await qb.first();
if (data) {
@ -168,15 +176,12 @@ class BaseModelSqlv2 {
): Promise<any> {
const { where, ...rest } = this._getListArgs(args as any);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
const aliasColObjMap = await this.model.getAliasColObjMap();
let sorts = extractSortsObject(args?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(args?.where, aliasColObjMap);
// todo: replace with view id
if (!ignoreFilterSort && this.viewId) {
await conditionV2(
@ -257,7 +262,7 @@ class BaseModelSqlv2 {
await this.model.getColumns();
const { where } = this._getListArgs(args);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
// qb.xwhere(where, await this.model.getAliasColMapping());
const aliasColObjMap = await this.model.getAliasColObjMap();
@ -328,7 +333,7 @@ class BaseModelSqlv2 {
) {
const { where, ...rest } = this._getListArgs(args as any);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
qb.count(`${this.model.primaryKey?.column_name || '*'} as count`);
qb.select(args.column_name);
@ -1615,7 +1620,7 @@ class BaseModelSqlv2 {
} else {
await this.model.getColumns();
const { where } = this._getListArgs(args);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
const aliasColObjMap = await this.model.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
@ -1677,7 +1682,7 @@ class BaseModelSqlv2 {
try {
await this.model.getColumns();
const { where } = this._getListArgs(args);
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
const aliasColObjMap = await this.model.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);

1
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts

@ -219,6 +219,7 @@ const parseConditionV2 = async (
qb = qb.where(field, val);
break;
case 'neq':
case 'not':
qb = qb.whereNot(field, val);
break;
case 'like':

2
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/getAst.ts

@ -86,7 +86,7 @@ const getAst = async ({
(!fields?.length || fields.includes(col.title)) &&
value
: fields?.length
? fields.includes(col.title)
? fields.includes(col.title) && value
: value
};
}, Promise.resolve({}));

4
packages/nocodb/src/lib/jobs/RedisJobsMgr.ts

@ -11,7 +11,9 @@ export default class RedisJobsMgr extends JobsMgr {
super();
this.queue = {};
this.workers = {};
this.connection = new Redis(config);
this.connection = new Redis(config, {
maxRetriesPerRequest: null
});
}
async add(

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,

7
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -416,6 +416,13 @@ export async function projectCost(req, res) {
cost = Math.min(120 * userCount, 36000);
}
Tele.event({
event: 'a:project:cost',
data: {
cost
}
});
res.json({ cost });
}

21
packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts

@ -166,6 +166,27 @@ export const getModelPaths = async (ctx: {
}
}
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/find-one`]: {
get: {
summary: `${ctx.tableName} find-one`,
operationId: 'db-table-row-find-one',
description: `Find first record matching the conditions.`,
tags: [ctx.tableName],
parameters: [fieldsParam, whereParam, sortParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`
}
}
}
}
}
}
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/groupby`]: {
get: {
summary: `${ctx.tableName} groupby`,

2
packages/nocodb/src/lib/meta/api/userApi/ui/auth/emailVerify.ts

@ -54,7 +54,7 @@ export default `<!DOCTYPE html>
methods: {},
async created() {
try {
const valid = (await axios.post('<%- baseUrl %>/api/v1/db/auth/email/validate/' + this.token)).data;
const valid = (await axios.post('<%- baseUrl %>/api/v1/auth/email/validate/' + this.token)).data;
this.valid = !!valid;
} catch (e) {
this.valid = false;

40
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -327,10 +327,10 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
subject: 'Password Reset Link',
text: `Visit following link to update your password : ${
(req as any).ncSiteUrl
}/api/v1/db/auth/password/reset/${token}.`,
}/api/v1/auth/password/reset/${token}.`,
html: ejs.render(template, {
resetLink:
(req as any).ncSiteUrl + `/api/v1/db/auth/password/reset/${token}`
(req as any).ncSiteUrl + `/api/v1/auth/password/reset/${token}`
})
})
);
@ -516,7 +516,7 @@ const mapRoutes = router => {
})(req, res, next)
);
// new API
// deprecated APIs
router.post('/api/v1/db/auth/user/signup', catchError(signup));
router.post('/api/v1/db/auth/user/signin', catchError(signin));
router.get(
@ -549,5 +549,39 @@ const mapRoutes = router => {
'/api/v1/db/auth/password/reset/:tokenId',
catchError(renderPasswordReset)
);
// new API
router.post('/api/v1/auth/user/signup', catchError(signup));
router.post('/api/v1/auth/user/signin', catchError(signin));
router.get(
'/api/v1/auth/user/me',
extractProjectIdAndAuthenticate,
catchError(me)
);
router.post('/api/v1/auth/password/forgot', catchError(passwordForgot));
router.post(
'/api/v1/auth/token/validate/:tokenId',
catchError(tokenValidate)
);
router.post(
'/api/v1/auth/password/reset/:tokenId',
catchError(passwordReset)
);
router.post(
'/api/v1/auth/email/validate/:tokenId',
catchError(emailVerification)
);
router.post(
'/api/v1/auth/password/change',
ncMetaAclMw(passwordChange, 'passwordChange')
);
router.post(
'/api/v1/auth/token/refresh',
ncMetaAclMw(refreshToken, 'refreshToken')
);
router.get(
'/api/v1/auth/password/reset/:tokenId',
catchError(renderPasswordReset)
);
};
export { mapRoutes as userApis };

1
packages/nocodb/src/lib/models/Filter.ts

@ -23,6 +23,7 @@ export default class Filter {
comparison_op?:
| 'eq'
| 'neq'
| 'not'
| 'like'
| 'nlike'
| 'empty'

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
*

BIN
scripts/pkg-executable/binaries/binding/napi-v3-darwin-arm64/node_sqlite3.node

Binary file not shown.

BIN
scripts/pkg-executable/binaries/binding/napi-v3-darwin-x64/node_sqlite3.node

Binary file not shown.

BIN
scripts/pkg-executable/binaries/binding/napi-v3-linux-x64/node_sqlite3.node

Binary file not shown.

BIN
scripts/pkg-executable/binaries/binding/napi-v3-win32-ia32/node_sqlite3.node

Binary file not shown.

BIN
scripts/pkg-executable/binaries/binding/napi-v3-win32-x64/node_sqlite3.node

Binary file not shown.

12
scripts/pkg-executable/index.js

@ -0,0 +1,12 @@
process.env.NC_BINARY_BUILD = 'true';
(async () => {
try {
const app = require('express')();
const {Noco} = require("nocodb");
const httpServer = app.listen(process.env.PORT || 8080);
app.use(await Noco.init({}, httpServer, app));
console.log(`Visit : localhost:${process.env.PORT}/dashboard`)
} catch(e) {
console.log(e)
}
})()

33
scripts/pkg-executable/package.json

@ -0,0 +1,33 @@
{
"name": "Noco",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"upgrade": "npm uninstall --save nocodb && npm install --save nocodb",
"build": "npx pkg . --out-path dist --compress GZip"
},
"pkg": {
"assets": [
"node_modules/**/*"
],
"targets": [
"node16-linux-arm64",
"node16-macos-arm64",
"node16-win-arm64",
"node16-linux-x64",
"node16-macos-x64",
"node16-win-x64"
]
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"nocodb": "0.91.10"
}
}

18
scripts/sdk/swagger.json

@ -10,7 +10,7 @@
}
],
"paths": {
"/api/v1/db/auth/user/signup": {
"/api/v1/auth/user/signup": {
"post": {
"summary": "Signup",
"operationId": "auth-signup",
@ -95,7 +95,7 @@
"description": "Create a new user with provided email and password and first user is marked as super admin. "
}
},
"/api/v1/db/auth/user/signin": {
"/api/v1/auth/user/signin": {
"post": {
"summary": "Signin",
"operationId": "auth-signin",
@ -167,7 +167,7 @@
},
"parameters": []
},
"/api/v1/db/auth/user/me": {
"/api/v1/auth/user/me": {
"parameters": [],
"get": {
"summary": "User info",
@ -214,7 +214,7 @@
]
}
},
"/api/v1/db/auth/password/forgot": {
"/api/v1/auth/password/forgot": {
"post": {
"summary": "Password forgot",
"operationId": "auth-password-forgot",
@ -248,7 +248,7 @@
},
"parameters": []
},
"/api/v1/db/auth/password/change": {
"/api/v1/auth/password/change": {
"post": {
"summary": "Password change",
"operationId": "auth-password-change",
@ -336,7 +336,7 @@
},
"parameters": []
},
"/api/v1/db/auth/token/validate/{token}": {
"/api/v1/auth/token/validate/{token}": {
"post": {
"summary": "Reset token verify",
"operationId": "auth-password-reset-token-validate",
@ -361,7 +361,7 @@
}
]
},
"/api/v1/db/auth/email/validate/{token}": {
"/api/v1/auth/email/validate/{token}": {
"post": {
"summary": "Verify email",
"operationId": "auth-email-validate",
@ -386,7 +386,7 @@
}
]
},
"/api/v1/db/auth/password/reset/{token}": {
"/api/v1/auth/password/reset/{token}": {
"post": {
"summary": "Password reset",
"operationId": "auth-password-reset",
@ -425,7 +425,7 @@
}
]
},
"/api/v1/db/auth/token/refresh": {
"/api/v1/auth/token/refresh": {
"post": {
"summary": "Refresh token",
"operationId": "auth-token-refresh",

Loading…
Cancel
Save