diff --git a/.env b/.env index eebd4f302..63a0743cf 100755 --- a/.env +++ b/.env @@ -36,7 +36,7 @@ SMTP_USER=project.1 SMTP_PASSWORD=secret.1 MAIL_FROM_USER="Madoc local " -MADOC_INSTALLATION_CODE='$2b$14$eofdZp3nY.HyK68a9zCfoOs3fuphxHRAR/KhckFm.8Qi8sEmgMcCK' +MADOC_INSTALLATION_CODE='$2b$14$9qnqDtpUMt7PQ0bs300y8el116vUbm4nF8Bf5vltCx78ZzuAWBu/K' # HTTPS # LOCAL PORT FOR HTTPS, CAN BE CHANGED diff --git a/.github/workflows/madoc-ts-docker.yaml b/.github/workflows/madoc-ts-docker.yaml index 53a731032..9e82db623 100644 --- a/.github/workflows/madoc-ts-docker.yaml +++ b/.github/workflows/madoc-ts-docker.yaml @@ -68,6 +68,10 @@ jobs: push: ${{ github.actor != 'dependabot[bot]' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + BUILD_TIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + BUILD_VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + BUILD_REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.gitignore b/.gitignore index d8adbcb6d..9bc116e86 100755 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ services/madoc-remix services/madoc-ts/service-jwts/madoc-remix.json e2e/test-fixtures/postgres/default/default.sql .DS_Store +services/madoc-next diff --git a/CHANGELOG.md b/CHANGELOG.md index f8663807e..6bdb4428e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/digirati-co-uk/madoc-platform/compare/v2.1.4...main) - +## [Unreleased](https://github.com/digirati-co-uk/madoc-platform/compare/v2.2.0...main) +## [v2.2.0](https://github.com/digirati-co-uk/madoc-platform/compare/v2.1.4...v2.2.0) + +[Full Release notes](https://docs.madoc.io/releases/v2.2) + +### Overview +- User profiles + user data +- Project members +- Project feedback +- Project updates +- New project page blocks +- Site terms and conditions +- Configurable auto-complete +- IIIF Viewer changes +- Review dashboard +- Local Autosave +- Page block translations +- Technical changes + ### Added +- New "custom auto-complete" configurations for external APIs (MAD-1408) +- New configuration to allow model field descriptions to be displayed as tooltips (MAD-1329) +- IIIF Collection endpoint for Madoc Collections (MAD-1407) +- New User preferences + profile options +- New customisable "Terms" page that users can accept +- Term API configuration admin interface +- Added TailwindCSS to the project +- Model fields can be dependent on each-others values + +### Fixed +- Manifest canvas model now allows rotation (MAD-1425) +- Scroll on large field lists (MAD-1323) - New cron-job to restart the queue at 3am every day (local to the server) ## [v2.1.4](https://github.com/digirati-co-uk/madoc-platform/compare/v2.1.3...v2.1.4) @@ -88,6 +117,11 @@ Hotfix release for submissions in progress. - Changed language on user dash from 'reviews' to 'review tasks' to differentiate between the two - Improved 'delete all contributions' by deleting all user tasks as well as capture model - Changed the project heading block to contain optional image and start contributing button +- Added max number option for allowMultiple fields in capture model + +### Changed +- Changed language on user dash from 'reviews' to 'review tasks' to differentiate between the two +- Improved 'delete all contributions' by deleting all user tasks as well as capture model ## [v2.1.0](https://github.com/digirati-co-uk/madoc-platform/compare/v2.0.8...v2.1.0) diff --git a/services/madoc-ts/.eslintrc.cjs b/services/madoc-ts/.eslintrc.cjs index 5afd126fa..834bc4aed 100644 --- a/services/madoc-ts/.eslintrc.cjs +++ b/services/madoc-ts/.eslintrc.cjs @@ -20,6 +20,7 @@ module.exports = { rules: { 'react/prop-types': 0, 'import/named': 0, + 'react/react-in-jsx-scope': 0, '@typescript-eslint/explicit-function-return-type': 0, '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/explicit-module-boundary-types': 0, @@ -70,11 +71,11 @@ module.exports = { allowTemplateLiterals: true }], 'space-before-blocks': ERROR, - 'space-before-function-paren': OFF + 'space-before-function-parent': OFF }, settings: { react: { version: 'detect' } } -}; \ No newline at end of file +}; diff --git a/services/madoc-ts/Dockerfile b/services/madoc-ts/Dockerfile index 7533860e1..57c9d3fae 100644 --- a/services/madoc-ts/Dockerfile +++ b/services/madoc-ts/Dockerfile @@ -19,6 +19,8 @@ COPY ./generate-schemas.js /home/node/app/generate-schemas.js COPY ./themes /home/node/app/themes COPY ./vite /home/node/app/vite COPY ./vite.config.js /home/node/app/vite.config.js +COPY ./postcss.config.js /home/node/app/postcss.config.js +COPY ./tailwind.config.js /home/node/app/tailwind.config.js ENV NODE_ENV=production @@ -41,6 +43,10 @@ RUN yarn install --no-interactive --frozen-lockfile --production=true FROM node:18-bullseye +ARG BUILD_TIME='unknown' +ARG BUILD_VERSION='unknown' +ARG BUILD_REVISION='unknown' + WORKDIR /home/node/app RUN npm install -g pm2@5 @@ -62,6 +68,10 @@ ENV DATABASE_PASSWORD=postgres ENV NODE_ENV=production ENV API_GATEWAY_HOST=http://gateway +ENV BUILD_TIME=${BUILD_TIME} +ENV BUILD_VERSION=${BUILD_VERSION} +ENV BUILD_REVISION=${BUILD_REVISION} + EXPOSE 3000 EXPOSE 3001 diff --git a/services/madoc-ts/Dockerfile.dev b/services/madoc-ts/Dockerfile.dev index 19927d685..b11d63fc6 100644 --- a/services/madoc-ts/Dockerfile.dev +++ b/services/madoc-ts/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:18 +FROM node:18-bullseye WORKDIR /home/node/app @@ -18,6 +18,8 @@ COPY ./tsconfig.frontend.json /home/node/app/tsconfig.frontend.json COPY ./migrations /home/node/app/migrations COPY ./config.json /home/node/app/config.json COPY ./generate-schemas.js /home/node/app/generate-schemas.js +COPY ./postcss.config.js /home/node/app/postcss.config.js +COPY ./tailwind.config.js /home/node/app/tailwind.config.js COPY ./translations /home/node/app/translations COPY ./schemas /home/node/app/schemas COPY ./themes /home/node/app/themes diff --git a/services/madoc-ts/Dockerfile.vite b/services/madoc-ts/Dockerfile.vite index b97b485f1..18729efe3 100644 --- a/services/madoc-ts/Dockerfile.vite +++ b/services/madoc-ts/Dockerfile.vite @@ -1,4 +1,4 @@ -FROM node:18 as build +FROM node:18-bullseye as build WORKDIR /home/node/app @@ -8,7 +8,7 @@ COPY ./npm /home/node/app/npm RUN yarn install -FROM node:18 +FROM node:18-bullseye WORKDIR /home/node/app @@ -17,6 +17,8 @@ COPY --from=build /home/node/app/yarn.lock /home/node/app/yarn.lock COPY --from=build /home/node/app/node_modules /home/node/app/node_modules COPY ./schemas /home/node/app/schemas COPY ./vite.config.js /home/node/app/vite.config.js +COPY ./postcss.config.js /home/node/app/postcss.config.js +COPY ./tailwind.config.js /home/node/app/tailwind.config.js COPY ./themes /home/node/app/themes COPY ./vite /home/node/app/vite COPY ./entrypoint /home/node/app/entrypoint diff --git a/services/madoc-ts/__tests__/utility/create-link.tests.ts b/services/madoc-ts/__tests__/utility/create-link.tests.ts new file mode 100644 index 000000000..b1f8dd752 --- /dev/null +++ b/services/madoc-ts/__tests__/utility/create-link.tests.ts @@ -0,0 +1,12 @@ +import { createLink } from '../../src/frontend/shared/utility/create-link'; + +describe('createLink', () => { + test('it can create /reviews/{taskId} link', () => { + const link = createLink({ + subRoute: 'reviews', + taskId: '1234', + }); + + expect(link).toEqual('/reviews/1234'); + }); +}); diff --git a/services/madoc-ts/config.json b/services/madoc-ts/config.json index 417b44572..5b401dd74 100644 --- a/services/madoc-ts/config.json +++ b/services/madoc-ts/config.json @@ -49,7 +49,9 @@ "preventContributionAfterSubmission": false, "preventMultipleUserSubmissionsPerResource": false, "preventContributionAfterManifestUnassign": false, - "hideViewerControls": false + "hideViewerControls": false, + "enableTooltipDescriptions": false, + "enableSplitView": false }, "projectPageOptions": { "hideStartContributing": false, diff --git a/services/madoc-ts/migrations/2023-07-17T13-42.term-configurations.sql b/services/madoc-ts/migrations/2023-07-17T13-42.term-configurations.sql new file mode 100644 index 000000000..69e4ea790 --- /dev/null +++ b/services/madoc-ts/migrations/2023-07-17T13-42.term-configurations.sql @@ -0,0 +1,21 @@ +--term-configurations (up) + +create table term_configurations ( + id uuid primary key, + url_pattern text not null, + results_path text not null, + label_path text not null, + uri_path text not null, + resource_class_path text, + description_path text, + language_path text, + term_label text not null, + term_description text, + attribution text, + site_id integer not null, + creator integer not null, + created_at timestamp not null +); + +create index term_configurations_site_id on term_configurations (site_id); + diff --git a/services/madoc-ts/migrations/2023-07-18T11-55.user-extensions.sql b/services/madoc-ts/migrations/2023-07-18T11-55.user-extensions.sql new file mode 100644 index 000000000..0a1c4d4bb --- /dev/null +++ b/services/madoc-ts/migrations/2023-07-18T11-55.user-extensions.sql @@ -0,0 +1,41 @@ +--user-extensions (up) + +alter table "user" add column user_info jsonb; +alter table "user" add column user_preferences jsonb; + +create table site_terms +( + id uuid primary key, + site_id integer references site (id), + created_at timestamp not null default CURRENT_TIMESTAMP, + terms_markdown text not null, + terms_text text not null +); + +alter table "user" add column terms_accepted uuid[]; + +create table badge +( + id uuid primary key, + site_id integer references site (id), + label jsonb not null, + description jsonb, + svg_code text, + tier_colors text[], + trigger_name text, + created_at timestamp, + updated_at timestamp +); + +create table badge_award +( + id uuid primary key, + site_id integer references site (id) not null, + user_id integer references "user" (id) not null, + project_id integer references iiif_project (id), + badge_id uuid references badge (id), + awarded_by integer references "user" (id), + reason text, + tier int, + awarded_at timestamp not null default CURRENT_TIMESTAMP +); diff --git a/services/madoc-ts/migrations/2023-07-27T11-04.project-extensions.sql b/services/madoc-ts/migrations/2023-07-27T11-04.project-extensions.sql new file mode 100644 index 000000000..246a5e0a9 --- /dev/null +++ b/services/madoc-ts/migrations/2023-07-27T11-04.project-extensions.sql @@ -0,0 +1,38 @@ +--project-extensions (up) + +alter table iiif_derived_resource add column placeholder_image text default null; +alter table iiif_resource add column placeholder_image text default null; + +alter table iiif_project add column due_date date default null; +alter table iiif_project add column start_date date default null; +alter table iiif_project add column members_only bool default false; + +alter table project_notes add column created timestamp not null default now(); +alter table project_notes add column updated timestamp not null default now(); + +create table project_feedback ( + id serial primary key, + project_id integer not null references iiif_project(id) on delete cascade, + user_id integer not null references "user"(id), + created timestamp not null default now(), + feedback text not null +); + +create table project_updates ( + id serial primary key, + project_id integer not null references iiif_project(id) on delete cascade, + user_id integer not null references "user"(id), + created timestamp not null default now(), + update text not null, + snapshot json +); + +create table project_members ( + id serial primary key, + project_id integer not null references iiif_project(id) on delete cascade, + user_id integer not null references "user"(id), + created timestamp not null default now(), + role text, + role_label text, + role_color text +); diff --git a/services/madoc-ts/migrations/2023-08-03T20-11.add-ids.sql b/services/madoc-ts/migrations/2023-08-03T20-11.add-ids.sql new file mode 100644 index 000000000..b439a2670 --- /dev/null +++ b/services/madoc-ts/migrations/2023-08-03T20-11.add-ids.sql @@ -0,0 +1,16 @@ +--add-ids (up) + +-- Make the Id field on plugin_token a primary key +alter table plugin_token add primary key (id); + +alter table site_page_slots add primary key (page_id, slot_id); + +alter table site_slot_blocks add primary key (block_id, slot_id); + +alter table webhook_call add primary key (id); + +-- remove constraint valid_invitation from invitation +alter table user_invitations drop constraint valid_invitation; + +-- Remove index iiif_metadata_uq +drop index iiif_metadata_uq; diff --git a/services/madoc-ts/migrations/down/2023-07-17T13-42.term-configurations.sql b/services/madoc-ts/migrations/down/2023-07-17T13-42.term-configurations.sql new file mode 100644 index 000000000..5b8a7d51f --- /dev/null +++ b/services/madoc-ts/migrations/down/2023-07-17T13-42.term-configurations.sql @@ -0,0 +1,4 @@ +--term-configurations (down) + +drop table if exists term_configurations; +drop index if exists term_configurations_site_id; diff --git a/services/madoc-ts/migrations/down/2023-07-18T11-55.user-extensions.sql b/services/madoc-ts/migrations/down/2023-07-18T11-55.user-extensions.sql new file mode 100644 index 000000000..345994b67 --- /dev/null +++ b/services/madoc-ts/migrations/down/2023-07-18T11-55.user-extensions.sql @@ -0,0 +1,8 @@ +--user-extensions (down) + +drop table if exists badge_award; +drop table if exists badge; +drop table if exists site_terms; +alter table "user" drop column terms_accepted; +alter table "user" drop column user_preferences; +alter table "user" drop column user_info; diff --git a/services/madoc-ts/migrations/down/2023-07-27T11-04.project-extensions.sql b/services/madoc-ts/migrations/down/2023-07-27T11-04.project-extensions.sql new file mode 100644 index 000000000..a1a08f290 --- /dev/null +++ b/services/madoc-ts/migrations/down/2023-07-27T11-04.project-extensions.sql @@ -0,0 +1,13 @@ +--project-extensions (down) + + +alter table iiif_derived_resource drop column if exists placeholder_image; +alter table iiif_resource drop column if exists placeholder_image; + +alter table iiif_project drop column if exists due_date; +alter table iiif_project drop column if exists start_date; +alter table iiif_project drop column if exists members_only; + +drop table if exists project_feedback; +drop table if exists project_updates; +drop table if exists project_members; diff --git a/services/madoc-ts/migrations/down/2023-08-03T20-11.add-ids.sql b/services/madoc-ts/migrations/down/2023-08-03T20-11.add-ids.sql new file mode 100644 index 000000000..cbc796e42 --- /dev/null +++ b/services/madoc-ts/migrations/down/2023-08-03T20-11.add-ids.sql @@ -0,0 +1 @@ +--add-ids (down) diff --git a/services/madoc-ts/package.json b/services/madoc-ts/package.json index 6a4a70620..0d545a288 100644 --- a/services/madoc-ts/package.json +++ b/services/madoc-ts/package.json @@ -78,6 +78,7 @@ "@iiif/vault-helpers": "^0.9.8", "@koa/router": "^10.1.1", "@madoc.io/types": "./npm/madoc-types", + "@manifest-editor/iiif-browser-bundle": "https://pkg.csb.dev/digirati-co-uk/iiif-manifest-editor/commit/cc804fcf/@manifest-editor/iiif-browser-bundle", "@slonik/migrator": "^0.2.0", "@styled-icons/entypo": "^10.34.0", "adm-zip": "^0.5.10", @@ -129,6 +130,7 @@ "koa-send": "^5.0.0", "koa2-connect": "^1.0.2", "locale-codes": "^1.3.1", + "localforage": "^1.10.0", "memfs": "^3.1.2", "memory-cache": "^0.2.0", "mirador": "^3.2.0", @@ -173,7 +175,7 @@ "react-syntax-highlighter": "^15.4.3", "react-textarea-autosize": "^8.3.1", "react-timeago": "^4.4.0", - "react-tooltip": "^4.2.21", + "react-tooltip": "^5.18.0", "redux-batched-subscribe": "^0.1.6", "rich-markdown-editor": "11.17.8", "semver": "^7.3.5", @@ -265,6 +267,7 @@ "@vitejs/plugin-legacy": "^3.0.1", "@vitejs/plugin-react": "^3.0.1", "@vitest/ui": "^0.28.2", + "autoprefixer": "^10.4.14", "awesome-typescript-loader": "^5.2.1", "babel-plugin-react-data-testid": "^0.2.0", "babel-plugin-styled-components": "^2.0.7", @@ -288,12 +291,14 @@ "jest-environment-jsdom": "^29.4.2", "jsdom": "^16.5.1", "koa-connect": "^2.1.0", + "postcss": "^8.4.27", "prettier": "^1.19.1", "react-is": "^16.13.1", "react-query-devtools": "^2.4.7", "react-refresh": "^0.10.0", "storybook": "^7.0.0-beta.45", "strip-indent": "^4.0.0", + "tailwindcss": "^3.3.3", "terser": "^5.14.2", "thread-loader": "^3.0.4", "traverse": "^0.6.6", diff --git a/services/madoc-ts/postcss.config.js b/services/madoc-ts/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/services/madoc-ts/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/services/madoc-ts/schemas/CollectionFull.json b/services/madoc-ts/schemas/CollectionFull.json index 0e9c9f439..e83732987 100644 --- a/services/madoc-ts/schemas/CollectionFull.json +++ b/services/madoc-ts/schemas/CollectionFull.json @@ -128,6 +128,9 @@ "type" ] } + }, + "placeholder_image": { + "type": "string" } }, "required": [ diff --git a/services/madoc-ts/schemas/ProjectConfiguration.json b/services/madoc-ts/schemas/ProjectConfiguration.json index 9b2649577..9964d1c60 100644 --- a/services/madoc-ts/schemas/ProjectConfiguration.json +++ b/services/madoc-ts/schemas/ProjectConfiguration.json @@ -1,6 +1,57 @@ { "type": "object", "properties": { + "_version": { + "type": "number", + "enum": [ + 1 + ] + }, + "_source": { + "type": "object", + "properties": { + "siteConfig": { + "type": "array", + "items": { + "type": "object", + "properties": { + "property": { + "type": "string" + }, + "original": {}, + "override": {} + }, + "required": [ + "original", + "override", + "property" + ] + } + }, + "staticConfig": { + "type": "array", + "items": { + "type": "object", + "properties": { + "property": { + "type": "string" + }, + "original": {}, + "override": {} + }, + "required": [ + "original", + "override", + "property" + ] + } + } + }, + "required": [ + "siteConfig", + "staticConfig" + ] + }, "allowCollectionNavigation": { "type": "boolean" }, @@ -213,6 +264,15 @@ }, "enableRotation": { "type": "boolean" + }, + "enableAutoSave": { + "type": "boolean" + }, + "enableTooltipDescriptions": { + "type": "boolean" + }, + "enableSplitView": { + "type": "boolean" } } }, diff --git a/services/madoc-ts/schemas/ProjectConfigurationNEW.json b/services/madoc-ts/schemas/ProjectConfigurationNEW.json new file mode 100644 index 000000000..de5db6abc --- /dev/null +++ b/services/madoc-ts/schemas/ProjectConfigurationNEW.json @@ -0,0 +1,449 @@ +{ + "type": "object", + "properties": { + "_version": { + "type": "number", + "enum": [ + 2 + ] + }, + "_source": { + "type": "object", + "properties": { + "siteConfig": { + "type": "array", + "items": { + "type": "object", + "properties": { + "property": { + "type": "string" + }, + "original": {}, + "override": {} + }, + "required": [ + "original", + "override", + "property" + ] + } + }, + "staticConfig": { + "type": "array", + "items": { + "type": "object", + "properties": { + "property": { + "type": "string" + }, + "original": {}, + "override": {} + }, + "required": [ + "original", + "override", + "property" + ] + } + } + }, + "required": [ + "siteConfig", + "staticConfig" + ] + }, + "headerOptions": { + "type": "object", + "properties": { + "hideSiteTitle": { + "type": "boolean" + }, + "hideProjectsLink": { + "type": "boolean" + }, + "hideCollectionsLink": { + "type": "boolean" + }, + "hideDashboardLink": { + "type": "boolean" + }, + "hidePageNavLinks": { + "type": "boolean" + }, + "hideSearchBar": { + "type": "boolean" + } + } + }, + "projectPageOptions": { + "type": "object", + "properties": { + "hideStatistics": { + "type": "boolean" + }, + "hideProjectCollectionNavigation": { + "type": "boolean" + }, + "hideProjectManifestNavigation": { + "type": "boolean" + }, + "hideStartContributing": { + "type": "boolean" + }, + "hideSearchButton": { + "type": "boolean" + }, + "hideRandomManifest": { + "type": "boolean" + }, + "hideRandomCanvas": { + "type": "boolean" + }, + "reviewerDashboard": { + "type": "boolean" + } + } + }, + "manifestPageOptions": { + "type": "object", + "properties": { + "hideManifestMetadataOnCanvas": { + "type": "boolean" + }, + "hideStartContributing": { + "type": "boolean" + }, + "hideOpenInMirador": { + "type": "boolean" + }, + "hideSearchButton": { + "type": "boolean" + }, + "hideRandomCanvas": { + "type": "boolean" + }, + "hideFilterImages": { + "type": "boolean" + }, + "directModelPage": { + "type": "boolean" + }, + "showIIIFLogo": { + "type": "boolean" + }, + "generatePDF": { + "type": "boolean" + }, + "coveredImages": { + "type": "boolean" + }, + "rectangularImages": { + "type": "boolean" + }, + "hideCanvasLabels": { + "type": "boolean" + }, + "skipManifestListingPage": { + "type": "boolean" + } + } + }, + "atlasBackground": { + "type": "string" + }, + "canvasPageOptions": { + "type": "object", + "properties": { + "miradorCanvasPage": { + "type": "boolean" + }, + "universalViewerCanvasPage": { + "type": "boolean" + }, + "hideCanvasThumbnailNavigation": { + "type": "boolean" + } + } + }, + "navigation": { + "type": "object", + "properties": { + "allowCollectionNavigation": { + "type": "boolean" + }, + "allowManifestNavigation": { + "type": "boolean" + }, + "allowCanvasNavigation": { + "type": "boolean" + } + }, + "required": [ + "allowCanvasNavigation", + "allowCollectionNavigation", + "allowManifestNavigation" + ] + }, + "searchStrategy": { + "type": "string", + "enum": [ + "string" + ] + }, + "searchOptions": { + "type": "object", + "properties": { + "nonLatinFulltext": { + "type": "boolean" + }, + "searchMultipleFields": { + "type": "boolean" + }, + "onlyShowManifests": { + "type": "boolean" + }, + "showSearchFacetCount": { + "type": "boolean" + } + } + }, + "contributionMode": { + "enum": [ + "annotation", + "transcription" + ], + "type": "string" + }, + "maxContributionsPerResource": { + "anyOf": [ + { + "enum": [ + false + ], + "type": "boolean" + }, + { + "type": "number" + } + ] + }, + "preventMultipleUserSubmissionsPerResource": { + "type": "boolean" + }, + "forkMode": { + "type": "boolean" + }, + "claimGranularity": { + "enum": [ + "canvas", + "manifest" + ], + "type": "string" + }, + "assigningCanvas": { + "type": "object", + "properties": { + "randomlyAssignCanvas": { + "type": "boolean" + }, + "priorityRandomness": { + "type": "boolean" + } + } + }, + "randomCanvas": { + "type": "boolean" + }, + "defaultEditorOrientation": { + "enum": [ + "horizontal", + "vertical" + ], + "type": "string" + }, + "modelPageOptions": { + "type": "object", + "properties": { + "hideViewerControls": { + "type": "boolean" + }, + "enableRotation": { + "type": "boolean" + }, + "fixedTranscriptionBar": { + "type": "boolean" + }, + "disableSaveForLater": { + "type": "boolean" + }, + "allowPersonalNotes": { + "type": "boolean" + }, + "enableAutoSave": { + "type": "boolean" + }, + "enableTooltipDescriptions": { + "type": "boolean" + }, + "enableSplitView": { + "type": "boolean" + } + } + }, + "contributionWarningTime": { + "anyOf": [ + { + "enum": [ + false + ], + "type": "boolean" + }, + { + "type": "number" + } + ] + }, + "shortExpiryTime": { + "type": "string" + }, + "longExpiryTime": { + "type": "string" + }, + "submissionOptions": { + "type": "object", + "properties": { + "disablePreview": { + "type": "boolean" + }, + "disableNextCanvas": { + "type": "boolean" + }, + "preventContributionAfterManifestUnassign": { + "type": "boolean" + }, + "preventContributionAfterRejection": { + "type": "boolean" + }, + "preventContributionAfterSubmission": { + "type": "boolean" + } + } + }, + "modelPageShowAnnotations": { + "enum": [ + "always", + "highlighted", + "when-open" + ], + "type": "string" + }, + "modelPageShowDocument": { + "enum": [ + "always", + "highlighted", + "when-open" + ], + "type": "string" + }, + "canvasPageShowAnnotations": { + "enum": [ + "always", + "highlighted", + "when-open" + ], + "type": "string" + }, + "canvasPageShowDocument": { + "enum": [ + "always", + "highlighted", + "when-open" + ], + "type": "string" + }, + "randomlyAssignReviewer": { + "type": "boolean" + }, + "adminsAreReviewers": { + "type": "boolean" + }, + "manuallyAssignedReviewer": { + "type": "number" + }, + "revisionApprovalsRequired": { + "type": "number" + }, + "reviewOptions": { + "type": "object", + "properties": { + "allowMerging": { + "type": "boolean" + }, + "enableAutoReview": { + "type": "boolean" + } + } + }, + "hideCompletedResources": { + "type": "boolean" + }, + "allowSubmissionsWhenCanvasComplete": { + "type": "boolean" + }, + "skipAutomaticOCRImport": { + "type": "boolean" + }, + "metadataSuggestions": { + "type": "object", + "properties": { + "manifest": { + "type": "boolean" + }, + "collection": { + "type": "boolean" + }, + "canvas": { + "type": "boolean" + } + } + }, + "activityStreams": { + "type": "object", + "properties": { + "manifest": { + "type": "boolean" + }, + "canvas": { + "type": "boolean" + }, + "curated": { + "type": "boolean" + }, + "published": { + "type": "boolean" + } + } + }, + "shadow": { + "type": "object", + "properties": { + "showCaptureModelOnManifest": { + "type": "boolean" + } + } + } + }, + "required": [ + "_version", + "adminsAreReviewers", + "allowSubmissionsWhenCanvasComplete", + "claimGranularity", + "contributionWarningTime", + "defaultEditorOrientation", + "hideCompletedResources", + "maxContributionsPerResource", + "randomlyAssignReviewer", + "revisionApprovalsRequired" + ], + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/services/madoc-ts/src/config.ts b/services/madoc-ts/src/config.ts index dfb6e44ea..25c9303f9 100644 --- a/services/madoc-ts/src/config.ts +++ b/services/madoc-ts/src/config.ts @@ -20,6 +20,11 @@ function validNumber(t: string | undefined, defaultValue: number) { } export const config: EnvConfig = { + build: { + time: process.env.BUILD_TIME || 'unknown', + version: process.env.BUILD_VERSION || 'unknown', + revision: process.env.BUILD_REVISION || 'unknown', + }, flags: { capture_model_api_migrated: castBool(process.env.CAPTURE_MODEL_API_MIGRATED, false), }, diff --git a/services/madoc-ts/src/database/queries/get-collection-snippets.ts b/services/madoc-ts/src/database/queries/get-collection-snippets.ts index 037785efc..3eb83a726 100644 --- a/services/madoc-ts/src/database/queries/get-collection-snippets.ts +++ b/services/madoc-ts/src/database/queries/get-collection-snippets.ts @@ -15,6 +15,8 @@ export type CollectionSnippetsRow = { source: string; resource_id: number; canvas_count?: number; + placeholder_image?: string; + published: boolean; }; type CollectionAggregate = TaggedTemplateLiteralInvocationType<{ @@ -24,6 +26,8 @@ type CollectionAggregate = TaggedTemplateLiteralInvocationType<{ manifest_canvas_count: number; collection_manifest_count: number; manifest_thumbnail: string; + published: boolean; + placeholder_image?: string; }>; export function getSingleCollection({ @@ -55,6 +59,7 @@ export function getSingleCollection({ collection_manifest_count: number; manifest_thumbnail: string; published: boolean; + placeholder_image?: string; }>` select ${collectionId}::int as collection_id, manifest_links.item_id as manifest_id, @@ -63,7 +68,8 @@ export function getSingleCollection({ resource.type as resource_type, resource.id as resource_id, manifest_thumbnail(${siteId}, manifest_links.item_id) as manifest_thumbnail, - single_collection.published as published + single_collection.published as published, + single_collection.placeholder_image as placeholder_image from iiif_derived_resource single_collection left join iiif_derived_resource_items manifest_links @@ -112,7 +118,7 @@ function selectCollections({ if (parentCollectionId) { return sql` - select cidr.resource_id as collection_id, cidr.published as published + select cidr.resource_id as collection_id, cidr.published as published, cidr.placeholder_image as placeholder_image from iiif_derived_resource cidr left join iiif_derived_resource_items cidri on cidr.resource_id = cidri.item_id where resource_type = 'collection' @@ -121,12 +127,12 @@ function selectCollections({ ${hideFlat ? sql`and cidr.flat = false` : SQL_EMPTY} ${onlyPublished ? sql`and cidr.published = true` : SQL_EMPTY} and cidri.resource_id = ${parentCollectionId} - limit ${perPage} offset ${offset} + limit ${perPage} offset ${offset} `; } return sql` - select cidr.resource_id as collection_id, cidr.published as published + select cidr.resource_id as collection_id, cidr.published as published, cidr.placeholder_image as placeholder_image from iiif_derived_resource cidr where resource_type = 'collection' and cidr.site_id = ${siteId} @@ -163,14 +169,16 @@ export function getCollectionList({ canvas_count.item_total as manifest_canvas_count, manifest_count.item_total as collection_manifest_count, manifest_thumbnail(${siteId}, manifest_links.item_id) as manifest_thumbnail, - collection.published as published + collection.published as published, + collection.placeholder_image as placeholder_image + from (${selectCollections({ parentCollectionId, siteId, perPage, page, onlyPublished, - })}) collection(collection_id, published) + })}) collection(collection_id, published, placeholder_image) left join (select im.item_id, im.resource_id, ir.type from iiif_derived_resource_items im left join iiif_resource ir on im.item_id = ir.id @@ -198,6 +206,7 @@ export function getCollectionSnippets( collections_aggregation.collection_manifest_count as manifest_count, collections_aggregation.resource_type as resource_type, collections_aggregation.published as published, + collections_aggregation.placeholder_image as placeholder_image, metadata.id as metadata_id, metadata.key, metadata.value, diff --git a/services/madoc-ts/src/extensions/capture-models/CanvasExplorer/CanvasExplorer.tsx b/services/madoc-ts/src/extensions/capture-models/CanvasExplorer/CanvasExplorer.tsx index 32cad0eba..744dbccce 100644 --- a/services/madoc-ts/src/extensions/capture-models/CanvasExplorer/CanvasExplorer.tsx +++ b/services/madoc-ts/src/extensions/capture-models/CanvasExplorer/CanvasExplorer.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; import { ImageStripBox } from '../../../frontend/shared/atoms/ImageStrip'; import { ImageGrid } from '../../../frontend/shared/atoms/ImageGrid'; import { Heading5 } from '../../../frontend/shared/typography/Heading5'; -import { CanvasSnippet } from '../../../frontend/shared/components/CanvasSnippet'; +import { CanvasSnippet } from '../../../frontend/shared/features/CanvasSnippet'; export type CanvasExplorerProps = { id: string; diff --git a/services/madoc-ts/src/extensions/page-blocks/current-manifest-snippet-block.tsx b/services/madoc-ts/src/extensions/page-blocks/current-manifest-snippet-block.tsx index 49325ab98..823940666 100644 --- a/services/madoc-ts/src/extensions/page-blocks/current-manifest-snippet-block.tsx +++ b/services/madoc-ts/src/extensions/page-blocks/current-manifest-snippet-block.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { captureModelShorthand } from '../../frontend/shared/capture-models/helpers/capture-model-shorthand'; -import { ManifestSnippet } from '../../frontend/shared/components/ManifestSnippet'; +import { ManifestSnippet } from '../../frontend/shared/features/ManifestSnippet'; import { ReactPageBlockDefinition } from './extension'; const definition: ReactPageBlockDefinition< @@ -10,7 +10,6 @@ const definition: ReactPageBlockDefinition< size?: 'lg' | 'md' | 'sm'; center?: boolean; buttonRole?: 'button' | 'link'; - containThumbnail?: boolean; smallLabel?: boolean; fluid?: boolean; }, @@ -52,11 +51,6 @@ const definition: ReactPageBlockDefinition< { value: 'link', text: 'Link' }, ], }, - containThumbnail: { - label: 'Thumbnail layout', - type: 'checkbox-field', - inlineLabel: 'Contain thumbnail', - }, smallLabel: { label: 'Label size', type: 'checkbox-field', @@ -74,7 +68,6 @@ const definition: ReactPageBlockDefinition< size: 'md', center: false, buttonRole: 'button', - containThumbnail: true, smallLabel: false, fluid: false, }, diff --git a/services/madoc-ts/src/extensions/page-blocks/default-definitions.ts b/services/madoc-ts/src/extensions/page-blocks/default-definitions.ts index 88586aa50..8d40c471c 100644 --- a/services/madoc-ts/src/extensions/page-blocks/default-definitions.ts +++ b/services/madoc-ts/src/extensions/page-blocks/default-definitions.ts @@ -1,14 +1,14 @@ -import { CanvasPanelBlock } from '../../frontend/shared/components/CanvasPanelBlock'; -import { SingleCollection } from '../../frontend/shared/components/SingleCollection'; -import { SingleProject } from '../../frontend/shared/components/SingleProject'; +import { CanvasPanelBlock } from '../../frontend/site/blocks/CanvasPanelBlock'; +import { SingleCollection } from '../../frontend/site/blocks/SingleCollection'; +import { SingleProject } from '../../frontend/site/blocks/SingleProject'; import { Surface } from '../../frontend/shared/layout/Surface'; -import { FeaturedItem } from '../../frontend/shared/components/FeaturedItem'; -import { CrowdSourcingBanner } from '../../frontend/shared/components/CrowdSourcingBanner'; +import { FeaturedItem } from '../../frontend/site/blocks/FeaturedItem'; +import { CrowdSourcingBanner } from '../../frontend/site/blocks/CrowdSourcingBanner'; import { PageBlockDefinition } from './extension'; import simpleHtmlBlock from './simple-html-block/simple-html-block'; import currentManifest from './current-manifest-snippet-block'; import simpleMarkdownBlock from './simple-markdown-block/simple-markdown-block'; -import { EmbedItem } from '../../frontend/site/features/EmbedItem'; +import { EmbedItem } from '../../frontend/site/blocks/EmbedItem'; export function getDefaultPageBlockDefinitions(): PageBlockDefinition[] { return [ diff --git a/services/madoc-ts/src/extensions/projects/types.ts b/services/madoc-ts/src/extensions/projects/types.ts index 7acfd0e61..419275cb6 100644 --- a/services/madoc-ts/src/extensions/projects/types.ts +++ b/services/madoc-ts/src/extensions/projects/types.ts @@ -52,7 +52,7 @@ export type JsonProjectTemplate = { export type CaptureModelShorthand = Record< Keys, - string | Partial | Partial + string | Partial | (Partial & Record) >; export type ProjectTemplateConfig = T extends ProjectTemplate ? R : never; diff --git a/services/madoc-ts/src/extensions/site-manager/extension.ts b/services/madoc-ts/src/extensions/site-manager/extension.ts index d539f93b0..db306ebc0 100644 --- a/services/madoc-ts/src/extensions/site-manager/extension.ts +++ b/services/madoc-ts/src/extensions/site-manager/extension.ts @@ -1,6 +1,9 @@ import { stringify } from 'query-string'; import { ApiClient } from '../../gateway/api'; +import { AwardBadgeRequest, Badge, BadgeAward, CreateBadgeRequest } from '../../types/badges'; import { Pagination } from '../../types/schemas/_pagination'; +import { SiteTerms } from '../../types/site-terms'; +import { TermConfiguration, TermConfigurationRequest } from '../../types/term-configurations'; import { BaseExtension, defaultDispose } from '../extension-manager'; import { CreateSiteRequest, @@ -175,4 +178,115 @@ export class SiteManagerExtension implements BaseExtension { body: config, }); } + + // Term configurations + async getTermConfiguration(id: string) { + return this.api.request(`/api/madoc/term-configuration/${id}`); + } + + async updateTermConfiguration(id: string, config: TermConfigurationRequest & { id: string }) { + return this.api.request(`/api/madoc/term-configuration/${id}`, { + method: 'PUT', + body: config, + }); + } + + async createTermConfiguration(config: TermConfigurationRequest) { + return this.api.request(`/api/madoc/term-configuration`, { + method: 'POST', + body: config, + }); + } + + async deleteTermConfiguration(id: string) { + return this.api.request(`/api/madoc/term-configuration/${id}`, { + method: 'DELETE', + }); + } + + async getAllTermConfigurations() { + return this.api.request<{ termConfigurations: TermConfiguration[] }>(`/api/madoc/term-configuration`); + } + + // Badges + async createBadge(badge: CreateBadgeRequest) { + return this.api.request(`/api/madoc/badges`, { + method: 'POST', + body: badge, + }); + } + + async getBadge(id: string) { + return this.api.request(`/api/madoc/badges/${id}`); + } + + async updateBadge(id: string, badge: CreateBadgeRequest) { + return this.api.request(`/api/madoc/badges/${id}`, { + method: 'PUT', + body: badge, + }); + } + + async deleteBadge(id: string) { + await this.api.request(`/api/madoc/badges/${id}`, { + method: 'DELETE', + }); + } + + async listBadges() { + return this.api.request<{ badges: Badge[] }>(`/api/madoc/badges`); + } + + //User badges + async listUserAwardedBadges(userId: number) { + return this.api.request<{ awards: BadgeAward[] }>(`/api/madoc/users/${userId}/badges`); + } + + async getAwardedBadge(userId: number, id: string) { + return this.api.request(`/api/madoc/users/${userId}/badges/${id}`); + } + + async awardBadge(userId: number, badge: AwardBadgeRequest) { + return this.api.request(`/api/madoc/users/${userId}/badges`, { + method: 'POST', + body: badge, + }); + } + + async removeAwardedBadge(userId: number, id: string) { + await this.api.request(`/api/madoc/users/${userId}/badges/${id}`, { + method: 'DELETE', + }); + } + + async listTerms() { + return this.api.request<{ terms: SiteTerms[] }>(`/api/madoc/terms`); + } + + async getLatestTerms() { + return this.api.request<{ latest: SiteTerms; list: Omit[] }>(`/api/madoc/terms/latest`); + } + + async acceptTerms() { + return this.api.request(`/api/madoc/terms/accept`, { + method: 'POST', + }); + } + + async getTermsById(termsId: string) { + return this.api.request(`/api/madoc/terms/${termsId}`); + } + + async createTerms(terms: { text: string; markdown: string }) { + return this.api.request(`/api/madoc/terms`, { + method: 'POST', + body: terms, + }); + } + + async deleteTerms(termsId: string) { + return this.api.request(`/api/madoc/terms/${termsId}`, { + method: 'DELETE', + }); + } } diff --git a/services/madoc-ts/src/extensions/site-manager/types.ts b/services/madoc-ts/src/extensions/site-manager/types.ts index d3302d4ed..55415986e 100644 --- a/services/madoc-ts/src/extensions/site-manager/types.ts +++ b/services/madoc-ts/src/extensions/site-manager/types.ts @@ -1,4 +1,5 @@ import { InternationalString } from '@iiif/presentation-3'; +import { BaseTask } from '../../gateway/tasks/base-task'; export type Site = { id: number; @@ -10,6 +11,7 @@ export type Site = { modified?: Date; owner?: { id: number; name?: string }; config: SiteSystemConfig; + latestTerms?: string; }; export type SiteSystemConfig = { @@ -18,12 +20,20 @@ export type SiteSystemConfig = { emailActivation: boolean; enableNotifications: boolean; autoPublishImport: boolean; + loginHeader?: string; + loginFooter?: string; + registerHeader?: string; + registerFooter?: string; }; +// Note: these can never be optional - breaks the setting. +// There is a default in site-user-repository.ts that has to be updated. export type SystemConfig = { installationTitle: string; defaultSite: string | null; autoPublishImport: boolean | null; + builtInUserProfile: Record; + userProfileModel: string; } & SiteSystemConfig; export type CreateSiteRequest = { @@ -72,6 +82,7 @@ export type UserRowWithoutPassword = { role: string; site_role?: string; is_active: boolean; + terms_accepted?: string[]; created_by?: number | null; automated: boolean; config?: any | null; @@ -121,6 +132,7 @@ export type UserCreationRequest = { export type SiteUser = { id: number; name: string; + terms_accepted?: string[]; /** * This is always the global role, never the site role. */ @@ -137,6 +149,52 @@ export type SiteUser = { export type CurrentUserWithScope = SiteUser & { scope: string[]; + preferences: UserPreferences; + information: UserInformation; + terms?: { + hasTerms: boolean; + hasAccepted: boolean; + }; +}; + +export type PublicUserProfile = { + user: { + id: number; + name: string; + email?: string; + automated?: boolean; + site_role?: string; + config?: UserConfig | null; + }; + infoLabels: Record; + info: Record<'email' | 'contributions' | 'contributionStatistics' | 'awards', string> & Record; + statistics?: Record< + 'crowdsourcing' | 'reviews', + { + statuses: Record; + total: number; + } + >; + recentTasks?: BaseTask[]; +}; + +export type UserPreferences = { + visibility?: Record< + 'email' | 'contributions' | 'contributionStatistics' | 'awards' | 'gravitar' | string, + 'public' | 'staff' | 'only-me' + >; +}; +export type UserInformation = Record; + +export type UserInformationRequest = { + fields: Record< + string, + { + value: any; + visibility: 'public' | 'staff' | 'only-me'; + } + >; + extraVisibility: Record; }; export type UpdateUser = { diff --git a/services/madoc-ts/src/extensions/tasks/extension.ts b/services/madoc-ts/src/extensions/tasks/extension.ts index 727389e01..d2b4b03a5 100644 --- a/services/madoc-ts/src/extensions/tasks/extension.ts +++ b/services/madoc-ts/src/extensions/tasks/extension.ts @@ -1,7 +1,9 @@ import { ApiClient } from '../../gateway/api'; import { BaseTask } from '../../gateway/tasks/base-task'; import { BaseExtension, defaultDispose } from '../extension-manager'; +import { ProjectResolver } from './resolvers/project-resolver'; import { Resolver } from './resolvers/resolver'; +import { SelectorThumbnailResolver } from './resolvers/selector-thumbnail'; import { SubjectResolver } from './resolvers/subject-resolver'; export class TaskExtension implements BaseExtension { @@ -10,7 +12,7 @@ export class TaskExtension implements BaseExtension { constructor(api: ApiClient) { this.api = api; - this.resolvers = [new SubjectResolver(api)]; + this.resolvers = [new SubjectResolver(api), new ProjectResolver(api), new SelectorThumbnailResolver(api)]; } dispose() { diff --git a/services/madoc-ts/src/extensions/tasks/resolvers/project-resolver.ts b/services/madoc-ts/src/extensions/tasks/resolvers/project-resolver.ts new file mode 100644 index 000000000..b38016181 --- /dev/null +++ b/services/madoc-ts/src/extensions/tasks/resolvers/project-resolver.ts @@ -0,0 +1,66 @@ +import { InternationalString } from '@iiif/presentation-3'; +import { ApiClient } from '../../../gateway/api'; +import { BaseTask } from '../../../gateway/tasks/base-task'; +import { Resolver } from './resolver'; + +export type ProjectTaskMetadata = { + slug: string; + label: InternationalString; +}; + +export class ProjectResolver implements Resolver<'project', ProjectTaskMetadata | null> { + api: ApiClient; + constructor(api: ApiClient) { + this.api = api; + } + + getKey() { + return 'project' as const; + } + + hasMetadata(task: BaseTask) { + const metadata = task.metadata; + + if (!task.root_task) { + return true; + } + + if (task.type !== 'crowdsourcing-task') { + return true; + } + + if (!metadata) { + return false; + } + + if (typeof metadata.project === 'undefined') { + return false; + } + + // Otherwise it should be up-to-date. + return true; + } + + async resolve(task: BaseTask) { + try { + if (!task.root_task) { + return null; + } + + const project = await this.api.getProjectByTaskId(task.root_task); + + if (project.id === -1) { + return null; + } + + return { + id: project.id, + slug: project.slug, + label: project.label, + }; + } catch (e) { + console.log('error', e); + return undefined; + } + } +} diff --git a/services/madoc-ts/src/extensions/tasks/resolvers/selector-thumbnail.ts b/services/madoc-ts/src/extensions/tasks/resolvers/selector-thumbnail.ts new file mode 100644 index 000000000..27c0b9bc9 --- /dev/null +++ b/services/madoc-ts/src/extensions/tasks/resolvers/selector-thumbnail.ts @@ -0,0 +1,66 @@ +import { ApiClient } from '../../../gateway/api'; +import { BaseTask } from '../../../gateway/tasks/base-task'; +import { Resolver } from './resolver'; + +export type SelectorThumbnail = { + svg: string; +}; + +export class SelectorThumbnailResolver implements Resolver<'selectorThumbnail', SelectorThumbnail | null> { + api: ApiClient; + constructor(api: ApiClient) { + this.api = api; + } + + getKey() { + return 'selectorThumbnail' as const; + } + + hasMetadata(task: BaseTask) { + const metadata = task.metadata; + + if (!task.root_task) { + return true; + } + + if (task.type !== 'crowdsourcing-task') { + return true; + } + + if (task.status !== 3) { + return true; + } + + if (!metadata) { + return false; + } + + if (typeof metadata.selectorThumbnail === 'undefined' || metadata.selectorThumbnail === null) { + return false; + } + + // Otherwise it should be up-to-date. + return true; + } + + async resolve(task: BaseTask) { + try { + if (!task.id) { + return null; + } + + const resp = await this.api.getProjectSVG('any', task.id); + + if (!resp || resp.empty) { + return null; + } + + return { + svg: resp.svg, + }; + } catch (e) { + console.log('error', e); + return null; + } + } +} diff --git a/services/madoc-ts/src/frontend/shared/components/AdminMenu.tsx b/services/madoc-ts/src/frontend/admin/components/AdminMenu.tsx similarity index 99% rename from services/madoc-ts/src/frontend/shared/components/AdminMenu.tsx rename to services/madoc-ts/src/frontend/admin/components/AdminMenu.tsx index c84c4dfdc..70d6055e2 100644 --- a/services/madoc-ts/src/frontend/shared/components/AdminMenu.tsx +++ b/services/madoc-ts/src/frontend/admin/components/AdminMenu.tsx @@ -1,7 +1,7 @@ import React, { useContext, useMemo, useRef } from 'react'; import { useLocation } from 'react-router-dom'; import styled, { css } from 'styled-components'; -import { SettingsIcon } from '../icons/SettingsIcon'; +import { SettingsIcon } from '../../shared/icons/SettingsIcon'; // Icons diff --git a/services/madoc-ts/src/frontend/admin/index.css b/services/madoc-ts/src/frontend/admin/index.css new file mode 100644 index 000000000..3ada9bc2f --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/index.css @@ -0,0 +1,4 @@ +/* Don't include the reset for the admin - too much to fix. */ +@tailwind components; +@tailwind utilities; + diff --git a/services/madoc-ts/src/frontend/admin/index.tsx b/services/madoc-ts/src/frontend/admin/index.tsx index a4dbf2685..00a46df23 100644 --- a/services/madoc-ts/src/frontend/admin/index.tsx +++ b/services/madoc-ts/src/frontend/admin/index.tsx @@ -4,7 +4,7 @@ import { CurrentUserWithScope, Site, SystemConfig } from '../../extensions/site- import { ApiClient } from '../../gateway/api'; import { useTranslation } from 'react-i18next'; import { GlobalStyles } from '../shared/typography/GlobalStyles'; -import { AdminLayoutContainer, AdminLayoutMain, AdminLayoutMenu } from '../shared/components/AdminMenu'; +import { AdminLayoutContainer, AdminLayoutMain, AdminLayoutMenu } from './components/AdminMenu'; import { UserBar } from '../shared/components/UserBar'; import { RenderConfigRoutes } from '../shared/utility/server-utils'; import { ApiContext, useIsApiRestarting } from '../shared/hooks/use-api'; @@ -12,6 +12,7 @@ import { ErrorMessage } from '../shared/callouts/ErrorMessage'; import '../shared/capture-models/plugins'; import { SiteProvider } from '../shared/hooks/use-site'; import { AdminSidebar } from './molecules/AdminSidebar'; +import './index.css'; export type AdminAppProps = { jwt?: string; diff --git a/services/madoc-ts/src/frontend/admin/molecules/AdminSidebar.tsx b/services/madoc-ts/src/frontend/admin/molecules/AdminSidebar.tsx index 320c946c1..499619108 100644 --- a/services/madoc-ts/src/frontend/admin/molecules/AdminSidebar.tsx +++ b/services/madoc-ts/src/frontend/admin/molecules/AdminSidebar.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; +import { ModelDocumentIcon } from '../../shared/icons/ModelDocumentIcon'; import { AdminMenuContainer, AdminMenuItem, @@ -21,7 +22,7 @@ import { SiteSwitcherBackButton, SiteSwitcherContainer, SiteSwitcherSiteName, -} from '../../shared/components/AdminMenu'; +} from '../components/AdminMenu'; import { useSite, useUser } from '../../shared/hooks/use-site'; import { HrefLink } from '../../shared/utility/href-link'; @@ -43,6 +44,7 @@ export const AdminSidebar: React.FC = () => { isManageManifests, isLocalisation, isMedia, + isPageBlocks, isSiteGlobal, } = useMemo(() => { return { @@ -53,6 +55,7 @@ export const AdminSidebar: React.FC = () => { pathname.startsWith('/import/manifest') || pathname.startsWith('/enrichment/ocr'), isProjects: pathname.startsWith('/projects'), + isPageBlocks: pathname.startsWith('/page-blocks'), isSiteConfiguration: pathname.startsWith('/configure') || pathname.startsWith('/system') || @@ -137,6 +140,15 @@ export const AdminSidebar: React.FC = () => { + + + + + + {t('Site pages')} + + + @@ -181,6 +193,12 @@ export const AdminSidebar: React.FC = () => { {t('Site permissions')} + + {t('Site terms and conditions')} + + + {t('External terms list')} + {t('Invitations')} diff --git a/services/madoc-ts/src/frontend/admin/molecules/MetadataEditor.tsx b/services/madoc-ts/src/frontend/admin/molecules/MetadataEditor.tsx index 752523648..917915834 100644 --- a/services/madoc-ts/src/frontend/admin/molecules/MetadataEditor.tsx +++ b/services/madoc-ts/src/frontend/admin/molecules/MetadataEditor.tsx @@ -32,6 +32,7 @@ export type MetadataEditorProps = { availableLanguages: string[]; defaultLocale?: string; allowCustomLanguage?: boolean; + multiline?: boolean; label?: string; disabled?: boolean; // Actions. @@ -180,7 +181,8 @@ export const MetadataEditor: React.FC = props => { ); } - const Component: typeof IntlMultiline = api && api.getIsServer() ? (IntlInput as any) : IntlMultiline; + const Component: typeof IntlMultiline = + props.multiline === false || (api && api.getIsServer()) ? (IntlInput as any) : IntlMultiline; return ( diff --git a/services/madoc-ts/src/frontend/admin/molecules/PreviewCollection.tsx b/services/madoc-ts/src/frontend/admin/molecules/PreviewCollection.tsx index fb9a9faf5..19bf460b1 100644 --- a/services/madoc-ts/src/frontend/admin/molecules/PreviewCollection.tsx +++ b/services/madoc-ts/src/frontend/admin/molecules/PreviewCollection.tsx @@ -49,18 +49,20 @@ export const PreviewCollection: React.FC<{ ); useVaultEffect( - vault => { + async vault => { if (props.manifestIds && props.manifestIds.length) { const mans: ManifestNormalized[] = []; - props.manifestIds.forEach(id => { - vault.loadManifest(id).then(man => { - if (man?.type !== 'Manifest') { - setError('Invalid manifest'); - } else { - mans.push(man); - } - }); - }); + await Promise.all( + props.manifestIds.map(async id => { + await vault.loadManifest(id).then(man => { + if (man?.type !== 'Manifest') { + setError('Invalid manifest'); + } else { + mans.push(man); + } + }); + }) + ); setManifests(mans); } }, diff --git a/services/madoc-ts/src/frontend/admin/pages/badges/create-badge.tsx b/services/madoc-ts/src/frontend/admin/pages/badges/create-badge.tsx new file mode 100644 index 000000000..828a2b351 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/badges/create-badge.tsx @@ -0,0 +1,117 @@ +import { InternationalString } from '@iiif/presentation-3'; +import React, { useState } from 'react'; +import { useMutation } from 'react-query'; +import { SystemListItem } from '../../../shared/atoms/SystemListItem'; +import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; +import { InternationalField } from '../../../shared/capture-models/editor/input-types/InternationalField/InternationalField'; +import { TextField } from '../../../shared/capture-models/editor/input-types/TextField/TextField'; +import { InputContainer, InputLabel } from '../../../shared/form/Input'; +import { LanguageFieldEditor } from '../../../shared/form/LanguageFieldEditor'; +import { useApi } from '../../../shared/hooks/use-api'; +import { Button } from '../../../shared/navigation/Button'; +import { HrefLink } from '../../../shared/utility/href-link'; + +export function CreateBadge() { + const api = useApi(); + + const [label, setLabel] = useState({ en: [''] }); + const [description, setDescription] = useState({ en: [''] }); + const [svg, setSvg] = useState(''); + const [colors, setColors] = useState(''); + + const [createBadge, createBadgeStatus] = useMutation(async () => { + if (!label.en || !label.en[0]) { + throw new Error('Label is required'); + } + + if (!svg) { + throw new Error('SVG is required'); + } + + return api.siteManager.createBadge({ + label, + description, + svg, + tier_colors: colors.split(','), + }); + }); + + if (createBadgeStatus.isLoading) { + return
Creating badge...
; + } + + if (createBadgeStatus.isSuccess) { + return ( +
+

Badge created

+ Go to badge +
+ ); + } + + return ( + +
+

Create badge

+ + {createBadgeStatus.error ? {(createBadgeStatus.error as any).message} : null} + + + Label + + + + Description + + + + + SVG code +

+ The SVG you provide will have access to their tier colour via var(--award-tier) in the css +

+ `} + /> +
+ + + Colour tiers (comma separated each hex colour) + + + + +
+
+ ); +} diff --git a/services/madoc-ts/src/frontend/admin/pages/badges/index.tsx b/services/madoc-ts/src/frontend/admin/pages/badges/index.tsx new file mode 100644 index 000000000..71951e854 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/badges/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Outlet } from 'react-router-dom'; +import { SystemBackground } from '../../../shared/atoms/SystemUI'; +import { AdminHeader } from '../../molecules/AdminHeader'; + +export function Badges() { + const { t } = useTranslation(); + return ( + <> + + + + + + ); +} diff --git a/services/madoc-ts/src/frontend/admin/pages/badges/list-badges.tsx b/services/madoc-ts/src/frontend/admin/pages/badges/list-badges.tsx new file mode 100644 index 000000000..30e9f1bd7 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/badges/list-badges.tsx @@ -0,0 +1,77 @@ +import React, { useEffect } from 'react'; +import { Badge } from '../../../../types/badges'; +import { SystemListItem } from '../../../shared/atoms/SystemListItem'; +import { + SystemListingDescription, + SystemListingLabel, + SystemListingThumbnail, + SystemMetadata, +} from '../../../shared/atoms/SystemUI'; +import { LocaleString } from '../../../shared/components/LocaleString'; +import { useData } from '../../../shared/hooks/use-data'; +import { Button } from '../../../shared/navigation/Button'; +import { serverRendererFor } from '../../../shared/plugins/external/server-renderer-for'; +import { HrefLink } from '../../../shared/utility/href-link'; + +export function ListBadges() { + const { data } = useData<{ badges: Badge[] }>(ListBadges); + + if (!data) { + return
Loading...
; + } + + return ( +
+ +
+ +
+
+ {data.badges.map(badge => { + return ; + })} +
+ ); +} + +function SingleBadge({ badge }: { badge: Badge }) { + const [currentTier, setCurrentTier] = React.useState(0); + + // Change on timer + useEffect(() => { + const timer = setInterval(() => { + setCurrentTier(t => t + 1); + }, 2000); + + return () => { + clearInterval(timer); + }; + }, []); + + const tier = badge.tier_colors ? badge.tier_colors[currentTier % badge.tier_colors.length] : '#000'; + + return ( + + +
+
+
+ + + + {badge.label} + + {badge.description} + + + ); +} + +serverRendererFor(ListBadges, { + getKey: () => ['list-badges', {}], + getData: async (key, vars, api) => { + return api.siteManager.listBadges(); + }, +}); diff --git a/services/madoc-ts/src/frontend/admin/pages/badges/view-badge.tsx b/services/madoc-ts/src/frontend/admin/pages/badges/view-badge.tsx new file mode 100644 index 000000000..29f52964d --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/badges/view-badge.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { useMutation } from 'react-query'; +import { useNavigate } from 'react-router-dom'; +import { Badge } from '../../../../types/badges'; +import { SystemListItem } from '../../../shared/atoms/SystemListItem'; +import { ConfirmButton } from '../../../shared/capture-models/editor/atoms/ConfirmButton'; +import { LocaleString } from '../../../shared/components/LocaleString'; +import { useApi } from '../../../shared/hooks/use-api'; +import { useData } from '../../../shared/hooks/use-data'; +import { Button } from '../../../shared/navigation/Button'; +import { serverRendererFor } from '../../../shared/plugins/external/server-renderer-for'; +import { Heading1, Subheading1 } from '../../../shared/typography/Heading1'; + +export function ViewBadge() { + const { data } = useData(ViewBadge); + const api = useApi(); + const navigate = useNavigate(); + const [deleteBadge, deleteBadgeStatus] = useMutation(async () => { + if (data) { + await api.siteManager.deleteBadge(data.id); + navigate('/configure/site/badges'); + } + }); + + if (!data || deleteBadgeStatus.isLoading) { + return
Loading...
; + } + + return ( + <> + +
+ {data.label} + {data.description} +

Tiers

+
+ {data.tier_colors ? ( + data.tier_colors.map((color, index) => { + return ( +
+
+
+ ); + }) + ) : ( +
+
+
+ )} +
+
+ + +
+ + + +
+
+ + ); +} + +serverRendererFor(ViewBadge, { + getKey: params => ['view-badge', { id: params.id }], + getData: async (key, vars, api) => { + return api.siteManager.getBadge(vars.id); + }, +}); diff --git a/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas-details.tsx b/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas-details.tsx index e82fdd5bd..9a356ef2d 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas-details.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas-details.tsx @@ -6,7 +6,7 @@ import { useData } from '../../../../shared/hooks/use-data'; import { createUniversalComponent } from '../../../../shared/utility/create-universal-component'; import { CanvasContext, useVaultEffect, VaultProvider } from 'react-iiif-vault'; import { CanvasNormalized } from '@iiif/presentation-3'; -import { SimpleAtlasViewer } from '../../../../shared/components/SimpleAtlasViewer'; +import { SimpleAtlasViewer } from '../../../../shared/features/SimpleAtlasViewer'; type CanvasDetailsType = { params: { id: number; manifestId: number }; diff --git a/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas-search-index.tsx b/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas-search-index.tsx index 3431cd655..726a3c8ee 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas-search-index.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas-search-index.tsx @@ -15,7 +15,11 @@ type CanvasSearchIndexType = { export const CanvasSearchIndex = createUniversalComponent( () => { - const { data, isError, isFetching, refetch } = useData(CanvasSearchIndex, {}, { retry: 0 }); + const { data, isError, isLoading: isCanvasLoading, isFetching, refetch } = useData( + CanvasSearchIndex, + {}, + { retry: 0 } + ); const { id } = useParams<{ id: string }>(); const api = useApi(); @@ -24,21 +28,21 @@ export const CanvasSearchIndex = createUniversalComponent await refetch(); }); - if (isError) { + if (isCanvasLoading) { + return
loading...
; + } + + if (isError || !data?.canvas) { return (
Item is not in the search index
); } - if (!data || isFetching) { - return
loading...
; - } - return (

This canvas in the search index.

diff --git a/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas.tsx b/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas.tsx index b073c16a0..be40b8d5e 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/canvases/canvas.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { CanvasNavigation } from '../../../../shared/components/CanvasNavigation'; +import { CanvasNavigation } from '../../../../shared/features/CanvasNavigation'; import { useSite } from '../../../../shared/hooks/use-site'; import { UniversalComponent } from '../../../../types'; import { LocaleString } from '../../../../shared/components/LocaleString'; @@ -12,7 +12,6 @@ import { createUniversalComponent } from '../../../../shared/utility/create-univ import { useApiManifest } from '../../../../shared/hooks/use-api-manifest'; import { AdminHeader } from '../../../molecules/AdminHeader'; import { WidePage } from '../../../../shared/layout/WidePage'; -import { ManifestFull } from '../../../../../types/schemas/manifest-full'; type CanvasViewType = { data: CanvasFull; diff --git a/services/madoc-ts/src/frontend/admin/pages/content/manifests/create-manifest.tsx b/services/madoc-ts/src/frontend/admin/pages/content/manifests/create-manifest.tsx index d0d5f6628..5d356cc7e 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/manifests/create-manifest.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/manifests/create-manifest.tsx @@ -17,7 +17,7 @@ import { GridContainer, HalfGird } from '../../../../shared/layout/Grid'; import { serverRendererFor } from '../../../../shared/plugins/external/server-renderer-for'; import { Heading3 } from '../../../../shared/typography/Heading3'; import { Button, SmallButton } from '../../../../shared/navigation/Button'; -import {Input, InputContainer, InputLabel, InputSubLabel} from '../../../../shared/form/Input'; +import { Input, InputContainer, InputLabel, InputSubLabel } from '../../../../shared/form/Input'; import { BrowserComponent } from '../../../../shared/utility/browser-component'; import { HrefLink } from '../../../../shared/utility/href-link'; import { Pagination } from '../../../molecules/Pagination'; diff --git a/services/madoc-ts/src/frontend/admin/pages/content/manifests/edit-manifest-structure.tsx b/services/madoc-ts/src/frontend/admin/pages/content/manifests/edit-manifest-structure.tsx index 04023deed..c6a6521f2 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/manifests/edit-manifest-structure.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/manifests/edit-manifest-structure.tsx @@ -4,15 +4,12 @@ import { UpdateManifestDetailsRequest } from '../../../../../routes/iiif/manifes import { SnippetThumbnail, SnippetThumbnailContainer } from '../../../../shared/atoms/SnippetLarge'; import { Input, InputContainer, InputLabel } from '../../../../shared/form/Input'; import { apiHooks } from '../../../../shared/hooks/use-api-query'; -import { useManifest } from '../../../../site/hooks/use-manifest'; import { UniversalComponent } from '../../../../types'; import { ItemStructureList } from '../../../../../types/schemas/item-structure-list'; import { LocaleString } from '../../../../shared/components/LocaleString'; import { useApi } from '../../../../shared/hooks/use-api'; -import { Link, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { Button, SmallButton, TinyButton } from '../../../../shared/navigation/Button'; -import { ContextHeading, Header } from '../../../../shared/atoms/Header'; -import { Subheading1 } from '../../../../shared/typography/Heading1'; import { ReorderTable, ReorderTableRow } from '../../../../shared/atoms/ReorderTable'; import { resetServerContext } from 'react-beautiful-dnd'; import { TableActions, TableContainer, TableRow, TableRowLabel } from '../../../../shared/layout/Table'; diff --git a/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-collections.tsx b/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-collections.tsx index 0f5820e8e..9c05fe2ac 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-collections.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-collections.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { CollectionSnippet } from '../../../../shared/components/CollectionSnippet'; +import { CollectionSnippet } from '../../../../shared/features/CollectionSnippet'; import { SmallButton } from '../../../../shared/navigation/Button'; import { Link } from 'react-router-dom'; import { useData } from '../../../../shared/hooks/use-data'; diff --git a/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-projects.tsx b/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-projects.tsx index 7ec62002c..acd1c221a 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-projects.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-projects.tsx @@ -43,11 +43,9 @@ export const ManifestProjects = createUniversalComponent(
{projects.map(project => ( {project.label}} - portrait={false} subtitle={{project.summary}} link={`/projects/${project.id}`} buttonText="view project" diff --git a/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-search-index.tsx b/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-search-index.tsx index e7789b127..2e70e9af0 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-search-index.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/manifests/manifest-search-index.tsx @@ -1,7 +1,5 @@ -import React, { useState } from 'react'; -import { useMutation } from 'react-query'; +import React from 'react'; import { Button } from '../../../../shared/navigation/Button'; -import { useApi } from '../../../../shared/hooks/use-api'; import { useData } from '../../../../shared/hooks/use-data'; import { createUniversalComponent } from '../../../../shared/utility/create-universal-component'; import { useParams } from 'react-router-dom'; @@ -17,7 +15,11 @@ type ManifestSearchIndexType = { export const ManifestSearchIndex = createUniversalComponent( () => { - const { data, isError, refetch } = useData(ManifestSearchIndex, {}, { retry: 0 }); + const { data, isLoading: isManifestLoading, refetch } = useData( + ManifestSearchIndex, + {}, + { retry: 0, useErrorBoundary: false } + ); const { data: structure } = useData(EditManifestStructure); const { id } = useParams<{ id: string }>(); const totalCanvases = structure?.items.length || 0; @@ -25,7 +27,11 @@ export const ManifestSearchIndex = createUniversalComponentloading...
; + } + + if (!data) { return (
Item is not in the search index @@ -36,10 +42,6 @@ export const ManifestSearchIndex = createUniversalComponentloading...
; - } - return (

This manifest in the search index.

diff --git a/services/madoc-ts/src/frontend/admin/pages/content/media/view-media.tsx b/services/madoc-ts/src/frontend/admin/pages/content/media/view-media.tsx index 80d1fa211..173db7164 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/media/view-media.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/media/view-media.tsx @@ -9,7 +9,7 @@ import { Heading5 } from '../../../../shared/typography/Heading5'; import { CroppedImage } from '../../../../shared/atoms/Images'; import { ImageStripBox } from '../../../../shared/atoms/ImageStrip'; import { ModalButton } from '../../../../shared/components/Modal'; -import { SimpleAtlasViewer } from '../../../../shared/components/SimpleAtlasViewer'; +import { SimpleAtlasViewer } from '../../../../shared/features/SimpleAtlasViewer'; import { useApi } from '../../../../shared/hooks/use-api'; import { useData } from '../../../../shared/hooks/use-data'; import { useLocationQuery } from '../../../../shared/hooks/use-location-query'; diff --git a/services/madoc-ts/src/frontend/admin/pages/content/metadata-configuration.tsx b/services/madoc-ts/src/frontend/admin/pages/content/metadata-configuration.tsx index 9c81ea9a4..2799614ce 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/metadata-configuration.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/metadata-configuration.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useMutation } from 'react-query'; import { WidePage } from '../../../shared/layout/WidePage'; -import { FacetConfig, MetadataFacetEditor } from '../../../shared/components/MetadataFacetEditor'; +import { FacetConfig, MetadataFacetEditor } from '../../../shared/features/MetadataFacetEditor'; import { useApi } from '../../../shared/hooks/use-api'; import { apiHooks } from '../../../shared/hooks/use-api-query'; import { AdminHeader } from '../../molecules/AdminHeader'; diff --git a/services/madoc-ts/src/frontend/admin/pages/content/site-configuration.tsx b/services/madoc-ts/src/frontend/admin/pages/content/site-configuration.tsx index fd5374936..6167fd546 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/site-configuration.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/site-configuration.tsx @@ -53,6 +53,21 @@ export const SiteConfiguration: React.FC = () => { Change the site-wide configuration, cannot be overridden by project. + + + Add or remove external term lists used for auto-completion. + + + + + Update terms and conditions of the site + + + + + Add or remove badges that can be awarded to users. + +
diff --git a/services/madoc-ts/src/frontend/admin/pages/content/system-configuration.tsx b/services/madoc-ts/src/frontend/admin/pages/content/system-configuration.tsx index a3b627907..d33ed94df 100644 --- a/services/madoc-ts/src/frontend/admin/pages/content/system-configuration.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/content/system-configuration.tsx @@ -23,15 +23,52 @@ const systemConfigModel = { type: 'checkbox-field', inlineLabel: 'Automatically publish manifest after importing', }, + // Login/Register messages + loginHeader: { + label: 'Login header message', + description: 'Message to display above the login form', + type: 'text-field', + multiline: true, + minLines: 4, + }, + loginFooter: { + label: 'Login footer message', + description: 'Message to display below the login form', + type: 'text-field', + multiline: true, + minLines: 4, + }, + registerHeader: { + label: 'Register header message', + description: 'Message to display above the registration form', + type: 'text-field', + multiline: true, + minLines: 4, + }, + registerFooter: { + label: 'Register footer message', + description: 'Message to display below the registration form', + type: 'text-field', + multiline: true, + minLines: 4, + }, }; export const SiteSystemConfiguration: React.FC = () => { const api = useApi(); - const config = useSystemConfig(); + const savedConfig = useSystemConfig(); const updateConfig = useUpdateSystemConfig(); const navigate = useNavigate(); const site = useSite(); + const config = { + loginHeader: '', + loginFooter: '', + registerHeader: '', + registerFooter: '', + ...(savedConfig as any), + }; + const [updateSystemConfig] = useMutation(async (newConfig: any) => { await api.siteManager.updateSite({ config: newConfig, diff --git a/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/model-editor/full-document-editor.tsx b/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/model-editor/full-document-editor.tsx index 9d473422a..11fc75cde 100644 --- a/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/model-editor/full-document-editor.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/model-editor/full-document-editor.tsx @@ -40,6 +40,7 @@ export const FullDocumentEditor: React.FC = () => { setLabel={actions.setLabel} setAllowMultiple={actions.setAllowMultiple} setRequired={actions.setRequired} + setDependant={actions.setDependant} setLabelledBy={actions.setLabelledBy} setPluralLabel={actions.setPluralLabel} deselectField={actions.deselectField} @@ -69,6 +70,7 @@ export const FullDocumentEditor: React.FC = () => {
(null); + const api = useApi(); + const banner = data?.placeholderImage; + const projectId = data?.id; + + const [changeBanner, changeBannerStatus] = useMutation(async (newBanner: string | null) => { + if (projectId) { + await api.updateProjectBanner(projectId, newBanner); + } + await refetch(); + }); + + if (isEditingBanner) { + return ( +
+ { + setTempBanner(value?.image || null); + changeBanner(value?.image || null).then(() => { + setTempBanner(null); + }); + setIsEditingBanner(false); + }} + /> +
+ ); + } + + if (banner || tempBanner) { + return ( +
+

Banner image

+ banner + + + + +
+ ); + } + + return ( +
+

Banner image

+ +
+ ); +} + +serverRendererFor(ProjectBannerEditor, { + getKey(params) { + return ['project-banner-data', { id: params.id }]; + }, + async getData(key, vars, api) { + return api.getProject(vars.id); + }, +}); diff --git a/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-configuration-old.tsx b/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-configuration-old.tsx index a69768357..c0acde13b 100644 --- a/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-configuration-old.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-configuration-old.tsx @@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom'; import { EmptyState } from '../../../../shared/layout/EmptyState'; import { SuccessMessage } from '../../../../shared/callouts/SuccessMessage'; import { EditShorthandCaptureModel } from '../../../../shared/capture-models/EditorShorthandCaptureModel'; -import { useAdminLayout } from '../../../../shared/components/AdminMenu'; +import { useAdminLayout } from '../../../components/AdminMenu'; import { postProcessConfiguration, siteConfigurationModel } from '../../../../shared/configuration/site-config'; import { useApi } from '../../../../shared/hooks/use-api'; import { apiHooks } from '../../../../shared/hooks/use-api-query'; diff --git a/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-configuration.tsx b/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-configuration.tsx index 337f11a03..10c379d52 100644 --- a/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-configuration.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-configuration.tsx @@ -9,7 +9,7 @@ import { EditorShorthandCaptureModelRef, EditShorthandCaptureModel, } from '../../../../shared/capture-models/EditorShorthandCaptureModel'; -import { useAdminLayout } from '../../../../shared/components/AdminMenu'; +import { useAdminLayout } from '../../../components/AdminMenu'; import { postProcessConfiguration, ProjectConfigContributions, @@ -66,11 +66,6 @@ export const ProjectConfiguration: React.FC = () => { return migrateConfig.version1to2(_projectConfiguration); }, [_projectConfiguration]); - console.log({ - v1: _projectConfiguration, - v2: projectConfiguration, - }); - const { t } = useTranslation(); const [didSave, setDidSave] = useShortMessage(); const projectTemplate = useProjectTemplate(project?.template); @@ -89,7 +84,9 @@ export const ProjectConfiguration: React.FC = () => { ...(otherRef.current?.getData() || {}), }; - await api.saveSiteConfiguration(postProcessConfiguration(config), { project_id: project.id }); + const toSave = migrateConfig.version2to1(config); + + await api.saveSiteConfiguration(postProcessConfiguration(toSave), { project_id: project.id }); await refetch(); setDidSave(); scrollToTop(); diff --git a/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-metadata.tsx b/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-metadata.tsx index ba10017d4..7483bbde6 100644 --- a/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-metadata.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/crowdsourcing/projects/project-metadata.tsx @@ -10,10 +10,11 @@ import { useData } from '../../../../shared/hooks/use-data'; import { createUniversalComponent } from '../../../../shared/utility/create-universal-component'; import { SuccessMessage } from '../../../../shared/callouts/SuccessMessage'; import { ErrorMessage } from '../../../../shared/callouts/ErrorMessage'; +import { ProjectBannerEditor } from './project-banner'; type ProjectMetadataType = { params: { id: number }; - query: {}; + query: any; variables: { id: number }; data: { metadata: ParsedMetadata; @@ -60,7 +61,7 @@ export const ProjectMetadata: UniversalComponent = createUn setSuccess('Changes saved'); } catch (err) { setSuccess(''); - setError(err.message || 'Unknown error'); + setError((err as any).message || 'Unknown error'); } }); @@ -72,6 +73,9 @@ export const ProjectMetadata: UniversalComponent = createUn <> {success ? {success} : null} {error ? {error} : null} + + + = { - enableRegistrations: { - label: 'User registrations', - type: 'checkbox-field', - inlineLabel: 'Allow users to register to the site', - }, - registeredUserTranscriber: { - label: 'User role', - type: 'checkbox-field', - inlineLabel: 'New users can contribute to crowdsourcing projects', - }, - installationTitle: { - label: 'Installation title', - type: 'text-field', - }, - defaultSite: { - label: 'Slug of default site', - type: 'dropdown-field', - }, -}; - export const GlobalSystemConfig: React.FC = () => { const { data, refetch, updatedAt } = useData(GlobalSystemConfig); const api = useApi(); @@ -71,6 +51,7 @@ export const GlobalSystemConfig: React.FC = () => {
{updateSystemConfigStatus.isSuccess ? Config updated : null} = createUniversalCompone {t('Manage collections', { count: 2 })}
  • - {t('Customise site pages')} + {t('Site pages')}
  • {t('Localisation')} diff --git a/services/madoc-ts/src/frontend/admin/pages/site-terms/create-terms.tsx b/services/madoc-ts/src/frontend/admin/pages/site-terms/create-terms.tsx new file mode 100644 index 000000000..e4e80c49f --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/site-terms/create-terms.tsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import { useMutation } from 'react-query'; +import RichMarkdownEditor, { renderToHtml } from 'rich-markdown-editor'; +import styled from 'styled-components'; +import { SiteTerms } from '../../../../types/site-terms'; +import { InfoMessage } from '../../../shared/callouts/InfoMessage'; +import { WarningMessage } from '../../../shared/callouts/WarningMessage'; +import { useApi } from '../../../shared/hooks/use-api'; +import { useData } from '../../../shared/hooks/use-data'; +import { Button, ButtonRow } from '../../../shared/navigation/Button'; +import { serverRendererFor } from '../../../shared/plugins/external/server-renderer-for'; + +const MarkdownEditorWrapper = styled.div` + padding: 0.6em 0.6em 0.6em 2em; + background: #fff; +`; + +export function CreateTerms() { + const [markdown, setMarkdown] = useState(''); + const { data } = useData<{ latest: SiteTerms }>( + CreateTerms, + {}, + { + onSuccess: d => { + setMarkdown(d.latest?.terms?.markdown || ''); + }, + retry: false, + refetchIntervalInBackground: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + } + ); + + const api = useApi(); + + const [createTerms, createTermsStatus] = useMutation(async () => { + const html = renderToHtml(markdown); + const temp = document.createElement('div'); + temp.innerHTML = html; + const text = temp.textContent || temp.innerText || ''; + return await api.siteManager.createTerms({ markdown, text }); + }); + + if (createTermsStatus.isSuccess) { + return ( + <> + Terms and conditions created. +
    + View the terms and conditions here. +
    + + ); + } + + if (!data) { + return null; + } + + return ( + <> + {data.latest?.terms?.markdown ? ( + + Changes to the terms and conditions will prompt new and existing users to accept the new terms. + + ) : ( + + Once you create the terms and conditions, new and existing users will be prompted to accept them. + + )} + + { + setMarkdown(value()); + }} + /> + + + + + + + ); +} + +serverRendererFor(CreateTerms, { + getKey: () => ['site-terms', {}], + getData: (key, vars, api) => { + return api.siteManager.getLatestTerms(); + }, +}); diff --git a/services/madoc-ts/src/frontend/admin/pages/site-terms/index.tsx b/services/madoc-ts/src/frontend/admin/pages/site-terms/index.tsx new file mode 100644 index 000000000..7b506d4aa --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/site-terms/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Outlet } from 'react-router-dom'; +import { SystemBackground } from '../../../shared/atoms/SystemUI'; +import { AdminHeader } from '../../molecules/AdminHeader'; + +export function SiteTerms() { + const { t } = useTranslation(); + return ( + <> + + + + + + ); +} diff --git a/services/madoc-ts/src/frontend/admin/pages/site-terms/list-terms.tsx b/services/madoc-ts/src/frontend/admin/pages/site-terms/list-terms.tsx new file mode 100644 index 000000000..d3e78f352 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/site-terms/list-terms.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { SiteTerms } from '../../../../types/site-terms'; +import { useData } from '../../../shared/hooks/use-data'; +import { serverRendererFor } from '../../../shared/plugins/external/server-renderer-for'; + +export function ListTerms() { + const { data } = useData<{ terms: SiteTerms[] }>(ListTerms); + + return ( +
    + List terms +
    {JSON.stringify(data, null, 2)}
    +
    + ); +} + +serverRendererFor(ListTerms, { + getKey: () => ['site-terms', {}], + getData: (key, vars, api) => { + return api.siteManager.listTerms(); + }, +}); diff --git a/services/madoc-ts/src/frontend/admin/pages/sites/site-permissions.tsx b/services/madoc-ts/src/frontend/admin/pages/sites/site-permissions.tsx index 7938cffc4..a2f027827 100644 --- a/services/madoc-ts/src/frontend/admin/pages/sites/site-permissions.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/sites/site-permissions.tsx @@ -10,7 +10,7 @@ import { DefaultSelect } from '../../../shared/form/DefaulSelect'; import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; import { WidePage } from '../../../shared/layout/WidePage'; import { ModalButton } from '../../../shared/components/Modal'; -import { AutocompleteUser, UserAutocomplete } from '../../../shared/components/UserAutocomplete'; +import { AutocompleteUser, UserAutocomplete } from '../../../site/features/UserAutocomplete'; import { useApi } from '../../../shared/hooks/use-api'; import { useData } from '../../../shared/hooks/use-data'; import { useUserDetails } from '../../../shared/hooks/use-user-details'; diff --git a/services/madoc-ts/src/frontend/admin/pages/system/development-plugin.tsx b/services/madoc-ts/src/frontend/admin/pages/system/development-plugin.tsx index 1467daa90..5fa7ecea6 100644 --- a/services/madoc-ts/src/frontend/admin/pages/system/development-plugin.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/system/development-plugin.tsx @@ -3,6 +3,7 @@ import { useMutation, useQuery } from 'react-query'; import { Navigate } from 'react-router-dom'; import styled from 'styled-components'; import { SitePlugin } from '../../../../types/schemas/plugins'; +import { BoxIcon } from '../../../shared/icons/BoxIcon'; import { Button } from '../../../shared/navigation/Button'; import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; import { useApi } from '../../../shared/hooks/use-api'; @@ -65,14 +66,6 @@ const DevHash = styled.div` margin: 1em 0; `; -function BoxIcon(props: React.SVGProps) { - return ( - - - - ); -} - export const DevelopmentPlugin: React.FC = () => { const api = useApi(); const user = useUser(); diff --git a/services/madoc-ts/src/frontend/admin/pages/system/system-status.tsx b/services/madoc-ts/src/frontend/admin/pages/system/system-status.tsx index 153b4cfc4..38207eb3d 100644 --- a/services/madoc-ts/src/frontend/admin/pages/system/system-status.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/system/system-status.tsx @@ -2,8 +2,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; import { Navigate } from 'react-router-dom'; +import { EnvConfig } from '../../../../types/env-config'; import { Pm2Status } from '../../../../types/pm2'; import { Statistic, StatisticContainer, StatisticLabel, StatisticNumber } from '../../../shared/atoms/Statistics'; +import { TimeAgo } from '../../../shared/atoms/TimeAgo'; import { useApi } from '../../../shared/hooks/use-api'; import { WidePage } from '../../../shared/layout/WidePage'; import { useData } from '../../../shared/hooks/use-data'; @@ -16,7 +18,7 @@ import { UniversalComponent } from '../../../types'; import { AdminHeader } from '../../molecules/AdminHeader'; type SystemStatusType = { - data: { list: Pm2Status[] }; + data: { list: Pm2Status[]; build: EnvConfig['build'] }; query: unknown; params: unknown; variables: unknown; @@ -50,6 +52,12 @@ export const SystemStatus: UniversalComponent = createUniversa }); }); + const [migrateProjectMembers, migrateProjectMembersStatus] = useMutation(async () => { + return api.request(`/api/madoc/system/migrate-project-members`, { + method: 'POST', + }); + }); + const { memory, cpu } = data ? data.list.reduce( (state, next) => { @@ -78,6 +86,47 @@ export const SystemStatus: UniversalComponent = createUniversa + {data ? ( +
    +

    Madoc Version

    +
      +
    • + Version: + {data.build.version.startsWith('v') ? ( + + {data.build.version} + + ) : ( + data.build.version + )} +
    • +
    • + Revision: + {data.build.revision === 'unknown' ? ( + 'unknown' + ) : ( + + {data.build.revision} + + )} +
    • + {data.build.time !== 'unknown' ? ( +
    • + Built at: + +
    • + ) : null} +
    +
    + ) : null} {systemCheck ? (

    {systemCheck.email.enabled ? 'Email server is running' : 'Email server is not running'}

    @@ -107,6 +156,9 @@ export const SystemStatus: UniversalComponent = createUniversa + {migrateModelsStatus.data ? ( @@ -120,6 +172,8 @@ export const SystemStatus: UniversalComponent = createUniversa ) ) : null} + {migrateProjectMembersStatus.data ? Migration complete : null} + {data ? data.list.map(item => { return ( diff --git a/services/madoc-ts/src/frontend/admin/pages/tasks/collection-import-task.tsx b/services/madoc-ts/src/frontend/admin/pages/tasks/collection-import-task.tsx index 32490ce59..acf7dd3fe 100644 --- a/services/madoc-ts/src/frontend/admin/pages/tasks/collection-import-task.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/tasks/collection-import-task.tsx @@ -5,7 +5,7 @@ import { Button } from '../../../shared/navigation/Button'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import { useApi } from '../../../shared/hooks/use-api'; -import { CollectionSnippet } from '../../../shared/components/CollectionSnippet'; +import { CollectionSnippet } from '../../../shared/features/CollectionSnippet'; import { CollapsibleTaskList } from '../../molecules/CollapsibleTaskList'; export const CollectionImportTask: React.FC<{ task: ImportManifestTask; statusBar?: JSX.Element }> = ({ diff --git a/services/madoc-ts/src/frontend/admin/pages/tasks/manifest-import-task.tsx b/services/madoc-ts/src/frontend/admin/pages/tasks/manifest-import-task.tsx index 0c40a30bf..f1d909851 100644 --- a/services/madoc-ts/src/frontend/admin/pages/tasks/manifest-import-task.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/tasks/manifest-import-task.tsx @@ -2,7 +2,7 @@ import { ImportManifestTask } from '../../../../gateway/tasks/import-manifest'; import React from 'react'; import { Button } from '../../../shared/navigation/Button'; import { useTranslation } from 'react-i18next'; -import { ManifestSnippet } from '../../../shared/components/ManifestSnippet'; +import { ManifestSnippet } from '../../../shared/features/ManifestSnippet'; import { GenericTask } from './generic-task'; export const ManifestImportTask: React.FC<{ task: ImportManifestTask; statusBar?: JSX.Element }> = ({ diff --git a/services/madoc-ts/src/frontend/admin/pages/tasks/task-router.tsx b/services/madoc-ts/src/frontend/admin/pages/tasks/task-router.tsx index de1c37bfe..0ba94f1ea 100644 --- a/services/madoc-ts/src/frontend/admin/pages/tasks/task-router.tsx +++ b/services/madoc-ts/src/frontend/admin/pages/tasks/task-router.tsx @@ -1,6 +1,5 @@ import { BaseTask } from '../../../../gateway/tasks/base-task'; import { Status } from '../../../shared/atoms/Status'; -import { FilePreview } from '../../../shared/components/FilePreview'; import { RootStatistics } from '../../../shared/components/RootStatistics'; import { TableContainer, TableRow, TableRowLabel } from '../../../shared/layout/Table'; import { UniversalComponent } from '../../../types'; @@ -16,7 +15,7 @@ import { useTranslation } from 'react-i18next'; import { SubtaskProgress } from '../../../shared/atoms/SubtaskProgress'; import { usePaginatedData } from '../../../shared/hooks/use-data'; import { createUniversalComponent } from '../../../shared/utility/create-universal-component'; -import { CanvasSnippet } from '../../../shared/components/CanvasSnippet'; +import { CanvasSnippet } from '../../../shared/features/CanvasSnippet'; import { Link } from 'react-router-dom'; import { SmallButton } from '../../../shared/navigation/Button'; diff --git a/services/madoc-ts/src/frontend/admin/pages/term-configurations/create-term-configuration.tsx b/services/madoc-ts/src/frontend/admin/pages/term-configurations/create-term-configuration.tsx new file mode 100644 index 000000000..6cb9c34db --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/term-configurations/create-term-configuration.tsx @@ -0,0 +1,304 @@ +import React, { useState } from 'react'; +import { useMutation, useQuery } from 'react-query'; +import { getValueDotNotation } from '../../../../utility/iiif-metadata'; +import { SystemListItem } from '../../../shared/atoms/SystemListItem'; +import { SystemDescription, SystemMetadata, SystemName } from '../../../shared/atoms/SystemUI'; +import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; +import { WarningMessage } from '../../../shared/callouts/WarningMessage'; +import { FilePreview } from '../../../shared/components/FilePreview'; +import { Input, InputContainer, InputLabel } from '../../../shared/form/Input'; +import { useApi } from '../../../shared/hooks/use-api'; +import { Button } from '../../../shared/navigation/Button'; +import { HrefLink } from '../../../shared/utility/href-link'; + +function PathSelector({ + label, + setValue, + value, + data, +}: { + label: string; + setValue: (v: string) => void; + value: string; + data: any; +}) { + const dataPreview = value ? getValueDotNotation(data, value) : data; + return ( +
    + + {label} + setValue(e.currentTarget.value)} + /> + + {Object.keys(data).map(key => ( + + + +
    +        {JSON.stringify(dataPreview, null, 2)}
    +      
    + + {value && !dataPreview ? ( + + Did not find item at key {value} + + ) : null} +
    + ); +} + +export function CreateTermConfiguration() { + const api = useApi(); + const [url, setUrl] = useState(''); + const [templateUrl, setTemplateUrl] = useState(''); + const [listPath, setListPath] = useState(''); + const [labelPath, setLabelPath] = useState(''); + const [descriptionPath, setDescriptionPath] = useState(''); + const [resourceClassPath, setResourceClassPath] = useState(''); + const [uriPath, setUriPath] = useState(''); + const [languagePath, setLanguagePath] = useState(''); + + const [listConfirmed, setListConfirmed] = useState(false); + const [fieldsConfirmed, setFieldsConfirmed] = useState(false); + + const [createTermConfig, createTermConfigStatus] = useMutation( + async (metadata: { label: string; description: string; attribution: string }) => { + return api.siteManager.createTermConfiguration({ + url_pattern: templateUrl, + paths: { + results: listPath, + label: labelPath, + description: descriptionPath, + resource_class: resourceClassPath, + uri: uriPath, + language: languagePath, + }, + label: metadata.label, + description: metadata.description || null, + attribution: metadata.attribution || null, + }); + } + ); + + const { data, isLoading } = useQuery( + ['preview-term', { url }], + async () => { + if (!url) { + return; + } + + return fetch(url).then(r => r.json()); + }, + { + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchIntervalInBackground: false, + retry: false, + onSuccess: () => { + // Reset + setListConfirmed(false); + setFieldsConfirmed(false); + setListPath(''); + setLabelPath(''); + setDescriptionPath(''); + setResourceClassPath(''); + setUriPath(''); + setLanguagePath(''); + }, + } + ); + + const submitUrl = (e: React.FormEvent) => { + e.preventDefault(); + setTemplateUrl(e.currentTarget.url.value); + setUrl(e.currentTarget.example.value); + }; + + const submitPath = (e: React.FormEvent) => { + e.preventDefault(); + setListPath(e.currentTarget.list_path.value); + }; + + const submitConfig = (e: React.FormEvent) => { + e.preventDefault(); + + createTermConfig({ + label: e.currentTarget.label.value, + description: e.currentTarget.description.value, + attribution: e.currentTarget.attribution.value, + }); + }; + + const selectedData = listPath ? getValueDotNotation(data, listPath) : data; + + if (createTermConfigStatus.isLoading) { + return
    Loading...
    ; + } + + if (createTermConfigStatus.data) { + return ( +
    +

    Term configuration created

    +

    Term configuration has been created.

    + View term configuration +
    + ); + } + + return ( + <> + +
    + + Create new term configuration + + Here you can link an external vocabulary to a term configuration.
    This will allow you to use the + vocabulary in your site using the built-in autocomplete. +
    +
    + {/* + The steps to create a new term configuration are: + - User provides the URL + - We make a test request and return the JSON-LD for the user to preview + - User can then select the path to the list of terms + - User can then select the path to the term itself + - User can then select the path to the label + - User can then select the path to the description + */} + +
    + + Example search query + + + + URL Template + + + +
    + + {data ? ( + <> +

    Data preview

    + +
    + + Path to list of results + + + {Object.keys(data).map(key => ( + + + {!listConfirmed ? : null} +
    + + {!listConfirmed ? ( + <> +
    +                    {data
    +                      ? JSON.stringify(listPath ? getValueDotNotation(data, listPath) : data, null, 2)
    +                      : 'No data yet'}
    +                  
    + + {selectedData && Array.isArray(selectedData) ? ( + <> + + + ) : null} + + {listPath && !selectedData ? ( + + Did not find list of results at key {listPath} + + ) : null} + + ) : null} + + {listConfirmed && !fieldsConfirmed ? ( + <> + + + + + + + + + + + + + ) : null} + + {fieldsConfirmed ? ( +
    +

    About this endpoint

    +
    + + Label + + + + Short description + + + + Attribution + + + + +
    +
    + ) : null} + + ) : null} +
    +
    + + ); +} diff --git a/services/madoc-ts/src/frontend/admin/pages/term-configurations/edit-term-configuration.tsx b/services/madoc-ts/src/frontend/admin/pages/term-configurations/edit-term-configuration.tsx new file mode 100644 index 000000000..cb5e07b84 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/term-configurations/edit-term-configuration.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export function EditTermConfiguration() { + return
    Edit term configuration
    ; +} diff --git a/services/madoc-ts/src/frontend/admin/pages/term-configurations/index.tsx b/services/madoc-ts/src/frontend/admin/pages/term-configurations/index.tsx new file mode 100644 index 000000000..291c4b674 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/term-configurations/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Outlet } from 'react-router-dom'; +import { SystemBackground } from '../../../shared/atoms/SystemUI'; +import { AdminHeader } from '../../molecules/AdminHeader'; + +export function TermConfigurations() { + const { t } = useTranslation(); + return ( + <> + + + + + + ); +} diff --git a/services/madoc-ts/src/frontend/admin/pages/term-configurations/list-term-configurations.tsx b/services/madoc-ts/src/frontend/admin/pages/term-configurations/list-term-configurations.tsx new file mode 100644 index 000000000..049a924d1 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/term-configurations/list-term-configurations.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; +import { SystemListItem } from '../../../shared/atoms/SystemListItem'; +import { SystemDescription, SystemMetadata, SystemName } from '../../../shared/atoms/SystemUI'; +import { useApi } from '../../../shared/hooks/use-api'; +import { Button, ButtonRow } from '../../../shared/navigation/Button'; +import { HrefLink } from '../../../shared/utility/href-link'; + +export function ListTermConfigurations() { + const api = useApi(); + const { t } = useTranslation(); + + const { data, isLoading } = useQuery('term-configurations', async () => { + return await api.siteManager.getAllTermConfigurations(); + }); + + if (isLoading) { + return
    Loading...
    ; + } + + return ( + <> + + + + + + + {data && data.termConfigurations.length ? ( + data.termConfigurations.map(termConfiguration => { + return ( + + + + {termConfiguration.label} + + {termConfiguration.description} + + {termConfiguration.url_pattern} + + + + ); + }) + ) : ( + No term configurations + )} + + ); +} diff --git a/services/madoc-ts/src/frontend/admin/pages/term-configurations/view-term-configuration.tsx b/services/madoc-ts/src/frontend/admin/pages/term-configurations/view-term-configuration.tsx new file mode 100644 index 000000000..13d963457 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/term-configurations/view-term-configuration.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation, useQuery } from 'react-query'; +import { useNavigate, useParams } from 'react-router-dom'; +import invariant from 'tiny-invariant'; +import { SystemListItem } from '../../../shared/atoms/SystemListItem'; +import { SystemDescription, SystemMetadata, SystemName } from '../../../shared/atoms/SystemUI'; +import { TimeAgo } from '../../../shared/atoms/TimeAgo'; +import { ConfirmButton } from '../../../shared/capture-models/editor/atoms/ConfirmButton'; +import { AutocompleteField } from '../../../shared/capture-models/editor/input-types/AutocompleteField/AutocompleteField'; +import { useApi } from '../../../shared/hooks/use-api'; +import { Button } from '../../../shared/navigation/Button'; +import { HrefLink } from '../../../shared/utility/href-link'; + +export function ViewTermConfiguration() { + const { t } = useTranslation(); + const api = useApi(); + const { id } = useParams<{ id: string }>(); + const { data: termConfiguration, error } = useQuery(['term-configuration', { id }], async () => { + return await api.siteManager.getTermConfiguration(id as string); + }); + const navigate = useNavigate(); + + const [deleteTerm, deleteTermStatus] = useMutation(async () => { + invariant(id, 'ID must be defined'); + await api.siteManager.deleteTermConfiguration(id); + // navigate to list. + navigate(`/configure/site/terms`); + }); + + if (deleteTermStatus.isLoading) { + return
    Deleting...
    ; + } + + if (error) { + return
    Error: {error}
    ; + } + + if (!termConfiguration) { + return
    Loading...
    ; + } + + return ( +
    + + + + {termConfiguration.label} + + {termConfiguration.description} + + {termConfiguration.url_pattern} + + {termConfiguration.attribution ? ( + +

    Attribution

    +

    {termConfiguration.attribution}

    +
    + ) : null} + + Created: + +
    +
    + + +
    +

    Paths

    +
      +
    • + URI: {termConfiguration.paths.uri} +
    • +
    • + Label: {termConfiguration.paths.label} +
    • +
    • + Description: {termConfiguration.paths.description} +
    • +
    • + Resource class: {termConfiguration.paths.resource_class} +
    • +
    • + Language: {termConfiguration.paths.language} +
    • +
    +
    +
    + + +
    +

    Preview

    +
    + void 0} + requestInitial={false} + /> +
    +
    +
    + + +
    + + + +
    +
    +
    + ); +} diff --git a/services/madoc-ts/src/frontend/admin/routes.tsx b/services/madoc-ts/src/frontend/admin/routes.tsx index eb7e17ea5..bbbcc69aa 100644 --- a/services/madoc-ts/src/frontend/admin/routes.tsx +++ b/services/madoc-ts/src/frontend/admin/routes.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { RouteObject } from 'react-router-dom'; import { annotationStylesRoutes } from './pages/annotation-styles/index'; +import { CreateBadge } from './pages/badges/create-badge'; +import { Badges } from './pages/badges/index'; +import { ListBadges } from './pages/badges/list-badges'; +import { ViewBadge } from './pages/badges/view-badge'; import { CanvasExport } from './pages/content/canvases/canvas-export'; import { CanvasJson } from './pages/content/canvases/canvas-json'; import { CanvasPlaintext } from './pages/content/canvases/canvas-plaintext'; @@ -63,6 +67,8 @@ import { Project } from './pages/crowdsourcing/projects/project'; import { ProjectModelEditor } from './pages/crowdsourcing/projects/project-model-editor'; import { CreateCollection } from './pages/content/collections/create-collection'; import { CreateManifest } from './pages/content/manifests/create-manifest'; +import { SiteTerms } from './pages/site-terms/index'; +import { ListTerms } from './pages/site-terms/list-terms'; import { CreateInvitation } from './pages/sites/create-invitation'; import { ListInvitations } from './pages/sites/list-invitations'; import { ListSites } from './pages/global/list-sites'; @@ -106,6 +112,12 @@ import { DeleteProject } from './pages/crowdsourcing/projects/delete-project'; import { ProjectExportTab } from './pages/crowdsourcing/projects/project-export'; import { GenerateApiKey } from './pages/system/generate-api-key'; import { CreateBot } from './pages/global/create-bot'; +import { CreateTermConfiguration } from './pages/term-configurations/create-term-configuration'; +import { EditTermConfiguration } from './pages/term-configurations/edit-term-configuration'; +import { TermConfigurations } from './pages/term-configurations/index'; +import { ListTermConfigurations } from './pages/term-configurations/list-term-configurations'; +import { ViewTermConfiguration } from './pages/term-configurations/view-term-configuration'; +import { CreateTerms } from './pages/site-terms/create-terms'; export const routes: RouteObject[] = [ { @@ -459,6 +471,60 @@ export const routes: RouteObject[] = [ path: '/configure/site/system', element: , }, + { + path: '/configure/site/terms', + element: , + children: [ + { + index: true, + element: , + }, + { + path: '/configure/site/terms/create', + element: , + }, + { + path: '/configure/site/terms/:id', + element: , + }, + { + path: '/configure/site/terms/:id/edit', + element: , + }, + ], + }, + { + path: '/configure/site/badges', + element: , + children: [ + { + index: true, + element: , + }, + { + path: '/configure/site/badges/create', + element: , + }, + { + path: '/configure/site/badges/:id', + element: , + }, + ], + }, + { + path: '/configure/site/terms-and-conditions', + element: , + children: [ + { + index: true, + element: , + }, + { + path: '/configure/site/terms-and-conditions/list', + element: , + }, + ], + }, { path: '/page-blocks', element: , diff --git a/services/madoc-ts/src/frontend/shared/atoms/Accordion.tsx b/services/madoc-ts/src/frontend/shared/atoms/Accordion.tsx deleted file mode 100644 index 55a4586e0..000000000 --- a/services/madoc-ts/src/frontend/shared/atoms/Accordion.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import styled from 'styled-components'; -import * as React from 'react'; -import { LocaleString } from '../components/LocaleString'; -import { InternationalString } from '@iiif/presentation-3'; -import { useState } from 'react'; -import { ChevronDown } from '../icons/ChevronIcon'; - -export const AccordionWrapper = styled.div` - background-color: inherit; - width: 100%; - - hr { - color: rgba(150, 141, 141, 0.26); - } -`; - -export const Top = styled.button` - width: 100%; - background-color: inherit; - border: none; - cursor: pointer; - border-radius: 3px; - - display: flex; - justify-content: space-between; - padding: 0.3em; - - :hover { - background-color: rgba(59, 59, 93, 0.1); - } - - svg { - height: 1.5em; - width: 1.5em; - transition: all 0.5s ease; - margin-top: auto; - margin-bottom: auto; - - &[data-is-open='true'] { - transform: rotatex(180deg); - } - } -`; - -export const Title = styled.h3` - text-transform: capitalize; -`; - -export const Content = styled.div` - width: 100%; - background-color: white; - overflow: hidden; - max-height: 0; - transition: max-height ease-in-out 0.4s; - - &[data-is-open='true'] { - overflow: auto; - max-height: 2000px; - } -`; - -interface AccordionProps { - children: React.ReactNode; - title?: InternationalString; - defaultOpen?: boolean; -} -export const Accordion = ({ title, children, defaultOpen = true }: AccordionProps) => { - const [isOpen, setIsOpen] = useState(defaultOpen); - - return ( - - { - setIsOpen(!isOpen); - }} - > - - <LocaleString>{title}</LocaleString> - - - - {children} -
    -
    - ); -}; diff --git a/services/madoc-ts/src/frontend/shared/atoms/CanvasStatus.tsx b/services/madoc-ts/src/frontend/shared/atoms/CanvasStatus.tsx index 8a9ec5fc5..52c60ae46 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/CanvasStatus.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/CanvasStatus.tsx @@ -1,6 +1,6 @@ import styled, { css } from 'styled-components'; import React from 'react'; -import ReactTooltip from 'react-tooltip'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; import { useTranslation } from 'react-i18next'; import { useApi } from '../hooks/use-api'; import { useUser } from '../hooks/use-site'; @@ -59,10 +59,10 @@ export const CanvasStatus: React.FC<{ status: number; floating?: boolean }> = ({ return ( <> - + {api.getIsServer() || !user || user.site_role === 'viewer' || user.site_role === 'editor' ? null : ( - + )} ); diff --git a/services/madoc-ts/src/frontend/shared/atoms/CanvasViewerGrid.tsx b/services/madoc-ts/src/frontend/shared/atoms/CanvasViewerGrid.tsx index 4dc89c68c..b8f34b8ac 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/CanvasViewerGrid.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/CanvasViewerGrid.tsx @@ -29,7 +29,8 @@ export const CanvasViewerGridSidebar = styled.div<{ $vertical?: boolean }>` export const CanvasViewerEditorStyleReset = styled.div` font-size: 13px; padding: 0 1em; - //overflow-x: scroll; + overflow-y: scroll; + max-height: 60vh; `; export const ContributionSaveButton = styled.div` diff --git a/services/madoc-ts/src/frontend/shared/atoms/ImageGrid.tsx b/services/madoc-ts/src/frontend/shared/atoms/ImageGrid.tsx index 9505718d7..526d919bc 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/ImageGrid.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/ImageGrid.tsx @@ -16,8 +16,7 @@ export const ImageGrid = styled.div<{ $size?: 'large' | 'small'; }>` display: grid; - grid-template-columns: repeat(auto-fit, minmax(${getSize}, 1fr)); - justify-content: space-between; + grid-template-columns: repeat(auto-fit, ${getSize}); background-color: inherit; grid-gap: 0.875em; width: 100%; diff --git a/services/madoc-ts/src/frontend/shared/atoms/ImageStrip.tsx b/services/madoc-ts/src/frontend/shared/atoms/ImageStrip.tsx index 4f2743b7d..89364ad6c 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/ImageStrip.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/ImageStrip.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { Subheading5 } from '../typography/Heading5'; +import { SingleLineHeading5, Subheading5 } from '../typography/Heading5'; export const ImageStripBox = styled.div<{ $size?: 'large' | 'small'; @@ -9,15 +9,19 @@ export const ImageStripBox = styled.div<{ }>` position: relative; flex-shrink: 0; + padding: 0.25em; border-radius: 3px; max-width: ${props => (props.$size === 'small' ? '200px' : '')}; border: 1px solid transparent; border-color: ${props => (props.$border ? props.$border : 'transparent')}; background-color: ${props => (props.$bgColor ? props.$bgColor : 'inherit')}; + ${SingleLineHeading5} { + font-size: 1em; + margin-bottom: 0.5em; + } h5, ${Subheading5} { - color: ${props => (props.$color ? props.$color : 'inherit')}!important; - padding: ${props => (props.$border ? '0 0 1em 0.5em' : '0')}; + color: ${props => (props.$color ? props.$color : '')}; } &:hover { diff --git a/services/madoc-ts/src/frontend/shared/atoms/Images.tsx b/services/madoc-ts/src/frontend/shared/atoms/Images.tsx index 3b50a0958..499601b88 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/Images.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/Images.tsx @@ -8,7 +8,6 @@ export const CroppedImage = styled.div<{ }>` background: #000; padding: 2px; - aspect-ratio: ${props => (props.$rect ? '1.618' : '1')}; display: flex; justify-content: center; @@ -55,18 +54,3 @@ export const CroppedImage = styled.div<{ `} } `; - -export const CoveredImage = styled.div` - overflow: hidden; - margin: 0.5em; - border-radius: 5px; - - img { - object-fit: cover; - object-position: 50% 50%; - display: block; - - height: 130px; - width: 100%; - } -`; diff --git a/services/madoc-ts/src/frontend/shared/atoms/LinkingProperty.tsx b/services/madoc-ts/src/frontend/shared/atoms/LinkingProperty.tsx index ac6e4164a..76c4018f6 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/LinkingProperty.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/LinkingProperty.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { useMutation } from 'react-query'; import styled from 'styled-components'; import { ResourceLinkResponse } from '../../../types/schemas/linking'; -import { LinkingPropertyEditor } from '../components/LinkingPropertyEditor'; +import { LinkingPropertyEditor } from '../features/LinkingPropertyEditor'; import { useApi } from '../hooks/use-api'; import { createLink } from '../utility/create-link'; import { Link } from 'react-router-dom'; diff --git a/services/madoc-ts/src/frontend/shared/atoms/SnippetLarge.tsx b/services/madoc-ts/src/frontend/shared/atoms/SnippetLarge.tsx index 2cae6b8f1..ac2121de0 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/SnippetLarge.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/SnippetLarge.tsx @@ -1,6 +1,6 @@ import { stringify } from 'query-string'; import React from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; export type SnippetLargeProps = { label: string | JSX.Element; @@ -11,19 +11,16 @@ export type SnippetLargeProps = { link: string; buttonText: string | JSX.Element; linkAs?: any; - flat?: boolean; - margin?: boolean; lightBackground?: boolean; size?: 'lg' | 'md' | 'sm'; center?: boolean; buttonRole?: 'button' | 'link'; hideButton?: boolean; - containThumbnail?: boolean; stackedThumbnail?: boolean; smallLabel?: boolean; - fluid?: boolean; interactive?: boolean; query?: any; + placeholderIcon?: JSX.Element; }; const sizeMap = { @@ -32,216 +29,177 @@ const sizeMap = { sm: '1em', }; -const SnippetButton = styled.a<{ role?: string; $center?: boolean }>` - margin-top: auto; - ${props => - props.$center && - css` - margin-left: auto; - `} - margin-right: auto; - display: inline-block; - width: auto; +const SnippetButton = styled.a<{ $center?: boolean }>` + margin-left: ${props => (props.$center ? 'auto' : '')}; justify-self: flex-end; padding: 0.4em 0; - font-size: 0.8rem; + font-size: 0.9rem; text-decoration: none; color: #3773db; + flex-shrink: 0; + &:link, &:visited { color: #3773db; } - ${props => - props.role === 'button' && - css` - padding: 0.4em 0.8em; - background: #ecf0ff; - border-radius: 3px; - &:hover { - background: #d3dbf5; - color: #3773db; - } - &:focus { - background: #3773db; - color: #fff; - } - `} + &:hover { + color: #0f306c; + } + + &[data-is-role='button'] { + padding: 0.4em 0.8em; + background: #ecf0ff; + color: #3773db; + border-radius: 3px; + + &:hover { + background: #d3dbf5; + color: #3773db; + } + + &:focus { + background: #3773db; + color: #fff; + } + } `; export const SnippetContainer = styled.div<{ - portrait?: boolean; - margin?: boolean; - flat?: boolean; size?: 'lg' | 'md' | 'sm'; - $center?: boolean; - interactive?: boolean; }>` box-sizing: border-box; background: #ffffff; font-size: ${props => (props.size ? sizeMap[props.size] : sizeMap.sm)}; - - ${props => - props.interactive && - css` - cursor: pointer; - &:hover { - background: #edf0fe; - ${SnippetButton} { - &[role='button'] { - background: #fff; - } + border-radius: 1px; + padding: 1rem 0.6rem; + display: flex; + justify-content: center; + align-items: center; + min-height: 8em; + max-width: 39em; + + flex-direction: row; + border: 1px solid #999; + + &[data-is-interactive='true'] { + cursor: pointer; + &:hover { + background: #edf0fe; + ${SnippetButton} { + &[role='button'] { + background: #fff; } } - `} - - ${props => - props.$center && - css` - align-items: center; - text-align: center; - `} - ${props => - !props.flat && - css` - border: 1px solid #e7e7e7; - box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.09); - `} - border-radius: 4px; - padding: 1rem; - display: flex; - ${props => - props.portrait - ? css` - //max-width: 13em; - flex-direction: column; - ` - : css` - min-height: 8em; - max-width: 26em; - flex-direction: row; - `}; - ${props => - props.margin && - css` - margin-bottom: 1rem; - `}; + } + } + &[data-is-center='true'] { + align-items: center; + text-align: center; + } + &[data-is-portrait='true'] { + flex-direction: column; + align-items: start; + max-width: 20em; + padding: 0.5rem 0.6rem; + } `; const SnippetMetadata = styled.div` display: flex; flex-direction: column; - flex: 1 1 0px; - text-decoration: none; + gap: 0.5em; + width: 100%; `; -const SnippetUnconstrainedContainer = styled.div<{ portrait?: boolean; fluid?: boolean }>` +export const SnippetThumbnailContainer = styled.div<{ + lightBackground?: boolean; +}>` display: flex; justify-content: center; - align-items: center; overflow: hidden; - ${props => - props.fluid && - css` - width: 100%; - `} - - ${props => - props.portrait - ? css` - margin-bottom: 1rem; - ` - : css` - margin-right: 1rem; - `} -`; - -export const SnippetThumbnailContainer = styled(SnippetUnconstrainedContainer)<{ - fluid?: boolean; - portrait?: boolean; - lightBackground?: boolean; - stackedThumbnail?: boolean; -}>` background: ${props => (props.lightBackground ? '#EEEEEE' : '#000')}; - + height: 6em; + width: 6em; + margin-right: 1rem; + min-width: 6em; + img { z-index: 3; } + + &[data-is-stacked='true'] { + position: relative; + padding: 0.4em; + background-color: transparent; + max-height: 6em; + max-width: 6em; + margin-right: 1rem; + + img { + border: 2px solid #fff; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2); + background: ${props => (props.lightBackground ? '#EEEEEE' : '#000')}; + } - ${props => - props.stackedThumbnail - ? css` - position: relative; - padding: 0.4em; - background-color: transparent; + &:after { + content: ''; + background: #999; + position: absolute; + border: 2px solid #fff; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2); + top: 0.4em; + bottom: 0.4em; + left: 0.4em; + right: 0.4em; + transform: rotate(4deg); + z-index: 2; - img { - border: 2px solid #fff; - box-shadow: 0px 2px 5px 0 rgba(0, 0, 0, 0.2); - background: ${() => (props.lightBackground ? '#EEEEEE' : '#000')}; - } + } - &:after { - content: ''; - background: #999; - position: absolute; - border: 2px solid #fff; - box-shadow: 0px 2px 5px 0 rgba(0, 0, 0, 0.2); - top: 0.4em; - bottom: 0.4em; - left: 0.4em; - right: 0.4em; - transform: rotate(4deg); - z-index: 2; - } - &:before { - content: ''; - background: #666; - position: absolute; - border: 2px solid #fff; - box-shadow: 0px 2px 5px 0 rgba(0, 0, 0, 0.2); - top: 0.4em; - bottom: 0.4em; - left: 0.4em; - right: 0.4em; - transform: rotate(-3deg); - z-index: 2; - } + &:before { + content: ''; + background: #666; + position: absolute; + border: 2px solid #fff; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2); + top: 0.4em; + bottom: 0.4em; + left: 0.4em; + right: 0.4em; + transform: rotate(-3deg); + z-index: 2; + } + + &[data-is-portrait='true'] { + margin-bottom: 1rem; + max-height: 11em; + width: 11em; + } + } + + &[data-is-portrait='true'] { + margin-bottom: 1rem; + margin-right: 0; + height: 9em; + width: 100% + } - ${() => - props.fluid - ? '' - : props.portrait - ? css` - margin-bottom: 1rem; - max-height: 11em; - max-width: 11em; - ` - : css` - max-height: 6em; - max-width: 6em; - margin-right: 1rem; - `} - ` - : props.fluid - ? '' - : props.portrait - ? css` - height: 11em; - width: 11em; - margin-bottom: 1rem; - ` - : css` - height: 6em; - width: 6em; - margin-right: 1rem; - min-width: 6em; - `} + &[data-is-icon='true'] { + background: #DDDDDD; + align-items: center; + svg { + height: 48px; + width: 48px; + fill: #888888; + } + } +} `; export const SnippetThumbnail = styled.img` display: inline-block; - object-fit: contain; + object-fit: cover; flex-shrink: 0; width: 100%; height: 100%; @@ -254,8 +212,7 @@ const SnippetLabel = styled.div<{ small?: boolean }>` const SnippetSubtitle = styled.div` font-size: 0.8rem; - color: #777; - margin-bottom: 0.8em; + color: #6b6b6b; text-decoration: none; `; @@ -266,47 +223,44 @@ const SnippetSummary = styled.div` `; export const SnippetLarge: React.FC = props => { - const buttonRole = props.buttonRole ? props.buttonRole : props.portrait ? 'link' : 'button'; - const containThumbnail = props.containThumbnail !== false; return ( {props.thumbnail ? ( - containThumbnail ? ( - - - - ) : ( - - - - ) + + + + ) : props.placeholderIcon ? ( + + {props.placeholderIcon} + ) : null} {props.label} {props.subtitle} {!props.portrait ? {props.summary} : null} - {!props.hideButton && ( - - {props.buttonText} - - )} + {!props.hideButton && ( + + {props.buttonText} + + )} ); }; diff --git a/services/madoc-ts/src/frontend/shared/atoms/Statistics.tsx b/services/madoc-ts/src/frontend/shared/atoms/Statistics.tsx index da3d07594..94d864220 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/Statistics.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/Statistics.tsx @@ -4,11 +4,18 @@ import { Link } from 'react-router-dom'; export const StatisticLabel = styled.div` font-size: 0.9em; + color: black; `; export const StatisticNumber = styled.div` - font-size: 3em; - line-height: 1em; + font-size: 1.5em; + font-weight: 600; +`; + +export const StatisticText = styled.div` + display: flex; + flex-direction: column; + line-height: 2em; `; export const Statistic = styled.div<{ $interactive?: boolean }>` diff --git a/services/madoc-ts/src/frontend/shared/atoms/SubtaskProgress.tsx b/services/madoc-ts/src/frontend/shared/atoms/SubtaskProgress.tsx index 42c5d138e..3d9be1b1d 100644 --- a/services/madoc-ts/src/frontend/shared/atoms/SubtaskProgress.tsx +++ b/services/madoc-ts/src/frontend/shared/atoms/SubtaskProgress.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; import React from 'react'; import { TickIcon } from '../icons/TickIcon'; import { useTranslation } from 'react-i18next'; -import ReactTooltip from 'react-tooltip'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; const SubtaskProgressContainer = styled.div` margin: 0.38em 1em; @@ -68,14 +68,16 @@ export const SubtaskProgress: React.FC<{ return ( - {tooltip ? : null} + {tooltip ? : null} ); }; diff --git a/services/madoc-ts/src/frontend/shared/components/CanvasVaultContext.tsx b/services/madoc-ts/src/frontend/shared/capture-models/CanvasVaultContext.tsx similarity index 100% rename from services/madoc-ts/src/frontend/shared/components/CanvasVaultContext.tsx rename to services/madoc-ts/src/frontend/shared/capture-models/CanvasVaultContext.tsx diff --git a/services/madoc-ts/src/frontend/shared/capture-models/EditorShorthandCaptureModel.tsx b/services/madoc-ts/src/frontend/shared/capture-models/EditorShorthandCaptureModel.tsx index d113ce5be..6ecbc75d2 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/EditorShorthandCaptureModel.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/EditorShorthandCaptureModel.tsx @@ -8,6 +8,7 @@ import { hydrateCaptureModel } from './helpers/hydrate-capture-model'; import { serialiseCaptureModel } from './helpers/serialise-capture-model'; import { CustomSubmitButton } from './new/components/CustomSubmitButton'; import { EditorSlots, ProfileConfig } from './new/components/EditorSlots'; +import { RevisionOnChange } from './new/components/RevisionOnChange'; import { RevisionProviderWithFeatures } from './new/components/RevisionProviderWithFeatures'; import { createRevisionFromDocument } from '../utility/create-revision-from-document'; import { RevisionStoreReference } from './new/components/RevisionStoreReference'; @@ -25,6 +26,7 @@ export interface EditorShorthandCaptureModelProps { immutableFields?: string[]; onSave?: (revision: any) => Promise | void; onPreview?: (revision: any) => Promise | void; + onChange?: (revision: RevisionRequest | null) => void; keepExtraFields?: boolean; structure?: CaptureModel['structure']; children?: any; @@ -52,6 +54,7 @@ export const EditShorthandCaptureModel = forwardRef( structure, enableSearch, searchLabel, + onChange, children, }: EditorShorthandCaptureModelProps, ref: React.Ref @@ -142,6 +145,7 @@ export const EditShorthandCaptureModel = forwardRef( revision={rev.revisionId} > + {onChange ? : null} {enableSearch ? ( {searchLabel ? {searchLabel} : null} diff --git a/services/madoc-ts/src/frontend/shared/capture-models/RevisionBreadcrumbs.tsx b/services/madoc-ts/src/frontend/shared/capture-models/RevisionBreadcrumbs.tsx index 3632d25cf..2517ff1f6 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/RevisionBreadcrumbs.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/RevisionBreadcrumbs.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { BreadcrumbDivider, BreadcrumbItem, BreadcrumbList } from '../components/Breadcrumbs'; +import { BreadcrumbDivider, BreadcrumbItem, BreadcrumbList } from '../../site/blocks/Breadcrumbs'; import { HomeIcon } from '../icons/HomeIcon'; import { useBreads } from './hooks/use-breads'; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/atoms/Dropdown.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/atoms/Dropdown.tsx index e82effd09..f2ab5658e 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/atoms/Dropdown.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/atoms/Dropdown.tsx @@ -18,7 +18,7 @@ export type DropdownProps = { isClearable?: boolean; value?: string; options: Array; - onChange: (value?: string) => void; + onChange: (value?: string | null) => void; }; function getValue(option: DropdownOption) { @@ -48,9 +48,8 @@ export const Dropdown: React.FC = ({ onChange, }) => { const onOptionChange = useCallback((option: DropdownOption | null): void => { - if (option) { - onChange(option ? option.value : undefined); - } + onChange(option ? option.value : undefined); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/CaptureModelVisualSettings/CaptureModelVisualSettings.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/CaptureModelVisualSettings/CaptureModelVisualSettings.tsx new file mode 100644 index 000000000..c9386bced --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/CaptureModelVisualSettings/CaptureModelVisualSettings.tsx @@ -0,0 +1,30 @@ +import React, { createContext, FC, useContext, useMemo } from 'react'; + +// React context +export interface CaptureModelVisualSettings { + descriptionTooltip?: boolean; +} + +const defaults: CaptureModelVisualSettings = { + descriptionTooltip: false, +}; +export const CaptureModelVisualSettingsContext = createContext({ + descriptionTooltip: false, +}); + +export function useCaptureModelVisualSettings() { + return useContext(CaptureModelVisualSettingsContext); +} + +export const CaptureModelVisualSettingsProvider: FC> = ({ children, ...props }) => { + const config = useMemo(() => { + return { + ...defaults, + ...props, + }; + }, [props]); + + return ( + {children} + ); +}; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/CardButton/CardButton.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/CardButton/CardButton.tsx index 6053aeb93..03bb5273c 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/CardButton/CardButton.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/CardButton/CardButton.tsx @@ -3,7 +3,7 @@ import { getTheme } from '../../themes'; export const CardButton = styled.button<{ size?: 'large' | 'medium' | 'small'; shadow?: boolean; inline?: boolean }>` box-sizing: border-box; - background: ${props => getTheme(props).colors.primary}; + background: ${props => getTheme(props).colors.primary} !important; /*Tailwind issue*/ color: ${props => getTheme(props).colors.textOnPrimary}; margin-bottom: ${props => { const size = getTheme(props).card[props.size === 'large' ? 'large' : 'small']; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/DocumentEditor/DocumentEditor.stories.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/DocumentEditor/DocumentEditor.stories.tsx index 6d3228205..33f10386f 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/DocumentEditor/DocumentEditor.stories.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/DocumentEditor/DocumentEditor.stories.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { CaptureModel } from '../../../types/capture-model'; import { DocumentEditor } from './DocumentEditor'; import { DocumentStore } from '../../stores/document/document-store'; @@ -27,6 +26,7 @@ const Inner = () => { setSelector: a.setSelector, setAllowMultiple: a.setAllowMultiple, setRequired: a.setRequired, + setDependant: a.setDependant, setLabelledBy: a.setLabelledBy, setPluralLabel: a.setPluralLabel, })); @@ -41,6 +41,7 @@ const Inner = () => { setLabel={actions.setLabel} setAllowMultiple={actions.setAllowMultiple} setRequired={actions.setRequired} + setDependant={actions.setDependant} setLabelledBy={actions.setLabelledBy} setPluralLabel={actions.setPluralLabel} popSubtree={actions.popSubtree} diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/DocumentEditor/DocumentEditor.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/DocumentEditor/DocumentEditor.tsx index 603d17d71..c639fed6c 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/DocumentEditor/DocumentEditor.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/DocumentEditor/DocumentEditor.tsx @@ -28,6 +28,7 @@ export type DocumentEditorProps = { setDescription: (label: string) => void; setAllowMultiple: (allow: boolean) => void; setRequired: (allow: boolean) => void; + setDependant: (payload?: any) => void; setLabelledBy: (label: string) => void; setPluralLabel: (label: string) => void; selectField: (term: string) => void; @@ -50,6 +51,7 @@ export const DocumentEditor: React.FC = ({ deselectField, setAllowMultiple, setRequired, + setDependant, setPluralLabel, setLabelledBy, addField, @@ -69,6 +71,8 @@ export const DocumentEditor: React.FC = ({ const [metadataOpen, setMetadataOpen] = useState(false); const [customLabelledBy, setCustomLabelBy] = useState(false); + const filteredFields = subtreeFields?.filter(f => f.term !== subtree.label); + const subtreeFieldOptions = useMemo( () => [ { @@ -169,6 +173,30 @@ export const DocumentEditor: React.FC = ({ {t('Required field')} + {subtreeFields && ( + + + {t('Depends on? (This field will only appear if the dependant field has a value)')} + + + f && { + key: f.value.id, + text: f.term || '', + value: f.term, + } + )} + value={subtree.dependant} + onChange={val => { + setDependant(val || undefined); + }} + /> + + )} {subtree.allowMultiple ? ( diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldEditor/FieldEditor.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldEditor/FieldEditor.tsx index d0241b19e..c68e57f44 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldEditor/FieldEditor.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldEditor/FieldEditor.tsx @@ -12,6 +12,7 @@ import { ConfirmButton } from '../../atoms/ConfirmButton'; import { ChooseSelectorButton } from '../ChooseSelectorButton/ChooseSelectorButton'; import { ChooseFieldButton } from '../ChooseFieldButton/ChooseFieldButton'; import { FormPreview } from '../FormPreview/FormPreview'; +import { Dropdown } from '../../atoms/Dropdown'; import { StyledCheckbox, StyledFormField, @@ -39,7 +40,8 @@ export const FieldEditor: React.FC<{ onChangeFieldType?: (type: string, defaults: any, term?: string) => void; setSaveHandler?: (handler: () => void) => void; sourceTypes?: Array; -}> = ({ onSubmit, onDelete, onChangeFieldType, sourceTypes, field: props, term }) => { + subtreeFields?: any[]; +}> = ({ onSubmit, onDelete, onChangeFieldType, sourceTypes, field: props, term, subtreeFields }) => { const { t } = useTranslation(); const ctx = useContext(PluginContext); const { fields, selectors } = useContext(PluginContext); @@ -47,6 +49,7 @@ export const FieldEditor: React.FC<{ const field = ctx.fields[props.type]; const [defaultValue, setDefaultValue] = useState(props.value); + const filteredFields = subtreeFields?.filter(f => f.term !== props.label); if (!field) { throw new Error(`Plugin ${props.type} does not exist`); } @@ -57,6 +60,7 @@ export const FieldEditor: React.FC<{ return sourceType.fieldTypes.indexOf(props.type) !== -1; }); const [dataSource, setDataSource] = useState(props.dataSources || []); + const [dependantField, setDependantField] = useState(props.dependant || undefined); return ( @@ -70,6 +74,7 @@ export const FieldEditor: React.FC<{ type: props.type, selector, dataSources: dataSource && dataSource.length ? dataSource : undefined, + dependent: dependantField ? dependantField : undefined, value: defaultValue, }), term @@ -81,6 +86,7 @@ export const FieldEditor: React.FC<{ type: props.type, selector, dataSources: dataSource && dataSource.length ? dataSource : undefined, + dependant: dependantField ? dependantField : undefined, value: defaultValue, }, term @@ -126,6 +132,28 @@ export const FieldEditor: React.FC<{ ) : null} + {filteredFields && ( + + {t('Depends on? (This field will appear if chosen field has value)')} + { + return { + key: f.value.id, + text: f.term || '', + value: f.term, + }; + })} + value={dependantField} + onChange={val => { + setDependantField(val || undefined); + }} + /> + + )} {dataSources ? ( diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldHeader/FieldHeader.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldHeader/FieldHeader.tsx index 0dbe59b8b..3c35e21bb 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldHeader/FieldHeader.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldHeader/FieldHeader.tsx @@ -1,9 +1,11 @@ import React, { useCallback, useState } from 'react'; +import { Tooltip } from 'react-tooltip'; import styled, { css } from 'styled-components'; import { ModelTranslation } from '../../../utility/model-translation'; import { Tag } from '../../atoms/Tag'; import { useTranslation } from 'react-i18next'; import { useSelectorHelper } from '../../stores/selectors/selector-helper'; +import { useCaptureModelVisualSettings } from '../CaptureModelVisualSettings/CaptureModelVisualSettings'; type FieldHeaderProps = { labelFor?: string; @@ -51,6 +53,31 @@ export const FieldHeaderTitle = styled.label` } `; +export const FieldHelp = styled.div` + display: inline-block; + + border-radius: 50%; + width: 1.2em; + height: 1.2em; + line-height: 1.2em; + font-size: 0.9em; + margin-bottom: 0.2em; + text-align: center; + background: #eee; + color: #555; + margin-left: 0.5em; + cursor: pointer; + transition: background-color 0.3s, color 0.3s; + &:hover { + background: #aaa7de; + color: #fff; + } +`; + +export const FieldHelpInner = styled.div` + white-space: pre-wrap; +`; + const FieldHeaderSubtitle = styled.label` letter-spacing: -0.25px; color: #555; @@ -127,6 +154,7 @@ export const FieldHeader: React.FC = ({ const { t } = useTranslation(); const [open, setOpen] = useState(false); const helper = useSelectorHelper(); + const settings = useCaptureModelVisualSettings(); const isSelectorRequired = selectorComponent && selectorComponent.props?.required; const isSelectorValue = selectorComponent && selectorComponent.props.state; const isSelectorInvalid = isSelectorRequired && !isSelectorValue; @@ -157,11 +185,22 @@ export const FieldHeader: React.FC = ({ )}{' '} {showTerm && term ? {term} : null} {required ? * : null} + {description && settings.descriptionTooltip ? ? : null} {description ? ( - - {description} - + settings.descriptionTooltip ? ( + + + {description} + + + ) : ( + + + {description} + + + ) ) : null} {selectorComponent ? ( diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldInstanceReadOnly/FieldInstanceReadOnly.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldInstanceReadOnly/FieldInstanceReadOnly.tsx index 771d63dd0..f91262388 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldInstanceReadOnly/FieldInstanceReadOnly.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/components/FieldInstanceReadOnly/FieldInstanceReadOnly.tsx @@ -4,7 +4,7 @@ import { BaseField } from '../../../types/field-types'; import { FieldPreview } from '../FieldPreview/FieldPreview'; import { Revisions } from '../../stores/revisions/index'; import { SelectorPreview } from '../SelectorPreview/SelectorPreview'; -import ReactTooltip from 'react-tooltip'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; const PreviewListContainer = styled.div` & ~ & { @@ -38,11 +38,11 @@ export const FieldInstanceReadOnly: React.FC<{ {fields.map(field => ( - + {showSelectorPreview && field.selector && field.selector.state ? ( - + = { selectorPreview?: any; disabled?: boolean; required?: boolean; + dependant?: string; selectorLabel?: string; // @todo other things for the selector. diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/AutocompleteField/AutocompleteField.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/AutocompleteField/AutocompleteField.tsx index 8cd0b38b7..f8daa6899 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/AutocompleteField/AutocompleteField.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/AutocompleteField/AutocompleteField.tsx @@ -1,5 +1,5 @@ import { InternationalString } from '@iiif/presentation-3'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import { Select } from 'react-functional-select'; import { LocaleString } from '../../../../components/LocaleString'; import { useOptionalApi } from '../../../../hooks/use-api'; @@ -55,6 +55,7 @@ export const AutocompleteField: FieldComponent = props = const [error, setError] = useState(''); const api = useOptionalApi(); const boxHeight = hasFetched && options.length && options[0].description ? 55 : undefined; + const pendingFetch = useRef(); const onOptionChange = (option: CompletionItem | undefined) => { if (!option) { props.updateValue(undefined); @@ -83,6 +84,13 @@ export const AutocompleteField: FieldComponent = props = setIsLoading(false); return; } + + if (pendingFetch.current) { + pendingFetch.current.abort(); + } + const abortController = new AbortController(); + pendingFetch.current = abortController; + const fetcher = (): Promise<{ completions: CompletionItem[] }> => { if (props.dataSource.startsWith('madoc-api://')) { const source = props.dataSource.slice('madoc-api://'.length); @@ -91,20 +99,27 @@ export const AutocompleteField: FieldComponent = props = } return api.request(`/api/madoc/${source.replace(/%/, value || '')}`); } - return fetch(`${props.dataSource}`.replace(/%/, value || '')).then( - r => r.json() as Promise<{ completions: CompletionItem[] }> - ); + return fetch(`${props.dataSource}`.replace(/%/, value || ''), { + signal: pendingFetch.current?.signal, + }).then(r => r.json() as Promise<{ completions: CompletionItem[] }>); }; // Make API Request. fetcher() .then(items => { + if (abortController.signal.aborted) { + return; + } + pendingFetch.current = undefined; setOptions(items.completions); setIsLoading(false); setHasFetched(true); setError(''); }) .catch(e => { + if (abortController.signal.aborted) { + return; + } console.error(e); setError(t('There was a problem fetching results')); }); diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/CheckboxListField/CheckboxFieldList.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/CheckboxListField/CheckboxFieldList.tsx index f33f78962..cf481ad3d 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/CheckboxListField/CheckboxFieldList.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/CheckboxListField/CheckboxFieldList.tsx @@ -44,6 +44,7 @@ const CheckboxContainer = styled.fieldset<{ inline?: boolean }>` `; export const CheckboxFieldList: FieldComponent = props => { + const value = props.value || {}; const { t: tModel } = useModelTranslation(); return ( @@ -55,10 +56,10 @@ export const CheckboxFieldList: FieldComponent = props = value={option.value} id={props.id} aria-label={option.label} - checked={props.value[option.value]} + checked={value[option.value]} onChange={v => { props.updateValue({ - ...(props.value || {}), + ...(value || {}), [option.value]: v.target.checked, }); }} diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/DropdownField/DropdownField.editor.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/DropdownField/DropdownField.editor.tsx index 6c79ed96d..7009566d8 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/DropdownField/DropdownField.editor.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/DropdownField/DropdownField.editor.tsx @@ -14,6 +14,7 @@ type Props = { enableExternalImages?: boolean; enableLinks?: boolean; placeholder?: string; + inline?: boolean; optionsAsText: string; clearable: boolean; }; @@ -51,6 +52,12 @@ const DropdownFieldEditor: React.FC = props => { Allow clearing of selection + + + + Use inline variant + + ); }; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/DropdownField/DropdownField.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/DropdownField/DropdownField.tsx index 9516def72..dff9118c8 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/DropdownField/DropdownField.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/input-types/DropdownField/DropdownField.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { InlineSelect } from '../../../../components/InlineSelect'; import { BaseField, FieldComponent } from '../../../types/field-types'; import { Dropdown, DropdownOption } from '../../atoms/Dropdown'; @@ -9,9 +10,21 @@ export interface DropdownFieldProps extends BaseField { options: DropdownOption[]; clearable?: boolean; disabled?: boolean; + inline?: boolean; } export const DropdownField: FieldComponent = props => { + if (props.inline) { + return ( + ({ label: o.text, value: o.value }))} + value={props.value} + onChange={props.updateValue} + onDeselect={props.clearable ? () => props.updateValue(undefined) : undefined} + /> + ); + } + return ( = ({ id, label, updateValue, + multiline, disabled, }) => { const defaultLocale = useDefaultLocale(); @@ -31,6 +32,7 @@ export const InternationalField: FieldComponent = ({ ; setAllowMultiple: Action; setRequired: Action; + setDependant: Action; // dependants ID setLabelledBy: Action; setPluralLabel: Action; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/stores/document/document-store.stories.tsx b/services/madoc-ts/src/frontend/shared/capture-models/editor/stores/document/document-store.stories.tsx index 2bf144e95..6001371c9 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/stores/document/document-store.stories.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/stores/document/document-store.stories.tsx @@ -29,6 +29,7 @@ const Test: React.FC = () => { setLabelledBy={actions.setLabelledBy} setAllowMultiple={actions.setAllowMultiple} setRequired={actions.setRequired} + setDependant={actions.setDependant} deselectField={actions.deselectField} popSubtree={actions.popSubtree} pushSubtree={actions.pushSubtree} diff --git a/services/madoc-ts/src/frontend/shared/capture-models/editor/stores/document/document-store.ts b/services/madoc-ts/src/frontend/shared/capture-models/editor/stores/document/document-store.ts index 2e69a01f8..39b3b0cf3 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/editor/stores/document/document-store.ts +++ b/services/madoc-ts/src/frontend/shared/capture-models/editor/stores/document/document-store.ts @@ -175,6 +175,15 @@ export const DocumentStore = createContextStore< resolveSubtree(state.subtreePath, state.document).required = payload; }), + // set the ID of the field this field depends on + setDependant: action((state, payload) => { + if (!payload) { + delete resolveSubtree(state.subtreePath, state.document).dependant; + } else { + resolveSubtree(state.subtreePath, state.document).dependant = payload; + } + }), + setPluralLabel: action((state, payload) => { if (!payload) { delete resolveSubtree(state.subtreePath, state.document).pluralLabel; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/helpers/capture-model-shorthand-text.ts b/services/madoc-ts/src/frontend/shared/capture-models/helpers/capture-model-shorthand-text.ts index e52a72b7a..81f0decc9 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/helpers/capture-model-shorthand-text.ts +++ b/services/madoc-ts/src/frontend/shared/capture-models/helpers/capture-model-shorthand-text.ts @@ -48,6 +48,12 @@ function addTemplateValuesToDocument( } break; } + case 'label': { + if (modifier.value) { + field.label = modifier.value; + } + break; + } case 'defaultLang': { if (modifier.value) { defaultLang = modifier.value; @@ -148,7 +154,7 @@ export function captureModelShorthandText( .filter(Boolean); const fields = bulkItems.map(t => templatedValueFormat< - 'many' | 'default' | 'type' | 'lang' | 'langs' | 'defaultLang' | 'description' | 'pluralLabel' + 'many' | 'default' | 'type' | 'lang' | 'langs' | 'defaultLang' | 'description' | 'pluralLabel' | 'label' >(t) ); diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/CoreModelEditor.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/CoreModelEditor.tsx index 777201190..4bf45632f 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/CoreModelEditor.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/CoreModelEditor.tsx @@ -4,9 +4,9 @@ import { useTranslation } from 'react-i18next'; import { PARAGRAPHS_PROFILE } from '../../../../extensions/capture-models/Paragraphs/Paragraphs.helpers'; import { slotConfig } from '../../../../extensions/capture-models/Paragraphs/Paragraphs.slots'; import { AnnotationStyles } from '../../../../types/annotation-styles'; -import { CanvasHighlightedRegions } from '../../../site/features/CanvasHighlightedRegions'; -import { CanvasModelUserStatus } from '../../../site/features/contributor/CanvasModelUserStatus'; -import { CanvasViewer, CanvasViewerProps } from '../../../site/features/CanvasViewer'; +import { CanvasHighlightedRegions } from '../../../site/features/canvas/CanvasHighlightedRegions'; +import { CanvasModelUserStatus } from '../../../site/features/canvas/CanvasModelUserStatus'; +import { CanvasViewer, CanvasViewerProps } from '../../../site/features/canvas/CanvasViewer'; import { CanvasViewerButton, CanvasViewerContentOverlay, @@ -17,9 +17,9 @@ import { CanvasViewerGridSidebar, ContributionSaveButton, } from '../../atoms/CanvasViewerGrid'; -import { CreateModelTestCase } from '../../../site/features/admin/CreateModelTestCase'; -import { OpenSeadragonViewer } from '../../../site/features/OpenSeadragonViewer.lazy'; -import { TranscriberModeWorkflowBar } from '../../../site/features/contributor/TranscriberModeWorkflowBar'; +import { CreateModelTestCase } from '../../../site/features/CreateModelTestCase'; +import { OpenSeadragonViewer } from '../../features/OpenSeadragonViewer.lazy'; +import { TranscriberModeWorkflowBar } from '../../../site/features/canvas/TranscriberModeWorkflowBar'; import { RouteContext } from '../../../site/hooks/use-route-context'; import { ViewReadOnlyAnnotation } from '../../atlas/ViewReadOnlyAnnotation'; import { InfoMessage } from '../../callouts/InfoMessage'; @@ -32,8 +32,9 @@ import { PlusIcon } from '../../icons/PlusIcon'; import { RotateIcon } from '../../icons/RotateIcon'; import { TickIcon } from '../../icons/TickIcon'; import { EmptyState } from '../../layout/EmptyState'; -import { Button, ButtonIcon } from '../../navigation/Button'; +import { Button } from '../../navigation/Button'; import { BrowserComponent } from '../../utility/browser-component'; +import { CaptureModelVisualSettings } from '../editor/components/CaptureModelVisualSettings/CaptureModelVisualSettings'; import { CaptureModel } from '../types/capture-model'; import { RevisionRequest } from '../types/revision-request'; import { BackToChoicesButton } from './components/BackToChoicesButton'; @@ -56,6 +57,7 @@ export interface CoreModelEditorProps { // Options isPreparing?: boolean; allowMultiple?: boolean; + autosave?: boolean; forkMode?: boolean; @@ -103,6 +105,8 @@ export interface CoreModelEditorProps { showBugReport?: boolean; children?: React.ReactNode; + + visualConfig?: Partial; } export function CoreModelEditor({ revision, @@ -110,6 +114,7 @@ export function CoreModelEditor({ annotationTheme, disablePreview, isEditing, + autosave, mode, isSegmentation, forkMode, @@ -132,6 +137,7 @@ export function CoreModelEditor({ enableHighlightedRegions, canvasViewerPins, showBugReport, + visualConfig, children, }: CoreModelEditorProps) { const { t } = useTranslation(); @@ -195,6 +201,7 @@ export function CoreModelEditor({ : { preventMultiple: !allowMultiple, forkMode: forkMode, + autosave: autosave, }; const _components: Partial = isPreparing @@ -247,6 +254,7 @@ export function CoreModelEditor({ features={features} revision={isSegmentation ? undefined : revision} captureModel={captureModel} + visualConfig={visualConfig} slotConfig={{ editor: { allowEditing: !preventFurtherSubmission, diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/DynamicVaultContext.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/DynamicVaultContext.tsx index a4bb70046..dca17d44d 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/DynamicVaultContext.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/DynamicVaultContext.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { CanvasContext, ManifestContext, useExternalManifest } from 'react-iiif-vault'; import { ViewContentFetch } from '../../../admin/molecules/ViewContentFetch'; -import { CanvasVaultContext } from '../../components/CanvasVaultContext'; -import { ContentExplorer } from '../../components/ContentExplorer'; +import { CanvasVaultContext } from '../CanvasVaultContext'; +import { ContentExplorer } from '../../features/ContentExplorer'; import { TinyButton } from '../../navigation/Button'; import { BrowserComponent } from '../../utility/browser-component'; import { EditorContentVariations } from './EditorContent'; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/EditorContent.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/EditorContent.tsx index a8770566f..df1355e27 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/EditorContent.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/EditorContent.tsx @@ -1,13 +1,13 @@ import { Preset } from '@atlas-viewer/atlas'; -import React, { Suspense } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { CanvasFull } from '../../../../types/canvas-full'; import { parseUrn } from '../../../../utility/parse-urn'; import { ViewContentFetch } from '../../../admin/molecules/ViewContentFetch'; import { TinyButton } from '../../navigation/Button'; -import { ContentExplorer } from '../../components/ContentExplorer'; +import { ContentExplorer } from '../../features/ContentExplorer'; import { ViewContent } from '../../components/ViewContent'; -import { ViewExternalContent } from '../../components/ViewExternalContent'; +import { ViewExternalContent } from './ViewExternalContent'; import { BrowserComponent } from '../../utility/browser-component'; import { CaptureModel } from '../types/capture-model'; diff --git a/services/madoc-ts/src/frontend/shared/components/ViewExternalContent.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/ViewExternalContent.tsx similarity index 83% rename from services/madoc-ts/src/frontend/shared/components/ViewExternalContent.tsx rename to services/madoc-ts/src/frontend/shared/capture-models/new/ViewExternalContent.tsx index 9c0cf8026..62d5774a7 100644 --- a/services/madoc-ts/src/frontend/shared/components/ViewExternalContent.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/ViewExternalContent.tsx @@ -1,7 +1,7 @@ import { Preset } from '@atlas-viewer/atlas'; import React, { useMemo } from 'react'; -import { useSiteConfiguration } from '../../site/features/SiteConfigurationContext'; -import { useContentType } from '../capture-models/plugin-api/hooks/use-content-type'; +import { useSiteConfiguration } from '../../../site/features/SiteConfigurationContext'; +import { useContentType } from '../plugin-api/hooks/use-content-type'; export const ViewExternalContent: React.FC<{ target: any; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultBreadcrumbs.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultBreadcrumbs.tsx index 941088a8e..b58515569 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultBreadcrumbs.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultBreadcrumbs.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { BreadcrumbDivider, BreadcrumbItem, BreadcrumbList } from '../../../components/Breadcrumbs'; +import { BreadcrumbDivider, BreadcrumbItem, BreadcrumbList } from '../../../../site/blocks/Breadcrumbs'; import { useModelTranslation } from '../../hooks/use-model-translation'; import { EditorRenderingConfig } from './EditorSlots'; import { HomeIcon } from '../../../icons/HomeIcon'; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultFieldInstance.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultFieldInstance.tsx index b3435d17a..3845877de 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultFieldInstance.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultFieldInstance.tsx @@ -5,6 +5,7 @@ import { BaseField } from '../../types/field-types'; import { useSlotConfiguration } from './EditorSlots'; import { FieldSet } from '../../../form/FieldSet'; import { useResolvedSelector } from '../hooks/use-resolved-selector'; +import { useResolvedDependant } from '../hooks/use-resolved-dependant'; export const DefaultFieldInstance: React.FC<{ field: BaseField; @@ -17,6 +18,7 @@ export const DefaultFieldInstance: React.FC<{ const { immutableFields = [] } = useSlotConfiguration(); const immutable = immutableFields.indexOf(property) !== -1; const [selector, { isBlockingForm }] = useResolvedSelector(field); + const dependantValue = useResolvedDependant(field); const disableForm = isBlockingForm; useEffect(() => { @@ -46,6 +48,9 @@ export const DefaultFieldInstance: React.FC<{ return null; } + if (!dependantValue) { + return null; + } return (
    ) : null} diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultManagePropertiesList.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultManagePropertiesList.tsx index feb3949f3..4fb340a35 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultManagePropertiesList.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultManagePropertiesList.tsx @@ -62,6 +62,7 @@ export const DefaultManagePropertyList: EditorRenderingConfig['ManagePropertyLis canAddAnother, canAdd, canRemove, + dependantValue, label, createNewEntity, createNewField, @@ -83,7 +84,7 @@ export const DefaultManagePropertyList: EditorRenderingConfig['ManagePropertyLis } }; - if (!allowMultiple || (!canRemove && !canAdd)) { + if (!allowMultiple || (!canRemove && !canAdd) || !dependantValue) { return <>{children}; } diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultSingleEntity.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultSingleEntity.tsx index 90b13395f..bd023695c 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultSingleEntity.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultSingleEntity.tsx @@ -29,7 +29,14 @@ export const DefaultSingleEntity: EditorRenderingConfig['SingleEntity'] = props - {showTitle ? : null} + {showTitle ? ( + + ) : null} {isModified && } diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultTopLevelEditor.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultTopLevelEditor.tsx index d5046d00d..ab2187296 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultTopLevelEditor.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/components/DefaultTopLevelEditor.tsx @@ -10,6 +10,7 @@ export const DefaultTopLevelEditor: EditorRenderingConfig['TopLevelEditor'] = () revisionSubtreeField: s.revisionSubtreeField, revisionSubtree: s.revisionSubtree, revisionSubtreePath: s.revisionSubtreePath, + revisionSubtreeFields: s.revisionSubtreeFields, })); const [currentView] = useNavigation(); diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/components/RevisionOnChange.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/components/RevisionOnChange.tsx new file mode 100644 index 000000000..11f329a3c --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/components/RevisionOnChange.tsx @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import { Revisions } from '../../editor/stores/revisions/index'; +import { RevisionRequest } from '../../types/revision-request'; + +export function RevisionOnChange({ onChange }: { onChange: (data: RevisionRequest | null) => void }) { + const store = Revisions.useStore(); + + useEffect(() => { + let lastRevision = store.getState().currentRevision; + return store.subscribe(() => { + const data = store.getState(); + if (data.currentRevision !== lastRevision) { + lastRevision = data.currentRevision; + onChange(data.currentRevision); + } + }); + }, [store]); + + return null; +} diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/components/RevisionProviderWithFeatures.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/components/RevisionProviderWithFeatures.tsx index cc4c14d8e..e3f85131f 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/components/RevisionProviderWithFeatures.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/components/RevisionProviderWithFeatures.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { AnnotationStyles } from '../../../../../types/annotation-styles'; import { AnnotationStyleProvider } from '../../AnnotationStyleContext'; +import { + CaptureModelVisualSettings, + CaptureModelVisualSettingsProvider, +} from '../../editor/components/CaptureModelVisualSettings/CaptureModelVisualSettings'; import { WithModelNamespace } from '../../hooks/use-model-translation'; import { CaptureModel } from '../../types/capture-model'; import { AutosaveRevision } from '../features/AutosaveRevision'; @@ -34,6 +38,7 @@ export const RevisionProviderWithFeatures: React.FC<{ revision?: string | undefined; excludeStructures?: boolean | undefined; features?: RevisionProviderFeatures; + visualConfig?: Partial; annotationTheme?: AnnotationStyles['theme']; }> = ({ annotationTheme, @@ -44,10 +49,11 @@ export const RevisionProviderWithFeatures: React.FC<{ excludeStructures, initialRevision, features, + visualConfig = {}, }) => { const { autoSelectingRevision = true, - autosave = false, + // autosave = true, revisionEditMode = true, directEdit = false, preventMultiple = false, @@ -59,31 +65,33 @@ export const RevisionProviderWithFeatures: React.FC<{ return ( - - - - {/**/} - {/**/} - {autosave ? : null} - {revisionEditMode ? : null} - {revisionEditMode ? : null} - {autoSelectingRevision ? ( - - ) : null} - {basicUnNesting ? : null} - - - {children} - - - - + + + + + {/**/} + {/**/} + {features?.autosave ? : null} + {revisionEditMode ? : null} + {revisionEditMode ? : null} + {autoSelectingRevision ? ( + + ) : null} + {basicUnNesting ? : null} + + + {children} + + + + + ); }; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/features/AutosaveRevision.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/features/AutosaveRevision.tsx index 20488b2dd..13bc144fc 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/features/AutosaveRevision.tsx +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/features/AutosaveRevision.tsx @@ -1,49 +1,116 @@ import React, { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; -import { useApi } from '../../../hooks/use-api'; import { Revisions } from '../../editor/stores/revisions/index'; +import * as localforage from 'localforage'; +import { TextButton } from '../../../navigation/Button'; +import { isEmptyRevision } from '../../helpers/is-empty-revision'; +import { InfoMessage } from '../../../callouts/InfoMessage'; +import { RevisionRequest } from '../../types/revision-request'; -export const AutosaveRevision: React.FC<{ minutes?: number }> = ({ minutes = 2 }) => { +export const AutosaveRevision: React.FC = () => { const currentRevisionId = Revisions.useStoreState(s => s.currentRevisionId); const currentRevision = Revisions.useStoreState(s => s.currentRevision); const currentDoc = currentRevision?.document.properties; const currentDocRef = useRef(); + const importRevision = Revisions.useStoreActions(a => a.importRevision); + const selectRevision = Revisions.useStoreActions(a => a.selectRevision); + const deselectRevision = Revisions.useStoreActions(a => a.deselectRevision); + const { t } = useTranslation(); - const api = useApi(); const [autosaveRevision] = useMutation(async () => { if (!currentRevision) { return; } + const isEmpty = isEmptyRevision(currentRevision); + if (isEmpty) { + return; + } + localforage + .setItem(`autosave-${currentRevision.captureModelId}`, currentRevision) + .then(value => { + console.log('saved!'); + }) + .catch(err => { + console.log(err); + throw new Error(t('Unable to save your submission')); + }); + }); - try { - // Change this to "draft" to save for later. - await api.updateCaptureModelRevision(currentRevision, 'draft'); - } catch (e) { - console.error(e); - throw new Error(t('Unable to save your submission')); + const [getSavedRevision, rev] = useMutation(async () => { + if (!currentRevision) { + return; } + return localforage + .getItem(`autosave-${currentRevision?.captureModelId}`) + .then(value => { + if (value) { + return value; + } + return; + }) + .catch(err => { + console.log(err); + throw new Error(t('Unable to retreive a submission')); + }); }); + const setRetrievedRevision = () => { + if (currentRevisionId && rev.data) { + deselectRevision({ revisionId: currentRevisionId }); + importRevision({ revisionRequest: rev.data }); + selectRevision({ revisionId: rev.data.revision.id }); + + localforage + .removeItem(`autosave-${currentRevision?.captureModelId}`) + .then(() => { + console.log('Key is cleared!'); + }) + .catch(err => { + console.log(err); + }); + } + }; + useEffect(() => { if (currentRevisionId && currentDoc) { currentDocRef.current = currentDoc; } }, [currentDoc, currentRevisionId]); + useEffect(() => { + if (currentRevisionId) { + getSavedRevision(); + } + }, [currentRevisionId, getSavedRevision]); + useEffect(() => { const saveInterval = setInterval(() => { if (currentDocRef.current && currentRevisionId) { currentDocRef.current = undefined; autosaveRevision(); } - }, minutes * 60 * 1000); + }, 10000); // 10 secs return () => { clearInterval(saveInterval); }; - }, [autosaveRevision, currentRevisionId, minutes]); + }, [autosaveRevision, currentRevisionId]); + if (rev.data) { + return ( + + { + setRetrievedRevision(); + }} + > + {t('Continue where you left off?')} + + + ); + } return null; }; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/hooks/use-manage-property-list.ts b/services/madoc-ts/src/frontend/shared/capture-models/new/hooks/use-manage-property-list.ts index e0dd6523a..e92f88c48 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/new/hooks/use-manage-property-list.ts +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/hooks/use-manage-property-list.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { Revisions } from '../../editor/stores/revisions/index'; import { useSlotContext } from '../components/EditorSlots'; import { useCurrentEntity } from './use-current-entity'; +import { useResolvedDependant } from './use-resolved-dependant'; export function useManagePropertyList(property: string) { const { configuration } = useSlotContext(); @@ -18,6 +19,9 @@ export function useManagePropertyList(property: string) { })); const canAddAnother = Boolean(maxMultiple ? maxMultiple > fields.length : true); + // @ts-ignore + const dependantValue = useResolvedDependant(fields[0]); + const createNewEntity = useCallback(() => { if (configuration.allowEditing) { createNewEntityInstance({ property, path: path as any }); @@ -60,6 +64,7 @@ export function useManagePropertyList(property: string) { canRemove: configuration.allowEditing, canAdd: configuration.allowEditing, removeItem, + dependantValue: dependantValue, createNewEntity, createNewField, }; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/new/hooks/use-resolved-dependant.ts b/services/madoc-ts/src/frontend/shared/capture-models/new/hooks/use-resolved-dependant.ts new file mode 100644 index 000000000..a6de338da --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/capture-models/new/hooks/use-resolved-dependant.ts @@ -0,0 +1,25 @@ +import { BaseField } from '../../types/field-types'; +import { Revisions } from '../../editor/stores/revisions'; +import { isEntityEmpty } from '../../utility/is-entity-empty'; + +export function useResolvedDependant(entityOrField: BaseField) { + const currentRevision = Revisions.useStoreState(s => s.currentRevision); + const revisionDocument = currentRevision?.document; + + if (entityOrField.dependant && revisionDocument) { + const dependantField = revisionDocument.properties[entityOrField.dependant][0]; + if (dependantField) { + if (dependantField.type === 'entity') { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const isEmpty = isEntityEmpty(dependantField); + return !isEmpty; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return !(!dependantField.value || dependantField.value === '') || !!dependantField.selector?.state; + } + return true; + } + return true; +} diff --git a/services/madoc-ts/src/frontend/shared/capture-models/types/base-property.ts b/services/madoc-ts/src/frontend/shared/capture-models/types/base-property.ts index 897326b0e..31d7f3704 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/types/base-property.ts +++ b/services/madoc-ts/src/frontend/shared/capture-models/types/base-property.ts @@ -13,6 +13,7 @@ export interface BaseProperty { allowMultiple?: boolean; maxMultiple?: number; required?: boolean; + dependant?: string; immutable?: boolean; profile?: string; dataSources?: string[]; diff --git a/services/madoc-ts/src/frontend/shared/capture-models/types/field-types.ts b/services/madoc-ts/src/frontend/shared/capture-models/types/field-types.ts index 09e2ad7e0..663a778aa 100644 --- a/services/madoc-ts/src/frontend/shared/capture-models/types/field-types.ts +++ b/services/madoc-ts/src/frontend/shared/capture-models/types/field-types.ts @@ -25,6 +25,7 @@ export type FieldSpecification = { allowMultiple: boolean; maxMultiple?: number; required?: boolean; + dependant?: string; defaultProps: Partial; Component: FC>; TextPreview: FC; diff --git a/services/madoc-ts/src/frontend/shared/components/AtlasTiledImages.tsx b/services/madoc-ts/src/frontend/shared/components/AtlasTiledImages.tsx deleted file mode 100644 index 671741648..000000000 --- a/services/madoc-ts/src/frontend/shared/components/AtlasTiledImages.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { Fragment, useEffect, useState } from 'react'; -import { CanvasNormalized, ImageService } from '@iiif/presentation-3'; -import { GetTile, getTileFromImageService, TileSet, useRuntime } from '@atlas-viewer/atlas'; - -export const AtlasTiledImages: React.FC<{ canvas: CanvasNormalized; service: ImageService }> = ({ - canvas, - service, -}) => { - const [tile, setTile] = useState(); - const runtime = useRuntime(); - - useEffect(() => { - if (service && runtime) { - getTileFromImageService((service as any).id, canvas.width, canvas.height).then(s => { - setTile(s); // only show the first image. - // @todo change this to be when the new image is REPLACED in the frame. Maybe better done at Atlas level. - runtime.goHome(); - }); - } - }, [runtime, service, canvas]); - - if (!tile) { - return ( - - - - ); - } - - return ; -}; diff --git a/services/madoc-ts/src/frontend/shared/components/CanvasNavigationMinimalist.tsx b/services/madoc-ts/src/frontend/shared/components/CanvasNavigationMinimalist.tsx deleted file mode 100644 index 85761ef89..000000000 --- a/services/madoc-ts/src/frontend/shared/components/CanvasNavigationMinimalist.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useRouteContext } from '../../site/hooks/use-route-context'; -import { useCanvasSearch } from '../hooks/use-canvas-search'; -import { useManifestStructure } from '../hooks/use-manifest-structure'; -import { createLink } from '../utility/create-link'; -import styled, { css } from 'styled-components'; -import { ItemStructureListItem } from '../../../types/schemas/item-structure-list'; -import { DownArrowIcon } from '../icons/DownArrowIcon'; -import { HrefLink } from '../utility/href-link'; - -const PaginationButton = styled.button` - padding: 0.1rem; - border: 1px solid #6c757d; - border-radius: 3px; - background-color: white; - margin: 0.5rem; - &:hover { - background-color: #eee; - } -`; - -export const PaginationContainer = styled.div<{ $size?: string }>` - display: flex; - justify-content: space-between; - align-items: center; - ${props => - props.$size === 'small' && - css` - width: 250px; - border: 1px solid #6c757d; - padding: 0; - margin-left: auto; - button { - border: none; - } - `} -`; - -export const PaginationText = styled.div` - white-space: nowrap; - font-size: 0.85em; - margin: 0 0.5em; -`; - -export const NavigationButton: React.FC<{ - label?: string; - link: string; - alignment?: 'left' | 'right'; - onClick?: (e: React.PointerEvent) => void; - item: ItemStructureListItem; -}> = props => { - return ( - - - {props.item ? ( - - ) : null} - - - ); -}; - -export function useManifestPagination(subRoute?: string) { - const { manifestId, collectionId, canvasId, projectId } = useRouteContext(); - const [searchText] = useCanvasSearch(canvasId); - const structure = useManifestStructure(manifestId); - const query = searchText ? { searchText } : undefined; - - const idx = structure.data && canvasId ? structure.data.ids.indexOf(canvasId) : -1; - - if (!structure.data || idx === -1 || !manifestId) { - return null; - } - - const hasPrevPage = idx > 0 && structure.data.items[idx - 1]; - const hasNextPage = idx < structure.data.items.length && structure.data.items[idx + 1]; - - return { - hasPrevPage: !!hasPrevPage, - prevItem: hasPrevPage, - prevPage: hasPrevPage - ? createLink({ - projectId, - collectionId, - manifestId, - canvasId: structure.data.items[idx - 1].id, - subRoute, - query, - }) - : undefined, - - hasNextPage: !!hasNextPage, - nextItem: hasNextPage, - nextPage: hasNextPage - ? createLink({ - projectId, - collectionId, - manifestId, - canvasId: structure.data.items[idx + 1].id, - subRoute, - query, - }) - : undefined, - }; -} - -export const CanvasNavigationMinimalist: React.FC<{ - hash?: string; - handleNavigation?: (canvasId: number) => Promise | void; - canvasId: string | number; - manifestId?: string | number; - projectId?: string | number; - collectionId?: string | number; - subRoute?: string; - query?: any; - size?: string | undefined; -}> = ({ canvasId: id, manifestId, projectId, collectionId, subRoute, query, handleNavigation, hash, size }) => { - const structure = useManifestStructure(manifestId); - const { t } = useTranslation(); - - const idx = structure.data ? structure.data.ids.indexOf(Number(id)) : -1; - - if (!structure.data || idx === -1 || !manifestId) { - return null; - } - - return ( - - {idx > 0 ? ( - { - if (handleNavigation) { - e.preventDefault(); - if (structure.data) { - handleNavigation(structure.data.items[idx - 1].id); - } - } - }} - link={createLink({ - projectId, - collectionId, - manifestId, - canvasId: structure.data.items[idx - 1].id, - subRoute, - query, - hash, - })} - item={structure.data.items[idx - 1]} - /> - ) : null} - { - - {t('Page {{page}} of {{count}}', { - page: idx + 1, - count: structure.data.items.length > 1 ? structure.data.items.length : 1, - })} - - } - {idx < structure.data.items.length - 1 ? ( - { - if (handleNavigation) { - e.preventDefault(); - if (structure.data) { - handleNavigation(structure.data.items[idx + 1].id); - } - } - }} - link={createLink({ - projectId, - collectionId, - manifestId, - canvasId: structure.data.items[idx + 1].id, - subRoute, - query, - hash, - })} - item={structure.data.items[idx + 1]} - /> - ) : null} - - ); -}; diff --git a/services/madoc-ts/src/frontend/shared/atoms/Carousel.tsx b/services/madoc-ts/src/frontend/shared/components/Carousel.tsx similarity index 100% rename from services/madoc-ts/src/frontend/shared/atoms/Carousel.tsx rename to services/madoc-ts/src/frontend/shared/components/Carousel.tsx diff --git a/services/madoc-ts/src/frontend/shared/components/ContinueTaskDisplay.tsx b/services/madoc-ts/src/frontend/shared/components/ContinueTaskDisplay.tsx deleted file mode 100644 index f5c97871a..000000000 --- a/services/madoc-ts/src/frontend/shared/components/ContinueTaskDisplay.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SubjectSnippet as SubjectSnippetType } from '../../../extensions/tasks/resolvers/subject-resolver'; -import { BaseTask } from '../../../gateway/tasks/base-task'; -import { useRelativeLinks } from '../../site/hooks/use-relative-links'; -import { useTaskMetadata } from '../../site/hooks/use-task-metadata'; -import { PrimaryButtonLink } from '../navigation/Button'; -import { SnippetLarge } from '../atoms/SnippetLarge'; -import { useCreateLocaleString } from './LocaleString'; - -export function ContinueTaskDisplay({ - task, - next, - manifestModel, -}: { - task: BaseTask; - next?: boolean; - manifestModel?: boolean; -}) { - const { subject } = useTaskMetadata<{ subject?: SubjectSnippetType }>(task); - const createLocalString = useCreateLocaleString(); - const createLink = useRelativeLinks(); - const { t } = useTranslation(); - - const link = subject - ? subject.type === 'manifest' - ? createLink({ manifestId: subject.id, subRoute: manifestModel ? 'model' : '' }) - : subject.type === 'canvas' - ? createLink({ - canvasId: subject.id, - manifestId: subject.parent?.id, - subRoute: 'model', - }) - : undefined - : undefined; - - if (!subject || !link) { - return null; - } - - return ( - - ); -} diff --git a/services/madoc-ts/src/frontend/shared/components/DashboardTabs.tsx b/services/madoc-ts/src/frontend/shared/components/DashboardTabs.tsx index ac1a56cdd..d6b6279ba 100644 --- a/services/madoc-ts/src/frontend/shared/components/DashboardTabs.tsx +++ b/services/madoc-ts/src/frontend/shared/components/DashboardTabs.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import styled, { css } from 'styled-components'; import { useBrowserLayoutEffect } from '../hooks/use-browser-layout-effect'; @@ -90,7 +90,6 @@ export const DashboardTab = styled.li<{ $active?: boolean }>` color: #fff; padding: 0.65em 1em; display: block; - border-bottom-width: 3px; &:hover { text-decoration: none; } diff --git a/services/madoc-ts/src/frontend/shared/components/IIIFHero.tsx b/services/madoc-ts/src/frontend/shared/components/IIIFHero.tsx index 94b6b6759..7cddcf32b 100644 --- a/services/madoc-ts/src/frontend/shared/components/IIIFHero.tsx +++ b/services/madoc-ts/src/frontend/shared/components/IIIFHero.tsx @@ -107,6 +107,7 @@ const HeroAssetThumbnails = styled.div<{ $background: string }>` const HeroAssetLargeThumbnail = styled.div` z-index: 2; img { + max-width: initial; height: 280px; object-fit: contain; box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.53); @@ -121,6 +122,7 @@ const HeroAssetThumbnail = styled.div` z-index: 2; margin-left: 10px; img { + max-width: initial; box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.53); &:first-child { margin-top: 0; diff --git a/services/madoc-ts/src/frontend/shared/components/InlineSelect.tsx b/services/madoc-ts/src/frontend/shared/components/InlineSelect.tsx new file mode 100644 index 000000000..08bb6002a --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/components/InlineSelect.tsx @@ -0,0 +1,154 @@ +import { InternationalString } from '@iiif/presentation-3'; +import React, { createRef, KeyboardEventHandler, useLayoutEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { LocaleString } from './LocaleString'; + +export interface InlineSelectProps { + name?: string; + id?: string; + value?: T | null; + onChange?: (value: T) => void; + onDeselect?: () => void; + options: Array<{ label: string | InternationalString; value: string }>; +} + +const Container = styled.div` + background: #e4e7f0; + display: flex; + border: 1px solid #b1b1b1; + border-radius: 3px; + overflow: hidden; + + &[data-vertical='true'] { + flex-direction: column; + max-height: 13em; + overflow: auto; + } +`; + +const Item = styled.button` + flex: 1; + background: transparent; + border: none; + padding: 0.6em 1em; + color: #777; + white-space: nowrap; + font-size: 0.875em; + + &:hover { + background: #eff2f6; + } + + &:focus { + // ? + } + + & ~ & { + border-left: 1px solid #b1b1b1; + } + + &[data-active='true'] { + background: #fff; + color: #000; + } + + [data-vertical='true'] & { + border-left: none; + } + + [data-vertical='true'] & ~ & { + border-top: 1px solid #b1b1b1; + } +`; + +export function InlineSelect(props: InlineSelectProps) { + const [currentValue, _setCurrentValue] = useState(props.value); + const itemsLength = props.options.length; + const elRefs = useMemo(() => props.options.map(() => createRef()), [props.options]) as any[]; + + const setCurrentValue = (newValue: T) => { + if (props.onChange) { + props.onChange(newValue); + } + _setCurrentValue(newValue); + }; + + useLayoutEffect(() => { + if (props.id) { + const listener = () => { + const idx = props.options.findIndex(b => b.value === props.value); + if (idx !== -1 && elRefs[idx].current) { + elRefs[idx].current.focus(); + } + }; + + const $el = document.querySelector(`label[for="${props.id}"]`); + if ($el) { + $el.addEventListener('click', listener); + return () => { + $el.removeEventListener('click', listener); + }; + } + } + }, [props.id]); + + const onKeyDown: KeyboardEventHandler = e => { + const currentEl = document.activeElement; + const currentIndex = elRefs.findIndex(r => r.current === currentEl); + if (currentIndex === -1) { + return; + } + + switch (e.code) { + case 'ArrowRight': + case 'ArrowDown': { + if (currentIndex !== itemsLength - 1) { + const next = currentIndex + 1; + if (elRefs[next]) { + elRefs[next].current?.focus(); + } + } + break; + } + // Focus previous + case 'ArrowLeft': + case 'ArrowUp': { + if (currentIndex !== 0) { + const next = currentIndex - 1; + if (elRefs[next]) { + elRefs[next].current?.focus(); + } + } + break; + } + } + }; + + return ( + 4}> + {props.options.map((option, idx) => { + return ( + { + if (option.value === currentValue && props.onDeselect) { + props.onDeselect(); + setCurrentValue('' as any); + } else { + setCurrentValue(option.value as any); + } + }} + > + {option.label} + + ); + })} + + {props.name ? : null} + + ); +} diff --git a/services/madoc-ts/src/frontend/shared/components/LocaleString.tsx b/services/madoc-ts/src/frontend/shared/components/LocaleString.tsx index 7f56d0d11..28dbe3914 100644 --- a/services/madoc-ts/src/frontend/shared/components/LocaleString.tsx +++ b/services/madoc-ts/src/frontend/shared/components/LocaleString.tsx @@ -1,6 +1,6 @@ import { InternationalString } from '@iiif/presentation-3'; import { useTranslation } from 'react-i18next'; -import React, { CSSProperties, useMemo } from 'react'; +import React, { useMemo } from 'react'; export const LanguageString: React.FC<{ [key: string]: any } & { as?: string | React.FC; language: string }> = ({ as: Component, diff --git a/services/madoc-ts/src/frontend/shared/components/ManifestProjectListing.tsx b/services/madoc-ts/src/frontend/shared/components/ManifestProjectListing.tsx deleted file mode 100644 index ef5a6d866..000000000 --- a/services/madoc-ts/src/frontend/shared/components/ManifestProjectListing.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ProjectListing } from '../atoms/ProjectListing'; -import React from 'react'; -import { useManifestProjects } from '../hooks/use-manifest-projects'; - -export const ManifestProjectListing: React.FC<{ - manifestId: string | number; - onContribute: (id: string | number) => void; -}> = ({ manifestId, onContribute }) => { - const projects = useManifestProjects(manifestId); - const hasProjects = projects.data && projects.data.projects.length; - - if (!hasProjects || !projects.data) { - return null; - } - - return ; -}; diff --git a/services/madoc-ts/src/frontend/shared/components/MetaDataDisplay.tsx b/services/madoc-ts/src/frontend/shared/components/MetaDataDisplay.tsx index b60ac6e17..1a4dd2687 100644 --- a/services/madoc-ts/src/frontend/shared/components/MetaDataDisplay.tsx +++ b/services/madoc-ts/src/frontend/shared/components/MetaDataDisplay.tsx @@ -6,7 +6,7 @@ import { MetadataEmptyState } from '../atoms/MetadataConfiguration'; import { Button } from '../navigation/Button'; import { HrefLink } from '../utility/href-link'; import { LocaleString } from './LocaleString'; -import { FacetConfig } from './MetadataFacetEditor'; +import { FacetConfig } from '../features/MetadataFacetEditor'; const MetadataDisplayContainer = styled.div<{ $variation?: 'list' | 'table'; $size?: 'lg' | 'md' | 'sm' }>` font-size: ${props => diff --git a/services/madoc-ts/src/frontend/shared/components/Modal.tsx b/services/madoc-ts/src/frontend/shared/components/Modal.tsx index 87adbf044..78486e352 100644 --- a/services/madoc-ts/src/frontend/shared/components/Modal.tsx +++ b/services/madoc-ts/src/frontend/shared/components/Modal.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import { useBrowserLayoutEffect } from '../hooks/use-browser-layout-effect'; import { Spinner } from '../icons/Spinner'; import { diff --git a/services/madoc-ts/src/frontend/shared/components/NavigationButton.tsx b/services/madoc-ts/src/frontend/shared/components/NavigationButton.tsx new file mode 100644 index 000000000..d35aa8ef3 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/components/NavigationButton.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useRouteContext } from '../../site/hooks/use-route-context'; +import { useCanvasSearch } from '../hooks/use-canvas-search'; +import { useManifestStructure } from '../hooks/use-manifest-structure'; +import { createLink } from '../utility/create-link'; +import styled, { css } from 'styled-components'; +import { ItemStructureListItem } from '../../../types/schemas/item-structure-list'; +import { DownArrowIcon } from '../icons/DownArrowIcon'; +import { HrefLink } from '../utility/href-link'; + +const PaginationButton = styled.button` + padding: 0.1rem; + border: 1px solid #6c757d; + border-radius: 3px; + background-color: white; + margin: 0.5rem; + &:hover { + background-color: #eee; + } +`; + +export const PaginationContainer = styled.div<{ $size?: string }>` + display: flex; + justify-content: space-between; + align-items: center; + ${props => + props.$size === 'small' && + css` + width: 250px; + border: 1px solid #6c757d; + padding: 0; + margin-left: auto; + button { + border: none; + } + `} +`; + +export const PaginationText = styled.div` + white-space: nowrap; + font-size: 0.85em; + margin: 0 0.5em; +`; + +export const NavigationButton: React.FC<{ + label?: string; + link: string; + alignment?: 'left' | 'right'; + onClick?: (e: React.PointerEvent) => void; + item: ItemStructureListItem; +}> = props => { + return ( + + + {props.item ? ( + + ) : null} + + + ); +}; diff --git a/services/madoc-ts/src/frontend/shared/components/NotFoundPage.tsx b/services/madoc-ts/src/frontend/shared/components/NotFoundPage.tsx index 7572818f1..e6d8864a5 100644 --- a/services/madoc-ts/src/frontend/shared/components/NotFoundPage.tsx +++ b/services/madoc-ts/src/frontend/shared/components/NotFoundPage.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { useLocation } from 'react-router-dom'; import { Button, ButtonRow } from '../navigation/Button'; import { ErrorMessage } from '../callouts/ErrorMessage'; import { WidePageWrapper } from '../layout/WidePage'; diff --git a/services/madoc-ts/src/frontend/shared/components/NotificationCenter.tsx b/services/madoc-ts/src/frontend/shared/components/NotificationCenter.tsx index 4e23fba0c..605b0633a 100644 --- a/services/madoc-ts/src/frontend/shared/components/NotificationCenter.tsx +++ b/services/madoc-ts/src/frontend/shared/components/NotificationCenter.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import useDropdownMenu from 'react-accessible-dropdown-menu-hook'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; diff --git a/services/madoc-ts/src/frontend/shared/components/PageCreator.tsx b/services/madoc-ts/src/frontend/shared/components/PageCreator.tsx index 15dce75e3..f0e3c5172 100644 --- a/services/madoc-ts/src/frontend/shared/components/PageCreator.tsx +++ b/services/madoc-ts/src/frontend/shared/components/PageCreator.tsx @@ -1,25 +1,64 @@ import { InternationalString } from '@iiif/presentation-3'; import React, { useEffect, useState } from 'react'; +import { CreateNormalPageRequest } from '../../../types/schemas/site-page'; +import { SitePage } from '../../../types/site-pages-recursive'; import { MetadataEditor } from '../../admin/molecules/MetadataEditor'; -import { Input, InputContainer, InputLabel } from '../form/Input'; +import { Input, InputCheckboxContainer, InputCheckboxInputContainer, InputContainer, InputLabel } from '../form/Input'; import { useDetailedSupportLocales } from '../hooks/use-site'; export const PageCreator: React.FC<{ - defaultPath?: string; - defaultTitle?: InternationalString; - defaultDescription?: InternationalString; - onUpdate: (page: { path: string; title: InternationalString; description: InternationalString }) => void; -}> = ({ defaultPath = '', defaultDescription, defaultTitle, onUpdate }) => { + page?: Partial; + onUpdate: (page: CreateNormalPageRequest) => void; +}> = ({ page = {}, onUpdate }) => { + const { + path: defaultPath = '', + description: defaultDescription, + title: defaultTitle, + navigationTitle: defaultNavigationTitle, + navigationOptions: defaultNavigationOptions, + } = page; + const { order: defaultNavigationOrder = 0, hide: defaultHideFromNavigation = true } = defaultNavigationOptions || {}; + const supported = useDetailedSupportLocales(); const [path, setPath] = useState(defaultPath); const [title, setTitle] = useState(defaultTitle || { en: [''] }); + const [navigationTitle, setNavTitle] = useState(defaultNavigationTitle || {}); const [description, setDescription] = useState(defaultDescription || { en: [''] }); + const [navOrder, setNavOrder] = useState(defaultNavigationOrder); + const [hideFromNavigation, setHideFromNavigation] = useState(defaultHideFromNavigation); const languages = (supported || []).map(key => key.code); useEffect(() => { - onUpdate({ path, title, description }); - }, [path, title, description, onUpdate]); + const navigationOptions = hideFromNavigation + ? { + hide: true, + order: 0, + root: defaultNavigationOptions?.root || false, + } + : { + order: navOrder, + hide: false, + root: defaultNavigationOptions?.root || false, + }; + + onUpdate({ + path, + title, + description, + navigationTitle: Object.keys(navigationTitle).length ? navigationTitle : undefined, + navigationOptions, + }); + }, [ + path, + title, + description, + onUpdate, + hideFromNavigation, + navOrder, + navigationTitle, + defaultNavigationOptions?.root, + ]); return (
    @@ -39,6 +78,46 @@ export const PageCreator: React.FC<{ /> + + + + setHideFromNavigation(e.target.checked)} + /> + + Hide from navigation + + + + {!hideFromNavigation && ( + <> + + Navigation title + setNavTitle(output.toInternationalString())} + availableLanguages={languages} + metadataKey={'label'} + /> + + + + Navigation order + setNavOrder(e.target.valueAsNumber)} + /> + + + )} + Description ; - annotationPages?: AnnotationPage[]; - unstable_webglRenderer?: boolean; - } ->(function SimpleAtlasViewer({ style = { height: 600 }, highlightedRegions, unstable_webglRenderer }, ref) { - const { t } = useTranslation(); - const canvas = useCanvas(); - const runtime = useRef(); - const { data: service } = useImageService(); - const { - project: { atlasBackground }, - } = useSiteConfiguration(); - const [isLoaded, setIsLoaded] = useState(false); - const controller = useSelectorController(); - const readOnlyAnnotations = useReadOnlyAnnotations(false); - - const goHome = () => { - if (runtime.current) { - runtime.current.world.goHome(); - } - }; - - const zoomIn = () => { - if (runtime.current) { - runtime.current.world.zoomIn(); - } - }; - - const zoomOut = () => { - if (runtime.current) { - runtime.current.world.zoomOut(); - } - }; - - useBrowserLayoutEffect(() => { - setIsLoaded(true); - }, []); - - useEffect(() => { - return controller.on('zoomTo', e => { - const found = readOnlyAnnotations.find(r => { - return r.id === e.selectorId; - }); - if (found) { - runtime.current?.world.gotoRegion(found.target); - } - }); - }, [readOnlyAnnotations, controller]); - - if (!canvas) { - return null; - } - - return ( -
    - }> - - {isLoaded ? ( - <> - void (runtime.current = preset.runtime)}> - - - - {highlightedRegions - ? highlightedRegions.map(([x, y, width, height], key) => { - return ( - - { - // no-op - }} - onClick={() => { - // no-op - }} - /> - - ); - }) - : null} - {readOnlyAnnotations.map(anno => ( - - ))} - - - - - - - - - - - - - - - - ) : null} - -
    - ); -}); diff --git a/services/madoc-ts/src/frontend/shared/components/StandaloneCanvasViewer.tsx b/services/madoc-ts/src/frontend/shared/components/StandaloneCanvasViewer.tsx index 4475fd178..fa6be0619 100644 --- a/services/madoc-ts/src/frontend/shared/components/StandaloneCanvasViewer.tsx +++ b/services/madoc-ts/src/frontend/shared/components/StandaloneCanvasViewer.tsx @@ -4,9 +4,9 @@ import React, { useState } from 'react'; import { CanvasFull } from '../../../types/canvas-full'; import { useViewerHeight } from '../../site/hooks/use-viewer-height'; import { apiHooks } from '../hooks/use-api-query'; -import { SimpleAtlasViewer } from './SimpleAtlasViewer'; +import { SimpleAtlasViewer } from '../features/SimpleAtlasViewer'; -export const CanvasViewer: React.FC<{ canvas: CanvasFull['canvas'] }> = ({ canvas }) => { +export const CanvasViewer: React.FC<{ canvas: CanvasFull['canvas']; isModel?: boolean }> = ({ canvas, isModel }) => { const [canvasRef, setCanvasRef] = useState(); const height = useViewerHeight(); @@ -27,15 +27,15 @@ export const CanvasViewer: React.FC<{ canvas: CanvasFull['canvas'] }> = ({ canva <> {canvasRef ? ( - + ) : null} ); }; -export function StandaloneCanvasViewer(props: { canvasId: number }) { +export function StandaloneCanvasViewer(props: { canvasId: number; isModel?: boolean }) { const { data: canvas } = apiHooks.getCanvasById(() => (props.canvasId ? [props.canvasId] : undefined)); - return <>{canvas ? : null}; + return <>{canvas ? : null}; } diff --git a/services/madoc-ts/src/frontend/shared/components/SubjectSnippet.tsx b/services/madoc-ts/src/frontend/shared/components/SubjectSnippet.tsx index de887f97b..ebda2bc60 100644 --- a/services/madoc-ts/src/frontend/shared/components/SubjectSnippet.tsx +++ b/services/madoc-ts/src/frontend/shared/components/SubjectSnippet.tsx @@ -1,9 +1,9 @@ import React, { useMemo } from 'react'; import { parseUrn } from '../../../utility/parse-urn'; import { SnippetLargeProps } from '../atoms/SnippetLarge'; -import { CollectionSnippet } from './CollectionSnippet'; -import { ManifestSnippet } from './ManifestSnippet'; -import { CanvasSnippet } from './CanvasSnippet'; +import { CollectionSnippet } from '../features/CollectionSnippet'; +import { ManifestSnippet } from '../features/ManifestSnippet'; +import { CanvasSnippet } from '../features/CanvasSnippet'; export const SubjectSnippet: React.FC<{ subject: string; diff --git a/services/madoc-ts/src/frontend/shared/components/TaskWrapper.tsx b/services/madoc-ts/src/frontend/shared/components/TaskWrapper.tsx index 25370632e..c6de9b51e 100644 --- a/services/madoc-ts/src/frontend/shared/components/TaskWrapper.tsx +++ b/services/madoc-ts/src/frontend/shared/components/TaskWrapper.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SubjectSnippet } from '../../../extensions/tasks/resolvers/subject-resolver'; import { BaseTask } from '../../../gateway/tasks/base-task'; import { ExpandGrid } from '../layout/Grid'; -import { TaskHeader } from './TaskHeader'; +import { TaskHeader } from '../../site/features/tasks/TaskHeader'; export const TaskWrapper: React.FC<{ task: BaseTask; subject?: SubjectSnippet; refetch?: () => Promise }> = ({ task, diff --git a/services/madoc-ts/src/frontend/shared/components/TermsPopup.tsx b/services/madoc-ts/src/frontend/shared/components/TermsPopup.tsx new file mode 100644 index 000000000..9ac85e9e5 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/components/TermsPopup.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSite, useUser } from '../hooks/use-site'; +import { HrefLink } from '../utility/href-link'; +import { useLocation } from 'react-router-dom'; + +export function TermsPopup({ admin }: { admin?: boolean }) { + const user = useUser(); + const site = useSite(); + const { t } = useTranslation(); + const location = useLocation(); + const [closed, setClosed] = React.useState(false); + + if (!user || !site.latestTerms) { + return null; + } + + let termsMessage: any = ''; + + if (user.terms_accepted?.includes(site.latestTerms)) { + return null; + } + + const newTerms = !user.terms_accepted?.length; + + if (!user.terms_accepted?.length) { + termsMessage = t('View terms'); + } else { + termsMessage = t('View new terms'); + } + + if (typeof window === 'undefined') { + return null; + } + + if (typeof window !== 'undefined' && window.location.pathname === '/terms') { + return null; + } + + if (location.pathname === '/terms') { + return null; + } + + if (closed) { + return null; + } + + return ( +
    +
    + +
    +
    + {newTerms ? ( +

    {t('You have not yet accepted the terms of use for this site.')}

    + ) : ( +

    {t('The terms of use for this site have changed since you last accepted them.')}

    + )} + {admin ? ( + + {termsMessage} + + ) : ( + + {termsMessage} + + )} +
    +
    + ); +} diff --git a/services/madoc-ts/src/frontend/shared/components/UserBar.tsx b/services/madoc-ts/src/frontend/shared/components/UserBar.tsx index 50f1726f7..6195a0cb2 100644 --- a/services/madoc-ts/src/frontend/shared/components/UserBar.tsx +++ b/services/madoc-ts/src/frontend/shared/components/UserBar.tsx @@ -2,7 +2,7 @@ import { stringify } from 'query-string'; import React, { useEffect, useMemo } from 'react'; import useDropdownMenu from 'react-accessible-dropdown-menu-hook'; import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import styled from 'styled-components'; import { siteRoles } from '../../config'; import { useSiteConfiguration } from '../../site/features/SiteConfigurationContext'; @@ -14,10 +14,11 @@ import { } from '../navigation/GlobalHeader'; import { LanguageSwitcher } from '../navigation/LanguageSwitcher'; import { useLocationQuery } from '../hooks/use-location-query'; -import { useSite, useSystemConfig } from '../hooks/use-site'; +import { useSite, useSystemConfig, useUser } from '../hooks/use-site'; import { ArrowDownIcon } from '../icons/ArrowDownIcon'; import { HrefLink } from '../utility/href-link'; import { NotificationCenter } from './NotificationCenter'; +import { TermsPopup } from './TermsPopup'; const UserBarContainer = styled.div` height: 36px; @@ -118,11 +119,12 @@ export const ViewRole: React.FC<{ role: string; site_role?: string }> = ({ role, export const UserBar: React.FC<{ user?: { name: string; id: number; scope: string[]; role: string; site_role?: string }; admin?: boolean; -}> = ({ user, admin }) => { +}> = ({ admin }) => { const { t } = useTranslation(); const location = useLocation(); const systemConfig = useSystemConfig(); const redirect = useLoginRedirect(admin); + const user = useUser(); const showAdmin = user && user.scope.indexOf('site.admin') !== -1; const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(showAdmin ? 5 : 4); const { editMode, setEditMode } = useSiteConfiguration(); @@ -135,6 +137,8 @@ export const UserBar: React.FC<{ return ( <> + + {systemConfig.installationTitle} {admin ? ( @@ -146,6 +150,8 @@ export const UserBar: React.FC<{ )} + + {user ? : null} {showAdmin && !admin ? ( @@ -179,7 +185,7 @@ export const UserBar: React.FC<{ {t('View site')} - + {t('Account')} - + {t('Account')} = { + enableRegistrations: { + label: 'User registrations', + type: 'checkbox-field', + inlineLabel: 'Allow users to register to the site', + }, + registeredUserTranscriber: { + label: 'User role', + type: 'checkbox-field', + inlineLabel: 'New users can contribute to crowdsourcing projects', + }, + installationTitle: { + label: 'Installation title', + type: 'text-field', + }, + defaultSite: { + label: 'Slug of default site', + type: 'dropdown-field', + }, + + // User profile options + ...createUserDetailCheckboxes(), + userProfileModel: { + label: 'User profile model', + description: 'Model shorthand for user profile', + type: 'text-field', + multiline: true, + minLines: 10, + }, +}; + +function createUserDetailCheckboxes(): CaptureModelShorthand<'builtInUserProfile'> { + const keys = Object.keys(userDetailConfig); + const checkboxes: Array<{ label: string; value: string }> = []; + for (const key of keys) { + checkboxes.push({ + label: `Enable "${key}" field`, + value: key, + }); + } + return { + builtInUserProfile: { + type: 'checkbox-list-field', + label: 'Enable built-in profile fields', + description: 'Show built-in profile fields to users', + options: checkboxes, + }, + }; +} diff --git a/services/madoc-ts/src/frontend/shared/configuration/site-config.ts b/services/madoc-ts/src/frontend/shared/configuration/site-config.ts index 13171a026..8f0e74364 100644 --- a/services/madoc-ts/src/frontend/shared/configuration/site-config.ts +++ b/services/madoc-ts/src/frontend/shared/configuration/site-config.ts @@ -299,6 +299,18 @@ export const siteConfigurationModel: { label: 'Enable rotation of images', value: 'enableRotation', }, + { + label: 'Enable autosave', + value: 'enableAutoSave', + }, + { + label: 'Enable tooltip descriptions', + value: 'enableTooltipDescriptions', + }, + { + label: 'Enable split-view', + value: 'enableSplitView', + }, ], }, reviewOptions: { @@ -338,7 +350,7 @@ export const siteConfigurationModel: { value: 'hideRandomCanvas', }, { - label: 'Show reviewer dashboard', + label: 'Hide reviewer dashboard', value: 'reviewerDashboard', }, ], @@ -420,6 +432,10 @@ export const siteConfigurationModel: { label: 'Hide the page navigation links', value: 'hidePageNavLinks', }, + { + label: 'Show reviews link', + value: 'showReviews', + }, { label: 'Hide the search bar', value: 'hideSearchBar', @@ -517,6 +533,10 @@ export const NonProjectOptions: { label: 'Hide the page navigation links', value: 'hidePageNavLinks', }, + { + label: 'Show reviews link', + value: 'showReviews', + }, { label: 'Hide the search bar', value: 'hideSearchBar', @@ -567,7 +587,7 @@ export const ProjectConfigInterface: { value: 'hideRandomCanvas', }, { - label: 'hide reviewer dashboard', + label: 'Hide reviewer dashboard', value: 'reviewerDashboard', }, ], @@ -827,11 +847,23 @@ export const ProjectConfigContributions: { label: 'Disable save for later button', value: 'disableSaveForLater', }, + { + label: 'Enable autosave', + value: 'enableAutoSave', + }, + { + label: 'Enable tooltip descriptions', + value: 'enableTooltipDescriptions', + }, { label: 'Allow personal notes', description: 'allow users to take personal notes only visible to themselves on canvases in a project', value: 'allowPersonalNotes', }, + { + label: 'Enable split view', + value: 'enableSplitView', + }, ], }, contributionWarningTime: { diff --git a/services/madoc-ts/src/frontend/shared/configuration/user-detail-config.ts b/services/madoc-ts/src/frontend/shared/configuration/user-detail-config.ts new file mode 100644 index 000000000..b31280342 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/configuration/user-detail-config.ts @@ -0,0 +1,28 @@ +import { CaptureModelShorthand } from '../../../extensions/projects/types'; + +export const userDetailConfig: CaptureModelShorthand = { + gravitar: { + type: 'checkbox-field', + label: 'Profile image', + description: 'Use your Gravitar profile image', + inlineLabel: 'Use Gravitar', + inlineDescription: 'The image will be fetched from Gravitar using your email address.', + }, + bio: { + type: 'text-field', + label: 'Bio', + description: 'A short bio about yourself', + multiline: true, + minLines: 3, + }, + institution: { + type: 'text-field', + label: 'Institution', + description: 'The institution you are affiliated with', + }, + status: { + type: 'text-field', + label: 'Current status', + description: 'Appears to other users on your profile next to your name', + }, +}; diff --git a/services/madoc-ts/src/frontend/shared/components/CanvasNavigation.tsx b/services/madoc-ts/src/frontend/shared/features/CanvasNavigation.tsx similarity index 96% rename from services/madoc-ts/src/frontend/shared/components/CanvasNavigation.tsx rename to services/madoc-ts/src/frontend/shared/features/CanvasNavigation.tsx index dd3a22ade..a07da2b17 100644 --- a/services/madoc-ts/src/frontend/shared/components/CanvasNavigation.tsx +++ b/services/madoc-ts/src/frontend/shared/features/CanvasNavigation.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useManifestStructure } from '../hooks/use-manifest-structure'; import { createLink } from '../utility/create-link'; -import { SnippetStructure } from './StructureSnippet'; +import { SnippetStructure } from '../components/StructureSnippet'; export const CanvasNavigation: React.FC<{ canvasId: string | number; diff --git a/services/madoc-ts/src/frontend/shared/components/CanvasSnippet.tsx b/services/madoc-ts/src/frontend/shared/features/CanvasSnippet.tsx similarity index 95% rename from services/madoc-ts/src/frontend/shared/components/CanvasSnippet.tsx rename to services/madoc-ts/src/frontend/shared/features/CanvasSnippet.tsx index 4d6069b98..492fc518b 100644 --- a/services/madoc-ts/src/frontend/shared/components/CanvasSnippet.tsx +++ b/services/madoc-ts/src/frontend/shared/features/CanvasSnippet.tsx @@ -3,9 +3,8 @@ import { useTranslation } from 'react-i18next'; import { useRelativeLinks } from '../../site/hooks/use-relative-links'; import { SnippetLarge, SnippetLargeProps } from '../atoms/SnippetLarge'; import { useApiCanvas } from '../hooks/use-api-canvas'; -import { LocaleString } from './LocaleString'; +import { LocaleString } from '../components/LocaleString'; import { HrefLink } from '../utility/href-link'; - export const CanvasSnippet: React.FC<{ id: number; manifestId?: number; @@ -27,7 +26,6 @@ export const CanvasSnippet: React.FC<{ if (!data) { return ( {data.canvas.label}} subtitle={t(`Canvas`)} summary={{data.canvas.summary}} diff --git a/services/madoc-ts/src/frontend/shared/components/CollectionSnippet.tsx b/services/madoc-ts/src/frontend/shared/features/CollectionSnippet.tsx similarity index 88% rename from services/madoc-ts/src/frontend/shared/components/CollectionSnippet.tsx rename to services/madoc-ts/src/frontend/shared/features/CollectionSnippet.tsx index e3af6d298..262c12410 100644 --- a/services/madoc-ts/src/frontend/shared/components/CollectionSnippet.tsx +++ b/services/madoc-ts/src/frontend/shared/features/CollectionSnippet.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { SnippetLarge, SnippetLargeProps } from '../atoms/SnippetLarge'; import { useApiCollection } from '../hooks/use-api-collection'; -import { LocaleString } from './LocaleString'; +import { LocaleString } from '../components/LocaleString'; import { HrefLink } from '../utility/href-link'; import { createLink } from '../utility/create-link'; +import CollectionIcon from '../icons/CollectionIcon'; export const CollectionSnippet: React.FC<{ id: number; projectId?: string | number } & Partial> = ({ id, @@ -21,11 +22,11 @@ export const CollectionSnippet: React.FC<{ id: number; projectId?: string | numb if (!data) { return ( } buttonText={t('view collection')} link={createLink({ collectionId: id, projectId: projectId })} {...props} @@ -41,12 +42,12 @@ export const CollectionSnippet: React.FC<{ id: number; projectId?: string | numb return ( {data.collection.label}} subtitle={t('Collection with {{count}} manifests', { count: data.pagination.totalResults })} summary={{data.collection.summary}} linkAs={HrefLink} thumbnail={thumbnail} + placeholderIcon={} buttonText={t('view collection')} link={createLink({ collectionId: id, projectId: projectId })} {...props} diff --git a/services/madoc-ts/src/frontend/shared/components/ContentExplorer.tsx b/services/madoc-ts/src/frontend/shared/features/ContentExplorer.tsx similarity index 98% rename from services/madoc-ts/src/frontend/shared/components/ContentExplorer.tsx rename to services/madoc-ts/src/frontend/shared/features/ContentExplorer.tsx index 65f52b146..4bac8ddfd 100644 --- a/services/madoc-ts/src/frontend/shared/components/ContentExplorer.tsx +++ b/services/madoc-ts/src/frontend/shared/features/ContentExplorer.tsx @@ -6,8 +6,8 @@ import { ExpandGrid, GridContainer } from '../layout/Grid'; import { useTranslation } from 'react-i18next'; import { Heading3 } from '../typography/Heading3'; import { TableActions, TableContainer, TableRow, TableRowLabel } from '../layout/Table'; -import { CollectionExplorer } from './CollectionExplorer'; -import { LocaleString } from './LocaleString'; +import { CollectionExplorer } from '../../site/features/collections/CollectionExplorer'; +import { LocaleString } from '../components/LocaleString'; import { ImageGrid, ImageGridItem } from '../atoms/ImageGrid'; import { CroppedImage } from '../atoms/Images'; import { SingleLineHeading5 } from '../typography/Heading5'; diff --git a/services/madoc-ts/src/frontend/shared/components/LinkingPropertyEditor.tsx b/services/madoc-ts/src/frontend/shared/features/LinkingPropertyEditor.tsx similarity index 100% rename from services/madoc-ts/src/frontend/shared/components/LinkingPropertyEditor.tsx rename to services/madoc-ts/src/frontend/shared/features/LinkingPropertyEditor.tsx diff --git a/services/madoc-ts/src/frontend/shared/components/ManifestSnippet.tsx b/services/madoc-ts/src/frontend/shared/features/ManifestSnippet.tsx similarity index 95% rename from services/madoc-ts/src/frontend/shared/components/ManifestSnippet.tsx rename to services/madoc-ts/src/frontend/shared/features/ManifestSnippet.tsx index 5800f9026..d9c6132cb 100644 --- a/services/madoc-ts/src/frontend/shared/components/ManifestSnippet.tsx +++ b/services/madoc-ts/src/frontend/shared/features/ManifestSnippet.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { SnippetLarge, SnippetLargeProps } from '../atoms/SnippetLarge'; import { useApiManifest } from '../hooks/use-api-manifest'; -import { LocaleString } from './LocaleString'; +import { LocaleString } from '../components/LocaleString'; import { HrefLink } from '../utility/href-link'; export const ManifestSnippet: React.FC<{ @@ -18,7 +18,6 @@ export const ManifestSnippet: React.FC<{ if (!data) { return ( {data.manifest.label}} subtitle={t('Manifest with {{count}} images', { count: data.pagination.totalResults })} summary={{data.manifest.summary}} diff --git a/services/madoc-ts/src/frontend/shared/components/MetadataFacetEditor.tsx b/services/madoc-ts/src/frontend/shared/features/MetadataFacetEditor.tsx similarity index 99% rename from services/madoc-ts/src/frontend/shared/components/MetadataFacetEditor.tsx rename to services/madoc-ts/src/frontend/shared/features/MetadataFacetEditor.tsx index ed8df6799..4c7dccbb6 100644 --- a/services/madoc-ts/src/frontend/shared/components/MetadataFacetEditor.tsx +++ b/services/madoc-ts/src/frontend/shared/features/MetadataFacetEditor.tsx @@ -38,7 +38,7 @@ import { apiHooks } from '../hooks/use-api-query'; import { useDrag, useDrop } from 'react-dnd'; import { useDefaultLocale, useSupportedLocales } from '../hooks/use-site'; import { Spinner } from '../icons/Spinner'; -import { LocaleString } from './LocaleString'; +import { LocaleString } from '../components/LocaleString'; const MetadataSingleValue: React.FC<{ parentLabel: string; value: string; total_items: number; language?: string }> = ({ parentLabel, diff --git a/services/madoc-ts/src/frontend/site/features/OpenSeadragonViewer.lazy.ts b/services/madoc-ts/src/frontend/shared/features/OpenSeadragonViewer.lazy.ts similarity index 74% rename from services/madoc-ts/src/frontend/site/features/OpenSeadragonViewer.lazy.ts rename to services/madoc-ts/src/frontend/shared/features/OpenSeadragonViewer.lazy.ts index 2c6284fe9..d381b0605 100644 --- a/services/madoc-ts/src/frontend/site/features/OpenSeadragonViewer.lazy.ts +++ b/services/madoc-ts/src/frontend/shared/features/OpenSeadragonViewer.lazy.ts @@ -1,4 +1,4 @@ -import { madocLazy } from '../../shared/utility/madoc-lazy'; +import { madocLazy } from '../utility/madoc-lazy'; export const OpenSeadragonViewer = madocLazy(async () => { const imported = await import('./OpenSeadragonViewer'); diff --git a/services/madoc-ts/src/frontend/site/features/OpenSeadragonViewer.tsx b/services/madoc-ts/src/frontend/shared/features/OpenSeadragonViewer.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/OpenSeadragonViewer.tsx rename to services/madoc-ts/src/frontend/shared/features/OpenSeadragonViewer.tsx diff --git a/services/madoc-ts/src/frontend/shared/features/SimpleAtlasViewer.tsx b/services/madoc-ts/src/frontend/shared/features/SimpleAtlasViewer.tsx new file mode 100644 index 000000000..ac4b19682 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/features/SimpleAtlasViewer.tsx @@ -0,0 +1,197 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { AnnotationPage } from '@iiif/presentation-3'; +import { RegionHighlight, Runtime } from '@atlas-viewer/atlas'; +import { ErrorBoundary } from 'react-error-boundary'; +import { useCanvas, useImageService, CanvasPanel, CanvasContext } from 'react-iiif-vault'; +import { useTranslation } from 'react-i18next'; +import { CanvasViewerButton, CanvasViewerControls } from '../atoms/CanvasViewerGrid'; +import { useSiteConfiguration } from '../../site/features/SiteConfigurationContext'; +import { ViewReadOnlyAnnotation } from '../atlas/ViewReadOnlyAnnotation'; +import { GhostCanvas } from '../atoms/GhostCanvas'; +import { useSelectorController } from '../capture-models/editor/stores/selectors/selector-helper'; +import { useBrowserLayoutEffect } from '../hooks/use-browser-layout-effect'; +import { HomeIcon } from '../icons/HomeIcon'; +import { MinusIcon } from '../icons/MinusIcon'; +import { PlusIcon } from '../icons/PlusIcon'; +import { useReadOnlyAnnotations } from '../hooks/use-read-only-annotations'; +import { InfoMessage } from '../callouts/InfoMessage'; +import { Button } from '../navigation/Button'; +import { BrowserComponent } from '../utility/browser-component'; +import { OpenSeadragonViewer } from './OpenSeadragonViewer.lazy'; +import { RotateIcon } from '../icons/RotateIcon'; +import { useModelPageConfiguration } from '../../site/hooks/use-model-page-configuration'; + +export const SimpleAtlasViewer = React.forwardRef< + any, + { + style?: React.CSSProperties; + highlightedRegions?: Array<[number, number, number, number]>; + annotationPages?: AnnotationPage[]; + unstable_webglRenderer?: boolean; + isModel?: boolean; + } +>(function SimpleAtlasViewer({ style = { height: 600 }, highlightedRegions, unstable_webglRenderer, isModel }, ref) { + const { t } = useTranslation(); + const canvas = useCanvas(); + const runtime = useRef(); + const osd = useRef(); + const { data: service } = useImageService(); + const { + project: { atlasBackground }, + } = useSiteConfiguration(); + const [isLoaded, setIsLoaded] = useState(false); + const controller = useSelectorController(); + const readOnlyAnnotations = useReadOnlyAnnotations(false); + const [isOSD, setIsOSD] = useState(false); + + const { enableRotation = false, hideViewerControls = false } = useModelPageConfiguration(); + + const goHome = () => { + if (runtime.current) { + runtime.current.world.goHome(); + } + if (osd.current) { + osd.current.goHome(); + } + }; + + const zoomIn = () => { + if (runtime.current) { + runtime.current.world.zoomIn(); + } + if (osd.current) { + osd.current.zoomIn(); + } + }; + + const zoomOut = () => { + if (runtime.current) { + runtime.current.world.zoomOut(); + } + if (osd.current) { + osd.current.zoomOut(); + } + }; + + const rotate = () => { + setIsOSD(true); + if (osd.current) { + osd.current.rotate(); + } + }; + + useBrowserLayoutEffect(() => { + setIsLoaded(true); + }, []); + + useEffect(() => { + return controller.on('zoomTo', e => { + const found = readOnlyAnnotations.find(r => { + return r.id === e.selectorId; + }); + if (found) { + runtime.current?.world.gotoRegion(found.target); + } + }); + }, [readOnlyAnnotations, controller]); + + if (!canvas) { + return null; + } + + return ( +
    + }> + + {isLoaded ? ( + <> + {isOSD ? ( + <> + + {t('You cannot edit annotations if you are rotating')} + + + + + + + ) : ( + void (runtime.current = preset.runtime)}> + + + + {highlightedRegions + ? highlightedRegions.map(([x, y, width, height], key) => { + return ( + + { + // no-op + }} + onClick={() => { + // no-op + }} + /> + + ); + }) + : null} + {readOnlyAnnotations.map(anno => ( + + ))} + + + + )} + + {hideViewerControls && isModel ? null : ( + + {enableRotation && isModel ? ( + + + + ) : null} + + + + + + + + + + + )} + + ) : null} + +
    + ); +}); diff --git a/services/madoc-ts/src/frontend/shared/hooks/use-annotation-page.ts b/services/madoc-ts/src/frontend/shared/hooks/use-annotation-page.ts index adf18f4be..d6f3ed387 100644 --- a/services/madoc-ts/src/frontend/shared/hooks/use-annotation-page.ts +++ b/services/madoc-ts/src/frontend/shared/hooks/use-annotation-page.ts @@ -1,7 +1,6 @@ import { useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { annotationPageToRegions } from '../utility/annotation-page-to-regions'; -import parseSelectorTarget from '../utility/parse-selector-target'; export function useAnnotationPage(pageId?: string) { const [highlightedAnnotation, setHighlightedAnnotation] = useState(undefined); diff --git a/services/madoc-ts/src/frontend/shared/hooks/use-api-query.ts b/services/madoc-ts/src/frontend/shared/hooks/use-api-query.ts index dae8c4991..afc8c9532 100644 --- a/services/madoc-ts/src/frontend/shared/hooks/use-api-query.ts +++ b/services/madoc-ts/src/frontend/shared/hooks/use-api-query.ts @@ -66,6 +66,7 @@ export type GetApiMethods = keyof Pick< | 'getSiteManifests' | 'getSitePage' | 'getSiteProject' + | 'getSiteProjectRecent' | 'getSiteProjects' | 'getSiteProjectCanvasModel' | 'getSiteProjectCanvasTasks' @@ -89,6 +90,10 @@ export type GetApiMethods = keyof Pick< | 'getSiteProjectManifestModel' | 'getLocale' | 'getAutomatedUsers' + | 'getAllPersonalNotes' + | 'getAllProjectFeedback' + | 'getAllSiteProjectMembers' + | 'getProjectMemberEmails' >; const keys = Object.getOwnPropertyNames(ApiClient.prototype); diff --git a/services/madoc-ts/src/frontend/shared/hooks/use-continue-contribution.ts b/services/madoc-ts/src/frontend/shared/hooks/use-continue-contribution.ts new file mode 100644 index 000000000..1c72c7fc6 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/hooks/use-continue-contribution.ts @@ -0,0 +1,27 @@ +import { SubjectSnippet as SubjectSnippetType } from '../../../extensions/tasks/resolvers/subject-resolver'; +import { BaseTask } from '../../../gateway/tasks/base-task'; +import { useRelativeLinks } from '../../site/hooks/use-relative-links'; +import { useTaskMetadata } from '../../site/hooks/use-task-metadata'; + +export function useContinueContribution(task?: BaseTask, manifestModel?: boolean) { + const { subject } = useTaskMetadata<{ subject?: SubjectSnippetType }>(task); + const createLink = useRelativeLinks(); + + const link = subject + ? subject.type === 'manifest' + ? createLink({ manifestId: subject.id, subRoute: manifestModel ? 'model' : '' }) + : subject.type === 'canvas' + ? createLink({ + canvasId: subject.id, + manifestId: subject.parent?.id, + subRoute: 'model', + }) + : undefined + : undefined; + + if (!subject || !link) { + return null; + } + + return link; +} diff --git a/services/madoc-ts/src/frontend/shared/hooks/use-manifest-pagination.ts b/services/madoc-ts/src/frontend/shared/hooks/use-manifest-pagination.ts new file mode 100644 index 000000000..b49e5840b --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/hooks/use-manifest-pagination.ts @@ -0,0 +1,48 @@ +import { useRouteContext } from '../../site/hooks/use-route-context'; +import { useCanvasSearch } from './use-canvas-search'; +import { useManifestStructure } from './use-manifest-structure'; +import { createLink } from '../utility/create-link'; + +export function useManifestPagination(subRoute?: string) { + const { manifestId, collectionId, canvasId, projectId } = useRouteContext(); + const [searchText] = useCanvasSearch(canvasId); + const structure = useManifestStructure(manifestId); + const query = searchText ? { searchText } : undefined; + + const idx = structure.data && canvasId ? structure.data.ids.indexOf(canvasId) : -1; + + if (!structure.data || idx === -1 || !manifestId) { + return null; + } + + const hasPrevPage = idx > 0 && structure.data.items[idx - 1]; + const hasNextPage = idx < structure.data.items.length && structure.data.items[idx + 1]; + + return { + hasPrevPage: !!hasPrevPage, + prevItem: hasPrevPage, + prevPage: hasPrevPage + ? createLink({ + projectId, + collectionId, + manifestId, + canvasId: structure.data.items[idx - 1].id, + subRoute, + query, + }) + : undefined, + + hasNextPage: !!hasNextPage, + nextItem: hasNextPage, + nextPage: hasNextPage + ? createLink({ + projectId, + collectionId, + manifestId, + canvasId: structure.data.items[idx + 1].id, + subRoute, + query, + }) + : undefined, + }; +} diff --git a/services/madoc-ts/src/frontend/shared/hooks/use-recent-user-tasks.ts b/services/madoc-ts/src/frontend/shared/hooks/use-recent-user-tasks.ts new file mode 100644 index 000000000..6b5f623ea --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/hooks/use-recent-user-tasks.ts @@ -0,0 +1,48 @@ +import { parseUrn } from '../../../utility/parse-urn'; +import { useSiteConfiguration } from '../../site/features/SiteConfigurationContext'; +import { useProject } from '../../site/hooks/use-project'; +import { firstNTasksWithUniqueSubjects } from '../utility/first-n-tasks-with-unique-subjects'; +import { useContributorTasks } from './use-contributor-tasks'; + +export function useRecentUserTasks(requested = 3, fallbackOnly = false) { + const { data: project } = useProject(); + const { + project: { contributionMode, claimGranularity }, + } = useSiteConfiguration(); + const contributorTasks = useContributorTasks({ rootTaskId: project?.task_id }, !!project); + + const currentTasks = contributorTasks?.drafts.tasks; + const tasksInReview = contributorTasks?.reviews.tasks; + + const firstThree = currentTasks && currentTasks.length ? firstNTasksWithUniqueSubjects(currentTasks, requested) : []; + + const tasksToReturn = firstThree.map(task => { + return { + task, + next: false, + }; + }); + + if (tasksToReturn.length && fallbackOnly) { + return tasksToReturn; + } + + if ( + contributionMode !== 'transcription' && + claimGranularity !== 'manifest' && + tasksInReview && + tasksInReview.length && + firstThree.length < requested + ) { + const firstThreeReviews = firstNTasksWithUniqueSubjects(tasksInReview, requested - firstThree.length); + + for (const task of firstThreeReviews) { + const parsed = parseUrn(task.subject); + if (parsed && parsed.type === 'canvas') { + tasksToReturn.push({ task, next: true }); + } + } + } + + return tasksToReturn; +} diff --git a/services/madoc-ts/src/frontend/shared/icons/ArrowForwardIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/ArrowForwardIcon.tsx index 9ac943fb5..98a98341a 100644 --- a/services/madoc-ts/src/frontend/shared/icons/ArrowForwardIcon.tsx +++ b/services/madoc-ts/src/frontend/shared/icons/ArrowForwardIcon.tsx @@ -4,7 +4,7 @@ export function ArrowForwardIcon(props: React.SVGProps) { return ( - + ); } diff --git a/services/madoc-ts/src/frontend/shared/icons/BoxIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/BoxIcon.tsx new file mode 100644 index 000000000..750756497 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/icons/BoxIcon.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export function BoxIcon(props: React.SVGProps) { + return ( + + + + ); +} diff --git a/services/madoc-ts/src/frontend/shared/icons/BuildingIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/BuildingIcon.tsx new file mode 100644 index 000000000..82c4cdca5 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/icons/BuildingIcon.tsx @@ -0,0 +1,12 @@ +import React, { SVGProps } from 'react'; + +export function BuildingIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/services/madoc-ts/src/frontend/shared/icons/CallMergeIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/CallMergeIcon.tsx index 1ba502546..121ae5971 100644 --- a/services/madoc-ts/src/frontend/shared/icons/CallMergeIcon.tsx +++ b/services/madoc-ts/src/frontend/shared/icons/CallMergeIcon.tsx @@ -4,7 +4,10 @@ export function CallMergeIcon(props: React.SVGProps) { return ( - + ); } diff --git a/services/madoc-ts/src/frontend/shared/icons/CollectionIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/CollectionIcon.tsx new file mode 100644 index 000000000..48f03e819 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/icons/CollectionIcon.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; +const CollectionIcon = (props: SVGProps) => ( + + + +); +export default CollectionIcon; diff --git a/services/madoc-ts/src/frontend/shared/icons/HourglassIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/HourglassIcon.tsx new file mode 100644 index 000000000..e72474a74 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/icons/HourglassIcon.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; +const HourglassIcon = (props: SVGProps) => ( + + + +); +export default HourglassIcon; diff --git a/services/madoc-ts/src/frontend/shared/icons/InProgressIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/InProgressIcon.tsx new file mode 100644 index 000000000..6ed0f68de --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/icons/InProgressIcon.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; +const InProgressIcon = (props: SVGProps) => ( + + + +); +export default InProgressIcon; diff --git a/services/madoc-ts/src/frontend/shared/icons/LockIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/LockIcon.tsx index d1cffe412..21097281e 100644 --- a/services/madoc-ts/src/frontend/shared/icons/LockIcon.tsx +++ b/services/madoc-ts/src/frontend/shared/icons/LockIcon.tsx @@ -4,7 +4,10 @@ export function LockIcon(props: React.SVGProps) { return ( - + ); } diff --git a/services/madoc-ts/src/frontend/shared/icons/MailIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/MailIcon.tsx new file mode 100644 index 000000000..660e196e1 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/icons/MailIcon.tsx @@ -0,0 +1,12 @@ +import React, { SVGProps } from 'react'; + +export function MailIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/services/madoc-ts/src/frontend/shared/icons/ModelDocumentIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/ModelDocumentIcon.tsx index ce38dadf8..e2cdfc554 100644 --- a/services/madoc-ts/src/frontend/shared/icons/ModelDocumentIcon.tsx +++ b/services/madoc-ts/src/frontend/shared/icons/ModelDocumentIcon.tsx @@ -4,7 +4,10 @@ export function ModelDocumentIcon(props: React.SVGProps) { return ( - + ); } diff --git a/services/madoc-ts/src/frontend/shared/icons/PendingIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/PendingIcon.tsx new file mode 100644 index 000000000..cc2f761e1 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/icons/PendingIcon.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; +const PendingIcon = (props: SVGProps) => ( + + + +); +export default PendingIcon; diff --git a/services/madoc-ts/src/frontend/shared/icons/SplitIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/SplitIcon.tsx new file mode 100644 index 000000000..b1ecf2042 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/icons/SplitIcon.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export function SplitIcon(props: React.SVGProps) { + return ( + + + + ); +} diff --git a/services/madoc-ts/src/frontend/shared/icons/TickIcon.tsx b/services/madoc-ts/src/frontend/shared/icons/TickIcon.tsx index 52dad947e..d421207c1 100644 --- a/services/madoc-ts/src/frontend/shared/icons/TickIcon.tsx +++ b/services/madoc-ts/src/frontend/shared/icons/TickIcon.tsx @@ -32,6 +32,7 @@ export const WhiteTickIcon = (props: any) => ( diff --git a/services/madoc-ts/src/frontend/shared/layout/Grid.tsx b/services/madoc-ts/src/frontend/shared/layout/Grid.tsx index ca6ebcf5a..9dee82c74 100644 --- a/services/madoc-ts/src/frontend/shared/layout/Grid.tsx +++ b/services/madoc-ts/src/frontend/shared/layout/Grid.tsx @@ -6,13 +6,42 @@ export const GridContainer = styled.div<{ $justify?: string }>` align-items: flex-start; `; +export const GridButton = styled.button` + display: flex; + width: auto; + height: 100%; + padding: 8px 10px; + flex-direction: column; + justify-content: center; + border: 1px dashed #999; + background-color: #f7f7f7; + color: #3773db; + text-decoration: none; + + &:hover { + border: 1px solid #0f306c; + color: #0f306c; + } +`; + +export const CSSHalfGrid = styled.div<{ $justify?: string }>` + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 1em; + justify-content: ${props => props.$justify}; + align-items: flex-start; + overflow-y: auto; + +`; + export const CSSThirdGrid = styled.div<{ $justify?: string }>` display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: auto auto auto 1fr; grid-gap: 1em; justify-content: ${props => props.$justify}; align-items: flex-start; overflow-y: auto; + `; export const HalfGird = styled.div<{ $margin?: boolean }>` diff --git a/services/madoc-ts/src/frontend/shared/layout/LayoutContainer.tsx b/services/madoc-ts/src/frontend/shared/layout/LayoutContainer.tsx index efc709907..5d0c1a5e3 100644 --- a/services/madoc-ts/src/frontend/shared/layout/LayoutContainer.tsx +++ b/services/madoc-ts/src/frontend/shared/layout/LayoutContainer.tsx @@ -18,6 +18,11 @@ export const OuterLayoutContainer = styled.div` export const NavIconContainer = styled.div<{ $active?: boolean; $disabled?: boolean }>` &:hover { background: #eee; + + svg { + fill: #4a64e1; + color: #4a64e1; + } } border-radius: 3px; @@ -32,6 +37,7 @@ export const NavIconContainer = styled.div<{ $active?: boolean; $disabled?: bool svg { fill: #666; + color: #666; width: 1.4em; height: 1.4em; } @@ -52,10 +58,15 @@ export const NavIconContainer = styled.div<{ $active?: boolean; $disabled?: bool svg { fill: #fff; + color: #fff; } &:hover { background: #4a64e1; + svg { + fill: #fff; + color: #fff; + } } `} @@ -67,10 +78,15 @@ export const NavIconContainer = styled.div<{ $active?: boolean; $disabled?: bool svg { fill: #ccc; + color: #ccc; } &:hover { background: transparent; + svg { + fill: #ccc; + color: #ccc; + } } `} `; @@ -151,6 +167,8 @@ export const LayoutSidebar = styled.div<{ $noScroll?: boolean }>` border-right: 1px solid #918f8f; overflow: auto; position: relative; + display: flex; + flex-direction: column; &[data-space='true'] { margin-right: 1em; diff --git a/services/madoc-ts/src/frontend/shared/layout/MaximiseWindow.tsx b/services/madoc-ts/src/frontend/shared/layout/MaximiseWindow.tsx index 488dfecca..1690b810b 100644 --- a/services/madoc-ts/src/frontend/shared/layout/MaximiseWindow.tsx +++ b/services/madoc-ts/src/frontend/shared/layout/MaximiseWindow.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import styled, { css } from 'styled-components'; export const MaximiseWindowContainer = styled.div<{ $open: boolean }>` diff --git a/services/madoc-ts/src/frontend/shared/layout/Modal.tsx b/services/madoc-ts/src/frontend/shared/layout/Modal.tsx index 8b4587954..8bb7d0e3e 100644 --- a/services/madoc-ts/src/frontend/shared/layout/Modal.tsx +++ b/services/madoc-ts/src/frontend/shared/layout/Modal.tsx @@ -8,7 +8,7 @@ export const ModalBackground = styled.div` left: 0; right: 0; bottom: 0; - z-index: 20; + z-index: 23; background: rgba(0, 0, 0, 0.4); `; @@ -18,7 +18,7 @@ export const ModalContainer = styled.div` left: 0; right: 0; bottom: 0; - z-index: 21; + z-index: 24; display: flex; `; diff --git a/services/madoc-ts/src/frontend/shared/layout/SiteHeader.tsx b/services/madoc-ts/src/frontend/shared/layout/SiteHeader.tsx index beb4f4f7d..6429f164b 100644 --- a/services/madoc-ts/src/frontend/shared/layout/SiteHeader.tsx +++ b/services/madoc-ts/src/frontend/shared/layout/SiteHeader.tsx @@ -39,10 +39,15 @@ export const SiteDetails = styled.div` export const SiteTitle = styled.a` text-decoration: none; - letter-spacing: -2px; + letter-spacing: -1px; color: ${headerText}; font-size: 1em; margin-right: 2em; + + h1 { + margin: 0.65em 0; + font-weight: 600; + } `; export const GlobalSearchContainer = styled.div` @@ -76,13 +81,19 @@ export const GlobalSearchInput = styled.input` export const GlobalSearchButton = styled.button` font-size: 0.9em; padding: 0.2em 1em; - background: #333; + background-color: #333; color: #fff; border: 2px solid #333; &:focus { outline: none; border-color: ${searchBorderFocusColor}; } + + /* Tailwind issue. */ + &[type='submit'], + &[type='button'] { + background-color: #333; + } `; export const SiteMenuContainer = styled.div` diff --git a/services/madoc-ts/src/frontend/shared/layout/SlotLayout.tsx b/services/madoc-ts/src/frontend/shared/layout/SlotLayout.tsx index bbdb622fb..239e2901d 100644 --- a/services/madoc-ts/src/frontend/shared/layout/SlotLayout.tsx +++ b/services/madoc-ts/src/frontend/shared/layout/SlotLayout.tsx @@ -91,8 +91,10 @@ export const SlotLayout = React.forwardRef(function SlotLayout( ); } return ( - - {children} + + + {children} + ); }); diff --git a/services/madoc-ts/src/frontend/shared/layout/Surface.tsx b/services/madoc-ts/src/frontend/shared/layout/Surface.tsx index 978cb4129..5d2ee896d 100644 --- a/services/madoc-ts/src/frontend/shared/layout/Surface.tsx +++ b/services/madoc-ts/src/frontend/shared/layout/Surface.tsx @@ -1,3 +1,4 @@ +import { InternationalString } from '@iiif/presentation-3'; import * as React from 'react'; import styled from 'styled-components'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; @@ -65,6 +66,7 @@ const SurfaceStyled = styled.div<{ `; export type SurfaceProps = { + label?: string; id?: string; background?: string; textColor?: string; @@ -118,6 +120,7 @@ blockEditorFor(Surface, { type: 'surface', internal: true, defaultProps: { + label: null, textColor: '', background: '', font: '', @@ -129,6 +132,7 @@ blockEditorFor(Surface, { marginBottom: 'none', }, editor: { + label: { label: 'Label', type: 'text-field' }, textColor: { label: 'Text color', type: 'color-field' }, background: { label: 'Background color', type: 'color-field' }, font: { label: 'Font (from google)', type: 'text-field' }, diff --git a/services/madoc-ts/src/frontend/shared/navigation/Breadcrumbs.tsx b/services/madoc-ts/src/frontend/shared/navigation/Breadcrumbs.tsx index cbdc22e1f..68104eadd 100644 --- a/services/madoc-ts/src/frontend/shared/navigation/Breadcrumbs.tsx +++ b/services/madoc-ts/src/frontend/shared/navigation/Breadcrumbs.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import styled, { css } from 'styled-components'; import React, { useMemo } from 'react'; import { Link } from 'react-router-dom'; -import { BreadcrumbDivider, BreadcrumbList, BreadcrumbItem as SiteBreadcrumbItem } from '../components/Breadcrumbs'; +import { BreadcrumbDivider, BreadcrumbList, BreadcrumbItem as SiteBreadcrumbItem } from '../../site/blocks/Breadcrumbs'; import { useSite } from '../hooks/use-site'; export type BreadcrumbItem = { diff --git a/services/madoc-ts/src/frontend/shared/navigation/Button.tsx b/services/madoc-ts/src/frontend/shared/navigation/Button.tsx index ae7f176bd..12be27b0f 100644 --- a/services/madoc-ts/src/frontend/shared/navigation/Button.tsx +++ b/services/madoc-ts/src/frontend/shared/navigation/Button.tsx @@ -44,6 +44,10 @@ export const Button = styled.button<{ background: linear-gradient(180deg, #fafbfc 0%, #eff3f6 90%); border: 1px solid rgba(27, 31, 35, 0.15); color: #333; + + &[type="submit"] { + background: linear-gradient(180deg, #fafbfc 0%, #eff3f6 90%); + } ${props => @@ -97,6 +101,10 @@ export const Button = styled.button<{ background: #4265e9; color: #fff; border: 1px solid #4265e9; + + &[type='submit'] { + background: #4265e9; + } &:active { box-shadow: inset 0 2px 8px 0 rgba(39, 75, 155, 0.8); } @@ -107,6 +115,7 @@ export const Button = styled.button<{ &:hover { background: #5371e9; border-color: #5371e9; + color: #fff; } &:focus, &:focus:hover { diff --git a/services/madoc-ts/src/frontend/shared/navigation/ContextualMenu.tsx b/services/madoc-ts/src/frontend/shared/navigation/ContextualMenu.tsx index 67bf89ca6..f5a409c8f 100644 --- a/services/madoc-ts/src/frontend/shared/navigation/ContextualMenu.tsx +++ b/services/madoc-ts/src/frontend/shared/navigation/ContextualMenu.tsx @@ -2,6 +2,10 @@ import styled, { css } from 'styled-components'; export const ContextualPositionWrapper = styled.div` position: relative; + + svg { + display: inline-block; + } `; export const ContextualLabel = styled.button` @@ -9,6 +13,10 @@ export const ContextualLabel = styled.button` border-radius: 3px; padding: 0.3em 0.8em; border: 2px solid #eee; + font-size: 0.875em; + display: flex; + align-items: center; + gap: 0.25em; &:hover { background: #ccc; diff --git a/services/madoc-ts/src/frontend/shared/navigation/GlobalHeader.tsx b/services/madoc-ts/src/frontend/shared/navigation/GlobalHeader.tsx index 9625007ce..685015792 100644 --- a/services/madoc-ts/src/frontend/shared/navigation/GlobalHeader.tsx +++ b/services/madoc-ts/src/frontend/shared/navigation/GlobalHeader.tsx @@ -38,6 +38,8 @@ export const GlobalHeaderMenuLabel = styled.button` font-size: 1em; border-top: 2px solid transparent; border-bottom: 2px solid transparent; + display: flex; + padding: 0.4em 0.75em; &:focus { color: rgba(255, 255, 255, 1); border-bottom: 2px solid dodgerblue; diff --git a/services/madoc-ts/src/frontend/shared/navigation/LightNavigation.tsx b/services/madoc-ts/src/frontend/shared/navigation/LightNavigation.tsx index 8d66922e2..06ab009fb 100644 --- a/services/madoc-ts/src/frontend/shared/navigation/LightNavigation.tsx +++ b/services/madoc-ts/src/frontend/shared/navigation/LightNavigation.tsx @@ -36,7 +36,7 @@ export const LightNavigationItem = styled.li<{ $active?: boolean }>` color: ${text}; padding: 0.3em 0.6em; display: block; - border-bottom-width: 3px; + border-bottom-width: 0px; &:hover { //text-decoration: underline; } diff --git a/services/madoc-ts/src/frontend/shared/page-blocks/AddBlock.tsx b/services/madoc-ts/src/frontend/shared/page-blocks/AddBlock.tsx index 4d116086f..30b18d90a 100644 --- a/services/madoc-ts/src/frontend/shared/page-blocks/AddBlock.tsx +++ b/services/madoc-ts/src/frontend/shared/page-blocks/AddBlock.tsx @@ -55,7 +55,7 @@ export const DefaultBlockIcon: React.FC> = props = return ( diff --git a/services/madoc-ts/src/frontend/shared/page-blocks/block-creator.tsx b/services/madoc-ts/src/frontend/shared/page-blocks/block-creator.tsx index 5ae7f7b78..12f3e2e2d 100644 --- a/services/madoc-ts/src/frontend/shared/page-blocks/block-creator.tsx +++ b/services/madoc-ts/src/frontend/shared/page-blocks/block-creator.tsx @@ -19,6 +19,7 @@ import { useApi } from '../hooks/use-api'; import { useSite } from '../hooks/use-site'; import { useBlockEditor } from './block-editor'; import { RenderBlock } from './render-block'; +import { useAvailableBlocks } from './use-available-blocks'; const BlockCreatorForm: React.FC<{ block: SiteBlock | SiteBlockRequest; @@ -26,7 +27,10 @@ const BlockCreatorForm: React.FC<{ onSave: (block: SiteBlock) => void | Promise; }> = props => { const latestPreview = useRef(); - const { saveChanges, editor, preview, canSubmit } = useBlockEditor(props.block, newBlock => (latestPreview.current = newBlock)); + const { saveChanges, editor, preview, canSubmit } = useBlockEditor( + props.block, + newBlock => (latestPreview.current = newBlock) + ); const [isSaving, setIsSaving] = useState(false); return ( @@ -71,66 +75,14 @@ export const BlockCreator: React.FC<{ const site = useSite(); const [chosenBlockType, setChosenBlockType] = useState(); - const { id: sourceId, type: sourceType } = props.source || {}; - - // Step 1: Choose a block type (possibly filter based on current context) - // - List all block types - const blockTypes = useMemo(() => { - return api.pageBlocks.getDefinitions(site.id, props.context); - }, [api.pageBlocks, props.context, site.id]); - - const availableBlocks = useMemo(() => { - return blockTypes.filter(block => { - if (sourceId) { - // We only want matching sources here. - return block?.source?.id === sourceId && block?.source?.type === sourceType; - } - - if (block?.source?.type === 'custom-page') { - return block?.source.id === props.pagePath; - } - - return !block.internal; - }); - }, [blockTypes, props.pagePath, sourceId, sourceType]); - - const contextBlocks = useMemo(() => { - const context = JSON.parse(JSON.stringify(props.context)); - return blockTypes.filter(block => { - if (block?.anyContext?.some(b => Object.keys(context).indexOf(b) >= 0)) { - return block; - } - return false; - }); - }, [blockTypes, props.context]); - - const [filteredBlocks, setFilteredBlocks] = useState(availableBlocks); - - function handleFilter(e: string) { - const result = availableBlocks - ? availableBlocks.filter(block => block.label.toLowerCase().includes(e.toLowerCase())) - : ''; - - setFilteredBlocks(result ? result : []); - } - - const pagePathBlocks = useMemo(() => { - return blockTypes.filter(block => { - if (block?.source?.type === 'custom-page' && props.pagePath) { - return block?.source.id === props.pagePath; - } - return false; - }); - }, [blockTypes, props.pagePath]); - - const pluginBlocks = useMemo(() => { - return blockTypes.filter(block => { - if (block?.source?.type === 'plugin') { - return block?.source.id; - } - return false; - }); - }, [blockTypes, props.pagePath]); + const { + filteredBlocks, + contextBlocks, + pluginBlocks, + searchBlocks, + availableBlocks, + pagePathBlocks, + } = useAvailableBlocks(props); const chosenBlock = useMemo(() => { if (chosenBlockType) { @@ -168,7 +120,7 @@ export const BlockCreator: React.FC<{ Added ) : null} - {Icon ? : } + {Icon ? : } {block.label} {block.source ? ( {block.source.name} @@ -208,7 +160,7 @@ export const BlockCreator: React.FC<{ {availableBlocks && availableBlocks.length ? ( <>

    All blocks

    - handleFilter(e.target.value)} placeholder="Search all blocks" /> + searchBlocks(e.target.value)} placeholder="Search all blocks" /> {filteredBlocks.map(renderBlock)} ) : null} diff --git a/services/madoc-ts/src/frontend/shared/page-blocks/slot.tsx b/services/madoc-ts/src/frontend/shared/page-blocks/slot.tsx index 578b672ca..3960fb11e 100644 --- a/services/madoc-ts/src/frontend/shared/page-blocks/slot.tsx +++ b/services/madoc-ts/src/frontend/shared/page-blocks/slot.tsx @@ -6,15 +6,18 @@ import { RenderBlankSlot } from './render-blank-slot'; import { RenderSlot } from './render-slot'; import { useSlots } from './slot-context'; -export const Slot: React.FC<{ +export interface SlotProps { id?: string; name: string; + label?: string; hidden?: boolean; layout?: string; noSurface?: boolean; small?: boolean; source?: { type: string; id: string }; -}> = props => { +} + +export const Slot: React.FC = props => { const { t } = useTranslation(); const { slots, context, editable, onUpdateSlot, onUpdateBlock, invalidateSlots, pagePath } = useSlots(); diff --git a/services/madoc-ts/src/frontend/shared/page-blocks/use-available-blocks.ts b/services/madoc-ts/src/frontend/shared/page-blocks/use-available-blocks.ts new file mode 100644 index 000000000..dcfb9a718 --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/page-blocks/use-available-blocks.ts @@ -0,0 +1,80 @@ +import { useMemo, useState } from 'react'; +import { EditorialContext } from '../../../types/schemas/site-page'; +import { useApi } from '../hooks/use-api'; +import { useSite } from '../hooks/use-site'; + +export function useAvailableBlocks(props: { + context?: EditorialContext; + pagePath?: string; + source?: { type: string; id: string }; +}) { + const api = useApi(); + const site = useSite(); + const { id: sourceId, type: sourceType } = props.source || {}; + + const blockTypes = useMemo(() => { + return api.pageBlocks.getDefinitions(site.id, props.context); + }, [api.pageBlocks, props.context, site.id]); + + const availableBlocks = useMemo(() => { + return blockTypes.filter(block => { + if (sourceId) { + // We only want matching sources here. + return block?.source?.id === sourceId && block?.source?.type === sourceType; + } + + if (block?.source?.type === 'custom-page') { + return block?.source.id === props.pagePath; + } + + return !block.internal; + }); + }, [blockTypes, props.pagePath, sourceId, sourceType]); + + const contextBlocks = useMemo(() => { + const context = JSON.parse(JSON.stringify(props.context)); + return blockTypes.filter(block => { + if (block?.anyContext?.some(b => Object.keys(context).indexOf(b) >= 0)) { + return block; + } + return false; + }); + }, [blockTypes, props.context]); + + const [filteredBlocks, setFilteredBlocks] = useState(availableBlocks); + + function searchBlocks(e: string) { + const result = availableBlocks + ? availableBlocks.filter(block => block.label.toLowerCase().includes(e.toLowerCase())) + : ''; + + setFilteredBlocks(result ? result : []); + } + + const pagePathBlocks = useMemo(() => { + return blockTypes.filter(block => { + if (block?.source?.type === 'custom-page' && props.pagePath) { + return block?.source.id === props.pagePath; + } + return false; + }); + }, [blockTypes, props.pagePath]); + + const pluginBlocks = useMemo(() => { + return blockTypes.filter(block => { + if (block?.source?.type === 'plugin') { + return block?.source.id; + } + return false; + }); + }, [blockTypes, props.pagePath]); + + return { + availableBlocks, + contextBlocks, + filteredBlocks, + searchBlocks, + pagePathBlocks, + pluginBlocks, + }; +} diff --git a/services/madoc-ts/src/frontend/shared/plugins/public-api.ts b/services/madoc-ts/src/frontend/shared/plugins/public-api.ts index 2721bc96d..6cbc81898 100644 --- a/services/madoc-ts/src/frontend/shared/plugins/public-api.ts +++ b/services/madoc-ts/src/frontend/shared/plugins/public-api.ts @@ -1,4 +1,3 @@ -import { UpdateModelConfigRequest } from '../../../gateway/api-definitions/update-model-config'; import { useCollectionList } from '../../site/hooks/use-collection-list'; import { useRouteContext } from '../../site/hooks/use-route-context'; import { useCustomTheme, usePageTheme } from '../../themes/helpers/CustomThemeProvider'; @@ -9,13 +8,10 @@ import { LoadingBlock } from '../callouts/LoadingBlock'; import { SmallToast } from '../callouts/SmallToast'; import { SuccessMessage } from '../callouts/SuccessMessage'; import { WarningMessage } from '../callouts/WarningMessage'; -import { - RevisionProviderFeatures, - RevisionProviderWithFeatures, -} from '../capture-models/new/components/RevisionProviderWithFeatures'; +import { RevisionProviderWithFeatures } from '../capture-models/new/components/RevisionProviderWithFeatures'; import { EditorContentViewer } from '../capture-models/new/EditorContent'; -import { CanvasVaultContext } from '../components/CanvasVaultContext'; -import { SingleProject } from '../components/SingleProject'; +import { CanvasVaultContext } from '../capture-models/CanvasVaultContext'; +import { SingleProject } from '../../site/blocks/SingleProject'; import { DefaultSelect } from '../form/DefaulSelect'; import { GlobalSearch } from '../form/GlobalSearch'; import { @@ -104,7 +100,7 @@ import { Button } from '../navigation/Button'; import { LocaleString } from '../components/LocaleString'; import { useApi } from '../hooks/use-api'; import { useSite, useUser } from '../hooks/use-site'; -import AdminPageTitle from '../typography/AdminPageTitle'; +import { AdminPageTitle } from '../typography/AdminPageTitle'; import { AttributionText } from '../typography/AttributionText'; import { GlobalStyles } from '../typography/GlobalStyles'; import { Heading1 } from '../typography/Heading1'; @@ -119,13 +115,10 @@ import { atoms, useAtoms } from './use-atoms'; import { useComponents } from './use-components'; import { useModule } from './use-module'; import { - EditorRenderingConfig, EditorSlots, ProfileProvider, - ProfileConfig, useProfile, useProfileOverride, - EditorConfig, useSlotConfiguration, useSlotContext, } from '../capture-models/new/components/EditorSlots'; diff --git a/services/madoc-ts/src/frontend/shared/typography/GlobalStyles.tsx b/services/madoc-ts/src/frontend/shared/typography/GlobalStyles.tsx index d6d37196c..72b2af581 100644 --- a/services/madoc-ts/src/frontend/shared/typography/GlobalStyles.tsx +++ b/services/madoc-ts/src/frontend/shared/typography/GlobalStyles.tsx @@ -36,8 +36,16 @@ export const GlobalStyles = createGlobalStyle` inset 0 0 0 1px rgba(255, 255, 255, 0.2); } + .react-tooltip { + z-index: 9999; + } + //.rfs-menu-container { // position: fixed; // max-width: 550px; //} + + .selector-preview-svg { + stroke: red; + } `; diff --git a/services/madoc-ts/src/frontend/shared/typography/Heading1.tsx b/services/madoc-ts/src/frontend/shared/typography/Heading1.tsx index 7102a4c86..50f373135 100644 --- a/services/madoc-ts/src/frontend/shared/typography/Heading1.tsx +++ b/services/madoc-ts/src/frontend/shared/typography/Heading1.tsx @@ -12,7 +12,8 @@ const Helmet = _Helmet as any; export const _Heading1 = styled.h1<{ $margin?: boolean }>` font-size: 2em; font-weight: 600; - margin-bottom: 0.2em; + margin-top: 0.5em; + margin-bottom: 0.5em; width: 100%; position: relative; z-index: 3; diff --git a/services/madoc-ts/src/frontend/shared/typography/Heading5.tsx b/services/madoc-ts/src/frontend/shared/typography/Heading5.tsx index 1f7025c2e..8c70a5381 100644 --- a/services/madoc-ts/src/frontend/shared/typography/Heading5.tsx +++ b/services/madoc-ts/src/frontend/shared/typography/Heading5.tsx @@ -18,6 +18,6 @@ export const SingleLineHeading5 = styled(Heading5)` export const Subheading5 = styled.div` font-size: 0.9em; - color: #999; + color: #6b6b6b; margin-bottom: 0.5em; `; diff --git a/services/madoc-ts/src/frontend/shared/utility/create-link.ts b/services/madoc-ts/src/frontend/shared/utility/create-link.ts index 584d823e4..b7b5dff9b 100644 --- a/services/madoc-ts/src/frontend/shared/utility/create-link.ts +++ b/services/madoc-ts/src/frontend/shared/utility/create-link.ts @@ -13,9 +13,9 @@ export function createLink(opt: { hash?: string; }) { const subRoute = opt.subRoute ? `/${opt.subRoute}` : ''; - const suffix = `${subRoute}${opt.query && Object.keys(opt.query).length ? `?${stringify(opt.query)}` : ''}${ - opt.hash ? `#${opt.hash}` : '' - }`; + const query = opt.query && Object.keys(opt.query).length ? `?${stringify(opt.query)}` : ''; + const hash = opt.hash ? `#${opt.hash}` : ''; + const suffix = `${subRoute}${query}${hash}`; const canvasSubRoute = opt.admin ? 'canvases' : 'c'; // Tasks. @@ -26,6 +26,13 @@ export function createLink(opt: { } return `/tasks/${opt.parentTaskId}/subtasks/${opt.taskId}${suffix}`; } + if (opt.subRoute === 'tasks' || opt.subRoute === 'reviews') { + if (opt.projectId) { + return `/projects/${opt.projectId}/${opt.subRoute}/${opt.taskId}${query}${hash}`; + } + + return `/${opt.subRoute}/${opt.taskId}${query}${hash}`; + } if (opt.projectId) { return `/projects/${opt.projectId}/tasks/${opt.taskId}${suffix}`; } @@ -84,5 +91,9 @@ export function createLink(opt: { return `/projects/${opt.projectId}${suffix}`; } + if (opt.subRoute === 'tasks' || opt.subRoute === 'reviews') { + return `/${opt.subRoute}${query}${hash}`; + } + return ''; } diff --git a/services/madoc-ts/src/frontend/shared/utility/create-server-renderer.tsx b/services/madoc-ts/src/frontend/shared/utility/create-server-renderer.tsx index 4927d346f..746a6e6ca 100644 --- a/services/madoc-ts/src/frontend/shared/utility/create-server-renderer.tsx +++ b/services/madoc-ts/src/frontend/shared/utility/create-server-renderer.tsx @@ -301,7 +301,7 @@ export function createServerRenderer( ) ); } catch (e) { - throw new ReactServerError(e); + throw new ReactServerError(e as any); } const helmet = Helmet.renderStatic(); diff --git a/services/madoc-ts/src/frontend/shared/viewers/iiif-explorer.tsx b/services/madoc-ts/src/frontend/shared/viewers/iiif-explorer.tsx new file mode 100644 index 000000000..a4218610e --- /dev/null +++ b/services/madoc-ts/src/frontend/shared/viewers/iiif-explorer.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { madocLazy } from '../utility/madoc-lazy'; +import '@manifest-editor/iiif-browser-bundle/dist-umd/style.css'; + +const ExplorerInner = madocLazy(() => + import('@manifest-editor/iiif-browser-bundle').then(m => ({ default: m.IIIFExplorer })) +); + +export function IIIFExplorer(props: import('@manifest-editor/iiif-browser-bundle').IIIFExplorerProps) { + return ( + + + + ); +} diff --git a/services/madoc-ts/src/frontend/site/features/AllCollectionsPaginatedItems.tsx b/services/madoc-ts/src/frontend/site/blocks/AllCollectionsPaginatedItems.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/AllCollectionsPaginatedItems.tsx rename to services/madoc-ts/src/frontend/site/blocks/AllCollectionsPaginatedItems.tsx diff --git a/services/madoc-ts/src/frontend/site/features/AllCollectionsPagination.tsx b/services/madoc-ts/src/frontend/site/blocks/AllCollectionsPagination.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/AllCollectionsPagination.tsx rename to services/madoc-ts/src/frontend/site/blocks/AllCollectionsPagination.tsx diff --git a/services/madoc-ts/src/frontend/site/features/AllManifestsPaginatedItems.tsx b/services/madoc-ts/src/frontend/site/blocks/AllManifestsPaginatedItems.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/AllManifestsPaginatedItems.tsx rename to services/madoc-ts/src/frontend/site/blocks/AllManifestsPaginatedItems.tsx diff --git a/services/madoc-ts/src/frontend/site/features/AllManifestsPagination.tsx b/services/madoc-ts/src/frontend/site/blocks/AllManifestsPagination.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/AllManifestsPagination.tsx rename to services/madoc-ts/src/frontend/site/blocks/AllManifestsPagination.tsx diff --git a/services/madoc-ts/src/frontend/site/features/AllProjectsPaginatedItems.tsx b/services/madoc-ts/src/frontend/site/blocks/AllProjectsPaginatedItems.tsx similarity index 95% rename from services/madoc-ts/src/frontend/site/features/AllProjectsPaginatedItems.tsx rename to services/madoc-ts/src/frontend/site/blocks/AllProjectsPaginatedItems.tsx index c09e9a9b9..59a6ac1b3 100644 --- a/services/madoc-ts/src/frontend/site/features/AllProjectsPaginatedItems.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/AllProjectsPaginatedItems.tsx @@ -1,7 +1,7 @@ import { InternationalString } from '@iiif/presentation-3'; import React from 'react'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { SingleProject } from '../../shared/components/SingleProject'; +import { SingleProject } from './SingleProject'; import { useProjectList } from '../hooks/use-project-list'; interface AllProjectPaginatedItemsProps { diff --git a/services/madoc-ts/src/frontend/site/features/AllProjectsPagination.tsx b/services/madoc-ts/src/frontend/site/blocks/AllProjectsPagination.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/AllProjectsPagination.tsx rename to services/madoc-ts/src/frontend/site/blocks/AllProjectsPagination.tsx diff --git a/services/madoc-ts/src/frontend/shared/components/Breadcrumbs.tsx b/services/madoc-ts/src/frontend/site/blocks/Breadcrumbs.tsx similarity index 87% rename from services/madoc-ts/src/frontend/shared/components/Breadcrumbs.tsx rename to services/madoc-ts/src/frontend/site/blocks/Breadcrumbs.tsx index f767499e0..19f9dceba 100644 --- a/services/madoc-ts/src/frontend/shared/components/Breadcrumbs.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/Breadcrumbs.tsx @@ -4,11 +4,15 @@ import { Helmet as _Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import { Link, useLocation } from 'react-router-dom'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { useCurrentAdminPages } from '../../site/hooks/use-current-admin-pages'; -import { useSite } from '../hooks/use-site'; -import { ErrorBoundary } from '../utility/error-boundary'; -import { LocaleString, useLocaleString } from './LocaleString'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; +import { DefaultBlockIcon } from '../../shared/page-blocks/AddBlock'; +import { HrefLink } from '../../shared/utility/href-link'; +import { useCurrentAdminPages } from '../hooks/use-current-admin-pages'; +import { useSite, useUser } from '../../shared/hooks/use-site'; +import { ErrorBoundary } from '../../shared/utility/error-boundary'; +import { LocaleString, useLocaleString } from '../../shared/components/LocaleString'; import styled, { css } from 'styled-components'; +import { useRelativeLinks } from '../hooks/use-relative-links'; type BreadcrumbContextType = { project?: { name: InternationalString; id: number | string }; @@ -92,6 +96,26 @@ const ViewInAdmin = styled.div` font-size: 0.75em; `; +const BlocksButton = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 0.4em 0.8em; + border-radius: 3px; + margin-left: 0.5em; + + svg { + font-size: 25px; + color: #999; + } + + &:hover { + background: #f0f0f0; + svg { + color: #5071f4; + } + } +`; const DividerIcon: React.FC<{ className?: string }> = ({ className }) => ( @@ -163,6 +187,8 @@ export const DisplayBreadcrumbs: React.FC = ({ currentPage, tex const breads = useBreadcrumbs(); const location = useLocation(); const adminLinks = useCurrentAdminPages(); + const user = useUser(); + const isAdmin = user && user.scope && user.scope.indexOf('site.admin') !== -1; const { t } = useTranslation(); const stack = useMemo(() => { @@ -297,6 +323,7 @@ export const DisplayBreadcrumbs: React.FC = ({ currentPage, tex ]); const activePage = stack.find(s => s.url === location.pathname); const [pageTitle] = useLocaleString(activePage?.label); + const createLink = useRelativeLinks(); if (stack.length === 0) { return ; @@ -337,6 +364,20 @@ export const DisplayBreadcrumbs: React.FC = ({ currentPage, tex })} ) : null} + {isAdmin ? ( + <> + + + + + + + + ) : null} ); }; diff --git a/services/madoc-ts/src/frontend/site/features/CanvasAtlasViewer.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasAtlasViewer.tsx similarity index 97% rename from services/madoc-ts/src/frontend/site/features/CanvasAtlasViewer.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasAtlasViewer.tsx index 03bd7b96b..762430f56 100644 --- a/services/madoc-ts/src/frontend/site/features/CanvasAtlasViewer.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasAtlasViewer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; import { CanvasImageViewer } from './CanvasImageViewer'; -import { CanvasViewer } from './CanvasViewer'; +import { CanvasViewer } from '../features/canvas/CanvasViewer'; export const CanvasAtlasViewer: React.FC<{ rendering?: 'webgl' | 'canvas'; diff --git a/services/madoc-ts/src/frontend/site/features/CanvasConfigurationViewer.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasConfigurationViewer.tsx similarity index 92% rename from services/madoc-ts/src/frontend/site/features/CanvasConfigurationViewer.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasConfigurationViewer.tsx index 16c6fcdbb..44015ebb3 100644 --- a/services/madoc-ts/src/frontend/site/features/CanvasConfigurationViewer.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasConfigurationViewer.tsx @@ -3,7 +3,7 @@ import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for import { CanvasAtlasViewer } from './CanvasAtlasViewer'; import { CanvasMiradorViewer } from './CanvasMiradorViewer'; import { CanvasUniversalViewer } from './CanvasUniversalViewer'; -import { useSiteConfiguration } from './SiteConfigurationContext'; +import { useSiteConfiguration } from '../features/SiteConfigurationContext'; export const CanvasConfigurationViewer: React.FC = () => { const { diff --git a/services/madoc-ts/src/frontend/site/features/CanvasImageViewer.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasImageViewer.tsx similarity index 94% rename from services/madoc-ts/src/frontend/site/features/CanvasImageViewer.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasImageViewer.tsx index f623f4e97..a82b3da6d 100644 --- a/services/madoc-ts/src/frontend/site/features/CanvasImageViewer.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasImageViewer.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { SimpleAtlasViewer } from '../../shared/components/SimpleAtlasViewer'; +import { SimpleAtlasViewer } from '../../shared/features/SimpleAtlasViewer'; import { useCanvasSearch } from '../../shared/hooks/use-canvas-search'; import { useRouteContext } from '../hooks/use-route-context'; import { useViewerHeight } from '../hooks/use-viewer-height'; diff --git a/services/madoc-ts/src/frontend/site/features/CanvasManifestPagination.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasManifestPagination.tsx similarity index 95% rename from services/madoc-ts/src/frontend/site/features/CanvasManifestPagination.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasManifestPagination.tsx index 73fcb2dd8..5ce7cecf2 100644 --- a/services/madoc-ts/src/frontend/site/features/CanvasManifestPagination.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasManifestPagination.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { CanvasNavigationMinimalist } from '../../shared/components/CanvasNavigationMinimalist'; +import { CanvasNavigationMinimalist } from '../features/canvas/CanvasNavigationMinimalist'; import { useCanvasSearch } from '../../shared/hooks/use-canvas-search'; import { usePrefetchData } from '../../shared/hooks/use-data'; import { useSlots } from '../../shared/page-blocks/slot-context'; diff --git a/services/madoc-ts/src/frontend/site/features/CanvasMiradorViewer.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasMiradorViewer.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/CanvasMiradorViewer.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasMiradorViewer.tsx diff --git a/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasModelCompleteMessage.tsx similarity index 67% rename from services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasModelCompleteMessage.tsx index ed880b26d..cc9b4badc 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasModelCompleteMessage.tsx @@ -1,18 +1,18 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { InfoMessage } from '../../../shared/callouts/InfoMessage'; -import { useUser } from '../../../shared/hooks/use-site'; -import { useCanvasUserTasks } from '../../hooks/use-canvas-user-tasks'; -import { useManifestTask } from '../../hooks/use-manifest-task'; -import { useProjectStatus } from '../../hooks/use-project-status'; -import { useRouteContext } from '../../hooks/use-route-context'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { InfoMessage } from '../../shared/callouts/InfoMessage'; +import { useUser } from '../../shared/hooks/use-site'; +import { useCanvasUserTasks } from '../hooks/use-canvas-user-tasks'; +import { useManifestTask } from '../hooks/use-manifest-task'; +import { useProjectStatus } from '../hooks/use-project-status'; +import { useRouteContext } from '../hooks/use-route-context'; export const CanvasModelCompleteMessage: React.FC = () => { const { projectId } = useRouteContext(); const { isManifestComplete, hasExpired } = useManifestTask(); const user = useUser(); - const { canUserSubmit, isLoading: isLoadingTasks, completedAndHide, completed } = useCanvasUserTasks(); + const { canUserSubmit, canCanvasTakeSubmission, isLoading: isLoadingTasks, completed } = useCanvasUserTasks(); const { t } = useTranslation(); @@ -22,7 +22,7 @@ export const CanvasModelCompleteMessage: React.FC = () => { const hideModelEditor = (!canUserSubmit && !isLoadingTasks) || - completedAndHide || + canCanvasTakeSubmission || isManifestComplete || hasExpired || (!isActive && !isPreparing); diff --git a/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelEditor.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasModelEditor.tsx similarity index 58% rename from services/madoc-ts/src/frontend/site/features/contributor/CanvasModelEditor.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasModelEditor.tsx index a73e06b9a..f43c9290a 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelEditor.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasModelEditor.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { useLocalStorage } from '../../../shared/hooks/use-local-storage'; -import { useLocationQuery } from '../../../shared/hooks/use-location-query'; -import { useUser } from '../../../shared/hooks/use-site'; -import { useCanvasNavigation } from '../../hooks/use-canvas-navigation'; -import { useProjectStatus } from '../../hooks/use-project-status'; -import { CanvasSimpleEditor } from './CanvasSimpleEditor'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { useLocalStorage } from '../../shared/hooks/use-local-storage'; +import { useLocationQuery } from '../../shared/hooks/use-location-query'; +import { useUser } from '../../shared/hooks/use-site'; +import { useCanvasNavigation } from '../hooks/use-canvas-navigation'; +import { useProjectStatus } from '../hooks/use-project-status'; +import { CanvasSimpleEditor } from '../features/canvas/CanvasSimpleEditor'; export const CanvasModelEditor: React.FC = () => { const { showCanvasNavigation } = useCanvasNavigation(); diff --git a/services/madoc-ts/src/frontend/site/features/admin/CanvasModelPrepareActions.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasModelPrepareActions.tsx similarity index 74% rename from services/madoc-ts/src/frontend/site/features/admin/CanvasModelPrepareActions.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasModelPrepareActions.tsx index 661ed99ec..5b4cf31be 100644 --- a/services/madoc-ts/src/frontend/site/features/admin/CanvasModelPrepareActions.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasModelPrepareActions.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { Button, ButtonRow } from '../../../shared/navigation/Button'; -import { useLocalStorage } from '../../../shared/hooks/use-local-storage'; -import { useProjectStatus } from '../../hooks/use-project-status'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { Button, ButtonRow } from '../../shared/navigation/Button'; +import { useLocalStorage } from '../../shared/hooks/use-local-storage'; +import { useProjectStatus } from '../hooks/use-project-status'; export const CanvasModelPrepareActions: React.FC = () => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/site/features/CanvasModelReadOnlyViewer.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasModelReadOnlyViewer.tsx similarity index 72% rename from services/madoc-ts/src/frontend/site/features/CanvasModelReadOnlyViewer.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasModelReadOnlyViewer.tsx index 25318f76e..515eb5c87 100644 --- a/services/madoc-ts/src/frontend/site/features/CanvasModelReadOnlyViewer.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasModelReadOnlyViewer.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { CanvasVaultContext } from '../../shared/components/CanvasVaultContext'; -import { CanvasHighlightedRegions } from './CanvasHighlightedRegions'; +import { CanvasVaultContext } from '../../shared/capture-models/CanvasVaultContext'; +import { CanvasHighlightedRegions } from '../features/canvas/CanvasHighlightedRegions'; import { CanvasImageViewer } from './CanvasImageViewer'; -import { CanvasViewer } from './CanvasViewer'; +import { CanvasViewer } from '../features/canvas/CanvasViewer'; export const CanvasModelReadOnlyViewer: React.FC = () => { return ( diff --git a/services/madoc-ts/src/frontend/site/features/CanvasNotAvailableToBrowse.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasNotAvailableToBrowse.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/CanvasNotAvailableToBrowse.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasNotAvailableToBrowse.tsx diff --git a/services/madoc-ts/src/frontend/site/features/CanvasPageHeader.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasPageHeader.tsx similarity index 96% rename from services/madoc-ts/src/frontend/site/features/CanvasPageHeader.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasPageHeader.tsx index 6439d1ed4..9ba936fc9 100644 --- a/services/madoc-ts/src/frontend/site/features/CanvasPageHeader.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasPageHeader.tsx @@ -11,9 +11,9 @@ import { ManifestLoader, CanvasLoader } from '../components'; import { useCanvasNavigation } from '../hooks/use-canvas-navigation'; import { useRelativeLinks } from '../hooks/use-relative-links'; import { useRouteContext } from '../hooks/use-route-context'; -import { AssignCanvasToUser } from './reviewer/AssignCanvasToUser'; +import { AssignCanvasToUser } from '../features/canvas/AssignCanvasToUser'; import { CanvasManifestPagination } from './CanvasManifestPagination'; -import { CanvasTaskProgress } from './admin/CanvasTaskProgress'; +import { CanvasTaskProgress } from '../features/canvas/CanvasTaskProgress'; import { RequiredStatement } from './RequiredStatement'; export const CanvasPageHeader: React.FC<{ subRoute?: string; title?: string }> = ({ subRoute, title }) => { diff --git a/services/madoc-ts/src/frontend/shared/components/CanvasPanelBlock.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasPanelBlock.tsx similarity index 100% rename from services/madoc-ts/src/frontend/shared/components/CanvasPanelBlock.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasPanelBlock.tsx diff --git a/services/madoc-ts/src/frontend/site/features/contributor/CanvasTaskWarningMessage.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasTaskWarningMessage.tsx similarity index 77% rename from services/madoc-ts/src/frontend/site/features/contributor/CanvasTaskWarningMessage.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasTaskWarningMessage.tsx index 8a1236878..0b56d9727 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/CanvasTaskWarningMessage.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasTaskWarningMessage.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { WarningMessage } from '../../../shared/callouts/WarningMessage'; -import { useCanvasUserTasks } from '../../hooks/use-canvas-user-tasks'; -import { useProjectStatus } from '../../hooks/use-project-status'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { WarningMessage } from '../../shared/callouts/WarningMessage'; +import { useCanvasUserTasks } from '../hooks/use-canvas-user-tasks'; +import { useProjectStatus } from '../hooks/use-project-status'; export const CanvasTaskWarningMessage: React.FC = () => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/site/features/CanvasThumbnailNavigation.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasThumbnailNavigation.tsx similarity index 94% rename from services/madoc-ts/src/frontend/site/features/CanvasThumbnailNavigation.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasThumbnailNavigation.tsx index 38c048c8c..d54764b9c 100644 --- a/services/madoc-ts/src/frontend/site/features/CanvasThumbnailNavigation.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CanvasThumbnailNavigation.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { CanvasNavigation } from '../../shared/components/CanvasNavigation'; +import { CanvasNavigation } from '../../shared/features/CanvasNavigation'; import { useCanvasSearchText } from '../../shared/hooks/use-canvas-search'; import { useRouteContext } from '../hooks/use-route-context'; diff --git a/services/madoc-ts/src/frontend/site/features/CanvasUniversalViewer.tsx b/services/madoc-ts/src/frontend/site/blocks/CanvasUniversalViewer.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/CanvasUniversalViewer.tsx rename to services/madoc-ts/src/frontend/site/blocks/CanvasUniversalViewer.tsx diff --git a/services/madoc-ts/src/frontend/site/features/CollectionFilterOptions.tsx b/services/madoc-ts/src/frontend/site/blocks/CollectionFilterOptions.tsx similarity index 87% rename from services/madoc-ts/src/frontend/site/features/CollectionFilterOptions.tsx rename to services/madoc-ts/src/frontend/site/blocks/CollectionFilterOptions.tsx index e860daa57..5cdde61c1 100644 --- a/services/madoc-ts/src/frontend/site/features/CollectionFilterOptions.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CollectionFilterOptions.tsx @@ -9,11 +9,11 @@ import { useSubjectMap } from '../../shared/hooks/use-subject-map'; import { HrefLink } from '../../shared/utility/href-link'; import { useRelativeLinks } from '../hooks/use-relative-links'; import { CollectionLoader } from '../pages/loaders/collection-loader'; -import { GoToRandomCanvas } from './contributor/GoToRandomCanvas'; -import { GoToRandomManifest } from './contributor/GoToRandomManifest'; +import { GoToRandomCanvas } from '../features/canvas/GoToRandomCanvas'; +import { GoToRandomManifest } from '../features/manifest/GoToRandomManifest'; import { useProjectPageConfiguration } from '../hooks/use-project-page-configuration'; -import { useSiteConfiguration } from './SiteConfigurationContext'; -import { StartContributingButton } from './contributor/StartContributingButton'; +import { useSiteConfiguration } from '../features/SiteConfigurationContext'; +import { StartContributingButton } from '../features/project/StartContributingButton'; export const CollectionFilterOptions: React.FC = () => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/site/features/CollectionItemPagination.tsx b/services/madoc-ts/src/frontend/site/blocks/CollectionItemPagination.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/CollectionItemPagination.tsx rename to services/madoc-ts/src/frontend/site/blocks/CollectionItemPagination.tsx diff --git a/services/madoc-ts/src/frontend/site/features/CollectionMetadata.tsx b/services/madoc-ts/src/frontend/site/blocks/CollectionMetadata.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/CollectionMetadata.tsx rename to services/madoc-ts/src/frontend/site/blocks/CollectionMetadata.tsx diff --git a/services/madoc-ts/src/frontend/site/features/CollectionPaginatedItems.tsx b/services/madoc-ts/src/frontend/site/blocks/CollectionPaginatedItems.tsx similarity index 65% rename from services/madoc-ts/src/frontend/site/features/CollectionPaginatedItems.tsx rename to services/madoc-ts/src/frontend/site/blocks/CollectionPaginatedItems.tsx index e2a404a56..e34ab7e1d 100644 --- a/services/madoc-ts/src/frontend/site/features/CollectionPaginatedItems.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CollectionPaginatedItems.tsx @@ -4,15 +4,22 @@ import { Link } from 'react-router-dom'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; import { CanvasStatus } from '../../shared/atoms/CanvasStatus'; import { SingleLineHeading5, Subheading5 } from '../../shared/typography/Heading5'; -import { ImageGrid, ImageGridItem } from '../../shared/atoms/ImageGrid'; +import { ImageGrid } from '../../shared/atoms/ImageGrid'; import { CroppedImage } from '../../shared/atoms/Images'; import { LocaleString, useCreateLocaleString } from '../../shared/components/LocaleString'; import { usePaginatedData } from '../../shared/hooks/use-data'; import { useSubjectMap } from '../../shared/hooks/use-subject-map'; import { useRelativeLinks } from '../hooks/use-relative-links'; import { CollectionLoader } from '../pages/loaders/collection-loader'; +import { ImageStripBox } from '../../shared/atoms/ImageStrip'; -export const CollectionPaginatedItems: React.FC = () => { +export function CollectionPaginatedItems(props: { + background?: string; + list?: boolean; + textColor?: string; + cardBorder?: string; + imageStyle?: string; +}) { const { t } = useTranslation(); const { data } = usePaginatedData(CollectionLoader); const createLink = useRelativeLinks(); @@ -27,7 +34,7 @@ export const CollectionPaginatedItems: React.FC = () => { } return ( - + {collection.items.map((manifest, idx) => ( { } )} > - - + + {manifest.thumbnail ? ( {createLocaleString(manifest.label, ) : null} @@ -55,17 +67,37 @@ export const CollectionPaginatedItems: React.FC = () => { ? t('{{count}} images', { count: manifest.canvasCount }) : t('{{count}} manifests', { count: manifest.canvasCount })} - + ))} ); -}; +} blockEditorFor(CollectionPaginatedItems, { type: 'default.CollectionPaginatedItems', label: 'Collection paginated items', anyContext: ['collection'], requiredContext: ['collection'], - editor: {}, + defaultProps: { + background: '', + list: false, + textColor: '', + cardBorder: '#999', + imageStyle: 'fit', + }, + editor: { + list: { type: 'checkbox-field', label: 'View', inlineLabel: 'Display as list' }, + background: { label: 'Card background color', type: 'color-field' }, + textColor: { label: 'Card text color', type: 'color-field' }, + cardBorder: { label: 'Card border', type: 'color-field' }, + imageStyle: { + label: 'Image Style', + type: 'dropdown-field', + options: [ + { value: 'covered', text: 'covered' }, + { value: 'fit', text: 'fit' }, + ], + }, + }, }); diff --git a/services/madoc-ts/src/frontend/site/features/CollectionTitle.tsx b/services/madoc-ts/src/frontend/site/blocks/CollectionTitle.tsx similarity index 76% rename from services/madoc-ts/src/frontend/site/features/CollectionTitle.tsx rename to services/madoc-ts/src/frontend/site/blocks/CollectionTitle.tsx index 785b8fb60..ec2df5b90 100644 --- a/services/madoc-ts/src/frontend/site/features/CollectionTitle.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/CollectionTitle.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; import { LocaleString } from '../../shared/components/LocaleString'; +import { Heading1 } from '../../shared/typography/Heading1'; import { usePaginatedCollection } from '../hooks/use-paginated-collection'; export const CollectionTitle: React.FC = () => { @@ -8,10 +9,10 @@ export const CollectionTitle: React.FC = () => { const collection = data?.collection; if (!collection) { - return null; + return {'...'}; } - return {collection.label}; + return {collection.label}; }; blockEditorFor(CollectionTitle, { diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ContinueCanvasSubmission.tsx b/services/madoc-ts/src/frontend/site/blocks/ContinueCanvasSubmission.tsx similarity index 71% rename from services/madoc-ts/src/frontend/site/features/contributor/ContinueCanvasSubmission.tsx rename to services/madoc-ts/src/frontend/site/blocks/ContinueCanvasSubmission.tsx index b5fb27e91..65430f8a0 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ContinueCanvasSubmission.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/ContinueCanvasSubmission.tsx @@ -1,35 +1,37 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { Button } from '../../../shared/navigation/Button'; -import { InfoMessage } from '../../../shared/callouts/InfoMessage'; -import { - ProjectListingDescription, - ProjectListingItem, - ProjectListingTitle, -} from '../../../shared/atoms/ProjectListing'; -import { LocaleString } from '../../../shared/components/LocaleString'; -import { ProjectDetailWrapper } from '../../../shared/components/ProjectDetailWrapper'; -import { useUser } from '../../../shared/hooks/use-site'; -import { HrefLink } from '../../../shared/utility/href-link'; -import { useCanvasUserTasks } from '../../hooks/use-canvas-user-tasks'; -import { useContinueSubmission } from '../../hooks/use-continue-submission'; -import { useManifestTask } from '../../hooks/use-manifest-task'; -import { useProject } from '../../hooks/use-project'; -import { useProjectShadowConfiguration } from '../../hooks/use-project-shadow-configuration'; -import { useProjectStatus } from '../../hooks/use-project-status'; -import { useRelativeLinks } from '../../hooks/use-relative-links'; -import { useRouteContext } from '../../hooks/use-route-context'; -import { useSiteConfiguration } from '../SiteConfigurationContext'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { Button } from '../../shared/navigation/Button'; +import { InfoMessage } from '../../shared/callouts/InfoMessage'; +import { ProjectListingDescription, ProjectListingItem, ProjectListingTitle } from '../../shared/atoms/ProjectListing'; +import { LocaleString } from '../../shared/components/LocaleString'; +import { ProjectDetailWrapper } from '../features/project/ProjectDetailWrapper'; +import { useUser } from '../../shared/hooks/use-site'; +import { HrefLink } from '../../shared/utility/href-link'; +import { useCanvasUserTasks } from '../hooks/use-canvas-user-tasks'; +import { useContinueSubmission } from '../hooks/use-continue-submission'; +import { useManifestTask } from '../hooks/use-manifest-task'; +import { useProject } from '../hooks/use-project'; +import { useProjectShadowConfiguration } from '../hooks/use-project-shadow-configuration'; +import { useProjectStatus } from '../hooks/use-project-status'; +import { useRelativeLinks } from '../hooks/use-relative-links'; +import { useRouteContext } from '../hooks/use-route-context'; +import { useSiteConfiguration } from '../features/SiteConfigurationContext'; export const ContinueCanvasSubmission: React.FC = () => { const { t } = useTranslation(); const { projectId, canvasId } = useRouteContext(); - const { completed, canClaimCanvas, canUserSubmit, isLoading, canContribute, userTasks } = useCanvasUserTasks(); + const { + completed, + canClaimCanvas, + canUserSubmit, + isLoading, + canContribute, + preventFurtherSubmission, + canCanvasTakeSubmission, + } = useCanvasUserTasks(); const { isManifestComplete, canClaimManifest, userManifestTask } = useManifestTask(); - const config = useSiteConfiguration(); - const allowMultiple = !config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource; - const preventFurtherSubmission = !allowMultiple && !!userTasks?.find(task => task.status === 2 || task.status === 3); + const { tasks: continueSubmission, inProgress: continueCount } = useContinueSubmission(); const createLink = useRelativeLinks(); const { data: project } = useProject(); @@ -84,11 +86,11 @@ export const ContinueCanvasSubmission: React.FC = () => { const revision = continueSubmission[0].state.revisionId; const notStarted = continueSubmission[0].status === 0; const started = continueSubmission[0].status === 1; - const completed = continueSubmission[0].status === 2 || continueSubmission[0].status === 3; + const isCompleted = continueSubmission[0].status === 2 || continueSubmission[0].status === 3; return ( - {!continueCount && !notStarted && !completed ? ( + {!continueCount && !notStarted && !isCompleted ? ( {started ? t('You have started this item') : t('You have already completed this item')} @@ -117,7 +119,7 @@ export const ContinueCanvasSubmission: React.FC = () => { ); } - if (canContribute && (canClaimCanvas || canClaimManifest || userManifestTask)) { + if (canCanvasTakeSubmission && (canClaimManifest || userManifestTask)) { return ( ); }; diff --git a/services/madoc-ts/src/frontend/site/features/ManifestCanvasGrid.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestCanvasGrid.tsx similarity index 99% rename from services/madoc-ts/src/frontend/site/features/ManifestCanvasGrid.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestCanvasGrid.tsx index 7fc79163c..887cc3f9f 100644 --- a/services/madoc-ts/src/frontend/site/features/ManifestCanvasGrid.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/ManifestCanvasGrid.tsx @@ -19,7 +19,7 @@ import { useManifestTask } from '../hooks/use-manifest-task'; import { useProjectShadowConfiguration } from '../hooks/use-project-shadow-configuration'; import { useProjectStatus } from '../hooks/use-project-status'; import { useRelativeLinks } from '../hooks/use-relative-links'; -import { CanvasViewer } from './CanvasViewer'; +import { CanvasViewer } from '../features/canvas/CanvasViewer'; import { usePreventCanvasNavigation } from '../hooks/use-prevent-canvas-navigation'; import { FilterInput } from '../../shared/atoms/FilterInput'; diff --git a/services/madoc-ts/src/frontend/site/features/ManifestHeading.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestHeading.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/ManifestHeading.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestHeading.tsx diff --git a/services/madoc-ts/src/frontend/site/features/ManifestHero.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestHero.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/ManifestHero.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestHero.tsx diff --git a/services/madoc-ts/src/frontend/site/features/ManifestMetadata.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestMetadata.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/ManifestMetadata.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestMetadata.tsx diff --git a/services/madoc-ts/src/frontend/site/features/ManifestModelCanvasPreview.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestModelCanvasPreview.tsx similarity index 93% rename from services/madoc-ts/src/frontend/site/features/ManifestModelCanvasPreview.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestModelCanvasPreview.tsx index 402ca9cde..c76fbdfed 100644 --- a/services/madoc-ts/src/frontend/site/features/ManifestModelCanvasPreview.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/ManifestModelCanvasPreview.tsx @@ -3,10 +3,10 @@ import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for import { CustomRouteContext } from '../../shared/page-blocks/slot-context'; import { Button } from '../../shared/navigation/Button'; import { HrefLink } from '../../shared/utility/href-link'; -import { CanvasViewer } from './CanvasViewer'; +import { CanvasViewer } from '../features/canvas/CanvasViewer'; import { StandaloneCanvasViewer } from '../../shared/components/StandaloneCanvasViewer'; import { useRouteContext } from '../hooks/use-route-context'; -import { useManifestPagination } from '../../shared/components/CanvasNavigationMinimalist'; +import { useManifestPagination } from '../../shared/hooks/use-manifest-pagination'; import { useRelativeLinks } from '../hooks/use-relative-links'; import { ManifestCanvasGrid } from './ManifestCanvasGrid'; import styled from 'styled-components'; @@ -105,7 +105,7 @@ export function ManifestModelCanvasPreview(props: { isModel?: boolean }) { - + ); diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ManifestModelEditor.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestModelEditor.tsx similarity index 58% rename from services/madoc-ts/src/frontend/site/features/contributor/ManifestModelEditor.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestModelEditor.tsx index 428602af9..c55e3926a 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ManifestModelEditor.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/ManifestModelEditor.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { useLocalStorage } from '../../../shared/hooks/use-local-storage'; -import { useLocationQuery } from '../../../shared/hooks/use-location-query'; -import { useUser } from '../../../shared/hooks/use-site'; -import { useProjectStatus } from '../../hooks/use-project-status'; -import { ManifestCaptureModelEditor } from './ManifestCaptureModelEditor'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { useLocalStorage } from '../../shared/hooks/use-local-storage'; +import { useLocationQuery } from '../../shared/hooks/use-location-query'; +import { useUser } from '../../shared/hooks/use-site'; +import { useProjectStatus } from '../hooks/use-project-status'; +import { ManifestCaptureModelEditor } from '../features/manifest/ManifestCaptureModelEditor'; export const ManifestModelEditor: React.FC = () => { const { revision } = useLocationQuery(); diff --git a/services/madoc-ts/src/frontend/site/features/ManifestNotAvailableToBrowse.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestNotAvailableToBrowse.tsx similarity index 92% rename from services/madoc-ts/src/frontend/site/features/ManifestNotAvailableToBrowse.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestNotAvailableToBrowse.tsx index 99f1189d2..759195f65 100644 --- a/services/madoc-ts/src/frontend/site/features/ManifestNotAvailableToBrowse.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/ManifestNotAvailableToBrowse.tsx @@ -4,7 +4,7 @@ import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for import { Heading3 } from '../../shared/typography/Heading3'; import { LockIcon } from '../../shared/icons/LockIcon'; import { usePreventCanvasNavigation } from '../hooks/use-prevent-canvas-navigation'; -import { RandomlyAssignCanvas } from './contributor/RandomlyAssignCanvas'; +import { RandomlyAssignCanvas } from '../features/canvas/RandomlyAssignCanvas'; export const ManifestNotAvailableToBrowse: React.FC = () => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/site/features/ManifestPagination.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestPagination.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/ManifestPagination.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestPagination.tsx diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ManifestUserNotification.tsx b/services/madoc-ts/src/frontend/site/blocks/ManifestUserNotification.tsx similarity index 68% rename from services/madoc-ts/src/frontend/site/features/contributor/ManifestUserNotification.tsx rename to services/madoc-ts/src/frontend/site/blocks/ManifestUserNotification.tsx index 63a591e93..e2c43eaf0 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ManifestUserNotification.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/ManifestUserNotification.tsx @@ -1,22 +1,22 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { Button } from '../../../shared/navigation/Button'; -import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; -import { InfoMessage } from '../../../shared/callouts/InfoMessage'; -import { SuccessMessage } from '../../../shared/callouts/SuccessMessage'; -import { WarningMessage } from '../../../shared/callouts/WarningMessage'; -import { useApi } from '../../../shared/hooks/use-api'; -import { useData } from '../../../shared/hooks/use-data'; -import { useUser } from '../../../shared/hooks/use-site'; -import { useManifestTask } from '../../hooks/use-manifest-task'; -import { useProjectShadowConfiguration } from '../../hooks/use-project-shadow-configuration'; -import { useProjectStatus } from '../../hooks/use-project-status'; -import { useRouteContext } from '../../hooks/use-route-context'; -import { ManifestLoader } from '../../pages/loaders/manifest-loader'; -import { useSiteConfiguration } from '../SiteConfigurationContext'; -import { useManifestUserTasks } from '../../hooks/use-manifest-user-tasks'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { Button } from '../../shared/navigation/Button'; +import { ErrorMessage } from '../../shared/callouts/ErrorMessage'; +import { InfoMessage } from '../../shared/callouts/InfoMessage'; +import { SuccessMessage } from '../../shared/callouts/SuccessMessage'; +import { WarningMessage } from '../../shared/callouts/WarningMessage'; +import { useApi } from '../../shared/hooks/use-api'; +import { useData } from '../../shared/hooks/use-data'; +import { useUser } from '../../shared/hooks/use-site'; +import { useManifestTask } from '../hooks/use-manifest-task'; +import { useProjectShadowConfiguration } from '../hooks/use-project-shadow-configuration'; +import { useProjectStatus } from '../hooks/use-project-status'; +import { useRouteContext } from '../hooks/use-route-context'; +import { ManifestLoader } from '../pages/loaders/manifest-loader'; +import { useSiteConfiguration } from '../features/SiteConfigurationContext'; +import { useManifestUserTasks } from '../hooks/use-manifest-user-tasks'; export function ManifestUserNotification(props: { isModal?: boolean }) { const { projectId, manifestId } = useRouteContext(); @@ -35,11 +35,8 @@ export function ManifestUserNotification(props: { isModal?: boolean }) { const { inProgress, done, inReview } = filteredTasks; const { isActive } = useProjectStatus(); const { t } = useTranslation(); - const user = useUser(); const api = useApi(); - const { allTasksDone } = useManifestUserTasks(); - const allowMultiple = !config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource; - const preventFurtherSubmission = !allowMultiple && allTasksDone; + const { user, preventFurtherSubmission } = useManifestUserTasks(); const isEdit = !preventFurtherSubmission && props.isModal; const [onSubmitForReview] = useMutation(async (tid: string) => { diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx b/services/madoc-ts/src/frontend/site/blocks/ProjectActions.tsx similarity index 67% rename from services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx rename to services/madoc-ts/src/frontend/site/blocks/ProjectActions.tsx index c5ba8e0e5..ee3234890 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/ProjectActions.tsx @@ -1,24 +1,26 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { Button, ButtonRow } from '../../../shared/navigation/Button'; -import { useUser } from '../../../shared/hooks/use-site'; -import { useProject } from '../../hooks/use-project'; -import { useProjectPageConfiguration } from '../../hooks/use-project-page-configuration'; -import { useProjectStatus } from '../../hooks/use-project-status'; -import { useRelativeLinks } from '../../hooks/use-relative-links'; -import { GoToRandomCanvas } from './GoToRandomCanvas'; -import { GoToRandomManifest } from './GoToRandomManifest'; -import { useSiteConfiguration } from '../SiteConfigurationContext'; -import { StartContributingButton } from './StartContributingButton'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { Button, ButtonRow } from '../../shared/navigation/Button'; +import { useUser } from '../../shared/hooks/use-site'; +import { useProject } from '../hooks/use-project'; +import { useProjectPageConfiguration } from '../hooks/use-project-page-configuration'; +import { useProjectStatus } from '../hooks/use-project-status'; +import { useRelativeLinks } from '../hooks/use-relative-links'; +import { GoToRandomCanvas } from '../features/canvas/GoToRandomCanvas'; +import { GoToRandomManifest } from '../features/manifest/GoToRandomManifest'; +import { useSiteConfiguration } from '../features/SiteConfigurationContext'; +import { StartContributingButton } from '../features/project/StartContributingButton'; export const ProjectActions: React.FC<{ - showContributingButton?: boolean }> = ({showContributingButton = false}) => { + showContributingButton?: boolean; +}> = ({ showContributingButton = false }) => { const { data: project } = useProject(); const { isActive } = useProjectStatus(); const { t } = useTranslation(); const createLink = useRelativeLinks(); + const { project: { allowCollectionNavigation = true, allowManifestNavigation = true, allowPersonalNotes }, } = useSiteConfiguration(); @@ -35,18 +37,13 @@ export const ProjectActions: React.FC<{
    {showContributingButton && } - {!options.hideSearchButton ? ( - - ) : null} {isReviewer && isActive ? (
    - + ); } diff --git a/services/madoc-ts/src/frontend/site/features/GoToFirstCanvas.tsx b/services/madoc-ts/src/frontend/site/features/canvas/GoToFirstCanvas.tsx similarity index 67% rename from services/madoc-ts/src/frontend/site/features/GoToFirstCanvas.tsx rename to services/madoc-ts/src/frontend/site/features/canvas/GoToFirstCanvas.tsx index c44389a77..97a71bd8f 100644 --- a/services/madoc-ts/src/frontend/site/features/GoToFirstCanvas.tsx +++ b/services/madoc-ts/src/frontend/site/features/canvas/GoToFirstCanvas.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { Button } from '../../shared/navigation/Button'; -import { useManifestStructure } from '../../shared/hooks/use-manifest-structure'; -import { useUser } from '../../shared/hooks/use-site'; -import { HrefLink } from '../../shared/utility/href-link'; -import { useRelativeLinks } from '../hooks/use-relative-links'; -import { useRouteContext } from '../hooks/use-route-context'; +import { Button } from '../../../shared/navigation/Button'; +import { useManifestStructure } from '../../../shared/hooks/use-manifest-structure'; +import { useUser } from '../../../shared/hooks/use-site'; +import { HrefLink } from '../../../shared/utility/href-link'; +import { useRelativeLinks } from '../../hooks/use-relative-links'; +import { useRouteContext } from '../../hooks/use-route-context'; export const GoToFirstCanvas: React.FC<{ navigateToModel?: boolean; diff --git a/services/madoc-ts/src/frontend/site/features/contributor/GoToRandomCanvas.tsx b/services/madoc-ts/src/frontend/site/features/canvas/GoToRandomCanvas.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/contributor/GoToRandomCanvas.tsx rename to services/madoc-ts/src/frontend/site/features/canvas/GoToRandomCanvas.tsx diff --git a/services/madoc-ts/src/frontend/site/features/admin/PrepareCanvasCaptureModel.tsx b/services/madoc-ts/src/frontend/site/features/canvas/PrepareCanvasCaptureModel.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/admin/PrepareCanvasCaptureModel.tsx rename to services/madoc-ts/src/frontend/site/features/canvas/PrepareCanvasCaptureModel.tsx diff --git a/services/madoc-ts/src/frontend/site/features/contributor/RandomlyAssignCanvas.tsx b/services/madoc-ts/src/frontend/site/features/canvas/RandomlyAssignCanvas.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/contributor/RandomlyAssignCanvas.tsx rename to services/madoc-ts/src/frontend/site/features/canvas/RandomlyAssignCanvas.tsx diff --git a/services/madoc-ts/src/frontend/site/features/RedirectToNextCanvas.tsx b/services/madoc-ts/src/frontend/site/features/canvas/RedirectToNextCanvas.tsx similarity index 71% rename from services/madoc-ts/src/frontend/site/features/RedirectToNextCanvas.tsx rename to services/madoc-ts/src/frontend/site/features/canvas/RedirectToNextCanvas.tsx index e51b1ab1d..c941c0118 100644 --- a/services/madoc-ts/src/frontend/site/features/RedirectToNextCanvas.tsx +++ b/services/madoc-ts/src/frontend/site/features/canvas/RedirectToNextCanvas.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; import { Navigate } from 'react-router-dom'; -import { useManifestStructure } from '../../shared/hooks/use-manifest-structure'; -import { useRelativeLinks } from '../hooks/use-relative-links'; -import { useRouteContext } from '../hooks/use-route-context'; +import { useManifestStructure } from '../../../shared/hooks/use-manifest-structure'; +import { useRelativeLinks } from '../../hooks/use-relative-links'; +import { useRouteContext } from '../../hooks/use-route-context'; export const RedirectToNextCanvas: React.FC<{ subRoute?: string }> = ({ subRoute }) => { const { manifestId, canvasId: id } = useRouteContext(); diff --git a/services/madoc-ts/src/frontend/site/features/contributor/TranscriberModeWorkflowBar.tsx b/services/madoc-ts/src/frontend/site/features/canvas/TranscriberModeWorkflowBar.tsx similarity index 98% rename from services/madoc-ts/src/frontend/site/features/contributor/TranscriberModeWorkflowBar.tsx rename to services/madoc-ts/src/frontend/site/features/canvas/TranscriberModeWorkflowBar.tsx index b13ce23c6..8bd5387de 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/TranscriberModeWorkflowBar.tsx +++ b/services/madoc-ts/src/frontend/site/features/canvas/TranscriberModeWorkflowBar.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import { useNavigate } from 'react-router-dom'; -import { WorkflowBar } from '../../../shared/components/WorkflowBar'; +import { WorkflowBar } from '../../blocks/WorkflowBar'; import { useApi } from '../../../shared/hooks/use-api'; import { useManifestStructure } from '../../../shared/hooks/use-manifest-structure'; import { createLink } from '../../../shared/utility/create-link'; diff --git a/services/madoc-ts/src/frontend/shared/components/CollectionExplorer.tsx b/services/madoc-ts/src/frontend/site/features/collections/CollectionExplorer.tsx similarity index 78% rename from services/madoc-ts/src/frontend/shared/components/CollectionExplorer.tsx rename to services/madoc-ts/src/frontend/site/features/collections/CollectionExplorer.tsx index e1e33c636..f43364f5b 100644 --- a/services/madoc-ts/src/frontend/shared/components/CollectionExplorer.tsx +++ b/services/madoc-ts/src/frontend/site/features/collections/CollectionExplorer.tsx @@ -1,12 +1,12 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ItemStructureListItem } from '../../../types/schemas/item-structure-list'; -import { TinyButton } from '../navigation/Button'; -import { SingleLineHeading5 } from '../typography/Heading5'; -import { ImageGrid, ImageGridItem } from '../atoms/ImageGrid'; -import { CroppedImage } from '../atoms/Images'; -import { useApiStructure } from '../hooks/use-api-structure'; -import { LocaleString } from './LocaleString'; +import { ItemStructureListItem } from '../../../../types/schemas/item-structure-list'; +import { TinyButton } from '../../../shared/navigation/Button'; +import { SingleLineHeading5 } from '../../../shared/typography/Heading5'; +import { ImageGrid, ImageGridItem } from '../../../shared/atoms/ImageGrid'; +import { CroppedImage } from '../../../shared/atoms/Images'; +import { useApiStructure } from '../../../shared/hooks/use-api-structure'; +import { LocaleString } from '../../../shared/components/LocaleString'; export const CollectionExplorer: React.FC<{ id: number; diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ProjectContributionButton.tsx b/services/madoc-ts/src/frontend/site/features/contributor/ProjectContributionButton.tsx deleted file mode 100644 index 2e419dc9d..000000000 --- a/services/madoc-ts/src/frontend/site/features/contributor/ProjectContributionButton.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; -import { parseUrn } from '../../../../utility/parse-urn'; -import { SmallButton } from '../../../shared/navigation/Button'; -import { CSSThirdGrid } from '../../../shared/layout/Grid'; -import { Heading3 } from '../../../shared/typography/Heading3'; -import { ContinueTaskDisplay } from '../../../shared/components/ContinueTaskDisplay'; -import { useContributorTasks } from '../../../shared/hooks/use-contributor-tasks'; -import { firstNTasksWithUniqueSubjects } from '../../../shared/utility/first-n-tasks-with-unique-subjects'; -import { HrefLink } from '../../../shared/utility/href-link'; -import { useProject } from '../../hooks/use-project'; -import { useProjectShadowConfiguration } from '../../hooks/use-project-shadow-configuration'; -import { useProjectStatus } from '../../hooks/use-project-status'; -import { useRelativeLinks } from '../../hooks/use-relative-links'; -import { useSiteConfiguration } from '../SiteConfigurationContext'; - -export const ProjectContributionButton: React.FC = () => { - const { t } = useTranslation(); - const { data: project } = useProject(); - const { isActive } = useProjectStatus(); - const createLink = useRelativeLinks(); - const { - project: { contributionMode, claimGranularity }, - } = useSiteConfiguration(); - const contributorTasks = useContributorTasks({ rootTaskId: project?.task_id }, !!project); - const { showCaptureModelOnManifest } = useProjectShadowConfiguration(); - - const currentTasks = contributorTasks?.drafts.tasks; - const tasksInReview = contributorTasks?.reviews.tasks; - - if (!project || !isActive) { - return null; - } - - const taskComponents = []; - - if (currentTasks && currentTasks.length) { - const firstThree = firstNTasksWithUniqueSubjects(currentTasks, 3); - - for (const task of firstThree) { - taskComponents.push( -
    - -
    - ); - } - } - - if ( - contributionMode !== 'transcription' && - claimGranularity !== 'manifest' && - tasksInReview && - tasksInReview.length && - taskComponents.length < 3 - ) { - const firstThree = firstNTasksWithUniqueSubjects(tasksInReview, 3 - taskComponents.length); - - for (const task of firstThree) { - const parsed = parseUrn(task.subject); - if (parsed && parsed.type === 'canvas') { - taskComponents.push( -
    - -
    - ); - } - } - } - - if (taskComponents.length === 0) { - return null; - } - - return ( -
    - {t('Continue where you left off')} - {taskComponents} - - {t('View all contributions')} - -
    - ); -}; - -blockEditorFor(ProjectContributionButton, { - type: 'default.ProjectContributionButton', - label: 'Continue where you left off', - anyContext: ['project'], - requiredContext: ['project'], - editor: {}, -}); diff --git a/services/madoc-ts/src/frontend/site/features/GlobalFooter.tsx b/services/madoc-ts/src/frontend/site/features/global/GlobalFooter.tsx similarity index 53% rename from services/madoc-ts/src/frontend/site/features/GlobalFooter.tsx rename to services/madoc-ts/src/frontend/site/features/global/GlobalFooter.tsx index afa812d70..a2cdfc5a4 100644 --- a/services/madoc-ts/src/frontend/site/features/GlobalFooter.tsx +++ b/services/madoc-ts/src/frontend/site/features/global/GlobalFooter.tsx @@ -1,21 +1,20 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import SimpleMarkdownBlock from '../../../extensions/page-blocks/simple-markdown-block/simple-markdown-block'; -import { StaticMarkdownBlock } from '../../../extensions/page-blocks/simple-markdown-block/static-markdown-block'; -import { FooterImageGrid } from '../../shared/components/FooterImageGrid'; -import { RenderFragment } from '../../shared/components/RenderFragment'; -import { GlobalSearch } from '../../shared/form/GlobalSearch'; -import { useSiteTheme } from '../../shared/hooks/use-site'; -import { FlexSpacer } from '../../shared/layout/FlexSpacer'; -import { SiteFooter, SiteFooterBackground } from '../../shared/layout/SiteFooter'; -import { AutoSlotLoader } from '../../shared/page-blocks/auto-slot-loader'; -import { AvailableBlocks } from '../../shared/page-blocks/available-blocks'; -import { Slot } from '../../shared/page-blocks/slot'; -import { GlobalStyles } from '../../shared/typography/GlobalStyles'; -import { maxWidth } from '../variables/global'; -import { footerColor, footerBackground, footerContainerBackground } from '../../shared/variables'; -import { GlobalMenuStack } from './GlobalMenuStack'; +import { HrefLink } from '../../../shared/utility/href-link'; +import { FooterImageGrid } from '../../blocks/FooterImageGrid'; +import { RenderFragment } from '../../../shared/components/RenderFragment'; +import { GlobalSearch } from '../../../shared/form/GlobalSearch'; +import { useSite, useSiteTheme } from '../../../shared/hooks/use-site'; +import { FlexSpacer } from '../../../shared/layout/FlexSpacer'; +import { SiteFooter, SiteFooterBackground } from '../../../shared/layout/SiteFooter'; +import { AutoSlotLoader } from '../../../shared/page-blocks/auto-slot-loader'; +import { AvailableBlocks } from '../../../shared/page-blocks/available-blocks'; +import { Slot } from '../../../shared/page-blocks/slot'; +import { GlobalStyles } from '../../../shared/typography/GlobalStyles'; +import { maxWidth } from '../../variables/global'; +import { footerColor, footerBackground, footerContainerBackground } from '../../../shared/variables'; +import { GlobalMenuStack } from '../../blocks/GlobalMenuStack'; const StyledGlobalFooter = styled.div` background: ${footerBackground}; @@ -34,6 +33,7 @@ const GlobalFooterContainer = styled.div` export const GlobalFooter: React.FC = () => { const { t, i18n } = useTranslation(); const siteTheme = useSiteTheme(); + const site = useSite(); const themFooter = siteTheme && @@ -52,7 +52,15 @@ export const GlobalFooter: React.FC = () => { - {t('Powered by Madoc')} + + {t('Powered by Madoc')} + {site.latestTerms ? ( + + | + {t('Terms of use')} + + ) : null} + diff --git a/services/madoc-ts/src/frontend/site/features/GlobalSiteHeader.tsx b/services/madoc-ts/src/frontend/site/features/global/GlobalSiteHeader.tsx similarity index 52% rename from services/madoc-ts/src/frontend/site/features/GlobalSiteHeader.tsx rename to services/madoc-ts/src/frontend/site/features/global/GlobalSiteHeader.tsx index cadab1207..0cccc3ec5 100644 --- a/services/madoc-ts/src/frontend/site/features/GlobalSiteHeader.tsx +++ b/services/madoc-ts/src/frontend/site/features/global/GlobalSiteHeader.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { ErrorMessage } from '../../shared/callouts/ErrorMessage'; -import { FlexSpacer } from '../../shared/layout/FlexSpacer'; -import { GlobalSearch } from '../../shared/form/GlobalSearch'; -import { GlobalStyles } from '../../shared/typography/GlobalStyles'; -import { SiteHeader, SiteHeaderBackground } from '../../shared/layout/SiteHeader'; -import { useApi, useIsApiRestarting } from '../../shared/hooks/use-api'; -import { AutoSlotLoader } from '../../shared/page-blocks/auto-slot-loader'; -import { Slot } from '../../shared/page-blocks/slot'; -import { GlobalMenuStack } from './GlobalMenuStack'; +import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; +import { FlexSpacer } from '../../../shared/layout/FlexSpacer'; +import { GlobalSearch } from '../../../shared/form/GlobalSearch'; +import { GlobalStyles } from '../../../shared/typography/GlobalStyles'; +import { SiteHeader, SiteHeaderBackground } from '../../../shared/layout/SiteHeader'; +import { useApi, useIsApiRestarting } from '../../../shared/hooks/use-api'; +import { AutoSlotLoader } from '../../../shared/page-blocks/auto-slot-loader'; +import { Slot } from '../../../shared/page-blocks/slot'; +import { GlobalMenuStack } from '../../blocks/GlobalMenuStack'; export const GlobalSiteHeader: React.FC<{ menu?: any }> = () => { const api = useApi(); diff --git a/services/madoc-ts/src/frontend/site/features/GlobalSiteNavigation.tsx b/services/madoc-ts/src/frontend/site/features/global/GlobalSiteNavigation.tsx similarity index 80% rename from services/madoc-ts/src/frontend/site/features/GlobalSiteNavigation.tsx rename to services/madoc-ts/src/frontend/site/features/global/GlobalSiteNavigation.tsx index 074ec25b5..96e666dcb 100644 --- a/services/madoc-ts/src/frontend/site/features/GlobalSiteNavigation.tsx +++ b/services/madoc-ts/src/frontend/site/features/global/GlobalSiteNavigation.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { LightNavigation, LightNavigationItem } from '../../shared/navigation/LightNavigation'; -import { LocaleString } from '../../shared/components/LocaleString'; -import { useNavigationOptions, useUser } from '../../shared/hooks/use-site'; -import { HrefLink } from '../../shared/utility/href-link'; -import { useSiteConfiguration } from './SiteConfigurationContext'; +import { LightNavigation, LightNavigationItem } from '../../../shared/navigation/LightNavigation'; +import { LocaleString } from '../../../shared/components/LocaleString'; +import { useNavigationOptions, useUser } from '../../../shared/hooks/use-site'; +import { HrefLink } from '../../../shared/utility/href-link'; +import { useSiteConfiguration } from '../SiteConfigurationContext'; export function GlobalSiteNavigation(props: { showHomepageMenu?: boolean; @@ -23,6 +23,7 @@ export function GlobalSiteNavigation(props: { const showCollections = enableCollections && !project.headerOptions?.hideCollectionsLink; const showDashboard = user && !project.headerOptions?.hideDashboardLink; const showNavLinks = !project.headerOptions?.hidePageNavLinks; + const showReviews = !!project.headerOptions?.showReviews; const isExtraActive = props.extraNavItems ? props.extraNavItems.some(i => i.slug === location.pathname) : false; return ( @@ -47,6 +48,11 @@ export function GlobalSiteNavigation(props: { {t('User dashboard')} ) : null} + {showReviews ? ( + + {t('Reviews')} + + ) : null} {props.extraNavItems?.length ? props.extraNavItems.map((item, i) => { return ( diff --git a/services/madoc-ts/src/frontend/site/features/contributor/UserGreeting.tsx b/services/madoc-ts/src/frontend/site/features/home/UserGreeting.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/contributor/UserGreeting.tsx rename to services/madoc-ts/src/frontend/site/features/home/UserGreeting.tsx diff --git a/services/madoc-ts/src/frontend/site/features/reviewer/AssignManifestToUser.tsx b/services/madoc-ts/src/frontend/site/features/manifest/AssignManifestToUser.tsx similarity index 95% rename from services/madoc-ts/src/frontend/site/features/reviewer/AssignManifestToUser.tsx rename to services/madoc-ts/src/frontend/site/features/manifest/AssignManifestToUser.tsx index 5de26a4d3..f122481d5 100644 --- a/services/madoc-ts/src/frontend/site/features/reviewer/AssignManifestToUser.tsx +++ b/services/madoc-ts/src/frontend/site/features/manifest/AssignManifestToUser.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import { Button } from '../../../shared/navigation/Button'; import { ModalButton } from '../../../shared/components/Modal'; -import { AutocompleteUser } from '../../../shared/components/UserAutocomplete'; +import { AutocompleteUser } from '../UserAutocomplete'; import { useApi } from '../../../shared/hooks/use-api'; import { useUserDetails } from '../../../shared/hooks/use-user-details'; import { isReviewer } from '../../../shared/utility/user-roles'; diff --git a/services/madoc-ts/src/frontend/site/features/reviewer/AssignUserToManifestTask.tsx b/services/madoc-ts/src/frontend/site/features/manifest/AssignUserToManifestTask.tsx similarity index 98% rename from services/madoc-ts/src/frontend/site/features/reviewer/AssignUserToManifestTask.tsx rename to services/madoc-ts/src/frontend/site/features/manifest/AssignUserToManifestTask.tsx index 2216610c8..ffeca8cb8 100644 --- a/services/madoc-ts/src/frontend/site/features/reviewer/AssignUserToManifestTask.tsx +++ b/services/madoc-ts/src/frontend/site/features/manifest/AssignUserToManifestTask.tsx @@ -9,7 +9,7 @@ import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; import { KanbanAssignee } from '../../../shared/atoms/Kanban'; import { Status } from '../../../shared/atoms/Status'; import { TableContainer, TableRow, TableRowLabel } from '../../../shared/layout/Table'; -import { AutocompleteUser, UserAutocomplete } from '../../../shared/components/UserAutocomplete'; +import { AutocompleteUser, UserAutocomplete } from '../UserAutocomplete'; import { useManifestTask } from '../../hooks/use-manifest-task'; export const AssignUserToManifestTask: React.FC<{ onAssign: (user: AutocompleteUser) => Promise }> = ({ diff --git a/services/madoc-ts/src/frontend/site/features/GenerateManifestPdf.tsx b/services/madoc-ts/src/frontend/site/features/manifest/GenerateManifestPdf.tsx similarity index 81% rename from services/madoc-ts/src/frontend/site/features/GenerateManifestPdf.tsx rename to services/madoc-ts/src/frontend/site/features/manifest/GenerateManifestPdf.tsx index 9ceedc7a9..7497a899d 100644 --- a/services/madoc-ts/src/frontend/site/features/GenerateManifestPdf.tsx +++ b/services/madoc-ts/src/frontend/site/features/manifest/GenerateManifestPdf.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { useRouteContext } from '../hooks/use-route-context'; -import { useSite } from '../../shared/hooks/use-site'; +import { useRouteContext } from '../../hooks/use-route-context'; +import { useSite } from '../../../shared/hooks/use-site'; import styled from 'styled-components'; -import { Button } from '../../shared/navigation/Button'; +import { Button } from '../../../shared/navigation/Button'; import { useTranslation } from 'react-i18next'; -import { ModalButton } from '../../shared/components/Modal'; +import { ModalButton } from '../../../shared/components/Modal'; const Wrapper = styled.div``; diff --git a/services/madoc-ts/src/frontend/site/features/contributor/GoToRandomManifest.tsx b/services/madoc-ts/src/frontend/site/features/manifest/GoToRandomManifest.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/contributor/GoToRandomManifest.tsx rename to services/madoc-ts/src/frontend/site/features/manifest/GoToRandomManifest.tsx diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ManifestCaptureModelEditor.tsx b/services/madoc-ts/src/frontend/site/features/manifest/ManifestCaptureModelEditor.tsx similarity index 87% rename from services/madoc-ts/src/frontend/site/features/contributor/ManifestCaptureModelEditor.tsx rename to services/madoc-ts/src/frontend/site/features/manifest/ManifestCaptureModelEditor.tsx index b700fd045..15d32bda2 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ManifestCaptureModelEditor.tsx +++ b/services/madoc-ts/src/frontend/site/features/manifest/ManifestCaptureModelEditor.tsx @@ -11,7 +11,6 @@ import { SimpleSaveButton } from '../../../shared/capture-models/new/components/ import { RevisionRequest } from '../../../shared/capture-models/types/revision-request'; import { LocaleString } from '../../../shared/components/LocaleString'; import { useApi } from '../../../shared/hooks/use-api'; -import { useCurrentUser } from '../../../shared/hooks/use-current-user'; import { EmptyState } from '../../../shared/layout/EmptyState'; import { Heading2 } from '../../../shared/typography/Heading2'; import { isEditingAnotherUsersRevision } from '../../../shared/utility/is-editing-another-users-revision'; @@ -21,10 +20,11 @@ import { useModelPageConfiguration } from '../../hooks/use-model-page-configurat import { useProject } from '../../hooks/use-project'; import { useProjectStatus } from '../../hooks/use-project-status'; import { RouteContext, useRouteContext } from '../../hooks/use-route-context'; -import { CanvasModelUserStatus } from './CanvasModelUserStatus'; +import { CanvasModelUserStatus } from '../canvas/CanvasModelUserStatus'; import { CanvasViewerEditorStyleReset, ContributionSaveButton } from '../../../shared/atoms/CanvasViewerGrid'; import { useSiteConfiguration } from '../SiteConfigurationContext'; import { useLoadedCaptureModel } from '../../../shared/hooks/use-loaded-capture-model'; +import { useRevisionList } from '../../../shared/capture-models/new/hooks/use-revision-list'; export function ManifestCaptureModelEditor({ revision }: { revision: string; isSegmentation?: boolean }) { const { t } = useTranslation(); @@ -32,9 +32,15 @@ export function ManifestCaptureModelEditor({ revision }: { revision: string; isS const { data: projectModel } = useManifestModel(); const { data: project } = useProject(); const [{ captureModel }, , modelRefetch] = useLoadedCaptureModel(projectModel?.model?.id, undefined, undefined); - const { updateClaim, allTasksDone, markedAsUnusable } = useManifestUserTasks(); + const { + updateClaim, + preventFurtherSubmission, + canContribute, + markedAsUnusable, + user, + submittedTasks, + } = useManifestUserTasks(); const { isPreparing } = useProjectStatus(); - const user = useCurrentUser(true); const config = useSiteConfiguration(); const { disableSaveForLater = false, @@ -45,21 +51,12 @@ export function ManifestCaptureModelEditor({ revision }: { revision: string; isS const [postSubmission, setPostSubmission] = useState(false); const [postSubmissionMessage, setPostSubmissionMessage] = useState(false); const allowMultiple = !config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource; - const preventFurtherSubmission = !allowMultiple && allTasksDone; - const isEditing = isEditingAnotherUsersRevision(captureModel, revision, user.user); - - const canContribute = - user && - user.scope && - (user.scope.indexOf('site.admin') !== -1 || - user.scope.indexOf('models.admin') !== -1 || - user.scope.indexOf('models.contribute') !== -1); + const isEditing = isEditingAnotherUsersRevision(captureModel, revision, user); + const autoSave = config.project.modelPageOptions?.enableAutoSave; - const isModelAdmin = - user && user.scope && (user.scope.indexOf('site.admin') !== -1 || user.scope.indexOf('models.admin') !== -1); const features: RevisionProviderFeatures = isPreparing ? { - autosave: false, + autosave: autoSave, autoSelectingRevision: true, revisionEditMode: false, directEdit: true, @@ -95,15 +92,17 @@ export function ManifestCaptureModelEditor({ revision }: { revision: string; isS } } - if (api.getIsServer() || !manifestId || !projectId || (isPreparing && !isModelAdmin)) { + if (api.getIsServer() || !manifestId || !projectId || (isPreparing && !canContribute)) { return null; } + const s = submittedTasks?.state.revisionId; + return ( {preventFurtherSubmission ? ( <> - + {t('Task is complete!')} {markedAsUnusable diff --git a/services/madoc-ts/src/frontend/site/features/ManifestItemFilter.tsx b/services/madoc-ts/src/frontend/site/features/manifest/ManifestItemFilter.tsx similarity index 91% rename from services/madoc-ts/src/frontend/site/features/ManifestItemFilter.tsx rename to services/madoc-ts/src/frontend/site/features/manifest/ManifestItemFilter.tsx index 0096f4c22..051b9856e 100644 --- a/services/madoc-ts/src/frontend/site/features/ManifestItemFilter.tsx +++ b/services/madoc-ts/src/frontend/site/features/manifest/ManifestItemFilter.tsx @@ -4,10 +4,10 @@ import useDropdownMenu from 'react-accessible-dropdown-menu-hook'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import styled, { css } from 'styled-components'; -import { Button, ButtonIcon } from '../../shared/navigation/Button'; -import { useLocationQuery } from '../../shared/hooks/use-location-query'; -import { FilterIcon } from '../../shared/icons/FilterIcon'; -import { useRouteContext } from '../hooks/use-route-context'; +import { Button, ButtonIcon } from '../../../shared/navigation/Button'; +import { useLocationQuery } from '../../../shared/hooks/use-location-query'; +import { FilterIcon } from '../../../shared/icons/FilterIcon'; +import { useRouteContext } from '../../hooks/use-route-context'; const ItemFilterContainer = styled.div` position: relative; diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ManifestTaskProgress.tsx b/services/madoc-ts/src/frontend/site/features/manifest/ManifestTaskProgress.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/contributor/ManifestTaskProgress.tsx rename to services/madoc-ts/src/frontend/site/features/manifest/ManifestTaskProgress.tsx diff --git a/services/madoc-ts/src/frontend/site/features/admin/PrepareManifestCaptureModel.tsx b/services/madoc-ts/src/frontend/site/features/manifest/PrepareManifestCaptureModel.tsx similarity index 94% rename from services/madoc-ts/src/frontend/site/features/admin/PrepareManifestCaptureModel.tsx rename to services/madoc-ts/src/frontend/site/features/manifest/PrepareManifestCaptureModel.tsx index fd00658db..dfe162128 100644 --- a/services/madoc-ts/src/frontend/site/features/admin/PrepareManifestCaptureModel.tsx +++ b/services/madoc-ts/src/frontend/site/features/manifest/PrepareManifestCaptureModel.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; import { InfoMessage } from '../../../shared/callouts/InfoMessage'; import { Spinner } from '../../../shared/icons/Spinner'; -import { usePreparedCanvasModel } from '../../hooks/use-prepared-canvas-model'; import { usePreparedManifestModel } from '../../hooks/use-prepared-manifest-model'; /** diff --git a/services/madoc-ts/src/frontend/shared/components/ProjectDetailWrapper.tsx b/services/madoc-ts/src/frontend/site/features/project/ProjectDetailWrapper.tsx similarity index 69% rename from services/madoc-ts/src/frontend/shared/components/ProjectDetailWrapper.tsx rename to services/madoc-ts/src/frontend/site/features/project/ProjectDetailWrapper.tsx index e4ebed457..931b83806 100644 --- a/services/madoc-ts/src/frontend/shared/components/ProjectDetailWrapper.tsx +++ b/services/madoc-ts/src/frontend/site/features/project/ProjectDetailWrapper.tsx @@ -1,11 +1,15 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useProject } from '../../site/hooks/use-project'; -import { useRouteContext } from '../../site/hooks/use-route-context'; -import { ProjectListingDescription, ProjectListingItem, ProjectListingTitle } from '../atoms/ProjectListing'; -import { Button } from '../navigation/Button'; -import { HrefLink } from '../utility/href-link'; -import { LocaleString } from './LocaleString'; +import { useProject } from '../../hooks/use-project'; +import { useRouteContext } from '../../hooks/use-route-context'; +import { + ProjectListingDescription, + ProjectListingItem, + ProjectListingTitle, +} from '../../../shared/atoms/ProjectListing'; +import { Button } from '../../../shared/navigation/Button'; +import { HrefLink } from '../../../shared/utility/href-link'; +import { LocaleString } from '../../../shared/components/LocaleString'; export const ProjectDetailWrapper: React.FC<{ message?: any }> = ({ message, children }) => { const { projectId } = useRouteContext(); diff --git a/services/madoc-ts/src/frontend/site/features/contributor/StartContributingButton.tsx b/services/madoc-ts/src/frontend/site/features/project/StartContributingButton.tsx similarity index 92% rename from services/madoc-ts/src/frontend/site/features/contributor/StartContributingButton.tsx rename to services/madoc-ts/src/frontend/site/features/project/StartContributingButton.tsx index 8fd50148b..a70e49355 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/StartContributingButton.tsx +++ b/services/madoc-ts/src/frontend/site/features/project/StartContributingButton.tsx @@ -5,8 +5,8 @@ import { useSiteConfiguration } from '../SiteConfigurationContext'; import { useUser } from '../../../shared/hooks/use-site'; import { useProjectPageConfiguration } from '../../hooks/use-project-page-configuration'; import { useProjectShadowConfiguration } from '../../hooks/use-project-shadow-configuration'; -import { GoToRandomManifest } from './GoToRandomManifest'; -import { GoToRandomCanvas } from './GoToRandomCanvas'; +import { GoToRandomManifest } from '../manifest/GoToRandomManifest'; +import { GoToRandomCanvas } from '../canvas/GoToRandomCanvas'; export const StartContributingButton: React.FC = () => { const { isActive } = useProjectStatus(); diff --git a/services/madoc-ts/src/frontend/site/features/reviewer/CanvasReviewList.tsx b/services/madoc-ts/src/frontend/site/features/reviewer/CanvasReviewList.tsx deleted file mode 100644 index 4a5b70e3a..000000000 --- a/services/madoc-ts/src/frontend/site/features/reviewer/CanvasReviewList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { BaseTask } from '../../../../gateway/tasks/base-task'; -import { Heading3 } from '../../../shared/typography/Heading3'; -import { Status } from '../../../shared/atoms/Status'; -import { TableContainer, TableRow, TableRowLabel } from '../../../shared/layout/Table'; -import { useProjectCanvasTasks } from '../../hooks/use-project-canvas-tasks'; - -export const CanvasReviewList: React.FC = () => { - const { t } = useTranslation(); - const { data: tasks, isLoading: isLoadingTasks } = useProjectCanvasTasks(); - - const reviews = useMemo( - () => - tasks?.userTasks - ? tasks.userTasks.filter( - task => (task as BaseTask).type === 'crowdsourcing-review' && (task.status === 2 || task.status === 1) - ) - : [], - [tasks] - ); - - if (!reviews.length || isLoadingTasks) { - return null; - } - - return ( -
    - {t('Reviews')} - - {reviews.map(task => ( - - - - - - {task.name} - - - ))} - -
    - ); -}; diff --git a/services/madoc-ts/src/frontend/shared/components/SearchResults.tsx b/services/madoc-ts/src/frontend/site/features/search/SearchResults.tsx similarity index 84% rename from services/madoc-ts/src/frontend/shared/components/SearchResults.tsx rename to services/madoc-ts/src/frontend/site/features/search/SearchResults.tsx index 6a0f5b173..1f1ce1f22 100644 --- a/services/madoc-ts/src/frontend/shared/components/SearchResults.tsx +++ b/services/madoc-ts/src/frontend/site/features/search/SearchResults.tsx @@ -1,18 +1,18 @@ import React from 'react'; import styled, { css } from 'styled-components'; -import { SearchResult } from '../../../types/search'; -import { parseUrn } from '../../../utility/parse-urn'; -import { useRouteContext } from '../../site/hooks/use-route-context'; -import { CroppedImage } from '../atoms/Images'; -import { ImageStripBox } from '../atoms/ImageStrip'; -import { GridContainer } from '../layout/Grid'; -import { SnippetThumbnail, SnippetThumbnailContainer } from '../atoms/SnippetLarge'; -import { createLink } from '../utility/create-link'; -import { HrefLink } from '../utility/href-link'; -import { LocaleString } from './LocaleString'; +import { SearchResult } from '../../../../types/search'; +import { parseUrn } from '../../../../utility/parse-urn'; +import { useRouteContext } from '../../hooks/use-route-context'; +import { CroppedImage } from '../../../shared/atoms/Images'; +import { ImageStripBox } from '../../../shared/atoms/ImageStrip'; +import { GridContainer } from '../../../shared/layout/Grid'; +import { SnippetThumbnail, SnippetThumbnailContainer } from '../../../shared/atoms/SnippetLarge'; +import { createLink } from '../../../shared/utility/create-link'; +import { HrefLink } from '../../../shared/utility/href-link'; +import { LocaleString } from '../../../shared/components/LocaleString'; const ResultsContainer = styled.div<{ $isFetching?: boolean }>` - flex: 1 1 0px; + flex: 1 1 0; transition: opacity 0.2s; ${props => @@ -104,7 +104,7 @@ const SearchItem: React.FC<{ result: SearchResult; size?: 'large' | 'small'; sea {isManifest ? ( - + void | Promise }> = ({ task, diff --git a/services/madoc-ts/src/frontend/site/features/tasks/ContinueTaskDisplay.tsx b/services/madoc-ts/src/frontend/site/features/tasks/ContinueTaskDisplay.tsx new file mode 100644 index 000000000..6af2aa1f6 --- /dev/null +++ b/services/madoc-ts/src/frontend/site/features/tasks/ContinueTaskDisplay.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SubjectSnippet as SubjectSnippetType } from '../../../../extensions/tasks/resolvers/subject-resolver'; +import { BaseTask } from '../../../../gateway/tasks/base-task'; +import { useContinueContribution } from '../../../shared/hooks/use-continue-contribution'; +import { useTaskMetadata } from '../../hooks/use-task-metadata'; +import { SnippetLarge } from '../../../shared/atoms/SnippetLarge'; +import { useCreateLocaleString } from '../../../shared/components/LocaleString'; + +export function ContinueTaskDisplay({ + task, + next, + manifestModel, +}: { + task: BaseTask; + next?: boolean; + manifestModel?: boolean; +}) { + const { subject } = useTaskMetadata<{ subject?: SubjectSnippetType }>(task); + const link = useContinueContribution(task, manifestModel); + const createLocalString = useCreateLocaleString(); + const { t } = useTranslation(); + + if (!subject || !link) { + return null; + } + + return ( + + ); +} diff --git a/services/madoc-ts/src/frontend/shared/components/RevisionList.tsx b/services/madoc-ts/src/frontend/site/features/tasks/RevisionList.tsx similarity index 84% rename from services/madoc-ts/src/frontend/shared/components/RevisionList.tsx rename to services/madoc-ts/src/frontend/site/features/tasks/RevisionList.tsx index 193a120a2..0024bf9a4 100644 --- a/services/madoc-ts/src/frontend/shared/components/RevisionList.tsx +++ b/services/madoc-ts/src/frontend/site/features/tasks/RevisionList.tsx @@ -1,17 +1,13 @@ -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import styled, { css } from 'styled-components'; -import { useNavigation } from '../capture-models/editor/hooks/useNavigation'; -import { Revisions } from '../capture-models/editor/stores/revisions/index'; -import { StatusTypes } from '../capture-models/types/capture-model'; -import { RevisionRequest } from '../capture-models/types/revision-request'; -import { useBrowserLayoutEffect } from '../hooks/use-browser-layout-effect'; -import { SmallButton } from '../navigation/Button'; -import { ViewDocument } from '../capture-models/inspector/ViewDocument'; -import { useContributors } from '../capture-models/new/components/ContributorContext'; -import { useSelectorHelper } from '../capture-models/editor/stores/selectors/selector-helper'; -import { resolveSelector } from '../capture-models/helpers/resolve-selector'; -import { useDecayState } from '../hooks/use-decay-state'; -import { useApiCaptureModel } from '../hooks/use-api-capture-model'; +import { useNavigation } from '../../../shared/capture-models/editor/hooks/useNavigation'; +import { Revisions } from '../../../shared/capture-models/editor/stores/revisions'; +import { StatusTypes } from '../../../shared/capture-models/types/capture-model'; +import { RevisionRequest } from '../../../shared/capture-models/types/revision-request'; +import { useBrowserLayoutEffect } from '../../../shared/hooks/use-browser-layout-effect'; +import { SmallButton } from '../../../shared/navigation/Button'; +import { ViewDocument } from '../../../shared/capture-models/inspector/ViewDocument'; +import { useContributors } from '../../../shared/capture-models/new/components/ContributorContext'; const RevisionListContainer = styled.div` padding: 10px; diff --git a/services/madoc-ts/src/frontend/shared/components/TaskContextualMenu.tsx b/services/madoc-ts/src/frontend/site/features/tasks/TaskContextualMenu.tsx similarity index 86% rename from services/madoc-ts/src/frontend/shared/components/TaskContextualMenu.tsx rename to services/madoc-ts/src/frontend/site/features/tasks/TaskContextualMenu.tsx index 97056740e..ab3d52d0e 100644 --- a/services/madoc-ts/src/frontend/shared/components/TaskContextualMenu.tsx +++ b/services/madoc-ts/src/frontend/site/features/tasks/TaskContextualMenu.tsx @@ -1,18 +1,18 @@ import React from 'react'; import useDropdownMenu from 'react-accessible-dropdown-menu-hook'; import { useMutation } from 'react-query'; -import { BaseTask } from '../../../gateway/tasks/base-task'; +import { BaseTask } from '../../../../gateway/tasks/base-task'; import { ContextualLabel, ContextualMenuList, ContextualMenuListItem, ContextualMenuWrapper, ContextualPositionWrapper, -} from '../navigation/ContextualMenu'; -import { useApi } from '../hooks/use-api'; -import { SettingsIcon } from '../icons/SettingsIcon'; +} from '../../../shared/navigation/ContextualMenu'; +import { useApi } from '../../../shared/hooks/use-api'; +import { SettingsIcon } from '../../../shared/icons/SettingsIcon'; import { AssignTask } from './AssignTask'; -import { ModalButton } from './Modal'; +import { ModalButton } from '../../../shared/components/Modal'; export const TaskContextualMenu: React.FC<{ task: BaseTask; refetch?: () => Promise }> = ({ task, refetch }) => { const { isOpen, buttonProps, itemProps } = useDropdownMenu(9); diff --git a/services/madoc-ts/src/frontend/site/features/TaskFilterStatuses.tsx b/services/madoc-ts/src/frontend/site/features/tasks/TaskFilterStatuses.tsx similarity index 73% rename from services/madoc-ts/src/frontend/site/features/TaskFilterStatuses.tsx rename to services/madoc-ts/src/frontend/site/features/tasks/TaskFilterStatuses.tsx index 4a255e352..7386c9ceb 100644 --- a/services/madoc-ts/src/frontend/site/features/TaskFilterStatuses.tsx +++ b/services/madoc-ts/src/frontend/site/features/tasks/TaskFilterStatuses.tsx @@ -1,15 +1,19 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { ItemFilter } from '../../shared/components/ItemFilter'; -import { useLocationQuery } from '../../shared/hooks/use-location-query'; -import { useGoToQuery } from '../hooks/use-go-to-query'; +import { ItemFilter } from '../../../shared/components/ItemFilter'; +import { useLocationQuery } from '../../../shared/hooks/use-location-query'; +import { useGoToQuery } from '../../hooks/use-go-to-query'; export const TaskFilterStatuses: React.FC<{ statuses: Array<{ label: any; value: number }> }> = ({ statuses: allStatuses, }) => { const { t } = useTranslation(); const { page, ...query } = useLocationQuery(); - const statuses = query.status ? query.status.split(',').map(Number) : []; + const statuses = query.status + ? Array.isArray(query.status) + ? query.status.map(Number) + : query.status.split(',').map(Number) + : []; const goToQuery = useGoToQuery(); const realLabel = statuses.length === 1 ? allStatuses.find(ty => ty.value && ty.value === statuses[0])?.label : ''; diff --git a/services/madoc-ts/src/frontend/site/features/TaskFilterType.tsx b/services/madoc-ts/src/frontend/site/features/tasks/TaskFilterType.tsx similarity index 88% rename from services/madoc-ts/src/frontend/site/features/TaskFilterType.tsx rename to services/madoc-ts/src/frontend/site/features/tasks/TaskFilterType.tsx index ccc131d94..9468dd15f 100644 --- a/services/madoc-ts/src/frontend/site/features/TaskFilterType.tsx +++ b/services/madoc-ts/src/frontend/site/features/tasks/TaskFilterType.tsx @@ -2,8 +2,8 @@ import { stringify } from 'query-string'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import { ItemFilter } from '../../shared/components/ItemFilter'; -import { useLocationQuery } from '../../shared/hooks/use-location-query'; +import { ItemFilter } from '../../../shared/components/ItemFilter'; +import { useLocationQuery } from '../../../shared/hooks/use-location-query'; export const TaskFilterType: React.FC<{ types: Array<{ label: any; value: string }> }> = ({ types: allTypes }) => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/shared/components/TaskHeader.tsx b/services/madoc-ts/src/frontend/site/features/tasks/TaskHeader.tsx similarity index 82% rename from services/madoc-ts/src/frontend/shared/components/TaskHeader.tsx rename to services/madoc-ts/src/frontend/site/features/tasks/TaskHeader.tsx index 2d6ab17a0..62edb0c2e 100644 --- a/services/madoc-ts/src/frontend/shared/components/TaskHeader.tsx +++ b/services/madoc-ts/src/frontend/site/features/tasks/TaskHeader.tsx @@ -1,27 +1,17 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { BaseTask } from '../../../gateway/tasks/base-task'; -import { extractIdFromUrn } from '../../../utility/parse-urn'; -import { useTaskMetadata } from '../../site/hooks/use-task-metadata'; -import { useProjectByTask } from '../hooks/use-project-by-task'; -import { - ContextualLabel, - ContextualMenuList, - ContextualMenuListItem, - ContextualMenuWrapper, - ContextualPositionWrapper, -} from '../navigation/ContextualMenu'; -import { StatusIcon, StatusWrapper } from '../atoms/Status'; -import { SettingsIcon } from '../icons/SettingsIcon'; -import { createLink } from '../utility/create-link'; -import { HrefLink } from '../utility/href-link'; -import { ModalButton } from './Modal'; +import { BaseTask } from '../../../../gateway/tasks/base-task'; +import { extractIdFromUrn } from '../../../../utility/parse-urn'; +import { useTaskMetadata } from '../../hooks/use-task-metadata'; +import { useProjectByTask } from '../../../shared/hooks/use-project-by-task'; +import { StatusIcon, StatusWrapper } from '../../../shared/atoms/Status'; +import { createLink } from '../../../shared/utility/create-link'; +import { HrefLink } from '../../../shared/utility/href-link'; import { TaskContextualMenu } from './TaskContextualMenu'; -import { UserAutocomplete } from './UserAutocomplete'; -import { TimeAgo } from '../atoms/TimeAgo'; -import { useBots } from '../hooks/use-bots'; -import { Tag } from '../capture-models/editor/atoms/Tag'; +import { TimeAgo } from '../../../shared/atoms/TimeAgo'; +import { useBots } from '../../../shared/hooks/use-bots'; +import { Tag } from '../../../shared/capture-models/editor/atoms/Tag'; const TaskHeaderContainer = styled.div` background: #fff; diff --git a/services/madoc-ts/src/frontend/site/features/TaskListItem.tsx b/services/madoc-ts/src/frontend/site/features/tasks/TaskListItem.tsx similarity index 74% rename from services/madoc-ts/src/frontend/site/features/TaskListItem.tsx rename to services/madoc-ts/src/frontend/site/features/tasks/TaskListItem.tsx index 8cb78dbb7..cb40e3663 100644 --- a/services/madoc-ts/src/frontend/site/features/TaskListItem.tsx +++ b/services/madoc-ts/src/frontend/site/features/tasks/TaskListItem.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { BaseTask } from '../../../gateway/tasks/base-task'; -import { extractIdFromUrn } from '../../../utility/parse-urn'; -import { LocaleString } from '../../shared/components/LocaleString'; -import { TaskItem } from '../../shared/components/TaskItem'; -import { useTaskMetadata } from '../hooks/use-task-metadata'; -import { useBots } from '../../shared/hooks/use-bots'; +import { BaseTask } from '../../../../gateway/tasks/base-task'; +import { extractIdFromUrn } from '../../../../utility/parse-urn'; +import { LocaleString } from '../../../shared/components/LocaleString'; +import { TaskItem } from '../../../shared/components/TaskItem'; +import { useTaskMetadata } from '../../hooks/use-task-metadata'; +import { useBots } from '../../../shared/hooks/use-bots'; export const TaskListItem: React.FC<{ task: BaseTask; onClick: () => void; selected?: boolean }> = ({ task, diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ContributorTasks.tsx b/services/madoc-ts/src/frontend/site/features/userDash/ContributionsTasks.tsx similarity index 98% rename from services/madoc-ts/src/frontend/site/features/contributor/ContributorTasks.tsx rename to services/madoc-ts/src/frontend/site/features/userDash/ContributionsTasks.tsx index d1caa8548..4d4010121 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ContributorTasks.tsx +++ b/services/madoc-ts/src/frontend/site/features/userDash/ContributionsTasks.tsx @@ -14,7 +14,7 @@ import { isContributor } from '../../../shared/utility/user-roles'; import { useRelativeLinks } from '../../hooks/use-relative-links'; import { useUserHomepage } from '../../hooks/use-user-homepage'; -export const ContributorTasks: React.FC = () => { +export const ContributionsTasks: React.FC = () => { const { t } = useTranslation(); const { data } = useUserHomepage(); const createLink = useRelativeLinks(); diff --git a/services/madoc-ts/src/frontend/site/features/reviewer/ReviewerTasks.tsx b/services/madoc-ts/src/frontend/site/features/userDash/ReviewerTasksTable.tsx similarity index 97% rename from services/madoc-ts/src/frontend/site/features/reviewer/ReviewerTasks.tsx rename to services/madoc-ts/src/frontend/site/features/userDash/ReviewerTasksTable.tsx index b89217061..16f5600bd 100644 --- a/services/madoc-ts/src/frontend/site/features/reviewer/ReviewerTasks.tsx +++ b/services/madoc-ts/src/frontend/site/features/userDash/ReviewerTasksTable.tsx @@ -10,7 +10,7 @@ import { isReviewer } from '../../../shared/utility/user-roles'; import { useRelativeLinks } from '../../hooks/use-relative-links'; import { useUserHomepage } from '../../hooks/use-user-homepage'; -export const ReviewerTasks: React.FC = () => { +export const ReviewerTasksTable: React.FC = () => { const { data } = useUserHomepage(); const { t } = useTranslation(); const createLink = useRelativeLinks(); diff --git a/services/madoc-ts/src/frontend/site/features/UserProjects.tsx b/services/madoc-ts/src/frontend/site/features/userDash/UserProjects.tsx similarity index 63% rename from services/madoc-ts/src/frontend/site/features/UserProjects.tsx rename to services/madoc-ts/src/frontend/site/features/userDash/UserProjects.tsx index a5721f83d..63d3382ad 100644 --- a/services/madoc-ts/src/frontend/site/features/UserProjects.tsx +++ b/services/madoc-ts/src/frontend/site/features/userDash/UserProjects.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { TinyButton } from '../../shared/navigation/Button'; -import { Heading3 } from '../../shared/typography/Heading3'; -import { ProjectListing } from '../../shared/atoms/ProjectListing'; -import { HrefLink } from '../../shared/utility/href-link'; -import { useUserHomepage } from '../hooks/use-user-homepage'; +import { TinyButton } from '../../../shared/navigation/Button'; +import { Heading3 } from '../../../shared/typography/Heading3'; +import { ProjectListing } from '../../../shared/atoms/ProjectListing'; +import { HrefLink } from '../../../shared/utility/href-link'; +import { useUserHomepage } from '../../hooks/use-user-homepage'; export const UserProjects: React.FC = () => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/site/features/contributor/UserStatistics.tsx b/services/madoc-ts/src/frontend/site/features/userDash/UserStatistics.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/contributor/UserStatistics.tsx rename to services/madoc-ts/src/frontend/site/features/userDash/UserStatistics.tsx diff --git a/services/madoc-ts/src/frontend/site/features/admin/AddSubpageButton.tsx b/services/madoc-ts/src/frontend/site/features/viewPage/AddSubpageButton.tsx similarity index 94% rename from services/madoc-ts/src/frontend/site/features/admin/AddSubpageButton.tsx rename to services/madoc-ts/src/frontend/site/features/viewPage/AddSubpageButton.tsx index ccfece989..5a1dc78dc 100644 --- a/services/madoc-ts/src/frontend/site/features/admin/AddSubpageButton.tsx +++ b/services/madoc-ts/src/frontend/site/features/viewPage/AddSubpageButton.tsx @@ -43,7 +43,7 @@ export const AddSubpageButton: React.FC<{ onCreate?: (page: SitePage) => void }> (subpage.current = req)} />} + render={() => (subpage.current = req)} />} footerAlignRight renderFooter={({ close }) => { return ( diff --git a/services/madoc-ts/src/frontend/site/features/admin/DeletePageButton.tsx b/services/madoc-ts/src/frontend/site/features/viewPage/DeletePageButton.tsx similarity index 98% rename from services/madoc-ts/src/frontend/site/features/admin/DeletePageButton.tsx rename to services/madoc-ts/src/frontend/site/features/viewPage/DeletePageButton.tsx index ed3ab6d2d..a7f734d27 100644 --- a/services/madoc-ts/src/frontend/site/features/admin/DeletePageButton.tsx +++ b/services/madoc-ts/src/frontend/site/features/viewPage/DeletePageButton.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import { useNavigate } from 'react-router-dom'; diff --git a/services/madoc-ts/src/frontend/site/features/admin/EditPageLayoutButton.tsx b/services/madoc-ts/src/frontend/site/features/viewPage/EditPageLayoutButton.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/admin/EditPageLayoutButton.tsx rename to services/madoc-ts/src/frontend/site/features/viewPage/EditPageLayoutButton.tsx diff --git a/services/madoc-ts/src/frontend/site/features/admin/EditPageMetadataButton.tsx b/services/madoc-ts/src/frontend/site/features/viewPage/EditPageMetadataButton.tsx similarity index 89% rename from services/madoc-ts/src/frontend/site/features/admin/EditPageMetadataButton.tsx rename to services/madoc-ts/src/frontend/site/features/viewPage/EditPageMetadataButton.tsx index cc1200ea8..dc4d2df9a 100644 --- a/services/madoc-ts/src/frontend/site/features/admin/EditPageMetadataButton.tsx +++ b/services/madoc-ts/src/frontend/site/features/viewPage/EditPageMetadataButton.tsx @@ -39,14 +39,7 @@ export const EditPageMetadataButton: React.FC<{ onUpdate?: (page: SitePage) => v ( - (newPage.current = req)} - /> - )} + render={() => (newPage.current = req)} />} footerAlignRight renderFooter={({ close }) => { return ( diff --git a/services/madoc-ts/src/frontend/site/features/admin/PageEditorBar.tsx b/services/madoc-ts/src/frontend/site/features/viewPage/PageEditorBar.tsx similarity index 100% rename from services/madoc-ts/src/frontend/site/features/admin/PageEditorBar.tsx rename to services/madoc-ts/src/frontend/site/features/viewPage/PageEditorBar.tsx diff --git a/services/madoc-ts/src/frontend/site/features/StaticPage.tsx b/services/madoc-ts/src/frontend/site/features/viewPage/StaticPage.tsx similarity index 78% rename from services/madoc-ts/src/frontend/site/features/StaticPage.tsx rename to services/madoc-ts/src/frontend/site/features/viewPage/StaticPage.tsx index eb98ea830..490536c4d 100644 --- a/services/madoc-ts/src/frontend/site/features/StaticPage.tsx +++ b/services/madoc-ts/src/frontend/site/features/viewPage/StaticPage.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; -import { useApi } from '../../shared/hooks/use-api'; -import { useData } from '../../shared/hooks/use-data'; -import { SlotProvider } from '../../shared/page-blocks/slot-context'; -import { PageLoader } from '../pages/loaders/page-loader'; -import { useSiteConfiguration } from './SiteConfigurationContext'; +import { useApi } from '../../../shared/hooks/use-api'; +import { useData } from '../../../shared/hooks/use-data'; +import { SlotProvider } from '../../../shared/page-blocks/slot-context'; +import { PageLoader } from '../../pages/loaders/page-loader'; +import { useSiteConfiguration } from '../SiteConfigurationContext'; export const StaticPage: React.FC<{ layout?: string; title: string }> = ({ title, layout = 'none', children }) => { const { pathname } = useLocation(); diff --git a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/browser-panel.tsx b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/browser-panel.tsx new file mode 100644 index 000000000..930deafaa --- /dev/null +++ b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/browser-panel.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSite } from '../../../shared/hooks/use-site'; +import { CallMergeIcon } from '../../../shared/icons/CallMergeIcon'; +import { Spinner } from '../../../shared/icons/Spinner'; +import { SplitIcon } from '../../../shared/icons/SplitIcon'; +import { TranscriptionIcon } from '../../../shared/icons/TranscriptionIcon'; +import { IIIFExplorer } from '../../../shared/viewers/iiif-explorer'; +import { useSiteConfiguration } from '../../features/SiteConfigurationContext'; +import { useProject } from '../use-project'; +import { useRouteContext } from '../use-route-context'; +import { CanvasMenuHook } from './types'; + +export function useBrowserPanel(): CanvasMenuHook { + const { projectId } = useRouteContext(); + const { t } = useTranslation(); + const { data: project } = useProject(); + const config = useSiteConfiguration(); + const collectionId = project?.collection_id; + const site = useSite(); + const origin = typeof window === 'undefined' ? '' : window.location.origin; + const url = collectionId ? `${origin}/s/${site.slug}/madoc/api/collections/${collectionId}/export/3.0` : null; + const enabled = config.project.modelPageOptions?.enableSplitView; + + const content = url ? ( + <> + + + ) : ( + + ); + + return { + id: 'split-view', + label: t('Split view'), + icon: , + notifications: 0, + isLoaded: true, + isHidden: !projectId || !enabled, + content, + }; +} diff --git a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/document-panel.tsx b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/document-panel.tsx index ee7f1e258..6f6933a1e 100644 --- a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/document-panel.tsx +++ b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/document-panel.tsx @@ -27,8 +27,11 @@ export function useDocumentPanel(): CanvasMenuHook { .flatMap(b => b) .map((f: any) => f.properties && Object.values(f.properties)); - const emptyFlat = nestedItems.filter(x => x).flatMap(a => a).flatMap(b => b.filter((f: any) => f.value)); - const emptyFields = flatProperties.flatMap(c => c).filter((f: any) => f.value); + const emptyFlat = nestedItems + .filter(x => x) + .flatMap(a => a) + .flatMap(b => b.filter((f: any) => f.value !== undefined)); + const emptyFields = flatProperties.flatMap(c => c).filter((f: any) => f.value !== undefined); const isApproved = model.revisions.filter((q: { approved: boolean }) => q.approved); return flatProperties.length > 0 && isApproved.length > 0 && (emptyFlat.length > 0 || emptyFields.length > 0); }) diff --git a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/metadata-panel.tsx b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/metadata-panel.tsx index 70c5248d0..e5778d731 100644 --- a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/metadata-panel.tsx +++ b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/metadata-panel.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useData, usePaginatedData } from '../../../shared/hooks/use-data'; import { InfoIcon } from '../../../shared/icons/InfoIcon'; -import { CanvasMetadata } from '../../features/CanvasMetadata'; -import { ManifestMetadata } from '../../features/ManifestMetadata'; +import { CanvasMetadata } from '../../features/canvas/CanvasMetadata'; +import { ManifestMetadata } from '../../blocks/ManifestMetadata'; import { CanvasLoader } from '../../pages/loaders/canvas-loader'; import { ManifestLoader } from '../../pages/loaders/manifest-loader'; import { useRouteContext } from '../use-route-context'; diff --git a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/personal-notes.tsx b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/personal-notes.tsx index 4623f114d..78fd2b25f 100644 --- a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/personal-notes.tsx +++ b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/personal-notes.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import styled from 'styled-components'; import { Button } from '../../../shared/navigation/Button'; -import { InputContainer, InputLabel } from '../../../shared/form/Input'; +import { InputContainer } from '../../../shared/form/Input'; import { LockIcon } from '../../../shared/icons/LockIcon'; import { useApi } from '../../../shared/hooks/use-api'; import { apiHooks } from '../../../shared/hooks/use-api-query'; @@ -14,7 +14,7 @@ import { CanvasMenuHook } from './types'; import { StyledFormMultilineInputElement } from '../../../shared/capture-models/editor/atoms/StyledForm'; const NoteContainer = styled.div` - padding: 0.5em; + padding: 1em; `; export function usePersonalNotesMenu(): CanvasMenuHook { @@ -48,8 +48,7 @@ export function usePersonalNotesMenu(): CanvasMenuHook { <> {data ? ( - - {t('Personal notes')} + { - const reviews = canvasTask?.userTasks - ? canvasTask.userTasks.filter( - task => (task as BaseTask).type === 'crowdsourcing-review' && (task.status === 2 || task.status === 1) - ) - : []; - const userTasks = canvasTask ? canvasTask.userTasks : undefined; const userContributions = (userTasks || []).filter( task => task.type === 'crowdsourcing-task' && task.status !== -1 ); - const completedAndHide = !config.project.allowSubmissionsWhenCanvasComplete && canvasTask?.canvasTask?.status === 3; - const completed = canvasTask?.canvasTask?.status === 3; + + const canContribute = + user && + (scope.indexOf('site.admin') === -1 || + scope.indexOf('models.contribute') === -1 || + scope.indexOf('models.admin') === -1); + + const canClaimCanvas = + user && (config.project.claimGranularity ? config.project.claimGranularity === 'canvas' : true); const maxContributors = canvasTask?.maxContributors && canvasTask.totalContributors @@ -71,13 +71,32 @@ export function useCanvasUserTasks() { // if max contributors reached check that the current user isnt one of them const maxContributorsReached = maxContributors ? !userTasks?.some(t => t.type === 'crowdsourcing-task') : false; - const canClaimCanvas = - user && (config.project.claimGranularity ? config.project.claimGranularity === 'canvas' : true); const canUserSubmit = user && !!canvasTask?.canUserSubmit; - const canContribute = user && (scope.indexOf('site.admin') !== -1 || scope.indexOf('models.contribute') !== -1); + + const canSubmitAfterRejection = config.project.modelPageOptions?.preventContributionAfterRejection + ? !userTasks?.some(task => task.status === -1) + : true; + + const canSubmitAfterSubmission = config.project.modelPageOptions?.preventContributionAfterSubmission + ? !userContributions?.some(task => task.status === 2) + : true; + + const canSubmitMultiple = config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource + ? !userContributions || userContributions.length === 0 || userContributions?.some(task => task.status === 1) + : true; + const allTasksDone = userContributions.length ? !userContributions.find(t => t.status === 0 || t.status === 1) : false; + + const completedAndHide = !config.project.allowSubmissionsWhenCanvasComplete && canvasTask?.canvasTask?.status === 3; + const completed = canvasTask?.canvasTask?.status === 3; + + const canCanvasTakeSubmission = canClaimCanvas && !completedAndHide && !maxContributorsReached; + const canSubmitAnother = canSubmitMultiple && canSubmitAfterRejection && canSubmitAfterSubmission; + + const preventFurtherSubmission = !canCanvasTakeSubmission || !canSubmitAnother || !(canContribute && canUserSubmit); + const markedAsUnusable = allTasksDone && (userContributions.length @@ -88,30 +107,31 @@ export function useCanvasUserTasks() { user, canvasTask: canvasTask?.canvasTask, isLoading, - reviews, userTasks, markedAsUnusable, - isManifestComplete: canvasTask?.isManifestComplete, - allTasksDone, + canCanvasTakeSubmission, + canUserSubmit, completedAndHide, completed, canClaimCanvas, - canUserSubmit, canContribute, - maxContributorsReached, updateClaim, updatedAt, refetch, + preventFurtherSubmission, }; }, [ + canvasTask, + config.project.allowSubmissionsWhenCanvasComplete, + config.project.claimGranularity, + config.project.modelPageOptions?.preventContributionAfterRejection, + config.project.modelPageOptions?.preventContributionAfterSubmission, + config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource, user, scope, isLoading, - canvasTask, - refetch, - updatedAt, updateClaim, - config.project.allowSubmissionsWhenCanvasComplete, - config.project.claimGranularity, + updatedAt, + refetch, ]); } diff --git a/services/madoc-ts/src/frontend/site/hooks/use-continue-submission.ts b/services/madoc-ts/src/frontend/site/hooks/use-continue-submission.ts index de4ec73e0..88c7765da 100644 --- a/services/madoc-ts/src/frontend/site/hooks/use-continue-submission.ts +++ b/services/madoc-ts/src/frontend/site/hooks/use-continue-submission.ts @@ -1,22 +1,19 @@ import { useMemo } from 'react'; -import { useUser } from '../../shared/hooks/use-site'; import { useSiteConfiguration } from '../features/SiteConfigurationContext'; import { useCanvasUserTasks } from './use-canvas-user-tasks'; import { useManifestUserTasks } from './use-manifest-user-tasks'; export function useContinueSubmission() { const canvasTasks = useCanvasUserTasks(); - const manifestTasks = useManifestUserTasks(); + const { user, userTasks } = useManifestUserTasks(); const config = useSiteConfiguration(); - const user = useUser(); return useMemo(() => { let inProgress = 0; let completed = 0; let assigned = 0; - const tasks = - config.project.contributionMode === 'transcription' ? canvasTasks?.userTasks : manifestTasks.userTasks; + const tasks = config.project.contributionMode === 'transcription' ? canvasTasks?.userTasks : userTasks; const allModels = tasks && tasks.length @@ -48,11 +45,5 @@ export function useContinueSubmission() { completed, loaded: !!tasks, }; - }, [ - canvasTasks?.userTasks, - config.project.claimGranularity, - config.project.contributionMode, - manifestTasks.userTasks, - user, - ]); + }, [canvasTasks?.userTasks, config.project.claimGranularity, config.project.contributionMode, userTasks, user]); } diff --git a/services/madoc-ts/src/frontend/site/hooks/use-manifest-user-tasks.ts b/services/madoc-ts/src/frontend/site/hooks/use-manifest-user-tasks.ts index 5791e144a..b6ed177f2 100644 --- a/services/madoc-ts/src/frontend/site/hooks/use-manifest-user-tasks.ts +++ b/services/madoc-ts/src/frontend/site/hooks/use-manifest-user-tasks.ts @@ -1,15 +1,16 @@ import { useMemo } from 'react'; import { useMutation } from 'react-query'; -import { BaseTask } from '../../../gateway/tasks/base-task'; import { RevisionRequest } from '../../shared/capture-models/types/revision-request'; import { useApi } from '../../shared/hooks/use-api'; import { useInvalidateAfterSubmission } from './use-invalidate-after-submission'; import { useProjectManifestTasks } from './use-project-manifest-tasks'; import { RouteContext } from './use-route-context'; +import { useSiteConfiguration } from '../features/SiteConfigurationContext'; const defaultScope: any[] = []; export function useManifestUserTasks() { const invalidate = useInvalidateAfterSubmission(); + const config = useSiteConfiguration(); const api = useApi(); const { user, scope = defaultScope } = api.getIsServer() ? { user: undefined } : api.getCurrentUser() || {}; const { data: manifestTask, isLoading, refetch, updatedAt } = useProjectManifestTasks(); @@ -46,33 +47,63 @@ export function useManifestUserTasks() { ); return useMemo(() => { - const reviews = manifestTask?.userTasks - ? manifestTask.userTasks.filter( - task => (task as BaseTask).type === 'crowdsourcing-review' && (task.status === 2 || task.status === 1) - ) - : []; - const userTasks = manifestTask ? manifestTask.userTasks : undefined; const userContributions = (userTasks || []).filter( task => task.type === 'crowdsourcing-task' && task.status !== -1 ); - const completedAndHide = manifestTask?.manifestTask?.status === 3; + + const canContribute = + user && + (scope.indexOf('site.admin') === -1 || + scope.indexOf('models.contribute') === -1 || + scope.indexOf('models.admin') === -1); + + const maxContributors = + manifestTask?.maxContributors && manifestTask.totalContributors + ? manifestTask.maxContributors >= manifestTask.totalContributors + : false; + + // if max contributors reached check that the current user isnt one of them + const maxContributorsReached = maxContributors ? !userTasks?.some(t => t.type === 'crowdsourcing-task') : false; + const canUserSubmit = user && !!manifestTask?.canUserSubmit; - const canContribute = user && (scope.indexOf('site.admin') !== -1 || scope.indexOf('models.contribute') !== -1); + + const canSubmitAfterRejection = config.project.modelPageOptions?.preventContributionAfterRejection + ? !userTasks?.some(task => task.status === -1) + : true; + + const canSubmitAfterSubmission = config.project.modelPageOptions?.preventContributionAfterSubmission + ? !userContributions?.some(task => task.status === 2) + : true; + + const canSubmitMultiple = config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource + ? !userContributions || userContributions.length === 0 || userContributions?.some(task => task.status === 1) + : true; + const allTasksDone = userContributions.length ? !userContributions.find(t => t.status === 0 || t.status === 1) : false; + + const completedAndHide = manifestTask?.manifestTask?.status === 3; + + const canManifestTakeSubmission = !completedAndHide && !maxContributorsReached; + const canSubmitAnother = canSubmitMultiple && canSubmitAfterRejection && canSubmitAfterSubmission; + + const preventFurtherSubmission = + !canManifestTakeSubmission || !canSubmitAnother || !(canContribute && canUserSubmit); + const markedAsUnusable = allTasksDone && (userContributions.length ? !!userContributions.find(t => (t.status === 2 || t.status === 3) && !t.state.revisionId) : false); + const submittedTasks = userContributions.find(t => t.status === 2); + return { user, isLoading, manifestTask: manifestTask?.manifestTask, - reviews, userTasks, markedAsUnusable, isManifestComplete: manifestTask?.isManifestComplete, @@ -83,6 +114,19 @@ export function useManifestUserTasks() { updateClaim, updatedAt, refetch, + preventFurtherSubmission, + submittedTasks, }; - }, [manifestTask, user, scope, isLoading, updateClaim, updatedAt, refetch]); + }, [ + manifestTask, + user, + scope, + config.project.modelPageOptions?.preventContributionAfterRejection, + config.project.modelPageOptions?.preventContributionAfterSubmission, + config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource, + isLoading, + updateClaim, + updatedAt, + refetch, + ]); } diff --git a/services/madoc-ts/src/frontend/site/hooks/use-project-assignee-stats.ts b/services/madoc-ts/src/frontend/site/hooks/use-project-assignee-stats.ts new file mode 100644 index 000000000..daaca8faa --- /dev/null +++ b/services/madoc-ts/src/frontend/site/hooks/use-project-assignee-stats.ts @@ -0,0 +1,16 @@ +import { useApi } from '../../shared/hooks/use-api'; +import { useRouteContext } from './use-route-context'; +import { useQuery } from 'react-query'; + +export function useProjectAssigneeStats() { + const api = useApi(); + const { projectId } = useRouteContext(); + + const assigneeStats = useQuery(['project-task-stats', { projectId }], async () => { + if (projectId) { + return api.getSiteProjectAssigneeTasks(projectId); + } + }); + + return assigneeStats; +} diff --git a/services/madoc-ts/src/frontend/site/hooks/use-project-updates-list.ts b/services/madoc-ts/src/frontend/site/hooks/use-project-updates-list.ts new file mode 100644 index 000000000..20bdee809 --- /dev/null +++ b/services/madoc-ts/src/frontend/site/hooks/use-project-updates-list.ts @@ -0,0 +1,16 @@ +import { useApi } from '../../shared/hooks/use-api'; +import { useRouteContext } from './use-route-context'; +import { usePaginatedQuery } from 'react-query'; +import { useLocationQuery } from '../../shared/hooks/use-location-query'; + +export function useProjectUpdatesList() { + const api = useApi(); + const { page = 1 } = useLocationQuery(); + const { projectId } = useRouteContext(); + + return usePaginatedQuery(['site-project-update-list', { projectId, page }], async () => { + if (projectId) { + return api.getAllSiteProjectUpdates(projectId, page); + } + }); +} diff --git a/services/madoc-ts/src/frontend/site/hooks/use-search.ts b/services/madoc-ts/src/frontend/site/hooks/use-search.ts index e81eb45b9..8628c21e4 100644 --- a/services/madoc-ts/src/frontend/site/hooks/use-search.ts +++ b/services/madoc-ts/src/frontend/site/hooks/use-search.ts @@ -1,6 +1,6 @@ import { InternationalString } from '@iiif/presentation-3'; import { useMemo } from 'react'; -import { FacetConfig } from '../../shared/components/MetadataFacetEditor'; +import { FacetConfig } from '../../shared/features/MetadataFacetEditor'; import { apiHooks, paginatedApiHooks } from '../../shared/hooks/use-api-query'; import { useSiteConfiguration } from '../features/SiteConfigurationContext'; import { useRouteContext } from './use-route-context'; diff --git a/services/madoc-ts/src/frontend/site/index.css b/services/madoc-ts/src/frontend/site/index.css new file mode 100644 index 000000000..326bf316f --- /dev/null +++ b/services/madoc-ts/src/frontend/site/index.css @@ -0,0 +1,50 @@ +@tailwind base; + +@layer base { + :root { + --color-primary: 74 103 228; + } + + h1 { + @apply text-3xl; + } + h2 { + @apply text-xl my-5; + } + h3 { + @apply text-lg my-3; + } + a { + @apply text-gray-700 underline; + } +} + +@tailwind components; + +@layer components { + .grid-t-sm { + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + } + .grid-t-md { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + } + .grid-t-lg { + grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); + } + +} + +@tailwind utilities; + + + +/** Highlight all styled components */ +/* +*[class*="-sc-"]:hover { + outline: 3px solid red !important; +} + +*[class*="-sc-"] { + outline: 3px solid #eee !important; +} +*/ diff --git a/services/madoc-ts/src/frontend/site/index.tsx b/services/madoc-ts/src/frontend/site/index.tsx index f94ee54b7..f158edb98 100644 --- a/services/madoc-ts/src/frontend/site/index.tsx +++ b/services/madoc-ts/src/frontend/site/index.tsx @@ -10,6 +10,7 @@ import { VaultProvider } from 'react-iiif-vault'; import '../shared/capture-models/plugins'; import { RouteObject } from 'react-router-dom'; import { SiteProvider } from '../shared/hooks/use-site'; +import './index.css'; export type SiteAppProps = { jwt?: string; diff --git a/services/madoc-ts/src/frontend/site/pages/Item-not-found.tsx b/services/madoc-ts/src/frontend/site/pages/Item-not-found.tsx index 20275ccb5..96f5ab3f6 100644 --- a/services/madoc-ts/src/frontend/site/pages/Item-not-found.tsx +++ b/services/madoc-ts/src/frontend/site/pages/Item-not-found.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Heading1 } from '../../shared/typography/Heading1'; import { Slot } from '../../shared/page-blocks/slot'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { useRouteContext } from '../hooks/use-route-context'; export const ItemNotFound: React.FC = () => { diff --git a/services/madoc-ts/src/frontend/site/pages/all-collections.tsx b/services/madoc-ts/src/frontend/site/pages/all-collections.tsx index db617ddb1..aaed11540 100644 --- a/services/madoc-ts/src/frontend/site/pages/all-collections.tsx +++ b/services/madoc-ts/src/frontend/site/pages/all-collections.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Heading1 } from '../../shared/typography/Heading1'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { Slot } from '../../shared/page-blocks/slot'; -import { AllCollectionsPaginatedItems } from '../features/AllCollectionsPaginatedItems'; -import { AllCollectionsPagination } from '../features/AllCollectionsPagination'; -import { StaticPage } from '../features/StaticPage'; +import { AllCollectionsPaginatedItems } from '../blocks/AllCollectionsPaginatedItems'; +import { AllCollectionsPagination } from '../blocks/AllCollectionsPagination'; +import { StaticPage } from '../features/viewPage/StaticPage'; export const AllCollections: React.FC = () => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/site/pages/all-manifests.tsx b/services/madoc-ts/src/frontend/site/pages/all-manifests.tsx index 99287942e..5a96468f9 100644 --- a/services/madoc-ts/src/frontend/site/pages/all-manifests.tsx +++ b/services/madoc-ts/src/frontend/site/pages/all-manifests.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Heading1 } from '../../shared/typography/Heading1'; import { Slot } from '../../shared/page-blocks/slot'; -import { AllManifestsPaginatedItems } from '../features/AllManifestsPaginatedItems'; -import { AllManifestsPagination } from '../features/AllManifestsPagination'; -import { StaticPage } from '../features/StaticPage'; +import { AllManifestsPaginatedItems } from '../blocks/AllManifestsPaginatedItems'; +import { AllManifestsPagination } from '../blocks/AllManifestsPagination'; +import { StaticPage } from '../features/viewPage/StaticPage'; export const AllManifests: React.FC = () => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/site/pages/all-projects.tsx b/services/madoc-ts/src/frontend/site/pages/all-projects.tsx index 8b8113b32..941136e62 100644 --- a/services/madoc-ts/src/frontend/site/pages/all-projects.tsx +++ b/services/madoc-ts/src/frontend/site/pages/all-projects.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Heading1 } from '../../shared/typography/Heading1'; import { Slot } from '../../shared/page-blocks/slot'; -import { AllProjectsPaginatedItems } from '../features/AllProjectsPaginatedItems'; -import { AllProjectsPagination } from '../features/AllProjectsPagination'; -import { StaticPage } from '../features/StaticPage'; +import { AllProjectsPaginatedItems } from '../blocks/AllProjectsPaginatedItems'; +import { AllProjectsPagination } from '../blocks/AllProjectsPagination'; +import { StaticPage } from '../features/viewPage/StaticPage'; export const AllProjects: React.FC = () => { const { t } = useTranslation(); diff --git a/services/madoc-ts/src/frontend/site/pages/all-tasks.tsx b/services/madoc-ts/src/frontend/site/pages/all-tasks.tsx index bcc8cd503..5e62af5d6 100644 --- a/services/madoc-ts/src/frontend/site/pages/all-tasks.tsx +++ b/services/madoc-ts/src/frontend/site/pages/all-tasks.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { BaseTask } from '../../../gateway/tasks/base-task'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { Button, ButtonIcon, ButtonRow } from '../../shared/navigation/Button'; import { LayoutContainer, @@ -20,9 +20,9 @@ import { useInfiniteData } from '../../shared/hooks/use-data'; import { Pagination as PaginationType } from '../../../types/schemas/_pagination'; import { Navigate, Outlet, useNavigate, useParams } from 'react-router-dom'; import { useLocationQuery } from '../../shared/hooks/use-location-query'; -import { TaskFilterStatuses } from '../features/TaskFilterStatuses'; -import { TaskFilterType } from '../features/TaskFilterType'; -import { TaskListItem } from '../features/TaskListItem'; +import { TaskFilterStatuses } from '../features/tasks/TaskFilterStatuses'; +import { TaskFilterType } from '../features/tasks/TaskFilterType'; +import { TaskListItem } from '../features/tasks/TaskListItem'; import { useGoToQuery } from '../hooks/use-go-to-query'; import { useInfiniteAction } from '../hooks/use-infinite-action'; import { useRelativeLinks } from '../hooks/use-relative-links'; diff --git a/services/madoc-ts/src/frontend/site/pages/blocks.tsx b/services/madoc-ts/src/frontend/site/pages/blocks.tsx new file mode 100644 index 000000000..889f5f97e --- /dev/null +++ b/services/madoc-ts/src/frontend/site/pages/blocks.tsx @@ -0,0 +1,41 @@ +import { RenderBlock } from '../../shared/page-blocks/render-block'; +import { useSlots } from '../../shared/page-blocks/slot-context'; +import { useAvailableBlocks } from '../../shared/page-blocks/use-available-blocks'; +import { useRouteContext } from '../hooks/use-route-context'; + +export function BlocksPage() { + const { context, pagePath } = useSlots(); + const { + filteredBlocks, + contextBlocks, + pluginBlocks, + searchBlocks, + availableBlocks, + pagePathBlocks, + } = useAvailableBlocks({ + context, + pagePath, + }); + + return ( +
    + {availableBlocks.map((block, k) => { + return ( +
    +

    {block.type}

    + +
    + ); + })} +
    + ); +} diff --git a/services/madoc-ts/src/frontend/site/pages/dashboard/dashboard.tsx b/services/madoc-ts/src/frontend/site/pages/dashboard/dashboard.tsx index cb13eccbb..c85859b88 100644 --- a/services/madoc-ts/src/frontend/site/pages/dashboard/dashboard.tsx +++ b/services/madoc-ts/src/frontend/site/pages/dashboard/dashboard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { UserProjects } from '../../features/UserProjects'; +import { UserProjects } from '../../features/userDash/UserProjects'; export const UserDashboard: React.FC = () => { return ( diff --git a/services/madoc-ts/src/frontend/site/pages/dashboard/user-contributions.tsx b/services/madoc-ts/src/frontend/site/pages/dashboard/user-contributions.tsx index eef497cad..ac39ebce1 100644 --- a/services/madoc-ts/src/frontend/site/pages/dashboard/user-contributions.tsx +++ b/services/madoc-ts/src/frontend/site/pages/dashboard/user-contributions.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { ContributorTasks } from '../../features/contributor/ContributorTasks'; -import { UserStatistics } from '../../features/contributor/UserStatistics'; +import { ContributionsTasks } from '../../features/userDash/ContributionsTasks'; +import { UserStatistics } from '../../features/userDash/UserStatistics'; export const UserContributions: React.FC = () => { return ( <> - + ); }; diff --git a/services/madoc-ts/src/frontend/site/pages/dashboard/user-reviews.tsx b/services/madoc-ts/src/frontend/site/pages/dashboard/user-reviews.tsx index 685c7deaf..b1ba3b464 100644 --- a/services/madoc-ts/src/frontend/site/pages/dashboard/user-reviews.tsx +++ b/services/madoc-ts/src/frontend/site/pages/dashboard/user-reviews.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { ReviewerTasks } from '../../features/reviewer/ReviewerTasks'; +import { ReviewerTasksTable } from '../../features/userDash/ReviewerTasksTable'; export const UserReviews: React.FC = () => { return ( <> - + ); }; diff --git a/services/madoc-ts/src/frontend/site/pages/homepage.tsx b/services/madoc-ts/src/frontend/site/pages/homepage.tsx index 4f6bd62df..13f15d1b9 100644 --- a/services/madoc-ts/src/frontend/site/pages/homepage.tsx +++ b/services/madoc-ts/src/frontend/site/pages/homepage.tsx @@ -1,7 +1,10 @@ import React from 'react'; +import { AvailableBlocks } from '../../shared/page-blocks/available-blocks'; import { Slot } from '../../shared/page-blocks/slot'; -import { DefaultHomepage } from '../features/DefaultHomepage'; -import { StaticPage } from '../features/StaticPage'; +import { HeroCentered } from '../../tailwind/blocks/HeroCentered'; +import { HeroHyper } from '../../tailwind/blocks/HeroHyper'; +import { DefaultHomepage } from '../blocks/DefaultHomepage'; +import { StaticPage } from '../features/viewPage/StaticPage'; export const Homepage = () => { return ( @@ -9,6 +12,10 @@ export const Homepage = () => { + + + + diff --git a/services/madoc-ts/src/frontend/site/pages/loaders/canvas-loader.tsx b/services/madoc-ts/src/frontend/site/pages/loaders/canvas-loader.tsx index b8f031090..69667f7c3 100644 --- a/services/madoc-ts/src/frontend/site/pages/loaders/canvas-loader.tsx +++ b/services/madoc-ts/src/frontend/site/pages/loaders/canvas-loader.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { CanvasFull } from '../../../../types/canvas-full'; -import { BreadcrumbContext } from '../../../shared/components/Breadcrumbs'; +import { BreadcrumbContext } from '../../blocks/Breadcrumbs'; import { ApiArgs } from '../../../shared/hooks/use-api-query'; import { useData } from '../../../shared/hooks/use-data'; import { HighlightedRegionProvider } from '../../../shared/hooks/use-highlighted-regions'; diff --git a/services/madoc-ts/src/frontend/site/pages/loaders/collection-list-loader.tsx b/services/madoc-ts/src/frontend/site/pages/loaders/collection-list-loader.tsx index de1342dca..899adcfc2 100644 --- a/services/madoc-ts/src/frontend/site/pages/loaders/collection-list-loader.tsx +++ b/services/madoc-ts/src/frontend/site/pages/loaders/collection-list-loader.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Outlet } from 'react-router-dom'; import { ProjectFull } from '../../../../types/project-full'; -import { BreadcrumbContext } from '../../../shared/components/Breadcrumbs'; +import { BreadcrumbContext } from '../../blocks/Breadcrumbs'; import { createUniversalComponent } from '../../../shared/utility/create-universal-component'; import { UniversalComponent } from '../../../types'; import { Pagination as PaginationType } from '../../../../types/schemas/_pagination'; diff --git a/services/madoc-ts/src/frontend/site/pages/loaders/collection-loader.tsx b/services/madoc-ts/src/frontend/site/pages/loaders/collection-loader.tsx index b6bd13900..a208076c5 100644 --- a/services/madoc-ts/src/frontend/site/pages/loaders/collection-loader.tsx +++ b/services/madoc-ts/src/frontend/site/pages/loaders/collection-loader.tsx @@ -5,7 +5,7 @@ import { AutoSlotLoader } from '../../../shared/page-blocks/auto-slot-loader'; import { createUniversalComponent } from '../../../shared/utility/create-universal-component'; import { UniversalComponent } from '../../../types'; import { usePaginatedData } from '../../../shared/hooks/use-data'; -import { BreadcrumbContext } from '../../../shared/components/Breadcrumbs'; +import { BreadcrumbContext } from '../../blocks/Breadcrumbs'; import { CollectionFull } from '../../../../types/schemas/collection-full'; import { ItemNotFound } from '../Item-not-found'; diff --git a/services/madoc-ts/src/frontend/site/pages/loaders/manifest-loader.tsx b/services/madoc-ts/src/frontend/site/pages/loaders/manifest-loader.tsx index 6ece4a944..ea39b5000 100644 --- a/services/madoc-ts/src/frontend/site/pages/loaders/manifest-loader.tsx +++ b/services/madoc-ts/src/frontend/site/pages/loaders/manifest-loader.tsx @@ -3,7 +3,7 @@ import { ApiArgs } from '../../../shared/hooks/use-api-query'; import { AutoSlotLoader } from '../../../shared/page-blocks/auto-slot-loader'; import { UniversalComponent } from '../../../types'; import { createUniversalComponent } from '../../../shared/utility/create-universal-component'; -import { BreadcrumbContext } from '../../../shared/components/Breadcrumbs'; +import { BreadcrumbContext } from '../../blocks/Breadcrumbs'; import { ManifestFull } from '../../../../types/schemas/manifest-full'; import { Outlet } from 'react-router-dom'; import { usePaginatedManifest } from '../../hooks/use-manifest'; diff --git a/services/madoc-ts/src/frontend/site/pages/loaders/page-loader.tsx b/services/madoc-ts/src/frontend/site/pages/loaders/page-loader.tsx index bd55be5b9..c42da9776 100644 --- a/services/madoc-ts/src/frontend/site/pages/loaders/page-loader.tsx +++ b/services/madoc-ts/src/frontend/site/pages/loaders/page-loader.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react'; import { Outlet } from 'react-router-dom'; import { EditorialContext } from '../../../../types/schemas/site-page'; import { SitePage } from '../../../../types/site-pages-recursive'; -import { BreadcrumbContext } from '../../../shared/components/Breadcrumbs'; +import { BreadcrumbContext } from '../../blocks/Breadcrumbs'; import { useApi } from '../../../shared/hooks/use-api'; import { SlotProvider } from '../../../shared/page-blocks/slot-context'; import { UniversalComponent } from '../../../types'; diff --git a/services/madoc-ts/src/frontend/site/pages/loaders/project-loader.tsx b/services/madoc-ts/src/frontend/site/pages/loaders/project-loader.tsx index 1448c4f72..699100f9a 100644 --- a/services/madoc-ts/src/frontend/site/pages/loaders/project-loader.tsx +++ b/services/madoc-ts/src/frontend/site/pages/loaders/project-loader.tsx @@ -14,10 +14,10 @@ import { nullTheme, useCustomTheme } from '../../../themes/helpers/CustomThemePr import { UniversalComponent } from '../../../types'; import { createUniversalComponent } from '../../../shared/utility/create-universal-component'; import { useStaticData } from '../../../shared/hooks/use-data'; -import { BreadcrumbContext } from '../../../shared/components/Breadcrumbs'; +import { BreadcrumbContext } from '../../blocks/Breadcrumbs'; import { ProjectFull } from '../../../../types/project-full'; import { ConfigProvider } from '../../features/SiteConfigurationContext'; -import { FooterImageGrid } from '../../../shared/components/FooterImageGrid'; +import { FooterImageGrid } from '../../blocks/FooterImageGrid'; import { Slot } from '../../../shared/page-blocks/slot'; import { ItemNotFound } from '../Item-not-found'; diff --git a/services/madoc-ts/src/frontend/site/pages/loaders/root-loader.tsx b/services/madoc-ts/src/frontend/site/pages/loaders/root-loader.tsx index 27f2b0223..1a3905690 100644 --- a/services/madoc-ts/src/frontend/site/pages/loaders/root-loader.tsx +++ b/services/madoc-ts/src/frontend/site/pages/loaders/root-loader.tsx @@ -10,9 +10,9 @@ import { ErrorBoundary } from '../../../shared/utility/error-boundary'; import { createUniversalComponent } from '../../../shared/utility/create-universal-component'; import { UniversalComponent } from '../../../types'; import { useStaticData } from '../../../shared/hooks/use-data'; -import { GlobalFooter } from '../../features/GlobalFooter'; -import { GlobalSiteHeader } from '../../features/GlobalSiteHeader'; -import { GlobalSiteNavigation } from '../../features/GlobalSiteNavigation'; +import { GlobalFooter } from '../../features/global/GlobalFooter'; +import { GlobalSiteHeader } from '../../features/global/GlobalSiteHeader'; +import { GlobalSiteNavigation } from '../../features/global/GlobalSiteNavigation'; import { ConfigProvider, SiteConfigurationContext } from '../../features/SiteConfigurationContext'; export type RootLoaderType = { diff --git a/services/madoc-ts/src/frontend/site/pages/loaders/task-loader.tsx b/services/madoc-ts/src/frontend/site/pages/loaders/task-loader.tsx index 2a5b34754..90fefec50 100644 --- a/services/madoc-ts/src/frontend/site/pages/loaders/task-loader.tsx +++ b/services/madoc-ts/src/frontend/site/pages/loaders/task-loader.tsx @@ -4,7 +4,7 @@ import { BaseTask } from '../../../../gateway/tasks/base-task'; import { UniversalComponent } from '../../../types'; import { createUniversalComponent } from '../../../shared/utility/create-universal-component'; import { useStaticData } from '../../../shared/hooks/use-data'; -import { BreadcrumbContext } from '../../../shared/components/Breadcrumbs'; +import { BreadcrumbContext } from '../../blocks/Breadcrumbs'; export type TaskContext = { task: Task & { id: string }; diff --git a/services/madoc-ts/src/frontend/site/pages/search.tsx b/services/madoc-ts/src/frontend/site/pages/search.tsx index ceba8067b..4613b0894 100644 --- a/services/madoc-ts/src/frontend/site/pages/search.tsx +++ b/services/madoc-ts/src/frontend/site/pages/search.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { CloseIcon } from '../../shared/icons/CloseIcon'; import { LoadingBlock } from '../../shared/callouts/LoadingBlock'; import { SearchBox } from '../../shared/atoms/SearchBox'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { LocaleString } from '../../shared/components/LocaleString'; import { Pagination } from '../../shared/components/Pagination'; import { @@ -18,9 +18,9 @@ import { SearchFilterTitle, SearchFilterToggle, } from '../../shared/components/SearchFilters'; -import { SearchResults, TotalResults } from '../../shared/components/SearchResults'; +import { SearchResults, TotalResults } from '../features/search/SearchResults'; import { AddIcon } from '../../shared/icons/AddIcon'; -import { useSiteConfiguration } from '../features/SiteConfigurationContext'; +import { useSiteConfiguration } from '../../site/features/SiteConfigurationContext'; import { useSearch } from '../hooks/use-search'; import { useSearchFacets } from '../hooks/use-search-facets'; import { useSearchQuery } from '../hooks/use-search-query'; diff --git a/services/madoc-ts/src/frontend/site/pages/site-terms.tsx b/services/madoc-ts/src/frontend/site/pages/site-terms.tsx new file mode 100644 index 000000000..6d0c92e5c --- /dev/null +++ b/services/madoc-ts/src/frontend/site/pages/site-terms.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from 'react-query'; +import { StaticMarkdownBlock } from '../../../extensions/page-blocks/simple-markdown-block/static-markdown-block'; +import { SuccessMessage } from '../../shared/callouts/SuccessMessage'; +import { WarningMessage } from '../../shared/callouts/WarningMessage'; +import { useApi } from '../../shared/hooks/use-api'; +import { useData } from '../../shared/hooks/use-data'; +import { useSite, useUser } from '../../shared/hooks/use-site'; +import { Button, ButtonRow } from '../../shared/navigation/Button'; +import { serverRendererFor } from '../../shared/plugins/external/server-renderer-for'; + +export function SiteTerms() { + const { t } = useTranslation(); + const user = useUser(); + const accepted = user?.terms?.hasAccepted; + const api = useApi(); + const site = useSite(); + const isAdmin = user && user.scope && user.scope.indexOf('site.admin') !== -1; + + const { data } = useData( + SiteTerms, + {}, + { + refetchIntervalInBackground: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + } + ); + + const [acceptTerms, acceptTermsStatus] = useMutation(async () => { + if (user) { + await api.siteManager.acceptTerms(); + window.location.reload(); + } + }); + + return ( +
    + {user && !accepted && !user.terms_accepted?.length ? ( + + {t( + 'You have not yet accepted the terms of use for this site. Please read the terms of use below and accept them' + )} + + ) : null} + + {user && !accepted && user.terms_accepted?.length ? ( + + {t( + 'The terms of use for this site have changed since you last accepted them. Please read the terms of use below' + )} + + ) : null} + + {user && accepted ? ( + {t('You have accepted the terms of use for this site.')} + ) : null} + +
    + + + {isAdmin ? ( + + + + ) : null} + + {user && !accepted ? ( + + + + ) : null} +
    +
    + ); +} + +serverRendererFor(SiteTerms, { + getKey: () => ['site-terms', {}], + getData: (key, vars, api) => { + return api.getSiteTerms(); + }, +}); diff --git a/services/madoc-ts/src/frontend/site/pages/tasks/api-action-task.tsx b/services/madoc-ts/src/frontend/site/pages/tasks/api-action-task.tsx index e2dbdfa73..88e55103f 100644 --- a/services/madoc-ts/src/frontend/site/pages/tasks/api-action-task.tsx +++ b/services/madoc-ts/src/frontend/site/pages/tasks/api-action-task.tsx @@ -9,9 +9,9 @@ import { ApiActionTask } from '../../../../gateway/tasks/api-action-task'; import { Button, ButtonRow } from '../../../shared/navigation/Button'; import { ErrorMessage } from '../../../shared/callouts/ErrorMessage'; import { SuccessMessage } from '../../../shared/callouts/SuccessMessage'; -import { CanvasSnippet } from '../../../shared/components/CanvasSnippet'; +import { CanvasSnippet } from '../../../shared/features/CanvasSnippet'; import { CodeBlock } from '../../../shared/components/CodeBlock.lazy'; -import { ManifestSnippet } from '../../../shared/components/ManifestSnippet'; +import { ManifestSnippet } from '../../../shared/features/ManifestSnippet'; import { useApi } from '../../../shared/hooks/use-api'; const resolveSubjectId = (subject: ApiDefinitionSubject, request: ApiRequest) => { diff --git a/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/ReviewNagivation.tsx b/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/ReviewNagivation.tsx index 4b8881b80..75599b3e9 100644 --- a/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/ReviewNagivation.tsx +++ b/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/ReviewNagivation.tsx @@ -1,10 +1,14 @@ -import React from 'react'; -import { createLink } from '../../../../shared/utility/create-link'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; -import { NavigationButton, PaginationText } from '../../../../shared/components/CanvasNavigationMinimalist'; +import { NavigationButton, PaginationText } from '../../../../shared/components/NavigationButton'; import { CrowdsourcingTask } from '../../../../../gateway/tasks/crowdsourcing-task'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocationQuery } from '../../../../shared/hooks/use-location-query'; +import { useRouteContext } from '../../../hooks/use-route-context'; +import { useInfiniteData } from '../../../../shared/hooks/use-data'; +import { ReviewListingPage } from '../../../components'; +import { useRelativeLinks } from '../../../hooks/use-relative-links'; const PaginationContainer = styled.div` display: flex; @@ -16,7 +20,6 @@ const PaginationContainer = styled.div` align-items: center; width: 130px; padding: 0; - margin-left: auto; button { border: none; background-color: transparent; @@ -28,24 +31,43 @@ const PaginationContainer = styled.div` `; export const ReviewNavigation: React.FC<{ - handleNavigation?: (taskId: string, page: number | string, getNext?: boolean) => Promise | void; taskId?: string; - projectId?: string; - subRoute?: string; - pages?: any; - query?: any; - size?: string | undefined; -}> = ({ taskId: id, pages: pages, projectId, subRoute, query, handleNavigation }) => { +}> = ({ taskId: id }) => { const { hash } = useLocation(); const hsh = hash.slice(1); const pg = hsh ? Number(hsh) - 1 : 0; - - const idx = pages && pages[pg] ? pages[pg].tasks.findIndex((i: CrowdsourcingTask) => i.id === id) : -1; + const { projectId } = useRouteContext(); + const { sort_by = '', ...query } = useLocationQuery(); + const navigate = useNavigate(); const { t } = useTranslation(); + const createLink = useRelativeLinks(); + + const { data: pages, fetchMore, canFetchMore, isFetchingMore } = useInfiniteData(ReviewListingPage, undefined, { + keepPreviousData: true, + getFetchMore: lastPage => { + if (lastPage.pagination.totalPages === 0 || lastPage.pagination.totalPages === lastPage.pagination.page) { + return undefined; + } + return { + page: lastPage.pagination.page + 1, + }; + }, + }); + const idx = pages && pages[pg] ? pages[pg].tasks.findIndex((i: CrowdsourcingTask) => i.id === id) : -1; // results per page = 20 const totalIndex = 20 * pg + (idx + 1); + const beforeNavigate = useCallback( + async (newTaskId, page, getNext): Promise => { + if (!isFetchingMore && canFetchMore && getNext) { + await fetchMore(); + } + navigate(createLink({ taskId: undefined, subRoute: `reviews/${newTaskId}`, query: { sort_by }, hash: page })); + }, + [canFetchMore, createLink, fetchMore, isFetchingMore, navigate, sort_by] + ); + if (!pages || idx === -1) { return null; } @@ -57,10 +79,10 @@ export const ReviewNavigation: React.FC<{ { - if (handleNavigation) { + if (beforeNavigate) { e.preventDefault(); if (pages[pg - 1].tasks) { - handleNavigation(prevPageItem.id, pg - 1); + return beforeNavigate(prevPageItem.id, pg - 1, false); } } }} @@ -85,10 +107,10 @@ export const ReviewNavigation: React.FC<{ { - if (handleNavigation) { + if (beforeNavigate) { e.preventDefault(); if (pages[pg + 1].tasks) { - handleNavigation(nextPageItem.id, pg + 2, idx + 2 > pages[pg].tasks.length - 1); + return beforeNavigate(nextPageItem.id, pg + 2, idx + 2 > pages[pg].tasks.length - 1); } } }} @@ -111,10 +133,10 @@ export const ReviewNavigation: React.FC<{ { - if (handleNavigation) { + if (beforeNavigate) { e.preventDefault(); if (pages[pg].tasks) { - handleNavigation(pages[pg].tasks[idx - 1].id, pg + 1); + return beforeNavigate(pages[pg].tasks[idx - 1].id, pg + 1, false); } } }} @@ -134,17 +156,16 @@ export const ReviewNavigation: React.FC<{ { - if (handleNavigation) { + if (beforeNavigate) { e.preventDefault(); if (pages[pg].tasks) { - handleNavigation(pages[pg].tasks[idx + 1].id, pg + 1, idx + 2 > pages[pg].tasks.length - 1); + return beforeNavigate(pages[pg].tasks[idx + 1].id, pg + 1, idx + 2 > pages[pg].tasks.length - 1); } } }} link={createLink({ projectId, taskId: pages[pg].tasks[idx + 1].id, - subRoute, query, })} item={pages[pg].tasks[idx + 1]} diff --git a/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/review-listing-page.tsx b/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/review-listing-page.tsx index efabbe5eb..6b0ec0f41 100644 --- a/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/review-listing-page.tsx +++ b/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/review-listing-page.tsx @@ -1,11 +1,11 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useNavigate, useParams } from 'react-router-dom'; +import { Link, Outlet, useNavigate, useParams } from 'react-router-dom'; import styled, { css } from 'styled-components'; import { SubjectSnippet } from '../../../../../extensions/tasks/resolvers/subject-resolver'; import { CrowdsourcingTask } from '../../../../../gateway/tasks/crowdsourcing-task'; import { SimpleStatus } from '../../../../shared/atoms/SimpleStatus'; -import { DisplayBreadcrumbs } from '../../../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../../../blocks/Breadcrumbs'; import { LocaleString } from '../../../../shared/components/LocaleString'; import { useInfiniteData } from '../../../../shared/hooks/use-data'; import { useLocationQuery } from '../../../../shared/hooks/use-location-query'; @@ -14,15 +14,13 @@ import { serverRendererFor } from '../../../../shared/plugins/external/server-re import { HrefLink } from '../../../../shared/utility/href-link'; import { useRelativeLinks } from '../../../hooks/use-relative-links'; import { useTaskMetadata } from '../../../hooks/use-task-metadata'; -import { Button, ButtonIcon } from '../../../../shared/navigation/Button'; +import { Button, ButtonIcon, TextButton } from '../../../../shared/navigation/Button'; import { Chevron } from '../../../../shared/icons/Chevron'; import { useResizeLayout } from '../../../../shared/hooks/use-resize-layout'; import { LayoutHandle } from '../../../../shared/layout/LayoutContainer'; import ResizeHandleIcon from '../../../../shared/icons/ResizeHandleIcon'; import { useInfiniteAction } from '../../../hooks/use-infinite-action'; import { RefetchProvider } from '../../../../shared/utility/refetch-context'; -import { useRouteContext } from '../../../hooks/use-route-context'; -import { ReviewNavigation } from './ReviewNagivation'; import { EmptyState } from '../../../../shared/layout/EmptyState'; import ListItemIcon from '../../../../shared/icons/ListItemIcon'; import { useKeyboardListNavigation } from '../../../hooks/use-keyboard-list-navigation'; @@ -78,6 +76,9 @@ const ThickTableRow = styled(SimpleTable.Row)<{ $active?: boolean }>` `; const HeaderLink = styled.a` + display: flex; + align-items: center; + gap: 0.5em; color: black; svg { @@ -125,11 +126,10 @@ const HeaderLink = styled.a` export function ReviewListingPage() { const { t } = useTranslation(); - const params = useParams<{ taskId?: string }>(); - const { projectId } = useRouteContext(); + const params = useParams<{ taskId?: string; slug?: string }>(); + const projectId = params.slug; const createLink = useRelativeLinks(); const { sort_by = '', ...query } = useLocationQuery(); - const navigate = useNavigate(); const { widthB, refs } = useResizeLayout(`review-dashboard-resize`, { left: true, @@ -160,16 +160,6 @@ export function ReviewListingPage() { container: refs.resizableDiv, }); - const beforeNavigate = useCallback( - async (newTaskId, page, getNext) => { - if (!isFetchingMore && canFetchMore && getNext) { - await fetchMore(); - } - navigate(createLink({ taskId: undefined, subRoute: `reviews/${newTaskId}`, query: { sort_by }, hash: page })); - }, - [canFetchMore, createLink, fetchMore, isFetchingMore, navigate, sort_by] - ); - const QuerySortToggle = (field: string) => { const sort = sort_by; if (sort && sort.includes(`${field}:desc`)) { @@ -192,6 +182,15 @@ export function ReviewListingPage() { +
    + + {t('Task view')} + +
    + {!pages ? ( @@ -290,13 +289,6 @@ export function ReviewListingPage() { {params.taskId ? ( <> - ) : ( @@ -333,7 +325,12 @@ function SingleReviewTableRow({ $active={active} onClick={() => navigate( - createLink({ taskId: undefined, subRoute: `reviews/${task.id}`, query, hash: page ? page.toString() : '1' }) + createLink({ + subRoute: `reviews`, + taskId: task.id, + query, + hash: page ? page.toString() : '1', + }) ) } > @@ -352,7 +349,7 @@ function SingleReviewTableRow({ )} {/* resource name */} - + {metadata.subject && metadata.subject.type === 'manifest' ? '' : metadata.subject?.label && {metadata.subject.label}} @@ -377,12 +374,12 @@ serverRendererFor(ReviewListingPage, { }, getData: async (key, vars, api) => { const slug = vars.projectSlug; - const project = await api.getProject(slug); + const project = slug ? await api.getProject(slug) : undefined; return api.getTasks(vars.page, { - all_tasks: false, + all_tasks: !project?.task_id, type: 'crowdsourcing-task', - root_task_id: project.task_id, + root_task_id: project?.task_id, per_page: 20, detail: true, ...vars.query, diff --git a/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/single-review.tsx b/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/single-review.tsx index 7f4a53beb..8b9628169 100644 --- a/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/single-review.tsx +++ b/services/madoc-ts/src/frontend/site/pages/tasks/review-listing/single-review.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState } from 'react'; -import { Navigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { CrowdsourcingReview } from '../../../../../gateway/tasks/crowdsourcing-review'; import { CrowdsourcingTask } from '../../../../../gateway/tasks/crowdsourcing-task'; import { EditorSlots } from '../../../../shared/capture-models/new/components/EditorSlots'; @@ -47,11 +47,8 @@ import { extractIdFromUrn } from '../../../../../utility/parse-urn'; import { useProjectAnnotationStyles } from '../../../hooks/use-project-annotation-styles'; import UnlockSmileyIcon from '../../../../shared/icons/UnlockSmileyIcon'; import { useCurrentUser } from '../../../../shared/hooks/use-current-user'; -import { ManifestCanvasGrid } from '../../../features/ManifestCanvasGrid'; -import { PreviewManifest } from '../../../../admin/molecules/PreviewManifest'; -import { ViewContentFetch } from '../../../../admin/molecules/ViewContentFetch'; -import { ProjectManifests } from '../../../features/ProjectManifests'; -import { ManifestSnippet } from '../../../../shared/components/ManifestSnippet'; +import { ManifestSnippet } from '../../../../shared/features/ManifestSnippet'; +import { ReviewNavigation } from './ReviewNagivation'; const ReviewContainer = styled.div` position: relative; @@ -62,6 +59,10 @@ const ReviewContainer = styled.div` &[data-is-max-window='true'] { height: 100vh; + + ${CanvasViewerControls} { + top: 9em; + } } `; @@ -80,6 +81,8 @@ const Label = styled.div` font-weight: 600; padding: 0.6em; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `; const SubLabel = styled.div` @@ -166,11 +169,13 @@ const Assignee = styled.div` function ViewSingleReview({ task, + taskId, review, toggle, isOpen, }: { task: CrowdsourcingTask & { id: string }; + taskId?: string; review: (CrowdsourcingReview & { id: string }) | null; toggle: any; isOpen: boolean; @@ -243,6 +248,8 @@ function ViewSingleReview({ )} + + @@ -269,7 +276,7 @@ function ViewSingleReview({ }} annotationTheme={annotationTheme} > - +
    @@ -404,7 +413,13 @@ function ViewSingleReview({ metadata.subject?.id && metadata.subject.type === 'manifest' && ( <> - + ) )} @@ -427,7 +442,13 @@ export function SingleReview() { } else return ( - + ); }} diff --git a/services/madoc-ts/src/frontend/site/pages/user-homepage.tsx b/services/madoc-ts/src/frontend/site/pages/user-homepage.tsx index 924572a0d..59a5f901c 100644 --- a/services/madoc-ts/src/frontend/site/pages/user-homepage.tsx +++ b/services/madoc-ts/src/frontend/site/pages/user-homepage.tsx @@ -13,7 +13,7 @@ import { CrowdsourcingReview } from '../../../gateway/tasks/crowdsourcing-review import { CrowdsourcingTask } from '../../../gateway/tasks/crowdsourcing-task'; import { Pagination } from '../../../types/schemas/_pagination'; import { isAdmin, isContributor, isReviewer } from '../../shared/utility/user-roles'; -import { UserGreeting } from '../features/contributor/UserGreeting'; +import { UserGreeting } from '../features/home/UserGreeting'; type UserHomepageType = { query: unknown; @@ -69,6 +69,10 @@ export const UserHomepage: UniversalComponent = createUniversa {t('My sites')} ) : null} + + + {t('Settings')} + diff --git a/services/madoc-ts/src/frontend/site/pages/user-settings.tsx b/services/madoc-ts/src/frontend/site/pages/user-settings.tsx new file mode 100644 index 000000000..07dcb3507 --- /dev/null +++ b/services/madoc-ts/src/frontend/site/pages/user-settings.tsx @@ -0,0 +1,129 @@ +import React, { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from 'react-query'; +import { UserInformationRequest } from '../../../extensions/site-manager/types'; +import { SuccessMessage } from '../../shared/callouts/SuccessMessage'; +import { + EditorShorthandCaptureModelRef, + EditShorthandCaptureModel, +} from '../../shared/capture-models/EditorShorthandCaptureModel'; +import { RevisionRequest } from '../../shared/capture-models/types/revision-request'; +import { useApi } from '../../shared/hooks/use-api'; +import { useData } from '../../shared/hooks/use-data'; +import { useSite, useUser } from '../../shared/hooks/use-site'; +import { EditIcon } from '../../shared/icons/EditIcon'; +import { Button, ButtonRow } from '../../shared/navigation/Button'; +import { serverRendererFor } from '../../shared/plugins/external/server-renderer-for'; +import { HrefLink } from '../../shared/utility/href-link'; + +export function UserSettings() { + const { data, refetch, updatedAt } = useData(UserSettings); + const ref = useRef(null); + const visibilityRef = useRef(null); + const { t } = useTranslation(); + const gFields = data?.model?.properties.gravitar; + const gField = gFields ? gFields[0] : null; + const gravitarInit = gField ? (gField as any).value === true : null; + const [gravitar, setUseGravitar] = React.useState(gravitarInit); + const site = useSite(); + const api = useApi(); + const user = useUser(); + + const [saveSettings, saveSettingsStatus] = useMutation(async () => { + try { + window.scrollTo(0, 0); + } catch (e) { + // ignore. + } + + const fields: UserInformationRequest['fields'] = {}; + const extraVisibility: UserInformationRequest['extraVisibility'] = {}; + const userInfo = ref.current?.getData(); + const visibility = visibilityRef.current?.getData(); + const keys = [...Object.keys(visibility), ...Object.keys(userInfo)]; + + for (const key of keys) { + if (fields[key] || extraVisibility[key]) { + continue; + } + if (typeof userInfo[key] !== 'undefined') { + fields[key] = { + value: userInfo[key], + visibility: visibility?.[key] || 'only-me', + }; + } else { + extraVisibility[key] = visibility?.[key] || 'only-me'; + } + } + + await api.saveSettingsModel({ + fields, + extraVisibility, + }); + + await refetch(); + }); + + const handleChange = (revision: RevisionRequest | null) => { + if (revision?.document && revision.document.properties.gravitar) { + const gravitarField = revision.document.properties.gravitar; + const field = gravitarField[0]; + if (field) { + setUseGravitar((field as any).value === true); + } + } + }; + + if (!data || !user) { + return null; + } + + const profileEnabled = !!data.model?.properties.gravitar; + + return ( +
    + {saveSettingsStatus.isSuccess ? Settings saved : null} + +

    Profile

    + + {profileEnabled ? ( +
    +
    + +
    + {gravitar ? ( + + {t('Change avatar')} + + ) : null} +
    + ) : null} +
    + + {t('Edit display name or password')} +
    + {data.model ? ( + + ) : null} +

    Privacy

    + {data.visibilityModel ? ( + + ) : null} + + + +
    + ); +} + +serverRendererFor(UserSettings, { + getKey: () => ['user-settings', {}], + getData: async (key, vars, api, pathname) => { + return api.getSettingsModel(); + }, +}); diff --git a/services/madoc-ts/src/frontend/site/pages/user/login-page.tsx b/services/madoc-ts/src/frontend/site/pages/user/login-page.tsx index de715dc88..c6bb80bad 100644 --- a/services/madoc-ts/src/frontend/site/pages/user/login-page.tsx +++ b/services/madoc-ts/src/frontend/site/pages/user/login-page.tsx @@ -10,13 +10,14 @@ import { Input, InputContainer, InputLabel } from '../../../shared/form/Input'; import { LoginActions, LoginContainer } from '../../../shared/layout/LoginContainer'; import { SuccessMessage } from '../../../shared/callouts/SuccessMessage'; import { useLocationQuery } from '../../../shared/hooks/use-location-query'; -import { useFormResponse, useSite, useUser } from '../../../shared/hooks/use-site'; +import { useFormResponse, useSite, useSystemConfig, useUser } from '../../../shared/hooks/use-site'; import { HrefLink } from '../../../shared/utility/href-link'; export const LoginPage: React.FC = () => { const { t } = useTranslation(); const user = useUser(); const site = useSite(); + const system = useSystemConfig(); const form = useFormResponse<{ loginError?: boolean; email?: string; success?: boolean }>(); const didError = form?.loginError || false; const { redirect } = useLocationQuery(); @@ -30,6 +31,9 @@ export const LoginPage: React.FC = () => { {form && form.success ? {t('You may now login')} : null} {t('Login')} + {system?.loginHeader ? ( +
    + ) : null}
    {t('Email')} @@ -41,10 +45,13 @@ export const LoginPage: React.FC = () => { {didError ? Incorrect email or password : null} + {system?.loginFooter ? ( +
    + ) : null} - +
    {t('Forgot password?')} - +
    diff --git a/services/madoc-ts/src/frontend/site/pages/user/register.tsx b/services/madoc-ts/src/frontend/site/pages/user/register.tsx index c3bf30a3b..6c3dfe359 100644 --- a/services/madoc-ts/src/frontend/site/pages/user/register.tsx +++ b/services/madoc-ts/src/frontend/site/pages/user/register.tsx @@ -68,6 +68,17 @@ export const Register: React.FC = () => { return ; } + const acceptTerms = site.latestTerms ? ( +
    +

    + {t('By registering you agree to the ')} + + {t('terms and conditions')} + +

    +
    + ) : null; + if (user && form?.invitation) { // @todo. return ( @@ -87,6 +98,7 @@ export const Register: React.FC = () => { + {acceptTerms}
    @@ -109,6 +121,9 @@ export const Register: React.FC = () => { {t('Register')} )} + {systemConfig?.registerHeader ? ( +
    + ) : null} {t('Display name')} @@ -118,11 +133,15 @@ export const Register: React.FC = () => { {form?.emailError ? {t('Email already in use')} : null} + {acceptTerms} + {systemConfig?.registerFooter ? ( +
    + ) : null} - +
    {t('Already have an account?')} {t('Login')} - +
    diff --git a/services/madoc-ts/src/frontend/site/pages/view-canvas-model.tsx b/services/madoc-ts/src/frontend/site/pages/view-canvas-model.tsx index 8ad19da92..1078d5692 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-canvas-model.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-canvas-model.tsx @@ -1,20 +1,19 @@ import React from 'react'; import { Navigate } from 'react-router-dom'; import { castBool } from '../../../utility/cast-bool'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; -import { useCurrentUser } from '../../shared/hooks/use-current-user'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { useLocationQuery } from '../../shared/hooks/use-location-query'; import { AutoSlotLoader } from '../../shared/page-blocks/auto-slot-loader'; import { Slot } from '../../shared/page-blocks/slot'; -import { CanvasModelCompleteMessage } from '../features/contributor/CanvasModelCompleteMessage'; -import { CanvasModelEditor } from '../features/contributor/CanvasModelEditor'; -import { CanvasModelPrepareActions } from '../features/admin/CanvasModelPrepareActions'; -import { CanvasModelReadOnlyViewer } from '../features/CanvasModelReadOnlyViewer'; -import { CanvasNotAvailableToBrowse } from '../features/CanvasNotAvailableToBrowse'; -import { CanvasPageHeader } from '../features/CanvasPageHeader'; -import { CanvasTaskWarningMessage } from '../features/contributor/CanvasTaskWarningMessage'; -import { CanvasThumbnailNavigation } from '../features/CanvasThumbnailNavigation'; -import { PrepareCanvasCaptureModel } from '../features/admin/PrepareCanvasCaptureModel'; +import { CanvasModelCompleteMessage } from '../blocks/CanvasModelCompleteMessage'; +import { CanvasModelEditor } from '../blocks/CanvasModelEditor'; +import { CanvasModelPrepareActions } from '../blocks/CanvasModelPrepareActions'; +import { CanvasModelReadOnlyViewer } from '../blocks/CanvasModelReadOnlyViewer'; +import { CanvasNotAvailableToBrowse } from '../blocks/CanvasNotAvailableToBrowse'; +import { CanvasPageHeader } from '../blocks/CanvasPageHeader'; +import { CanvasTaskWarningMessage } from '../blocks/CanvasTaskWarningMessage'; +import { CanvasThumbnailNavigation } from '../blocks/CanvasThumbnailNavigation'; +import { PrepareCanvasCaptureModel } from '../features/canvas/PrepareCanvasCaptureModel'; import { useSiteConfiguration } from '../features/SiteConfigurationContext'; import { useCanvasNavigation } from '../hooks/use-canvas-navigation'; import { useCanvasUserTasks } from '../hooks/use-canvas-user-tasks'; @@ -23,14 +22,14 @@ import { useProjectShadowConfiguration } from '../hooks/use-project-shadow-confi import { useProjectStatus } from '../hooks/use-project-status'; import { useRelativeLinks } from '../hooks/use-relative-links'; import { useRouteContext } from '../hooks/use-route-context'; -import { RedirectToNextCanvas } from '../features/RedirectToNextCanvas'; +import { RedirectToNextCanvas } from '../features/canvas/RedirectToNextCanvas'; export const ViewCanvasModel: React.FC = () => { const { canvasId } = useRouteContext(); const { showCanvasNavigation, showWarning } = useCanvasNavigation(); const { isManifestComplete, hasExpired } = useManifestTask(); - const { canUserSubmit, isLoading: isLoadingTasks, completedAndHide } = useCanvasUserTasks(); - const user = useCurrentUser(true); + const { canUserSubmit, canContribute, isLoading: isLoadingTasks, completedAndHide } = useCanvasUserTasks(); + const { goToNext } = useLocationQuery(); const shouldGoToNext = castBool(goToNext); const { @@ -38,12 +37,6 @@ export const ViewCanvasModel: React.FC = () => { } = useSiteConfiguration(); const createLink = useRelativeLinks(); const { isActive, isPreparing } = useProjectStatus(); - const canContribute = - user && - user.scope && - (user.scope.indexOf('site.admin') !== -1 || - user.scope.indexOf('models.admin') !== -1 || - user.scope.indexOf('models.contribute') !== -1); const { showCaptureModelOnManifest } = useProjectShadowConfiguration(); diff --git a/services/madoc-ts/src/frontend/site/pages/view-canvas.tsx b/services/madoc-ts/src/frontend/site/pages/view-canvas.tsx index 8b79f9364..ee14ffbb5 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-canvas.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-canvas.tsx @@ -1,17 +1,17 @@ import React from 'react'; import { castBool } from '../../../utility/cast-bool'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; -import { CanvasVaultContext } from '../../shared/components/CanvasVaultContext'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; +import { CanvasVaultContext } from '../../shared/capture-models/CanvasVaultContext'; import { useLocationQuery } from '../../shared/hooks/use-location-query'; import { Slot } from '../../shared/page-blocks/slot'; -import { CanvasConfigurationViewer } from '../features/CanvasConfigurationViewer'; -import { CanvasHighlightedRegions } from '../features/CanvasHighlightedRegions'; -import { CanvasPageHeader } from '../features/CanvasPageHeader'; -import { CanvasThumbnailNavigation } from '../features/CanvasThumbnailNavigation'; -import { ContinueCanvasSubmission } from '../features/contributor/ContinueCanvasSubmission'; -import { HighlightedCanvasSearchResults } from '../features/HighlightedCanvasSearchResults'; -import { ManifestMetadata } from '../features/ManifestMetadata'; -import { RedirectToNextCanvas } from '../features/RedirectToNextCanvas'; +import { CanvasConfigurationViewer } from '../blocks/CanvasConfigurationViewer'; +import { CanvasHighlightedRegions } from '../features/canvas/CanvasHighlightedRegions'; +import { CanvasPageHeader } from '../blocks/CanvasPageHeader'; +import { CanvasThumbnailNavigation } from '../blocks/CanvasThumbnailNavigation'; +import { ContinueCanvasSubmission } from '../blocks/ContinueCanvasSubmission'; +import { HighlightedCanvasSearchResults } from '../blocks/HighlightedCanvasSearchResults'; +import { ManifestMetadata } from '../blocks/ManifestMetadata'; +import { RedirectToNextCanvas } from '../features/canvas/RedirectToNextCanvas'; import { useSiteConfiguration } from '../features/SiteConfigurationContext'; import { useCanvasNavigation } from '../hooks/use-canvas-navigation'; diff --git a/services/madoc-ts/src/frontend/site/pages/view-collection.tsx b/services/madoc-ts/src/frontend/site/pages/view-collection.tsx index e5d510b53..bb9544cf6 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-collection.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-collection.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { Slot } from '../../shared/page-blocks/slot'; -import { CollectionFilterOptions } from '../features/CollectionFilterOptions'; -import { CollectionItemPagination } from '../features/CollectionItemPagination'; -import { CollectionPaginatedItems } from '../features/CollectionPaginatedItems'; -import { CollectionTitle } from '../features/CollectionTitle'; +import { CollectionFilterOptions } from '../blocks/CollectionFilterOptions'; +import { CollectionItemPagination } from '../blocks/CollectionItemPagination'; +import { CollectionPaginatedItems } from '../blocks/CollectionPaginatedItems'; +import { CollectionTitle } from '../blocks/CollectionTitle'; import { usePaginatedCollection } from '../hooks/use-paginated-collection'; -import { CollectionMetadata } from '../features/CollectionMetadata'; +import { CollectionMetadata } from '../blocks/CollectionMetadata'; export const ViewCollection: React.FC = () => { const { isLoading } = usePaginatedCollection(); diff --git a/services/madoc-ts/src/frontend/site/pages/view-manifest-mirador.tsx b/services/madoc-ts/src/frontend/site/pages/view-manifest-mirador.tsx index ed8e41873..fd14b851a 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-manifest-mirador.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-manifest-mirador.tsx @@ -3,7 +3,7 @@ import { BrowserComponent } from '../../shared/utility/browser-component'; import { Mirador } from '../../shared/viewers/mirador.lazy'; import { useApi } from '../../shared/hooks/use-api'; import React, { useMemo } from 'react'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { useRouteContext } from '../hooks/use-route-context'; import { ErrorBoundary } from '../../shared/utility/error-boundary'; diff --git a/services/madoc-ts/src/frontend/site/pages/view-manifest-model.tsx b/services/madoc-ts/src/frontend/site/pages/view-manifest-model.tsx index b59161153..71fe52d72 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-manifest-model.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-manifest-model.tsx @@ -1,37 +1,27 @@ import React from 'react'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; -import { useCurrentUser } from '../../shared/hooks/use-current-user'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { Slot } from '../../shared/page-blocks/slot'; -import { ManifestHeading } from '../features/ManifestHeading'; -import { ManifestModelEditor } from '../features/contributor/ManifestModelEditor'; -import { ManifestPagination } from '../features/ManifestPagination'; -import { ManifestUserNotification } from '../features/contributor/ManifestUserNotification'; -import { PrepareManifestsCaptureModel } from '../features/admin/PrepareManifestCaptureModel'; -import { RequiredStatement } from '../features/RequiredStatement'; +import { ManifestHeading } from '../blocks/ManifestHeading'; +import { ManifestModelEditor } from '../blocks/ManifestModelEditor'; +import { ManifestPagination } from '../blocks/ManifestPagination'; +import { ManifestUserNotification } from '../blocks/ManifestUserNotification'; +import { PrepareManifestsCaptureModel } from '../features/manifest/PrepareManifestCaptureModel'; +import { RequiredStatement } from '../blocks/RequiredStatement'; import { useManifestTask } from '../hooks/use-manifest-task'; import { useManifestUserTasks } from '../hooks/use-manifest-user-tasks'; import { useProjectShadowConfiguration } from '../hooks/use-project-shadow-configuration'; import { useProjectStatus } from '../hooks/use-project-status'; import { useRelativeLinks } from '../hooks/use-relative-links'; import { Navigate } from 'react-router-dom'; -import '../features/ManifestHero'; -import { ManifestModelCanvasPreview } from '../features/ManifestModelCanvasPreview'; +import { ManifestModelCanvasPreview } from '../blocks/ManifestModelCanvasPreview'; export function ViewManifestModel() { const createLink = useRelativeLinks(); const { isManifestComplete, hasExpired } = useManifestTask(); - const { canUserSubmit, isLoading: isLoadingTasks, completedAndHide } = useManifestUserTasks(); - const user = useCurrentUser(true); + const { canUserSubmit, isLoading: isLoadingTasks, completedAndHide, canContribute } = useManifestUserTasks();; const { isActive, isPreparing } = useProjectStatus(); const shadow = useProjectShadowConfiguration(); - const canContribute = - user && - user.scope && - (user.scope.indexOf('site.admin') !== -1 || - user.scope.indexOf('models.admin') !== -1 || - user.scope.indexOf('models.contribute') !== -1); - const isReadOnly = (!canUserSubmit && !isLoadingTasks) || completedAndHide || diff --git a/services/madoc-ts/src/frontend/site/pages/view-manifest-uv.tsx b/services/madoc-ts/src/frontend/site/pages/view-manifest-uv.tsx index 115f91121..822e0557d 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-manifest-uv.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-manifest-uv.tsx @@ -1,7 +1,7 @@ import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; import { useApi } from '../../shared/hooks/use-api'; import React, { useMemo } from 'react'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { BrowserComponent } from '../../shared/utility/browser-component'; import { useRouteContext } from '../hooks/use-route-context'; import { UniversalViewer } from '../../shared/viewers/universal-viewer.lazy'; diff --git a/services/madoc-ts/src/frontend/site/pages/view-manifest.tsx b/services/madoc-ts/src/frontend/site/pages/view-manifest.tsx index 2741e2d26..4489ad418 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-manifest.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-manifest.tsx @@ -1,21 +1,20 @@ import React from 'react'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { useLocationQuery } from '../../shared/hooks/use-location-query'; import { Slot } from '../../shared/page-blocks/slot'; -import { ManifestActions } from '../features/contributor/ManifestActions'; -import { ManifestCanvasGrid } from '../features/ManifestCanvasGrid'; -import { ManifestHeading } from '../features/ManifestHeading'; -import { ManifestMetadata } from '../features/ManifestMetadata'; -import { ManifestNotAvailableToBrowse } from '../features/ManifestNotAvailableToBrowse'; -import { ManifestPagination } from '../features/ManifestPagination'; -import { ManifestUserNotification } from '../features/contributor/ManifestUserNotification'; -import { ExternalLinks } from '../features/ExternalLinks'; -import { RequiredStatement } from '../features/RequiredStatement'; +import { ManifestActions } from '../blocks/ManifestActions'; +import { ManifestCanvasGrid } from '../blocks/ManifestCanvasGrid'; +import { ManifestHeading } from '../blocks/ManifestHeading'; +import { ManifestMetadata } from '../blocks/ManifestMetadata'; +import { ManifestNotAvailableToBrowse } from '../blocks/ManifestNotAvailableToBrowse'; +import { ManifestPagination } from '../blocks/ManifestPagination'; +import { ManifestUserNotification } from '../blocks/ManifestUserNotification'; +import { ExternalLinks } from '../blocks/ExternalLinks'; +import { RequiredStatement } from '../blocks/RequiredStatement'; import { useSiteConfiguration } from '../features/SiteConfigurationContext'; import { useManifest } from '../hooks/use-manifest'; import { useRelativeLinks } from '../hooks/use-relative-links'; import { Navigate } from 'react-router-dom'; -import '../features/ManifestHero'; export function ViewManifest() { const { data } = useManifest(); @@ -37,7 +36,7 @@ export function ViewManifest() { if (!manifest) { return null; } - return ; + return ; } return ( diff --git a/services/madoc-ts/src/frontend/site/pages/view-page.tsx b/services/madoc-ts/src/frontend/site/pages/view-page.tsx index c0efb538d..df5ba071c 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-page.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-page.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { LocaleString } from '../../shared/components/LocaleString'; import { Slot } from '../../shared/page-blocks/slot'; import { SlotProvider } from '../../shared/page-blocks/slot-context'; -import { PageEditorBar } from '../features/admin/PageEditorBar'; +import { PageEditorBar } from '../features/viewPage/PageEditorBar'; import { useSiteConfiguration } from '../features/SiteConfigurationContext'; import { usePage } from './loaders/page-loader'; import { PageNotFound } from './page-not-found'; diff --git a/services/madoc-ts/src/frontend/site/pages/view-project-notes.tsx b/services/madoc-ts/src/frontend/site/pages/view-project-notes.tsx index 7fca3d99a..daf484ad0 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project-notes.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project-notes.tsx @@ -7,10 +7,10 @@ import { Heading1, Subheading1 } from '../../shared/typography/Heading1'; import { CroppedImage } from '../../shared/atoms/Images'; import { ImageStripBox } from '../../shared/atoms/ImageStrip'; import { SnippetThumbnail, SnippetThumbnailContainer } from '../../shared/atoms/SnippetLarge'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { LocaleString } from '../../shared/components/LocaleString'; import { Pagination } from '../../shared/components/Pagination'; -import { ResultTitle } from '../../shared/components/SearchResults'; +import { ResultTitle } from '../features/search/SearchResults'; import { usePaginatedData } from '../../shared/hooks/use-data'; import { serverRendererFor } from '../../shared/plugins/external/server-renderer-for'; import { HrefLink } from '../../shared/utility/href-link'; @@ -50,11 +50,15 @@ export const ViewProjectNotes: React.FC = () => { ? data.notes.map(note => { const isManifest = note.type === 'manifest'; return ( - + {note.resource.thumbnail ? ( {isManifest ? ( - + ) : ( diff --git a/services/madoc-ts/src/frontend/site/pages/view-project-tasks.tsx b/services/madoc-ts/src/frontend/site/pages/view-project-tasks.tsx index 8c50076c7..2a396ce27 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project-tasks.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project-tasks.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Heading1, Subheading1 } from '../../shared/typography/Heading1'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { ContributorTasks } from '../../shared/components/ContributorTasks'; import { LocaleString } from '../../shared/components/LocaleString'; import { ReviewerTasks } from '../../shared/components/ReviewerTasks'; diff --git a/services/madoc-ts/src/frontend/site/pages/view-project.tsx b/services/madoc-ts/src/frontend/site/pages/view-project.tsx index dd5a4ac5c..c18ae93aa 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project.tsx @@ -1,20 +1,45 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { ProjectStatus } from '../../shared/atoms/ProjectStatus'; -import { DisplayBreadcrumbs } from '../../shared/components/Breadcrumbs'; +import { useUser } from '../../shared/hooks/use-site'; +import { ProjectMembersList } from '../../tailwind/blocks/project/ProjectMembersList'; +import { SlotTabs } from '../../tailwind/components/slot-tabs'; +import { DisplayBreadcrumbs } from '../blocks/Breadcrumbs'; import { AvailableBlocks } from '../../shared/page-blocks/available-blocks'; import { Slot } from '../../shared/page-blocks/slot'; -import { ProjectActions } from '../features/contributor/ProjectActions'; -import { ProjectContributionButton } from '../features/contributor/ProjectContributionButton'; -import { ProjectCollections } from '../features/ProjectCollections'; -import { ProjectContributors } from '../features/ProjectContributors'; -import { ProjectHeading } from '../features/ProjectHeading'; -import { ProjectManifests } from '../features/ProjectManifests'; +import { MostRecentProjectUpdate } from '../../tailwind/blocks/project/MostRecentProjectUpdate'; +import { PostNewProjectUpdate } from '../../tailwind/blocks/project/PostNewProjectUpdate'; +import { ProjectBanner } from '../../tailwind/blocks/project/ProjectBanner'; +import { ProjectContinueSubmissions } from '../../tailwind/blocks/project/ProjectContinueSubmissions'; +import { ProjectFeedback } from '../../tailwind/blocks/project/ProjectFeedback'; +import { ProjectFeedbackListing } from '../../tailwind/blocks/project/ProjectFeedbackListing'; +import { ProjectManifestList } from '../../tailwind/blocks/project/ProjectManifestList'; +import { ProjectMyWork } from '../../tailwind/blocks/project/ProjectMyWork'; +import { ProjectPersonalNotes } from '../../tailwind/blocks/project/ProjectPersonalNotes'; +import { ProjectSearchBox } from '../../tailwind/blocks/project/ProjectSearchBox'; +import { ProjectActions } from '../blocks/ProjectActions'; +import { ProjectCollections } from '../blocks/ProjectCollections'; +import { ProjectContributionButton } from '../blocks/ProjectContributionButton'; +import { ProjectContributors } from '../blocks/ProjectContributors'; +import { ProjectHeading } from '../blocks/ProjectHeading'; +import { ProjectStatisticsWithIcons } from '../blocks/ProjectStatisticsWithIcons'; import { ProjectStatistics } from '../features/ProjectStatistics'; +import { useProject } from '../hooks/use-project'; +import { ListProjectUpdates } from '../../tailwind/blocks/project/ListProjectUpdates'; +import { ProjectContributorStatistics } from '../../tailwind/blocks/project/ProjectContributorStatistics'; export const ViewProject: React.FC = () => { + const { t } = useTranslation(); + const user = useUser(); + const isAdmin = (user?.scope || []).includes('site.admin'); + const { data: project } = useProject(); const available = ( + {/**/} + + + ); @@ -26,22 +51,45 @@ export const ViewProject: React.FC = () => { - + + {available} + - - {available} - - - - {available} - + + + {available} + + + + + + {available} + + + + + + + + ); }; diff --git a/services/madoc-ts/src/frontend/site/pages/view-user.tsx b/services/madoc-ts/src/frontend/site/pages/view-user.tsx index f8507a354..731487c1b 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-user.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-user.tsx @@ -1,58 +1,362 @@ import React from 'react'; -import { SiteUser } from '../../../extensions/site-manager/types'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; +import styled from 'styled-components'; +import { PublicUserProfile } from '../../../extensions/site-manager/types'; +import { CrowdsourcingTask } from '../../../gateway/tasks/crowdsourcing-task'; +import { MetaDataDisplay } from '../../shared/components/MetaDataDisplay'; +import { useSite } from '../../shared/hooks/use-site'; +import { BuildingIcon } from '../../shared/icons/BuildingIcon'; +import { EditIcon } from '../../shared/icons/EditIcon'; import { TableContainer, TableRow, TableRowLabel } from '../../shared/layout/Table'; import { useData } from '../../shared/hooks/use-data'; +import { LocaleString, useApi, useUser } from '../../shared/plugins/public-api'; +import { Heading1 } from '../../shared/typography/Heading1'; import { createUniversalComponent } from '../../shared/utility/create-universal-component'; +import { HrefLink } from '../../shared/utility/href-link'; import { UniversalComponent } from '../../types'; import { Tag } from '../../shared/capture-models/editor/atoms/Tag'; import { getBotType } from '../../../automation/utils/get-bot-type'; +import { MailIcon } from '../../shared/icons/MailIcon'; +import { useTaskMetadata } from '../hooks/use-task-metadata'; type ViewUserType = { query: any; variables: { id: number }; params: { id: string }; - data: { user: SiteUser }; + data: PublicUserProfile; context: any; }; +const ProfileHeader = styled.div` + display: flex; + flex-direction: column; + position: relative; +`; + +const ProfileImage = styled.div` + width: 120px; + height: 120px; + border-radius: 50%; + overflow: hidden; + position: relative; + + img { + width: 100%; + height: auto; + object-fit: cover; + } +`; + +const ProfileStatusBadge = styled.div` + background: #e0f9ff; + border: 1px solid #d5eefc; + color: rgba(0, 0, 0, 0.6); + padding: 0.35em 1.2em; + align-items: center; + font-size: 0.75em; + align-self: center; + margin-left: 1em; + height: 2.5em; + display: flex; + border-radius: 1.25em; +`; + +const ProfileName = styled(Heading1)` + margin: 1em 0; + margin-bottom: 0.5em; +`; + +const ProfileRole = styled.div` + color: #666; +`; + +const ProfileActions = styled.div` + display: flex; + //background: #eee; + margin-bottom: 1em; + margin-top: 1em; + + & > * { + padding: 0 1em; + border-right: 1px solid #ccc; + + &:last-child { + border-right: none; + } + + &:first-child { + padding-left: 0; + } + } +`; + +const ProfileBio = styled.div` + margin-bottom: 1em; + white-space: pre-wrap; + max-width: 600px; +`; + +const ProfileContainer = styled.div` + display: flex; +`; + +const ProfileDetails = styled.div` + flex: 1; + overflow: hidden; +`; + +const ProfileSidebar = styled.div` + width: 380px; + background: #f9f9f9; + padding: 1em 2em; + margin-left: 1em; + border-radius: 4px; +`; + +// const ProfileStat = styled.div` +// margin-bottom: 0.5em; +// display: flex; +// align-items: center; +// `; + +const ProfileStatValue = styled.span` + font-size: 1.2em; + text-align: right; +`; + +const ProfileAction = styled.div` + display: flex; + align-items: center; + gap: 0.5em; +`; + +const ProfileStatLabel = styled.span` + font-size: 0.875em; + color: #333; + align-self: center; +`; + +const ProfileStats = styled.div` + display: grid; + grid-template-columns: auto 1fr; + grid-gap: 1em; + margin-bottom: 3em; + grid-template-areas: 'value' 'label'; +`; + +const manuallyDisplayed = ['gravitar', 'bio', 'institution', 'status']; + +const ContributionThumb = styled.div` + background: #eee; + + img, + svg { + width: 100%; + height: 100%; + object-fit: contain; + } +`; + +function ContributionSnippet({ task, first }: { task: CrowdsourcingTask; first: boolean }) { + const metadata = useTaskMetadata(task); + + const resourceLink = + metadata.subject && metadata.subject.parent && metadata.project + ? `/projects/${metadata.project.slug}/manifests/${metadata.subject.parent.id}/c/${metadata.subject.id}` + : metadata.subject && metadata.subject.type === 'manifest' && metadata.project + ? `/projects/${metadata.project.slug}/manifests/${metadata.subject.id}` + : null; + + return ( +
    + + + {metadata.selectorThumbnail && metadata.selectorThumbnail.svg ? ( +
    + ) : metadata.subject && metadata.subject.thumbnail ? ( + + ) : null} + + +
    + {metadata.subject ? ( + <> +

    + {metadata.subject.parent ? ( + <> + {metadata.subject.parent.label} + {' - '} + + ) : null} + {metadata.subject.label} +

    + + ) : ( +

    {task.name}

    + )} + {metadata.project ? ( + <> +
    + Project:{' '} + + {metadata.project.label} + +
    + + ) : null} + + {resourceLink ? ( +
    + View resource +
    + ) : null} +
    +
    + ); +} + export const ViewUser: UniversalComponent = createUniversalComponent( () => { + const { t } = useTranslation(); const { data } = useData(ViewUser); + const user = useUser(); + const site = useSite(); + + const isSelf = user && user.id === data?.user.id; if (!data) { return
    Loading...
    ; } + const infoKeys = Object.keys(data.info).filter(key => { + return !manuallyDisplayed.includes(key); + }); + return ( <> -
    -

    {data.user.name}

    - {data.user.automated ? ( - - {getBotType(data.user.config?.bot?.type) || 'bot'} - + + + + +
    + {data.user.name} + {data.user.automated ? ( + + {getBotType(data.user.config?.bot?.type) || 'bot'} + + ) : null} + {data.info.status ? {data.info.status} : null} +
    +
    + + + + {data.info.bio ? {data.info.bio} : null} + + {data.user.site_role} + {data.user.email ? ( + + + {data.user.email} + + ) : null} + {data.info.institution ? ( + + {data.info.institution} + + ) : null} + {isSelf ? ( + + + {t('Edit profile')} + + ) : null} + + + {infoKeys.length ? ( +
    + { + const info = data.info[infoKey]; + const label = (data.infoLabels || {})[infoKey] || infoKey; + return { + label: { en: [label] }, + value: { en: [info] }, + }; + })} + /> +
    + ) : null} + + {data.recentTasks && data.recentTasks.length ? ( +
    +

    Recent contributions

    + {data.recentTasks.map((task, i) => ( +
    + +
    + ))} +
    + ) : null} +
    + {data.statistics?.crowdsourcing || data.statistics?.reviews ? ( + +
    + {data.statistics?.crowdsourcing ? ( + <> +

    {t('Contributions')}

    + + <> + {data.statistics.crowdsourcing.statuses['1']} + {t('Accepted')} + + <> + {data.statistics.crowdsourcing.statuses['2']} + {t('In review')} + + <> + {data.statistics.crowdsourcing.statuses['3']} + {t('Completed')} + + <> + {data.statistics.crowdsourcing.total} + {t('Total')} + + + + ) : null} + {data.statistics?.reviews ? ( + <> +

    {t('Reviews')}

    + + <> + {data.statistics.reviews.statuses['1']} + {t('Pending')} + + <> + {data.statistics.reviews.statuses['2']} + {t('In review')} + + <> + {data.statistics.reviews.statuses['3']} + {t('Completed')} + + <> + {data.statistics.reviews.total} + {t('Total')} + + + + ) : null} +
    +
    ) : null} -
    - - - - Email - - {data.user.email} - - - - Role - - {data.user.site_role} - - - - ID - - urn:madoc:user:{data.user.id} - - + ); }, diff --git a/services/madoc-ts/src/frontend/site/routes.tsx b/services/madoc-ts/src/frontend/site/routes.tsx index e9572076a..21d4cc451 100644 --- a/services/madoc-ts/src/frontend/site/routes.tsx +++ b/services/madoc-ts/src/frontend/site/routes.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { CreateRouteType } from '../types'; +import { BlocksPage } from './pages/blocks'; type BaseRouteComponents = typeof import('./components'); // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -12,6 +13,11 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { exact: true, element: , }, + { + path: '/terms', + exact: true, + element: , + }, { path: '/collections', exact: true, @@ -104,6 +110,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -116,6 +126,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -128,6 +142,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -204,6 +222,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -216,6 +238,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -320,6 +346,11 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { exact: true, element: , }, + { + path: '/projects/:slug/collections/:collectionId/manifests/:manifestId/model/:canvasId', + exact: true, + element: , + }, { path: '/projects/:slug/collections/:collectionId/manifests/:manifestId/uv', exact: true, @@ -352,6 +383,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -364,6 +399,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -376,6 +415,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -453,6 +496,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -465,6 +512,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, ], @@ -525,6 +576,31 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, + ], + }, + ], + }, + { + path: '/reviews', + element: , + exact: true, + }, + { + path: '/reviews/:taskId', + element: , + children: [ + { + path: '/reviews/:taskId', + element: , + children: [ + { + index: true, + element: , + }, ], }, ], @@ -588,6 +664,11 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { exact: true, element: , }, + { + path: '/dashboard/settings', + exact: true, + element: , + }, { path: '/dashboard', // Fallback here. @@ -655,6 +736,10 @@ export function createRoutes(Components: RouteComponents): CreateRouteType { path: '*', element: , }, + { + path: '_blocks', + element: , + }, ], }, routes: routes, diff --git a/services/madoc-ts/src/frontend/tailwind/blocks/HeroCentered.tsx b/services/madoc-ts/src/frontend/tailwind/blocks/HeroCentered.tsx new file mode 100644 index 000000000..8a097f878 --- /dev/null +++ b/services/madoc-ts/src/frontend/tailwind/blocks/HeroCentered.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { Button } from '../../shared/navigation/Button'; +import { HrefLink } from '../../shared/utility/href-link'; + +type HeroCenteredProps = { + title: string; + description: string; + image: { + id: string; + image: string; + thumbnail: string; + } | null; + button?: string; + buttonLink?: string; +}; + +export function HeroCentered(props: HeroCenteredProps) { + return ( +
    +
    + {props.image ? ( + hero + ) : null} +
    +

    {props.title}

    +

    {props.description}

    + {props.button ? ( +
    + +
    + ) : null} +
    +
    +
    + ); +} + +blockEditorFor(HeroCentered, { + type: 'default.HeroCentered', + label: 'Hero centered', + anyContext: [], + defaultProps: { + title: '', + description: '', + image: null, + button: '', + buttonLink: '', + }, + editor: { + title: 'text-field', + description: 'text-field', + image: { + label: 'Image', + type: 'madoc-media-explorer', + }, + button: 'text-field', + buttonLink: 'text-field', + }, +}); diff --git a/services/madoc-ts/src/frontend/tailwind/blocks/HeroHyper.tsx b/services/madoc-ts/src/frontend/tailwind/blocks/HeroHyper.tsx new file mode 100644 index 000000000..4282e1e7d --- /dev/null +++ b/services/madoc-ts/src/frontend/tailwind/blocks/HeroHyper.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { Button } from '../../shared/navigation/Button'; +import { HrefLink } from '../../shared/utility/href-link'; + +// Source: https://www.hyperui.dev/components/marketing/banners#component-3 +interface HeroHyperProps { + title: string; + titleEmphasis?: string; + description: string; + button?: string; + buttonLink?: string; + image: { + id: string; + image: string; + thumbnail: string; + } | null; + secondButton?: string; + secondButtonLink?: string; +} +export function HeroHyper(props: HeroHyperProps) { + return ( +
    +
    + +
    +
    +

    + {props.title} + {props.titleEmphasis ? ( + {props.titleEmphasis} + ) : null} +

    + +

    {props.description}

    + +
    + {props.button ? ( + + ) : null} + + {props.secondButton ? ( + + ) : null} +
    +
    +
    +
    + ); +} + +blockEditorFor(HeroHyper, { + type: 'default.HeroHyper', + label: 'Hero hyper', + anyContext: [], + defaultProps: { + title: '', + titleEmphasis: '', + description: '', + image: null, + button: '', + buttonLink: '', + secondButton: '', + secondButtonLink: '', + }, + editor: { + title: 'text-field', + titleEmphasis: 'text-field', + description: 'text-field', + image: { + label: 'Image', + type: 'madoc-media-explorer', + }, + buttonLink: 'text-field', + button: 'text-field', + secondButtonLink: 'text-field', + secondButton: 'text-field', + }, +}); diff --git a/services/madoc-ts/src/frontend/tailwind/blocks/project/EmailProjectMembers.tsx b/services/madoc-ts/src/frontend/tailwind/blocks/project/EmailProjectMembers.tsx new file mode 100644 index 000000000..e72af10f5 --- /dev/null +++ b/services/madoc-ts/src/frontend/tailwind/blocks/project/EmailProjectMembers.tsx @@ -0,0 +1,55 @@ +import { useTranslation } from 'react-i18next'; +import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; +import { ModalButton } from '../../../shared/components/Modal'; +import { apiHooks } from '../../../shared/hooks/use-api-query'; +import { useUser } from '../../../shared/hooks/use-site'; +import { MailIcon } from '../../../shared/icons/MailIcon'; +import { useRouteContext } from '../../../site/hooks/use-route-context'; + +export function EmailProjectMembers() { + const { t } = useTranslation(); + const { projectId } = useRouteContext(); + const { data } = apiHooks.getProjectMemberEmails(() => (projectId ? [projectId] : undefined)); + const user = useUser(); + const isAdmin = user && user.scope && user.scope.indexOf('site.admin') !== -1; + + if (!isAdmin || !projectId) { + return null; + } + + const users = data?.users; + return ( + { + return ( + <> + {!users?.length ? ( +
    {t('No emails available')}
    + ) : ( + <> +

    + {t('Only users who have made their emails accessible are listed')} +

    +