diff --git a/services/madoc-ts/src/router.ts b/services/madoc-ts/src/router.ts index 33f8f1b34..ffd29ee6b 100644 --- a/services/madoc-ts/src/router.ts +++ b/services/madoc-ts/src/router.ts @@ -10,6 +10,8 @@ import { updateCanvasDetails } from './routes/iiif/canvases/update-canvas-detail import { updateManifestDetails } from './routes/iiif/manifests/update-manifest-details'; import { getAutomatedUsers } from './routes/manage-site/get-automated-users'; import { createProjectExport } from './routes/projects/create-project-export'; +import { getCanvasDetail } from "./routes/projects/get-canvas-detail"; +import { getCanvasTask } from "./routes/projects/get-canvas-task"; import { getProjectRawData } from './routes/projects/get-project-raw-data'; import { listProjectModelEntityAutocomplete } from './routes/projects/list-project-model-entity-autocomplete'; import { updateProjectAnnotationStyle } from './routes/projects/update-project-annotation-style'; @@ -506,6 +508,11 @@ export const router = new TypedRouter({ '/api/madoc/projects/:id/personal-notes/:resourceId', updateProjectNote, ], + // New canvas endpoints. + 'get-project-canvas-task': [TypedRouter.GET, '/api/madoc/projects/:id/canvas/:canvasId/task', getCanvasTask], + 'get-project-canvas-detail': [TypedRouter.GET, '/api/madoc/projects/:id/canvas/:canvasId/detail', getCanvasDetail], + + 'get-project-deletion-summary': [TypedRouter.GET, '/api/madoc/projects/:id/deletion-summary', deleteProjectSummary], 'delete-project': [TypedRouter.DELETE, '/api/madoc/projects/:id', deleteProjectEndpoint], diff --git a/services/madoc-ts/src/routes/projects/get-canvas-detail.ts b/services/madoc-ts/src/routes/projects/get-canvas-detail.ts new file mode 100644 index 000000000..54b7a6858 --- /dev/null +++ b/services/madoc-ts/src/routes/projects/get-canvas-detail.ts @@ -0,0 +1,31 @@ +import { RouteMiddleware } from '../../../types/route-middleware'; + +export const getCanvasDetail: RouteMiddleware = async context => { + // This will combine the typical requests made on the canvas page and project canvas page. + + // Sample requests made: + // - Annotation page + // - getSiteCanvas + // - getSiteCanvasPublishedModels + // - getSiteProjectCanvasModel + // - Manifest structure (next/prev) + // - API Canvas + // - getPersonalNote + // - getSiteProjectManifestTasks + // - getSiteProjectCanvasTasks + // - model-preview + + // Some require access to the Tasks API: + // - getSiteProjectManifestTasks + // - getSiteProjectCanvasTasks + + // Things this route will not do, but will also be combined into a single request: + // - Get the latest capture model (this is done on the client) + // - Can the current user submit + // - Does the user have access to the project + // - Message to the current user + + // This second route will be refetched when the user makes contributions. + + context.response.body = { test: 'getCanvasDetail' }; +}; diff --git a/services/madoc-ts/src/routes/projects/get-canvas-task.ts b/services/madoc-ts/src/routes/projects/get-canvas-task.ts new file mode 100644 index 000000000..26035968f --- /dev/null +++ b/services/madoc-ts/src/routes/projects/get-canvas-task.ts @@ -0,0 +1,155 @@ +import invariant from 'tiny-invariant'; +import { getProject } from '../../database/queries/project-queries'; +import { api } from '../../gateway/api.server'; +import { CrowdsourcingCanvasTask } from '../../gateway/tasks/crowdsourcing-canvas-task'; +import { RouteMiddleware } from '../../types/route-middleware'; +import { parseProjectId } from '../../utility/parse-project-id'; +import { optionalUserWithScope } from '../../utility/user-with-scope'; + +export const getCanvasTask: RouteMiddleware = async context => { + const { siteId, siteUrn, userUrn } = optionalUserWithScope(context, ['site.admin']); + const parsedId = parseProjectId(context.params.id); + const project = parsedId ? await context.connection.one(getProject(parsedId, siteId)) : undefined; + const canvasId = Number(context.params.canvasId as string); + const manifestId = context.query.manifest_id as string | undefined; + const siteApi = api.asUser({ siteId }); + + invariant(project, 'Project not found'); + + // Gather all tasks and data. + const [config, allTasks, projectTask, manifestTasks] = await Promise.all([ + siteApi.getProjectConfiguration(project.id, siteUrn), + siteApi.getTasks(0, { + all: true, + root_task_id: project.task_id, + subject: `urn:madoc:canvas:${canvasId}`, + detail: true, + }), + siteApi.getTask(project.task_id), + Promise.resolve( + manifestId + ? siteApi + .getTasks(0, { + all: true, + root_task_id: project.task_id, + subject: `urn:madoc:manifest:${manifestId}`, + detail: true, + }) + .then(t => t?.tasks) || [] + : [] + ), + ]); + + const canvasTask = allTasks.tasks.find(task => task.type === 'crowdsourcing-canvas-task') as + | CrowdsourcingCanvasTask + | undefined; + + const manifestTask = manifestTasks.find(task => task.type === 'crowdsourcing-manifest-task'); + const userManifestTasks = manifestTasks.filter( + task => task.type === 'crowdsourcing-task' && task.assignee && task.assignee.id === userUrn + ); + + // Start gathering the facts. + const isProjectActive = projectTask?.status === 1; + const isProjectPreparing = projectTask?.status === 4; + const isProjectPaused = !isProjectActive && !isProjectPreparing; + const isManifestComplete = manifestTask?.status === 3; + const isCanvasComplete = canvasTask?.status === 3; + + const allUserTasks = allTasks.tasks.filter(task => task.assignee && task.assignee.id === userUrn); + const currentUserTasks = allUserTasks.filter(task => task.type === 'crowdsourcing-task'); + const doesCurrentUserHaveReviewTask = allUserTasks.some( + task => task.type === 'crowdsourcing-review' && task.status !== 3 + ); + + const hasUserTaskBeenRejected = currentUserTasks.some(task => task.status === -1); + const canUserStillSubmitAfterRejection = !config.modelPageOptions?.preventContributionAfterRejection; + + const uniqueContributors = new Set(); + + for (const task of allTasks.tasks) { + if (task.type === 'crowdsourcing-task' && task.status !== -1 && task.assignee) { + uniqueContributors.add(task.assignee.id); + } + } + const totalUniqueContributors = uniqueContributors.size; + const maxContributorsReached = config.maxContributionsPerResource + ? totalUniqueContributors >= config.maxContributionsPerResource + : false; + + const hasUserSubmittedTask = currentUserTasks.some(task => task.status === 2); + const hasUserCompletedTask = currentUserTasks.some(task => task.status === 3); + const hasUserCompletedAndCanStillContribute = + (hasUserCompletedTask || hasUserSubmittedTask) && config.allowSubmissionsWhenCanvasComplete; + + const isUserAssignedManifest = userManifestTasks.filter(task => task.status !== -1).length > 0; + const hasUserCompletedManifest = userManifestTasks.filter(task => (task.status || 0) >= 2).length > 0; + const isManifestInReview = userManifestTasks.filter(task => (task.status || 0) === 2).length > 0; + + const isUserWorkingOnCanvas = currentUserTasks.length > 0; + + const inProgressUserTask = currentUserTasks.find( + task => (task.type === 'crowdsourcing-task' && task.status === 1) || task.status === 0 + ); + const rejectedUserTask = currentUserTasks.find(task => task.type === 'crowdsourcing-task' && task.status === -1); + + const rejectedMessage = !inProgressUserTask && rejectedUserTask ? rejectedUserTask.state.rejectedMessage : undefined; + const progressMessage = inProgressUserTask ? 'In progress' : undefined; + + const completeMessage = hasUserCompletedTask ? 'Complete' : hasUserSubmittedTask ? 'Submitted for review' : undefined; + + const canUserMakeNewContribution = + (isUserWorkingOnCanvas && + (!hasUserCompletedTask || hasUserCompletedAndCanStillContribute) && + (!hasUserTaskBeenRejected || canUserStillSubmitAfterRejection)) || + !maxContributorsReached; + + context.response.body = { + // This will change the most often. + canWork: canUserMakeNewContribution, + + // @todo get the new field for contribution "type" from the project template. + // - Add data that would be required from the tasks (e.g. revision) + // detail: { + // type: 'default', + // revisionId: '...', + // }, + messages: { + rejected: rejectedMessage, + progress: progressMessage, + complete: completeMessage, + }, + manifestFacts: { + isManifestComplete, + isManifestInReview, + isUserAssignedManifest, + hasUserCompletedManifest, + }, + canvasFacts: { + isUserWorkingOnCanvas, + canUserMakeNewContribution, + hasUserSubmittedTask, + hasUserCompletedTask, + hasUserCompletedAndCanStillContribute, + maxContributorsReached, + totalUniqueContributors, + hasUserTaskBeenRejected, + canUserStillSubmitAfterRejection, + doesCurrentUserHaveReviewTask, + isManifestComplete, + isCanvasComplete, + }, + projectFacts: { + isProjectPaused, + isProjectPreparing, + isProjectActive, + }, + userManifestTasks, + currentUserTasks, + manifestId, + manifestTask, + canvasTask, + config, + project, + }; +}; diff --git a/services/madoc-ts/src/types/canvas-sidebar.ts b/services/madoc-ts/src/types/canvas-sidebar.ts new file mode 100644 index 000000000..bc40610fa --- /dev/null +++ b/services/madoc-ts/src/types/canvas-sidebar.ts @@ -0,0 +1,52 @@ +import { InternationalString, MetadataItem } from '@iiif/presentation-3'; +import { CaptureModel } from '../frontend/shared/capture-models/types/capture-model'; + +export interface CanvasSidebar { + metadata?: MetadataItem[]; + + annotations?: Array<{ + id: string; + label: InternationalString; + type: 'project' | 'capture-model' | 'external'; + count?: number; + }>; + + transcriptions: Array<{ + id: string; + text: string; + primary?: boolean; + }>; + + translations: Array<{ + id: string; + text: string; + language: string; + }>; + + documents: Array<{ + id: string; + label: InternationalString; + project?: { id: number; slug: string; template?: string }; + model?: CaptureModel['document']; + template?: { + type: 'markdown' | 'html' | 'text'; + value: string; + }; + }>; + + personalNotes: { + enabled?: boolean; + count: number; + }; + + downloads: { + enabled?: boolean; + count: number; + items: Array<{ + id: string; + label: InternationalString; + type: string; + url: string; + }>; + }; +}