diff --git a/.github/workflows/build-chromatic.yml b/.github/workflows/build-chromatic.yml deleted file mode 100644 index 435453f192ce..000000000000 --- a/.github/workflows/build-chromatic.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: "Build Storybook - UI Tests with Chromatic" - -on: - push: - branches: - - release - paths: - - "app/client/packages/design-system/**" - - "app/client/packages/storybook/**" - pull_request: - paths: - - "app/client/packages/design-system/**" - - "app/client/packages/storybook/**" - -jobs: - chromatic-deployment: - runs-on: ubuntu-latest - - steps: - - name: Checkout PR if pull_request event - if: github.event_name == 'pull_request' - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: refs/pull/${{ github.event.pull_request.number }}/merge - - - name: Checkout PR if push event - if: github.event_name == 'push' - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: release - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version-file: app/client/package.json - - - name: Install Dependencies - working-directory: ./app/client/packages/storybook - run: yarn install --immutable - - - name: Publish to Chromatic - id: chromatic-publish - uses: chromaui/action@v1 - env: - CHROMATIC: 1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - workingDir: ./app/client/packages/storybook - exitOnceUploaded: true - buildScriptName: "build-storybook" diff --git a/.github/workflows/build-storybook.yml b/.github/workflows/build-storybook.yml deleted file mode 100644 index 3a985c0f7876..000000000000 --- a/.github/workflows/build-storybook.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: "Build Storybook - Docs with Chromatic" - -on: - push: - branches: - - release - paths: - - "app/client/packages/design-system/**" - - "app/client/packages/storybook/**" - pull_request: - paths: - - "app/client/packages/design-system/**" - - "app/client/packages/storybook/**" - -jobs: - chromatic-deployment: - runs-on: ubuntu-latest - - steps: - - name: Checkout PR if pull_request event - if: github.event_name == 'pull_request' - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: refs/pull/${{ github.event.pull_request.number }}/merge - - - name: Checkout PR if push event - if: github.event_name == 'push' - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: release - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version-file: app/client/package.json - - - name: Install Dependencies - working-directory: ./app/client/packages/storybook - run: yarn install --immutable - - - name: Publish to Chromatic - id: chromatic-publish - uses: chromaui/action@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - projectToken: ${{ secrets.STORYBOOK_PROJECT_TOKEN }} - workingDir: ./app/client/packages/storybook - exitOnceUploaded: true - buildScriptName: "build-storybook" diff --git a/.github/workflows/ci-test-hosted.yml b/.github/workflows/ci-test-hosted.yml deleted file mode 100644 index 6f2796c63a66..000000000000 --- a/.github/workflows/ci-test-hosted.yml +++ /dev/null @@ -1,324 +0,0 @@ -name: Appsmith CI Test Workflow For Hosted Instance - -on: - # Schedule to run the workflow everyday at 7 AM or UTC (1.30 AM) only on weekday - schedule: - - cron: "30 1 * * 1-5" - # This line enables manual triggering of this workflow. - workflow_dispatch: - inputs: - pr: - description: "This is the PR number in case the workflow is being called in a pull request" - required: false - type: number - default: 0 - workflow_call: - inputs: - pr: - description: "This is the PR number in case the workflow is being called in a pull request" - required: false - type: number - -jobs: - ci-test: - runs-on: ubuntu-latest - if: | - github.event.pull_request.head.repo.full_name == github.repository || - github.event_name == 'push' || - github.event_name == 'workflow_dispatch' || - github.event_name == 'repository_dispatch' || - github.event_name == 'schedule' - defaults: - run: - shell: bash - - # Service containers to run with this job. Required for running tests - services: - # Label used to access the service container - redis: - # Docker Hub image for Redis - image: redis - ports: - # Opens tcp port 6379 on the host and service container - - 6379:6379 - mongo: - image: mongo - ports: - - 27017:27017 - - steps: - - name: Set up Depot CLI - uses: depot/setup-action@v1 - - # Check out merge commit - - name: Fork based /ok-to-test checkout - if: inputs.pr != 0 - uses: actions/checkout@v4 - with: - ref: "refs/pull/${{ inputs.pr }}/merge" - - # Checkout the code in the current branch in case the workflow is called because of a branch push event - - name: Checkout the head commit of the branch - if: inputs.pr == 0 || github.event_name == 'schedule' - uses: actions/checkout@v4 - - # Timestamp will be used to create cache key - - id: timestamp - run: echo "timestamp=$(date +'%Y-%m-%dT%H:%M:%S')" >> $GITHUB_OUTPUT - - # In case this is second attempt try restoring status of the prior attempt from cache - - name: Restore the previous run result - id: cache-appsmith - uses: actions/cache@v4 - with: - path: | - ~/run_result - key: ${{ github.run_id }}-${{ github.job }} - restore-keys: | - ${{ github.run_id }}-${{ github.job }} - - - name: Get the previous run result - if: steps.cache-appsmith.outputs.cache-hit == 'true' - id: run_result - run: | - run_result_env=$(cat ~/run_result) - echo "run_result=$run_result_env" >> $GITHUB_OUTPUT - if [[ "$run_result_env" == "failedtest" ]]; then - echo "rerun=true" >> $GITHUB_OUTPUT - else - echo "rerun=false" >> $GITHUB_OUTPUT - fi - - - name: Dump steps context - env: - STEPS_CONTEXT: ${{ toJson(steps) }} - run: echo "$STEPS_CONTEXT" - - - if: steps.run_result.outputs.run_result != 'success' && steps.run_result.outputs.run_result != 'failedtest' - run: echo "Starting full run" && exit 0 - - - if: steps.run_result.outputs.run_result == 'failedtest' - run: echo "Rerunning failed tests" && exit 0 - - - name: cat run_result - run: echo ${{ steps.run_result.outputs.run_result }} - - - name: Use Node.js - if: steps.run_result.outputs.run_result != 'success' - uses: actions/setup-node@v4 - with: - node-version-file: app/client/package.json - - # actions/setup-node@v4 doesn’t work properly with Yarn 3 - # when the project lives in a subdirectory: https://github.com/actions/setup-node/issues/488 - # Restoring the cache manually instead - - name: Restore Yarn cache - if: steps.run_result.outputs.run_result != 'success' - uses: actions/cache@v4 - with: - path: | - app/client/.yarn/cache - app/client/node_modules/.cache/webpack/ - key: v1-yarn3-${{ hashFiles('app/client/yarn.lock') }} - restore-keys: | - v1-yarn3- - - # Install all the dependencies - - name: Install dependencies - if: steps.run_result.outputs.run_result != 'success' - working-directory: app/client - run: yarn install --immutable - - - name: Setting up the cypress tests - if: steps.run_result.outputs.run_result != 'success' - shell: bash - run: | - cd app/client - chmod a+x ./cypress/setup-test-ci.sh - ./cypress/setup-test-ci.sh - - - uses: browser-actions/setup-chrome@latest - with: - chrome-version: stable - - run: | - echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV - - - name: Save Git values - # pass env variables from this step to other steps - # using GitHub Actions environment file - # https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#environment-files - run: | - PR_NUMBER=${{ inputs.pr }} - echo COMMIT_INFO_BRANCH=$(git rev-parse --abbrev-ref HEAD) >> $GITHUB_ENV - echo COMMIT_INFO_MESSAGE=OkToTest run on PR# ${{ inputs.pr }} >> $GITHUB_ENV - echo COMMIT_INFO_EMAIL=$(git show -s --pretty=%ae) >> $GITHUB_ENV - echo COMMIT_INFO_AUTHOR=$(git show -s --pretty=%an) >> $GITHUB_ENV - echo COMMIT_INFO_SHA=$(git show -s --pretty=%H) >> $GITHUB_ENV - echo COMMIT_INFO_TIMESTAMP=$(git show -s --pretty=%ct) >> $GITHUB_ENV - echo COMMIT_INFO_REMOTE=$(git config --get remote.origin.url) >> $GITHUB_ENV - # delete the .git folder afterwords to use the environment values - rm -rf .git - - - name: Show Git values - run: | - echo Branch $COMMIT_INFO_BRANCH - echo Message $COMMIT_INFO_MESSAGE - echo Email $COMMIT_INFO_EMAIL - echo Author $COMMIT_INFO_AUTHOR - echo SHA $COMMIT_INFO_SHA - echo Timestamp $COMMIT_INFO_TIMESTAMP - echo Remote $COMMIT_INFO_REMOTE - - - name: Set Commit Message - env: - EVENT_COMMITS: ${{ toJson(github.event.commits[0].message) }} - run: | - if [[ ${{ github.event_name }} == 'schedule' ]]; then - echo "COMMIT_INFO_MESSAGE=Scheduled run for Hosted tests" >> $GITHUB_ENV - else - echo "COMMIT_INFO_MESSAGE=Manual workflow run for Hosted tests" >> $GITHUB_ENV - fi - - - name: Run the cypress test - uses: cypress-io/github-action@v6 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CYPRESS_USERNAME: ${{ secrets.CYPRESS_USERNAME }} - CYPRESS_PASSWORD: ${{ secrets.CYPRESS_PASSWORD }} - CYPRESS_TESTUSERNAME1: ${{ secrets.CYPRESS_TESTUSERNAME1 }} - CYPRESS_TESTPASSWORD1: ${{ secrets.CYPRESS_TESTPASSWORD1 }} - CYPRESS_TESTUSERNAME2: ${{ secrets.CYPRESS_TESTUSERNAME2 }} - CYPRESS_TESTPASSWORD2: ${{ secrets.CYPRESS_TESTPASSWORD1 }} - CYPRESS_TESTUSERNAME3: ${{ secrets.CYPRESS_TESTUSERNAME3 }} - CYPRESS_TESTPASSWORD3: ${{ secrets.CYPRESS_TESTPASSWORD3 }} - CYPRESS_TESTUSERNAME4: ${{ secrets.CYPRESS_TESTUSERNAME4 }} - CYPRESS_TESTPASSWORD4: ${{ secrets.CYPRESS_TESTPASSWORD4 }} - CYPRESS_S3_ACCESS_KEY: ${{ secrets.CYPRESS_S3_ACCESS_KEY }} - CYPRESS_S3_SECRET_KEY: ${{ secrets.CYPRESS_S3_SECRET_KEY }} - CYPRESS_AIRTABLE_BEARER: ${{ secrets.AIRTABLE_BEARER }} - CYPRESS_ORACLE_HOST: ${{ secrets.ORACLE_HOST }} - CYPRESS_ORACLE_SERVICE: ${{ secrets.ORACLE_SERVICE }} - CYPRESS_ORACLE_USERNAME: ${{ secrets.ORACLE_USERNAME }} - CYPRESS_ORACLE_PASSWORD: ${{ secrets.ORACLE_PASSWORD }} - CYPRESS_FIRESTORE_PRIVATE_KEY: ${{ secrets.FIRESTORE_PRIVATE_KEY }} - CYPRESS_APPSMITH_OAUTH2_GOOGLE_CLIENT_ID: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_GOOGLE_CLIENT_ID }} - CYPRESS_APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET }} - CYPRESS_APPSMITH_OAUTH2_GITHUB_CLIENT_ID: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_GITHUB_CLIENT_ID }} - CYPRESS_APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET }} - CYPRESS_OAUTH_SAML_EMAIL: ${{ secrets.CYPRESS_OAUTH_SAML_EMAIL }} - CYPRESS_OAUTH_SAML_ENTITY_ID: ${{ secrets.CYPRESS_OAUTH_SAML_ENTITY_ID }} - CYPRESS_OAUTH_SAML_METADATA_URL: ${{ secrets.CYPRESS_OAUTH_SAML_METADATA_URL }} - CYPRESS_OAUTH_SAML_METADATA_XML: ${{ secrets.CYPRESS_OAUTH_SAML_METADATA_XML }} - CYPRESS_OAUTH_SAML_PUB_CERT: ${{ secrets.CYPRESS_OAUTH_SAML_PUB_CERT }} - CYPRESS_OAUTH_SAML_SSO_URL: ${{ secrets.CYPRESS_OAUTH_SAML_SSO_URL }} - CYPRESS_OAUTH_SAML_REDIRECT_URL: ${{ secrets.CYPRESS_OAUTH_SAML_REDIRECT_URL }} - CYPRESS_APPSMITH_OAUTH2_OIDC_CLIENT_ID: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_OIDC_CLIENT_ID }} - CYPRESS_APPSMITH_OAUTH2_OIDC_CLIENT_SECRET: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_OIDC_CLIENT_SECRET }} - CYPRESS_APPSMITH_OAUTH2_OIDC_AUTH_URL: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_OIDC_AUTH_URL }} - CYPRESS_APPSMITH_OAUTH2_OIDC_TOKEN_URL: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_OIDC_TOKEN_URL }} - CYPRESS_APPSMITH_OAUTH2_OIDC_USER_INFO: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_OIDC_USER_INFO }} - CYPRESS_APPSMITH_OAUTH2_OIDC_JWKS_URL: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_OIDC_JWKS_URL }} - CYPRESS_APPSMITH_OAUTH2_OIDC_OKTA_PASSWORD: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_OIDC_OKTA_PASSWORD }} - CYPRESS_APPSMITH_OAUTH2_OIDC_DIRECT_URL: ${{ secrets.CYPRESS_APPSMITH_OAUTH2_OIDC_DIRECT_URL }} - CYPRESS_EXCLUDE_TAGS: "airgap" - CYPRESS_AIRGAPPED: false - APPSMITH_DISABLE_TELEMETRY: true - APPSMITH_GOOGLE_MAPS_API_KEY: ${{ secrets.APPSMITH_GOOGLE_MAPS_API_KEY }} - COMMIT_INFO_MESSAGE: ${{ env.COMMIT_INFO_MESSAGE }} - CYPRESS_VERIFY_TIMEOUT: 100000 - RUNID: ${{ github.run_id }} - ATTEMPT_NUMBER: ${{ github.run_attempt }} - REPOSITORY: ${{ github.repository }} - COMMITTER: ${{ env.COMMIT_INFO_AUTHOR }} - TAG: ${{ github.event_name }} - BRANCH: ${{ env.COMMIT_INFO_BRANCH }} - THIS_RUNNER: ${{ strategy.job-index }} - TOTAL_RUNNERS: ${{ strategy.job-total }} - CYPRESS_RERUN: ${{steps.run_result.outputs.rerun}} - CYPRESS_DB_USER: ${{ secrets.CYPRESS_DB_USER }} - CYPRESS_DB_HOST: ${{ secrets.CYPRESS_DB_HOST }} - CYPRESS_DB_NAME: ${{ secrets.CYPRESS_DB_NAME }} - CYPRESS_DB_PWD: ${{ secrets.CYPRESS_DB_PWD }} - CYPRESS_S3_ACCESS: ${{ secrets.CYPRESS_S3_ACCESS }} - CYPRESS_S3_SECRET: ${{ secrets.CYPRESS_S3_SECRET }} - CYPRESS_STATIC_ALLOCATION: true - with: - install: false - config-file: cypress_ci_hosted.config.ts - working-directory: app/client - env: "NODE_ENV=development" - - - name: Rename reports - if: failure() - run: | - mkdir -p ~/results - mv ${{ github.workspace }}/app/client/results ~/results/${{ matrix.job }} - - - name: Upload cypress report - if: failure() - uses: actions/upload-artifact@v4 - with: - name: results-${{github.run_attempt}} - path: ~/results - overwrite: true - - - name: Upload cypress snapshots - if: failure() - uses: actions/upload-artifact@v4 - with: - name: snapshots - path: ${{ github.workspace }}/app/client/cypress/snapshots - overwrite: true - - # Set status = failedtest - - name: Set fail if there are test failures - id: test_status - if: failure() - run: | - echo "run_result=failedtest" >> $GITHUB_OUTPUT - echo "failedtest" > ~/run_result - - # Force store previous run result to cache - - name: Store the previous run result - if: failure() - uses: actions/cache/save@v4 - with: - path: | - ~/run_result - key: ${{ github.run_id }}-${{ github.job }} - - - name: Generate slack message - continue-on-error: true - if: always() - id: slack_notification - run: | - if [[ "${{ steps.test_status.outputs.run_result }}" == "failedtest" ]]; then - echo "slack_message=There are test failures in the run. Cypress Dashboard: " >> $GITHUB_OUTPUT - echo "slack_color=#FF0000" >> $GITHUB_OUTPUT - echo "slack_icon=:parachute:" >> $GITHUB_OUTPUT - else - echo "slack_message=All tests passed successfully :tada: . Cypress Dashboard: " >> $GITHUB_OUTPUT - echo "slack_color=#00FF00" >> $GITHUB_OUTPUT - echo "slack_icon=:eight_spoked_asterisk:" >> $GITHUB_OUTPUT - fi - - - name: Slack Notification - continue-on-error: true - if: always() - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_CHANNEL: cypresspushworkflow - SLACK_COLOR: ${{steps.slack_notification.outputs.slack_color}} - SLACK_ICON_EMOJI: ${{steps.slack_notification.outputs.slack_icon}} - SLACK_MESSAGE: ${{steps.slack_notification.outputs.slack_message}} - SLACK_TITLE: "Result:" - SLACK_USERNAME: Cloud Hosted Run - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_HOSTED }} - MSG_MINIMAL: Ref,Event,Commit - SLACK_FOOTER: "Hosted run" - - # Set status = success - - name: Save the status of the run - run: | - echo "run_result=success" >> $GITHUB_OUTPUT - echo "success" > ~/run_result diff --git a/.github/workflows/github-commit.yml b/.github/workflows/github-commit.yml new file mode 100644 index 000000000000..c3ef5a77111a --- /dev/null +++ b/.github/workflows/github-commit.yml @@ -0,0 +1,81 @@ +name: Appsmith Build Test Github + +# This workflow builds Docker images for server and client, and then pushes them to Docker Hub. +# The docker-tag with which this push happens is `latest` and the release tag (e.g., v1.2.3 etc.). +# This workflow does NOT run tests. +# This workflow is automatically triggered when a release is created on GitHub. + +on: + # Ref: . + push: + branches: ["release"] + +jobs: + prelude: + runs-on: ubuntu-latest + + outputs: + tag: ${{ steps.get_version.outputs.tag }} + + steps: + - name: Environment details + run: | + echo ${{ secrets.DOCKER_HUB_USERNAME2 }} + echo "PWD: $PWD" + echo "GITHUB_REF: $GITHUB_REF" + echo "GITHUB_SHA: $GITHUB_SHA" + echo "GITHUB_EVENT_NAME: $GITHUB_EVENT_NAME" + + rts-build: + needs: + - prelude + + defaults: + run: + working-directory: app/client/packages/rts + + runs-on: ubuntu-latest + + steps: + # Checkout the code + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: app/client/package.json + + # actions/setup-node@v4 doesn’t work properly with Yarn 3 + # when the project lives in a subdirectory: https://github.com/actions/setup-node/issues/488 + # Restoring the cache manually instead + - name: Restore Yarn cache + if: steps.run_result.outputs.run_result != 'success' + uses: actions/cache@v4 + with: + path: app/.yarn/cache + key: v1-yarn3-${{ hashFiles('app/yarn.lock') }} + restore-keys: | + v1-yarn3- + + # Install all the dependencies + - name: Install dependencies + if: steps.run_result.outputs.run_result != 'success' + run: yarn install --immutable + + - name: Build + run: | + echo 'export const VERSION = "${{ needs.prelude.outputs.tag }}"' > src/version.js + yarn build + + # Tar the bundles to speed up the upload & download process + - name: Tar the rts bundles + run: | + tar -cvf rts-dist.tar dist + + # Upload the build artifacts and dependencies so that it can be used by the test & deploy job in other workflows + - name: Upload rts build bundle + uses: actions/upload-artifact@v4 + with: + name: rts-dist + path: app/client/packages/rts/rts-dist.tar + overwrite: true \ No newline at end of file diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index cedac1f84817..786252a629d6 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -22,11 +22,16 @@ jobs: steps: - name: Environment details run: | + echo ${{ secrets.DOCKER_HUB_USERNAME2 }} echo "PWD: $PWD" echo "GITHUB_REF: $GITHUB_REF" echo "GITHUB_SHA: $GITHUB_SHA" echo "GITHUB_EVENT_NAME: $GITHUB_EVENT_NAME" - + - name: Login to DockerHub + uses: docker/login-action@v3.3.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME2 }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN2 }} - name: Get the version id: get_version run: | @@ -40,7 +45,7 @@ jobs: needs: - prelude - runs-on: ubuntu-latest-4-cores + runs-on: ubuntu-latest defaults: run: @@ -75,9 +80,9 @@ jobs: - name: Create the bundle env: REACT_APP_ENVIRONMENT: "PRODUCTION" - REACT_APP_FUSIONCHARTS_LICENSE_KEY: "${{ secrets.APPSMITH_FUSIONCHARTS_LICENSE_KEY }}" - REACT_APP_SEGMENT_CE_KEY: "${{ secrets.APPSMITH_SEGMENT_CE_KEY }}" - REACT_APP_INTERCOM_APP_ID: "${{ secrets.APPSMITH_INTERCOM_ID }}" + REACT_APP_FUSIONCHARTS_LICENSE_KEY: "${{ secrets.APPSMITH_FUSIONCHARTS_LICENSE_KEY2 }}" + REACT_APP_SEGMENT_CE_KEY: "${{ secrets.APPSMITH_SEGMENT_CE_KEY2 }}" + REACT_APP_INTERCOM_APP_ID: "${{ secrets.APPSMITH_INTERCOM_ID2 }}" REACT_APP_VERSION_EDITION: "Community" run: | yarn build @@ -209,8 +214,8 @@ jobs: - name: Checkout the merged commit from PR and base branch uses: actions/checkout@v4 - - name: Set up Depot CLI - uses: depot/setup-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Download the client build artifact uses: actions/download-artifact@v4 @@ -247,31 +252,27 @@ jobs: run: | scripts/generate_info_json.sh - # As pg docker image is continuously updated for each scheduled cron on release, we are using the nightly tag while building the latest tag - name: Place server artifacts-es run: | if [[ -f scripts/prepare_server_artifacts.sh ]]; then - PG_TAG=nightly scripts/prepare_server_artifacts.sh - else - echo "No script found to prepare server artifacts" - exit 1 + scripts/prepare_server_artifacts.sh fi - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + username: ${{ secrets.DOCKER_HUB_USERNAME2 }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN2 }} - - name: Build and push fat image - uses: depot/build-push-action@v1 + - name: Build and push Docker image + uses: docker/build-push-action@v3 with: context: . push: true platforms: linux/arm64,linux/amd64 build-args: | - APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY }} - BASE=${{ vars.DOCKER_HUB_ORGANIZATION }}/base-${{ vars.EDITION }}:nightly + APPSMITH_SEGMENT_CE_KEY=${{ secrets.APPSMITH_SEGMENT_CE_KEY2 }} + BASE=appsmith/base-ce:nightly tags: | - ${{ vars.DOCKER_HUB_ORGANIZATION }}/appsmith-${{ vars.EDITION }}:${{needs.prelude.outputs.tag}} - ${{ vars.DOCKER_HUB_ORGANIZATION }}/appsmith-${{ vars.EDITION }}:latest + ${{ secrets.DOCKER_HUB_USERNAME2 }}/appsmith-ce:${{needs.prelude.outputs.tag}} + ${{ secrets.DOCKER_HUB_USERNAME2 }}/appsmith-ce:latest diff --git a/.github/workflows/sync-release-to-pg.yml b/.github/workflows/sync-release-to-pg.yml deleted file mode 100644 index f5ad35e0669c..000000000000 --- a/.github/workflows/sync-release-to-pg.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Merge release to pg - -on: - push: - branches: - - release # Trigger on push to the release branch - -jobs: - merge-release-to-pg: - runs-on: ubuntu-latest - - steps: - - name: Checkout release branch - uses: actions/checkout@v3 - with: - ref: release # Checkout the release branch - fetch-depth: 0 - - - name: Set Git config values - run: | - git config pull.rebase false - git config user.email "automated@github.com" - git config user.name "Automated Github Action" - - - name: Checkout pg branch - run: git checkout pg - - - name: Merge release to pg - id: merge_commits - run: | - PG_HEAD=$(git rev-parse pg) - RELEASE_HEAD=$(git rev-parse release) - - echo "PG_HEAD=$PG_HEAD" - echo "RELEASE_HEAD=$RELEASE_HEAD" - - # Attempt to merge release into pg - if ! git merge release; then - echo "Merge conflict detected during merge" - - # Capture the conflicting commit SHAs (both HEAD of pg and the merge commit from release) - CONFLICTING_COMMIT=$(git log -1 --pretty=format:"%H") - echo "CONFLICTING_COMMIT=$CONFLICTING_COMMIT" >> $GITHUB_ENV - - echo "MERGE_CONFLICT=true" >> $GITHUB_ENV - else - echo "MERGE_CONFLICT=false" >> $GITHUB_ENV - fi - - - name: Push changes - if: env.MERGE_CONFLICT == 'false' - run: | - git push origin pg - - - name: Notify on merge conflicts - if: env.MERGE_CONFLICT == 'true' - env: - REPOSITORY_URL: ${{ github.repositoryUrl }} - CONFLICTING_COMMIT: ${{ env.CONFLICTING_COMMIT }} - run: | - # Prepare the message for Slack - message="Merge conflict detected while merging release into pg branch. Conflicted commits:\n" - commit_url="$REPOSITORY_URL/commit/$CONFLICTING_COMMIT" - message+="$commit_url\n" - - # Send the message to Slack - # This unwieldy horror of a sed command, converts standard Markdown links to Slack's unwieldy link syntax. - slack_message="$(echo "$message" | sed -E 's/\[([^]]+)\]\(([^)]+)\)/<\2|\1>/g')" - - echo "$slack_message" - - # This is the ChannelId of the proj postgres channel. - body="$(jq -nc \ - --arg channel C07JMLWEXDJ \ - --arg text "$slack_message" \ - '$ARGS.named' - )" - - curl --version - curl -v https://slack.com/api/chat.postMessage \ - --header 'Authorization: Bearer ${{ secrets.SLACK_APPSMITH_ALERTS_TOKEN }}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data-raw "$body" diff --git a/app/client/package.json b/app/client/package.json index 3b205ee5ea81..195a408067fb 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -126,7 +126,7 @@ "cypress-repeat-pro": "^1.0.1", "d3-geo": "^3.1.0", "date-fns": "2.30.0", - "dayjs": "^1.10.6", + "dayjs": "^1.11.10", "deep-diff": "^1.0.2", "downloadjs": "^1.4.7", "echarts": "^5.4.2", @@ -185,6 +185,7 @@ "react-json-view": "^1.21.3", "react-media-recorder": "^1.6.1", "react-modal": "^3.15.1", + "react-multi-date-picker": "^4.5.2", "react-page-visibility": "^7.0.0", "react-player": "^2.3.1", "react-qr-barcode-scanner": "^1.0.6", diff --git a/app/client/packages/design-system/theming/src/hooks/src/useTheme.tsx b/app/client/packages/design-system/theming/src/hooks/src/useTheme.tsx index 7cee09699342..3d411b818e5e 100644 --- a/app/client/packages/design-system/theming/src/hooks/src/useTheme.tsx +++ b/app/client/packages/design-system/theming/src/hooks/src/useTheme.tsx @@ -10,7 +10,7 @@ import { } from "./"; import type { ColorMode } from "../../color"; -import type { TokenSource } from "../../token"; +import type { TokenSource, FontFamily } from "../../token"; const tokensAccessor = new TokensAccessor({ ...(defaultTokens as TokenSource), @@ -22,10 +22,12 @@ export interface UseThemeProps { borderRadius?: string; userDensity?: number; userSizing?: number; + fontFamily?: FontFamily; } export function useTheme(props: UseThemeProps = {}) { const { + fontFamily, borderRadius, colorMode = "light", seedColor, @@ -42,6 +44,7 @@ export function useTheme(props: UseThemeProps = {}) { ); const { typography } = useTypography( tokensConfigs.typography, + fontFamily, userDensity, userSizing, ); @@ -52,6 +55,7 @@ export function useTheme(props: UseThemeProps = {}) { ); const theme = useMemo(() => { + // Color mode tokensAccessor.updateColorMode(colorMode); @@ -77,6 +81,7 @@ export function useTheme(props: UseThemeProps = {}) { // Typography if (typography != null) { tokensAccessor.updateTypography(typography); + tokensAccessor.updateFontFamily(fontFamily); } // Sizing @@ -107,6 +112,7 @@ export function useTheme(props: UseThemeProps = {}) { ...tokensAccessor.getInnerSpacing(), ...tokensAccessor.getIconSize(), ...tokensAccessor.getStrokeWidth(), + fontFamily: tokensAccessor.getFontFamily(), }; }, [ colorMode, @@ -118,6 +124,7 @@ export function useTheme(props: UseThemeProps = {}) { innerSpacing, iconSize, strokeWidth, + fontFamily, ]); return { theme }; diff --git a/app/client/packages/design-system/theming/src/hooks/src/useTypography.tsx b/app/client/packages/design-system/theming/src/hooks/src/useTypography.tsx index 05380e99d634..7c0ec6f960e6 100644 --- a/app/client/packages/design-system/theming/src/hooks/src/useTypography.tsx +++ b/app/client/packages/design-system/theming/src/hooks/src/useTypography.tsx @@ -4,17 +4,27 @@ import { createStyleObject } from "@capsizecss/core"; import appleSystem from "@capsizecss/metrics/appleSystem"; import type { + FontFamily, Typography, TypographyVariantMetric, TokenScaleConfig, } from "../../token"; -import { TYPOGRAPHY_VARIANTS } from "../../token/src/types"; +import { FONT_METRICS, TYPOGRAPHY_VARIANTS } from "../../token/src/types"; import { objectKeys } from "@appsmith/utils"; +const getFontMetrics = (fontFamily?: FontFamily) => { + return !Boolean(fontFamily) || + fontFamily == null || + fontFamily === "System Default" + ? appleSystem + : FONT_METRICS[fontFamily]; +}; + export const getTypography = ( typography: TokenScaleConfig, userDensity = 1, userSizing = 1, + fontFamily?: FontFamily, ) => { const { userDensityRatio = 1, userSizingRatio = 1, V, ...rest } = typography; const ratio = userDensity * userDensityRatio + userSizing * userSizingRatio; @@ -28,7 +38,7 @@ export const getTypography = ( const typographyStyle = createStyleObject({ capHeight: currentValue, lineGap: currentValue, - fontMetrics: appleSystem, + fontMetrics: getFontMetrics(fontFamily), }); metrics.push({ @@ -53,12 +63,13 @@ export const getTypography = ( export const useTypography = ( config: TokenScaleConfig, + fontFamily?: FontFamily, userDensity = 1, userSizing = 1, ) => { const typography = useMemo(() => { - return getTypography(config, userDensity, userSizing); - }, [config, userDensity, userSizing]); + return getTypography(config, userDensity, userSizing, fontFamily); + }, [config, userDensity, userSizing, fontFamily]); return { typography, diff --git a/app/client/packages/design-system/theming/src/token/src/TokensAccessor.ts b/app/client/packages/design-system/theming/src/token/src/TokensAccessor.ts index 05c3bcbfb310..8860584a110a 100644 --- a/app/client/packages/design-system/theming/src/token/src/TokensAccessor.ts +++ b/app/client/packages/design-system/theming/src/token/src/TokensAccessor.ts @@ -8,6 +8,7 @@ import type { TokenSource, TokenType, Typography, + FontFamily, } from "./types"; export class TokensAccessor { @@ -24,6 +25,7 @@ export class TokensAccessor { private zIndex?: TokenObj; private strokeWidth?: TokenObj; private iconSize?: TokenObj; + private fontFamily?: FontFamily; constructor({ borderRadiusElevation, @@ -39,6 +41,7 @@ export class TokensAccessor { strokeWidth, typography, zIndex, + fontFamily, }: TokenSource) { this.seedColor = seedColor; this.colorMode = colorMode; @@ -53,12 +56,19 @@ export class TokensAccessor { this.zIndex = zIndex; this.strokeWidth = strokeWidth; this.iconSize = iconSize; + this.fontFamily = fontFamily; } updateTypography = (typography: Typography) => { this.typography = typography; }; + + updateFontFamily = (fontFamily?: FontFamily) => { + this.fontFamily = fontFamily; + }; + + updateSeedColor = (color: ColorTypes) => { this.seedColor = color; }; @@ -131,9 +141,14 @@ export class TokensAccessor { ...this.getStrokeWidth(), ...this.getIconSize(), colorMode: this.getColorMode(), + fontFamily: this.getFontFamily(), }; }; + getFontFamily = () => { + return this.fontFamily; + }; + getTypography = () => { return this.typography; }; diff --git a/app/client/packages/design-system/theming/src/token/src/types.ts b/app/client/packages/design-system/theming/src/token/src/types.ts index 5a457153f690..9555b68fe9b3 100644 --- a/app/client/packages/design-system/theming/src/token/src/types.ts +++ b/app/client/packages/design-system/theming/src/token/src/types.ts @@ -36,6 +36,7 @@ export interface TokenSource { innerSpacing?: TokenObj; strokeWidth?: TokenObj; iconSize?: TokenObj; + fontFamily?: FontFamily; } export interface TokenObj { @@ -64,6 +65,38 @@ export interface TokenScaleConfig { userDensityRatio?: number; } +import arial from "@capsizecss/metrics/arial"; +import inter from "@capsizecss/metrics/inter"; +import rubik from "@capsizecss/metrics/rubik"; +import roboto from "@capsizecss/metrics/roboto"; +import ubuntu from "@capsizecss/metrics/ubuntu"; +import poppins from "@capsizecss/metrics/poppins"; +import segoeUI from "@capsizecss/metrics/segoeUI"; +import openSans from "@capsizecss/metrics/openSans"; +import notoSans from "@capsizecss/metrics/notoSans"; +import montserrat from "@capsizecss/metrics/montserrat"; +import nunitoSans from "@capsizecss/metrics/nunitoSans12pt"; +import appleSystem from "@capsizecss/metrics/appleSystem"; +import BlinkMacSystemFont from "@capsizecss/metrics/blinkMacSystemFont"; +import VazirMatn from "@capsizecss/metrics/vazirmatn"; + +export const FONT_METRICS = { + Poppins: poppins, + Inter: inter, + Roboto: roboto, + Rubik: rubik, + Ubuntu: ubuntu, + "Noto Sans": notoSans, + "Open Sans": openSans, + Montserrat: montserrat, + "Nunito Sans": nunitoSans, + Arial: arial, + "-apple-system": appleSystem, + BlinkMacSystemFont: BlinkMacSystemFont, + "Segoe UI": segoeUI, + VazirMatn: VazirMatn, +} as const; + // we use "as const" here because we need to iterate by variants export const TYPOGRAPHY_VARIANTS = { footnote: "footnote", @@ -86,6 +119,8 @@ export const TYPOGRAPHY_FONT_WEIGHTS = { 900: 900, } as const; +export type FontFamily = keyof typeof FONT_METRICS | "System Default"; + export interface TypographyVariantMetric { fontSize: string; lineHeight: string; diff --git a/app/client/packages/dsl/src/migrate/helpers/widget-configs.json b/app/client/packages/dsl/src/migrate/helpers/widget-configs.json index 2aefa6fae959..79b1a823bdf4 100644 --- a/app/client/packages/dsl/src/migrate/helpers/widget-configs.json +++ b/app/client/packages/dsl/src/migrate/helpers/widget-configs.json @@ -385,6 +385,14 @@ { "label": "Ubuntu", "value": "Ubuntu" + }, + { + "label": "YekanBakh", + "value": "YekanBakh" + }, + { + "label" : "VazirMatn", + "value" : "VazirMatn" } ], "defaultValue": "System Default", diff --git a/app/client/src/assets/fonts/custom/index.css b/app/client/src/assets/fonts/custom/index.css index 647ce74ca796..03cc59610014 100644 --- a/app/client/src/assets/fonts/custom/index.css +++ b/app/client/src/assets/fonts/custom/index.css @@ -8,3 +8,5 @@ @import "./rubik/rubik.css"; @import "./ubuntu/ubuntu.css"; @import "./twemoji/twemoji.css"; +@import "./yekanBakh/yekanBakh.css"; +@import "./vazir-matn/vazirMatn.css"; \ No newline at end of file diff --git a/app/client/src/assets/fonts/custom/vazir-matn/vazir-matn-01/Vazirmatn.ttf b/app/client/src/assets/fonts/custom/vazir-matn/vazir-matn-01/Vazirmatn.ttf new file mode 100644 index 000000000000..b02ceb054d5f Binary files /dev/null and b/app/client/src/assets/fonts/custom/vazir-matn/vazir-matn-01/Vazirmatn.ttf differ diff --git a/app/client/src/assets/fonts/custom/vazir-matn/vazirMatn.css b/app/client/src/assets/fonts/custom/vazir-matn/vazirMatn.css new file mode 100644 index 000000000000..f553d946b4de --- /dev/null +++ b/app/client/src/assets/fonts/custom/vazir-matn/vazirMatn.css @@ -0,0 +1,6 @@ +@font-face { + font-display: swap; + font-family: "VazirMatn"; + font-style: normal; + src: url("./vazir-matn-01/Vazirmatn.ttf") + } diff --git a/app/client/src/assets/fonts/custom/yekanBakh/yekanBakh.css b/app/client/src/assets/fonts/custom/yekanBakh/yekanBakh.css new file mode 100644 index 000000000000..6cec90976d75 --- /dev/null +++ b/app/client/src/assets/fonts/custom/yekanBakh/yekanBakh.css @@ -0,0 +1,5 @@ +@font-face { + font-display: swap; + font-family: "YekanBakh"; + src: url("./yekanBakh/YekanBakhFaNum-VF.ttf"); +} diff --git a/app/client/src/assets/fonts/custom/yekanBakh/yekanBakh/YekanBakhFaNum-VF.ttf b/app/client/src/assets/fonts/custom/yekanBakh/yekanBakh/YekanBakhFaNum-VF.ttf new file mode 100644 index 000000000000..b5eaaaffc609 Binary files /dev/null and b/app/client/src/assets/fonts/custom/yekanBakh/yekanBakh/YekanBakhFaNum-VF.ttf differ diff --git a/app/client/src/components/editorComponents/PartialImportExport/PartialExportModal/unitTestUtils.ts b/app/client/src/components/editorComponents/PartialImportExport/PartialExportModal/unitTestUtils.ts index 5fde2d76c5bf..c44f8e3b5d42 100644 --- a/app/client/src/components/editorComponents/PartialImportExport/PartialExportModal/unitTestUtils.ts +++ b/app/client/src/components/editorComponents/PartialImportExport/PartialExportModal/unitTestUtils.ts @@ -12760,7 +12760,7 @@ export const defaultAppState = { release_anvil_enabled: false, release_app_sidebar_enabled: false, license_git_branch_protection_enabled: false, - license_widget_rtl_support_enabled: false, + license_widget_rtl_support_enabled: true, release_show_new_sidebar_announcement_enabled: false, rollout_app_sidebar_enabled: false, ab_one_click_learning_popover_enabled: false, @@ -16270,6 +16270,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -16704,6 +16706,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -17137,6 +17141,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -17153,7 +17159,7 @@ export const defaultAppState = { "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", }, fontFamily: { - appFont: "Rubik", + appFont: "YekanBakh", }, }, stylesheet: { @@ -17571,6 +17577,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -17587,7 +17595,7 @@ export const defaultAppState = { "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", }, fontFamily: { - appFont: "Rubik", + appFont: "YekanBakh", }, }, stylesheet: { @@ -18005,6 +18013,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -18439,6 +18449,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -18873,6 +18885,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -19307,6 +19321,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -19741,6 +19757,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "VazirMatn", + "YekanBakh" ], }, }, @@ -20181,6 +20199,8 @@ export const defaultAppState = { "Roboto", "Rubik", "Ubuntu", + "YekanBakh", + "VazirMatn" ], }, }, diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index f5ff85758468..8c285d5a03a2 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -241,6 +241,7 @@ export const WIDGET_TAGS = { CONTENT: "Content", EXTERNAL: "External", BUILDING_BLOCKS: "Building Blocks", + MYDATEPICKER : "MyDatePicker", } as const; export type WidgetTags = (typeof WIDGET_TAGS)[keyof typeof WIDGET_TAGS]; diff --git a/app/client/src/pages/AppViewer/Navigation/index.tsx b/app/client/src/pages/AppViewer/Navigation/index.tsx index 6056575735be..6603d053cda0 100644 --- a/app/client/src/pages/AppViewer/Navigation/index.tsx +++ b/app/client/src/pages/AppViewer/Navigation/index.tsx @@ -119,7 +119,7 @@ export function Navigation() { return ( -
+
{/* Since the Backend doesn't have navigationSetting field by default and we are creating the default values only when any nav settings via the settings pane has changed, we need to hide the navbar ONLY when the showNavbar diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index abe1ebbf3884..d55afd3169c0 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -208,6 +208,7 @@ function AppViewer(props: Props) { const renderChildren = () => { return ( +
{!isAnvilLayout && ( {isInitialized && } - +
); }; diff --git a/app/client/src/pages/Editor/ThemePropertyPane/controls/ThemeFontControl.tsx b/app/client/src/pages/Editor/ThemePropertyPane/controls/ThemeFontControl.tsx index 3f6025150519..eb30a5768113 100644 --- a/app/client/src/pages/Editor/ThemePropertyPane/controls/ThemeFontControl.tsx +++ b/app/client/src/pages/Editor/ThemePropertyPane/controls/ThemeFontControl.tsx @@ -48,7 +48,7 @@ function ThemeFontControl(props: ThemeFontControlProps) { Aa -
{option}
+
{option === 'Rubik' ? "YekanBakh" : option}
))} diff --git a/app/client/src/widgets/ButtonWidget/widget/index.tsx b/app/client/src/widgets/ButtonWidget/widget/index.tsx index 42dc902d874a..6dfdbb901c09 100644 --- a/app/client/src/widgets/ButtonWidget/widget/index.tsx +++ b/app/client/src/widgets/ButtonWidget/widget/index.tsx @@ -338,6 +338,119 @@ class ButtonWidget extends BaseWidget { ], }, { + sectionName: "Font", + children: [ + { + propertyName: "fontFamily", + label: "Font family", + helpText: "Controls the font family being used", + controlType: "DROP_DOWN", + options: [ + { + label: "System Default", + value: "System Default", + }, + { + label: "Nunito Sans", + value: "Nunito Sans", + }, + { + label: "Poppins", + value: "Poppins", + }, + { + label: "Inter", + value: "Inter", + }, + { + label: "Montserrat", + value: "Montserrat", + }, + { + label: "Noto Sans", + value: "Noto Sans", + }, + { + label: "Open Sans", + value: "Open Sans", + }, + { + label: "Roboto", + value: "Roboto", + }, + { + label: "Rubik", + value: "Rubik", + }, + { + label: "Ubuntu", + value: "Ubuntu", + }, + { + label: "YekanBakh", + value: "YekanBakh", + }, + { + label: "VazirMatn", + value: "VazirMatn" + }, + ], + defaultValue: "System Default", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.TEXT, + }, + }, + { + propertyName: "fontSize", + label: "Font size", + helpText: "Controls the size of the font used", + controlType: "DROP_DOWN", + defaultValue: "1rem", + options: [ + { + label: "S", + value: "0.875rem", + subText: "0.875rem", + }, + { + label: "M", + value: "1rem", + subText: "1rem", + }, + { + label: "L", + value: "1.25rem", + subText: "1.25rem", + }, + { + label: "XL", + value: "1.875rem", + subText: "1.875rem", + }, + { + label: "XXL", + value: "3rem", + subText: "3rem", + }, + { + label: "3XL", + value: "3.75rem", + subText: "3.75rem", + }, + ], + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.TEXT, + }, + }, + ], + } + ,{ sectionName: "Icon", children: [ { diff --git a/app/client/src/widgets/MyDatePickerWidget/component/index.tsx b/app/client/src/widgets/MyDatePickerWidget/component/index.tsx new file mode 100644 index 000000000000..b4c80ce4e7bc --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/component/index.tsx @@ -0,0 +1,517 @@ +import React from "react"; +import styled from "styled-components"; +import { IntentColors } from "constants/DefaultTheme"; +import type { IRef, Alignment } from "@blueprintjs/core"; +import { ControlGroup, Classes } from "@blueprintjs/core"; +import type { ComponentProps } from "widgets/BaseComponent"; +import moment from "moment"; +import "@blueprintjs/datetime/lib/css/blueprint-datetime.css"; +import type { DatePickerType } from "../constants"; +import type { TimePrecision } from "../constants"; +import type { TextSize } from "constants/WidgetConstants"; +import { Colors } from "constants/Colors"; +import { ISO_DATE_FORMAT } from "constants/WidgetValidation"; +import ErrorTooltip from "components/editorComponents/ErrorTooltip"; +import { + createMessage, + DATE_WIDGET_DEFAULT_VALIDATION_ERROR, +} from "ee/constants/messages"; +import { LabelPosition } from "components/constants"; +import { parseDate } from "./utils"; +import { lightenColor, PopoverStyles } from "widgets/WidgetUtils"; +import LabelWithTooltip, { + labelLayoutStyles, +} from "widgets/components/LabelWithTooltip"; + +const DATEPICKER_POPUP_CLASSNAME = "datepickerwidget-popup"; +import { required } from "utils/validation/common"; +import DatePicker, { DateObject } from "react-multi-date-picker"; + +import persian from "react-date-object/calendars/persian" +import persian_fa from "react-date-object/locales/persian_fa" +import TimePicker from "react-multi-date-picker/plugins/time_picker"; + + + +function hasFulfilledRequiredCondition( + isRequired: boolean | undefined, + value: any, +) { + // if the required condition is not enabled then it has fulfilled + if (!isRequired) return true; + + return !required(value); +} +const StyledControlGroup = styled(ControlGroup) <{ + isValid: boolean; + compactMode: boolean; + labelPosition?: LabelPosition; + borderRadius: string; + boxShadow?: string; + accentColor: string; +}>` + ${labelLayoutStyles} + + /** + When the label is on the left it is not center aligned + here set height to auto and not 100% because the input + has fixed height and stretch the container. + */ + ${({ labelPosition }) => { + if (labelPosition === LabelPosition.Left) { + return ` + height: auto !important; + align-items: stretch; + `; + } + }} + + &&& { + .${Classes.INPUT} { + color: var(--wds-color-text); + background: var(--wds-color-bg); + border-radius: ${({ borderRadius }) => borderRadius} !important; + box-shadow: ${({ boxShadow }) => `${boxShadow}`} !important; + border: 1px solid; + border-color: ${({ isValid }) => + !isValid + ? `var(--wds-color-border-danger);` + : `var(--wds-color-border);`}; + width: 100%; + height: 100%; + min-height: 32px; + align-items: center; + transition: none; + + &:active:not(:disabled) { + border-color: ${({ accentColor, isValid }) => + !isValid ? `var(--wds-color-border-danger)` : accentColor}; + } + + &:hover:not(:disabled) { + border-color: ${({ isValid }) => + !isValid + ? `var(--wds-color-border-danger-hover)` + : `var(--wds-color-border-hover)`}; + } + + &:focus:not(:disabled) { + outline: 0; + border: 1px solid; + border-color: ${({ accentColor, isValid }) => + !isValid + ? `var(--wds-color-border-danger-focus) !important` + : accentColor}; + box-shadow: ${({ accentColor, isValid }) => + `0px 0px 0px 2px ${isValid + ? lightenColor(accentColor) + : "var(--wds-color-border-danger-focus-light)" + } !important;`}; + } + } + + .${Classes.INPUT}:disabled { + background: var(--wds-color-bg-disabled); + color: var(--wds-color-text-disabled); + } + + .${Classes.INPUT}:not(:disabled)::placeholder { + color: var(--wds-color-text-light); + } + + .${Classes.INPUT}::placeholder { + color: var(--wds-color-text-disabled-light); + } + + .${Classes.INPUT_GROUP} { + display: block; + margin: 0; + } + + .${Classes.CONTROL_GROUP} { + justify-content: flex-start; + } + } + &&& { + input { + border: 1px solid; + border-color: ${(props) => + !props.isValid ? IntentColors.danger : Colors.HIT_GRAY}; + box-shadow: none; + font-size: ${(props) => props.theme.fontSizes[3]}px; + } + } +`; + +export const DateInputWrapper = styled.div<{ + compactMode: boolean; + labelPosition?: LabelPosition; +}>` + display: flex; + &&& { + flex-grow: 0; + } + width: 100%; +`; + +class DatePickerComponent extends React.Component< + DatePickerComponentProps, + DatePickerComponentState +> { + constructor(props: DatePickerComponentProps) { + super(props); + this.state = { + selectedDate: props.selectedDate, + }; + } + + componentDidUpdate(prevProps: DatePickerComponentProps) { + // prevProps.selectedDate can undefined and moment(undefined) returns now + if ( + this.props.selectedDate !== this.state.selectedDate && + (!moment(this.props.selectedDate).isSame( + moment(prevProps.selectedDate), + "seconds", + ) || + (!prevProps.selectedDate && this.props.selectedDate)) + ) { + this.setState({ selectedDate: this.props.selectedDate }); + } + } + + getValidDate = (date: string, format: string) => { + const _date = moment(date, format); + return _date.isValid() ? _date.toDate() : undefined; + }; + + getConditionalPopoverProps = (props: DatePickerComponentProps) => { + if (typeof props.isPopoverOpen === "boolean") { + return { + isOpen: props.isPopoverOpen, + }; + } + return {}; + }; + + render() { + const { + compactMode, + isDisabled, + isLoading, + isRequired, + labelAlignment, + labelPosition, + labelStyle, + labelText, + labelTextColor, + labelTextSize, + labelTooltip, + labelWidth, + } = this.props; + + const now = moment(); + const year = now.get("year"); + const minDate = this.props.minDate + ? new Date(this.props.minDate) + : now + .clone() + .set({ month: 0, date: 1, year: year - 100 }) + .toDate(); + const maxDate = this.props.maxDate + ? new Date(this.props.maxDate) + : now + .clone() + .set({ month: 11, date: 31, year: year + 100 }) + .toDate(); + const isValid = this.state.selectedDate + ? this.isValidDate(new Date(this.state.selectedDate)) + : true; + const value = + isValid && this.state.selectedDate + ? new Date(this.state.selectedDate) + : null; + + const hasFulfilledRequired = hasFulfilledRequiredCondition( + isRequired, + value, + ); + + const getInitialMonth = () => { + // None + if ( + !this.props.minDate && + !this.props.maxDate && + !this.state.selectedDate + ) { + return new Date(); + } + // Min-Max-Selcted + else if ( + this.props.minDate && + this.props.maxDate && + this.state.selectedDate + ) { + switch (true) { + case new Date(this.props.minDate) > new Date(this.state.selectedDate): + return new Date(this.props.minDate); + case new Date(this.props.minDate) < new Date(this.state.selectedDate): + return isValid + ? new Date(this.state.selectedDate) + : new Date(this.props.minDate); + default: + return new Date(); + } + } + // Min-Max-!Selcted + else if ( + this.props.minDate && + this.props.maxDate && + !this.state.selectedDate + ) { + switch (true) { + case new Date(this.props.minDate) > new Date(): + case new Date(this.props.maxDate) < new Date(): + return new Date(this.props.minDate); + default: + return new Date(); + } + } + // Min-Selcted + else if (this.props.minDate && this.state.selectedDate) { + switch (true) { + case new Date(this.props.minDate) > new Date(this.state.selectedDate): + return new Date(this.props.minDate); + case new Date(this.props.minDate) < new Date(this.state.selectedDate): + return new Date(this.state.selectedDate); + default: + return new Date(); + } + } + // Max-Selcted + else if (this.props.maxDate && this.state.selectedDate) { + switch (true) { + case new Date(this.props.maxDate) > new Date(this.state.selectedDate): + return new Date(this.state.selectedDate); + case new Date(this.props.maxDate) < new Date(this.state.selectedDate): + return new Date(this.props.maxDate); + default: + return new Date(); + } + } + // Selected + else if (this.state.selectedDate) { + return new Date(this.state.selectedDate); + } + // Min + else if (this.props.minDate) { + switch (true) { + case new Date(this.props.minDate) > new Date(): + return new Date(this.props.minDate); + default: + return new Date(); + } + } + // Max + else if (this.props.maxDate) { + switch (true) { + case new Date(this.props.maxDate) < new Date(): + return new Date(this.props.maxDate); + default: + return new Date(); + } + } else { + return new Date(); + } + }; + const initialMonth = getInitialMonth(); + + const timePrecisionHandler = () => { + if (this.props.timePrecision === 'None') return undefined + else if (this.props.timePrecision === 'minute') return [ + + ] + + else return [ + + ] + } + + console.log("test", this.props.selectedDate) + + return ( + { + e.stopPropagation(); + }} + > + {labelText && ( + + )} + + + this.props.onDateSelected(e.toDate())} + /> + + + + + ); + } + + isValidDate = (date: Date): boolean => { + let isValid = true; + const parsedCurrentDate = moment(date); + if (this.props.minDate) { + const parsedMinDate = moment(this.props.minDate); + if ( + this.props.minDate && + parsedMinDate.isValid() && + !parsedCurrentDate.isSame(parsedMinDate, "day") && + parsedCurrentDate.isBefore(parsedMinDate) + ) { + isValid = false; + } + } + if (this.props.maxDate) { + const parsedMaxDate = moment(this.props.maxDate); + if ( + isValid && + this.props.maxDate && + parsedMaxDate.isValid() && + !parsedCurrentDate.isSame(parsedMaxDate, "day") && + parsedCurrentDate.isAfter(parsedMaxDate) + ) { + isValid = false; + } + } + if (!isValid && this.props?.onDateOutOfRange) { + this.props.onDateOutOfRange(); + } + return isValid; + }; + + formatDate = (date: Date): string => { + const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT; + return moment(date).format(dateFormat); + }; + + parseDate = (dateStr: string): Date | null => { + //when user clears date field the value of dateStr will be empty + //and that means user is clearing date field + if (!dateStr) { + return null; + } else { + const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT; + return parseDate(dateStr, dateFormat); + } + }; + + /** + * checks if selelectedDate is null or not, + * sets state and calls props onDateSelected + * if its null, don't call onDateSelected + * update internal state while changing month/year to update calender + * + * @param selectedDate + */ + onDateSelected = (selectedDate: Date | null, isUserChange: boolean) => { + if (isUserChange) { + const { onDateSelected } = this.props; + const date = selectedDate ? selectedDate.toISOString() : ""; + this.setState({ + selectedDate: date, + }); + onDateSelected(date); + } + }; +} + +interface DatePickerComponentProps extends ComponentProps { + compactMode: boolean; + labelText: string; + labelPosition?: LabelPosition; + labelAlignment?: Alignment; + labelWidth?: number; + labelTextColor?: string; + labelTextSize?: TextSize; + labelStyle?: string; + dateFormat: string; + selectedDate?: string; + minDate?: string; + maxDate?: string; + timezone?: string; + datePickerType: DatePickerType; + isDisabled: boolean; + isDynamicHeightEnabled?: boolean; + onDateSelected: (selectedDate: string) => void; + isLoading: boolean; + withoutPortal?: boolean; + closeOnSelection: boolean; + shortcuts: boolean; + firstDayOfWeek?: number; + timePrecision: TimePrecision; + inputRef?: IRef; + borderRadius: string; + boxShadow?: string; + accentColor: string; + labelTooltip?: string; + onFocus?: () => void; + onBlur?: () => void; + onPopoverClosed?: (e: unknown) => void; + isPopoverOpen?: boolean; + onDateOutOfRange?: () => void; + isRequired?: boolean; +} + +interface DatePickerComponentState { + selectedDate?: string; +} + +export default DatePickerComponent; diff --git a/app/client/src/widgets/MyDatePickerWidget/component/utils.ts b/app/client/src/widgets/MyDatePickerWidget/component/utils.ts new file mode 100644 index 000000000000..422b2ac8b4bd --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/component/utils.ts @@ -0,0 +1,7 @@ +import moment from "moment"; + +export const parseDate = (dateStr: string, dateFormat: string): Date => { + const date = moment(dateStr, dateFormat); + if (date.isValid()) return date.toDate(); + else return moment().toDate(); +}; diff --git a/app/client/src/widgets/MyDatePickerWidget/constants.ts b/app/client/src/widgets/MyDatePickerWidget/constants.ts new file mode 100644 index 000000000000..94793419b40c --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/constants.ts @@ -0,0 +1,7 @@ +export type DatePickerType = "DATE_PICKER" | "DATE_RANGE_PICKER"; + +export enum TimePrecision { + NONE = "None", + MINUTE = "minute", + SECOND = "second", +} diff --git a/app/client/src/widgets/MyDatePickerWidget/icon.svg b/app/client/src/widgets/MyDatePickerWidget/icon.svg new file mode 100644 index 000000000000..b076daf8166d --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/icon.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/client/src/widgets/MyDatePickerWidget/index.ts b/app/client/src/widgets/MyDatePickerWidget/index.ts new file mode 100644 index 000000000000..b668e51d0683 --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/index.ts @@ -0,0 +1,3 @@ +import Widget from "./widget"; + +export default Widget; diff --git a/app/client/src/widgets/MyDatePickerWidget/widget/constants.ts b/app/client/src/widgets/MyDatePickerWidget/widget/constants.ts new file mode 100644 index 000000000000..d5d7145ca358 --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/widget/constants.ts @@ -0,0 +1,149 @@ +import { SubTextPosition } from "components/constants"; +import moment from "moment"; + +export const DateFormatOptions = [ + { + label: moment().format("YYYY-MM-DDTHH:mm:ss.sssZ"), + subText: "ISO 8601", + value: "YYYY-MM-DDTHH:mm:ss.sssZ", + }, + { + label: moment().format("YYYY/MM/DDTHH:mm:ss.sssZ"), + subText: "ISO 8601", + value: "YYYY/MM/DDTHH:mm:ss.sssZ", + }, + { + label: moment().format("YYYY-MM-DD HH:mm"), + subText: "YYYY-MM-DD HH:mm", + value: "YYYY-MM-DD HH:mm", + }, + { + label: moment().format("YYYY/MM/DD HH:mm"), + subText: "YYYY/MM/DD HH:mm", + value: "YYYY/MM/DD HH:mm", + }, + { + label: moment().format("YYYY-MM-DDTHH:mm:ss"), + subText: "YYYY-MM-DDTHH:mm:ss", + value: "YYYY-MM-DDTHH:mm:ss", + }, + { + label: moment().format("YYYY/MM/DDTHH:mm:ss"), + subText: "YYYY/MM/DDTHH:mm:ss", + value: "YYYY/MM/DDTHH:mm:ss", + }, + { + label: moment().format("YYYY-MM-DD hh:mm:ss A"), + subText: "YYYY-MM-DD hh:mm:ss A", + value: "YYYY-MM-DD hh:mm:ss A", + }, + { + label: moment().format("YYYY/MM/DD A hh:mm:ss"), + subText: "YYYY/MM/DD hh:mm:ss A", + value: "YYYY/MM/DD hh:mm:ss A", + }, + { + label: moment().format("DD/MM/YYYY HH:mm"), + subText: "DD/MM/YYYY HH:mm", + value: "DD/MM/YYYY HH:mm", + }, + { + label: moment().format("D MMMM, YYYY"), + subText: "D MMMM, YYYY", + value: "D MMMM, YYYY", + }, + { + label: moment().format("D MMMM YYYY"), + subText: "D MMMM YYYY", + value: "D MMMM YYYY", + }, + { + label: moment().format("YYYY MM DD"), + subText: "YYYY MM DD", + value: "YYYY MM DD", + }, + { + label: moment().format("H:mm A D MMMM, YYYY"), + subText: "H:mm A D MMMM, YYYY", + value: "H:mm A D MMMM, YYYY", + }, + { + label: moment().format("D MMMM YYYY H:mm A"), + subText: "D MMMM YYYY H:mm A", + value: "D MMMM YYYY H:mm A", + }, + { + label: moment().format("YYYY MM DD A H:mm"), + subText: "YYYY MM DD A H:mm", + value: "YYYY MM DD A H:mm", + }, + { + label: moment().format("DD MM YYYY H:mm A"), + subText: "DD MM YYYY H:mm A", + value: "DD MM YYYY H:mm A", + }, + { + label: moment().format("YYYY/MM/DD H:mm A"), + subText: "YYYY/MM/DD H:mm A", + value: "YYYY/MM/DD H:mm A", + }, + + { + label: moment().format("YYYY-MM-DD H:mm A"), + subText: "YYYY-MM-DD H:mm A", + value: "YYYY-MM-DD H:mm A", + }, + { + label: moment().format("YYYY-MM-DD"), + subText: "YYYY-MM-DD", + value: "YYYY-MM-DD", + }, + { + label: moment().format("YYYY/MM/DD"), + subText: "YYYY/MM/DD", + value: "YYYY/MM/DD", + }, + { + label: moment().format("MM-DD-YYYY"), + subText: "MM-DD-YYYY", + value: "MM-DD-YYYY", + }, + { + label: moment().format("DD-MM-YYYY"), + subText: "DD-MM-YYYY", + value: "DD-MM-YYYY", + }, + { + label: moment().format("MM/DD/YYYY"), + subText: "MM/DD/YYYY", + value: "MM/DD/YYYY", + }, + { + label: moment().format("DD/MM/YYYY"), + subText: "DD/MM/YYYY", + value: "DD/MM/YYYY", + }, + { + label: moment().format("DD/MM/YY"), + subText: "DD/MM/YY", + value: "DD/MM/YY", + }, + { + label: moment().format("YY/MM/DD"), + subText: "YY/MM/DD", + value: "YY/MM/DD", + }, + { + label: moment().format("YY-MM-DD"), + subText: "YY-MM-DD", + value: "YY-MM-DD", + }, + { + label: moment().format("MM/DD/YY"), + subText: "MM/DD/YY", + value: "MM/DD/YY", + }, +].map((x) => ({ + ...x, + subTextPosition: SubTextPosition.BOTTOM, +})); diff --git a/app/client/src/widgets/MyDatePickerWidget/widget/derived.js b/app/client/src/widgets/MyDatePickerWidget/widget/derived.js new file mode 100644 index 000000000000..ea7457cee720 --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/widget/derived.js @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-unused-vars*/ +export default { + isValidDate: (props, moment, _) => { + const minDate = new Date(props.minDate); + const maxDate = new Date(props.maxDate); + const selectedDate = + props.selectedDate !== "" + ? moment(new Date(props.selectedDate)) + : props.selectedDate; + let dateValid = true; + if (!!props.minDate && !!props.maxDate) { + dateValid = !!selectedDate + ? selectedDate.isBetween(minDate, maxDate) + : !props.isRequired; + } else if (!!props.minDate) { + dateValid = !!selectedDate + ? selectedDate.isAfter(minDate) + : !props.isRequired; + } else if (!!props.maxDate) { + dateValid = !!selectedDate + ? selectedDate.isBefore(maxDate) + : !props.isRequired; + } else { + dateValid = props.isRequired ? !!selectedDate : true; + } + return dateValid; + }, + // +}; diff --git a/app/client/src/widgets/MyDatePickerWidget/widget/index.tsx b/app/client/src/widgets/MyDatePickerWidget/widget/index.tsx new file mode 100644 index 000000000000..4336d99ece98 --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/widget/index.tsx @@ -0,0 +1,744 @@ +import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; +import type { TextSize } from "constants/WidgetConstants"; +import React from "react"; +import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; +import BaseWidget from "widgets/BaseWidget"; +import DatePickerComponent from "../component"; + + + +import { ValidationTypes } from "constants/WidgetValidation"; +import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; +import type { DerivedPropertiesMap } from "WidgetProvider/factory"; + +import { Alignment } from "@blueprintjs/core"; +import { LabelPosition } from "components/constants"; +import type { SetterConfig, Stylesheet } from "entities/AppTheming"; +import { + isAutoHeightEnabledForWidget, + DefaultAutocompleteDefinitions, + isCompactMode, +} from "widgets/WidgetUtils"; +import type { DatePickerType } from "../constants"; +import { TimePrecision } from "../constants"; +import { DateFormatOptions } from "./constants"; +import derivedProperties from "./parseDerivedProperties"; +import { isAutoLayout } from "layoutSystems/autolayout/utils/flexWidgetUtils"; +import type { + AnvilConfig, + AutocompletionDefinitions, +} from "WidgetProvider/constants"; +import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants"; +import moment from "moment"; +import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; +import { DynamicHeight } from "utils/WidgetFeatures"; +import IconSVG from "../icon.svg"; +import type { + SnipingModeProperty, + PropertyUpdates, +} from "WidgetProvider/constants"; +import { WIDGET_TAGS } from "constants/WidgetConstants"; + +function allowedRange(value: any) { + const allowedValues = [0, 1, 2, 3, 4, 5, 6]; + const isValid = allowedValues.includes(Number(value)); + return { + isValid: isValid, + parsed: isValid ? Number(value) : 0, + messages: isValid + ? [ + { + name: "", + message: "", + }, + ] + : [ + { + name: "RangeError", + message: "Number should be between 0-6.", + }, + ], + }; +} +class DatePickerWidget extends BaseWidget { + static type = "JALALI_DATE_PICKER_WIDGET"; + + static getConfig() { + return { + name: "JalaliDatePicker", + iconSVG: IconSVG, + tags: [WIDGET_TAGS.INPUTS], + needsMeta: true, + searchTags: ["calendar"], + }; + } + + static getFeatures() { + return { + dynamicHeight: { + sectionIndex: 3, + defaultValue: DynamicHeight.FIXED, + active: true, + }, + }; + } + + static getDefaults() { + return { + isDisabled: false, + datePickerType: "DATE_PICKER", + rows: 7, + label: "Label", + labelPosition: LabelPosition.Top, + labelAlignment: Alignment.LEFT, + labelWidth: 5, + labelTextSize: "0.875rem", + dateFormat: "YYYY-MM-DD HH:mm", + columns: 20, + widgetName: "DatePicker", + defaultDate: moment().toISOString(), + minDate: "1920-12-31T18:30:00.000Z", + maxDate: "2121-12-31T18:29:00.000Z", + version: 2, + isRequired: false, + closeOnSelection: true, + shortcuts: false, + firstDayOfWeek: 0, + timePrecision: TimePrecision.MINUTE, + animateLoading: true, + responsiveBehavior: ResponsiveBehavior.Fill, + minWidth: FILL_WIDGET_MIN_WIDTH, + }; + } + + static getMethods() { + return { + getSnipingModeUpdates: ( + propValueMap: SnipingModeProperty, + ): PropertyUpdates[] => { + return [ + { + propertyPath: "defaultDate", + propertyValue: propValueMap.data, + isDynamicPropertyPath: true, + }, + ]; + }, + }; + } + + static getAutoLayoutConfig() { + return { + disabledPropsDefaults: { + labelPosition: LabelPosition.Top, + labelTextSize: "0.875rem", + }, + defaults: { + rows: 6.6, + }, + autoDimension: { + height: true, + }, + widgetSize: [ + { + viewportMinWidth: 0, + configuration: () => { + return { + minWidth: "120px", + }; + }, + }, + ], + disableResizeHandles: { + vertical: true, + }, + }; + } + + static getAnvilConfig(): AnvilConfig | null { + return { + isLargeWidget: false, + widgetSize: { + maxHeight: {}, + maxWidth: {}, + minHeight: {}, + minWidth: { base: "120px" }, + }, + }; + } + + static getAutocompleteDefinitions(): AutocompletionDefinitions { + return { + "!doc": + "Datepicker is used to capture the date and time from a user. It can be used to filter data base on the input date range as well as to capture personal information such as date of birth", + "!url": "https://docs.appsmith.com/widget-reference/datepicker", + isVisible: DefaultAutocompleteDefinitions.isVisible, + selectedDate: "string", + formattedDate: "string", + isDisabled: "bool", + }; + } + + static getSetterConfig(): SetterConfig { + return { + __setters: { + setVisibility: { + path: "isVisible", + type: "boolean", + }, + setDisabled: { + path: "isDisabled", + type: "boolean", + }, + setRequired: { + path: "isRequired", + type: "boolean", + }, + setValue: { + path: "defaultDate", + type: "string", + accessor: "selectedDate", + }, + }, + }; + } + + static getPropertyPaneContentConfig() { + return [ + { + sectionName: "Data", + children: [ + { + helpText: "Sets the format of the selected date", + propertyName: "dateFormat", + label: "Date format", + controlType: "DROP_DOWN", + isJSConvertible: true, + optionWidth: "340px", + options: DateFormatOptions, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + hideSubText: true, + }, + { + propertyName: "defaultDate", + label: "Default Date", + helpText: + "Sets the default date of the widget. The date is updated if the default date changes", + controlType: "DATE_PICKER", + placeholderText: "Enter Default Date", + useValidationMessage: true, + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.DATE_ISO_STRING }, + }, + { + propertyName: "firstDayOfWeek", + label: "First Day Of Week", + helpText: "Defines the first day of the week for calendar", + controlType: "INPUT_TEXT", + defaultValue: "0", + inputType: "INTEGER", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fn: allowedRange, + expected: { + type: "0 : sunday\n1 : monday\n2 : tuesday\n3 : wednesday\n4 : thursday\n5 : friday\n6 : saturday", + example: "0", + autocompleteDataType: AutocompleteDataType.STRING, + }, + }, + }, + }, + { + propertyName: "timePrecision", + label: "Time Precision", + controlType: "DROP_DOWN", + helpText: "Sets the different time picker or hide.", + defaultValue: TimePrecision.MINUTE, + options: [ + { + label: "None", + value: TimePrecision.NONE, + }, + { + label: "Minute", + value: TimePrecision.MINUTE, + }, + { + label: "Second", + value: TimePrecision.SECOND, + }, + ], + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.TEXT, + params: { + allowedValues: [ + TimePrecision.NONE, + TimePrecision.MINUTE, + TimePrecision.SECOND, + ], + default: TimePrecision.MINUTE, + }, + }, + }, + ], + }, + { + sectionName: "Label", + children: [ + { + helpText: "Sets the label text of the widget", + propertyName: "label", + label: "Text", + controlType: "INPUT_TEXT", + placeholderText: "Enter label text", + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + helpText: "Sets the label position of the widget", + propertyName: "labelPosition", + label: "Position", + controlType: "ICON_TABS", + fullWidth: true, + hidden: isAutoLayout, + options: [ + { label: "Auto", value: LabelPosition.Auto }, + { label: "Left", value: LabelPosition.Left }, + { label: "Top", value: LabelPosition.Top }, + ], + defaultValue: LabelPosition.Top, + isBindProperty: false, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + helpText: "Sets the label alignment of the widget", + propertyName: "labelAlignment", + label: "Alignment", + controlType: "LABEL_ALIGNMENT_OPTIONS", + fullWidth: false, + options: [ + { + startIcon: "align-left", + value: Alignment.LEFT, + }, + { + startIcon: "align-right", + value: Alignment.RIGHT, + }, + ], + isBindProperty: false, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + hidden: (props: DatePickerWidget2Props) => + props.labelPosition !== LabelPosition.Left, + dependencies: ["labelPosition"], + }, + { + helpText: + "Sets the label width of the widget as the number of columns", + propertyName: "labelWidth", + label: "Width (in columns)", + controlType: "NUMERIC_INPUT", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + min: 0, + validation: { + type: ValidationTypes.NUMBER, + params: { + natural: true, + }, + }, + hidden: (props: DatePickerWidget2Props) => + props.labelPosition !== LabelPosition.Left, + dependencies: ["labelPosition"], + }, + ], + }, + { + sectionName: "Validation", + children: [ + { + propertyName: "isRequired", + label: "Required", + helpText: "Makes input to the widget mandatory", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "minDate", + label: "Min Date", + helpText: "Defines the min date for this widget", + controlType: "DATE_PICKER", + useValidationMessage: true, + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.DATE_ISO_STRING }, + }, + { + propertyName: "maxDate", + label: "Max Date", + helpText: "Defines the max date for this widget", + controlType: "DATE_PICKER", + useValidationMessage: true, + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.DATE_ISO_STRING }, + }, + ], + }, + { + sectionName: "General", + children: [ + { + helpText: "Show help text or details about current selection", + propertyName: "labelTooltip", + label: "Tooltip", + controlType: "INPUT_TEXT", + placeholderText: "Add tooltip text here", + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + propertyName: "isVisible", + label: "Visible", + helpText: "Controls the visibility of the widget", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "isDisabled", + label: "Disabled", + helpText: "Disables input to this widget", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + ], + }, + { + sectionName: "Events", + children: [ + { + propertyName: "onDateSelected", + label: "onDateSelected", + helpText: "when a date is selected in the calendar", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + }, + // { + // propertyName: "onFocus", + // label: "onFocus", + // helpText: "when the date picker receives focus", + // controlType: "ACTION_SELECTOR", + // isJSConvertible: true, + // isBindProperty: true, + // isTriggerProperty: true, + // }, + // { + // propertyName: "onBlur", + // label: "onBlur", + // helpText: "when the date picker loses focus", + // controlType: "ACTION_SELECTOR", + // isJSConvertible: true, + // isBindProperty: true, + // isTriggerProperty: true, + // }, + ], + }, + ]; + } + + static getPropertyPaneStyleConfig() { + return [ + { + sectionName: "Label styles", + children: [ + { + propertyName: "labelTextColor", + label: "Font color", + helpText: "Control the color of the label associated", + controlType: "COLOR_PICKER", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + propertyName: "labelTextSize", + label: "Font size", + helpText: "Control the font size of the label associated", + controlType: "DROP_DOWN", + defaultValue: "0.875rem", + hidden: isAutoLayout, + options: [ + { + label: "S", + value: "0.875rem", + subText: "0.875rem", + }, + { + label: "M", + value: "1rem", + subText: "1rem", + }, + { + label: "L", + value: "1.25rem", + subText: "1.25rem", + }, + { + label: "XL", + value: "1.875rem", + subText: "1.875rem", + }, + { + label: "XXL", + value: "3rem", + subText: "3rem", + }, + { + label: "3XL", + value: "3.75rem", + subText: "3.75rem", + }, + ], + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + propertyName: "labelStyle", + label: "Emphasis", + helpText: "Control if the label should be bold or italics", + controlType: "BUTTON_GROUP", + options: [ + { + icon: "text-bold", + value: "BOLD", + }, + { + icon: "text-italic", + value: "ITALIC", + }, + ], + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + ], + }, + { + sectionName: "Border and shadow", + children: [ + { + propertyName: "accentColor", + label: "Accent color", + controlType: "COLOR_PICKER", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + invisible: true, + }, + { + propertyName: "borderRadius", + label: "Border radius", + helpText: + "Rounds the corners of the icon button's outer border edge", + controlType: "BORDER_RADIUS_OPTIONS", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + propertyName: "boxShadow", + label: "Box shadow", + helpText: + "Enables you to cast a drop shadow from the frame of the widget", + controlType: "BOX_SHADOW_OPTIONS", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + ], + }, + ]; + } + + static getDerivedPropertiesMap(): DerivedPropertiesMap { + return { + isValid: `{{(()=>{${derivedProperties.isValidDate}})()}}`, + selectedDate: `{{ this.value ? moment(this.value).toISOString() : "" }}`, + formattedDate: `{{ this.value ? moment(this.value).format(this.dateFormat) : "" }}`, + }; + } + + static getDefaultPropertiesMap(): Record { + return { + value: "defaultDate", + }; + } + + static getMetaPropertiesMap(): Record { + return { + value: undefined, + isDirty: false, + }; + } + + static getStylesheetConfig(): Stylesheet { + return { + accentColor: "{{appsmith.theme.colors.primaryColor}}", + borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", + boxShadow: "none", + }; + } + + componentDidUpdate(prevProps: DatePickerWidget2Props): void { + if ( + this.props.defaultDate !== prevProps.defaultDate && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + + getWidgetView() { + const { componentHeight } = this.props; + + return ( + + ); + } + + onDateSelected = (selectedDate: string) => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + + this.props.updateWidgetMetaProperty("value", selectedDate, { + triggerPropertyName: "onDateSelected", + dynamicString: this.props.onDateSelected, + event: { + type: EventType.ON_DATE_SELECTED, + }, + }); + }; + + onFocus = () => { + if (this.props.onFocus) + super.executeAction({ + triggerPropertyName: "onFocus", + dynamicString: this.props.onFocus, + event: { + type: EventType.ON_FOCUS, + }, + }); + }; + + onBlur = () => { + if (this.props.onBlur) + super.executeAction({ + triggerPropertyName: "onBlur", + dynamicString: this.props.onBlur, + event: { + type: EventType.ON_BLUR, + }, + }); + }; +} + +export interface DatePickerWidget2Props extends WidgetProps { + defaultDate: string; + selectedDate: string; + formattedDate: string; + isDisabled: boolean; + dateFormat: string; + label: string; + labelPosition?: LabelPosition; + labelAlignment?: Alignment; + labelWidth?: number; + labelTextColor?: string; + labelTextSize?: TextSize; + labelStyle?: string; + datePickerType: DatePickerType; + onDateSelected?: string; + onDateRangeSelected?: string; + maxDate: string; + minDate: string; + isRequired?: boolean; + closeOnSelection: boolean; + shortcuts: boolean; + backgroundColor: string; + borderRadius: string; + boxShadow?: string; + accentColor: string; + firstDayOfWeek?: number; + timePrecision: TimePrecision; + onFocus?: string; + onBlur?: string; + labelComponentWidth?: number; +} + +export default DatePickerWidget; diff --git a/app/client/src/widgets/MyDatePickerWidget/widget/parseDerivedProperties.ts b/app/client/src/widgets/MyDatePickerWidget/widget/parseDerivedProperties.ts new file mode 100644 index 000000000000..4c2df3a5c0b1 --- /dev/null +++ b/app/client/src/widgets/MyDatePickerWidget/widget/parseDerivedProperties.ts @@ -0,0 +1,36 @@ +// @ts-expect-error: loader types not available +import widgetPropertyFns from "!!raw-loader!./derived.js"; + +// TODO(abhinav): +// Add unit test cases +// Handle edge cases +// Error out on wrong values +const derivedProperties: any = {}; +// const regex = /(\w+):\s?\(props\)\s?=>\s?{([\w\W]*?)},/gim; +const regex = + /(\w+):\s?\(props, moment, _\)\s?=>\s?{([\w\W\n]*?)},\n?\s+?\/\//gim; + +let m; + +while ((m = regex.exec(widgetPropertyFns)) !== null) { + // This is necessary to avoid infinite loops with zero-width matches + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + + let key = ""; + // The result can be accessed through the `m`-variable. + m.forEach((match, groupIndex) => { + if (groupIndex === 1) { + key = match; + } + if (groupIndex === 2) { + derivedProperties[key] = match + .trim() + .replace(/\n/g, "") + .replace(/props\./g, "this."); + } + }); +} + +export default derivedProperties; diff --git a/app/client/src/widgets/TextWidget/component/index.tsx b/app/client/src/widgets/TextWidget/component/index.tsx index e4920b609167..e657fa0c8a9b 100644 --- a/app/client/src/widgets/TextWidget/component/index.tsx +++ b/app/client/src/widgets/TextWidget/component/index.tsx @@ -110,6 +110,7 @@ type StyledTextProps = React.PropsWithChildren<{ fontStyle?: string; fontSize?: TextSize; minHeight?: number; + rtl?: boolean; }>; export const StyledText = styled(Text)` @@ -153,6 +154,7 @@ export const StyledText = styled(Text)` line-height: 1.2; white-space: pre-wrap; text-align: ${(props) => props.textAlign.toLowerCase()}; + direction: ${(props) => props.rtl ? "rtl" : "ltr"}; } ${({ minHeight }) => ` span { @@ -224,6 +226,7 @@ export interface TextComponentProps extends ComponentProps { borderWidth?: number; overflow: OverflowTypes; minHeight?: number; + rtl?: boolean; } interface State { @@ -305,6 +308,7 @@ class TextComponent extends React.Component { textAlign, textColor, truncateButtonColor, + rtl, } = this.props; return ( @@ -313,6 +317,7 @@ class TextComponent extends React.Component { fontFamily={fontFamily !== "System Default" ? fontFamily : undefined} > { isTriggerProperty: false, validation: { type: ValidationTypes.BOOLEAN }, }, + { + propertyName: "rtl", + label: "Enable RTL", + helpText: "Enables right to left text direction", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, ], }, ]; @@ -302,6 +312,14 @@ class TextWidget extends BaseWidget { label: "Ubuntu", value: "Ubuntu", }, + { + label: "YekanBakh", + value: "YekanBakh", + }, + { + label: "VazirMatn", + value: "VazirMatn" + }, ], defaultValue: "System Default", isJSConvertible: true, @@ -571,6 +589,7 @@ class TextWidget extends BaseWidget { this.props.truncateButtonColor || this.props.accentColor } widgetId={this.props.widgetId} + rtl={this.props.rtl} /> ); @@ -614,6 +633,7 @@ export interface TextWidgetProps extends WidgetProps, TextStyles { borderColor?: Color; borderWidth?: number; overflow: OverflowTypes; + rtl?: boolean; } export default TextWidget; diff --git a/app/client/src/widgets/index.ts b/app/client/src/widgets/index.ts index c1e4cb2e52ca..c3ff5e03da45 100644 --- a/app/client/src/widgets/index.ts +++ b/app/client/src/widgets/index.ts @@ -88,6 +88,7 @@ import { WDSNumberInputWidget } from "modules/ui-builder/ui/wds/WDSNumberInputWi import { WDSMultilineInputWidget } from "modules/ui-builder/ui/wds/WDSMultilineInputWidget"; import { WDSSelectWidget } from "modules/ui-builder/ui/wds/WDSSelectWidget"; import { EEWDSWidgets } from "ee/modules/ui-builder/ui/wds"; +import MyDatePickerWidget from './MyDatePickerWidget'; const LegacyWidgets = [ CanvasWidget, @@ -140,6 +141,7 @@ const LegacyWidgets = [ CodeScannerWidget, ListWidgetV2, ExternalWidget, + MyDatePickerWidget, ]; const DeprecatedWidgets = [ diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 5f8a7a105b65..dcb499283898 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -12858,7 +12858,7 @@ __metadata: cypress-xpath: ^1.6.0 d3-geo: ^3.1.0 date-fns: 2.30.0 - dayjs: ^1.10.6 + dayjs: ^1.11.10 deep-diff: ^1.0.2 diff: ^5.0.0 dotenv: ^8.1.0 @@ -12954,6 +12954,7 @@ __metadata: react-json-view: ^1.21.3 react-media-recorder: ^1.6.1 react-modal: ^3.15.1 + react-multi-date-picker: ^4.5.2 react-page-visibility: ^7.0.0 react-player: ^2.3.1 react-qr-barcode-scanner: ^1.0.6 @@ -16349,10 +16350,10 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.10.4, dayjs@npm:^1.10.6": - version: 1.11.7 - resolution: "dayjs@npm:1.11.7" - checksum: 5003a7c1dd9ed51385beb658231c3548700b82d3548c0cfbe549d85f2d08e90e972510282b7506941452c58d32136d6362f009c77ca55381a09c704e9f177ebb +"dayjs@npm:^1.10.4, dayjs@npm:^1.11.10": + version: 1.11.10 + resolution: "dayjs@npm:1.11.10" + checksum: a6b5a3813b8884f5cd557e2e6b7fa569f4c5d0c97aca9558e38534af4f2d60daafd3ff8c2000fed3435cfcec9e805bcebd99f90130c6d1c5ef524084ced588c4 languageName: node linkType: hard @@ -28353,6 +28354,13 @@ __metadata: languageName: node linkType: hard +"react-date-object@npm:^2.1.8": + version: 2.1.8 + resolution: "react-date-object@npm:2.1.8" + checksum: 8c4ecc7f849efccc483fd78411d88971b48b2e695f1fe331ff83a0e1d87299f3c9d073bd7efba0dd542f5500923ed05dc65af0ea03e1bb344918e3e9e998d703 + languageName: node + linkType: hard + "react-datepicker@npm:^4.10.0": version: 4.11.0 resolution: "react-datepicker@npm:4.11.0" @@ -28512,6 +28520,16 @@ __metadata: languageName: node linkType: hard +"react-element-popper@npm:^2.1.6": + version: 2.1.6 + resolution: "react-element-popper@npm:2.1.6" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 0140c2f78ea5b4ef92d2711ca66d96513403ffab62063cd3bdfee662d5424ceaf50f08a74da3a5e370b3160b1e345d144afa834f90bea5e06fa23c1682660ac4 + languageName: node + linkType: hard + "react-element-to-jsx-string@npm:^15.0.0": version: 15.0.0 resolution: "react-element-to-jsx-string@npm:15.0.0" @@ -28697,6 +28715,19 @@ __metadata: languageName: node linkType: hard +"react-multi-date-picker@npm:^4.5.2": + version: 4.5.2 + resolution: "react-multi-date-picker@npm:4.5.2" + dependencies: + react-date-object: ^2.1.8 + react-element-popper: ^2.1.6 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 6ed0e03f2c07d409c1d0db040a94d6aa632f81b62d017c0167b90d51a7587ade8b29abcd520ef0d41a93253d37607e8275cff9f4c9c748875fafb49f4e9efc99 + languageName: node + linkType: hard + "react-onclickoutside@npm:^6.12.2": version: 6.13.0 resolution: "react-onclickoutside@npm:6.13.0" diff --git a/app/server/appsmith-server/src/main/resources/system-themes.json b/app/server/appsmith-server/src/main/resources/system-themes.json index d3e61bbfe67f..d6d354886f3f 100644 --- a/app/server/appsmith-server/src/main/resources/system-themes.json +++ b/app/server/appsmith-server/src/main/resources/system-themes.json @@ -34,7 +34,9 @@ "Open Sans", "Roboto", "Rubik", - "Ubuntu" + "Ubuntu", + "YekanBakh", + "VazirMatn" ] } }, @@ -410,7 +412,7 @@ "appBoxShadow": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)" }, "fontFamily": { - "appFont": "System Default" + "appFont": "VazirMatn" } } }, @@ -449,7 +451,9 @@ "Open Sans", "Roboto", "Rubik", - "Ubuntu" + "Ubuntu", + "YekanBakh", + "VazirMatn" ] } }, @@ -821,7 +825,7 @@ "appBoxShadow": "none" }, "fontFamily": { - "appFont": "System Default" + "appFont": "VazirMatn" } } }, @@ -860,7 +864,9 @@ "Open Sans", "Roboto", "Rubik", - "Ubuntu" + "Ubuntu", + "YekanBakh", + "VazirMatn" ] } }, @@ -1231,7 +1237,7 @@ "appBoxShadow": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)" }, "fontFamily": { - "appFont": "Rubik" + "appFont": "VazirMatn" } } }, @@ -1270,7 +1276,9 @@ "Open Sans", "Roboto", "Rubik", - "Ubuntu" + "Ubuntu", + "YekanBakh", + "VazirMatn" ] } }, @@ -1637,7 +1645,7 @@ "appBoxShadow": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)" }, "fontFamily": { - "appFont": "Rubik" + "appFont": "VazirMatn" } } }, @@ -1676,7 +1684,9 @@ "Open Sans", "Roboto", "Rubik", - "Ubuntu" + "Ubuntu", + "YekanBakh", + "VazirMatn" ] } }, @@ -2449,7 +2459,7 @@ "appBoxShadow": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)" }, "fontFamily": { - "appFont": "Inter" + "appFont": "VazirMatn" } } }, @@ -2488,7 +2498,9 @@ "Open Sans", "Roboto", "Rubik", - "Ubuntu" + "Ubuntu", + "YekanBakh", + "VazirMatn" ] } }, @@ -2855,7 +2867,7 @@ "appBoxShadow": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)" }, "fontFamily": { - "appFont": "Nunito Sans" + "appFont": "VazirMatn" } } }, @@ -2894,7 +2906,9 @@ "Open Sans", "Roboto", "Rubik", - "Ubuntu" + "Ubuntu", + "YekanBakh", + "VazirMatn" ] } }, @@ -3261,7 +3275,7 @@ "appBoxShadow": "none" }, "fontFamily": { - "appFont": "Nunito Sans" + "appFont": "VazirMatn" } } }, @@ -3301,7 +3315,9 @@ "Open Sans", "Roboto", "Rubik", - "Ubuntu" + "Ubuntu", + "YekanBakh", + "VazirMatn" ] } }, @@ -3677,7 +3693,7 @@ "appBoxShadow": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)" }, "fontFamily": { - "appFont": "Nunito Sans" + "appFont": "VazirMatn" } } }