diff --git a/.github/workflows/pr-to-master.yml b/.github/workflows/pr-to-master.yml index a4eef87b54..989f239320 100644 --- a/.github/workflows/pr-to-master.yml +++ b/.github/workflows/pr-to-master.yml @@ -55,7 +55,7 @@ jobs: echo "Pull Request URL - ${{ steps.cpr.outputs.pr_url }}" - name: automerge if: ${{ github.event.inputs.targetEnv == 'PROD' || inputs.targetEnv == 'PROD' }} - uses: "pascalgn/automerge-action@v0.14.3" + uses: "pascalgn/automerge-action@v0.15.5" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" PULL_REQUEST: "${{ steps.cpr.outputs.pr_number }}" diff --git a/.github/workflows/release-executables.yml b/.github/workflows/release-executables.yml index fff82b1e69..c05211abe8 100644 --- a/.github/workflows/release-executables.yml +++ b/.github/workflows/release-executables.yml @@ -21,14 +21,6 @@ jobs: build-executables: runs-on: ubuntu-latest steps: - # 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: - latest: 1 - draft: true - uses: actions/checkout@v3 - name: Cache node modules id: cache-npm @@ -107,54 +99,25 @@ jobs: mv ./dist/Noco-macos-arm64 ./mac-dist/ mv ./dist/Noco-macos-x64 ./mac-dist/ - - name: Upload win-arm64 build to asset + - name: Upload executables to asset id: upload-release-asset - uses: actions/upload-release-asset@v1 + uses: softprops/action-gh-release@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.exe - 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.exe - asset_content_type: application/octet-stream - - - name: Upload linux-arm64 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-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 + draft: true + tag_name: ${{ github.event.inputs.tag || inputs.tag }} + files: | + ./scripts/pkg-executable/dist/Noco-win-arm64.exe + ./scripts/pkg-executable/dist/Noco-win-x64.exe + ./scripts/pkg-executable/dist/Noco-linux-arm64 + ./scripts/pkg-executable/dist/Noco-linux-x64 - uses: actions/upload-artifact@master with: name: ${{ github.event.inputs.tag || inputs.tag }} 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 @@ -205,40 +168,17 @@ jobs: 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 + - name: Upload macos executable to asset + uses: softprops/action-gh-release@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 - + draft: true + tag_name: ${{ github.event.inputs.tag || inputs.tag }} + files: | + ./scripts/pkg-executable/mac-dist/Noco-macos-x64 + ./scripts/pkg-executable/mac-dist/Noco-macos-arm64 + ./scripts/pkg-executable/mac-dist/nocodb.tar.gz - name: Generate Homebrew Formula class and push run: | @@ -267,7 +207,3 @@ jobs: git commit ./Formula/nocodb.rb -m "Automatic publish" git push - - - - diff --git a/.github/workflows/release-nightly-dev.yml b/.github/workflows/release-nightly-dev.yml index 1dda89956f..30bd16e41e 100644 --- a/.github/workflows/release-nightly-dev.yml +++ b/.github/workflows/release-nightly-dev.yml @@ -12,6 +12,11 @@ jobs: set-tag: runs-on: 'ubuntu-latest' steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 1 + ref: ${{ github.ref }} - name: set-tag id: tag-step run: | @@ -21,7 +26,7 @@ jobs: TAG_NAME=${CURRENT_DATE}-${CURRENT_TIME} IS_DAILY='Y' # Get current version - CURRENT_VERSION=$(curl -fs https://docs.nocodb.com/releases | grep article | grep div | grep h2 | grep 'id\="[^"]*' -o | cut -c 5-) + CURRENT_VERSION=$(cat ./packages/nocodb/package.json | jq -r ".version") # Set the tag if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then IS_DAILY='N' @@ -49,13 +54,13 @@ jobs: NPM_TOKEN: "${{ secrets.NPM_TOKEN }}" # Build executables and publish to GitHub - release-executables: - needs: [set-tag, release-npm] - uses: ./.github/workflows/release-timely-executables.yml - with: - tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }} - secrets: - NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}" + # release-executables: + # needs: [set-tag, release-npm] + # uses: ./.github/workflows/release-timely-executables.yml + # with: + # tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }} + # secrets: + # NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}" # Build docker image and push to docker hub release-docker: diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index 4da1dd71e5..55ec46d9af 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -68,7 +68,7 @@ jobs: - name: Create Pull Request if: ${{ github.event.inputs.targetEnv == 'PROD' || inputs.targetEnv == 'PROD' }} id: cpr - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v4.2.3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -84,7 +84,7 @@ jobs: echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" - name: automerge if: ${{ github.event.inputs.targetEnv == 'PROD' || inputs.targetEnv == 'PROD' }} - uses: "pascalgn/automerge-action@v0.14.3" + uses: "pascalgn/automerge-action@v0.15.5" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" PULL_REQUEST: "${{ steps.cpr.outputs.pull-request-number }}" diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 4a9a56b537..d29e6b7a17 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -33,7 +33,7 @@ jobs: # Get current PR number PR_NUMBER=${{github.event.number}} # Get current version - CURRENT_VERSION=$(curl -fs https://docs.nocodb.com/releases | grep article | grep div | grep h2 | grep 'id\="[^"]*' -o | cut -c 5-) + CURRENT_VERSION=$(curl -fs https://docs.nocodb.com/releases | grep article | grep div | grep h2 | grep 'id\="[^"]*' -o | cut -c 5- | cut -d\: -f1) # Construct tag name TAG_NAME=pr-${PR_NUMBER}-${CURRENT_DATE}-${CURRENT_TIME} echo "TARGET_TAG=${TAG_NAME}" >> $GITHUB_OUTPUT @@ -72,14 +72,14 @@ jobs: DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" # Build executables and publish to GitHub - release-executables: - if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }} - needs: [set-tag, release-npm] - uses: ./.github/workflows/release-timely-executables.yml - with: - tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }} - secrets: - NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}" + # release-executables: + # if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }} + # needs: [set-tag, release-npm] + # uses: ./.github/workflows/release-timely-executables.yml + # with: + # tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }} + # secrets: + # NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}" # Add a comment for PR docker build leave-comment: @@ -132,40 +132,40 @@ jobs: retention-days: 2 # Add a comment for PR executable build - leave-executable-comment: - if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }} - runs-on: 'ubuntu-latest' - needs: [release-executables, set-tag] - steps: - - uses: peter-evans/commit-comment@v2 - with: - body: | - ### Run Executables + # leave-executable-comment: + # if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }} + # runs-on: 'ubuntu-latest' + # needs: [release-executables, set-tag] + # steps: + # - uses: peter-evans/commit-comment@v2 + # with: + # body: | + # ### Run Executables - #### MacOS + # #### MacOS - ```bash - 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 - ``` - #### Linux + # ```bash + # 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 + # ``` + # #### Linux - ```bash - 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 + # ```bash + # 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 - ```bash - iwr http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-win-arm64.exe - .\Noco-win-arm64.exe - ``` + # ```bash + # iwr http://dl.nocodb.com/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}/Noco-win-arm64.exe + # .\Noco-win-arm64.exe + # ``` - For executables visit [here](https://github.com/nocodb/nocodb-timely/releases/tag/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}) + # For executables visit [here](https://github.com/nocodb/nocodb-timely/releases/tag/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}) # delete the uffizzi preview created off of this PR delete-uffizzi-preview: diff --git a/.github/workflows/update-sdk-path.yml b/.github/workflows/update-sdk-path.yml index fed2a4e22e..b71bf2010b 100644 --- a/.github/workflows/update-sdk-path.yml +++ b/.github/workflows/update-sdk-path.yml @@ -27,7 +27,7 @@ jobs: - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v4.2.3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -41,7 +41,7 @@ jobs: echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" - name: automerge - uses: "pascalgn/automerge-action@v0.14.3" + uses: "pascalgn/automerge-action@v0.15.5" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" PULL_REQUEST: "${{ steps.cpr.outputs.pull-request-number }}" diff --git a/charts/nocodb/.gitignore b/charts/nocodb/.gitignore new file mode 100644 index 0000000000..f791801bca --- /dev/null +++ b/charts/nocodb/.gitignore @@ -0,0 +1,2 @@ +charts/ +Chart.lock diff --git a/charts/nocodb/.helmignore b/charts/nocodb/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/charts/nocodb/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/nocodb/Chart.yaml b/charts/nocodb/Chart.yaml new file mode 100644 index 0000000000..db58bf049b --- /dev/null +++ b/charts/nocodb/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +appVersion: 0.100.2 +dependencies: +- condition: postgresql.enabled + name: postgresql + repository: https://charts.bitnami.com/bitnami + version: ~11.6.6 +description: A Helm chart for Kubernetes +maintainers: [] +name: nocodb +type: application +version: 0.3.0 diff --git a/charts/nocodb/templates/NOTES.txt b/charts/nocodb/templates/NOTES.txt new file mode 100644 index 0000000000..0594ffd042 --- /dev/null +++ b/charts/nocodb/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "nocodb.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "nocodb.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "nocodb.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "nocodb.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/charts/nocodb/templates/_helpers.tpl b/charts/nocodb/templates/_helpers.tpl new file mode 100644 index 0000000000..6a5723a0df --- /dev/null +++ b/charts/nocodb/templates/_helpers.tpl @@ -0,0 +1,82 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nocodb.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "nocodb.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nocodb.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nocodb.labels" -}} +helm.sh/chart: {{ include "nocodb.chart" . }} +{{ include "nocodb.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nocodb.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nocodb.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "nocodb.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nocodb.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "postgresHost" -}} +{{- printf "%s-postgresql" .Release.Name }} +{{- end }} + +{{- define "postgresDatabase" -}} +{{- .Values.postgresql.auth.database }} +{{- end }} + +{{- define "postgresUsername" -}} +{{- .Values.postgresql.auth.username }} +{{- end }} + +{{- define "postgresPassword" -}} +{{- .Values.postgresql.auth.password }} +{{- end }} + +{{- define "databaseUri" -}} +{{- printf "pg://%s:5432?u=%s&p=%s&d=%s" (include "postgresHost" .) (include "postgresUsername" .) (include "postgresPassword" .) (include "postgresDatabase" .) }} +{{- end }} diff --git a/charts/nocodb/templates/configmap.yaml b/charts/nocodb/templates/configmap.yaml new file mode 100644 index 0000000000..ef83a94612 --- /dev/null +++ b/charts/nocodb/templates/configmap.yaml @@ -0,0 +1,13 @@ +{{- if .Values.extraEnvs }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "nocodb.fullname" . }} + labels: + {{- include "nocodb.labels" . | nindent 4 }} +data: +{{- range $key, $value := .Values.extraEnvs }} + {{ $key }}: |- + {{- $value | nindent 4 }} +{{- end }} +{{- end }} diff --git a/charts/nocodb/templates/deployment.yaml b/charts/nocodb/templates/deployment.yaml new file mode 100644 index 0000000000..582d365df8 --- /dev/null +++ b/charts/nocodb/templates/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nocodb.fullname" . }} + labels: + {{- include "nocodb.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "nocodb.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nocodb.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nocodb.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + volumeMounts: + - name: {{ include "nocodb.fullname" . }} + mountPath: /usr/app/data + envFrom: + - configMapRef: + name: {{ include "nocodb.fullname" . }} + - secretRef: + name: {{ include "nocodb.fullname" . }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: {{ include "nocodb.fullname" . }} + persistentVolumeClaim: + claimName: {{ include "nocodb.fullname" . }} diff --git a/charts/nocodb/templates/hpa.yaml b/charts/nocodb/templates/hpa.yaml new file mode 100644 index 0000000000..d09679632c --- /dev/null +++ b/charts/nocodb/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "nocodb.fullname" . }} + labels: + {{- include "nocodb.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "nocodb.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/nocodb/templates/ingress.yaml b/charts/nocodb/templates/ingress.yaml new file mode 100644 index 0000000000..86dd9bc2f8 --- /dev/null +++ b/charts/nocodb/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "nocodb.fullname" . -}} +{{- $svcPort := .Values.ingress.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "nocodb.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/nocodb/templates/pvc.yaml b/charts/nocodb/templates/pvc.yaml new file mode 100644 index 0000000000..67656c01d1 --- /dev/null +++ b/charts/nocodb/templates/pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "nocodb.fullname" . }} + labels: + {{- include "nocodb.selectorLabels" . | nindent 8 }} +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: {{ .Values.storage.size }} + storageClassName: {{ .Values.storage.storageClassName }} + volumeMode: Filesystem diff --git a/charts/nocodb/templates/secret.yaml b/charts/nocodb/templates/secret.yaml new file mode 100644 index 0000000000..85dd6743aa --- /dev/null +++ b/charts/nocodb/templates/secret.yaml @@ -0,0 +1,13 @@ +{{- if .Values.extraSecretEnvs }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "nocodb.fullname" . }} + labels: + {{- include "nocodb.labels" . | nindent 4 }} +data: + NC_DB: {{ include "databaseUri" . | b64enc}} +{{- range $key, $value := .Values.extraSecretEnvs }} + {{ $key }}: '{{ $value | b64enc }}' +{{- end }} +{{- end }} diff --git a/charts/nocodb/templates/service.yaml b/charts/nocodb/templates/service.yaml new file mode 100644 index 0000000000..92d484d6bf --- /dev/null +++ b/charts/nocodb/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nocodb.fullname" . }} + labels: + {{- include "nocodb.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "nocodb.selectorLabels" . | nindent 4 }} diff --git a/charts/nocodb/templates/serviceaccount.yaml b/charts/nocodb/templates/serviceaccount.yaml new file mode 100644 index 0000000000..e61ce26515 --- /dev/null +++ b/charts/nocodb/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nocodb.serviceAccountName" . }} + labels: + {{- include "nocodb.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/nocodb/templates/tests/test-connection.yaml b/charts/nocodb/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..528686c6e7 --- /dev/null +++ b/charts/nocodb/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "nocodb.fullname" . }}-test-connection" + labels: + {{- include "nocodb.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "nocodb.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/nocodb/values.yaml b/charts/nocodb/values.yaml new file mode 100644 index 0000000000..5d7bf7887e --- /dev/null +++ b/charts/nocodb/values.yaml @@ -0,0 +1,98 @@ +# Default values for nocodb. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nocodb/nocodb + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: false + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +extraEnvs: + NC_PUBLIC_URL: https:/nocodb.local.org + +extraSecretEnvs: + NC_AUTH_JWT_SECRET: secretString + +storage: + size: 3Gi + storageClassName: "" + +postgresql: + enabled: false + auth: + database: nocodb + username: nocodb + password: secretPass + persistence: + size: 8Gi diff --git a/markdown/readme/languages/korean.md b/markdown/readme/languages/korean.md index 0e87dfd4dc..91bb1b9402 100644 --- a/markdown/readme/languages/korean.md +++ b/markdown/readme/languages/korean.md @@ -6,7 +6,7 @@

-MySQL, PostgreSQL, SQL Server, SQLite, MariaDB를 똑똑한 스프레드시트로 바꿔줍니다. +MySQL, PostgreSQL, SQL Server, SQLite, MariaDB를 스마트 스프레드시트로 바꿔줍니다.

@@ -32,7 +32,7 @@ MySQL, PostgreSQL, SQL Server, SQLite, MariaDB를 똑똑한 스프레드시트 NocoDB - The Open Source Airtable alternative | Product Hunt

-# 바로 써보기 +# 빠른 시도 ### Docker 사용 @@ -65,7 +65,7 @@ npm start ### GUI -대시보드 접근하기 : [http://localhost:8080/dashboard](http://localhost:8080/dashboard) +대시보드 접근 : [http://localhost:8080/dashboard](http://localhost:8080/dashboard) # 커뮤니티 가입 @@ -114,18 +114,17 @@ npm start # 기능 -### 스프레드시트같은 인터페이스 +### 스프레드시트 인터페이스 -- ⚡ 정말 쉬운 검색, 정렬, 필터링, 열 숨기기 -- ⚡ 뷰 만들기: 그리드, 갤러리, 칸반, 간트 차트, 양식 -- ⚡ 뷰 공유하기: 완전 공개, 패스워드 걸고 공개 -- ⚡ 개인화하거나 잠글 수 있는 뷰 -- ⚡ 이미지를 자신의 공간에 업로드 (S3, Minio, GCP, Azure, Digitalocean, Linode, OVH, Backblaze 등) -- ⚡ 역할 부여: 소유자, 작성자, 편집자, 보기 전용, 의견 제시만, 원하는 대로 -- ⚡ 접근 통제: 데이터베이스, 테이블 및 열 수준까지도 상세한 통제 가능 +- ⚡ 기본 오퍼레이션: 테이블, 칼럼, 로우 CRUD +- ⚡ 필드 오퍼레이션: 정렬, 필터, 칼럼 보기/숨기기 +- ⚡ 뷰 타입: 그리드, 갤러리, 칸반, 간트 차트, 양식(Form) +- ⚡ 공유: 공개 / 비공개 뷰 (비밀 번호 설정) +- ⚡ 다양한 셀 타입: ID, LinkToAnotherRecord, Lookup, Rollup, SingleLine Text, Attachment, Currency, Formula 등 +- ⚡ 역할에 따른 접근 제한: 다양한 수준의 세분화된 액세스 제어 ### 워크플로 자동화를 위한 앱스토어 - +크게 채팅, 이메일, 저장소 세 가지 카테고리에 대한 통합을 제공합니다. 자세한 사항은 App Store 를 참고하세요. - ⚡ 채팅: MS 팀즈, 슬랙, 디스코드, 매터모스트 - ⚡ 이메일: SMTP, SES, MailChimp - ⚡ SMS: Twilio @@ -188,12 +187,12 @@ docker-compose up -d 여기서 확인해주세요. [환경변수 ](https://docs.nocodb.com/getting-started/installation#environment-variables) -# 개발 환경에 설치하기 +# 개발 환경에 설치 여기서 확인해주세요. [개발 환경에 설치하는 법](https://docs.nocodb.com/engineering/development-setup) -# 기여하기 +# 기여 여기서 확인해주세요. [기여 가이드라인](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md). diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts index 9ee21c06dd..386d0f7775 100644 --- a/packages/nc-gui/components.d.ts +++ b/packages/nc-gui/components.d.ts @@ -126,6 +126,7 @@ declare module '@vue/runtime-core' { MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default'] MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default'] MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default'] + MdiArrowULeftBottom: typeof import('~icons/mdi/arrow-u-left-bottom')['default'] MdiAt: typeof import('~icons/mdi/at')['default'] MdiBackburger: typeof import('~icons/mdi/backburger')['default'] MdiBookOpenOutline: typeof import('~icons/mdi/book-open-outline')['default'] @@ -139,6 +140,7 @@ declare module '@vue/runtime-core' { MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default'] MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] + MdiChevronUp: typeof import('~icons/mdi/chevron-up')['default'] MdiClose: typeof import('~icons/mdi/close')['default'] MdiCloseBox: typeof import('~icons/mdi/close-box')['default'] MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default'] diff --git a/packages/nc-gui/components/account/UserList.vue b/packages/nc-gui/components/account/UserList.vue index 4ce5764c3e..4c1eaa18ae 100644 --- a/packages/nc-gui/components/account/UserList.vue +++ b/packages/nc-gui/components/account/UserList.vue @@ -71,7 +71,7 @@ const deleteUser = async (userId: string) => { Modal.confirm({ title: 'Are you sure you want to delete this user?', type: 'warn', - content: 'On deleting, user will remove from organization and any sync source(Airtable) created by user will get removed', + content: 'Upon deletion, the user will be removed from the installation.', onOk: async () => { try { await api.orgUsers.delete(userId) diff --git a/packages/nc-gui/components/cell/Checkbox.vue b/packages/nc-gui/components/cell/Checkbox.vue index 6b1bd0e50c..dfffdec862 100644 --- a/packages/nc-gui/components/cell/Checkbox.vue +++ b/packages/nc-gui/components/cell/Checkbox.vue @@ -65,7 +65,7 @@ useSelectedCellKeyupListener(active, (e) => {
-
-
+
+
@@ -134,8 +134,8 @@ useSelectedCellKeyupListener(active, (e) => {
Cancel
- -
Save
+ +
Save
diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue index 3815933e49..7b7d69c191 100644 --- a/packages/nc-gui/components/cell/MultiSelect.vue +++ b/packages/nc-gui/components/cell/MultiSelect.vue @@ -19,6 +19,7 @@ import { useEventListener, useMetas, useProject, + useRoles, useSelectedCellKeyupListener, watch, } from '#imports' @@ -57,6 +58,8 @@ const { $api } = useNuxtApp() const { getMeta } = useMetas() +const { hasRole } = useRoles() + const { isPg, isMysql } = useProject() // a variable to keep newly created options value @@ -80,6 +83,10 @@ 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 editAllowed = computed(() => hasEditRoles.value && (active.value || editable.value)) + const vModel = computed({ get: () => { const selected = selectedIds.value.reduce((acc, id) => { @@ -154,10 +161,12 @@ watch( ) watch(isOpen, (n, _o) => { - if (!n) { - aselect.value?.$el?.querySelector('input')?.blur() - } else { - aselect.value?.$el?.querySelector('input')?.focus() + if (editAllowed.value) { + if (!n) { + aselect.value?.$el?.querySelector('input')?.blur() + } else { + aselect.value?.$el?.querySelector('input')?.focus() + } } }) @@ -167,7 +176,7 @@ useSelectedCellKeyupListener(active, (e) => { isOpen.value = false break case 'Enter': - if (active.value && !isOpen.value) { + if (editAllowed.value && active.value && !isOpen.value) { isOpen.value = true } break @@ -179,6 +188,10 @@ useSelectedCellKeyupListener(active, (e) => { // skip break default: + if (!editAllowed.value) { + e.preventDefault() + break + } // toggle only if char key pressed if (!(e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) && e.key?.length === 1) { e.stopPropagation() @@ -272,14 +285,14 @@ const onTagClick = (e: Event, onClose: Function) => { :bordered="false" clear-icon show-search - :show-arrow="!readOnly" + :show-arrow="hasEditRoles && !readOnly && (editable || (active && vModel.length === 0))" :open="isOpen && (active || editable)" :disabled="readOnly" - :class="{ '!ml-[-8px]': readOnly }" + :class="{ '!ml-[-8px]': readOnly, 'caret-transparent': !hasEditRoles }" :dropdown-class-name="`nc-dropdown-multi-select-cell ${isOpen ? 'active' : ''}`" @search="search" @keydown.stop - @click="isOpen = (active || editable) && !isOpen" + @click="isOpen = editAllowed && !isOpen" > { - +
@@ -318,7 +335,7 @@ const onTagClick = (e: Event, onClose: Function) => { class="rounded-tag nc-selected-option" :style="{ display: 'flex', alignItems: 'center' }" :color="options.find((el) => el.title === val)?.color" - :closable="(active || editable) && (vModel.length > 1 || !column?.rqd)" + :closable="editAllowed && (active || editable) && (vModel.length > 1 || !column?.rqd)" :close-icon="h(MdiCloseCircle, { class: ['ms-close-icon'] })" @click="onTagClick($event, onClose)" @close="onClose" diff --git a/packages/nc-gui/components/cell/Percent.vue b/packages/nc-gui/components/cell/Percent.vue index c8907656d2..bea2acd71b 100644 --- a/packages/nc-gui/components/cell/Percent.vue +++ b/packages/nc-gui/components/cell/Percent.vue @@ -12,7 +12,18 @@ const emits = defineEmits(['update:modelValue']) const editEnabled = inject(EditModeInj) -const vModel = useVModel(props, 'modelValue', emits) +const _vModel = useVModel(props, 'modelValue', emits) + +const vModel = computed({ + get: () => _vModel.value, + set: (value) => { + if (value === '') { + _vModel.value = null + } else { + _vModel.value = value + } + }, +}) const focus: VNodeRef = (el) => { ;(el as HTMLInputElement)?.focus() diff --git a/packages/nc-gui/components/cell/SingleSelect.vue b/packages/nc-gui/components/cell/SingleSelect.vue index 5258ffe327..521b1e3657 100644 --- a/packages/nc-gui/components/cell/SingleSelect.vue +++ b/packages/nc-gui/components/cell/SingleSelect.vue @@ -14,6 +14,7 @@ import { extractSdkResponseErrorMsg, inject, ref, + useRoles, useSelectedCellKeyupListener, watch, } from '#imports' @@ -49,6 +50,8 @@ const searchVal = ref() const { getMeta } = useMetas() +const { hasRole } = useRoles() + const { isPg, isMysql } = useProject() // a variable to keep newly created option value @@ -73,6 +76,10 @@ 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 editAllowed = computed(() => hasEditRoles.value && (active.value || editable.value)) + const vModel = computed({ get: () => tempSelectedOptState.value ?? modelValue, set: (val) => { @@ -87,10 +94,12 @@ const vModel = computed({ }) watch(isOpen, (n, _o) => { - if (!n) { - aselect.value?.$el?.querySelector('input')?.blur() - } else { - aselect.value?.$el?.querySelector('input')?.focus() + if (editAllowed.value) { + if (!n) { + aselect.value?.$el?.querySelector('input')?.blur() + } else { + aselect.value?.$el?.querySelector('input')?.focus() + } } }) @@ -100,11 +109,15 @@ useSelectedCellKeyupListener(active, (e) => { isOpen.value = false break case 'Enter': - if (active.value && !isOpen.value) { + if (editAllowed.value && active.value && !isOpen.value) { isOpen.value = true } break default: + if (!editAllowed.value) { + e.preventDefault() + break + } // toggle only if char key pressed if (!(e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) && e.key?.length === 1) { e.stopPropagation() @@ -174,7 +187,7 @@ const toggleMenu = (e: Event) => { vModel.value = '' return } - isOpen.value = (active.value || editable.value) && !isOpen.value + isOpen.value = editAllowed.value && !isOpen.value } @@ -183,11 +196,12 @@ const toggleMenu = (e: Event) => { ref="aselect" v-model:value="vModel" class="w-full" - :allow-clear="!column.rqd && active" + :class="{ 'caret-transparent': !hasEditRoles }" + :allow-clear="!column.rqd && editAllowed" :bordered="false" - :open="isOpen && (active || editable)" + :open="isOpen" :disabled="readOnly" - :show-arrow="!readOnly && (active || editable || vModel === null)" + :show-arrow="hasEditRoles && !readOnly && (editable || (active && vModel === null))" :dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen ? 'active' : ''}`" show-search @select="isOpen = false" @@ -216,8 +230,11 @@ const toggleMenu = (e: Event) => { - - +
diff --git a/packages/nc-gui/components/dashboard/settings/Modal.vue b/packages/nc-gui/components/dashboard/settings/Modal.vue index 5f563dc0b9..a67233cabd 100644 --- a/packages/nc-gui/components/dashboard/settings/Modal.vue +++ b/packages/nc-gui/components/dashboard/settings/Modal.vue @@ -1,10 +1,8 @@ diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts b/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts index fa34f2188a..faf59c603f 100644 --- a/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts +++ b/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts @@ -246,7 +246,7 @@ export default async ( count: UITypes.Count, lookup: UITypes.Lookup, autoNumber: UITypes.AutoNumber, - barcode: UITypes.Barcode, + barcode: UITypes.SingleLineText, button: UITypes.Button, }; @@ -1401,10 +1401,6 @@ export default async ( } else rec[key] = `${value?.name} <${value?.email}>`; break; - case UITypes.Barcode: - rec[key] = value.text; - break; - case UITypes.Button: rec[key] = `${value?.label} <${value?.url}>`; break; @@ -1473,6 +1469,13 @@ export default async ( } break; + case UITypes.SingleLineText: + // Barcode data + if (value?.text) { + rec[key] = value.text; + } + break; + default: break; } diff --git a/packages/nocodb/src/lib/meta/helpers/webhookHelpers.ts b/packages/nocodb/src/lib/meta/helpers/webhookHelpers.ts index 697115bcec..c41997e5c3 100644 --- a/packages/nocodb/src/lib/meta/helpers/webhookHelpers.ts +++ b/packages/nocodb/src/lib/meta/helpers/webhookHelpers.ts @@ -66,12 +66,38 @@ export async function validateCondition(filters: Filter[], data: any) { data[field] === undefined ); break; + case 'checked': + res = !!data[field]; + break; + case 'notchecked': + res = !data[field]; + break; case 'null': res = res = data[field] === null; break; case 'notnull': res = data[field] !== null; break; + case 'allof': + res = (filter.value?.split(',').map((item) => item.trim()) ?? []).every((item) => + (data[field]?.split(',') ?? []).includes(item) + ); + break; + case 'anyof': + res = (filter.value?.split(',').map((item) => item.trim()) ?? []).some((item) => + (data[field]?.split(',') ?? []).includes(item) + ); + break; + case 'nallof': + res = !(filter.value?.split(',').map((item) => item.trim()) ?? []).every((item) => + (data[field]?.split(',') ?? []).includes(item) + ); + break; + case 'nanyof': + res = !(filter.value?.split(',').map((item) => item.trim()) ?? []).some((item) => + (data[field]?.split(',') ?? []).includes(item) + ); + break; case 'lt': res = +data[field] < +filter.value; break; diff --git a/packages/nocodb/src/lib/models/Filter.ts b/packages/nocodb/src/lib/models/Filter.ts index 34ff8a2f50..d6683d82f1 100644 --- a/packages/nocodb/src/lib/models/Filter.ts +++ b/packages/nocodb/src/lib/models/Filter.ts @@ -1,6 +1,7 @@ import Noco from '../Noco'; import Model from './Model'; import Column from './Column'; +import Hook from './Hook'; import { CacheDelDirection, CacheGetType, @@ -10,6 +11,7 @@ import { import View from './View'; import { FilterType, UITypes } from 'nocodb-sdk'; import NocoCache from '../cache/NocoCache'; +import { NcError } from '../meta/helpers/catchError'; export default class Filter { id: string; @@ -30,6 +32,12 @@ export default class Filter { | 'notempty' | 'null' | 'notnull' + | 'checked' + | 'notchecked' + | 'allof' + | 'anyof' + | 'nallof' + | 'nanyof' | 'gt' | 'lt' | 'gte' @@ -90,7 +98,16 @@ export default class Filter { }), }; if (!(filter.project_id && filter.base_id)) { - const model = await Column.get({ colId: filter.fk_column_id }, ncMeta); + let model: { project_id?: string; base_id?: string }; + if (filter.fk_view_id) { + model = await View.get(filter.fk_view_id, ncMeta); + } else if (filter.fk_hook_id) { + model = await Hook.get(filter.fk_hook_id, ncMeta); + } else if (filter.fk_column_id) { + model = await Column.get({ colId: filter.fk_column_id }, ncMeta); + } else { + NcError.badRequest('Invalid filter'); + } insertObj.project_id = model.project_id; insertObj.base_id = model.base_id; } diff --git a/packages/nocodb/src/lib/models/Model.ts b/packages/nocodb/src/lib/models/Model.ts index a8fe688707..03f844d596 100644 --- a/packages/nocodb/src/lib/models/Model.ts +++ b/packages/nocodb/src/lib/models/Model.ts @@ -392,11 +392,16 @@ export default class Model implements TableType { } if (force) { - const leftOverColumns = await ncMeta.metaList2(null, null, MetaTable.COL_RELATIONS, { - condition: { - fk_related_model_id: this.id, - }, - }); + const leftOverColumns = await ncMeta.metaList2( + null, + null, + MetaTable.COL_RELATIONS, + { + condition: { + fk_related_model_id: this.id, + }, + } + ); for (const col of leftOverColumns) { await NocoCache.deepDel( @@ -410,7 +415,7 @@ export default class Model implements TableType { fk_related_model_id: this.id, }); } - + await NocoCache.deepDel( CacheScope.COLUMN, `${CacheScope.COLUMN}:${this.id}`, diff --git a/packages/nocodb/src/lib/utils/projectAcl.ts b/packages/nocodb/src/lib/utils/projectAcl.ts index b9c1317313..2ca914d7d6 100644 --- a/packages/nocodb/src/lib/utils/projectAcl.ts +++ b/packages/nocodb/src/lib/utils/projectAcl.ts @@ -17,6 +17,7 @@ export default { pluginRead: true, pluginUpdate: true, isPluginActive: true, + projectDelete: true, }, }, guest: {}, diff --git a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts index a51d04910b..9f7ae6d07f 100644 --- a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts +++ b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts @@ -8,6 +8,7 @@ import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045'; import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000'; import ncDataTypesUpgrader from './ncDataTypesUpgrader'; import ncProjectRolesUpgrader from './ncProjectRolesUpgrader'; +import ncFilterUpgrader from './ncFilterUpgrader'; const log = debug('nc:version-upgrader'); import boxen from 'boxen'; @@ -35,6 +36,7 @@ export default class NcUpgrader { { name: '0090000', handler: ncProjectUpgraderV2_0090000 }, { name: '0098004', handler: ncDataTypesUpgrader }, { name: '0098005', handler: ncProjectRolesUpgrader }, + { name: '0100002', handler: ncFilterUpgrader }, ]; if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) { return; diff --git a/packages/nocodb/src/lib/version-upgrader/ncFilterUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/ncFilterUpgrader.ts new file mode 100644 index 0000000000..6c250bd04c --- /dev/null +++ b/packages/nocodb/src/lib/version-upgrader/ncFilterUpgrader.ts @@ -0,0 +1,40 @@ +import { NcUpgraderCtx } from './NcUpgrader'; +import { MetaTable } from '../utils/globals'; +import View from '../models/View'; +import Hook from '../models/Hook'; +import Column from '../models/Column'; + +// before 0.101.0, an incorrect project_id was inserted when +// a filter is created without specifying the column +// this upgrader is to retrieve the correct project id from either view, hook, or column +// and update the project id +export default async function ({ ncMeta }: NcUpgraderCtx) { + const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); + for (const filter of filters) { + let model: { project_id?: string; base_id?: string }; + if (filter.fk_view_id) { + model = await View.get(filter.fk_view_id, ncMeta); + } else if (filter.fk_hook_id) { + model = await Hook.get(filter.fk_hook_id, ncMeta); + } else if (filter.fk_column_id) { + model = await Column.get({ colId: filter.fk_column_id }, ncMeta); + } else { + continue; + } + + // skip if related model is not found + if (!model) { + continue; + } + + if (filter.project_id !== model.project_id) { + await ncMeta.metaUpdate( + null, + null, + MetaTable.FILTER_EXP, + { base_id: model.base_id, project_id: model.project_id }, + filter.id + ); + } + } +} diff --git a/scripts/sdk/swagger.json b/scripts/sdk/swagger.json index fd0816a2ee..62fb7a6df3 100644 --- a/scripts/sdk/swagger.json +++ b/scripts/sdk/swagger.json @@ -1972,8 +1972,7 @@ "project_id": { "type": "string" }, - "meta": { - } + "meta": {} } } } @@ -2105,7 +2104,7 @@ ] }, "delete": { - "summary": "", + "summary": "Column Delete", "operationId": "db-table-column-delete", "responses": { "200": { @@ -2115,6 +2114,18 @@ "tags": [ "DB table column" ] + }, + "get": { + "summary": "Column Get", + "operationId": "db-table-column-get", + "responses": { + "200": { + "description": "OK" + } + }, + "tags": [ + "DB Table Column" + ] } }, "/api/v1/db/meta/columns/{columnId}/primary": { @@ -2194,8 +2205,7 @@ "order": { "type": "number" }, - "meta": { - }, + "meta": {}, "title": { "type": "string" }, @@ -3877,6 +3887,13 @@ "in": "query", "name": "nested", "description": "Query params for nested data" + }, + { + "schema": { + "type": "number" + }, + "in": "query", + "name": "offset" } ], "responses": { @@ -5529,6 +5546,9 @@ "client": { "type": "string" }, + "base_id": { + "type": "string" + }, "columns": { "allOf": [ { @@ -7489,8 +7509,7 @@ "number" ] }, - "meta": { - } + "meta": {} }, "required": [ "table_name", @@ -7588,8 +7607,7 @@ "uuid": { "type": "string" }, - "meta": { - }, + "meta": {}, "show_system_fields": { "type": "boolean" }, @@ -7832,8 +7850,7 @@ "$ref": "#/components/schemas/Column" } }, - "meta": { - } + "meta": {} }, "required": [ "table_name", @@ -10039,4 +10056,4 @@ } } } -} +} \ No newline at end of file diff --git a/tests/playwright/package-lock.json b/tests/playwright/package-lock.json index 6ee489570a..048933e0cf 100644 --- a/tests/playwright/package-lock.json +++ b/tests/playwright/package-lock.json @@ -14,7 +14,7 @@ "xlsx": "^0.18.5" }, "devDependencies": { - "@playwright/test": "^1.27.1", + "@playwright/test": "1.27.1", "@typescript-eslint/eslint-plugin": "^4.0.1", "@typescript-eslint/parser": "^4.0.1", "axios": "^0.24.0", @@ -2726,9 +2726,9 @@ "dev": true }, "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -6911,9 +6911,9 @@ "dev": true }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" diff --git a/tests/playwright/pages/Dashboard/Settings/AppStore.ts b/tests/playwright/pages/Account/AppStore.ts similarity index 63% rename from tests/playwright/pages/Dashboard/Settings/AppStore.ts rename to tests/playwright/pages/Account/AppStore.ts index 13a27088fd..c617df54a5 100644 --- a/tests/playwright/pages/Dashboard/Settings/AppStore.ts +++ b/tests/playwright/pages/Account/AppStore.ts @@ -1,21 +1,31 @@ import { expect } from '@playwright/test'; -import { SettingsPage } from '.'; -import BasePage from '../../Base'; +import BasePage from '../Base'; +import { AccountPage } from './index'; -export class AppStoreSettingsPage extends BasePage { - private readonly settings: SettingsPage; +export class AccountAppStorePage extends BasePage { + private accountPage: AccountPage; - constructor(settings: SettingsPage) { - super(settings.rootPage); - this.settings = settings; + constructor(accountPage: AccountPage) { + super(accountPage.rootPage); + this.accountPage = accountPage; + } + + async goto() { + await this.rootPage.goto('/#/account/apps', { waitUntil: 'networkidle' }); + } + + async waitUntilContentLoads() { + return this.rootPage.waitForResponse( + resp => resp.url().includes('api/v1/db/meta/plugins') && resp.status() === 200 + ); } get() { - return this.settings.get().locator(`[data-testid="nc-settings-subtab-appStore"]`); + return this.accountPage.get().locator(`[data-testid="nc-settings-subtab-appStore"]`); } async install({ name }: { name: string }) { - const card = await this.settings.get().locator(`.nc-app-store-card-${name}`); + const card = await this.accountPage.get().locator(`.nc-app-store-card-${name}`); await card.click(); // todo: Hack to solve the issue when if the test installing a plugin fails, the next test will fail because the plugin is already installed @@ -48,7 +58,7 @@ export class AppStoreSettingsPage extends BasePage { } async uninstall(param: { name: string }) { - const card = this.settings.get().locator(`.nc-app-store-card-${param.name}`); + const card = this.accountPage.get().locator(`.nc-app-store-card-${param.name}`); // await card.scrollIntoViewIfNeeded(); await card.click(); diff --git a/tests/playwright/pages/Account/index.ts b/tests/playwright/pages/Account/index.ts index 34c321160d..504bae9823 100644 --- a/tests/playwright/pages/Account/index.ts +++ b/tests/playwright/pages/Account/index.ts @@ -3,17 +3,20 @@ import BasePage from '../Base'; import { AccountSettingsPage } from './Settings'; import { AccountTokenPage } from './Token'; import { AccountUsersPage } from './Users'; +import { AccountAppStorePage } from './AppStore'; export class AccountPage extends BasePage { readonly settings: AccountSettingsPage; readonly token: AccountTokenPage; readonly users: AccountUsersPage; + readonly appStore: AccountAppStorePage; constructor(page: Page) { super(page); this.settings = new AccountSettingsPage(this); this.token = new AccountTokenPage(this); this.users = new AccountUsersPage(this); + this.appStore = new AccountAppStorePage(this); } get() { diff --git a/tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts b/tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts index cffae77689..a05fcd166b 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts +++ b/tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts @@ -1,5 +1,6 @@ import { ColumnPageObject } from '.'; import BasePage from '../../../Base'; +import { expect } from '@playwright/test'; export class SelectOptionColumnPageObject extends BasePage { readonly column: ColumnPageObject; @@ -35,6 +36,18 @@ export class SelectOptionColumnPageObject extends BasePage { if (!skipColumnModal && columnTitle) await this.column.save({ isUpdated: true }); } + // add multiple options at once after column creation is completed + // + async addOptions({ columnTitle, options }: { columnTitle: string; options: string[] }) { + await this.column.openEdit({ title: columnTitle }); + for (let i = 0; i < options.length; i++) { + await this.column.get().locator('button:has-text("Add option")').click(); + await this.column.get().locator(`[data-testid="select-column-option-input-${i}"]`).click(); + await this.column.get().locator(`[data-testid="select-column-option-input-${i}"]`).fill(options[i]); + } + await this.column.save({ isUpdated: true }); + } + async editOption({ columnTitle, index, newOption }: { index: number; columnTitle: string; newOption: string }) { await this.column.openEdit({ title: columnTitle }); @@ -49,6 +62,22 @@ export class SelectOptionColumnPageObject extends BasePage { await this.column.get().locator(`svg[data-testid="select-column-option-remove-${index}"]`).click(); + await expect(this.column.get().getByTestId(`select-column-option-${index}`)).toHaveClass(/removed/); + + await this.column.save({ isUpdated: true }); + } + + async deleteOptionWithUndo({ columnTitle, index }: { index: number; columnTitle: string }) { + await this.column.openEdit({ title: columnTitle }); + + await this.column.get().locator(`svg[data-testid="select-column-option-remove-${index}"]`).click(); + + await expect(this.column.get().getByTestId(`select-column-option-${index}`)).toHaveClass(/removed/); + + await this.column.get().locator(`svg[data-testid="select-column-option-remove-undo-${index}"]`).click(); + + await expect(this.column.get().getByTestId(`select-column-option-${index}`)).not.toHaveClass(/removed/); + await this.column.save({ isUpdated: true }); } diff --git a/tests/playwright/pages/Dashboard/Grid/Column/index.ts b/tests/playwright/pages/Dashboard/Grid/Column/index.ts index 1f1b9ecea7..483e6945a3 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/index.ts +++ b/tests/playwright/pages/Dashboard/Grid/Column/index.ts @@ -77,16 +77,6 @@ export class ColumnPageObject extends BasePage { switch (type) { case 'SingleSelect': case 'MultiSelect': - await this.selectOption.addOption({ - index: 0, - option: 'Option 1', - skipColumnModal: true, - }); - await this.selectOption.addOption({ - index: 1, - option: 'Option 2', - skipColumnModal: true, - }); break; case 'Duration': if (format) { @@ -279,7 +269,7 @@ export class ColumnPageObject extends BasePage { timeFormat?: string; }) { await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').click(); - await this.rootPage.locator('li[role="menuitem"]:has-text("Edit")').click(); + await this.rootPage.locator('li[role="menuitem"]:has-text("Edit")').last().click(); await this.get().waitFor({ state: 'visible' }); diff --git a/tests/playwright/pages/Dashboard/Settings/index.ts b/tests/playwright/pages/Dashboard/Settings/index.ts index c46f55396f..448f78550d 100644 --- a/tests/playwright/pages/Dashboard/Settings/index.ts +++ b/tests/playwright/pages/Dashboard/Settings/index.ts @@ -1,14 +1,12 @@ import { DashboardPage } from '..'; import BasePage from '../../Base'; import { AuditSettingsPage } from './Audit'; -import { AppStoreSettingsPage } from './AppStore'; import { MiscSettingsPage } from './Miscellaneous'; import { TeamsPage } from './Teams'; import { DataSourcesPage } from './DataSources'; export enum SettingTab { TeamAuth = 'teamAndAuth', - AppStore = 'appStore', DataSources = 'dataSources', Audit = 'audit', ProjectSettings = 'projectSettings', @@ -22,7 +20,6 @@ export enum SettingsSubTab { export class SettingsPage extends BasePage { readonly audit: AuditSettingsPage; - readonly appStore: AppStoreSettingsPage; readonly miscellaneous: MiscSettingsPage; readonly dataSources: DataSourcesPage; readonly teams: TeamsPage; @@ -30,7 +27,6 @@ export class SettingsPage extends BasePage { constructor(dashboard: DashboardPage) { super(dashboard.rootPage); this.audit = new AuditSettingsPage(this); - this.appStore = new AppStoreSettingsPage(this); this.miscellaneous = new MiscSettingsPage(this); this.dataSources = new DataSourcesPage(this); this.teams = new TeamsPage(this); diff --git a/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts b/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts index 6c7a8b70c4..2b8f4ea71b 100644 --- a/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts +++ b/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts @@ -28,8 +28,7 @@ export class ToolbarFilterPage extends BasePage { ).toBeChecked(); } - // Todo: Handle the case of operator does not need a value - async addNew({ + async add({ columnTitle, opType, value, @@ -37,7 +36,7 @@ export class ToolbarFilterPage extends BasePage { }: { columnTitle: string; opType: string; - value: string; + value?: string; isLocallySaved: boolean; }) { await this.get().locator(`button:has-text("Add Filter")`).first().click(); @@ -70,17 +69,20 @@ export class ToolbarFilterPage extends BasePage { await this.toolbar.parent.dashboard.waitForLoaderToDisappear(); } - const fillFilter = this.rootPage.locator('.nc-filter-value-select').last().fill(value); - await this.waitForResponse({ - uiAction: fillFilter, - httpMethodsToMatch: ['GET'], - requestUrlPathToMatch: isLocallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`, - }); - await this.toolbar.parent.dashboard.waitForLoaderToDisappear(); - await this.toolbar.parent.waitLoading(); + // if value field was provided, fill it + if (value) { + const fillFilter = this.rootPage.locator('.nc-filter-value-select').last().fill(value); + await this.waitForResponse({ + uiAction: fillFilter, + httpMethodsToMatch: ['GET'], + requestUrlPathToMatch: isLocallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`, + }); + await this.toolbar.parent.dashboard.waitForLoaderToDisappear(); + await this.toolbar.parent.waitLoading(); + } } - async resetFilter() { + async reset() { await this.toolbar.clickFilter(); await this.waitForResponse({ uiAction: this.get().locator('.nc-filter-item-remove-btn').click(), diff --git a/tests/playwright/pages/Dashboard/common/Toolbar/Sort.ts b/tests/playwright/pages/Dashboard/common/Toolbar/Sort.ts index 67e6046e85..17281cd4d2 100644 --- a/tests/playwright/pages/Dashboard/common/Toolbar/Sort.ts +++ b/tests/playwright/pages/Dashboard/common/Toolbar/Sort.ts @@ -21,7 +21,7 @@ export class ToolbarSortPage extends BasePage { ).toHaveText(direction); } - async addSort({ + async add({ columnTitle, isAscending, isLocallySaved, @@ -67,7 +67,7 @@ export class ToolbarSortPage extends BasePage { } // todo: remove this opening sort menu logic - async resetSort() { + async reset() { // open sort menu await this.toolbar.clickSort(); diff --git a/tests/playwright/tests/columnCheckbox.spec.ts b/tests/playwright/tests/columnCheckbox.spec.ts new file mode 100644 index 0000000000..b36af016d2 --- /dev/null +++ b/tests/playwright/tests/columnCheckbox.spec.ts @@ -0,0 +1,129 @@ +import { test } from '@playwright/test'; +import { DashboardPage } from '../pages/Dashboard'; +import setup from '../setup'; +import { ToolbarPage } from '../pages/Dashboard/common/Toolbar'; +import { isPg } from '../setup/db'; + +test.describe('Checkbox - cell, filter, sort', () => { + let dashboard: DashboardPage, toolbar: ToolbarPage; + let context: any; + + // define validateRowArray function + async function validateRowArray(value: string[]) { + const length = value.length; + for (let i = 0; i < length; i++) { + await dashboard.grid.cell.verify({ + index: i, + columnHeader: 'Title', + value: value[i], + }); + } + } + + async function verifyFilter(param: { opType: string; value?: string; result: string[] }) { + await toolbar.clickFilter(); + await toolbar.filter.add({ + columnTitle: 'checkbox', + opType: param.opType, + value: param.value, + isLocallySaved: false, + }); + await toolbar.clickFilter(); + + // verify filtered rows + await validateRowArray(param.result); + // Reset filter + await toolbar.filter.reset(); + } + + test.beforeEach(async ({ page }) => { + context = await setup({ page }); + dashboard = new DashboardPage(page, context.project); + toolbar = dashboard.grid.toolbar; + }); + + test('Checkbox', async () => { + // close 'Team & Auth' tab + await dashboard.closeTab({ title: 'Team & Auth' }); + + await dashboard.treeView.createTable({ title: 'Sheet1' }); + + await dashboard.grid.addNewRow({ index: 0, value: '1a' }); + await dashboard.grid.addNewRow({ index: 1, value: '1b' }); + await dashboard.grid.addNewRow({ index: 2, value: '1c' }); + await dashboard.grid.addNewRow({ index: 3, value: '1d' }); + await dashboard.grid.addNewRow({ index: 4, value: '1e' }); + await dashboard.grid.addNewRow({ index: 5, value: '1f' }); + + // Create Checkbox column + await dashboard.grid.column.create({ + title: 'checkbox', + type: 'Checkbox', + }); + + // In cell insert + await dashboard.grid.cell.checkbox.click({ index: 0, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.click({ index: 1, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.click({ index: 2, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.click({ index: 5, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.click({ index: 1, columnHeader: 'checkbox' }); + + // verify checkbox state + await dashboard.grid.cell.checkbox.verifyChecked({ index: 0, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.verifyChecked({ index: 2, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.verifyChecked({ index: 5, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.verifyUnchecked({ index: 1, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.verifyUnchecked({ index: 3, columnHeader: 'checkbox' }); + await dashboard.grid.cell.checkbox.verifyUnchecked({ index: 4, columnHeader: 'checkbox' }); + + // column values + // 1a : true + // 1b : false + // 1c : true + // 1d : null + // 1e : null + // 1f : true + + // Filter column + await verifyFilter({ opType: 'is checked', result: ['1a', '1c', '1f'] }); + await verifyFilter({ opType: 'is not checked', result: ['1b', '1d', '1e'] }); + await verifyFilter({ opType: 'is equal', value: '0', result: ['1b'] }); + await verifyFilter({ opType: 'is not equal', value: '1', result: ['1b', '1d', '1e'] }); + await verifyFilter({ opType: 'is null', result: ['1d', '1e'] }); + await verifyFilter({ opType: 'is not null', result: ['1a', '1b', '1c', '1f'] }); + + // Sort column + await toolbar.sort.add({ + columnTitle: 'checkbox', + isAscending: true, + isLocallySaved: false, + }); + if (isPg(context)) { + await validateRowArray(['1b', '1a', '1c', '1f', '1d', '1e']); + } else { + await validateRowArray(['1d', '1e', '1b', '1a', '1c', '1f']); + } + await toolbar.sort.reset(); + + // sort descending & validate + await toolbar.sort.add({ + columnTitle: 'checkbox', + isAscending: false, + isLocallySaved: false, + }); + if (isPg(context)) { + await validateRowArray(['1d', '1e', '1a', '1c', '1f', '1b']); + } else { + await validateRowArray(['1a', '1c', '1f', '1b', '1d', '1e']); + } + await toolbar.sort.reset(); + + // wait for 10 seconds + await dashboard.rootPage.waitForTimeout(10000); + + // TBD: Add more tests + // Expanded form insert + // Expanded record insert + // Expanded form insert + }); +}); diff --git a/tests/playwright/tests/columnFormula.spec.ts b/tests/playwright/tests/columnFormula.spec.ts index bc08da00ba..8fda28aae3 100644 --- a/tests/playwright/tests/columnFormula.spec.ts +++ b/tests/playwright/tests/columnFormula.spec.ts @@ -38,6 +38,10 @@ const formulaDataByDbType = (context: NcContext) => [ formula: `DATETIME_DIFF("2022/10/14", "2022/10/15", "minutes")`, result: ['-1440', '-1440', '-1440', '-1440', '-1440'], }, + { + formula: `DATETIME_DIFF("2023/10/14", "2023/01/13", "minutes")`, + result: ['394560', '394560', '394560', '394560', '394560'], + }, { formula: `DATETIME_DIFF("2022/10/14", "2022/10/15", "seconds")`, result: ['-86400', '-86400', '-86400', '-86400', '-86400'], @@ -66,10 +70,34 @@ const formulaDataByDbType = (context: NcContext) => [ formula: `DATETIME_DIFF("2022/10/14", "2023/10/14", "y")`, result: ['-1', '-1', '-1', '-1', '-1'], }, + { + formula: `DATETIME_DIFF("2023/01/12", "2023/10/14", "y")`, + result: ['0', '0', '0', '0', '0'], + }, + { + formula: `DATETIME_DIFF("2023/10/14", "2023/01/12", "y")`, + result: ['0', '0', '0', '0', '0'], + }, + { + formula: `DATETIME_DIFF("2023-01-12", "2021-08-29", "y")`, + result: ['1', '1', '1', '1', '1'], + }, + { + formula: `DATETIME_DIFF("2021-01-12", "2026-01-29", "y")`, + result: ['-5', '-5', '-5', '-5', '-5'], + }, + { + formula: `DATETIME_DIFF("1990-01-12", "2046-12-29", "y")`, + result: ['-56', '-56', '-56', '-56', '-56'], + }, { formula: `DATETIME_DIFF("2022/10/14", "2023/10/14", "d")`, result: ['-365', '-365', '-365', '-365', '-365'], }, + { + formula: `DATETIME_DIFF("2022/10/14", "2023/01/12", "d")`, + result: ['-90', '-90', '-90', '-90', '-90'], + }, { formula: `CONCAT(UPPER({City}), LOWER({City}), TRIM(' trimmed '))`, result: [ diff --git a/tests/playwright/tests/columnMultiSelect.spec.ts b/tests/playwright/tests/columnMultiSelect.spec.ts index 5eff82acfa..4314196175 100644 --- a/tests/playwright/tests/columnMultiSelect.spec.ts +++ b/tests/playwright/tests/columnMultiSelect.spec.ts @@ -15,6 +15,10 @@ test.describe('Multi select', () => { await dashboard.treeView.createTable({ title: 'sheet1' }); await grid.column.create({ title: 'MultiSelect', type: 'MultiSelect' }); + await grid.column.selectOption.addOptions({ + columnTitle: 'MultiSelect', + options: ['Option 1', 'Option 2'], + }); await grid.addNewRow({ index: 0, value: 'Row 0' }); }); diff --git a/tests/playwright/tests/columnSingleSelect.spec.ts b/tests/playwright/tests/columnSingleSelect.spec.ts index 2b2d737889..03710b9075 100644 --- a/tests/playwright/tests/columnSingleSelect.spec.ts +++ b/tests/playwright/tests/columnSingleSelect.spec.ts @@ -15,6 +15,10 @@ test.describe('Single select', () => { await dashboard.treeView.createTable({ title: 'sheet1' }); await grid.column.create({ title: 'SingleSelect', type: 'SingleSelect' }); + await grid.column.selectOption.addOptions({ + columnTitle: 'SingleSelect', + options: ['Option 1', 'Option 2'], + }); await grid.addNewRow({ index: 0, value: 'Row 0' }); }); @@ -53,6 +57,13 @@ test.describe('Single select', () => { await grid.cell.selectOption.select({ index: 0, columnHeader: 'SingleSelect', option: 'Option 3' }); await grid.cell.selectOption.verify({ index: 0, columnHeader: 'SingleSelect', option: 'Option 3' }); + await grid.column.selectOption.deleteOptionWithUndo({ index: 0, columnTitle: 'SingleSelect' }); + await grid.cell.selectOption.verifyOptions({ + index: 0, + columnHeader: 'SingleSelect', + options: ['Option 1', 'Option 2', 'Option 3'], + }); + await grid.column.selectOption.deleteOption({ index: 2, columnTitle: 'SingleSelect' }); await grid.cell.selectOption.verifyNoOptionsSelected({ index: 0, columnHeader: 'SingleSelect' }); diff --git a/tests/playwright/tests/keyboardShortcuts.spec.ts b/tests/playwright/tests/keyboardShortcuts.spec.ts index d9de119030..31e5766f5e 100644 --- a/tests/playwright/tests/keyboardShortcuts.spec.ts +++ b/tests/playwright/tests/keyboardShortcuts.spec.ts @@ -156,10 +156,18 @@ test.describe('Verify shortcuts', () => { title: 'SingleSelect', type: 'SingleSelect', }); + await dashboard.grid.column.selectOption.addOptions({ + columnTitle: 'SingleSelect', + options: ['Option 1', 'Option 2'], + }); await dashboard.grid.column.create({ title: 'MultiSelect', type: 'MultiSelect', }); + await dashboard.grid.column.selectOption.addOptions({ + columnTitle: 'MultiSelect', + options: ['Option 1', 'Option 2'], + }); await dashboard.grid.column.create({ title: 'Checkbox', type: 'Checkbox', diff --git a/tests/playwright/tests/metaSync.spec.ts b/tests/playwright/tests/metaSync.spec.ts index 2f50fd90bb..2011508ca5 100644 --- a/tests/playwright/tests/metaSync.spec.ts +++ b/tests/playwright/tests/metaSync.spec.ts @@ -255,14 +255,14 @@ test.describe('Meta sync', () => { await dashboard.grid.toolbar.fields.click({ title: 'Col1' }); await dashboard.grid.toolbar.clickFields(); - await dashboard.grid.toolbar.sort.addSort({ + await dashboard.grid.toolbar.sort.add({ columnTitle: 'Col1', isAscending: false, isLocallySaved: false, }); await dashboard.grid.toolbar.clickFilter(); - await dashboard.grid.toolbar.filter.addNew({ + await dashboard.grid.toolbar.filter.add({ columnTitle: 'Col1', opType: '>=', value: '5', diff --git a/tests/playwright/tests/toolbarOperations.spec.ts b/tests/playwright/tests/toolbarOperations.spec.ts index d5aab70b6f..acf7064322 100644 --- a/tests/playwright/tests/toolbarOperations.spec.ts +++ b/tests/playwright/tests/toolbarOperations.spec.ts @@ -48,16 +48,16 @@ test.describe('Toolbar operations (GRID)', () => { await validateFirstRow('Afghanistan'); // Sort column - await toolbar.sort.addSort({ columnTitle: 'Country', isAscending: false, isLocallySaved: false }); + await toolbar.sort.add({ columnTitle: 'Country', isAscending: false, isLocallySaved: false }); await validateFirstRow('Zambia'); // reset sort - await toolbar.sort.resetSort(); + await toolbar.sort.reset(); await validateFirstRow('Afghanistan'); // Filter column await toolbar.clickFilter(); - await toolbar.filter.addNew({ + await toolbar.filter.add({ columnTitle: 'Country', value: 'India', opType: 'is equal', @@ -68,7 +68,7 @@ test.describe('Toolbar operations (GRID)', () => { await validateFirstRow('India'); // Reset filter - await toolbar.filter.resetFilter(); + await toolbar.filter.reset(); await validateFirstRow('Afghanistan'); await dashboard.closeTab({ title: 'Country' }); diff --git a/tests/playwright/tests/viewForm.spec.ts b/tests/playwright/tests/viewForm.spec.ts index 734f73bfc0..1685f7c886 100644 --- a/tests/playwright/tests/viewForm.spec.ts +++ b/tests/playwright/tests/viewForm.spec.ts @@ -1,20 +1,25 @@ import { test } from '@playwright/test'; import { DashboardPage } from '../pages/Dashboard'; -import { SettingTab } from '../pages/Dashboard/Settings'; import setup from '../setup'; import { FormPage } from '../pages/Dashboard/Form'; import { SharedFormPage } from '../pages/SharedForm'; +import { AccountPage } from '../pages/Account'; +import { AccountAppStorePage } from '../pages/Account/AppStore'; // todo: Move most of the ui actions to page object and await on the api response test.describe('Form view', () => { let dashboard: DashboardPage; let form: FormPage; + let accountAppStorePage: AccountAppStorePage; + let accountPage: AccountPage; let context: any; test.beforeEach(async ({ page }) => { context = await setup({ page }); dashboard = new DashboardPage(page, context.project); form = dashboard.form; + accountPage = new AccountPage(page); + accountAppStorePage = accountPage.appStore; }); test('Field re-order operations', async () => { @@ -78,7 +83,7 @@ test.describe('Form view', () => { }); }); - test('Form elements validation', async () => { + test('Form elements validation', async ({ page }) => { // close 'Team & Auth' tab await dashboard.closeTab({ title: 'Team & Auth' }); await dashboard.treeView.openTable({ title: 'Country' }); @@ -168,12 +173,16 @@ test.describe('Form view', () => { await dashboard.verifyToast({ message: 'Please activate SMTP plugin in App store for enabling email notification', }); + const url = dashboard.rootPage.url(); // activate SMTP plugin - await dashboard.gotoSettings(); - await dashboard.settings.selectTab({ tab: SettingTab.AppStore }); - await dashboard.settings.appStore.install({ name: 'SMTP' }); - await dashboard.settings.appStore.configureSMTP({ + await accountAppStorePage.goto(); + await accountAppStorePage.rootPage.reload({ waitUntil: 'networkidle' }); + await accountAppStorePage.waitUntilContentLoads(); + + // install SMTP + await accountAppStorePage.install({ name: 'SMTP' }); + await accountAppStorePage.configureSMTP({ email: 'a@b.com', host: 'smtp.gmail.com', port: '587', @@ -181,7 +190,9 @@ test.describe('Form view', () => { await dashboard.verifyToast({ message: 'Successfully installed and email notification will use SMTP configuration', }); - await dashboard.settings.close(); + + // revisit form view + await page.goto(url); // enable 'email-me' option await dashboard.viewSidebar.openView({ title: 'CountryForm' }); @@ -192,15 +203,15 @@ test.describe('Form view', () => { showBlankForm: false, }); - // reset SMTP - await dashboard.gotoSettings(); - await dashboard.settings.selectTab({ tab: SettingTab.AppStore }); - await dashboard.settings.appStore.uninstall({ name: 'SMTP' }); + // Uninstall SMTP + await accountAppStorePage.goto(); + await accountAppStorePage.rootPage.reload({ waitUntil: 'networkidle' }); + await accountAppStorePage.waitUntilContentLoads(); + await accountAppStorePage.uninstall({ name: 'SMTP' }); await dashboard.verifyToast({ message: 'Plugin uninstalled successfully', }); - await dashboard.settings.close(); }); test('Form share, verify attachment file', async () => { diff --git a/tests/playwright/tests/viewGridShare.spec.ts b/tests/playwright/tests/viewGridShare.spec.ts index 6e6f82baa5..76e862b93d 100644 --- a/tests/playwright/tests/viewGridShare.spec.ts +++ b/tests/playwright/tests/viewGridShare.spec.ts @@ -33,14 +33,14 @@ test.describe('Shared view', () => { // hide column await dashboard.grid.toolbar.fields.toggle({ title: 'Address2' }); // sort - await dashboard.grid.toolbar.sort.addSort({ + await dashboard.grid.toolbar.sort.add({ columnTitle: 'District', isAscending: false, isLocallySaved: false, }); // filter await dashboard.grid.toolbar.clickFilter(); - await dashboard.grid.toolbar.filter.addNew({ + await dashboard.grid.toolbar.filter.add({ columnTitle: 'Address', value: 'Ab', opType: 'is like', @@ -101,7 +101,7 @@ test.describe('Shared view', () => { **/ // create new sort & filter criteria in shared view - await sharedPage.grid.toolbar.sort.addSort({ + await sharedPage.grid.toolbar.sort.add({ columnTitle: 'Address', isAscending: true, isLocallySaved: true, @@ -109,7 +109,7 @@ test.describe('Shared view', () => { if (isMysql(context)) { await sharedPage.grid.toolbar.clickFilter(); - await sharedPage.grid.toolbar.filter.addNew({ + await sharedPage.grid.toolbar.filter.add({ columnTitle: 'District', value: 'Ta', opType: 'is like', @@ -196,7 +196,7 @@ test.describe('Shared view', () => { isVisible: true, }); await sharedPage2.grid.toolbar.clickFilter(); - await sharedPage2.grid.toolbar.filter.addNew({ + await sharedPage2.grid.toolbar.filter.add({ columnTitle: 'Country', value: 'New Country', opType: 'is like', diff --git a/tests/playwright/tests/viewKanban.spec.ts b/tests/playwright/tests/viewKanban.spec.ts index 0fe4ab45f7..1feb5c9acf 100644 --- a/tests/playwright/tests/viewKanban.spec.ts +++ b/tests/playwright/tests/viewKanban.spec.ts @@ -118,7 +118,7 @@ test.describe('View', () => { }); // verify sort - await toolbar.sort.addSort({ + await toolbar.sort.add({ columnTitle: 'Title', isAscending: false, isLocallySaved: false, @@ -133,7 +133,7 @@ test.describe('View', () => { stackIndex: i, order: order2[i - 1], }); - await toolbar.sort.resetSort(); + await toolbar.sort.reset(); // verify card order const order3 = [ ['ACE GOLDFINGER', 'AFFAIR PREJUDICE', 'AFRICAN EGG'], @@ -149,7 +149,7 @@ test.describe('View', () => { await toolbar.clickFilter({ networkValidation: true, }); - await toolbar.filter.addNew({ + await toolbar.filter.add({ columnTitle: 'Title', opType: 'is like', value: 'BA', @@ -167,7 +167,7 @@ test.describe('View', () => { stackIndex: i, order: order4[i - 1], }); - await toolbar.filter.resetFilter(); + await toolbar.filter.reset(); const order5 = [ ['ACE GOLDFINGER', 'AFFAIR PREJUDICE', 'AFRICAN EGG'], ['ACADEMY DINOSAUR', 'AGENT TRUMAN', 'ALASKA PHANTOM'], @@ -192,14 +192,14 @@ test.describe('View', () => { index: 1, }); - await toolbar.sort.addSort({ + await toolbar.sort.add({ columnTitle: 'Title', isAscending: false, isLocallySaved: false, }); await toolbar.clickFilter(); - await toolbar.filter.addNew({ + await toolbar.filter.add({ columnTitle: 'Title', opType: 'is like', value: 'BA', @@ -267,8 +267,8 @@ test.describe('View', () => { await toolbar.fields.toggleShowSystemFields(); await toolbar.fields.toggle({ title: 'LanguageId' }); await toolbar.fields.toggle({ title: 'Title' }); - await toolbar.sort.resetSort(); - await toolbar.filter.resetFilter(); + await toolbar.sort.reset(); + await toolbar.filter.reset(); await kanban.addCard({ stackIndex: 6 }); await dashboard.expandedForm.fillField({ @@ -301,4 +301,30 @@ test.describe('View', () => { count: [1, 25, 25, 25, 25, 25], }); }); + + test('Kanban shared view operations', async ({ page }) => { + test.slow(); + + await dashboard.viewSidebar.createKanbanView({ + title: 'Film Kanban', + }); + await dashboard.viewSidebar.verifyView({ + title: 'Film Kanban', + index: 1, + }); + + // Share view + await toolbar.fields.toggle({ title: 'Rating' }); + await toolbar.clickShareView(); + const sharedLink = await toolbar.shareView.getShareLink(); + await toolbar.shareView.close(); + + // sign-out + await dashboard.signOut(); + + // Open shared view & verify stack count + await page.goto(sharedLink); + const kanban = dashboard.kanban; + await kanban.verifyStackCount({ count: 6 }); + }); });