From cab8412976754291e57488c84c05cdc1f6148b54 Mon Sep 17 00:00:00 2001 From: Emily-Ke <50299119+Emily-Ke@users.noreply.github.com> Date: Fri, 29 Jul 2022 14:00:14 -0700 Subject: [PATCH] Release v1.0.2 (#437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Automatic Docker builds on tagged releases and PRs with multi-platform support * Remove nightly action, which was merged into build * Allow all files to be copied in * Dex 761 social group site settings (#419) * DEX-761 flesh out social group form except for multipleInGroup * DEX-761 begin fleshing out the switch * DEX-761 continue with fleshing out the switch * DEX-761 add useEffect * DEX-761 rename the role component, clean up, and remove the unnecessary useState * DEX-761 internationalize and add description as caption * DEX-761 respond to code review feedback * DEX-761 remove unecessary flexbox div * DEX-761 handle the case where a guidless role makes it into the SocialGroupRole component * move server status components to siteStatus directory * Change server status text to site status * Add server status SuperText keys, prevent 'hide' prop from passing to Text * Internationalize aria labels in site status components * dex 1117 - Replace progress with pipeline_status in Asset Group (#420) * Replace asset group progress with pipeline_status * Change px to number and abstract showAlert in AssetGroup * Add public AGS page (#416) * Add public AGS page * Remove unused property * Fixes * Rename things * Remove unused translation key * use new terms (#418) * begin release procedure (#424) * begin release procedure * clarify about merge conflicts * respond to feedback * respond to code review feedback * Update release procedure doc * Run on sentence Co-authored-by: Ben Scheiner * DEX-922 implement stickyHeader and add a meaningful maxHeight to ever… (#430) * DEX-922 implement stickyHeader and add a meaningful maxHeight to every DataDisplay. Also add horizontalScroll * DEX-922 remove seemingly unnecessary horizontal scroll prop * DEX-922 respond to code review feedback. Remove default maxHeight for Card component and replaced with optional styles, which was then implemented for ImageCard and StatusCard. Remove maxHeight from Card in relationshipCard, remove unused paperStyles from DataDisplay. * DEX-922 respond to code review feedback pt 2 * DEX-922 add tableContainerStyles to DataDisplay and remove maxHeight * DEX-922 remove tableContainerStyles from SightingsDisplay and fix a few little things * DEX-922 final little fixes * Compute new form state from previous state in Settings (#433) * 922 follow up (#435) * DEX-922 print the collaboration data to make the stackBlitz question easier * DEX-922 I do not understand why tableData was undefined, but cool, codex, I can play this game with you * DEX-922 try disablePortal * DEX-922 change the z-index * DEX-922 clean up * DEX-922 respond to code review feedback * Update version to 1.0.2 (#436) Co-authored-by: Jason Parham Co-authored-by: Mark Co-authored-by: Ben Scheiner --- .dockerfiles/www/default.conf | 19 ++ .github/workflows/build.yml | 167 ++++++++++++++++-- .github/workflows/nightly.yml | 49 ----- Dockerfile | 15 ++ docs/contribution-guide.md | 22 ++- docs/release-procedure.md | 33 ++++ locale/en.json | 23 ++- package.json | 2 +- prod/Dockerfile | 14 -- prod/README.md | 1 - prod/default.conf | 92 ---------- scripts/build.npm.sh | 4 + scripts/buildx.docker.sh | 57 ++++++ src/AuthenticatedSwitch.jsx | 8 +- .../AuthenticatedAppHeader/ActionsPane.jsx | 22 ++- src/components/IndividualSelector.jsx | 2 +- src/components/SuperText.jsx | 2 +- src/components/cards/Card.jsx | 5 +- src/components/cards/CollaborationsCard.jsx | 15 +- src/components/cards/CooccurrenceCard.jsx | 1 + src/components/cards/EncountersCard.jsx | 30 ++-- src/components/cards/RelationshipsCard.jsx | 2 +- src/components/cards/SightingsCard.jsx | 30 ++-- src/components/dataDisplays/DataDisplay.jsx | 12 +- ....jsx => ElasticsearchSightingsDisplay.jsx} | 2 +- .../dataDisplays/HoustonSightingsDisplay.jsx | 101 +++++++++++ .../dataDisplays/RelationshipDisplay.jsx | 1 + src/constants/queryKeys.js | 1 + src/hooks/useOptions.js | 3 +- src/models/assetGroupSighting/usePublicAGS.js | 9 + .../individual/useIndividualFieldSchemas.js | 6 +- .../sighting/useSightingFieldSchemas.js | 2 +- src/pages/assetGroup/AGSTable.jsx | 1 + src/pages/assetGroup/AssetGroup.jsx | 53 +++--- src/pages/fieldManagement/FieldManagement.jsx | 8 +- .../settings/CategoryTable.jsx | 1 + .../settings/CustomFieldTable.jsx | 14 +- .../settings/DefaultFieldTable.jsx | 29 ++- .../settings/constants/customFieldTypes.js | 4 +- .../RelationshipEditor.jsx | 2 +- .../SocialGroupComponents/SocialGroupRole.jsx | 108 +++++++++++ .../SocialGroupsEditor.jsx | 115 ++++++++++++ .../defaultFieldComponents/SpeciesEditor.jsx | 2 +- src/pages/generalSettings/GeneralSettings.jsx | 4 +- src/pages/match/ImageCard.jsx | 2 +- src/pages/match/MatchCandidatesTable.jsx | 4 +- src/pages/match/QueryAnnotationsTable.jsx | 1 + .../PendingCitizenScienceSightings.jsx | 47 +++++ src/pages/settings/Settings.jsx | 8 +- src/pages/sighting/SearchSightings.jsx | 4 +- src/pages/sighting/statusCard/StatusCard.jsx | 2 +- .../SiteStatus.jsx} | 15 +- .../components/LegendItem.jsx | 9 +- .../components/SummaryCard.jsx | 0 .../components/WaffleSquare.jsx | 9 +- src/pages/splashSettings/SplashSettings.jsx | 4 +- src/pages/userManagement/UserEditTable.jsx | 1 + .../UserManagerCollaborationEditTable.jsx | 7 +- 58 files changed, 908 insertions(+), 298 deletions(-) create mode 100644 .dockerfiles/www/default.conf delete mode 100644 .github/workflows/nightly.yml create mode 100644 Dockerfile create mode 100644 docs/release-procedure.md delete mode 100644 prod/Dockerfile delete mode 100644 prod/README.md delete mode 100644 prod/default.conf create mode 100755 scripts/build.npm.sh create mode 100755 scripts/buildx.docker.sh rename src/components/dataDisplays/{SightingsDisplay.jsx => ElasticsearchSightingsDisplay.jsx} (98%) create mode 100644 src/components/dataDisplays/HoustonSightingsDisplay.jsx create mode 100644 src/models/assetGroupSighting/usePublicAGS.js create mode 100644 src/pages/fieldManagement/settings/defaultFieldComponents/SocialGroupComponents/SocialGroupRole.jsx create mode 100644 src/pages/fieldManagement/settings/defaultFieldComponents/SocialGroupsEditor.jsx create mode 100644 src/pages/pendingCitizenScienceSightings/PendingCitizenScienceSightings.jsx rename src/pages/{serverStatus/ServerStatus.jsx => siteStatus/SiteStatus.jsx} (97%) rename src/pages/{serverStatus => siteStatus}/components/LegendItem.jsx (72%) rename src/pages/{serverStatus => siteStatus}/components/SummaryCard.jsx (100%) rename src/pages/{serverStatus => siteStatus}/components/WaffleSquare.jsx (93%) diff --git a/.dockerfiles/www/default.conf b/.dockerfiles/www/default.conf new file mode 100644 index 000000000..6c7199296 --- /dev/null +++ b/.dockerfiles/www/default.conf @@ -0,0 +1,19 @@ +server { + listen 80; + listen [::]:80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eeb05336f..65e5cdecc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,17 +1,22 @@ name: Build + on: - schedule: - - cron: '0 0 * * *' # Every day at midnight - push: pull_request: - paths: - - '.github/workflows/build.yml' - # Build when a fundamental package change has occured - - 'package.json' + branches: + - main + - develop + push: + branches: + - main + - develop + tags: + - v* + schedule: + - cron: '0 16 * * *' # Every day at 16:00 UTC (~09:00 PT) jobs: build: - name: Build the frontend releasable package + name: Build releasable package runs-on: ubuntu-latest steps: @@ -22,11 +27,8 @@ jobs: with: node-version: '15.x' - - name: Install dependencies - run: npm install --legacy-peer-deps - - - name: Compile build - run: npm run build -- --env=houston=relative + - name: Install dependencies and compile + run: ./scripts/build.npm.sh - name: Package the build run: | @@ -39,3 +41,142 @@ jobs: path: codex-frontend.tar.gz # default: # retention-days: 90 + + deploy: + name: Docker image build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + if: github.event_name == 'schedule' + with: + ref: develop + + - uses: actions/checkout@v2 + if: github.event_name != 'schedule' + + - uses: docker/setup-qemu-action@v1 + name: Set up QEMU + id: qemu + with: + image: tonistiigi/binfmt:latest + platforms: all + + - uses: docker/setup-buildx-action@v1 + name: Set up Docker Buildx + id: buildx + + - name: Available platforms + run: echo ${{ steps.buildx.outputs.platforms }} + + # Build images + - name: Build Codex + run: | + ./scripts/buildx.docker.sh + + # Log into container registries + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: wildmebot + password: ${{ secrets.WBIA_WILDMEBOT_DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + # Push tagged image (version tag + latest) to registries + - name: Tagged Docker Hub + if: ${{ github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') }} + run: | + VERSION=$(echo ${GITHUB_REF} | sed 's#.*/v##') + ./scripts/buildx.docker.sh -t ${VERSION} + ./scripts/buildx.docker.sh -t latest + + - name: Tagged GHCR + if: ${{ github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') }} + run: | + VERSION=$(echo ${GITHUB_REF} | sed 's#.*/v##') + ./scripts/buildx.docker.sh -t ${VERSION} -r ghcr.io/wildmeorg/codex-frontend + ./scripts/buildx.docker.sh -t latest -r ghcr.io/wildmeorg/codex-frontend + + # Push stable image (main tag) to registries + - name: Stable Docker Hub + if: github.ref == 'refs/heads/main' + run: | + ./scripts/buildx.docker.sh -t main + + - name: Stable GHCR + if: github.ref == 'refs/heads/main' + run: | + ./scripts/buildx.docker.sh -t main -r ghcr.io/wildmeorg/codex-frontend + + # Push bleeding-edge image (develop tag) to registries + - name: Bleeding Edge Docker Hub + if: github.ref == 'refs/heads/develop' + run: | + ./scripts/buildx.docker.sh -t develop + + - name: Bleeding Edge GHCR + if: github.ref == 'refs/heads/develop' + run: | + ./scripts/buildx.docker.sh -t develop -r ghcr.io/wildmeorg/codex-frontend + + # Push nightly image (nightly tag) to registries + - name: Nightly Docker Hub + if: github.event_name == 'schedule' + run: | + ./scripts/buildx.docker.sh -t nightly + + - name: Nightly GHCR + if: github.event_name == 'schedule' + run: | + ./scripts/buildx.docker.sh -t nightly -r ghcr.io/wildmeorg/codex-frontend + + # Notify status in Slack + - name: Slack Notification + if: ${{ failure() && github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') }} + uses: rtCamp/action-slack-notify@master + env: + SLACK_CHANNEL: dev-houston + SLACK_COLOR: '#FF0000' + SLACK_ICON: https://avatars.slack-edge.com/2020-03-02/965719891842_db87aa21ccb61076f236_44.png + SLACK_MESSAGE: 'Tagged / Latest Docker build of Codex Frontend failed :sob:' + SLACK_USERNAME: "GitHub CI" + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/main' }} + uses: rtCamp/action-slack-notify@master + env: + SLACK_CHANNEL: dev-houston + SLACK_COLOR: '#FF0000' + SLACK_ICON: https://avatars.slack-edge.com/2020-03-02/965719891842_db87aa21ccb61076f236_44.png + SLACK_MESSAGE: 'Stable Docker build of Codex Frontend failed :sob:' + SLACK_USERNAME: "GitHub CI" + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + - name: Slack Notification + if: ${{ failure() && github.ref == 'refs/heads/develop' }} + uses: rtCamp/action-slack-notify@master + env: + SLACK_CHANNEL: dev-houston + SLACK_COLOR: '#FF0000' + SLACK_ICON: https://avatars.slack-edge.com/2020-03-02/965719891842_db87aa21ccb61076f236_44.png + SLACK_MESSAGE: 'Bleeding Edge Docker build of Codex Frontend failed :sob:' + SLACK_USERNAME: "GitHub CI" + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + - name: Slack Notification + if: ${{ failure() && github.event_name == 'schedule' }} + uses: rtCamp/action-slack-notify@master + env: + SLACK_CHANNEL: dev-houston + SLACK_COLOR: '#FF0000' + SLACK_ICON: https://avatars.slack-edge.com/2020-03-02/965719891842_db87aa21ccb61076f236_44.png + SLACK_MESSAGE: 'Nightly Docker build of Codex Frontend failed :sob:' + SLACK_USERNAME: "GitHub CI" + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index df6b81e2d..000000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Nightly -on: - schedule: - - cron: '0 16 * * *' # Every day at 16:00 UTC (~09:00 PT) - push: - paths: - - '.github/workflows/nightly.yml' - - 'prod/**' - pull_request: - paths: - - '.github/workflows/nightly.yml' - - 'prod/**' - -jobs: - build-image: - name: DevOps nightly image build - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - if: github.event_name == 'schedule' - with: - ref: develop - - - uses: actions/checkout@v2 - if: github.event_name != 'schedule' - - # Log into image registries - - name: Log into Docker Hub - run: echo "${{ secrets.WBIA_WILDMEBOT_DOCKER_HUB_TOKEN }}" | docker login -u wildmebot --password-stdin - # - name: Log into GitHub Packages - # run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin - - # Build images - - name: Build docker image - run: ./build-n-push.sh --tag dev - working-directory: ./prod - - # Notify status in Slack - - name: Slack Notification - if: failure() && github.event_name == 'schedule' - uses: rtCamp/action-slack-notify@master - env: - SLACK_CHANNEL: dev-houston - SLACK_COLOR: '#FF0000' - SLACK_ICON: https://avatars.slack-edge.com/2020-03-02/965719891842_db87aa21ccb61076f236_44.png - SLACK_MESSAGE: 'nightly build of the *codex-frontend image* failed :sob:' - SLACK_USERNAME: "Nightly" - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..f00c2ddd5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM node:lts as org.wildme.codex.frontend.build + +COPY . /code + +WORKDIR /code + +RUN set -ex \ + && ./scripts/build.npm.sh + +########################################################################################## +FROM nginx:alpine as org.wildme.codex.frontend.deploy + +COPY --from=org.wildme.codex.frontend.build /code/dist /usr/share/nginx/html + +COPY ./.dockerfiles/www/default.conf /etc/nginx/conf.d/default.conf diff --git a/docs/contribution-guide.md b/docs/contribution-guide.md index f74bdeb68..88a935833 100644 --- a/docs/contribution-guide.md +++ b/docs/contribution-guide.md @@ -4,33 +4,37 @@ This document covers the conventions and paradigms used in this codebase. We gen ## Components -We rely heavily on Material UI. When a component exists in Material UI, use it instead of creating one by hand. +We rely heavily on Material UI. When a component exists in Material UI, use it instead of creating one by hand. -## Styles +## Styles -- Currently we use inline styles. The plan is to continue doing so until performance issues arise. +- Currently we use inline styles. The plan is to continue doing so until performance issues arise. - We use a 4px grid system. All padding, margins, and widths should be divisible by 4. - When using the main theme colors (white, black, or purple), please use Material UI's useTheme hook. -## Global state +## Global state There are a few things stored in context, but mostly pages fetch their own data. -## Translations +## Translations -All displayed text must support translation - for this we use `react-intl`. Translation keys are verbatum English abbreviations of the displayed text in all caps. You can see some examples in `/locale/en.json`. +All displayed text must support translation - for this we use `react-intl`. Translation keys are verbatum English abbreviations of the displayed text in all caps. You can see some examples in `/locale/en.json`. If you want to help translate the project, that is very much appreciated and needed, but please don't do it by manually editing files in `/locale`. Your changes will wind up getting overwritten by Lokalise. -## Conventions +## Conventions - Any file with a React component should have the suffix `.jsx` - Data fetching goes in `/src/models` - Code specific to a certain page goes in `/src/pages` - Components that are reused widely go in `/src/components` -- Post any questions in Github issues or send an email to ben@wildme.org +- Post any questions in Github issues or send an email to ben@wildme.org - Thanks for contributing =) +## Release procedure + +Please refer to [this document](https://github.com/WildMeOrg/codex-frontend/blob/develop/docs/release-procedure.md) for guidelines on making a new release. + ## Making a hotfix -Likely you wont need to make a hotfix unless you are a Wild Me employee, but if the need arises please follow the [hotfix procedures](https://github.com/WildMeOrg/codex-frontend/blob/develop/docs/hotfix-procedure.md). \ No newline at end of file +Likely you won't need to make a hotfix unless you are a Wild Me employee, but if the need arises please follow the [hotfix procedures](https://github.com/WildMeOrg/codex-frontend/blob/develop/docs/hotfix-procedure.md). diff --git a/docs/release-procedure.md b/docs/release-procedure.md new file mode 100644 index 000000000..a55a18318 --- /dev/null +++ b/docs/release-procedure.md @@ -0,0 +1,33 @@ +# Release procedure + +## Alert the team + +A release PR should not happen while there are remaining work items in QA. + +Consult the front-end team if there are still items in QA, or if other previous merges need to be omitted from the release. + +The develop branch should be "frozen" during the release procedure; please be sure that everyone on the team is notified of the freeze and when it is completed. + +## Merge and create the PR + +First, resolve any potential merge conflicts with main on the develop branch + +1. `git checkout develop` +2. `git pull origin develop` +3. `git checkout main` +4. `git pull origin main` +5. `git merge develop` (there should be no merge conflicts) +6. Update the version in package.json. + +Then, issue a pull request for merging the develop branch into the main branch. Await approval and then merge using the "squash and merge" method. + +Note: if you encounter merge conflicts when merging develop into main, something went wrong. Investigate what happened thoroughly before continuing. + +## Draft a new release + +After completion of the previous steps, draft a new release of the main branch on GitHub: + +1. Navigate to [https://github.com/WildMeOrg/codex-frontend/releases/new](https://github.com/WildMeOrg/codex-frontend/releases/new). +2. Designate the target as the main branch. +3. Create a new tag following the SemVer pattern outlined [here](https://semver.org/) (vX.Y.Z). Note that this exact pattern (no period between v and the first name, for instance) must be followed explicitly. The release title should be the same as the tag. You can leave the release description blank. +4. Publish the release. diff --git a/locale/en.json b/locale/en.json index dc2dee3b3..f283d0a5d 100644 --- a/locale/en.json +++ b/locale/en.json @@ -12,6 +12,8 @@ "LABEL": "Label", "TYPE": "Type", "FOREST": "Forest", + "STAGE": "Stage", + "NO_PENDING_PUBLIC_SIGHTINGS": "There are no pending public sightings", "DATA_UNAVAILABLE": "Data unavailable", "DATA_MANAGER": "Data Manager", "ANNOTATION_CLASS": "Annotation class", @@ -727,7 +729,7 @@ "LOCATION_DESCRIPTION": "The precise decimal latitude and longitude where the sighting took place.", "WEBSITE": "Website", "REVIEW_MATCH_CANDIDATES": "Review match candidates", - "SERVER_STATUS": "Server Status", + "SITE_STATUS_PAGE_TITLE": "Site Status", "YOUR_NAME": "Your name", "CHOOSE_PHOTO": "Choose photo", "EMAIL_ADDRESS": "Email address", @@ -836,10 +838,10 @@ "SEND_INVITATION": "Send invitation", "SEX": "Sex", "MEMBERS": "Members", - "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_OCCURRENCE_LABEL": "Custom sighting fields", - "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_OCCURRENCE_DESCRIPTION": "Submitters will enter these fields once for each sighting they report.", - "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_MARKEDINDIVIDUAL_LABEL": "Custom individual fields", - "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_MARKEDINDIVIDUAL_DESCRIPTION": "Users can view and edit these fields on each individual's profile page.", + "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_SIGHTING_LABEL": "Custom sighting fields", + "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_SIGHTING_DESCRIPTION": "Submitters will enter these fields once for each sighting they report.", + "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_INDIVIDUAL_LABEL": "Custom individual fields", + "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_INDIVIDUAL_DESCRIPTION": "Users can view and edit these fields on each individual's profile page.", "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_ENCOUNTER_LABEL": "Custom encounter fields", "CONFIGURATION_SITE_CUSTOM_CUSTOMFIELDS_ENCOUNTER_DESCRIPTION": "Submitters will enter these fields for every individual in the sightings they report.", "CONFIGURATION_SITE_CUSTOM_REGIONS_LABEL": "Regions", @@ -974,6 +976,8 @@ "SERVER_TOOLTIP_JOBCOUNTER_LABEL": "Jobcounter:", "SERVER_TOOLTIP_STATUS_LABEL": "Status:", "SERVER_TOOLTIP_DATE_RECEIVED_LABEL": "Received on:", + "SERVER_TOOLTIP_SUMMARY": "Jobcounter: {jobcounter}; Status: {status}; Received on: {dateReceived}", + "SERVER_COLOR_LEGEND_BOX": "{color} legend box", "PRINTABLE_PHOTOBOOK": "Printable Photobook", "NO_IMAGES": "There are no images for this individual.", "TASKS": "Tasks", @@ -1126,6 +1130,7 @@ "COLLABORATION_RESTORE_ERROR": "Error restoring the collaboration", "COLLAB_RESTORE_ERROR_SUPPLEMENTAL": "Collaboration was not successfully restored.", "PENDING": "pending", + "PENDING_CITIZEN_SCIENCE_SIGHTINGS": "Pending citizen science sightings", "CANDIDATE_ANNOTATIONS": "Candidate annotations", "SIGHTING_DELETE_VULNERABLE_INDIVIDUAL_MESSAGE": "Deleting this sighting would result in assigned individuals being deleted. Are you sure you want to continue?", "REQUEST_REQUIRES_ADDITIONAL_CONFIRMATION": "Request requires additional confirmation", @@ -1135,5 +1140,13 @@ "PAGINATION_LAST_PAGE": "last page", "PAGINATION_PREVIOUS_PAGE": "previous page", "PAGINATION_NEXT_PAGE": "next page", + "SOCIAL_GROUPS": "Social groups", + "CONFIGURATION_SOCIAL_GROUP_ROLES_DESCRIPTION": "These roles populate the dropdown menu for any social groups made. It is recommended to include a generic role, such as, \"Member\"", + "CONFIGURATION_SOCIAL_GROUP_ROLES_LABEL": "Social group roles", + "NEW_SOCIAL_GROUP_ROLE": "Add role", + "ALLOW_MULTIPLE_OF_THIS_ROLE": "Allow multiple of this role", + "ONE_OR_MORE_ROLES_MISSING_LABELS": "One or more roles are missing labels", + "TWO_OR_MORE_ROLES_SAME_LABEL": "Two or more roles have the same label. Make sure each label is different", + "ROLE_GUID_MISSING": "Role is missing ID", "UNFINISHED_OPTIONS": "Options must have valid values and labels and unique values" } diff --git a/package.json b/package.json index c02dc265f..c73292333 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codex-frontend", - "version": "1.0.1", + "version": "1.0.2", "description": "", "main": "index.js", "scripts": { diff --git a/prod/Dockerfile b/prod/Dockerfile deleted file mode 100644 index 242aafd27..000000000 --- a/prod/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM node:lts as dist-build -RUN set -x \ - && cd /tmp \ - && git clone https://github.com/WildMeOrg/codex-frontend.git -# Install dependencies & build distribution -RUN set -ex \ - && cd /tmp/codex-frontend \ - && npm install \ - && npm run build -- --env=houston=relative - - -FROM nginx:alpine as main -COPY default.conf /etc/nginx/conf.d/default.conf -COPY --from=dist-build /tmp/codex-frontend/dist /usr/share/nginx/html diff --git a/prod/README.md b/prod/README.md deleted file mode 100644 index 1fd1a357a..000000000 --- a/prod/README.md +++ /dev/null @@ -1 +0,0 @@ -This `Dockerfile` is used to make a static build of the frontend code that is then served by nginx. This is intended to be a temporary solution. We would ideally like to mount the built code into the basic nginx image for serving. This method however requires less configuration, which is fine for a temporary solution. diff --git a/prod/default.conf b/prod/default.conf deleted file mode 100644 index f6b2f650f..000000000 --- a/prod/default.conf +++ /dev/null @@ -1,92 +0,0 @@ -server { - listen 80; - listen [::]:80; - server_name localhost; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } - - #error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - -} - - -# https://stackoverflow.com/a/49204467/176882 -# server { - -# listen 80; -# server_name www.xyz.io; -# root /opt/xyz/www; - -# # index -# index index.html; - -# # $uri, index.html -# location / { -# try_files $uri $uri/ /index.html; -# } - -# location /api { -# proxy_pass http://127.0.0.1:8200; -# proxy_http_version 1.1; -# proxy_set_header Upgrade $http_upgrade; -# proxy_set_header Connection "Upgrade"; -# } - -# } - -# original default -# server { -# listen 80; -# listen [::]:80; -# server_name localhost; - -# #access_log /var/log/nginx/host.access.log main; - -# location / { -# root /usr/share/nginx/html; -# index index.html index.htm; -# } - -# #error_page 404 /404.html; - -# # redirect server error pages to the static page /50x.html -# # -# error_page 500 502 503 504 /50x.html; -# location = /50x.html { -# root /usr/share/nginx/html; -# } - -# # proxy the PHP scripts to Apache listening on 127.0.0.1:80 -# # -# #location ~ \.php$ { -# # proxy_pass http://127.0.0.1; -# #} - -# # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 -# # -# #location ~ \.php$ { -# # root html; -# # fastcgi_pass 127.0.0.1:9000; -# # fastcgi_index index.php; -# # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; -# # include fastcgi_params; -# #} - -# # deny access to .htaccess files, if Apache's document root -# # concurs with nginx's one -# # -# #location ~ /\.ht { -# # deny all; -# #} -# } diff --git a/scripts/build.npm.sh b/scripts/build.npm.sh new file mode 100755 index 000000000..b6f3f969c --- /dev/null +++ b/scripts/build.npm.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +npm install --legacy-peer-deps +npm run build -- --env=houston=relative diff --git a/scripts/buildx.docker.sh b/scripts/buildx.docker.sh new file mode 100755 index 000000000..dba5e24a4 --- /dev/null +++ b/scripts/buildx.docker.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -e + +export DOCKER_BUILDKIT=1 + +export DOCKER_CLI_EXPERIMENTAL=enabled + +usage () { + echo "Usage: $0 [-t ] [-r ] [ ...]"; +} + +if [ ! -d ".git/" ]; then + echo "Cannot build Docker image when this repo is a submodule within a larger repo." + exit 1; +fi + +# Parse commandline options +while getopts ":t:r:" option; do + case ${option} in + t ) TAG=${OPTARG};; + r ) REGISTRY=${OPTARG};; + \? ) usage; exit 1;; + esac +done +shift $((OPTIND - 1)) + +# Assign variables +TAG=${TAG:-universal} +REGISTRY=${REGISTRY:-} +IMAGES=${@:-houston-frontend codex-frontend} +# Set the image prefix +if [ -n "$REGISTRY" ]; then + IMG_PREFIX="${REGISTRY}/" +else + IMG_PREFIX="wildme/" +fi + +# docker buildx create --name multi-arch-builder --use + +for IMG in $IMAGES; do + echo "Building and Pushing Multi-platform ${IMG_PREFIX}${IMG}:${TAG}" + if [ "$TAG" == "universal" ]; then + docker buildx build \ + --no-cache \ + -t ${IMG_PREFIX}${IMG}:${TAG} \ + --platform linux/amd64,linux/arm64 \ + . + else + docker buildx build \ + --no-cache \ + -t ${IMG_PREFIX}${IMG}:${TAG} \ + --platform linux/amd64,linux/arm64 \ + --push \ + . + fi +done diff --git a/src/AuthenticatedSwitch.jsx b/src/AuthenticatedSwitch.jsx index 1e20d649c..2b645ddb3 100644 --- a/src/AuthenticatedSwitch.jsx +++ b/src/AuthenticatedSwitch.jsx @@ -6,7 +6,7 @@ import { TransitionGroup } from 'react-transition-group'; import AuthenticatedAppHeader from './components/AuthenticatedAppHeader'; import SaveCustomField from './pages/fieldManagement/settings/saveField/SaveField'; import GeneralSettings from './pages/generalSettings/GeneralSettings'; -import ServerStatus from './pages/serverStatus/ServerStatus'; +import SiteStatus from './pages/siteStatus/SiteStatus'; import SplashSettings from './pages/splashSettings/SplashSettings'; import FieldManagement from './pages/fieldManagement/FieldManagement'; import UserManagement from './pages/userManagement/UserManagement'; @@ -20,6 +20,7 @@ import Sighting from './pages/sighting/Sighting'; import AssetGroupSighting from './pages/sighting/AssetGroupSighting'; import Splash from './pages/splash/Splash'; import AssetGroup from './pages/assetGroup/AssetGroup'; +import PendingCitizenScienceSightings from './pages/pendingCitizenScienceSightings/PendingCitizenScienceSightings'; import User from './pages/user/User'; import Users from './pages/user/Users'; import MergeIndividuals from './pages/merge/MergeIndividuals'; @@ -100,7 +101,7 @@ export default function AuthenticatedSwitch({ - + @@ -150,6 +151,9 @@ export default function AuthenticatedSwitch({ + + + diff --git a/src/components/AuthenticatedAppHeader/ActionsPane.jsx b/src/components/AuthenticatedAppHeader/ActionsPane.jsx index fc954a469..c944a9aca 100644 --- a/src/components/AuthenticatedAppHeader/ActionsPane.jsx +++ b/src/components/AuthenticatedAppHeader/ActionsPane.jsx @@ -8,6 +8,7 @@ import MenuItem from '@material-ui/core/MenuItem'; import MenuList from '@material-ui/core/MenuList'; import Divider from '@material-ui/core/Divider'; import SettingsIcon from '@material-ui/icons/Settings'; +import PublicIcon from '@material-ui/icons/SupervisedUserCircle'; import ControlPanelIcon from '@material-ui/icons/PermDataSetting'; import BulkImportIcon from '@material-ui/icons/PostAdd'; import LogoutIcon from '@material-ui/icons/ExitToApp'; @@ -23,6 +24,14 @@ const actions = [ messageId: 'BULK_IMPORT', icon: BulkImportIcon, }, + { + id: 'pending-citizen-science-sightings', + href: '/pending-citizen-science-sightings', + permissionsTest: userData => + userData?.is_admin || userData?.is_data_manager, + messageId: 'PENDING_CITIZEN_SCIENCE_SIGHTINGS', + icon: PublicIcon, + }, { id: 'settings', href: '/settings', @@ -32,7 +41,8 @@ const actions = [ { id: 'control-panel', href: '/admin', - adminOrUserManagerOnly: true, + permissionsTest: userData => + userData?.is_admin || userData?.is_user_manager, messageId: 'CONTROL_PANEL', icon: ControlPanelIcon, }, @@ -46,8 +56,6 @@ export default function NotificationsPane({ const theme = useTheme(); const closePopover = () => setAnchorEl(null); - const isAdministrator = get(userData, 'is_admin', false); - const isUserManager = get(userData, 'is_user_manager', false); const name = get(userData, 'full_name') || 'Unnamed user'; const profileSrc = get(userData, ['profile_fileupload', 'src']); @@ -84,10 +92,10 @@ export default function NotificationsPane({ {actions.map(action => { - const elevatedPermissions = - isAdministrator || isUserManager; - if (action.adminOrUserManagerOnly && !elevatedPermissions) - return null; + if (action.permissionsTest) { + const visible = action.permissionsTest(userData); + if (!visible) return null; + } return ( ; } diff --git a/src/components/cards/Card.jsx b/src/components/cards/Card.jsx index 012064de3..78ab7c8fb 100644 --- a/src/components/cards/Card.jsx +++ b/src/components/cards/Card.jsx @@ -7,7 +7,6 @@ export default function Card({ title, titleId, htmlId = null, - maxHeight = 360, overflow = 'auto', overflowX = 'auto', renderActions, @@ -35,9 +34,7 @@ export default function Card({ {renderActions} -
- {children} -
+
{children}
); diff --git a/src/components/cards/CollaborationsCard.jsx b/src/components/cards/CollaborationsCard.jsx index b0cbd498e..76dec05b5 100644 --- a/src/components/cards/CollaborationsCard.jsx +++ b/src/components/cards/CollaborationsCard.jsx @@ -15,8 +15,9 @@ export default function CollaborationsCard({ htmlId = null, }) { const intl = useIntl(); - const [activeCollaboration, setActiveCollaboration] = - useState(null); + const [activeCollaboration, setActiveCollaboration] = useState( + null, + ); const [ collabDialogButtonClickLoading, setCollabDialogButtonClickLoading, @@ -24,9 +25,12 @@ export default function CollaborationsCard({ const { data, loading } = useGetMe(); - useEffect(() => { - setCollabDialogButtonClickLoading(false); - }, [data]); + useEffect( + () => { + setCollabDialogButtonClickLoading(false); + }, + [data], + ); const collaborations = get(data, ['collaborations'], []); const tableData = collaborations.map(collaboration => { @@ -139,6 +143,7 @@ export default function CollaborationsCard({ columns={columns} data={tableData} idKey="guid" + tableContainerStyles={{ maxHeight: 600 }} /> ); diff --git a/src/components/cards/CooccurrenceCard.jsx b/src/components/cards/CooccurrenceCard.jsx index f61d197e9..bfd7f8e3d 100644 --- a/src/components/cards/CooccurrenceCard.jsx +++ b/src/components/cards/CooccurrenceCard.jsx @@ -34,6 +34,7 @@ export default function CooccurrenceCard({ }, ]} data={data} + tableContainerStyles={{ maxHeight: 600 }} /> ); diff --git a/src/components/cards/EncountersCard.jsx b/src/components/cards/EncountersCard.jsx index 38ec9e138..fba5fae78 100644 --- a/src/components/cards/EncountersCard.jsx +++ b/src/components/cards/EncountersCard.jsx @@ -34,20 +34,23 @@ export default function EncountersCard({ const mapModeClicked = () => setShowMapView(true); const listModeClicked = () => setShowMapView(false); - const encountersWithLocationData = useMemo(() => { - // hotfix // - if (!encounters) return []; - // hotfix // + const encountersWithLocationData = useMemo( + () => { + // hotfix // + if (!encounters) return []; + // hotfix // - return encounters.map(encounter => ({ - ...encounter, - formattedLocation: formatLocationFromSighting( - encounter, - regionOptions, - intl, - ), - })); - }, [get(encounters, 'length')]); + return encounters.map(encounter => ({ + ...encounter, + formattedLocation: formatLocationFromSighting( + encounter, + regionOptions, + intl, + ), + })); + }, + [get(encounters, 'length')], + ); const tooFewEncounters = encounters.length <= 1; @@ -162,6 +165,7 @@ export default function EncountersCard({ columns={filteredColumns} data={encountersWithLocationData} tableStyles={{ tableLayout: 'fixed' }} + tableContainerStyles={{ maxHeight: 600 }} /> )} {!noEncounters && showMapView &&
Map goes here
} diff --git a/src/components/cards/RelationshipsCard.jsx b/src/components/cards/RelationshipsCard.jsx index d4fc58be2..a2aa7b6bb 100644 --- a/src/components/cards/RelationshipsCard.jsx +++ b/src/components/cards/RelationshipsCard.jsx @@ -341,7 +341,7 @@ export default function RelationshipsCard({ , - + {/* // renderActions={ //
// setShowMapView(true); const listModeClicked = () => setShowMapView(false); - const sightingsWithLocationData = useMemo(() => { - // hotfix // - if (!sightings) return []; - // hotfix // + const sightingsWithLocationData = useMemo( + () => { + // hotfix // + if (!sightings) return []; + // hotfix // - return sightings.map(sighting => ({ - ...sighting, - formattedLocation: formatLocationFromSighting( - sighting, - regionOptions, - intl, - ), - })); - }, [get(sightings, 'length')]); + return sightings.map(sighting => ({ + ...sighting, + formattedLocation: formatLocationFromSighting( + sighting, + regionOptions, + intl, + ), + })); + }, + [get(sightings, 'length')], + ); const allColumns = [ { @@ -149,6 +152,7 @@ export default function SightingsCard({ data={sightings} loading={loading} noResultsTextId={noSightingsMsg} + tableContainerStyles={{ maxHeight: 400 }} /> )} {!noSightings && showMapView && ( diff --git a/src/components/dataDisplays/DataDisplay.jsx b/src/components/dataDisplays/DataDisplay.jsx index 6519d9c7c..f829f7218 100644 --- a/src/components/dataDisplays/DataDisplay.jsx +++ b/src/components/dataDisplays/DataDisplay.jsx @@ -61,9 +61,10 @@ export default function DataDisplay({ sortExternally, searchParams, setSearchParams, - paperStyles = {}, tableStyles = {}, cellStyles = {}, + stickyHeader = true, + tableContainerStyles = {}, ...rest }) { const intl = useIntl(); @@ -80,8 +81,9 @@ export default function DataDisplay({ ); const [filter, setFilter] = useState(''); const [internalSortColumn, setInternalSortColumn] = useState(null); - const [internalSortDirection, setInternalSortDirection] = - useState(null); + const [internalSortDirection, setInternalSortDirection] = useState( + null, + ); const [anchorEl, setAnchorEl] = useState(null); const filterPopperOpen = Boolean(anchorEl); @@ -142,6 +144,7 @@ export default function DataDisplay({ anchorEl={anchorEl} placement="bottom-end" transition + style={{ zIndex: theme.zIndex.appBar - 1 }} > {({ TransitionProps }) => ( @@ -258,9 +261,10 @@ export default function DataDisplay({ ( +
+ + {onDelete && ( + onDelete(value)} + /> + )} +
+ ), + }, + }, + ]; + + const filteredColumns = allColumns?.filter(c => + columns.includes(c.reference), + ); + + return ( + + ); +} diff --git a/src/components/dataDisplays/RelationshipDisplay.jsx b/src/components/dataDisplays/RelationshipDisplay.jsx index c3200b5a5..1fdceb6af 100644 --- a/src/components/dataDisplays/RelationshipDisplay.jsx +++ b/src/components/dataDisplays/RelationshipDisplay.jsx @@ -47,6 +47,7 @@ export default function RelationshipDisplay({ columns={relationshipCols} data={data} loading={loading} + tableContainerStyles={{ maxHeight: 400 }} /> ); } diff --git a/src/constants/queryKeys.js b/src/constants/queryKeys.js index 4260c7159..a0db809d8 100644 --- a/src/constants/queryKeys.js +++ b/src/constants/queryKeys.js @@ -12,6 +12,7 @@ export default { unreadNotifications: 'unreadNotifications', twitterBotTestResults: 'twitterBotTestResults', publicData: 'publicData', + publicAssetGroupSightings: 'publicAssetGroupSightings', sageJobs: ['sage', 'jobs'], }; diff --git a/src/hooks/useOptions.js b/src/hooks/useOptions.js index 139c6b499..9c39c6618 100644 --- a/src/hooks/useOptions.js +++ b/src/hooks/useOptions.js @@ -9,7 +9,8 @@ export default function useOptions() { useSiteSettings(); return useMemo(() => { - if (loading || error) return {}; + if (loading || error) + return { regionOptions: [], speciesOptions: [] }; const backendRegionOptions = get( data, diff --git a/src/models/assetGroupSighting/usePublicAGS.js b/src/models/assetGroupSighting/usePublicAGS.js new file mode 100644 index 000000000..0a1613d77 --- /dev/null +++ b/src/models/assetGroupSighting/usePublicAGS.js @@ -0,0 +1,9 @@ +import queryKeys from '../../constants/queryKeys'; +import useFetch from '../../hooks/useFetch'; + +export default function usePublicAGS() { + return useFetch({ + queryKey: queryKeys.publicAssetGroupSightings, + url: '/asset_groups/sighting/pending/public', + }); +} diff --git a/src/models/individual/useIndividualFieldSchemas.js b/src/models/individual/useIndividualFieldSchemas.js index e0f410609..4a2d518aa 100644 --- a/src/models/individual/useIndividualFieldSchemas.js +++ b/src/models/individual/useIndividualFieldSchemas.js @@ -20,11 +20,7 @@ export default function useIndividualFieldSchemas() { const customFields = get( data, - [ - 'site.custom.customFields.MarkedIndividual', - 'value', - 'definitions', - ], + ['site.custom.customFields.Individual', 'value', 'definitions'], [], ); const customFieldSchemas = customFields.map( diff --git a/src/models/sighting/useSightingFieldSchemas.js b/src/models/sighting/useSightingFieldSchemas.js index bb18564f5..3b42cb269 100644 --- a/src/models/sighting/useSightingFieldSchemas.js +++ b/src/models/sighting/useSightingFieldSchemas.js @@ -74,7 +74,7 @@ export default function useSightingFieldSchemas() { const customFields = get( data, - ['site.custom.customFields.Occurrence', 'value', 'definitions'], + ['site.custom.customFields.Sighting', 'value', 'definitions'], [], ); const customFieldSchemas = customFields.map( diff --git a/src/pages/assetGroup/AGSTable.jsx b/src/pages/assetGroup/AGSTable.jsx index 7653a2abf..47f8fd690 100644 --- a/src/pages/assetGroup/AGSTable.jsx +++ b/src/pages/assetGroup/AGSTable.jsx @@ -73,6 +73,7 @@ export default function AGSTable({ assetGroupSightings }) { data={transformedData} columns={columns} style={{ marginTop: 20 }} + tableContainerStyles={{ maxHeight: 600 }} /> ); } diff --git a/src/pages/assetGroup/AssetGroup.jsx b/src/pages/assetGroup/AssetGroup.jsx index 5fff211e7..8b59946de 100644 --- a/src/pages/assetGroup/AssetGroup.jsx +++ b/src/pages/assetGroup/AssetGroup.jsx @@ -26,18 +26,25 @@ import AGSTable from './AGSTable'; const POLLING_INTERVAL = 5000; // 5 seconds +function isProgressSettled(pipelineStatus) { + const { skipped, failed, complete } = pipelineStatus || {}; + + return skipped || complete || failed; +} + function deriveRefetchInterval(resultData, query) { - const { complete, status } = - resultData?.data?.progress_preparation || {}; + const pipelineStatus = get( + resultData, + 'data.pipeline_status.preparation', + {}, + ); - const error = query.state?.error && { ...query.state.error }; + const isSettled = isProgressSettled(pipelineStatus); - const isProgressSettled = - complete || ['failed', 'cancelled'].includes(status) || error; + const isError = Boolean(query.state?.error); - const refetchInterval = isProgressSettled - ? false - : POLLING_INTERVAL; + const refetchInterval = + isSettled || isError ? false : POLLING_INTERVAL; return refetchInterval; } @@ -86,15 +93,17 @@ export default function AssetGroup() { intl.formatMessage({ id: 'UNNAMED_USER' }); const creatorUrl = `/users/${sightingCreator?.guid}`; - const { - complete: isPreparationProgressComplete, - failed: isPreparationProgressFailed, - cancelled: isPreparationProgressCancelled, - } = get(data, 'progress_preparation', {}); - const showPreparationErrorAlert = - isPreparationProgressFailed || isPreparationProgressCancelled; - const showPreparationInProgressAlert = - !showPreparationErrorAlert && !isPreparationProgressComplete; + const pipelineStatusPreparation = get( + data, + 'pipeline_status.preparation', + {}, + ); + const isPreparationFailed = pipelineStatusPreparation.failed; + + const showPreparationInProgressAlert = !( + isPreparationFailed || + isProgressSettled(pipelineStatusPreparation) + ); return ( @@ -146,7 +155,7 @@ export default function AssetGroup() { )} - {showPreparationErrorAlert && ( + {isPreparationFailed && ( diff --git a/src/pages/fieldManagement/FieldManagement.jsx b/src/pages/fieldManagement/FieldManagement.jsx index 1bd505b9e..b572fad9e 100644 --- a/src/pages/fieldManagement/FieldManagement.jsx +++ b/src/pages/fieldManagement/FieldManagement.jsx @@ -41,11 +41,11 @@ export default function FieldManagement() { ); const customIndividualFields = getCustomFields( siteSettings, - 'MarkedIndividual', + 'Individual', ); const customSightingFields = getCustomFields( siteSettings, - 'Occurrence', + 'Sighting', ); if (loading || error) return null; @@ -87,7 +87,7 @@ export default function FieldManagement() { fields={customIndividualFields} titleId="CUSTOM_INDIVIDUAL_FIELDS" descriptionId="CUSTOM_INDIVIDUAL_FIELDS_DESCRIPTION" - settingName="site.custom.customFields.MarkedIndividual" + settingName="site.custom.customFields.Individual" noFieldsTextId="NO_CUSTOM_INDIVIDUAL_FIELDS_MESSAGE" /> ); diff --git a/src/pages/fieldManagement/settings/CustomFieldTable.jsx b/src/pages/fieldManagement/settings/CustomFieldTable.jsx index 5eff750bd..2ea09b98a 100644 --- a/src/pages/fieldManagement/settings/CustomFieldTable.jsx +++ b/src/pages/fieldManagement/settings/CustomFieldTable.jsx @@ -47,8 +47,9 @@ export default function CustomFieldTable({ const intl = useIntl(); const [deleteField, setDeleteField] = useState(null); const [previewField, setPreviewField] = useState(null); - const [previewInitialValue, setPreviewInitialValue] = - useState(null); + const [previewInitialValue, setPreviewInitialValue] = useState( + null, + ); const { removeCustomField, needsForce, @@ -101,7 +102,9 @@ export default function CustomFieldTable({ /> ); diff --git a/src/pages/fieldManagement/settings/DefaultFieldTable.jsx b/src/pages/fieldManagement/settings/DefaultFieldTable.jsx index dd56235ad..e1a63baf7 100644 --- a/src/pages/fieldManagement/settings/DefaultFieldTable.jsx +++ b/src/pages/fieldManagement/settings/DefaultFieldTable.jsx @@ -12,6 +12,7 @@ import Text from '../../../components/Text'; import categoryTypes from '../../../constants/categoryTypes'; import { RegionEditor } from './defaultFieldComponents/Editors'; import RelationshipEditor from './defaultFieldComponents/RelationshipEditor'; +import SocialGroupsEditor from './defaultFieldComponents/SocialGroupsEditor'; import SpeciesEditor from './defaultFieldComponents/SpeciesEditor'; import { cellRendererTypes } from '../../../components/dataDisplays/cellRenderers'; @@ -37,18 +38,34 @@ const configurableFields = [ type: categoryTypes.individual, Editor: RelationshipEditor, }, + { + id: 'socialGroups', + backendPath: 'social_group_roles', + labelId: 'SOCIAL_GROUPS', + type: categoryTypes.individual, + Editor: SocialGroupsEditor, + }, ]; function getInitialFormState(siteSettings) { - const regions = get(siteSettings, ['site.custom.regions', 'value']); + const regions = get( + siteSettings, + ['site.custom.regions', 'value'], + [], + ); const species = get(siteSettings, ['site.species', 'value'], []); const relationships = get( siteSettings, ['relationship_type_roles', 'value'], [], ); + const socialGroups = get( + siteSettings, + ['social_group_roles', 'value'], + [], + ); - return { regions, species, relationships }; + return { regions, species, relationships, socialGroups }; } export default function DefaultFieldTable({ @@ -138,6 +155,13 @@ export default function DefaultFieldTable({ }); if (response?.status === 200) onCloseEditor(); } + if (editField?.id === 'socialGroups') { + const response = await putSiteSetting({ + property: editField.backendPath, + data: formSettings.socialGroups, + }); + if (response?.status === 200) onCloseEditor(); + } }} > {error ? ( @@ -174,6 +198,7 @@ export default function DefaultFieldTable({ variant="secondary" columns={tableColumns} data={configurableFields} + tableContainerStyles={{ maxHeight: 300 }} /> ); diff --git a/src/pages/fieldManagement/settings/constants/customFieldTypes.js b/src/pages/fieldManagement/settings/constants/customFieldTypes.js index f1750eb2f..e2c4974d7 100644 --- a/src/pages/fieldManagement/settings/constants/customFieldTypes.js +++ b/src/pages/fieldManagement/settings/constants/customFieldTypes.js @@ -1,11 +1,11 @@ export default { individual: { name: 'individual', - backendPath: 'site.custom.customFields.MarkedIndividual', + backendPath: 'site.custom.customFields.Individual', }, sighting: { name: 'sighting', - backendPath: 'site.custom.customFields.Occurrence', + backendPath: 'site.custom.customFields.Sighting', }, encounter: { name: 'encounter', diff --git a/src/pages/fieldManagement/settings/defaultFieldComponents/RelationshipEditor.jsx b/src/pages/fieldManagement/settings/defaultFieldComponents/RelationshipEditor.jsx index 7614227e2..0f0636720 100644 --- a/src/pages/fieldManagement/settings/defaultFieldComponents/RelationshipEditor.jsx +++ b/src/pages/fieldManagement/settings/defaultFieldComponents/RelationshipEditor.jsx @@ -3,8 +3,8 @@ import { get, uniq } from 'lodash-es'; import { v4 as uuid } from 'uuid'; import ConfigureDefaultField from './ConfigureDefaultField'; -import Button from '../../../../components/Button'; import Text from '../../../../components/Text'; +import Button from '../../../../components/Button'; import Alert from '../../../../components/Alert'; import Type from './relationshipComponents/Type'; diff --git a/src/pages/fieldManagement/settings/defaultFieldComponents/SocialGroupComponents/SocialGroupRole.jsx b/src/pages/fieldManagement/settings/defaultFieldComponents/SocialGroupComponents/SocialGroupRole.jsx new file mode 100644 index 000000000..6b3bd7e73 --- /dev/null +++ b/src/pages/fieldManagement/settings/defaultFieldComponents/SocialGroupComponents/SocialGroupRole.jsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; + +import { get, filter } from 'lodash-es'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import Switch from '@material-ui/core/Switch'; +import FormGroup from '@material-ui/core/FormGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; + +import TextInput from '../../../../../components/inputs/TextInput'; +import Alert from '../../../../../components/Alert'; +import Text from '../../../../../components/Text'; +import DeleteButton from '../../../../../components/DeleteButton'; + +function deleteRole(roles, roleGuid) { + return filter(roles, role => role?.guid !== roleGuid); +} + +function updateRoleLabel(roles, roleGuid, newRoleLabel) { + const modifiedRoles = roles.map(role => { + if (role?.guid === roleGuid) + return { ...role, label: newRoleLabel }; + return role; + }); + return modifiedRoles; +} + +function updateRoleMultipleInGroupStatus(roles, roleGuid, newStatus) { + const modifiedRoles = roles.map(role => { + if (role?.guid === roleGuid) + return { ...role, multipleInGroup: newStatus }; + return role; + }); + return modifiedRoles; +} + +export default function SocialGroupRole({ + roles, + currentRole, + onChange, +}) { + const intl = useIntl(); + const roleGuid = currentRole?.guid; + const roleLabel = currentRole?.label; + + const checked = get(currentRole, 'multipleInGroup', false); + + if (!roleGuid) + return ( + + + + ); + return ( +
+ + { + onChange( + updateRoleMultipleInGroupStatus( + roles, + roleGuid, + e.target.checked, + ), + ); + }} + /> + } + label={intl.formatMessage({ + id: 'ALLOW_MULTIPLE_OF_THIS_ROLE', + })} + /> + + { + onChange(updateRoleLabel(roles, roleGuid, newLabel)); + }} + value={roleLabel} + autoFocus + InputProps={{ + endAdornment: ( + + { + onChange(deleteRole(roles, roleGuid)); + }} + /> + + ), + }} + /> +
+ ); +} diff --git a/src/pages/fieldManagement/settings/defaultFieldComponents/SocialGroupsEditor.jsx b/src/pages/fieldManagement/settings/defaultFieldComponents/SocialGroupsEditor.jsx new file mode 100644 index 000000000..13ff1d2d7 --- /dev/null +++ b/src/pages/fieldManagement/settings/defaultFieldComponents/SocialGroupsEditor.jsx @@ -0,0 +1,115 @@ +import React, { useState } from 'react'; +import { useIntl } from 'react-intl'; + +import { v4 as uuid } from 'uuid'; +import { get, uniq, map, some, filter } from 'lodash-es'; + +import ConfigureDefaultField from './ConfigureDefaultField'; +import Text from '../../../../components/Text'; +import Button from '../../../../components/Button'; +import SocialGroupRole from './SocialGroupComponents/SocialGroupRole'; + +function createRole(roles) { + const newRoleGuid = uuid(); + return [ + ...roles, + { guid: newRoleGuid, label: '', multipleInGroup: true }, + ]; +} + +function validateSocialGroups(roles, intl) { + const errors = []; + const roleLabels = roles.map(role => role?.label); + if (some(roleLabels, roleLabel => !roleLabel)) { + errors.push( + intl.formatMessage({ + id: 'ONE_OR_MORE_ROLES_MISSING_LABELS', + }), + ); + } + const uniqueRoleLabels = uniq(roleLabels); + if (uniqueRoleLabels.length !== roleLabels.length) + errors.push( + intl.formatMessage({ + id: 'TWO_OR_MORE_ROLES_SAME_LABEL', + }), + ); + return errors.length > 0 ? errors : null; +} + +export default function SocialGroupsEditor({ + onClose, + onSubmit, + formSettings, + setFormSettings, +}) { + const intl = useIntl(); + const [formErrors, setFormErrors] = useState(null); + function setRoles(roles) { + setFormSettings({ ...formSettings, socialGroups: roles }); + } + + const roles = get(formSettings, 'socialGroups', []); + const safeRoles = filter(roles, role => Boolean(role?.guid)); + + return ( + { + const errors = validateSocialGroups(safeRoles, intl); + setFormErrors(errors); + if (!errors) onSubmit(); + }} + open + error={formErrors?.map(error => ( + + {error} + + ))} + > +
+ +
+ +
+ {map(safeRoles, role => ( + + ))} +
+
+ ); +} diff --git a/src/pages/fieldManagement/settings/defaultFieldComponents/SpeciesEditor.jsx b/src/pages/fieldManagement/settings/defaultFieldComponents/SpeciesEditor.jsx index 19d224b0e..c5cca2529 100644 --- a/src/pages/fieldManagement/settings/defaultFieldComponents/SpeciesEditor.jsx +++ b/src/pages/fieldManagement/settings/defaultFieldComponents/SpeciesEditor.jsx @@ -167,13 +167,13 @@ export default function SpeciesEditor({ )} + ); } diff --git a/src/pages/match/QueryAnnotationsTable.jsx b/src/pages/match/QueryAnnotationsTable.jsx index 625da4be2..c005dde04 100644 --- a/src/pages/match/QueryAnnotationsTable.jsx +++ b/src/pages/match/QueryAnnotationsTable.jsx @@ -55,6 +55,7 @@ export default function QueryAnnotationsTable({ setSelectedQueryAnnotation(nextSelection); } }} + tableContainerStyles={{ maxHeight: 300 }} /> ); } diff --git a/src/pages/pendingCitizenScienceSightings/PendingCitizenScienceSightings.jsx b/src/pages/pendingCitizenScienceSightings/PendingCitizenScienceSightings.jsx new file mode 100644 index 000000000..506e741e7 --- /dev/null +++ b/src/pages/pendingCitizenScienceSightings/PendingCitizenScienceSightings.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; + +import usePublicAGS from '../../models/assetGroupSighting/usePublicAGS'; +import useDocumentTitle from '../../hooks/useDocumentTitle'; + +import MainColumn from '../../components/MainColumn'; +import LoadingScreen from '../../components/LoadingScreen'; +import SadScreen from '../../components/SadScreen'; +import EntityHeader from '../../components/EntityHeader'; +import HoustonSightingsDisplay from '../../components/dataDisplays/HoustonSightingsDisplay'; + +const columns = [ + 'date', + 'location', + 'submissionTime', + 'stage', + 'actions', +]; + +export default function PendingCitizenScienceSightings() { + const intl = useIntl(); + + const { data, loading, error, statusCode } = usePublicAGS(); + + useDocumentTitle('PENDING_CITIZEN_SCIENCE_SIGHTINGS'); + + if (error) return ; + if (loading) return ; + + return ( + + + + + ); +} diff --git a/src/pages/settings/Settings.jsx b/src/pages/settings/Settings.jsx index 3b810ddad..4ebb865c3 100644 --- a/src/pages/settings/Settings.jsx +++ b/src/pages/settings/Settings.jsx @@ -44,8 +44,12 @@ export default function Settings() { const [formValues, setFormValues] = useState({}); useEffect(() => { const initialValues = getInitialFormValues(schemas, data); - setFormValues({ ...initialValues, ...formValues }); - }, [formValues, schemas, data]); + + setFormValues(prevFormValues => ({ + ...initialValues, + ...prevFormValues, + })); + }, [schemas, data]); const backendValues = useMemo( () => getInitialFormValues(schemas, data), diff --git a/src/pages/sighting/SearchSightings.jsx b/src/pages/sighting/SearchSightings.jsx index 69615f6fb..16a680634 100644 --- a/src/pages/sighting/SearchSightings.jsx +++ b/src/pages/sighting/SearchSightings.jsx @@ -5,7 +5,7 @@ import useFilterSightings from '../../models/sighting/useFilterSightings'; import SearchPage from '../../components/SearchPage'; import FilterPanel from '../../components/FilterPanel'; import SearchFilterList from '../../components/SearchFilterList'; -import SightingsDisplay from '../../components/dataDisplays/SightingsDisplay'; +import ElasticsearchSightingsDisplay from '../../components/dataDisplays/ElasticsearchSightingsDisplay'; import Paginator from '../../components/dataDisplays/Paginator'; const rowsPerPage = 100; @@ -47,7 +47,7 @@ export default function SearchSightings() { /> } > - + - +
{usersError ? ( {collaborationError ? (