diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db245d58..9af1fef69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - Added runtime configuration options for homepage branding and support link. - Added an environment variable to docker-compose-dev.yml to hide the OIDC client used in the SPA from the JSF frontend: DATAVERSE_AUTH_OIDC_HIDDEN_JSF: 1 - Download with terms of use and guestbook. +- Show terms modal before download when dataset has custom terms, a non-default license (not CC0 1.0), or a guestbook. Draft datasets and dataset editors bypass the modal. ### Changed diff --git a/dev-env/shib-dev-env/keycloak/test-realm.json b/dev-env/shib-dev-env/keycloak/test-realm.json index ea5a07378..0b53d51db 100644 --- a/dev-env/shib-dev-env/keycloak/test-realm.json +++ b/dev-env/shib-dev-env/keycloak/test-realm.json @@ -940,7 +940,7 @@ "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ - "https://localhost/spa/*" + "https://localhost/modern/*" ], "webOrigins": [ "+" diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx index fda7c18bd..74d1ae2cf 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx @@ -8,12 +8,13 @@ import { DatasetLicense, DatasetPermissions, DatasetPublishingStatus, - DatasetVersion + DatasetVersion, + defaultLicense } from '../../../../dataset/domain/models/Dataset' import { FileDownloadSize, FileDownloadMode } from '../../../../files/domain/models/FileMetadata' import { DatasetExploreOptions } from '../DatasetToolsOptions' import { useAccessRepository } from '@/sections/access/AccessRepositoryContext' -import { DownloadWithGuestbookModal } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal' +import { DownloadWithTermsAndGuestbookModal } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal' import { downloadFromSignedUrl, EMPTY_GUESTBOOK_RESPONSE, @@ -47,14 +48,18 @@ export function AccessDatasetMenu({ customTerms }: AccessDatasetMenuProps) { const { t } = useTranslation('dataset') - const [showDownloadWithGuestbookModal, setShowDownloadWithGuestbookModal] = useState(false) + const [showDownloadWithTermsAndGuestbookModal, setShowDownloadWithTermsAndGuestbookModal] = + useState(false) const [selectedDownloadFormat, setSelectedDownloadFormat] = useState( FileDownloadMode.ORIGINAL ) - const hasGuestbook = - guestbookId !== undefined && - version.publishingStatus !== DatasetPublishingStatus.DRAFT && - !permissions.canUpdateDataset + const isDraft = version.publishingStatus === DatasetPublishingStatus.DRAFT + const bypassTermsGuard = isDraft || permissions.canUpdateDataset + const hasGuestbook = guestbookId !== undefined + const hasNonDefaultLicense = license !== undefined && license.name !== defaultLicense.name + const hasCustomTerms = customTerms !== undefined + const shouldShowModal = + !bypassTermsGuard && (hasGuestbook || hasCustomTerms || hasNonDefaultLicense) const flesToDownloadSizeIsZero = fileDownloadSizes.map(({ value }) => value).reduce((acc, curr) => acc + curr, 0) === 0 @@ -79,7 +84,7 @@ export function AccessDatasetMenu({ ) => { event.preventDefault() setSelectedDownloadFormat(mode) - setShowDownloadWithGuestbookModal(true) + setShowDownloadWithTermsAndGuestbookModal(true) } return ( @@ -96,15 +101,15 @@ export function AccessDatasetMenu({ datasetNumericId={datasetNumericId} hasOneTabularFileAtLeast={hasOneTabularFileAtLeast} fileDownloadSizes={fileDownloadSizes} - hasGuestbook={hasGuestbook} + requiresTermsOrGuestbook={shouldShowModal} onDownloadWithGuestbook={handleDownloadWithGuestbook} /> - {hasGuestbook && showDownloadWithGuestbookModal && ( - setShowDownloadWithGuestbookModal(false)} + {shouldShowModal && showDownloadWithTermsAndGuestbookModal && ( + setShowDownloadWithTermsAndGuestbookModal(false)} datasetId={datasetNumericId} // TODO: we should allow this to pass persistentId when we have the backend support for guestbook submission with persistentId datasetPersistentId={persistentId} guestbookId={guestbookId} @@ -121,7 +126,7 @@ interface DatasetDownloadOptionsProps { datasetNumericId?: number | string hasOneTabularFileAtLeast: boolean fileDownloadSizes: FileDownloadSize[] - hasGuestbook: boolean + requiresTermsOrGuestbook: boolean onDownloadWithGuestbook: (event: React.MouseEvent, mode: FileDownloadMode) => void } @@ -129,7 +134,7 @@ const DatasetDownloadOptions = ({ datasetNumericId, hasOneTabularFileAtLeast, fileDownloadSizes, - hasGuestbook, + requiresTermsOrGuestbook, onDownloadWithGuestbook }: DatasetDownloadOptionsProps) => { const { t } = useTranslation('dataset') @@ -140,7 +145,7 @@ const DatasetDownloadOptions = ({ event: React.MouseEvent, mode: FileDownloadMode ): void => { - if (hasGuestbook) { + if (requiresTermsOrGuestbook) { onDownloadWithGuestbook(event, mode) return } diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.tsx index 7283a0ef2..96336e6c4 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.tsx @@ -3,14 +3,14 @@ import { useTranslation } from 'react-i18next' import { Button, DropdownButton, DropdownButtonItem } from '@iqss/dataverse-design-system' import { MouseEvent, useState } from 'react' import { toast } from 'react-toastify' -import { DatasetPublishingStatus } from '@/dataset/domain/models/Dataset' +import { DatasetPublishingStatus, defaultLicense } from '@/dataset/domain/models/Dataset' import { FileDownloadMode } from '../../../../../../files/domain/models/FileMetadata' import { useDataset } from '../../../../DatasetContext' import { FileSelection } from '../../row-selection/useFileSelection' import { NoSelectedFilesModal } from '../no-selected-files-modal/NoSelectedFilesModal' import { FilePreview } from '../../../../../../files/domain/models/FilePreview' import { useMediaQuery } from '../../../../../../shared/hooks/useMediaQuery' -import { DownloadWithGuestbookModal } from '../file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal' +import { DownloadWithTermsAndGuestbookModal } from '../file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal' import { downloadFromSignedUrl, EMPTY_GUESTBOOK_RESPONSE, @@ -31,7 +31,8 @@ export function DownloadFilesButton({ files, fileSelection }: DownloadFilesButto const { t } = useTranslation('files') const { dataset } = useDataset() const [showNoFilesSelectedModal, setShowNoFilesSelectedModal] = useState(false) - const [showDownloadWithGuestbookModal, setShowDownloadWithGuestbookModal] = useState(false) + const [showDownloadWithTermsAndGuestbookModal, setShowDownloadWithTermsAndGuestbookModal] = + useState(false) const [selectedDownloadFormat, setSelectedDownloadFormat] = useState( FileDownloadMode.ORIGINAL ) @@ -43,10 +44,15 @@ export function DownloadFilesButton({ files, fileSelection }: DownloadFilesButto const fileIdsForGuestbookSubmission = allFilesSelected ? undefined : getFileIdsFromSelection(fileSelection) - const hasGuestbook = - dataset?.guestbookId !== undefined && - dataset?.version.publishingStatus !== DatasetPublishingStatus.DRAFT && - !dataset?.permissions.canUpdateDataset + const isDraftDataset = dataset?.version.publishingStatus === DatasetPublishingStatus.DRAFT + const canEdit = dataset?.permissions.canUpdateDataset ?? false + const bypassTermsGuard = isDraftDataset || canEdit + const hasGuestbook = dataset?.guestbookId !== undefined + const hasNonDefaultLicense = + dataset?.license !== undefined && dataset.license.name !== defaultLicense.name + const hasCustomTerms = dataset?.termsOfUse?.customTerms !== undefined + const shouldShowModal = + !bypassTermsGuard && (hasGuestbook || hasCustomTerms || hasNonDefaultLicense) const onClick = (event: MouseEvent, downloadMode: FileDownloadMode) => { if (fileSelectionCount === SELECTED_FILES_EMPTY) { @@ -55,10 +61,10 @@ export function DownloadFilesButton({ files, fileSelection }: DownloadFilesButto return } - if (hasGuestbook) { + if (shouldShowModal) { event.preventDefault() setSelectedDownloadFormat(downloadMode) - setShowDownloadWithGuestbookModal(true) + setShowDownloadWithTermsAndGuestbookModal(true) return } @@ -101,15 +107,17 @@ export function DownloadFilesButton({ files, fileSelection }: DownloadFilesButto show={showNoFilesSelectedModal} handleClose={() => setShowNoFilesSelectedModal(false)} /> - {hasGuestbook && showDownloadWithGuestbookModal && ( - setShowDownloadWithGuestbookModal(false)} + datasetLicense={dataset.license} + datasetCustomTerms={dataset.termsOfUse?.customTerms} + show={showDownloadWithTermsAndGuestbookModal} + handleClose={() => setShowDownloadWithTermsAndGuestbookModal(false)} /> )} @@ -140,27 +148,15 @@ export function DownloadFilesButton({ files, fileSelection }: DownloadFilesButto // no tabular file content return ( <> - {hasGuestbook ? ( - - ) : ( - - )} + {downloadFeedbackModals} diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal.module.scss b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.module.scss similarity index 100% rename from src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal.module.scss rename to src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.module.scss diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.tsx similarity index 94% rename from src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal.tsx rename to src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.tsx index f1f0f93dd..1e873c074 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.tsx @@ -17,10 +17,10 @@ import { GuestbookAnswerDTO, GuestbookResponseDTO } from '@/access/domain/repositories/AccessRepository' -import { downloadFromSignedUrl } from '@/shared/helpers/DownloadHelper' +import { downloadFromSignedUrl, EMPTY_GUESTBOOK_RESPONSE } from '@/shared/helpers/DownloadHelper' import { FileDownloadMode } from '@/files/domain/models/FileMetadata' -interface DownloadWithGuestbookModalProps { +interface DownloadWithTermsAndGuestbookModalProps { fileId?: number | string fileIds?: Array format?: string | FileDownloadMode @@ -34,7 +34,7 @@ interface DownloadWithGuestbookModalProps { } type GuestbookFormValues = Record -export function DownloadWithGuestbookModal({ +export function DownloadWithTermsAndGuestbookModal({ fileId, fileIds, format, @@ -45,18 +45,19 @@ export function DownloadWithGuestbookModal({ datasetCustomTerms, show, handleClose -}: DownloadWithGuestbookModalProps) { +}: DownloadWithTermsAndGuestbookModalProps) { const { t: tFiles } = useTranslation('files') const { t: tDataset } = useTranslation('dataset') const { user } = useSession() const accessRepository = useAccessRepository() const guestbookRepository = useGuestbookRepository() + const hasGuestbook = guestbookId !== undefined const [formValues, setFormValues] = useState({}) const { guestbook, isLoadingGuestbook, errorGetGuestbook } = useGetGuestbookById({ guestbookRepository, guestbookId, - enabled: show + enabled: show && hasGuestbook }) const accountFieldKeys = useMemo(() => ['name', 'email', 'institution', 'position'], []) @@ -178,6 +179,10 @@ export function DownloadWithGuestbookModal({ } const buildGuestbookResponse = (): GuestbookResponseDTO => { + if (!guestbook) { + return EMPTY_GUESTBOOK_RESPONSE + } + const customQuestionAnswers = customQuestions.reduce( (answers, question, index) => { const fieldName = getGuestbookCustomQuestionFieldName(question, index) @@ -275,12 +280,14 @@ export function DownloadWithGuestbookModal({ onClick={() => void handleSubmit({ hasFormErrors: hasAccountFieldErrors || hasCustomQuestionErrors, - guestbook, guestbookResponse: buildGuestbookResponse() }) } disabled={ - isLoadingGuestbook || isSubmittingGuestbook || !!errorGetGuestbook || !guestbook + isLoadingGuestbook || + isSubmittingGuestbook || + !!errorGetGuestbook || + (hasGuestbook && !guestbook) } aria-label={tFiles('requestAccess.confirmation')}> {isSubmittingGuestbook ? : null}{' '} diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/GuestbookCollectForm.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/GuestbookCollectForm.tsx index 4150a5efa..be72aba58 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/GuestbookCollectForm.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/GuestbookCollectForm.tsx @@ -6,7 +6,7 @@ import { CustomTerms as CustomTermsModel, DatasetLicense } from '@/dataset/domai import { Validator as validator } from '@/shared/helpers/Validator' import { CustomTerms } from '@/sections/dataset/dataset-terms/CustomTerms' import { QueryParamKey, Route } from '@/sections/Route.enum' -import styles from './DownloadWithGuestbookModal.module.scss' +import styles from './DownloadWithTermsAndGuestbookModal.module.scss' interface GuestbookCollectFormProps { license?: DatasetLicense @@ -54,7 +54,7 @@ export function GuestbookCollectForm({ [guestbook?.customQuestions] ) const customTermsHref = datasetPersistentId - ? `/spa${Route.DATASETS}?${QueryParamKey.PERSISTENT_ID}=${encodeURIComponent( + ? `/modern${Route.DATASETS}?${QueryParamKey.PERSISTENT_ID}=${encodeURIComponent( datasetPersistentId )}&${QueryParamKey.TAB}=terms&termsTab=guestbook` : undefined @@ -176,65 +176,69 @@ export function GuestbookCollectForm({ - {accountFieldKeys.map((accountFieldKey) => { - const fieldLabel = tGuestbooks(`create.fields.dataCollected.options.${accountFieldKey}`) - const isRequired = isAccountFieldRequired(accountFieldKey) - const isIdentityField = accountFieldKey === 'name' || accountFieldKey === 'email' - const shouldDisableInput = shouldLockIdentityFields && isIdentityField - const invalid = hasAttemptedAccept && accountFieldErrors[accountFieldKey] !== null + {guestbook && ( + <> + {accountFieldKeys.map((accountFieldKey) => { + const fieldLabel = tGuestbooks(`create.fields.dataCollected.options.${accountFieldKey}`) + const isRequired = isAccountFieldRequired(accountFieldKey) + const isIdentityField = accountFieldKey === 'name' || accountFieldKey === 'email' + const shouldDisableInput = shouldLockIdentityFields && isIdentityField + const invalid = hasAttemptedAccept && accountFieldErrors[accountFieldKey] !== null - return ( - - - - {fieldLabel} - {isRequired && *} - - - - - onFieldChange(accountFieldKey, (event.target as HTMLInputElement).value) - } - isInvalid={invalid} - disabled={shouldDisableInput} - aria-required={isRequired} - /> - - {invalid ? accountFieldErrors[accountFieldKey] : undefined} - - - - ) - })} + return ( + + + + {fieldLabel} + {isRequired && *} + + + + + onFieldChange(accountFieldKey, (event.target as HTMLInputElement).value) + } + isInvalid={invalid} + disabled={shouldDisableInput} + aria-required={isRequired} + /> + + {invalid ? accountFieldErrors[accountFieldKey] : undefined} + + + + ) + })} - {customQuestions.length > 0 && ( - - - - {tFiles('actions.optionsMenu.guestbookCollectModal.additionalQuestions')} - - - - {customQuestions.map((question, index) => ( -
- - {renderCustomQuestionField(question, index)} -
- ))} - -
+ {customQuestions.length > 0 && ( + + + + {tFiles('actions.optionsMenu.guestbookCollectModal.additionalQuestions')} + + + + {customQuestions.map((question, index) => ( +
+ + {renderCustomQuestionField(question, index)} +
+ ))} + +
+ )} + )} ) diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/useGuestbookCollectSubmission.ts b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/useGuestbookCollectSubmission.ts index 52d829a1a..0dcecb195 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/useGuestbookCollectSubmission.ts +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/useGuestbookCollectSubmission.ts @@ -10,7 +10,6 @@ import { AccessRepository, GuestbookResponseDTO } from '@/access/domain/repositories/AccessRepository' -import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' interface UseGuestbookCollectSubmissionProps { @@ -25,7 +24,6 @@ interface UseGuestbookCollectSubmissionProps { interface HandleSubmitProps { hasFormErrors: boolean - guestbook?: Guestbook guestbookResponse: GuestbookResponseDTO } @@ -62,11 +60,11 @@ export const useGuestbookCollectSubmission = ({ : undefined const handleSubmit = useCallback( - async ({ hasFormErrors, guestbook, guestbookResponse }: HandleSubmitProps) => { + async ({ hasFormErrors, guestbookResponse }: HandleSubmitProps) => { setHasAttemptedAccept(true) setErrorDownloadSignedUrlFile(null) - if (hasFormErrors || !guestbook) { + if (hasFormErrors) { return } diff --git a/src/sections/file/file-action-buttons/access-file-menu/FileDownloadOptions.tsx b/src/sections/file/file-action-buttons/access-file-menu/FileDownloadOptions.tsx index 888c48e6e..b9f793018 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/FileDownloadOptions.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/FileDownloadOptions.tsx @@ -13,9 +13,10 @@ import { useDataset } from '@/sections/dataset/DatasetContext' import { CustomTerms, DatasetLicense, - DatasetPublishingStatus + DatasetPublishingStatus, + defaultLicense } from '@/dataset/domain/models/Dataset' -import { DownloadWithGuestbookModal } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal' +import { DownloadWithTermsAndGuestbookModal } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal' interface FileDownloadOptionsProps { fileId: number @@ -48,7 +49,8 @@ export function FileDownloadOptions({ }: FileDownloadOptionsProps) { const { t } = useTranslation('files') const { dataset } = useDataset() - const [showDownloadWithGuestbookModal, setShowDownloadWithGuestbookModal] = useState(false) + const [showDownloadWithTermsAndGuestbookModal, setShowDownloadWithTermsAndGuestbookModal] = + useState(false) const [selectedDownloadFormat, setSelectedDownloadFormat] = useState( FileDownloadMode.ORIGINAL ) @@ -57,7 +59,6 @@ export function FileDownloadOptions({ return <> } - const resolvedGuestbookId = guestbookId ?? dataset?.guestbookId const resolvedDatasetPersistentId = datasetPersistentId ?? dataset?.persistentId const resolvedDatasetLicense = datasetLicense ?? dataset?.license const resolvedDatasetCustomTerms = datasetCustomTerms ?? dataset?.termsOfUse?.customTerms @@ -65,12 +66,18 @@ export function FileDownloadOptions({ const resolvedIsDraftDataset = isDraft ?? dataset?.version.publishingStatus === DatasetPublishingStatus.DRAFT const resolvedCanEdit = canEdit ?? dataset?.permissions.canUpdateDataset ?? false - const hasGuestbook = - resolvedGuestbookId !== undefined && !resolvedIsDraftDataset && !resolvedCanEdit + const resolvedGuestbookId = guestbookId ?? dataset?.guestbookId + const hasGuestbook = resolvedGuestbookId !== undefined + const bypassTermsGuard = resolvedIsDraftDataset || resolvedCanEdit + const hasNonDefaultLicense = + resolvedDatasetLicense !== undefined && resolvedDatasetLicense.name !== defaultLicense.name + const hasCustomTerms = resolvedDatasetCustomTerms !== undefined + const shouldShowModal = + !bypassTermsGuard && (hasGuestbook || hasCustomTerms || hasNonDefaultLicense) const openGuestbookModal = (format: string | FileDownloadMode) => { setSelectedDownloadFormat(format) - setShowDownloadWithGuestbookModal(true) + setShowDownloadWithTermsAndGuestbookModal(true) } return ( @@ -84,14 +91,14 @@ export function FileDownloadOptions({ type={type} ingestInProgress={ingestInProgress} downloadUrls={downloadUrls} - hasGuestbook={hasGuestbook} + requiresTermsOrGuestbook={shouldShowModal} onOpenGuestbookModal={openGuestbookModal} isLockedFromFileDownload={isLockedFromFileDownload} /> ) : ( openGuestbookModal(FileDownloadMode.ORIGINAL)} type={type} ingestIsInProgress={ingestInProgress} @@ -99,16 +106,16 @@ export function FileDownloadOptions({ isLockedFromFileDownload={isLockedFromFileDownload} /> )} - {hasGuestbook && showDownloadWithGuestbookModal && ( - setShowDownloadWithGuestbookModal(false)} + show={showDownloadWithTermsAndGuestbookModal} + handleClose={() => setShowDownloadWithTermsAndGuestbookModal(false)} /> )} diff --git a/src/sections/file/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.tsx b/src/sections/file/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.tsx index 27369dbe0..01992f9a2 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.tsx @@ -12,7 +12,7 @@ import { interface FileNonTabularDownloadOptionsProps { fileId: number - hasGuestbook: boolean + requiresTermsOrGuestbook: boolean onOpenGuestbookModal: () => void type: FileType downloadUrlOriginal: string @@ -22,7 +22,7 @@ interface FileNonTabularDownloadOptionsProps { export function FileNonTabularDownloadOptions({ fileId, - hasGuestbook, + requiresTermsOrGuestbook, onOpenGuestbookModal, type, ingestIsInProgress, @@ -37,7 +37,7 @@ export function FileNonTabularDownloadOptions({ return } - if (hasGuestbook) { + if (requiresTermsOrGuestbook) { event.preventDefault() onOpenGuestbookModal() return diff --git a/src/sections/file/file-action-buttons/access-file-menu/FileTabularDownloadOptions.tsx b/src/sections/file/file-action-buttons/access-file-menu/FileTabularDownloadOptions.tsx index 63ab95a88..86b7787fe 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/FileTabularDownloadOptions.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/FileTabularDownloadOptions.tsx @@ -17,7 +17,7 @@ interface FileTabularDownloadOptionsProps { type: FileType ingestInProgress: boolean downloadUrls: FileDownloadUrls - hasGuestbook: boolean + requiresTermsOrGuestbook: boolean onOpenGuestbookModal: (format: string) => void isLockedFromFileDownload: boolean } @@ -27,7 +27,7 @@ export function FileTabularDownloadOptions({ type, ingestInProgress, downloadUrls, - hasGuestbook, + requiresTermsOrGuestbook, onOpenGuestbookModal, isLockedFromFileDownload }: FileTabularDownloadOptionsProps) { @@ -42,7 +42,7 @@ export function FileTabularDownloadOptions({ event.preventDefault() - if (hasGuestbook) { + if (requiresTermsOrGuestbook) { onOpenGuestbookModal(format) return } diff --git a/src/stories/guestbooks/guestbook-applied-modal/DownloadWithGuestbookModal.stories.tsx b/src/stories/guestbooks/guestbook-applied-modal/DownloadWithTermsAndGuestbookModal.stories.tsx similarity index 66% rename from src/stories/guestbooks/guestbook-applied-modal/DownloadWithGuestbookModal.stories.tsx rename to src/stories/guestbooks/guestbook-applied-modal/DownloadWithTermsAndGuestbookModal.stories.tsx index e99810440..1d1dfb810 100644 --- a/src/stories/guestbooks/guestbook-applied-modal/DownloadWithGuestbookModal.stories.tsx +++ b/src/stories/guestbooks/guestbook-applied-modal/DownloadWithTermsAndGuestbookModal.stories.tsx @@ -1,5 +1,5 @@ import { Meta, StoryObj } from '@storybook/react' -import { DownloadWithGuestbookModal } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal' +import { DownloadWithTermsAndGuestbookModal } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal' import { WithI18next } from '@/stories/WithI18next' import { WithLoggedInUser } from '@/stories/WithLoggedInUser' import { GuestbookMockRepository } from '@/stories/shared-mock-repositories/guestbook/GuestbookMockRepository' @@ -25,9 +25,9 @@ const accessRepository: AccessRepository = { const guestbookRepository = new GuestbookMockRepository() -const meta: Meta = { - title: 'Sections/Guestbooks/DownloadWithGuestbookModal', - component: DownloadWithGuestbookModal, +const meta: Meta = { + title: 'Sections/Guestbooks/DownloadWithTermsAndGuestbookModal', + component: DownloadWithTermsAndGuestbookModal, decorators: [ WithI18next, WithLoggedInUser, @@ -49,7 +49,7 @@ const meta: Meta = { } export default meta -type Story = StoryObj +type Story = StoryObj export const SingleFile: Story = { args: { @@ -68,3 +68,29 @@ export const MultipleFiles: Story = { guestbookId: 3 } } + +export const CustomTermsOnly: Story = { + args: { + show: true, + handleClose: () => {}, + fileId: 123, + datasetCustomTerms: { + termsOfUse: 'These are custom terms of use for this dataset.', + confidentialityDeclaration: 'All data must be kept confidential.', + restrictions: 'Do not redistribute without permission.' + } + } +} + +export const NonDefaultLicenseOnly: Story = { + args: { + show: true, + handleClose: () => {}, + fileId: 123, + datasetLicense: { + name: 'CC BY 4.0', + uri: 'https://creativecommons.org/licenses/by/4.0', + iconUri: 'https://licensebuttons.net/l/by/4.0/88x31.png' + } + } +} diff --git a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx index 63b2afae5..639811886 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx @@ -9,6 +9,7 @@ import { getGuestbook, submitGuestbookForDatasetDownload } from '@iqss/dataverse import { ReactNode, Suspense } from 'react' import { useTranslation } from 'react-i18next' import { AccessRepository } from '@/access/domain/repositories/AccessRepository' +import { DatasetPermissions } from '@/dataset/domain/models/Dataset' import { AccessRepositoryProvider } from '@/sections/access/AccessRepositoryProvider' function TranslationPreloader({ children }: { children: ReactNode }) { @@ -287,7 +288,7 @@ describe('AccessDatasetMenu', () => { cy.findByRole('button', { name: 'Access Dataset' }).should('not.exist') }) - it('opens DownloadWithGuestbookModal when guestbook exists and download option is clicked', () => { + it('opens DownloadWithTermsAndGuestbookModal when guestbook exists and download option is clicked', () => { const version = DatasetVersionMother.createReleased() const permissions = DatasetPermissionsMother.createWithFilesDownloadAllowedButNotUpdatePermissions() @@ -433,7 +434,7 @@ describe('AccessDatasetMenu', () => { cy.findByRole('dialog').should('exist') }) - it('closes DownloadWithGuestbookModal when cancel is clicked', () => { + it('closes DownloadWithTermsAndGuestbookModal when cancel is clicked', () => { const version = DatasetVersionMother.createReleased() const permissions = DatasetPermissionsMother.createWithFilesDownloadAllowedButNotUpdatePermissions() @@ -680,4 +681,171 @@ describe('AccessDatasetMenu', () => { cy.findByRole('dialog').should('not.exist') cy.findByText('Your download has started.').should('exist') }) + + it('opens the terms modal when custom terms exist without a guestbook', () => { + const version = DatasetVersionMother.createReleased() + const permissions: DatasetPermissions = + DatasetPermissionsMother.createWithFilesDownloadAllowedButNotUpdatePermissions() + const fileDownloadSizes = [ + DatasetFileDownloadSizeMother.createOriginal({ value: 2000, unit: FileSizeUnit.BYTES }) + ] + + cy.customMount( + withAccessRepository( + + + + + + ) + ) + + cy.findByRole('button', { name: 'Access Dataset' }).click() + cy.findByRole('button', { name: /Download ZIP/ }) + .should('exist') + .click() + + cy.findByRole('dialog').should('exist') + cy.findByText('Custom Dataset Terms').should('exist') + // Guestbook fields should not be visible since there is no guestbook + cy.findByLabelText(/name/i).should('not.exist') + cy.findByLabelText(/email/i).should('not.exist') + }) + + it('opens the terms modal when CC BY 4.0 license exists without a guestbook', () => { + const version = DatasetVersionMother.createReleased() + const permissions: DatasetPermissions = + DatasetPermissionsMother.createWithFilesDownloadAllowedButNotUpdatePermissions() + const fileDownloadSizes = [ + DatasetFileDownloadSizeMother.createOriginal({ value: 2000, unit: FileSizeUnit.BYTES }) + ] + + cy.customMount( + withAccessRepository( + + + + + + ) + ) + + cy.findByRole('button', { name: 'Access Dataset' }).click() + cy.findByRole('button', { name: /Download ZIP/ }) + .should('exist') + .click() + + cy.findByRole('dialog').should('exist') + // Guestbook fields should not be visible since there is no guestbook + cy.findByLabelText(/name/i).should('not.exist') + cy.findByLabelText(/email/i).should('not.exist') + }) + + it('downloads directly when custom terms exist but user can edit the dataset', () => { + const version = DatasetVersionMother.createReleased() + const permissions = DatasetPermissionsMother.create({ + canDownloadFiles: true, + canUpdateDataset: true + }) + const fileDownloadSizes = [ + DatasetFileDownloadSizeMother.createOriginal({ value: 2000, unit: FileSizeUnit.BYTES }) + ] + + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + + cy.customMount( + withAccessRepository( + + + + + + ) + ) + + cy.findByRole('button', { name: 'Access Dataset' }).click() + cy.findByRole('button', { name: /Download ZIP/ }).click() + + cy.get('@anchorClick').should('have.been.calledOnce') + cy.findByRole('dialog').should('not.exist') + cy.findByText('Your download has started.').should('exist') + }) + + it('bypasses the custom terms modal for draft datasets', () => { + const version = DatasetVersionMother.createDraft() + const permissions = + DatasetPermissionsMother.createWithFilesDownloadAllowedButNotUpdatePermissions() + const fileDownloadSizes = [ + DatasetFileDownloadSizeMother.createOriginal({ value: 2000, unit: FileSizeUnit.BYTES }) + ] + + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + + cy.customMount( + withAccessRepository( + + + + + + ) + ) + + cy.findByRole('button', { name: 'Access Dataset' }).click() + cy.findByRole('button', { name: /Download ZIP/ }).click() + + cy.get('@anchorClick').should('have.been.calledOnce') + cy.findByRole('dialog').should('not.exist') + cy.findByText('Your download has started.').should('exist') + }) }) diff --git a/tests/component/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.spec.tsx index 0845f76a8..077c3ae3c 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.spec.tsx @@ -20,6 +20,10 @@ import { FileMetadataMother } from '../../../../../../files/domain/models/FileMe import { MultipleFileDownloadProvider } from '../../../../../../../../src/sections/file/multiple-file-download/MultipleFileDownloadProvider' import { FileRepository } from '../../../../../../../../src/files/domain/repositories/FileRepository' import { FilePreviewMother } from '../../../../../../files/domain/models/FilePreviewMother' +import { + CustomTermsMother, + TermsOfUseMother +} from '../../../../../../dataset/domain/models/TermsOfUseMother' const datasetRepository: DatasetRepository = {} as DatasetRepository const fileRepository = {} as FileRepository @@ -642,6 +646,104 @@ describe('DownloadFilesButton', () => { cy.findByText('Your download has started.').should('exist') }) + it('opens the terms modal when custom terms exist without a guestbook for non-tabular files', () => { + const datasetWithCustomTerms = DatasetMother.create({ + permissions: DatasetPermissionsMother.createWithFilesDownloadAllowedButNotUpdatePermissions(), + hasOneTabularFileAtLeast: false, + termsOfUse: TermsOfUseMother.create({ + customTerms: CustomTermsMother.create({ termsOfUse: 'Custom terms for bulk download' }) + }) + }) + const files = FilePreviewMother.createMany(2, { + metadata: FileMetadataMother.createNonTabular() + }) + const fileSelection = { + 'some-file-id': files[0] + } + + cy.mountAuthenticated( + withDataset( + , + datasetWithCustomTerms + ) + ) + + cy.get('#download-files').click() + cy.findByRole('dialog').should('exist') + cy.findByText('Custom terms for bulk download').should('exist') + // Guestbook fields should not be visible since there is no guestbook + cy.findByLabelText(/^Name/).should('not.exist') + cy.findByLabelText(/^Email/).should('not.exist') + cy.findByRole('button', { name: 'Accept' }).should('exist').and('not.be.disabled') + }) + + it('opens the terms modal when custom terms exist without a guestbook for tabular files', () => { + const datasetWithCustomTerms = DatasetMother.create({ + permissions: DatasetPermissionsMother.createWithFilesDownloadAllowedButNotUpdatePermissions(), + hasOneTabularFileAtLeast: true, + termsOfUse: TermsOfUseMother.create({ + customTerms: CustomTermsMother.create({ termsOfUse: 'Custom terms for bulk download' }) + }) + }) + const files = FilePreviewMother.createMany(2, { + metadata: FileMetadataMother.createTabular() + }) + const fileSelection = { + 'some-file-id': files[0] + } + + cy.mountAuthenticated( + withDataset( + , + datasetWithCustomTerms + ) + ) + + cy.get('#download-files').click() + cy.findByRole('button', { name: 'Original Format' }).click() + cy.findByRole('dialog').should('exist') + cy.findByText('Custom terms for bulk download').should('exist') + cy.findByRole('button', { name: 'Accept' }).should('exist').and('not.be.disabled') + }) + + it('bypasses the terms modal for editors even when custom terms exist', () => { + const datasetWithCustomTerms = DatasetMother.create({ + permissions: DatasetPermissionsMother.create({ + canDownloadFiles: true, + canUpdateDataset: true + }), + hasOneTabularFileAtLeast: false, + termsOfUse: TermsOfUseMother.create({ + customTerms: CustomTermsMother.create({ termsOfUse: 'Custom terms for bulk download' }) + }) + }) + const files = FilePreviewMother.createMany(2, { + metadata: FileMetadataMother.createNonTabular() + }) + const fileSelection = { + 'some-file-id': files[0] + } + + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + + cy.mountAuthenticated( + withAccessRepository( + withDataset( + , + datasetWithCustomTerms + ) + ) + ) + + cy.get('#download-files').click() + + cy.get('@anchorClick').should('have.been.calledOnce') + cy.findByRole('dialog').should('not.exist') + cy.findByText('Your download has started.').should('exist') + }) + it('does not render the AccessDatasetMenu if the file store does not start with "s3"', () => { const datasetWithDownloadFilesPermission = DatasetMother.create({ permissions: DatasetPermissionsMother.createWithFilesDownloadAllowed(), diff --git a/tests/component/sections/dataset/dataset-files/guestbook/DownloadWithGuestbookModal.spec.tsx b/tests/component/sections/dataset/dataset-files/guestbook/DownloadWithTermsAndGuestbookModal.spec.tsx similarity index 87% rename from tests/component/sections/dataset/dataset-files/guestbook/DownloadWithGuestbookModal.spec.tsx rename to tests/component/sections/dataset/dataset-files/guestbook/DownloadWithTermsAndGuestbookModal.spec.tsx index 8dee82cae..66095d974 100644 --- a/tests/component/sections/dataset/dataset-files/guestbook/DownloadWithGuestbookModal.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/guestbook/DownloadWithTermsAndGuestbookModal.spec.tsx @@ -1,4 +1,4 @@ -import { DownloadWithGuestbookModal } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithGuestbookModal' +import { DownloadWithTermsAndGuestbookModal } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal' import { DatasetContext } from '@/sections/dataset/DatasetContext' import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { DatasetLicense } from '@/dataset/domain/models/Dataset' @@ -90,7 +90,7 @@ const datasetLicense: DatasetLicense = { uri: 'https://creativecommons.org/publicdomain/zero/1.0/' } -describe('DownloadWithGuestbookModal', () => { +describe('DownloadWithTermsAndGuestbookModal', () => { let getGuestbookImpl: (guestbookId: number) => Promise let submitGuestbookForDatafileDownloadImpl: ( fileId: number | string, @@ -176,7 +176,7 @@ describe('DownloadWithGuestbookModal', () => { it('renders modal title and actions', () => { cy.customMount( withAnonymousSession( - { it('renders dataset terms and license when they are provided', () => { cy.customMount( withAnonymousSession( - { it('renders custom dataset terms when custom terms are available', () => { cy.customMount( withAnonymousSession( - { cy.findByRole('link', { name: 'Custom Dataset Terms' }).should( 'have.attr', 'href', - '/spa/datasets?persistentId=doi%3A10.5072%2FFK2%2FFILEPAGE&tab=terms&termsTab=guestbook' + '/modern/datasets?persistentId=doi%3A10.5072%2FFK2%2FFILEPAGE&tab=terms&termsTab=guestbook' ) }) - it('keeps accept disabled when no guestbook is loaded', () => { + it('enables accept when no guestbook but custom terms exist (custom terms only)', () => { cy.customMount( withAnonymousSession( - ) ) - cy.findByRole('button', { name: 'Accept' }).should('be.disabled') + cy.findByRole('button', { name: 'Accept' }).should('not.be.disabled') + cy.findByText('Custom terms for this dataset').should('exist') + // Guestbook fields should not be shown + cy.findByLabelText(/^Name/).should('not.exist') + cy.findByLabelText(/^Email/).should('not.exist') + }) + + it('downloads file when accepting custom terms only (no guestbook)', () => { + const handleClose = cy.stub().as('handleClose') + + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + + cy.customMount( + withAnonymousSession( + + ) + ) + + cy.findByRole('button', { name: 'Accept' }).click() + + cy.get('@submitGuestbookForDatafileDownload').should('have.been.calledOnce') + cy.get('@anchorClick').should('have.been.calledOnce') + cy.get('@handleClose').should('have.been.calledOnce') + cy.findByText('Your download has started.').should('exist') }) it('calls handleClose when clicking cancel', () => { @@ -257,7 +293,7 @@ describe('DownloadWithGuestbookModal', () => { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - + ) ) @@ -306,7 +347,12 @@ describe('DownloadWithGuestbookModal', () => { cy.customMount( withAnonymousSession( - + ) ) @@ -334,7 +380,12 @@ describe('DownloadWithGuestbookModal', () => { cy.customMount( withAnonymousSession( - + ) ) @@ -359,7 +410,7 @@ describe('DownloadWithGuestbookModal', () => { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - { it('disables name and email fields for authenticated users', () => { cy.mountAuthenticated( withRepositories( - { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - { it('shows required field validation after clicking accept', () => { cy.customMount( withAnonymousSession( - { it('does not show required field validation before clicking accept', () => { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - + ) ) @@ -618,7 +674,12 @@ describe('DownloadWithGuestbookModal', () => { cy.customMount( withAnonymousSession( - + ) ) @@ -666,7 +727,12 @@ describe('DownloadWithGuestbookModal', () => { cy.customMount( withAnonymousSession( - + ) ) @@ -686,7 +752,7 @@ describe('DownloadWithGuestbookModal', () => { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - { cy.customMount( withAnonymousSession( - { cy.findByRole('dialog').should('not.exist') cy.findByText('Your download has started.').should('exist') }) + + it('opens the terms modal when custom terms exist without a guestbook', () => { + const guestbookRepository: GuestbookRepository = { + getGuestbook: cy.stub().as('getGuestbook').resolves(guestbook), + getGuestbooksByCollectionId: cy.stub().resolves([]), + assignDatasetGuestbook: cy.stub().resolves(), + removeDatasetGuestbook: cy.stub().resolves() + } + const accessRepository: AccessRepository = { + submitGuestbookForDatasetDownload: cy.stub().resolves('signed-url-dataset'), + submitGuestbookForDatafileDownload: cy + .stub() + .as('submitGuestbookForDatafileDownload') + .resolves('signed-url-file'), + submitGuestbookForDatafilesDownload: cy.stub().resolves('signed-url-datafiles') + } + + cy.customMount( + + + + + + + + + ) + + cy.findByRole('button', { name: 'Access File' }).click() + cy.findByText('Download Options').should('exist') + cy.findByRole('button', { name: 'Plain Text' }).click() + + cy.get('@getGuestbook').should('not.have.been.called') + cy.findByRole('dialog').should('exist') + cy.findByText('Custom Dataset Terms').should('exist') + // Guestbook fields should not be visible since there is no guestbook + cy.findByLabelText(/^Name/).should('not.exist') + cy.findByLabelText(/^Email/).should('not.exist') + cy.findByRole('button', { name: 'Accept' }).should('exist').and('not.be.disabled') + }) + + it('bypasses the custom terms modal for files in draft datasets', () => { + const accessRepository: AccessRepository = { + submitGuestbookForDatasetDownload: cy.stub().resolves('signed-url-dataset'), + submitGuestbookForDatafileDownload: cy + .stub() + .as('submitGuestbookForDatafileDownload') + .resolves('signed-url-file'), + submitGuestbookForDatafilesDownload: cy.stub().resolves('signed-url-datafiles') + } + + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + + cy.customMount( + + + + + + + ) + + cy.findByRole('button', { name: 'Access File' }).click() + cy.findByRole('button', { name: 'Plain Text' }).click() + + cy.get('@submitGuestbookForDatafileDownload').should('have.been.calledOnce') + cy.get('@anchorClick').should('have.been.calledOnce') + cy.findByRole('dialog').should('not.exist') + cy.findByText('Your download has started.').should('exist') + }) + + it('bypasses the custom terms modal for users who can edit the dataset', () => { + const accessRepository: AccessRepository = { + submitGuestbookForDatasetDownload: cy.stub().resolves('signed-url-dataset'), + submitGuestbookForDatafileDownload: cy + .stub() + .as('submitGuestbookForDatafileDownload') + .resolves('signed-url-file'), + submitGuestbookForDatafilesDownload: cy.stub().resolves('signed-url-datafiles') + } + + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + + cy.customMount( + + + + + + + ) + + cy.findByRole('button', { name: 'Access File' }).click() + cy.findByRole('button', { name: 'Plain Text' }).click() + + cy.get('@submitGuestbookForDatafileDownload').should('have.been.calledOnce') + cy.get('@anchorClick').should('have.been.calledOnce') + cy.findByRole('dialog').should('not.exist') + cy.findByText('Your download has started.').should('exist') + }) }) diff --git a/tests/component/sections/file/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.spec.tsx b/tests/component/sections/file/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.spec.tsx index 9c10b9200..f9a19cf46 100644 --- a/tests/component/sections/file/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.spec.tsx +++ b/tests/component/sections/file/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.spec.tsx @@ -35,7 +35,7 @@ describe('FileNonTabularDownloadOptions', () => { cy.customMount( { cy.customMount( { cy.customMount( { cy.customMount( { cy.customMount( { withAccessRepository( { fileId={defaultProps.fileId} type={tabularType} downloadUrls={downloadUrls} - hasGuestbook={false} + requiresTermsOrGuestbook={false} onOpenGuestbookModal={cy.stub()} ingestInProgress={false} isLockedFromFileDownload={defaultProps.isLockedFromFileDownload} @@ -40,7 +40,7 @@ describe('FileTabularDownloadOptions', () => { fileId={defaultProps.fileId} type={unknownType} downloadUrls={downloadUrls} - hasGuestbook={false} + requiresTermsOrGuestbook={false} onOpenGuestbookModal={cy.stub()} ingestInProgress={false} isLockedFromFileDownload={defaultProps.isLockedFromFileDownload} @@ -60,7 +60,7 @@ describe('FileTabularDownloadOptions', () => { fileId={defaultProps.fileId} type={tabularType} downloadUrls={downloadUrls} - hasGuestbook={false} + requiresTermsOrGuestbook={false} onOpenGuestbookModal={cy.stub()} ingestInProgress isLockedFromFileDownload={defaultProps.isLockedFromFileDownload} @@ -82,7 +82,7 @@ describe('FileTabularDownloadOptions', () => { fileId={defaultProps.fileId} type={tabularType} downloadUrls={downloadUrls} - hasGuestbook={false} + requiresTermsOrGuestbook={false} onOpenGuestbookModal={cy.stub()} ingestInProgress={false} isLockedFromFileDownload @@ -105,7 +105,7 @@ describe('FileTabularDownloadOptions', () => { fileId={defaultProps.fileId} type={rDataType} downloadUrls={downloadUrls} - hasGuestbook={false} + requiresTermsOrGuestbook={false} onOpenGuestbookModal={cy.stub()} ingestInProgress={false} isLockedFromFileDownload={defaultProps.isLockedFromFileDownload} @@ -129,7 +129,7 @@ describe('FileTabularDownloadOptions', () => { fileId={defaultProps.fileId} type={tabularType} downloadUrls={downloadUrls} - hasGuestbook + requiresTermsOrGuestbook onOpenGuestbookModal={onOpenGuestbookModal} ingestInProgress={false} isLockedFromFileDownload={defaultProps.isLockedFromFileDownload} @@ -149,7 +149,7 @@ describe('FileTabularDownloadOptions', () => { fileId={defaultProps.fileId} type={tabularType} downloadUrls={downloadUrls} - hasGuestbook + requiresTermsOrGuestbook onOpenGuestbookModal={onOpenGuestbookModal} ingestInProgress={false} isLockedFromFileDownload={defaultProps.isLockedFromFileDownload} diff --git a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx index 073b9cb28..1bba11036 100644 --- a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx @@ -868,12 +868,69 @@ describe('Dataset', () => { .click({ force: true }) cy.findByRole('dialog').should('be.visible') + cy.findByRole('dialog').find('.modal-title').should('contain.text', 'Dataset Terms') cy.findByLabelText(/name/i).should('be.enabled') cy.findByLabelText(/email/i).should('be.enabled') }) }) }) + it('opens the custom terms modal for guests when downloading a dataset with custom terms and no guestbook', () => { + cy.wrap( + DatasetHelper.createWithFiles(FileHelper.createMany(2)).then(async (dataset) => { + await DatasetHelper.setCustomTermsOfUse(dataset.id, { + termsOfUse: 'These are custom terms of use for testing' + }) + await DatasetHelper.publish(dataset.persistentId) + return dataset + }) + ).then((dataset) => { + TestsUtils.logout() + cy.visit(`${FRONTEND_BASE_PATH}/datasets?persistentId=${dataset.persistentId}`) + cy.wait(1500) + + cy.findByText('Files').should('exist') + cy.findByRole('button', { name: 'Access Dataset' }).should('exist').click({ force: true }) + cy.findByRole('button', { name: /Original Format ZIP/ }) + .should('exist') + .click({ force: true }) + + cy.findByRole('dialog').should('be.visible') + cy.findByRole('dialog').find('.modal-title').should('contain.text', 'Dataset Terms') + // Guestbook fields should not be visible since there is no guestbook + cy.findByLabelText(/name/i).should('not.exist') + cy.findByLabelText(/email/i).should('not.exist') + }) + }) + + it('downloads the dataset directly for editors even when custom terms exist without a guestbook', () => { + cy.wrap( + DatasetHelper.createWithFiles(FileHelper.createMany(2)).then(async (dataset) => { + await DatasetHelper.setCustomTermsOfUse(dataset.id, { + termsOfUse: 'These are custom terms of use for testing' + }) + await DatasetHelper.publish(dataset.persistentId) + return dataset + }) + ).then((dataset) => { + cy.visit(`${FRONTEND_BASE_PATH}/datasets?persistentId=${dataset.persistentId}`) + cy.wait(1500) + + cy.findByText('Files').should('exist') + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + cy.findByRole('button', { name: 'Access Dataset' }).should('exist').click({ force: true }) + cy.findByRole('button', { name: /Original Format ZIP/ }) + .should('exist') + .click({ force: true }) + + cy.get('@anchorClick').should('have.been.calledOnce') + cy.findByRole('dialog').should('not.exist') + cy.findByText('Your download has started.').should('exist') + }) + }) + it('downloads a file', () => { cy.wrap( DatasetHelper.createWithFiles(FileHelper.createMany(1)).then((dataset) => diff --git a/tests/e2e-integration/e2e/sections/file/File.spec.tsx b/tests/e2e-integration/e2e/sections/file/File.spec.tsx index bee0c1594..38276a6f6 100644 --- a/tests/e2e-integration/e2e/sections/file/File.spec.tsx +++ b/tests/e2e-integration/e2e/sections/file/File.spec.tsx @@ -198,11 +198,84 @@ describe('File', () => { cy.get('@accessButton').click() cy.findByTestId('download-original-file').should('exist').click({ force: true }) - cy.findByRole('dialog').should('be.visible') + cy.findByRole('dialog').find('.modal-title').should('contain.text', 'Dataset Terms') cy.findByLabelText(/name/i).should('be.enabled') cy.findByLabelText(/email/i).should('be.enabled') }) }) }) + + it('opens the custom terms modal for guests on the file page when custom terms exist without a guestbook', () => { + cy.wrap(DatasetHelper.createWithFile(FileHelper.create())).then((dataset) => { + if (!dataset.file) { + throw new Error('Expected created dataset to include a file') + } + const file = dataset.file + + return cy + .wrap( + DatasetHelper.setCustomTermsOfUse(dataset.id, { + termsOfUse: 'File page custom terms for testing' + }).then(async () => { + await DatasetHelper.publish(dataset.persistentId) + return file + }) + ) + .then((file) => { + TestsUtils.logout() + cy.visit(`${FRONTEND_BASE_PATH}/files?id=${file.id}`) + cy.wait(1500) + + cy.findByRole('button', { name: 'Access File' }).as('accessButton') + cy.get('@accessButton').should('be.visible') + cy.wait(500) + cy.get('@accessButton').click() + cy.findByTestId('download-original-file').should('exist').click({ force: true }) + + cy.findByRole('dialog').should('be.visible') + cy.findByText('Custom Dataset Terms').should('exist') + // Guestbook fields should not be visible since there is no guestbook + cy.findByLabelText(/name/i).should('not.exist') + cy.findByLabelText(/email/i).should('not.exist') + }) + }) + }) + + it('downloads the file directly for editors on the file page when custom terms exist without a guestbook', () => { + cy.wrap(DatasetHelper.createWithFile(FileHelper.create())).then((dataset) => { + if (!dataset.file) { + throw new Error('Expected created dataset to include a file') + } + const file = dataset.file + + return cy + .wrap( + DatasetHelper.setCustomTermsOfUse(dataset.id, { + termsOfUse: 'File page custom terms for testing' + }).then(async () => { + await DatasetHelper.publish(dataset.persistentId) + return file + }) + ) + .then((file) => { + cy.visit(`${FRONTEND_BASE_PATH}/files?id=${file.id}`) + cy.wait(1500) + + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + + cy.findByRole('button', { name: 'Access File' }).as('accessButton') + cy.get('@accessButton').should('be.visible') + cy.wait(500) + cy.get('@accessButton').click() + cy.findByTestId('download-original-file').should('exist').click({ force: true }) + + cy.get('@anchorClick').should('have.been.calledOnce') + cy.findByRole('dialog').should('not.exist') + cy.findByText('Your download has started.').should('exist') + }) + }) + }) }) }) diff --git a/tests/e2e-integration/shared/datasets/DatasetHelper.ts b/tests/e2e-integration/shared/datasets/DatasetHelper.ts index c6b8d3878..57e3647d4 100644 --- a/tests/e2e-integration/shared/datasets/DatasetHelper.ts +++ b/tests/e2e-integration/shared/datasets/DatasetHelper.ts @@ -262,6 +262,22 @@ export class DatasetHelper extends DataverseApiHelper { }>(`/datasets/${id}/lock/${reason}`, 'POST') } + static async setCustomTermsOfUse( + datasetId: string | number, + customTerms: { + termsOfUse: string + confidentialityDeclaration?: string + specialPermissions?: string + restrictions?: string + citationRequirements?: string + depositorRequirements?: string + conditions?: string + disclaimer?: string + } + ): Promise { + await this.request(`/datasets/${datasetId}/license`, 'PUT', { customTerms }) + } + static async createTemplate(collectionAlias?: string): Promise<{ id: number }> { if (collectionAlias == undefined) { collectionAlias = ':root'