From cf4fffa7af6c1060b3993ebff973771bc7f3efd8 Mon Sep 17 00:00:00 2001 From: Heather0K Date: Wed, 31 May 2023 16:26:17 +0100 Subject: [PATCH 001/130] WIP dependant fields in capture model --- .../model-editor/full-document-editor.tsx | 1 + .../components/FieldEditor/FieldEditor.tsx | 30 ++++++++++++++++++- .../capture-models/types/base-property.ts | 1 + .../capture-models/types/field-types.ts | 1 + services/madoc-ts/translations/en/madoc.json | 8 +++++ 5 files changed, 40 insertions(+), 1 deletion(-) 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..bae5e12f5 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 @@ -69,6 +69,7 @@ export const FullDocumentEditor: React.FC = () => {
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); @@ -57,6 +59,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 +73,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 +85,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 +131,29 @@ export const FieldEditor: React.FC<{ ) : null} + {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.value.id, + } + : null + )} + value={dependantField} + onChange={val => { + setDependantField(val || undefined); + }} + /> + + )} {dataSources ? ( 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/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index 256923d75..4d8147770 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -1,4 +1,5 @@ { + "..": "..", "...": "...", "A longer description for your form field, appears under the label (like this)": "A longer description for your form field, appears under the label (like this)", "A longer description for your transcription form field, appears under the label (like this)": "A longer description for your transcription form field, appears under the label (like this)", @@ -139,6 +140,7 @@ "Checkbox options (value,label one per line)": "Checkbox options (value,label one per line)", "Choice": "Choice", "Choose": "Choose", + "Choose a field": "Choose a field", "Choose a root for the form": "Choose a root for the form", "Choose a selector": "Choose a selector", "Choose data sources": "Choose data sources", @@ -252,6 +254,11 @@ "Delete task": "Delete task", "Delete this entity": "Delete this entity", "Delete user": "Delete user", + "Dependant": "Dependant", + "Depends on": "Depends on", + "Depends on?": "Depends on?", + "Depends on? (This field will only appear if the dependant field has a value)": "Depends on? (This field will only appear if the dependant field has a value)", + "Depends on? (this field will only appear if the dependant field has a value)": "Depends on? (this field will only appear if the dependant field has a value)", "Description": "Description", "Details": "Details", "Disable next canvas prompt after submission": "Disable next canvas prompt after submission", @@ -946,6 +953,7 @@ "paused": "paused", "pending": "pending", "preview": "preview", + "project 1": "project 1", "project_id": "project_id", "projects": "projects", "rejected": "rejected", From f9adc67f549a28d2777198b1fd8bd512d52fb82b Mon Sep 17 00:00:00 2001 From: Heather0K Date: Thu, 22 Jun 2023 15:28:57 +0100 Subject: [PATCH 002/130] reduced canUSerClaim.. to canUSerClaimResource and exported can submit logic from use-manifes-user-task/use-manifes-canvas-tasks WIP --- docker-compose.yml | 3 +- .../capture-models/new/CoreModelEditor.tsx | 2 +- .../site/features/admin/SearchThisItem.tsx | 46 ++++++ .../CanvasModelCompleteMessage.tsx | 6 +- .../contributor/CanvasSimpleEditor.tsx | 13 +- .../contributor/ContinueCanvasSubmission.tsx | 21 ++- .../features/contributor/ManifestActions.tsx | 6 +- .../ManifestCaptureModelEditor.tsx | 10 +- .../features/contributor/ProjectActions.tsx | 1 + .../TranscriberModeWorkflowBar.tsx | 5 +- .../site/hooks/use-canvas-user-tasks.ts | 53 +++++-- .../site/hooks/use-manifest-user-tasks.ts | 51 ++++++- .../frontend/site/pages/view-canvas-model.tsx | 14 +- .../site/pages/view-manifest-model.tsx | 11 +- .../routes/projects/create-resource-claim.ts | 89 +++-------- .../src/routes/site/site-canvas-tasks.ts | 5 +- .../src/routes/site/site-manifest-tasks.ts | 9 +- .../madoc-ts/src/utility/claim-utilities.ts | 138 +++++++----------- services/madoc-ts/translations/en/madoc.json | 2 + 19 files changed, 256 insertions(+), 229 deletions(-) create mode 100644 services/madoc-ts/src/frontend/site/features/admin/SearchThisItem.tsx diff --git a/docker-compose.yml b/docker-compose.yml index 4ab52b696..866bd8df5 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -203,7 +203,8 @@ services: condition: service_healthy search: - image: ghcr.io/digirati-co-uk/madoc-search-service:247f854b1258e3971907e6912cef2374a1da8474 + image: ghcr.io/digirati-co-uk/madoc-search-service:main + platform: linux/amd64 # build: # context: services/search # dockerfile: Dockerfile 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 f8c4924d3..4ee617707 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 @@ -31,7 +31,7 @@ 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 { CaptureModel } from '../types/capture-model'; import { RevisionRequest } from '../types/revision-request'; diff --git a/services/madoc-ts/src/frontend/site/features/admin/SearchThisItem.tsx b/services/madoc-ts/src/frontend/site/features/admin/SearchThisItem.tsx new file mode 100644 index 000000000..82d2f897d --- /dev/null +++ b/services/madoc-ts/src/frontend/site/features/admin/SearchThisItem.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { Button } from '../../../shared/navigation/Button'; +import { useProjectPageConfiguration } from '../../hooks/use-project-page-configuration'; +import { useRelativeLinks } from '../../hooks/use-relative-links'; + +export const SearchThisItem: React.FC = () => { + const { t } = useTranslation(); + const createLink = useRelativeLinks(); + const options = useProjectPageConfiguration(); + + // Search this Project + + //https://madoc.local/s/default/projects/my-project/search + + //s/default/madoc/api/search + //facet_on_manifests true + // facets [] + // fulltext "" + // non_latin_fulltext false + // projectId "my-project" + // search_multiple_fields false + // search_type "websearch" + + ///s/default/madoc/api/search + //facet_on_manifests true + // facets [] + // fulltext "" + // iiif_type "Manifest" + // non_latin_fulltext false + // search_multiple_fields false + // search_type "websearch" + + // Search this Manifest + // Search this Collection + return ( +
+ {!options.hideSearchButton ? ( + + ) : null} +
+ ); +}; diff --git a/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx b/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx index ed880b26d..00331e5d7 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx +++ b/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx @@ -12,7 +12,7 @@ export const CanvasModelCompleteMessage: React.FC = () => { const { projectId } = useRouteContext(); const { isManifestComplete, hasExpired } = useManifestTask(); const user = useUser(); - const { canUserSubmit, isLoading: isLoadingTasks, completedAndHide, completed } = useCanvasUserTasks(); + const { canSubmit, canCanvasTakeSubmission, isLoading: isLoadingTasks, completed } = useCanvasUserTasks(); const { t } = useTranslation(); @@ -21,8 +21,8 @@ export const CanvasModelCompleteMessage: React.FC = () => { const projectPaused = !isActive && !isPreparing; const hideModelEditor = - (!canUserSubmit && !isLoadingTasks) || - completedAndHide || + (!canSubmit && !isLoadingTasks) || + canCanvasTakeSubmission || isManifestComplete || hasExpired || (!isActive && !isPreparing); diff --git a/services/madoc-ts/src/frontend/site/features/contributor/CanvasSimpleEditor.tsx b/services/madoc-ts/src/frontend/site/features/contributor/CanvasSimpleEditor.tsx index 04bafcd4b..aa15baa28 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/CanvasSimpleEditor.tsx +++ b/services/madoc-ts/src/frontend/site/features/contributor/CanvasSimpleEditor.tsx @@ -26,7 +26,7 @@ export function CanvasSimpleEditor({ revision, isSegmentation }: CanvasSimpleEdi const { projectId, canvasId } = useRouteContext(); const { data: projectModel } = useCanvasModel(); const [{ captureModel }, , modelRefetch] = useLoadedCaptureModel(projectModel?.model?.id, undefined, canvasId); - const { updateClaim, allTasksDone, maxContributorsReached, canUserSubmit, markedAsUnusable } = useCanvasUserTasks(); + const { updateClaim, preventFurtherSubmission, canContribute, markedAsUnusable } = useCanvasUserTasks(); const { isPreparing } = useProjectStatus(); const annotationTheme = useProjectAnnotationStyles(); const user = useCurrentUser(true); @@ -42,17 +42,13 @@ export function CanvasSimpleEditor({ revision, isSegmentation }: CanvasSimpleEdi const api = useApi(); const readOnlyAnnotations = useReadOnlyAnnotations(true); const allowMultiple = !config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource; + const hideViewerControls = !!config.project.modelPageOptions?.hideViewerControls; const forkMode = !!config.project.forkMode; - const preventFurtherSubmission = (!allowMultiple && allTasksDone) || (allTasksDone && maxContributorsReached); + const isEditing = isEditingAnotherUsersRevision(captureModel, revision, user.user); + const { isAdmin } = useUserPermissions(); - const canContribute = - user && - user.scope && - (user.scope.indexOf('site.admin') !== -1 || - user.scope.indexOf('models.admin') !== -1 || - user.scope.indexOf('models.contribute') !== -1); const isModelAdmin = user && user.scope && (user.scope.indexOf('site.admin') !== -1 || user.scope.indexOf('models.admin') !== -1); @@ -60,7 +56,6 @@ export function CanvasSimpleEditor({ revision, isSegmentation }: CanvasSimpleEdi if (api.getIsServer() || !canvasId || !projectId || (isPreparing && !isModelAdmin)) { return null; } - return ( { 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 +89,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 +122,7 @@ export const ContinueCanvasSubmission: React.FC = () => { ); } - if (canContribute && (canClaimCanvas || canClaimManifest || userManifestTask)) { + if (canCanvasTakeSubmission && (canClaimManifest || userManifestTask)) { return ( - ) : null} -
- ); -}; diff --git a/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx b/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx index 00331e5d7..be3647844 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx +++ b/services/madoc-ts/src/frontend/site/features/contributor/CanvasModelCompleteMessage.tsx @@ -12,7 +12,7 @@ export const CanvasModelCompleteMessage: React.FC = () => { const { projectId } = useRouteContext(); const { isManifestComplete, hasExpired } = useManifestTask(); const user = useUser(); - const { canSubmit, canCanvasTakeSubmission, isLoading: isLoadingTasks, completed } = useCanvasUserTasks(); + const { canUserSubmit, canCanvasTakeSubmission, isLoading: isLoadingTasks, completed } = useCanvasUserTasks(); const { t } = useTranslation(); @@ -21,7 +21,7 @@ export const CanvasModelCompleteMessage: React.FC = () => { const projectPaused = !isActive && !isPreparing; const hideModelEditor = - (!canSubmit && !isLoadingTasks) || + (!canUserSubmit && !isLoadingTasks) || canCanvasTakeSubmission || isManifestComplete || hasExpired || diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx b/services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx index c3ac0a3f3..708f11b8f 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx +++ b/services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx @@ -18,7 +18,7 @@ export const ProjectActions: React.FC = () => { const { isActive } = useProjectStatus(); const { t } = useTranslation(); const createLink = useRelativeLinks(); - console.log(project) + const { project: { allowCollectionNavigation = true, allowManifestNavigation = true, claimGranularity, allowPersonalNotes }, } = useSiteConfiguration(); diff --git a/services/madoc-ts/src/frontend/site/features/contributor/TranscriberModeWorkflowBar.tsx b/services/madoc-ts/src/frontend/site/features/contributor/TranscriberModeWorkflowBar.tsx index 59c338bed..b13ce23c6 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/TranscriberModeWorkflowBar.tsx +++ b/services/madoc-ts/src/frontend/site/features/contributor/TranscriberModeWorkflowBar.tsx @@ -20,7 +20,7 @@ export const TranscriberModeWorkflowBar: React.FC = () => { const { fixedTranscriptionBar } = useModelPageConfiguration(); const invalidate = useInvalidateAfterSubmission(); const { isManifestComplete, userManifestStats, filteredTasks } = useManifestTask(); - const { userTasks, canSubmit, markedAsUnusable } = useCanvasUserTasks(); + const { userTasks, canUserSubmit, markedAsUnusable } = useCanvasUserTasks(); const { inReview } = filteredTasks; const { submitAllClaims, isSubmitting, canSubmit: canSubmitClaims } = useSubmitAllClaims(); const { projectId, canvasId, manifestId } = useRouteContext(); @@ -39,6 +39,7 @@ export const TranscriberModeWorkflowBar: React.FC = () => { // The amount of tasks completed matches the amount of items. !!(structure?.items.length && userManifestStats && userManifestStats?.done === structure?.items.length); + const canSubmit = !!canUserSubmit && canSubmitClaims; const [isUnusable, setIsUsable] = useState(false); useEffect(() => { @@ -118,7 +119,7 @@ export const TranscriberModeWorkflowBar: React.FC = () => { willExpireSoon, isComplete, isSubmitted, - canSubmit: !!canSubmit && canSubmitClaims, + canSubmit, isUnusable, hasExpired, }} diff --git a/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts b/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts index 294f0b8b7..17a831095 100644 --- a/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts +++ b/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts @@ -78,7 +78,7 @@ export function useCanvasUserTasks() { ? canvasTask.maxContributors <= canvasTask.totalContributors : false; - const canSubmit = user && !!canvasTask?.canUserSubmit; + const canUserSubmit = user && !!canvasTask?.canUserSubmit; const cantSubmitAfterRejection = config.project.modelPageOptions?.preventContributionAfterRejection ? userTasks?.some(task => task.status === -1) @@ -98,7 +98,7 @@ export function useCanvasUserTasks() { const canCanvasTakeSubmission = canClaimCanvas && cantSubmitMultiple && completedAndHide && maxContributorsReached; - const canUserSubmit = canContribute && canSubmit && cantSubmitAfterRejection && cantSubmitAfterSubmission; + const canSubmitTask = canContribute && canUserSubmit && cantSubmitAfterRejection && cantSubmitAfterSubmission; const preventFurtherSubmission = canCanvasTakeSubmission && canUserSubmit; @@ -122,7 +122,6 @@ export function useCanvasUserTasks() { completedAndHide, completed, canClaimCanvas, - canSubmit, canContribute, maxContributorsReached, updateClaim, 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 e177a00ca..1c843502d 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 @@ -28,7 +28,7 @@ export const ViewCanvasModel: React.FC = () => { const { canvasId } = useRouteContext(); const { showCanvasNavigation, showWarning } = useCanvasNavigation(); const { isManifestComplete, hasExpired } = useManifestTask(); - const { canSubmit, canContribute, isLoading: isLoadingTasks, completedAndHide } = useCanvasUserTasks(); + const { canUserSubmit, canContribute, isLoading: isLoadingTasks, completedAndHide } = useCanvasUserTasks(); const { goToNext } = useLocationQuery(); const shouldGoToNext = castBool(goToNext); @@ -40,9 +40,8 @@ export const ViewCanvasModel: React.FC = () => { const { showCaptureModelOnManifest } = useProjectShadowConfiguration(); - const isReadOnly = - (!canSubmit && !isLoadingTasks) || + (!canUserSubmit && !isLoadingTasks) || completedAndHide || isManifestComplete || hasExpired || diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index a16f14fdf..a15c3c338 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -992,7 +992,6 @@ "paused": "paused", "pending": "pending", "preview": "preview", - "project 1": "project 1", "projects": "projects", "rejected": "rejected", "remove": "remove", From fe513d95530a4b466ec0f2bc250dfa60a4de1efd Mon Sep 17 00:00:00 2001 From: Heather0K Date: Fri, 23 Jun 2023 12:48:33 +0100 Subject: [PATCH 004/130] fix some logic for max contributors and drafts --- .../site/hooks/use-canvas-user-tasks.ts | 42 ++++++++++--------- services/madoc-ts/translations/en/madoc.json | 1 + 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts b/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts index 17a831095..bbc4ff392 100644 --- a/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts +++ b/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts @@ -61,46 +61,48 @@ export function useCanvasUserTasks() { 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); + (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 maxContributorsReached = + const maxContributors = canvasTask?.maxContributors && canvasTask.totalContributors - ? canvasTask.maxContributors <= canvasTask.totalContributors + ? canvasTask.maxContributors >= canvasTask.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 && !!canvasTask?.canUserSubmit; - const cantSubmitAfterRejection = config.project.modelPageOptions?.preventContributionAfterRejection - ? userTasks?.some(task => task.status === -1) - : false; + const canSubmitAfterRejection = config.project.modelPageOptions?.preventContributionAfterRejection + ? userTasks?.some(task => task.status !== -1) + : true; - const cantSubmitAfterSubmission = config.project.modelPageOptions?.preventContributionAfterSubmission - ? userContributions?.some(task => task.status === 2) - : false; + const canSubmitAfterSubmission = config.project.modelPageOptions?.preventContributionAfterSubmission + ? userContributions?.some(task => task.status !== 2) + : true; - const cantSubmitMultiple = config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource - ? userContributions.length > 0 - : false; + const canSubmitMultiple = config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource + ? !userContributions || userContributions.length === 0 + : true; const allTasksDone = userContributions.length ? !userContributions.find(t => t.status === 0 || t.status === 1) : false; - const canCanvasTakeSubmission = canClaimCanvas && cantSubmitMultiple && completedAndHide && maxContributorsReached; + const completedAndHide = !config.project.allowSubmissionsWhenCanvasComplete && canvasTask?.canvasTask?.status === 3; + const completed = canvasTask?.canvasTask?.status === 3; - const canSubmitTask = canContribute && canUserSubmit && cantSubmitAfterRejection && cantSubmitAfterSubmission; + const canCanvasTakeSubmission = canClaimCanvas && canSubmitMultiple && !completedAndHide && !maxContributorsReached; - const preventFurtherSubmission = canCanvasTakeSubmission && canUserSubmit; + const canSubmitTask = canContribute && canUserSubmit && canSubmitAfterRejection && canSubmitAfterSubmission; + const preventFurtherSubmission = !canCanvasTakeSubmission || !canSubmitTask; const markedAsUnusable = allTasksDone && diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index a15c3c338..a16f14fdf 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -992,6 +992,7 @@ "paused": "paused", "pending": "pending", "preview": "preview", + "project 1": "project 1", "projects": "projects", "rejected": "rejected", "remove": "remove", From ef184d7ef8252c0ce6bf79fa505694ae4c6611e3 Mon Sep 17 00:00:00 2001 From: Heather0K Date: Mon, 26 Jun 2023 11:36:59 +0100 Subject: [PATCH 005/130] use-manifest-user-tasks changes --- .../schemas/ProjectConfiguration.json | 51 ++ .../schemas/ProjectConfigurationNEW.json | 440 ++++++++++++++++++ .../ManifestCaptureModelEditor.tsx | 9 +- .../contributor/ManifestUserNotification.tsx | 5 +- .../site/hooks/use-canvas-user-tasks.ts | 19 +- .../site/hooks/use-continue-submission.ts | 15 +- .../site/hooks/use-manifest-user-tasks.ts | 72 ++- services/madoc-ts/translations/en/madoc.json | 1 + yarn.lock | 4 + 9 files changed, 540 insertions(+), 76 deletions(-) create mode 100644 services/madoc-ts/schemas/ProjectConfigurationNEW.json create mode 100644 yarn.lock diff --git a/services/madoc-ts/schemas/ProjectConfiguration.json b/services/madoc-ts/schemas/ProjectConfiguration.json index 9b2649577..06bb40448 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" }, diff --git a/services/madoc-ts/schemas/ProjectConfigurationNEW.json b/services/madoc-ts/schemas/ProjectConfigurationNEW.json new file mode 100644 index 000000000..c2eebf1d4 --- /dev/null +++ b/services/madoc-ts/schemas/ProjectConfigurationNEW.json @@ -0,0 +1,440 @@ +{ + "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" + } + } + }, + "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/frontend/site/features/contributor/ManifestCaptureModelEditor.tsx b/services/madoc-ts/src/frontend/site/features/contributor/ManifestCaptureModelEditor.tsx index f3d4304a6..1947c9fe4 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ManifestCaptureModelEditor.tsx +++ b/services/madoc-ts/src/frontend/site/features/contributor/ManifestCaptureModelEditor.tsx @@ -32,9 +32,8 @@ 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, preventFurtherSubmission, canContribute, markedAsUnusable } = useManifestUserTasks(); + const { updateClaim, preventFurtherSubmission, canContribute, markedAsUnusable, user } = useManifestUserTasks(); const { isPreparing } = useProjectStatus(); - const user = useCurrentUser(true); const config = useSiteConfiguration(); const { disableSaveForLater = false, @@ -45,10 +44,8 @@ export function ManifestCaptureModelEditor({ revision }: { revision: string; isS const [postSubmission, setPostSubmission] = useState(false); const [postSubmissionMessage, setPostSubmissionMessage] = useState(false); const allowMultiple = !config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource; - const isEditing = isEditingAnotherUsersRevision(captureModel, revision, user.user); + const isEditing = isEditingAnotherUsersRevision(captureModel, revision, user); - const isModelAdmin = - user && user.scope && (user.scope.indexOf('site.admin') !== -1 || user.scope.indexOf('models.admin') !== -1); const features: RevisionProviderFeatures = isPreparing ? { autosave: false, @@ -87,7 +84,7 @@ export function ManifestCaptureModelEditor({ revision }: { revision: string; isS } } - if (api.getIsServer() || !manifestId || !projectId || (isPreparing && !isModelAdmin)) { + if (api.getIsServer() || !manifestId || !projectId || (isPreparing && !canContribute)) { return null; } diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ManifestUserNotification.tsx b/services/madoc-ts/src/frontend/site/features/contributor/ManifestUserNotification.tsx index 63a591e93..91a18d532 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ManifestUserNotification.tsx +++ b/services/madoc-ts/src/frontend/site/features/contributor/ManifestUserNotification.tsx @@ -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/hooks/use-canvas-user-tasks.ts b/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts index bbc4ff392..d7ab199ee 100644 --- a/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts +++ b/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts @@ -1,6 +1,5 @@ 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 { useSiteConfiguration } from '../features/SiteConfigurationContext'; @@ -50,12 +49,6 @@ export function useCanvasUserTasks() { ); return useMemo(() => { - 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 @@ -81,7 +74,7 @@ export function useCanvasUserTasks() { const canUserSubmit = user && !!canvasTask?.canUserSubmit; const canSubmitAfterRejection = config.project.modelPageOptions?.preventContributionAfterRejection - ? userTasks?.some(task => task.status !== -1) + ? !userTasks?.some(task => task.status === -1) : true; const canSubmitAfterSubmission = config.project.modelPageOptions?.preventContributionAfterSubmission @@ -99,10 +92,10 @@ export function useCanvasUserTasks() { const completedAndHide = !config.project.allowSubmissionsWhenCanvasComplete && canvasTask?.canvasTask?.status === 3; const completed = canvasTask?.canvasTask?.status === 3; - const canCanvasTakeSubmission = canClaimCanvas && canSubmitMultiple && !completedAndHide && !maxContributorsReached; + const canCanvasTakeSubmission = canClaimCanvas && !completedAndHide && !maxContributorsReached; + const canSubmitAnother = canSubmitMultiple && canSubmitAfterRejection && canSubmitAfterSubmission; - const canSubmitTask = canContribute && canUserSubmit && canSubmitAfterRejection && canSubmitAfterSubmission; - const preventFurtherSubmission = !canCanvasTakeSubmission || !canSubmitTask; + const preventFurtherSubmission = !canCanvasTakeSubmission || !canSubmitAnother || !(canContribute && canUserSubmit); const markedAsUnusable = allTasksDone && @@ -114,18 +107,14 @@ export function useCanvasUserTasks() { user, canvasTask: canvasTask?.canvasTask, isLoading, - reviews, userTasks, markedAsUnusable, - isManifestComplete: canvasTask?.isManifestComplete, - allTasksDone, canCanvasTakeSubmission, canUserSubmit, completedAndHide, completed, canClaimCanvas, canContribute, - maxContributorsReached, updateClaim, 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 f93a5ecf4..a550939ef 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,6 +1,5 @@ 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'; @@ -48,66 +47,61 @@ 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 canUserSubmit = user && !!manifestTask?.canUserSubmit; const canContribute = user && - (scope.indexOf('site.admin') !== -1 || - scope.indexOf('models.contribute') !== -1 || - scope.indexOf('models.admin') !== -1); - - const allTasksDone = userContributions.length - ? !userContributions.find(t => t.status === 0 || t.status === 1) - : false; + (scope.indexOf('site.admin') === -1 || + scope.indexOf('models.contribute') === -1 || + scope.indexOf('models.admin') === -1); - const markedAsUnusable = - allTasksDone && - (userContributions.length - ? !!userContributions.find(t => (t.status === 2 || t.status === 3) && !t.state.revisionId) - : false); - - const maxContributorsReached = + const maxContributors = manifestTask?.maxContributors && manifestTask.totalContributors - ? manifestTask.maxContributors <= manifestTask.totalContributors + ? manifestTask.maxContributors >= manifestTask.totalContributors : false; - const cantSubmitAfterRejection = config.project.modelPageOptions?.preventContributionAfterRejection - ? userTasks?.some(task => task.status === -1) - : 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 cantSubmitAfterSubmission = config.project.modelPageOptions?.preventContributionAfterSubmission - ? userContributions?.some(task => task.status === 2) - : false; + const canUserSubmit = user && !!manifestTask?.canUserSubmit; + + 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 + : true; - const cantSubmitMultiple = config.project.modelPageOptions?.preventMultipleUserSubmissionsPerResource - ? userContributions.length > 0 + 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 = - !canContribute && - !canUserSubmit && + !canManifestTakeSubmission || !canSubmitAnother || !(canContribute && canUserSubmit); + + const markedAsUnusable = allTasksDone && - maxContributorsReached && - cantSubmitAfterRejection && - cantSubmitAfterSubmission && - cantSubmitMultiple; + (userContributions.length + ? !!userContributions.find(t => (t.status === 2 || t.status === 3) && !t.state.revisionId) + : false); return { user, isLoading, manifestTask: manifestTask?.manifestTask, - reviews, userTasks, markedAsUnusable, isManifestComplete: manifestTask?.isManifestComplete, diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index a16f14fdf..f8a23dbbb 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -760,6 +760,7 @@ "Tabs text": "Tabs text", "Tag": "Tag", "Task is complete!": "Task is complete!", + "Task is rejected!": "Task is rejected!", "Tasks": "Tasks", "Text align": "Text align", "Text color": "Text color", diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + From 8de9a081a5942824cfa34ed2939fbbb497e8e933 Mon Sep 17 00:00:00 2001 From: Heather0K Date: Tue, 27 Jun 2023 10:34:54 +0100 Subject: [PATCH 006/130] fix maxContirbutors reached --- .../madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts | 2 +- .../src/frontend/site/hooks/use-manifest-user-tasks.ts | 2 +- services/madoc-ts/translations/en/madoc.json | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts b/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts index d7ab199ee..0e7c90f6e 100644 --- a/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts +++ b/services/madoc-ts/src/frontend/site/hooks/use-canvas-user-tasks.ts @@ -69,7 +69,7 @@ export function useCanvasUserTasks() { : 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 maxContributorsReached = maxContributors ? !userTasks?.some(t => t.type === 'crowdsourcing-task') : false; const canUserSubmit = user && !!canvasTask?.canUserSubmit; 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 a550939ef..c5eaf0f47 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 @@ -64,7 +64,7 @@ export function useManifestUserTasks() { : 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 maxContributorsReached = maxContributors ? !userTasks?.some(t => t.type === 'crowdsourcing-task') : false; const canUserSubmit = user && !!manifestTask?.canUserSubmit; diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index c0e9f0f6c..94c3e2051 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -227,6 +227,7 @@ "Create new project": "Create new project", "Create new style": "Create new style", "Create project": "Create project", + "Create site": "Create site", "Create translation": "Create translation", "Create translation for {{name}}": "Create translation for {{name}}", "Create user": "Create user", @@ -764,7 +765,6 @@ "Tabs text": "Tabs text", "Tag": "Tag", "Task is complete!": "Task is complete!", - "Task is rejected!": "Task is rejected!", "Tasks": "Tasks", "Text align": "Text align", "Text color": "Text color", @@ -997,7 +997,6 @@ "paused": "paused", "pending": "pending", "preview": "preview", - "project 1": "project 1", "projects": "projects", "rejected": "rejected", "remove": "remove", From 6799cf8f2f622f505cdfe0f7a2f5e839714b4d0d Mon Sep 17 00:00:00 2001 From: Heather0K Date: Wed, 28 Jun 2023 10:44:48 +0100 Subject: [PATCH 007/130] fix empty document not showing in left panel --- .env | 2 +- docker-compose.yml | 18 +++++++++++------- .../shared/hooks/use-annotation-page.ts | 1 - .../site/hooks/canvas-menu/document-panel.tsx | 7 +++++-- services/shared-postgres/Dockerfile | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.env b/.env index eebd4f302..96875726d 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$GHLbFCfotntr9O0wvgO9iuHPl6pSrfjZfveYdsTkHPcskETg8KjLi' # HTTPS # LOCAL PORT FOR HTTPS, CAN BE CHANGED diff --git a/docker-compose.yml b/docker-compose.yml index 866bd8df5..7958e3762 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -99,6 +99,7 @@ services: tasks-api: tty: true image: ghcr.io/digirati-co-uk/tasks-api:latest + platform: linux/amd64 environment: - SERVER_PORT=3000 - DATABASE_HOST=shared-postgres @@ -119,6 +120,7 @@ services: model-api: tty: true image: digirati/capture-models:latest + platform: linux/amd64 environment: - SERVER_PORT=3000 - DATABASE_HOST=shared-postgres @@ -171,6 +173,7 @@ services: config-service: image: digirati/madoc_config_service_django:175410fc5b7dbef4cc259686564fbedeb60c8789 + platform: linux/amd64 environment: - USE_DOCKER=yes - IPYTHONDIR=/app/.ipython @@ -194,6 +197,7 @@ services: storage-api: tty: true image: ghcr.io/digirati-co-uk/storage-api:main + platform: linux/amd64 environment: - GATEWAY_HOST=${GATEWAY_HOST} volumes: @@ -203,11 +207,11 @@ services: condition: service_healthy search: - image: ghcr.io/digirati-co-uk/madoc-search-service:main + image: ghcr.io/digirati-co-uk/madoc-search-service:247f854b1258e3971907e6912cef2374a1da8474 platform: linux/amd64 -# build: -# context: services/search -# dockerfile: Dockerfile + # build: + # context: services/search + # dockerfile: Dockerfile environment: - BROWSABLE=False - USE_DOCKER=yes @@ -226,9 +230,9 @@ services: - DATABASE_URL=postgres://${POSTGRES_SEARCH_API_USER}:${POSTGRES_SEARCH_API_PASSWORD}@shared-postgres:${POSTGRES_PORT}/${POSTGRES_DB} links: - shared-postgres -# volumes: -# - ./services/search/search_service:/app + # volumes: + # - ./services/search/search_service:/app okra: image: digirati/okra:latest - + platform: linux/amd64 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/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/shared-postgres/Dockerfile b/services/shared-postgres/Dockerfile index ebbfa04c3..12daade64 100644 --- a/services/shared-postgres/Dockerfile +++ b/services/shared-postgres/Dockerfile @@ -1,4 +1,4 @@ -FROM library/postgres:12 +FROM library/postgres:12-bullseye ENV TASKS_API_PASSWORD=tasks_api_password ENV MODEL_API_PASSWORD=model_api_password From 691a3815f82d9415400bc61d0e162ed77c0b8080 Mon Sep 17 00:00:00 2001 From: Heather0K Date: Fri, 30 Jun 2023 11:42:06 +0100 Subject: [PATCH 008/130] tab component on project page --- .../manifests/edit-manifest-structure.tsx | 5 +- .../{ => projectDash}/ProjectCollections.tsx | 19 ++- .../projectDash/ProjectContentTab.tsx | 15 +++ .../{ => projectDash}/ProjectManifests.tsx | 26 ++-- .../features/projectDash/ProjectMyWorkTab.tsx | 14 +++ .../site/features/projectDash/ProjectTabs.tsx | 117 ++++++++++++++++++ .../src/frontend/site/pages/view-project.tsx | 8 +- services/madoc-ts/translations/en/madoc.json | 5 + 8 files changed, 176 insertions(+), 33 deletions(-) rename services/madoc-ts/src/frontend/site/features/{ => projectDash}/ProjectCollections.tsx (70%) create mode 100644 services/madoc-ts/src/frontend/site/features/projectDash/ProjectContentTab.tsx rename services/madoc-ts/src/frontend/site/features/{ => projectDash}/ProjectManifests.tsx (77%) create mode 100644 services/madoc-ts/src/frontend/site/features/projectDash/ProjectMyWorkTab.tsx create mode 100644 services/madoc-ts/src/frontend/site/features/projectDash/ProjectTabs.tsx 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/site/features/ProjectCollections.tsx b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectCollections.tsx similarity index 70% rename from services/madoc-ts/src/frontend/site/features/ProjectCollections.tsx rename to services/madoc-ts/src/frontend/site/features/projectDash/ProjectCollections.tsx index 025482ee2..b290a684b 100644 --- a/services/madoc-ts/src/frontend/site/features/ProjectCollections.tsx +++ b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectCollections.tsx @@ -1,15 +1,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useMatch, useLocation } from 'react-router-dom'; -import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { Button } from '../../shared/navigation/Button'; -import { Heading3 } from '../../shared/typography/Heading3'; -import { ImageGrid } from '../../shared/atoms/ImageGrid'; -import { CollectionSnippet } from '../../shared/components/CollectionSnippet'; -import { apiHooks } from '../../shared/hooks/use-api-query'; -import { HrefLink } from '../../shared/utility/href-link'; -import { useProject } from '../hooks/use-project'; -import { useSiteConfiguration } from './SiteConfigurationContext'; +import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; +import { Button } from '../../../shared/navigation/Button'; +import { Heading3 } from '../../../shared/typography/Heading3'; +import { ImageGrid } from '../../../shared/atoms/ImageGrid'; +import { CollectionSnippet } from '../../../shared/components/CollectionSnippet'; +import { apiHooks } from '../../../shared/hooks/use-api-query'; +import { HrefLink } from '../../../shared/utility/href-link'; +import { useProject } from '../../hooks/use-project'; +import { useSiteConfiguration } from '../SiteConfigurationContext'; export const ProjectCollections: React.FC = () => { const { data: project } = useProject(); diff --git a/services/madoc-ts/src/frontend/site/features/projectDash/ProjectContentTab.tsx b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectContentTab.tsx new file mode 100644 index 000000000..1299f5a2b --- /dev/null +++ b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectContentTab.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Slot } from '../../../shared/page-blocks/slot'; +import { ProjectCollections } from './ProjectCollections'; +import { ProjectManifests } from './ProjectManifests'; + +export const ProjectContentTab: React.FC = () => { + const { t } = useTranslation(); + return ( + + + + + ); +}; diff --git a/services/madoc-ts/src/frontend/site/features/ProjectManifests.tsx b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectManifests.tsx similarity index 77% rename from services/madoc-ts/src/frontend/site/features/ProjectManifests.tsx rename to services/madoc-ts/src/frontend/site/features/projectDash/ProjectManifests.tsx index 1a40981ba..b6639bf9a 100644 --- a/services/madoc-ts/src/frontend/site/features/ProjectManifests.tsx +++ b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectManifests.tsx @@ -1,18 +1,18 @@ -import React, { useRef } from 'react'; +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 { CanvasStatus } from '../../shared/atoms/CanvasStatus'; -import { Heading3 } from '../../shared/typography/Heading3'; -import { SingleLineHeading5, Subheading5 } from '../../shared/typography/Heading5'; -import { ImageGrid, ImageGridItem } from '../../shared/atoms/ImageGrid'; -import { CroppedImage } from '../../shared/atoms/Images'; -import { LocaleString, useCreateLocaleString } from '../../shared/components/LocaleString'; -import { useSubjectMap } from '../../shared/hooks/use-subject-map'; -import { useProject } from '../hooks/use-project'; -import { useProjectManifests } from '../hooks/use-project-manifests'; -import { useSiteConfiguration } from './SiteConfigurationContext'; -import { ImageStripBox } from '../../shared/atoms/ImageStrip'; +import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; +import { CanvasStatus } from '../../../shared/atoms/CanvasStatus'; +import { Heading3 } from '../../../shared/typography/Heading3'; +import { SingleLineHeading5, Subheading5 } from '../../../shared/typography/Heading5'; +import { ImageGrid } from '../../../shared/atoms/ImageGrid'; +import { CroppedImage } from '../../../shared/atoms/Images'; +import { LocaleString, useCreateLocaleString } from '../../../shared/components/LocaleString'; +import { useSubjectMap } from '../../../shared/hooks/use-subject-map'; +import { useProject } from '../../hooks/use-project'; +import { useProjectManifests } from '../../hooks/use-project-manifests'; +import { useSiteConfiguration } from '../SiteConfigurationContext'; +import { ImageStripBox } from '../../../shared/atoms/ImageStrip'; export function ProjectManifests(props: { background?: string; diff --git a/services/madoc-ts/src/frontend/site/features/projectDash/ProjectMyWorkTab.tsx b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectMyWorkTab.tsx new file mode 100644 index 000000000..1925cc314 --- /dev/null +++ b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectMyWorkTab.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ProjectContributionButton } from '../contributor/ProjectContributionButton'; +import { Slot } from '../../../shared/page-blocks/slot'; + +export const ProjectMyWorkTab: React.FC = () => { + const { t } = useTranslation(); + // dunno if this will work? nested slots seem danger + return ( + + + + ); +}; diff --git a/services/madoc-ts/src/frontend/site/features/projectDash/ProjectTabs.tsx b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectTabs.tsx new file mode 100644 index 000000000..46a1c980b --- /dev/null +++ b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectTabs.tsx @@ -0,0 +1,117 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { blockEditorFor } from '../../../../extensions/page-blocks/block-editor-for'; +import styled from 'styled-components'; +import { ProjectMyWorkTab } from './ProjectMyWorkTab'; +import {ProjectContentTab} from "./ProjectContentTab"; + +const TabContainer = styled.div` + display: flex; + justify-content: flex-start; + position: relative; + &:after { + height: 2px; + background-color: #999; + opacity: 0.5; + content: ''; + width: 90%; + position: absolute; + bottom: 0; + z-index: 0; + } +`; + +const Tab = styled.button` + margin-right: 24px; + font-size: 14px; + border-radius: 5px; + border: rgba(112, 112, 112, 0.05); + background-color: transparent; + color: #6b6b6b; + cursor: pointer; + position: relative; + &:before { + transition: all 100ms ease-in-out; + content: ''; + width: 100%; + position: absolute; + bottom: 0; + left: 0; + } + &[data-is-active='true'] { + color: black; + &:before { + transition: all 100ms ease-in-out; + background-color: #3498db; + height: 2px; + } + } + + :hover { + } +`; + +const TabContent = styled.div` + padding: 2em; +`; + +export const ProjectTabs: React.FC = () => { + const { t } = useTranslation(); + + const [tab, setTab] = useState('1'); + + const Tab1Content =

Hiya im tab1

; + const Tab2Content =

Hiya im tab2

; + + const tabs = [ + { + id: '1', + title: 'My work', + content: , + }, + { + id: '2', + title: 'Manifests and Collections', + content: , + }, + { + id: '3', + title: 'Contributors', + content: Tab2Content, + }, + ]; + + const SelectedTab = tabs.find(tb => tb.id === tab); + + return ( + <> + + {tabs.map((tb, index) => { + return ( + { + setTab(tb.id); + }} + > + {t(`${tb.title}`)} + + ); + })} + + {SelectedTab?.content} + + ); +}; + +blockEditorFor(ProjectTabs, { + type: 'default.ProjectTabs', + label: 'Project tabs', + anyContext: ['project'], + requiredContext: ['project'], + editor: {}, +}); 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..5591a6d34 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project.tsx @@ -4,12 +4,10 @@ import { DisplayBreadcrumbs } from '../../shared/components/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 { ProjectStatistics } from '../features/ProjectStatistics'; +import { ProjectTabs } from '../features/projectDash/ProjectTabs'; export const ViewProject: React.FC = () => { const available = ( @@ -32,14 +30,12 @@ export const ViewProject: React.FC = () => { - {available} - - + {available} diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index 9bd08472b..3566c5f97 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -210,6 +210,7 @@ "Contributions": "Contributions", "Contributions in progress": "Contributions in progress", "Contributions in review": "Contributions in review", + "Contributors": "Contributors", "Contributors per resource": "Contributors per resource", "Could not load external OCR": "Could not load external OCR", "Could not load some OCR materials": "Could not load some OCR materials", @@ -519,6 +520,7 @@ "Manifest with {{count}} images": "Manifest with {{count}} images", "Manifest with {{count}} images_plural": "Manifest with {{count}} images", "Manifests": "Manifests", + "Manifests and Collections": "Manifests and Collections", "Manifests_plural": "Manifests", "Margin left and right": "Margin left and right", "Mark as complete": "Mark as complete", @@ -541,6 +543,7 @@ "My Contributions": "My Contributions", "My Contributions_plural": "My Contributions", "My sites": "My sites", + "My work": "My work", "Name": "Name", "Name (optional)": "Name (optional)", "Navigate directly to model page from manifest": "Navigate directly to model page from manifest", @@ -761,6 +764,8 @@ "Switch to {{name}}": "Switch to {{name}}", "System": "System", "System status": "System status", + "Tab1": "Tab1", + "Tab2": "Tab2", "Tabs position": "Tabs position", "Tabs text": "Tabs text", "Tag": "Tag", From e0733d9e4b447ab7e9e18ba613c2cab25e42f557 Mon Sep 17 00:00:00 2001 From: Heather0K Date: Fri, 30 Jun 2023 14:59:09 +0100 Subject: [PATCH 009/130] project stats new --- .../src/frontend/shared/atoms/Statistics.tsx | 11 +- .../frontend/shared/icons/HourglassIcon.tsx | 8 + .../frontend/shared/icons/InProgressIcon.tsx | 8 + .../src/frontend/shared/icons/PendingIcon.tsx | 8 + .../site/features/ProjectStatistics.tsx | 173 +++++++++++++++--- .../src/frontend/site/pages/view-project.tsx | 4 +- services/madoc-ts/translations/en/madoc.json | 1 + 7 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 services/madoc-ts/src/frontend/shared/icons/HourglassIcon.tsx create mode 100644 services/madoc-ts/src/frontend/shared/icons/InProgressIcon.tsx create mode 100644 services/madoc-ts/src/frontend/shared/icons/PendingIcon.tsx diff --git a/services/madoc-ts/src/frontend/shared/atoms/Statistics.tsx b/services/madoc-ts/src/frontend/shared/atoms/Statistics.tsx index da3d07594..0f7b603ff 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: 2em; + font-weight: 600; + + > svg { + font-size: 48px; + vertical-align: -10px; + fill: #999999; + } `; export const Statistic = styled.div<{ $interactive?: boolean }>` 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/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/site/features/ProjectStatistics.tsx b/services/madoc-ts/src/frontend/site/features/ProjectStatistics.tsx index ea6882ccf..65f6d0a25 100644 --- a/services/madoc-ts/src/frontend/site/features/ProjectStatistics.tsx +++ b/services/madoc-ts/src/frontend/site/features/ProjectStatistics.tsx @@ -1,48 +1,171 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { Statistic, StatisticContainer, StatisticLabel, StatisticNumber } from '../../shared/atoms/Statistics'; -import { SubtaskProgress } from '../../shared/atoms/SubtaskProgress'; +import { StatisticContainer, StatisticLabel, StatisticNumber } from '../../shared/atoms/Statistics'; import { useProject } from '../hooks/use-project'; import { useSiteConfiguration } from './SiteConfigurationContext'; +import styled from 'styled-components'; +import { useProjectManifests } from '../hooks/use-project-manifests'; +import CheckCircleIcon from '../../shared/icons/CheckCircleIcon'; +import HourglassIcon from '../../shared/icons/HourglassIcon'; +import PendingIcon from '../../shared/icons/PendingIcon'; +import InProgressIcon from '../../shared/icons/InProgressIcon'; +const ProgessHeading = styled.h2` + font-size: 1.2em; + padding: 0 1em; +`; + +const ContentStat = styled.div` + background: #fff; + text-align: center; + border: 1px solid #d9d9d9; + padding: 1em 2em; + color: #333; + width: 100%; +`; +const ContentStatNumber = styled.div` + font-size: 2em; + font-weight: 600; +`; +const ContentStatisticLabel = styled.div` + font-size: 2em; + font-weight: 300; +`; + +const ProgressBar = styled.progress<{ $valueColour?: string; $barColour?: string }>` + margin: 1em 0; + width: 100%; + height: 8px; + border-radius: 0.5em; + display: flex; + overflow: hidden; + appearance: none; + background-color: ${props => (props.$barColour ? props.$barColour : '')}; + ::-webkit-progress-bar, + ::-moz-progress-bar { + background-color: ${props => (props.$valueColour ? props.$valueColour : '#47991b')}; + } +`; + +const ProgressContainer = styled.div` + margin: 0 auto; + width: auto; + padding: 2em; +`; + +const ProjectStatisticContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; +`; + +const ProgressBars = styled.div` + border: 1px solid #d9d9d9; + display: flex; + justify-content: space-evenly; + padding: 1em; + width: 100%; + gap: 1em; +`; + +const ProgressStat = styled.div` + background: #fff; + text-align: center; + border: 1px solid #d9d9d9; + padding: 1em 3em; + width: 100%; + color: #333; +`; export const ProjectStatistics: React.FC = () => { const { data: project } = useProject(); + const { data: manifests } = useProjectManifests(); + const { t } = useTranslation(); const { project: { hideStatistics }, } = useSiteConfiguration(); + const completedReviewTotal = Math.max(0, project?.statistics['2'] || 0) + Math.max(0, project?.statistics['3'] || 0); + const progressNotStartedTotal = + Math.max(0, project?.statistics['0'] || 0) + Math.max(0, project?.statistics['1'] || 0); + if (hideStatistics || !project) { return null; } return ( - <> +
+ {t('Project progress')} - - {Math.max(0, project.statistics['0'] || 0)} - {t('Not started')} - - - {Math.max(0, project.statistics['1'] || 0)} - {t('In progress')} - - - {Math.max(0, project.statistics['2'] || 0)} - {t('In review')} - - - {Math.max(0, project.statistics['3'] || 0)} - {t('Completed')} - + + {project.content.manifests} + {t('Manifests')} + + + + {project.content.canvases} + {t('Canvases')} + + + + ?? + {t('Collections')} + + - - + + + + + + + + + + + {Math.max(0, project.statistics['0'] || 0)} + + {t('Not started')} + + + + + + {Math.max(0, project.statistics['2'] || 0)} + + {t('In review')} + + + + + + {Math.max(0, project.statistics['1'] || 0)} + + {t('In progress')} + + + + + + {Math.max(0, project.statistics['3'] || 0)} + + {t('Completed')} + + + +
); }; 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 5591a6d34..6b956224c 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project.tsx @@ -28,9 +28,9 @@ export const ViewProject: React.FC = () => { {available} - - + + {available} diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index 3566c5f97..be55f4a88 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -642,6 +642,7 @@ "Project landing page": "Project landing page", "Project not found": "Project not found", "Project page options": "Project page options", + "Project progress": "Project progress", "Project published feed (not yet implemented)": "Project published feed (not yet implemented)", "Project tasks": "Project tasks", "Projects": "Projects", From b5aaaa1fd9755704adfc0b7f38039207e56011dd Mon Sep 17 00:00:00 2001 From: Heather0K Date: Fri, 30 Jun 2023 16:20:38 +0100 Subject: [PATCH 010/130] update project header and make SearchResource --- .../frontend/site/features/ProjectHeading.tsx | 85 ----------------- .../frontend/site/features/SearchResource.tsx | 46 +++++++++ .../ProjectActions.tsx | 16 ++-- .../features/projectDash/ProjectHeading.tsx | 95 +++++++++++++++++++ .../{ => projectDash}/ProjectStatistics.tsx | 20 ++-- .../src/frontend/site/pages/view-project.tsx | 8 +- 6 files changed, 164 insertions(+), 106 deletions(-) delete mode 100644 services/madoc-ts/src/frontend/site/features/ProjectHeading.tsx create mode 100644 services/madoc-ts/src/frontend/site/features/SearchResource.tsx rename services/madoc-ts/src/frontend/site/features/{contributor => projectDash}/ProjectActions.tsx (86%) create mode 100644 services/madoc-ts/src/frontend/site/features/projectDash/ProjectHeading.tsx rename services/madoc-ts/src/frontend/site/features/{ => projectDash}/ProjectStatistics.tsx (89%) diff --git a/services/madoc-ts/src/frontend/site/features/ProjectHeading.tsx b/services/madoc-ts/src/frontend/site/features/ProjectHeading.tsx deleted file mode 100644 index fb087a7aa..000000000 --- a/services/madoc-ts/src/frontend/site/features/ProjectHeading.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; -import { Heading1, Subheading1 } from '../../shared/typography/Heading1'; -import { LocaleString } from '../../shared/components/LocaleString'; -import { useProject } from '../hooks/use-project'; -import styled from 'styled-components'; -import { StartContributingButton } from './contributor/StartContributingButton'; - -const ProjectHeadingContainer = styled.div` - display: flex; - flex-direction: column; - gap: 1em; -`; - -const ProjectTitle = styled.div``; - -const ProjectImage = styled.div` - width: 100%; - height: 100%; - max-height: 200px; - - img { - object-fit: cover; - width: 100%; - height: 100%; - } - &[data-full-width='true'] { - margin: 0 -2em; - width: auto; - } -`; -export const ProjectHeading: React.FC<{ - headerImage?: { - id: string; - image: string; - thumbnail: string; - } | null; - fullWidth?: boolean; - imageHeight?: string; - showContributingButton?: boolean; -}> = ({ headerImage, fullWidth, imageHeight = 200, showContributingButton = true }) => { - const { data: project } = useProject(); - - if (!project) { - return null; - } - - return ( - - - {project.label} - {project.summary} - - - - {''} - - - {showContributingButton && } - - ); -}; - -blockEditorFor(ProjectHeading, { - type: 'default.ProjectHeading', - label: 'Project heading', - anyContext: ['project'], - requiredContext: ['project'], - defaultProps: { - headerImage: null, - fullWidth: false, - imageHeight: 200, - showContributingButton: true, - }, - editor: { - headerImage: { label: 'Background image', type: 'madoc-media-explorer' }, - fullWidth: { label: 'Full width', type: 'checkbox-field', inlineLabel: 'Show image full width' }, - imageHeight: { label: 'Image height', type: 'text-field' }, - showContributingButton: { - label: 'Start contributing button', - type: 'checkbox-field', - inlineLabel: 'Show start Contributing button', - }, - }, -}); diff --git a/services/madoc-ts/src/frontend/site/features/SearchResource.tsx b/services/madoc-ts/src/frontend/site/features/SearchResource.tsx new file mode 100644 index 000000000..4ebe29534 --- /dev/null +++ b/services/madoc-ts/src/frontend/site/features/SearchResource.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { blockEditorFor } from '../../../extensions/page-blocks/block-editor-for'; +import { useProjectPageConfiguration } from '../hooks/use-project-page-configuration'; +import { useRelativeLinks } from '../hooks/use-relative-links'; +import { SearchBox } from '../../shared/atoms/SearchBox'; +import { useRouteContext } from '../hooks/use-route-context'; + +const SearchResourceContainer = styled.div` + padding: 1em 0; + width: 500px; + + label { + font-size: 1em; + line-height: 2em; + } +`; +export const SearchResource: React.FC = () => { + const { t } = useTranslation(); + const options = useProjectPageConfiguration(); + const { projectId, collectionId, manifestId } = useRouteContext(); + + const navigate = useNavigate(); + const createLink = useRelativeLinks(); + const resource = manifestId ? 'manifest' : collectionId ? 'collection' : projectId ? 'project' : 'resource'; + const handleSearch = (val: string) => { + navigate(createLink({ subRoute: 'search', query: { fulltext: val } })); + }; + + return ( + + + {!options.hideSearchButton ? handleSearch(val)} placeholder="" value="" /> : null} + + ); +}; + +blockEditorFor(SearchResource, { + type: 'default.SearchResource', + label: 'Search Resource', + anyContext: ['project', 'collection', 'manifest'], + requiredContext: [], + editor: {}, +}); diff --git a/services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectActions.tsx similarity index 86% rename from services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx rename to services/madoc-ts/src/frontend/site/features/projectDash/ProjectActions.tsx index c5ba8e0e5..b64039bd5 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/ProjectActions.tsx +++ b/services/madoc-ts/src/frontend/site/features/projectDash/ProjectActions.tsx @@ -8,10 +8,10 @@ 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 { GoToRandomCanvas } from '../contributor/GoToRandomCanvas'; +import { GoToRandomManifest } from '../contributor/GoToRandomManifest'; import { useSiteConfiguration } from '../SiteConfigurationContext'; -import { StartContributingButton } from './StartContributingButton'; +import { StartContributingButton } from '../contributor/StartContributingButton'; export const ProjectActions: React.FC<{ showContributingButton?: boolean }> = ({showContributingButton = false}) => { @@ -35,11 +35,11 @@ export const ProjectActions: React.FC<{
{showContributingButton && } - {!options.hideSearchButton ? ( - - ) : null} + {/*{!options.hideSearchButton ? (*/} + {/* */} + {/*) : null}*/} {isReviewer && isActive ? (
- + ); } diff --git a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/transcription-panel.tsx b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/transcription-panel.tsx index 65720af73..aa26bab07 100644 --- a/services/madoc-ts/src/frontend/site/hooks/canvas-menu/transcription-panel.tsx +++ b/services/madoc-ts/src/frontend/site/hooks/canvas-menu/transcription-panel.tsx @@ -6,8 +6,6 @@ import { TranscriptionIcon } from '../../../shared/icons/TranscriptionIcon'; import { CanvasPlaintext } from '../../features/CanvasPlaintext'; import { CanvasLoader } from '../../pages/loaders/canvas-loader'; import { CanvasMenuHook } from './types'; -import { IIIFExplorer } from '@manifest-editor/iiif-browser-bundle'; -import '@manifest-editor/iiif-browser-bundle/dist-umd/style.css'; export function useTranscriptionMenu(): CanvasMenuHook { const { data } = useData(CanvasLoader, []); @@ -21,8 +19,6 @@ export function useTranscriptionMenu(): CanvasMenuHook { ) : ( No plaintext )} - - ); diff --git a/services/madoc-ts/yarn.lock b/services/madoc-ts/yarn.lock index 20a106f27..0f8b97019 100644 --- a/services/madoc-ts/yarn.lock +++ b/services/madoc-ts/yarn.lock @@ -2732,6 +2732,18 @@ resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4" integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ== +"@floating-ui/core@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.1.tgz#4d795b649cc3b1cbb760d191c80dcb4353c9a366" + integrity sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g== + +"@floating-ui/dom@^1.0.0": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.4.5.tgz#336dfb9870c98b471ff5802002982e489b8bd1c5" + integrity sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw== + dependencies: + "@floating-ui/core" "^1.3.1" + "@gar/promisify@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" @@ -8843,6 +8855,11 @@ classnames@^2.2, classnames@^2.2.5, classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== +classnames@^2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + clean-css@^3.0.1: version "3.4.28" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff" @@ -20323,13 +20340,13 @@ react-timeago@^4.4.0: resolved "https://registry.yarnpkg.com/react-timeago/-/react-timeago-4.4.0.tgz#4520dd9ba63551afc4d709819f52b14b9343ba2b" integrity sha512-Zj8RchTqZEH27LAANemzMR2RpotbP2aMd+UIajfYMZ9KW4dMcViUVKzC7YmqfiqlFfz8B0bjDw2xUBjmcxDngA== -react-tooltip@^4.2.21: - version "4.2.21" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.21.tgz#840123ed86cf33d50ddde8ec8813b2960bfded7f" - integrity sha512-zSLprMymBDowknr0KVDiJ05IjZn9mQhhg4PRsqln0OZtURAJ1snt1xi5daZfagsh6vfsziZrc9pErPTDY1ACig== +react-tooltip@^5.18.0: + version "5.18.0" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.18.0.tgz#33e5880ef13e473def47714b17c21b3d77bf258a" + integrity sha512-qjDK/skUJJ27sc9lTWeNxp2rLzmenBTskSsRiDOCPnupGSz2GhL5IZxDizK/sOsk0hn5iSCywt+3jKxUJ3Y4Sw== dependencies: - prop-types "^15.7.2" - uuid "^7.0.3" + "@floating-ui/dom" "^1.0.0" + classnames "^2.3.0" react-transition-group@^1.2.0: version "1.2.1" @@ -23927,7 +23944,7 @@ uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^7.0.1, uuid@^7.0.3: +uuid@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== From 1c49dc404893157e0da20f00a180e7d3bdad73fd Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Fri, 14 Jul 2023 17:45:51 +0100 Subject: [PATCH 023/130] Login/registration message config --- .../src/extensions/site-manager/types.ts | 4 ++ .../pages/content/system-configuration.tsx | 39 ++++++++++++++++++- .../shared/configuration/site-config.ts | 8 ++++ .../frontend/site/pages/user/login-page.tsx | 9 ++++- .../src/frontend/site/pages/user/register.tsx | 6 +++ .../types/schemas/project-configuration.ts | 2 + services/madoc-ts/translations/en/madoc.json | 1 + 7 files changed, 67 insertions(+), 2 deletions(-) diff --git a/services/madoc-ts/src/extensions/site-manager/types.ts b/services/madoc-ts/src/extensions/site-manager/types.ts index d3302d4ed..db1559882 100644 --- a/services/madoc-ts/src/extensions/site-manager/types.ts +++ b/services/madoc-ts/src/extensions/site-manager/types.ts @@ -24,6 +24,10 @@ export type SystemConfig = { installationTitle: string; defaultSite: string | null; autoPublishImport: boolean | null; + loginHeader?: string; + loginFooter?: string; + registerHeader?: string; + registerFooter?: string; } & SiteSystemConfig; export type CreateSiteRequest = { 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..65dd33abd 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, + }; + const [updateSystemConfig] = useMutation(async (newConfig: any) => { await api.siteManager.updateSite({ config: newConfig, 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 048ac6ff2..4d62b1a0a 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,10 @@ export const siteConfigurationModel: { label: 'Enable rotation of images', value: 'enableRotation', }, + { + label: 'Enable autosave', + value: 'enableAutoSave', + }, ], }, reviewOptions: { @@ -822,6 +826,10 @@ export const ProjectConfigContributions: { label: 'Disable save for later button', value: 'disableSaveForLater', }, + { + label: 'Enable autosave', + value: 'enableAutoSave', + }, { label: 'Allow personal notes', description: 'allow users to take personal notes only visible to themselves on canvases in a project', 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..8e1e9357d 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,6 +45,9 @@ 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..a61692543 100644 --- a/services/madoc-ts/src/frontend/site/pages/user/register.tsx +++ b/services/madoc-ts/src/frontend/site/pages/user/register.tsx @@ -109,6 +109,9 @@ export const Register: React.FC = () => { {t('Register')} )} + {systemConfig?.registerHeader ? ( +
+ ) : null} {t('Display name')} @@ -118,6 +121,9 @@ export const Register: React.FC = () => { {form?.emailError ? {t('Email already in use')} : null} + {systemConfig?.registerFooter ? ( +
+ ) : null} diff --git a/services/madoc-ts/src/types/schemas/project-configuration.ts b/services/madoc-ts/src/types/schemas/project-configuration.ts index dfea9a39b..5f04ac7ce 100644 --- a/services/madoc-ts/src/types/schemas/project-configuration.ts +++ b/services/madoc-ts/src/types/schemas/project-configuration.ts @@ -60,6 +60,7 @@ export type ProjectConfiguration = { disablePreview?: boolean; disableNextCanvas?: boolean; enableRotation?: boolean; + enableAutoSave?: boolean; }; projectPageOptions?: { hideStartContributing?: boolean; @@ -202,6 +203,7 @@ export type ProjectConfigurationNEW = { fixedTranscriptionBar?: boolean; disableSaveForLater?: boolean; allowPersonalNotes?: boolean; // changed to sub-item + enableAutoSave?: boolean; }; contributionWarningTime: false | number; shortExpiryTime?: string; diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index cbfdd1d0c..14921a59b 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -296,6 +296,7 @@ "Editing field": "Editing field", "Email": "Email", "Empty message": "Empty message", + "Enable autosave": "Enable autosave", "Enable on content": "Enable on content", "Enable plugin": "Enable plugin", "Enable rotation of images": "Enable rotation of images", From d9d3cd4ce1242a9ec05fd943e0eac7f9b4e9a91c Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Fri, 14 Jul 2023 18:08:20 +0100 Subject: [PATCH 024/130] MAD-1329 - Tooltips in capture model descriptions --- .../CaptureModelVisualSettings.tsx | 30 ++++++++++ .../components/FieldHeader/FieldHeader.tsx | 45 +++++++++++++- .../capture-models/new/CoreModelEditor.tsx | 5 ++ .../RevisionProviderWithFeatures.tsx | 58 +++++++++++-------- .../shared/configuration/site-config.ts | 8 +++ .../contributor/CanvasSimpleEditor.tsx | 13 ++++- .../types/schemas/project-configuration.ts | 2 + services/madoc-ts/translations/en/madoc.json | 1 + 8 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 services/madoc-ts/src/frontend/shared/capture-models/editor/components/CaptureModelVisualSettings/CaptureModelVisualSettings.tsx 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/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/new/CoreModelEditor.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/CoreModelEditor.tsx index f8c4924d3..62ae9f8fd 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 @@ -33,6 +33,7 @@ import { TickIcon } from '../../icons/TickIcon'; import { EmptyState } from '../../layout/EmptyState'; import { Button, ButtonIcon } 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'; @@ -102,6 +103,8 @@ export interface CoreModelEditorProps { showBugReport?: boolean; children?: React.ReactNode; + + visualConfig?: Partial; } export function CoreModelEditor({ revision, @@ -131,6 +134,7 @@ export function CoreModelEditor({ enableHighlightedRegions, canvasViewerPins, showBugReport, + visualConfig, children, }: CoreModelEditorProps) { const { t } = useTranslation(); @@ -246,6 +250,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/components/RevisionProviderWithFeatures.tsx b/services/madoc-ts/src/frontend/shared/capture-models/new/components/RevisionProviderWithFeatures.tsx index cc4c14d8e..456061d24 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,6 +49,7 @@ export const RevisionProviderWithFeatures: React.FC<{ excludeStructures, initialRevision, features, + visualConfig = {}, }) => { const { autoSelectingRevision = true, @@ -59,31 +65,33 @@ export const RevisionProviderWithFeatures: React.FC<{ return ( - - - - {/**/} - {/**/} - {autosave ? : null} - {revisionEditMode ? : null} - {revisionEditMode ? : null} - {autoSelectingRevision ? ( - - ) : null} - {basicUnNesting ? : null} - - - {children} - - - - + + + + + {/**/} + {/**/} + {autosave ? : null} + {revisionEditMode ? : null} + {revisionEditMode ? : null} + {autoSelectingRevision ? ( + + ) : null} + {basicUnNesting ? : null} + + + {children} + + + + + ); }; 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 4d62b1a0a..7eb435547 100644 --- a/services/madoc-ts/src/frontend/shared/configuration/site-config.ts +++ b/services/madoc-ts/src/frontend/shared/configuration/site-config.ts @@ -303,6 +303,10 @@ export const siteConfigurationModel: { label: 'Enable autosave', value: 'enableAutoSave', }, + { + label: 'Enable tooltip descriptions', + value: 'enableTooltipDescriptions', + }, ], }, reviewOptions: { @@ -830,6 +834,10 @@ export const ProjectConfigContributions: { 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', diff --git a/services/madoc-ts/src/frontend/site/features/contributor/CanvasSimpleEditor.tsx b/services/madoc-ts/src/frontend/site/features/contributor/CanvasSimpleEditor.tsx index 04bafcd4b..45f09a795 100644 --- a/services/madoc-ts/src/frontend/site/features/contributor/CanvasSimpleEditor.tsx +++ b/services/madoc-ts/src/frontend/site/features/contributor/CanvasSimpleEditor.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useMemo } from 'react'; +import { CaptureModelVisualSettings } from '../../../shared/capture-models/editor/components/CaptureModelVisualSettings/CaptureModelVisualSettings'; import { CoreModelEditor } from '../../../shared/capture-models/new/CoreModelEditor'; import { DynamicVaultContext } from '../../../shared/capture-models/new/DynamicVaultContext'; import { useReadOnlyAnnotations } from '../../../shared/hooks/use-read-only-annotations'; @@ -36,6 +37,8 @@ export function CanvasSimpleEditor({ revision, isSegmentation }: CanvasSimpleEdi disablePreview = false, disableNextCanvas = false, enableRotation = false, + enableAutoSave = false, + enableTooltipDescriptions = false, } = useModelPageConfiguration(); const mode = useContributionMode(); const isVertical = config.project.defaultEditorOrientation === 'vertical'; @@ -57,6 +60,13 @@ export function CanvasSimpleEditor({ revision, isSegmentation }: CanvasSimpleEdi const isModelAdmin = user && user.scope && (user.scope.indexOf('site.admin') !== -1 || user.scope.indexOf('models.admin') !== -1); + const visualConfig = useMemo>( + () => ({ + descriptionTooltip: enableTooltipDescriptions, + }), + [enableTooltipDescriptions] + ); + if (api.getIsServer() || !canvasId || !projectId || (isPreparing && !isModelAdmin)) { return null; } @@ -89,6 +99,7 @@ export function CanvasSimpleEditor({ revision, isSegmentation }: CanvasSimpleEdi enableRotation={enableRotation} updateClaim={updateClaim} showBugReport={isAdmin} + visualConfig={visualConfig} /> ); diff --git a/services/madoc-ts/src/types/schemas/project-configuration.ts b/services/madoc-ts/src/types/schemas/project-configuration.ts index 5f04ac7ce..9f1108ae4 100644 --- a/services/madoc-ts/src/types/schemas/project-configuration.ts +++ b/services/madoc-ts/src/types/schemas/project-configuration.ts @@ -61,6 +61,7 @@ export type ProjectConfiguration = { disableNextCanvas?: boolean; enableRotation?: boolean; enableAutoSave?: boolean; + enableTooltipDescriptions?: boolean; }; projectPageOptions?: { hideStartContributing?: boolean; @@ -204,6 +205,7 @@ export type ProjectConfigurationNEW = { disableSaveForLater?: boolean; allowPersonalNotes?: boolean; // changed to sub-item enableAutoSave?: boolean; + enableTooltipDescriptions?: boolean; }; contributionWarningTime: false | number; shortExpiryTime?: string; diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index 14921a59b..dd0db359a 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -302,6 +302,7 @@ "Enable rotation of images": "Enable rotation of images", "Enable search": "Enable search", "Enable theme": "Enable theme", + "Enable tooltip descriptions": "Enable tooltip descriptions", "Enable unique submissions": "Enable unique submissions", "Enabled translations": "Enabled translations", "Enter IIIF Manifest Collection URL": "Enter IIIF Manifest Collection URL", From 51f3ff2b38164ad2dc01fa25ef0665ccc553ffdb Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 18 Jul 2023 12:40:48 +0100 Subject: [PATCH 025/130] MAD-1408 - Custom auto-complete endpoints --- CHANGELOG.md | 3 + .../2023-07-17T13-42.term-configurations.sql | 21 ++ .../2023-07-17T13-42.term-configurations.sql | 4 + .../src/extensions/site-manager/extension.ts | 30 ++ .../pages/content/site-configuration.tsx | 5 + .../create-term-configuration.tsx | 300 ++++++++++++++++++ .../edit-term-configuration.tsx | 5 + .../admin/pages/term-configurations/index.tsx | 25 ++ .../list-term-configurations.tsx | 53 ++++ .../view-term-configuration.tsx | 115 +++++++ .../madoc-ts/src/frontend/admin/routes.tsx | 27 ++ .../AutocompleteField/AutocompleteField.tsx | 23 +- services/madoc-ts/src/gateway/api.ts | 7 + .../src/middleware/postgres-connection.ts | 2 + .../term-configurations-repository.ts | 143 +++++++++ services/madoc-ts/src/router.ts | 16 + .../src/routes/admin/term-configurations.ts | 47 +++ .../src/routes/site/site-term-proxy.ts | 81 +++++ .../madoc-ts/src/types/application-context.ts | 2 + .../madoc-ts/src/types/term-configurations.ts | 43 +++ services/madoc-ts/translations/en/madoc.json | 2 + 21 files changed, 950 insertions(+), 4 deletions(-) create mode 100644 services/madoc-ts/migrations/2023-07-17T13-42.term-configurations.sql create mode 100644 services/madoc-ts/migrations/down/2023-07-17T13-42.term-configurations.sql create mode 100644 services/madoc-ts/src/frontend/admin/pages/term-configurations/create-term-configuration.tsx create mode 100644 services/madoc-ts/src/frontend/admin/pages/term-configurations/edit-term-configuration.tsx create mode 100644 services/madoc-ts/src/frontend/admin/pages/term-configurations/index.tsx create mode 100644 services/madoc-ts/src/frontend/admin/pages/term-configurations/list-term-configurations.tsx create mode 100644 services/madoc-ts/src/frontend/admin/pages/term-configurations/view-term-configuration.tsx create mode 100644 services/madoc-ts/src/repository/term-configurations-repository.ts create mode 100644 services/madoc-ts/src/routes/admin/term-configurations.ts create mode 100644 services/madoc-ts/src/routes/site/site-term-proxy.ts create mode 100644 services/madoc-ts/src/types/term-configurations.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 76093cef9..162046ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Multi-lingual page blocks (NS-32) - Added block customisations to Project header +### 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) ### Fixed - Fix bug when importing Manifests with long labels (NS-29) 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/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/src/extensions/site-manager/extension.ts b/services/madoc-ts/src/extensions/site-manager/extension.ts index d539f93b0..7d078599a 100644 --- a/services/madoc-ts/src/extensions/site-manager/extension.ts +++ b/services/madoc-ts/src/extensions/site-manager/extension.ts @@ -1,6 +1,7 @@ import { stringify } from 'query-string'; import { ApiClient } from '../../gateway/api'; import { Pagination } from '../../types/schemas/_pagination'; +import { TermConfiguration, TermConfigurationRequest } from '../../types/term-configurations'; import { BaseExtension, defaultDispose } from '../extension-manager'; import { CreateSiteRequest, @@ -175,4 +176,33 @@ 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`); + } } 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..a2af8c63e 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,11 @@ 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. + +
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..29b4bdab8 --- /dev/null +++ b/services/madoc-ts/src/frontend/admin/pages/term-configurations/create-term-configuration.tsx @@ -0,0 +1,300 @@ +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..246b7c539 100644 --- a/services/madoc-ts/src/frontend/admin/routes.tsx +++ b/services/madoc-ts/src/frontend/admin/routes.tsx @@ -106,6 +106,11 @@ 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'; export const routes: RouteObject[] = [ { @@ -459,6 +464,28 @@ 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: '/page-blocks', element: , 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 aedc797b0..edbe358d2 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'; @@ -56,6 +56,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); @@ -84,6 +85,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); @@ -92,20 +100,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/gateway/api.ts b/services/madoc-ts/src/gateway/api.ts index 6e6ad6feb..e9ac97c27 100644 --- a/services/madoc-ts/src/gateway/api.ts +++ b/services/madoc-ts/src/gateway/api.ts @@ -24,6 +24,7 @@ import { SiteManagerExtension } from '../extensions/site-manager/extension'; import { SystemExtension } from '../extensions/system/extension'; import { TaskExtension } from '../extensions/tasks/extension'; import { ThemeExtension } from '../extensions/themes/extension'; +import { CompletionItem } from '../frontend/shared/capture-models/editor/input-types/AutocompleteField/AutocompleteField'; import { CaptureModel } from '../frontend/shared/capture-models/types/capture-model'; import { BaseField } from '../frontend/shared/capture-models/types/field-types'; import { RevisionRequest } from '../frontend/shared/capture-models/types/revision-request'; @@ -2225,6 +2226,12 @@ export class ApiClient { return this.publicRequest(`/madoc/api/projects/${projectId}/manifest-tasks/${manifestId}`); } + async queryCustomTermConfiguration(id: string, query: string) { + return this.publicRequest<{ completions: CompletionItem[] }>( + `/madoc/api/term-proxy/${id}?q=${encodeURIComponent(query)}` + ); + } + async siteUserAutocomplete(q: string, roles?: string[]) { return this.publicRequest<{ completions: Array<{ diff --git a/services/madoc-ts/src/middleware/postgres-connection.ts b/services/madoc-ts/src/middleware/postgres-connection.ts index 8ecd6437a..5b936b951 100644 --- a/services/madoc-ts/src/middleware/postgres-connection.ts +++ b/services/madoc-ts/src/middleware/postgres-connection.ts @@ -8,6 +8,7 @@ import { NotificationRepository } from '../repository/notification-repository'; import { PageBlocksRepository } from '../repository/page-blocks-repository'; import { PluginRepository } from '../repository/plugin-repository'; import { ProjectRepository } from '../repository/project-repository'; +import { TermConfigurationsRepository } from '../repository/term-configurations-repository'; import { ThemeRepository } from '../repository/theme-repository'; import { ApiKeyRepository } from '../repository/api-key-repository'; import { EnvConfig } from '../types/env-config'; @@ -31,6 +32,7 @@ export const postgresConnection = ( context.notifications = new NotificationRepository(connection); context.projects = new ProjectRepository(connection); context.annotationStyles = new AnnotationStylesRepository(connection); + context.termConfigurations = new TermConfigurationsRepository(connection); context.captureModels = new CaptureModelRepository(connection, { capture_model_api_migrated: env.flags.capture_model_api_migrated, }); diff --git a/services/madoc-ts/src/repository/term-configurations-repository.ts b/services/madoc-ts/src/repository/term-configurations-repository.ts new file mode 100644 index 000000000..2ad4a6fc3 --- /dev/null +++ b/services/madoc-ts/src/repository/term-configurations-repository.ts @@ -0,0 +1,143 @@ +import { DatabasePoolConnectionType, DatabaseTransactionConnectionType, sql } from 'slonik'; +import invariant from 'tiny-invariant'; +import { v4 } from 'uuid'; +import { + TermConfiguration, + TermConfigurationRequest, + TermConfigurationRow, + TermConfigurationRowUserJoin, +} from '../types/term-configurations'; +import { BaseRepository } from './base-repository'; + +export class TermConfigurationsRepository extends BaseRepository { + constructor(postgres: DatabasePoolConnectionType | DatabaseTransactionConnectionType) { + super(postgres); + } + + static queries = { + listAllTermConfigurations: (site_id: number) => sql` + select * from term_configurations where site_id = ${site_id} + `, + getTermConfiguration: (id: string, site_id: number) => sql` + select t.*, u.name as creator_name from term_configurations t + join "user" u on t.creator = u.id + where t.id = ${id} and site_id = ${site_id} limit 1 + `, + }; + + static mutations = { + createTermConfiguration: (term: TermConfigurationRequest, user_id: number, site_id: number) => sql< + TermConfigurationRow + >` + insert into term_configurations + ( + id, url_pattern, results_path, label_path, uri_path, resource_class_path, + description_path, language_path, + term_label, term_description, attribution, site_id, creator, created_at + ) values ( + ${v4()}, + ${term.url_pattern}, + ${term.paths.results}, + ${term.paths.label}, + ${term.paths.uri}, + ${term.paths.resource_class || null}, + ${term.paths.description || null}, + ${term.paths.language || null}, + ${term.label}, + ${term.description || null}, + ${term.attribution || null}, + ${site_id}, + ${user_id}, + now() + ) returning * + `, + + updateTermConfiguration: (term: TermConfigurationRequest & { id: string }, site_id: number) => sql< + TermConfigurationRow + >` + update term_configurations set + url_pattern = ${term.url_pattern}, + results_path = ${term.paths.results}, + label_path = ${term.paths.label}, + uri_path = ${term.paths.uri}, + resource_class_path = ${term.paths.resource_class || null}, + description_path = ${term.paths.description || null}, + language_path = ${term.paths.language || null}, + term_label = ${term.label}, + term_description = ${term.description || null}, + attribution = ${term.attribution || null} + where id = ${term.id} and site_id = ${site_id} returning * + `, + + deleteTermConfiguration: (id: string, site_id: number) => sql` + delete from term_configurations where id = ${id} and site_id = ${site_id} + `, + }; + + async createTermConfiguration(term: TermConfigurationRequest, user_id: number, site_id: number) { + invariant(user_id, 'User id must be provided'); + invariant(site_id, 'Site id must be provided'); + + const result = await this.connection.one( + TermConfigurationsRepository.mutations.createTermConfiguration(term, user_id, site_id) + ); + return TermConfigurationsRepository.mapRow(result); + } + + async updateTermConfiguration(term: TermConfigurationRequest & { id: string }, site_id: number) { + invariant(term.id, 'Term id must be provided'); + invariant(site_id, 'Site id must be provided'); + + const result = await this.connection.one( + TermConfigurationsRepository.mutations.updateTermConfiguration(term, site_id) + ); + return TermConfigurationsRepository.mapRow(result); + } + + async deleteTermConfiguration(id: string, site_id: number) { + invariant(id, 'Term id must be provided'); + invariant(site_id, 'Site id must be provided'); + + await this.connection.query(TermConfigurationsRepository.mutations.deleteTermConfiguration(id, site_id)); + } + + async listAllTermConfigurations(site_id: number) { + invariant(site_id, 'Site id must be provided'); + + const result = await this.connection.any(TermConfigurationsRepository.queries.listAllTermConfigurations(site_id)); + return result.map(TermConfigurationsRepository.mapRow); + } + + async getTermConfiguration(id: string, site_id: number) { + invariant(id, 'Term id must be provided'); + invariant(site_id, 'Site id must be provided'); + + const result = await this.connection.maybeOne( + TermConfigurationsRepository.queries.getTermConfiguration(id, site_id) + ); + return result ? TermConfigurationsRepository.mapRow(result) : undefined; + } + + static mapRow(row: TermConfigurationRow | TermConfigurationRowUserJoin): TermConfiguration { + return { + id: row.id, + url_pattern: row.url_pattern, + paths: { + results: row.results_path, + label: row.label_path, + uri: row.uri_path, + description: row.description_path, + language: row.language_path, + resource_class: row.resource_class_path, + }, + label: row.term_label, + description: row.term_description, + attribution: row.attribution, + created_at: new Date(row.created_at), + creator: { + id: row.creator, + name: (row as TermConfigurationRowUserJoin).creator_name, + }, + }; + } +} diff --git a/services/madoc-ts/src/router.ts b/services/madoc-ts/src/router.ts index 33f8f1b34..f184c14de 100644 --- a/services/madoc-ts/src/router.ts +++ b/services/madoc-ts/src/router.ts @@ -2,6 +2,13 @@ import { getAuthRoutes } from './auth'; import { deleteApiKey } from './routes/admin/delete-api-key'; import { keyRegenerate } from './routes/admin/key-regenerate'; import { listApiKeys } from './routes/admin/list-api-keys'; +import { + createTermConfiguration, + deleteTermConfiguration, + getTermConfiguration, + listTermConfigurations, + updateTermConfiguration, +} from './routes/admin/term-configurations'; import { getProjectAnnotationStyle } from './routes/annotation-styles/get-project-annotation-style'; import { annotationStyles } from './routes/annotation-styles/index'; import { searchAllUsers } from './routes/global/search-all-users'; @@ -127,6 +134,7 @@ import { siteManifestTasks } from './routes/site/site-manifest-tasks'; import { getStaticPage, sitePages } from './routes/site/site-pages'; import { listProjectsAutocomplete } from './routes/projects/list-projects-autocomplete'; import { siteTaskMetadata } from './routes/site/site-task-metadata'; +import { termListProxy } from './routes/site/site-term-proxy'; import { siteUserAutocomplete } from './routes/site/site-user-autocomplete'; import { forgotPassword } from './routes/user/forgot-password'; import { getSiteUser } from './routes/user/get-site-user'; @@ -509,6 +517,13 @@ export const router = new TypedRouter({ 'get-project-deletion-summary': [TypedRouter.GET, '/api/madoc/projects/:id/deletion-summary', deleteProjectSummary], 'delete-project': [TypedRouter.DELETE, '/api/madoc/projects/:id', deleteProjectEndpoint], + // Term configurations + 'get-term-configuration': [TypedRouter.GET, '/api/madoc/term-configuration/:id', getTermConfiguration], + 'update-term-configuration': [TypedRouter.PUT, '/api/madoc/term-configuration/:id', updateTermConfiguration], + 'create-term-configuration': [TypedRouter.POST, '/api/madoc/term-configuration', createTermConfiguration], + 'delete-term-configuration': [TypedRouter.DELETE, '/api/madoc/term-configuration/:id', deleteTermConfiguration], + 'list-term-configuration': [TypedRouter.GET, '/api/madoc/term-configuration', listTermConfigurations], + // Themes 'list-themes': [TypedRouter.GET, '/api/madoc/system/themes', listThemes], // 'get-theme': [TypedRouter.GET, '/api/madoc/system/themes/:theme_id', getTheme], @@ -595,6 +610,7 @@ export const router = new TypedRouter({ 'site-project': [TypedRouter.GET, '/s/:slug/madoc/api/projects/:projectSlug', siteProject], 'site-projects': [TypedRouter.GET, '/s/:slug/madoc/api/projects', siteProjects], 'site-search': [TypedRouter.POST, '/s/:slug/madoc/api/search', siteSearch], + 'site-term-proxy': [TypedRouter.GET, '/s/:slug/madoc/api/term-proxy/:id', termListProxy], 'site-topic': [TypedRouter.GET, '/s/:slug/madoc/api/topics/:type/:id', siteTopic], 'site-topic-type': [TypedRouter.GET, '/s/:slug/madoc/api/topics/:type', siteTopicType], 'site-topic-types': [TypedRouter.GET, '/s/:slug/madoc/api/topics', siteTopicTypes], diff --git a/services/madoc-ts/src/routes/admin/term-configurations.ts b/services/madoc-ts/src/routes/admin/term-configurations.ts new file mode 100644 index 000000000..7b5351a2f --- /dev/null +++ b/services/madoc-ts/src/routes/admin/term-configurations.ts @@ -0,0 +1,47 @@ +import { stringify } from 'query-string'; +import invariant from 'tiny-invariant'; +import { CompletionItem } from '../../frontend/shared/capture-models/editor/input-types/AutocompleteField/AutocompleteField'; +import { RouteMiddleware } from '../../types/route-middleware'; +import { TermConfigurationRequest } from '../../types/term-configurations'; +import { getValueDotNotation } from '../../utility/iiif-metadata'; +import { optionalUserWithScope, userWithScope } from '../../utility/user-with-scope'; + +export const listTermConfigurations: RouteMiddleware = async context => { + const { siteId } = userWithScope(context, ['site.admin']); + + context.response.body = { + termConfigurations: await context.termConfigurations.listAllTermConfigurations(siteId), + }; +}; + +export const getTermConfiguration: RouteMiddleware<{ id: string }> = async context => { + const { siteId } = userWithScope(context, ['site.admin']); + + context.response.body = await context.termConfigurations.getTermConfiguration(context.params.id, siteId); +}; + +export const createTermConfiguration: RouteMiddleware = async context => { + const { siteId, id } = userWithScope(context, ['site.admin']); + + context.response.body = await context.termConfigurations.createTermConfiguration(context.requestBody, id, siteId); +}; + +export const updateTermConfiguration: RouteMiddleware< + { id: string }, + TermConfigurationRequest & { id: string } +> = async context => { + const { siteId } = userWithScope(context, ['site.admin']); + + const request = context.requestBody; + invariant(request.id === context.params.id, 'ID must match'); + + context.response.body = await context.termConfigurations.updateTermConfiguration(request, siteId); +}; + +export const deleteTermConfiguration: RouteMiddleware<{ id: string }> = async context => { + const { siteId } = userWithScope(context, ['site.admin']); + + await context.termConfigurations.deleteTermConfiguration(context.params.id, siteId); + + context.response.status = 204; +}; diff --git a/services/madoc-ts/src/routes/site/site-term-proxy.ts b/services/madoc-ts/src/routes/site/site-term-proxy.ts new file mode 100644 index 000000000..66329dfa5 --- /dev/null +++ b/services/madoc-ts/src/routes/site/site-term-proxy.ts @@ -0,0 +1,81 @@ +import { stringify } from 'query-string'; +import invariant from 'tiny-invariant'; +import { CompletionItem } from '../../frontend/shared/capture-models/editor/input-types/AutocompleteField/AutocompleteField'; +import { RouteMiddleware } from '../../types/route-middleware'; +import { getValueDotNotation } from '../../utility/iiif-metadata'; + +export const termListProxy: RouteMiddleware<{ id: string }> = async context => { + const site = context.state.site; + const siteId = site.id; + + invariant(siteId, 'Site ID must be set'); + + const { q, ...query } = context.query || {}; + + const queryKeys = Object.keys(query); + const config = await context.termConfigurations.getTermConfiguration(context.params.id, siteId); + invariant(config, 'Term configuration not found'); + + let url = config.url_pattern.replace('%', q || ''); + if (queryKeys.length) { + if (url.includes('?')) { + url += `&${stringify(query)}`; + } else { + url += `?${stringify(query)}`; + } + } + + const abortController = new AbortController(); + + context.req.on('close', () => { + // your logic here... + abortController.abort(); + }); + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + signal: abortController.signal, + }); + + if (!response.ok) { + console.log('Error', response.status, response.statusText); + context.response.status = response.status; + context.response.body = { error: 'unknown error' }; + return; + } + + const jsonResponse = await response.json(); + + // Now we map using the term configuration. + const completions: CompletionItem[] = []; + + // 1. find the list of items in the response. + const items = getValueDotNotation(jsonResponse, config.paths.results); + + if (items && Array.isArray(items)) { + // 2. map the items. + for (const item of items) { + const completion: CompletionItem = { + label: getValueDotNotation(item, config.paths.label), + uri: getValueDotNotation(item, config.paths.uri), + }; + if (config.paths.description) { + completion.description = getValueDotNotation(item, config.paths.description); + } + if (config.paths.language) { + completion.language = getValueDotNotation(item, config.paths.language); + } + if (config.paths.resource_class) { + completion.resource_class = getValueDotNotation(item, config.paths.resource_class); + } + completions.push(completion); + } + } + + context.response.body = { + completions, + }; +}; diff --git a/services/madoc-ts/src/types/application-context.ts b/services/madoc-ts/src/types/application-context.ts index a390a0351..8a9534226 100644 --- a/services/madoc-ts/src/types/application-context.ts +++ b/services/madoc-ts/src/types/application-context.ts @@ -11,6 +11,7 @@ import { PageBlocksRepository } from '../repository/page-blocks-repository'; import { PluginRepository } from '../repository/plugin-repository'; import { ProjectRepository } from '../repository/project-repository'; import { SiteUserRepository } from '../repository/site-user-repository'; +import { TermConfigurationsRepository } from '../repository/term-configurations-repository'; import { ThemeRepository } from '../repository/theme-repository'; import { CronJobs } from '../utility/cron-jobs'; import { Mailer } from '../utility/mailer'; @@ -43,6 +44,7 @@ declare module 'koa' { captureModels: CaptureModelRepository; siteManager: SiteUserRepository; webhooks: WebhookRepository; + termConfigurations: TermConfigurationsRepository; pluginManager: PluginManager; cron: CronJobs; diff --git a/services/madoc-ts/src/types/term-configurations.ts b/services/madoc-ts/src/types/term-configurations.ts new file mode 100644 index 000000000..7368039a4 --- /dev/null +++ b/services/madoc-ts/src/types/term-configurations.ts @@ -0,0 +1,43 @@ +export interface TermConfigurationRow { + id: string; + url_pattern: string; + results_path: string; + label_path: string; + uri_path: string; + resource_class_path: string | null; + description_path: string | null; + language_path: string | null; + term_label: string; + term_description: string | null; + attribution: string | null; + site_id: number; + creator: number; + created_at: string; +} + +export interface TermConfigurationRowUserJoin extends TermConfigurationRow { + creator_name: string; +} + +export interface TermConfiguration { + id: string; + url_pattern: string; + paths: { + results: string; + label: string; + uri: string; + resource_class: string | null; + description: string | null; + language: string | null; + }; + label: string; + description: string | null; + attribution: string | null; + creator: { + id: number; + name?: string; + }; + created_at: Date; +} + +export type TermConfigurationRequest = Omit; diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index dd0db359a..95f063be5 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -28,6 +28,7 @@ "Add new submission": "Add new submission", "Add scope": "Add scope", "Add subpage": "Add subpage", + "Add term configuration": "Add term configuration", "Admin": "Admin", "Admin dashboard": "Admin dashboard", "Administer": "Administer", @@ -769,6 +770,7 @@ "Tag": "Tag", "Task is complete!": "Task is complete!", "Tasks": "Tasks", + "Term configurations": "Term configurations", "Text align": "Text align", "Text color": "Text color", "Text color active": "Text color active", From 47ed41644129ce7d04294634651db0d477cf9325 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 18 Jul 2023 12:41:42 +0100 Subject: [PATCH 026/130] MAD-1407 - collection endpoint --- CHANGELOG.md | 1 + services/madoc-ts/src/router.ts | 1 + .../src/routes/site/site-manifest-build.ts | 101 ++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 162046ead..6ccee5980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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) ### Fixed - Fix bug when importing Manifests with long labels (NS-29) diff --git a/services/madoc-ts/src/router.ts b/services/madoc-ts/src/router.ts index f184c14de..bc3932ac0 100644 --- a/services/madoc-ts/src/router.ts +++ b/services/madoc-ts/src/router.ts @@ -655,6 +655,7 @@ export const router = new TypedRouter({ 'manifest-search': [TypedRouter.GET, '/s/:slug/madoc/api/manifests/:id/search/1.0', searchManifest], // 'manifest-export': [TypedRouter.GET, '/s/:slug/madoc/api/manifests/:id/export/source', exportManifest], 'manifest-build': [TypedRouter.GET, '/s/:slug/madoc/api/manifests/:id/export/:version', siteManifestBuild], + 'collection-build': [TypedRouter.GET, '/s/:slug/madoc/api/collections/:id/export/:version', siteManifestBuild], 'manifest-project-build': [ TypedRouter.GET, '/s/:slug/madoc/api/projects/:projectSlug/export/manifest/:id/:version', diff --git a/services/madoc-ts/src/routes/site/site-manifest-build.ts b/services/madoc-ts/src/routes/site/site-manifest-build.ts index 77b145af7..35549bd48 100644 --- a/services/madoc-ts/src/routes/site/site-manifest-build.ts +++ b/services/madoc-ts/src/routes/site/site-manifest-build.ts @@ -5,6 +5,7 @@ import { deprecationGetItemsJson } from '../../deprecations/01-local-source-canv import { gatewayHost } from '../../gateway/api.server'; import { RouteMiddleware } from '../../types/route-middleware'; import { IIIFBuilder } from 'iiif-builder'; +import { NotFound } from '../../utility/errors/not-found'; import { createMetadataReducer } from '../../utility/iiif-metadata'; type IIIFExportRow = { @@ -294,6 +295,106 @@ export const siteManifestBuild: RouteMiddleware<{ const useSourceIds = version === 'source' || configOptions.sourceIds; + if (manifestRow.type === 'collection') { + const collectionRow = manifestRow; + const newCollectionId = + collectionRow.source && useSourceIds + ? collectionRow.source + : `${baseUrl}/madoc/api/collections/${manifestId}/export/${version}`; + + const newCollection = builder.createCollection(newCollectionId, collection => { + const collectionMetadata = table.Metadata[collectionRow.id] || {}; + // + collection.setLabel(collectionMetadata.label); + + const collectionItems = (table.Relations[manifestId] || []) + .sort((a, b) => a.order - b.order) + .map(c => table.Resource[c.id]); + + for (const itemRow of collectionItems) { + if (itemRow.type === 'manifest' || itemRow.type === 'collection') { + const newManifestId = + itemRow.source && useSourceIds + ? itemRow.source + : `${baseUrl}/madoc/api/manifests/${itemRow.id}/export/${version}`; + const fn: 'createManifest' = + itemRow.type === 'collection' ? ('createCollection' as 'createManifest') : 'createManifest'; + collection[fn](newManifestId, manifest => { + const manifestMetadata = table.Metadata[itemRow.id] || {}; + if (itemRow.thumbnail) { + manifest.addThumbnail({ + id: itemRow.thumbnail, + type: 'Image', + format: 'image/jpeg', + }); + } + // Metadata / descriptive. + manifest.setLabel(manifestMetadata.label); + + if (manifestMetadata.summary) { + manifest.setSummary(manifestMetadata.summary); + } + }); + } + } + + if (collectionMetadata.summary) { + collection.setSummary(collectionMetadata.summary); + } + + if (collectionMetadata.metadata) { + collection.setMetadata(collectionMetadata.metadata); + } + + if (collectionMetadata.requiredStatement) { + collection.setRequiredStatement(collectionMetadata.requiredStatement); + } + if (collectionRow.thumbnail) { + collection.addThumbnail({ + id: collectionRow.thumbnail, + type: 'Image', + format: 'image/jpeg', + }); + } + }); + + switch (version) { + case 'normalized': + context.response.body = { + collectionId: newCollection.id, + madocId: collectionRow.id, + store: vault.getState().iiif, + }; + break; + case '3.0': + case 'source': { + context.set('Access-Control-Allow-Origin', '*'); + context.response.status = 200; + const collectionJson: any = builder.toPresentation3({ id: newCollection.id, type: 'Manifest' }); + collectionJson['@context'] = 'http://iiif.io/api/presentation/3/context.json'; + context.response.body = collectionJson; + return; + } + case '2.1': { + context.set('Access-Control-Allow-Origin', '*'); + context.response.status = 200; + const collectionJson: any = builder.toPresentation2({ id: newCollection.id, type: 'Manifest' }); + collectionJson['@context'] = 'http://iiif.io/api/presentation/2/context.json'; + context.response.body = collectionJson; + return; + } + default: + context.status = 404; + return; + } + + return; + } + + if (manifestRow.type !== 'manifest') { + throw new NotFound(); + } + const newManifestId = manifestRow.source && useSourceIds ? manifestRow.source From c5f290c5d90f6978dd24b6126b0786b902e8343b Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 18 Jul 2023 12:49:20 +0100 Subject: [PATCH 027/130] Fixed merge --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ccee5980..3f870eda4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/digirati-co-uk/madoc-platform/compare/v2.1.3...main) +### 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) + + diff --git a/services/madoc-ts/stories/admin/admin-siderbar.stories.tsx b/services/madoc-ts/stories/admin/admin-siderbar.stories.tsx index ac631ee20..604dc0265 100644 --- a/services/madoc-ts/stories/admin/admin-siderbar.stories.tsx +++ b/services/madoc-ts/stories/admin/admin-siderbar.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; +import { AdminLayoutContainer, AdminLayoutMenu } from '../../src/frontend/admin/components/AdminMenu'; import { AdminSidebar } from '../../src/frontend/admin/molecules/AdminSidebar'; -import { AdminLayoutContainer, AdminLayoutMenu } from '../../src/frontend/shared/components/AdminMenu'; import { SiteProvider } from '../../src/frontend/shared/hooks/use-site'; export default { diff --git a/services/madoc-ts/stories/capture-models/interactions/CaptureModelTestHarness.tsx b/services/madoc-ts/stories/capture-models/interactions/CaptureModelTestHarness.tsx index 91e664d4a..9b676666b 100644 --- a/services/madoc-ts/stories/capture-models/interactions/CaptureModelTestHarness.tsx +++ b/services/madoc-ts/stories/capture-models/interactions/CaptureModelTestHarness.tsx @@ -24,7 +24,7 @@ import { EditorContentVariations } from '../../../src/frontend/shared/capture-mo import { PluginProvider } from '../../../src/frontend/shared/capture-models/plugin-api/context'; import { CaptureModel } from '../../../src/frontend/shared/capture-models/types/capture-model'; import { RevisionRequest } from '../../../src/frontend/shared/capture-models/types/revision-request'; -import { RevisionList } from '../../../src/frontend/shared/components/RevisionList'; +import { RevisionList } from '../../../src/frontend/shared/capture-models/RevisionList'; import { TaskTabBackground, TaskTabItem, TaskTabRow } from '../../../src/frontend/shared/components/TaskTabs'; import { ViewerSavingContext } from '../../../src/frontend/shared/hooks/use-viewer-saving'; import { AnnotationStyles } from '../../../src/types/annotation-styles'; diff --git a/services/madoc-ts/stories/legacy/admin.stories.tsx b/services/madoc-ts/stories/legacy/admin.stories.tsx index be6416267..cc7f75409 100644 --- a/services/madoc-ts/stories/legacy/admin.stories.tsx +++ b/services/madoc-ts/stories/legacy/admin.stories.tsx @@ -1,28 +1,5 @@ import { useState } from 'react'; -import styled from 'styled-components'; -import { AdminHeader } from '../../src/frontend/admin/molecules/AdminHeader'; import * as React from 'react'; -import { Button, ButtonRow } from '../../src/frontend/shared/navigation/Button'; -import { EmptySlotActions, EmptySlotContainer, EmptySlotLabel } from '../../src/frontend/shared/layout/EmptySlot'; -import { GlobalHeader } from '../../src/frontend/shared/navigation/GlobalHeader'; -import { - PageEditorActions, - PageEditorButton, - PageEditorContainer, - PageEditorDescription, - PageEditorTitle, -} from '../../src/frontend/shared/page-blocks/PageEditor'; -import { - SlotEditorButton, - SlotEditorContainer, - SlotEditorLabel, - SlotEditorLabelReadOnly, - SlotEditorReadOnly, - SlotEditorWhy, - SlotOutlineContainer, -} from '../../src/frontend/shared/layout/SlotEditor'; -import { WidePage } from '../../src/frontend/shared/layout/WidePage'; -import { LightNavigation, LightNavigationItem } from '../../src/frontend/shared/navigation/LightNavigation'; import { AdminLayoutContainer, AdminLayoutMain, @@ -44,7 +21,30 @@ import { SiteSwitcherBackButton, SiteSwitcherContainer, SiteSwitcherSiteName, -} from '../../src/frontend/shared/components/AdminMenu'; +} from '../../src/frontend/admin/components/AdminMenu'; +import { AdminHeader } from '../../src/frontend/admin/molecules/AdminHeader'; +import { Button, ButtonRow } from '../../src/frontend/shared/navigation/Button'; +import { EmptySlotActions, EmptySlotContainer, EmptySlotLabel } from '../../src/frontend/shared/layout/EmptySlot'; +import { GlobalHeader } from '../../src/frontend/shared/navigation/GlobalHeader'; +import { + PageEditorActions, + PageEditorButton, + PageEditorContainer, + PageEditorDescription, + PageEditorTitle, +} from '../../src/frontend/shared/page-blocks/PageEditor'; +import { + SlotEditorButton, + SlotEditorContainer, + SlotEditorLabel, + SlotEditorLabelReadOnly, + SlotEditorReadOnly, + SlotEditorWhy, + SlotOutlineContainer, +} from '../../src/frontend/shared/layout/SlotEditor'; +import { WidePage } from '../../src/frontend/shared/layout/WidePage'; +import { LightNavigation, LightNavigationItem } from '../../src/frontend/shared/navigation/LightNavigation'; + import { ModalButton } from '../../src/frontend/shared/components/Modal'; import { SiteProvider } from '../../src/frontend/shared/hooks/use-site'; diff --git a/services/madoc-ts/stories/legacy/breadcrumbs.stories.tsx b/services/madoc-ts/stories/legacy/breadcrumbs.stories.tsx index 12c1c9789..fce43f036 100644 --- a/services/madoc-ts/stories/legacy/breadcrumbs.stories.tsx +++ b/services/madoc-ts/stories/legacy/breadcrumbs.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { BreadcrumbDivider, BreadcrumbItem, BreadcrumbList } from '../../src/frontend/shared/components/Breadcrumbs'; +import { BreadcrumbDivider, BreadcrumbItem, BreadcrumbList } from '../../src/frontend/site/blocks/Breadcrumbs'; export default { title: 'Legacy/Breadcrumbs' }; diff --git a/services/madoc-ts/stories/legacy/capture-model.stories.tsx b/services/madoc-ts/stories/legacy/capture-model.stories.tsx index 652a8ad7e..7eed76f76 100644 --- a/services/madoc-ts/stories/legacy/capture-model.stories.tsx +++ b/services/madoc-ts/stories/legacy/capture-model.stories.tsx @@ -1,10 +1,12 @@ import {Revisions} from '../../src/frontend/shared/capture-models/editor/stores/revisions/index'; -import { URLContextExplorer } from '../../src/frontend/shared/components/ContentExplorer'; + import * as React from 'react'; +import { ViewExternalContent } from "../../src/frontend/shared/capture-models/new/ViewExternalContent"; +import { URLContextExplorer } from "../../src/frontend/shared/features/ContentExplorer"; import { TinyButton } from '../../src/frontend/shared/navigation/Button'; import { RevisionNavigation } from '../../src/frontend/shared/capture-models/RevisionNavigation'; import { VaultProvider } from 'react-iiif-vault'; -import { ViewExternalContent } from '../../src/frontend/shared/components/ViewExternalContent'; + import '../../src/frontend/shared/capture-models/refinements/index'; export default { title: 'Legacy/Capture models' }; diff --git a/services/madoc-ts/stories/legacy/inspector.stories.tsx b/services/madoc-ts/stories/legacy/inspector.stories.tsx index 73f7142e0..0cbc8011f 100644 --- a/services/madoc-ts/stories/legacy/inspector.stories.tsx +++ b/services/madoc-ts/stories/legacy/inspector.stories.tsx @@ -2,7 +2,7 @@ import '../../src/frontend/shared/capture-models/editor/bundle'; import { hydrateCompressedModel } from '../../src/frontend/shared/capture-models/helpers/hydrate-compressed-model'; import * as React from 'react'; import { ViewDocument } from '../../src/frontend/shared/capture-models/inspector/ViewDocument'; -import { RevisionList } from '../../src/frontend/shared/components/RevisionList'; +import { RevisionList } from '../../src/frontend/shared/capture-models/RevisionList'; const fixtureOptions = [ { diff --git a/services/madoc-ts/stories/legacy/pagination.stories.tsx b/services/madoc-ts/stories/legacy/pagination.stories.tsx index cf7b0e43c..7d1cc2f43 100644 --- a/services/madoc-ts/stories/legacy/pagination.stories.tsx +++ b/services/madoc-ts/stories/legacy/pagination.stories.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { MemoryRouter } from 'react-router-dom'; import * as MoleculesPagination from '../../src/frontend/admin/molecules/Pagination'; -import { NavigationButton, PaginationContainer } from '../../src/frontend/shared/components/CanvasNavigationMinimalist'; +import { NavigationButton, PaginationContainer } from '../../src/frontend/shared/components/NavigationButton'; import { PaginationNumbered } from '../../src/frontend/shared/components/Pagination'; import * as SharedPagination from '../../src/frontend/shared/components/Pagination'; From b7c3f199efab61c5a9aba6c7d77bdca4e02a2055 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Mon, 31 Jul 2023 15:23:44 +0100 Subject: [PATCH 062/130] Site route for updates --- .../src/frontend/site/pages/view-project.tsx | 2 +- services/madoc-ts/src/gateway/api.ts | 6 +++--- services/madoc-ts/src/router.ts | 2 ++ .../src/routes/projects/project-updates.ts | 2 +- .../src/routes/site/site-project-updates.ts | 18 ++++++++++++++++++ 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 services/madoc-ts/src/routes/site/site-project-updates.ts 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 ac6ff4ffb..98e18c191 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project.tsx @@ -22,7 +22,7 @@ import { ProjectCollections } from '../blocks/ProjectCollections'; import { ProjectContributionButton } from '../blocks/ProjectContributionButton'; import { ProjectContributors } from '../blocks/ProjectContributors'; import { ProjectHeading } from '../blocks/ProjectHeading'; -import { ProjectStatistics } from '../features/ProjectStatistics'; +import { ProjectStatistics } from '../blocks/ProjectStatistics'; import { useProject } from '../hooks/use-project'; export const ViewProject: React.FC = () => { diff --git a/services/madoc-ts/src/gateway/api.ts b/services/madoc-ts/src/gateway/api.ts index 86d365db6..96ba33d5b 100644 --- a/services/madoc-ts/src/gateway/api.ts +++ b/services/madoc-ts/src/gateway/api.ts @@ -28,7 +28,7 @@ import { CompletionItem } from '../frontend/shared/capture-models/editor/input-t import { CaptureModel } from '../frontend/shared/capture-models/types/capture-model'; import { BaseField } from '../frontend/shared/capture-models/types/field-types'; import { RevisionRequest } from '../frontend/shared/capture-models/types/revision-request'; -import { FacetConfig } from '../frontend/shared/components/MetadataFacetEditor'; +import { FacetConfig } from "../frontend/shared/features/MetadataFacetEditor"; import { GetLocalisationResponse, ListLocalisationsResponse } from '../routes/admin/localisation'; import { UpdateManifestDetailsRequest } from '../routes/iiif/manifests/update-manifest-details'; import { AnnotationStyles } from '../types/annotation-styles'; @@ -686,8 +686,8 @@ export class ApiClient { }); } - async listProjectUpdates(id: string | number) { - return this.request<{ updates: ProjectUpdate[] }>(`/api/madoc/projects/${id}/updates`); + async listProjectUpdates(id: string | number, page = 1) { + return this.request<{ updates: ProjectUpdate[] }>(`/api/madoc/projects/${id}/updates?page=${page}`); } async getLatestProjectUpdate(id: string | number): Promise { diff --git a/services/madoc-ts/src/router.ts b/services/madoc-ts/src/router.ts index 474b5e226..a1a2d0a1a 100644 --- a/services/madoc-ts/src/router.ts +++ b/services/madoc-ts/src/router.ts @@ -162,6 +162,7 @@ import { siteManifestTasks } from './routes/site/site-manifest-tasks'; import { getStaticPage, sitePages } from './routes/site/site-pages'; import { listProjectsAutocomplete } from './routes/projects/list-projects-autocomplete'; import { siteProjectRecent } from './routes/site/site-project-recent'; +import { siteProjectUpdates } from "./routes/site/site-project-updates"; import { siteTaskMetadata } from './routes/site/site-task-metadata'; import { termListProxy } from './routes/site/site-term-proxy'; import { siteTerms } from './routes/site/site-terms'; @@ -692,6 +693,7 @@ export const router = new TypedRouter({ 'site-static-page': [TypedRouter.GET, '/s/:slug/madoc/api/page/static/root/:paths*', getStaticPage], 'site-resolve-slot': [TypedRouter.GET, '/s/:slug/madoc/api/slots', resolveSlots], 'site-project': [TypedRouter.GET, '/s/:slug/madoc/api/projects/:projectSlug', siteProject], + 'site-project-updates': [TypedRouter.GET, '/s/:slug/madoc/api/projects/:projectSlug/updates', siteProjectUpdates], 'site-project-recent': [TypedRouter.GET, '/s/:slug/madoc/api/projects/:projectSlug/recent', siteProjectRecent], 'site-projects': [TypedRouter.GET, '/s/:slug/madoc/api/projects', siteProjects], 'site-search': [TypedRouter.POST, '/s/:slug/madoc/api/search', siteSearch], diff --git a/services/madoc-ts/src/routes/projects/project-updates.ts b/services/madoc-ts/src/routes/projects/project-updates.ts index 1c5c3a666..33c7baeb2 100644 --- a/services/madoc-ts/src/routes/projects/project-updates.ts +++ b/services/madoc-ts/src/routes/projects/project-updates.ts @@ -17,7 +17,7 @@ export const listProjectUpdates: RouteMiddleware = async context => { const onlyOne = castBool(context.query.latest); const perPage = onlyOne ? 1 : Number(context.query.per_page || 10); - const updates = await context.projects.listProjectUpdates(project.id, siteId, perPage, page * perPage); + const updates = await context.projects.listProjectUpdates(project.id, siteId, perPage, (page - 1) * perPage); const total = await context.projects.countProjectUpdates(project.id, siteId); const totalPages = Math.ceil(total / perPage); diff --git a/services/madoc-ts/src/routes/site/site-project-updates.ts b/services/madoc-ts/src/routes/site/site-project-updates.ts new file mode 100644 index 000000000..f4c6d3d7a --- /dev/null +++ b/services/madoc-ts/src/routes/site/site-project-updates.ts @@ -0,0 +1,18 @@ +import { RouteMiddleware } from '../../types/route-middleware'; +import { castBool } from '../../utility/cast-bool'; +import { NotFound } from '../../utility/errors/not-found'; + +export const siteProjectUpdates: RouteMiddleware = async context => { + const { siteApi } = context.state; + const page = context.query.page ? Number(context.query.page) : 1; + const scope = context.state.jwt?.scope || []; + const projectSlug = context.params.projectSlug; + const onlyPublished = scope.indexOf('site.admin') !== -1 ? castBool(context.request.query.published) : true; + const project = await siteApi.getProject(projectSlug, { published: onlyPublished }); + + if (!project) { + throw new NotFound(); + } + + context.response.body = await siteApi.listProjectUpdates(projectSlug, page); +}; From f4b409ca3ae25194a6f26df06f6cdc3fbf2d1495 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Mon, 31 Jul 2023 16:57:04 +0100 Subject: [PATCH 063/130] Fixed a few bugs --- .../frontend/shared/components/TermsPopup.tsx | 41 ++++++++++++++++++- .../src/frontend/shared/navigation/Button.tsx | 8 ++++ .../site/blocks/ProjectStatistics.tsx | 2 +- .../frontend/site/pages/user/login-page.tsx | 4 +- .../src/frontend/site/pages/user/register.tsx | 6 +-- .../src/frontend/site/pages/view-project.tsx | 17 ++++---- .../tailwind/components/slot-tabs.tsx | 2 +- .../src/repository/site-user-repository.ts | 3 +- 8 files changed, 65 insertions(+), 18 deletions(-) diff --git a/services/madoc-ts/src/frontend/shared/components/TermsPopup.tsx b/services/madoc-ts/src/frontend/shared/components/TermsPopup.tsx index 8046f2e4e..567e74ab4 100644 --- a/services/madoc-ts/src/frontend/shared/components/TermsPopup.tsx +++ b/services/madoc-ts/src/frontend/shared/components/TermsPopup.tsx @@ -2,11 +2,14 @@ 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() { const user = useUser(); const site = useSite(); const { t } = useTranslation(); + const location = useLocation(); + const [closed, setClosed] = React.useState(false); if (!user || !site.latestTerms) { return null; @@ -34,6 +37,14 @@ export function TermsPopup() { 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.')}

)} - + {termsMessage}
diff --git a/services/madoc-ts/src/frontend/shared/navigation/Button.tsx b/services/madoc-ts/src/frontend/shared/navigation/Button.tsx index c2a69794c..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); } diff --git a/services/madoc-ts/src/frontend/site/blocks/ProjectStatistics.tsx b/services/madoc-ts/src/frontend/site/blocks/ProjectStatistics.tsx index bd7691c8c..9cc366d51 100644 --- a/services/madoc-ts/src/frontend/site/blocks/ProjectStatistics.tsx +++ b/services/madoc-ts/src/frontend/site/blocks/ProjectStatistics.tsx @@ -46,7 +46,7 @@ export const ProjectStatistics: React.FC = () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const total = Object.values(project?.statistics).reduce((a, b) => Math.max(a, 0) + Math.max(b, 0)); + const total = Object.values(project?.statistics || {}).reduce((a, b) => Math.max(a, 0) + Math.max(b, 0), 0); if (hideStatistics || !project) { return null; 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 8e1e9357d..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 @@ -49,9 +49,9 @@ export const LoginPage: React.FC = () => {
) : 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 8e7654fe8..6c3dfe359 100644 --- a/services/madoc-ts/src/frontend/site/pages/user/register.tsx +++ b/services/madoc-ts/src/frontend/site/pages/user/register.tsx @@ -69,7 +69,7 @@ export const Register: React.FC = () => { } const acceptTerms = site.latestTerms ? ( -
+

{t('By registering you agree to the ')} @@ -139,9 +139,9 @@ export const Register: React.FC = () => { ) : null} - +

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 98e18c191..91811c4dc 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project.tsx @@ -28,6 +28,7 @@ import { useProject } from '../hooks/use-project'; export const ViewProject: React.FC = () => { const { t } = useTranslation(); const user = useUser(); + const isAdmin = (user?.scope || []).includes('site.admin'); const { data: project } = useProject(); const available = ( @@ -56,24 +57,24 @@ export const ViewProject: React.FC = () => { {available} - + - {available} + - + - - + + +

{t('Project updates')}

- {updates?.map(update => ( + {data.updates?.map(update => ( ))} ); } -// TODO - pagination buttons, highlight most recent From eb227d9105d2b1d5bd86b788b8cd542ac1097f45 Mon Sep 17 00:00:00 2001 From: Heather0K Date: Tue, 1 Aug 2023 09:58:54 +0100 Subject: [PATCH 067/130] change use-project-update-list to use site endpoint --- .../frontend/site/hooks/use-project-updates-list.ts | 2 +- services/madoc-ts/src/gateway/api.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) 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 index a6051cfdc..3c0b2798e 100644 --- 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 @@ -10,7 +10,7 @@ export function useProjectUpdatesList() { return usePaginatedQuery(['site-project-update-list', { projectId, page }], async () => { if (projectId) { - return api.listProjectUpdates(projectId, page); + return api.ListSiteProjectUpdates(projectId, page); } }); } diff --git a/services/madoc-ts/src/gateway/api.ts b/services/madoc-ts/src/gateway/api.ts index a29319576..acfe5510e 100644 --- a/services/madoc-ts/src/gateway/api.ts +++ b/services/madoc-ts/src/gateway/api.ts @@ -686,11 +686,8 @@ export class ApiClient { }); } - async listProjectUpdates(id: string | number, page = 1) { - return this.request<{ - pagination: Pagination; - updates: ProjectUpdate[]; - }>(`/api/madoc/projects/${id}/updates?page=${page}`); + async listProjectUpdates(id: string | number) { + return this.request<{ updates: ProjectUpdate[] }>(`/api/madoc/projects/${id}/updates/`); } async getLatestProjectUpdate(id: string | number): Promise { @@ -2276,6 +2273,11 @@ export class ApiClient { `/madoc/api/projects/${projectId}/manifest-models/${manifestId}` ); } + async ListSiteProjectUpdates(id: string | number, page = 1) { + return this.publicRequest<{ pagination: Pagination; updates: ProjectUpdate[] }>( + `/madoc/api/projects/${id}/updates?page=${page}` + ); + } async getSiteConfiguration(query?: import('../routes/site/site-configuration').SiteConfigurationQuery) { return this.publicRequest(`/madoc/api/configuration`, query); From 87e63605e1fa1ef57c3239037f76d9cf60e8fda5 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 1 Aug 2023 13:23:43 +0100 Subject: [PATCH 068/130] Slot shuffle --- .../madoc-ts/src/frontend/site/pages/view-project.tsx | 5 ++++- .../src/frontend/tailwind/components/slot-tabs.tsx | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) 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 ca0a435e2..1f152381b 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project.tsx @@ -36,6 +36,7 @@ export const ViewProject: React.FC = () => { + ); @@ -60,12 +61,13 @@ export const ViewProject: React.FC = () => { - + {available} @@ -76,6 +78,7 @@ export const ViewProject: React.FC = () => { + {available}
+ {saveSettingsStatus.isSuccess ? Settings saved : null} +

Profile

- {saveSettingsStatus.isSuccess ? Settings saved : null} - {data.model ? : null} + {profileEnabled ? ( +
+ ) : null} + {data.model ? ( + + ) : null}

Privacy

{data.visibilityModel ? ( 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 01739db27..f8cfa9e92 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-user.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-user.tsx @@ -5,6 +5,7 @@ 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'; @@ -144,7 +145,7 @@ const ProfileStats = styled.div` grid-template-areas: 'value' 'label'; `; -const manuallyDisplayed = ['bio', 'institution', 'status']; +const manuallyDisplayed = ['gravitar', 'bio', 'institution', 'status']; const ContributionThumb = styled.div` background: #eee; @@ -220,6 +221,7 @@ export const ViewUser: UniversalComponent = createUniversalCompone const { t } = useTranslation(); const { data } = useData(ViewUser); const user = useUser(); + const site = useSite(); const isSelf = user && user.id === data?.user.id; @@ -235,7 +237,7 @@ export const ViewUser: UniversalComponent = createUniversalCompone <> - +
{data.user.name} diff --git a/services/madoc-ts/src/router.ts b/services/madoc-ts/src/router.ts index 1192d30c6..0c0a7f153 100644 --- a/services/madoc-ts/src/router.ts +++ b/services/madoc-ts/src/router.ts @@ -168,6 +168,7 @@ import { siteTaskMetadata } from './routes/site/site-task-metadata'; import { termListProxy } from './routes/site/site-term-proxy'; import { siteTerms } from './routes/site/site-terms'; import { siteUserAutocomplete } from './routes/site/site-user-autocomplete'; +import { siteUserImage } from './routes/site/site-user-image'; import { siteUserProfile } from './routes/site/site-user-profile'; import { saveUserSettings } from './routes/user/save-user-settings'; import { userSettingsModel } from './routes/user/user-settings-model'; @@ -755,6 +756,7 @@ export const router = new TypedRouter({ // User profile 'user-profile': [TypedRouter.GET, '/s/:slug/madoc/api/users/:id', siteUserProfile], + 'user-profile-image': [TypedRouter.GET, '/s/:slug/madoc/api/users/:id/image', siteUserImage], // Other routes. ...activityStreamRoutes, diff --git a/services/madoc-ts/src/routes/site/site-user-image.ts b/services/madoc-ts/src/routes/site/site-user-image.ts new file mode 100644 index 000000000..006a01f16 --- /dev/null +++ b/services/madoc-ts/src/routes/site/site-user-image.ts @@ -0,0 +1,39 @@ +import { pastelColour } from '../../frontend/shared/atoms/Kanban'; +import { RouteMiddleware } from '../../types/route-middleware'; +import { createHash } from 'crypto'; + +export const siteUserImage: RouteMiddleware = async context => { + const { site } = context.state; + const requestingUserId = context.state.jwt?.user.id; + const id = context.params.id; + + const fullUser = await context.siteManager.getSiteUserById(id, site.id); + const fullUserWithEmail = await context.siteManager.getUserById(id); + const info = await context.siteManager.requestUserDetails(id, requestingUserId, site.id); + + const initial = fullUser.name.trim()[0] || '?'; + const [background, colour] = pastelColour(fullUser.name + fullUser.id); + + const preview = context.query.preview; + let shouldFetchGravitar = info.allowedDetails.gravitar; + if (requestingUserId === fullUser.id && (preview === 'gravitar' || preview === 'none')) { + shouldFetchGravitar = preview === 'gravitar'; + } + + if (shouldFetchGravitar && fullUserWithEmail.email) { + const hash = createHash('md5') + .update(fullUserWithEmail.email) + .digest('hex'); + context.redirect(`https://www.gravatar.com/avatar/${hash}?s=100&d=mp&r=g`); + } else { + context.response.body = ` + + + + ${initial} + + + `; + context.set('Content-Type', 'image/svg+xml'); + } +}; diff --git a/services/madoc-ts/translations/en/madoc.json b/services/madoc-ts/translations/en/madoc.json index ed73cb1f3..ed1c38c34 100644 --- a/services/madoc-ts/translations/en/madoc.json +++ b/services/madoc-ts/translations/en/madoc.json @@ -145,6 +145,7 @@ "Card text color": "Card text color", "Carousel": "Carousel", "Center": "Center", + "Change avatar": "Change avatar", "Change password": "Change password", "Change revision": "Change revision", "Change the background of the deep zoom viewer": "Change the background of the deep zoom viewer", @@ -311,6 +312,7 @@ "Email": "Email", "Empty message": "Empty message", "Enable \"bio\" field": "Enable \"bio\" field", + "Enable \"gravitar\" field": "Enable \"gravitar\" field", "Enable \"institution\" field": "Enable \"institution\" field", "Enable \"status\" field": "Enable \"status\" field", "Enable autosave": "Enable autosave", @@ -839,6 +841,7 @@ "Thank you. You finished this manifest. Go back to the project to find a new manifest to transcribe.": "Thank you. You finished this manifest. Go back to the project to find a new manifest to transcribe.", "The following changes were requested": "The following changes were requested", "The following languages were found in content you imported": "The following languages were found in content you imported", + "The image will be fetched from Gravitar using your email address.": "The image will be fetched from Gravitar using your email address.", "The maximum number of contributions has been reached": "The maximum number of contributions has been reached", "The number of minutes after which to expire an accepted manifest task (default = 1440 mins = 1 day)": "The number of minutes after which to expire an accepted manifest task (default = 1440 mins = 1 day)", "The number of minutes after which to expire an un-started manifest task (default = 10 mins)": "The number of minutes after which to expire an un-started manifest task (default = 10 mins)", @@ -899,6 +902,7 @@ "Update required approvals": "Update required approvals", "Update terms and conditions": "Update terms and conditions", "Updates": "Updates", + "Use Gravitar": "Use Gravitar", "Use Mirador Viewer": "Use Mirador Viewer", "Use Mirador in place of the default viewer": "Use Mirador in place of the default viewer", "Use Mirador on canvas page": "Use Mirador on canvas page", From 9a2b7118ce59d88151ae507e743b3f80b6e89f41 Mon Sep 17 00:00:00 2001 From: Heather0K Date: Wed, 2 Aug 2023 12:37:24 +0100 Subject: [PATCH 071/130] hook and middlewear for getting stats and user data back for submissions --- .../site/hooks/use-project-assignee-stats.ts | 16 ++++++++ .../src/frontend/site/pages/view-project.tsx | 5 ++- .../project/ProjectContributorStatistics.tsx | 20 ++++++++++ services/madoc-ts/src/gateway/api.ts | 10 +++++ services/madoc-ts/src/router.ts | 6 +++ .../projects/list-project-assignee-stats.ts | 39 +++++++++++++++++++ services/madoc-ts/translations/en/madoc.json | 1 + 7 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 services/madoc-ts/src/frontend/site/hooks/use-project-assignee-stats.ts create mode 100644 services/madoc-ts/src/frontend/tailwind/blocks/project/ProjectContributorStatistics.tsx create mode 100644 services/madoc-ts/src/routes/projects/list-project-assignee-stats.ts 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..fd274f302 --- /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.listProjectAssigneeStats(projectId); + } + }); + + return assigneeStats; +} 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 1f152381b..39ede1dab 100644 --- a/services/madoc-ts/src/frontend/site/pages/view-project.tsx +++ b/services/madoc-ts/src/frontend/site/pages/view-project.tsx @@ -25,6 +25,7 @@ import { ProjectHeading } from '../blocks/ProjectHeading'; import { ProjectStatistics } from '../blocks/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(); @@ -80,7 +81,9 @@ export const ViewProject: React.FC = () => { {available} - + + +