diff --git a/README.md b/README.md index fcfdd4684..23eb2b473 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,10 @@ The environment is accessible through the following URLs: > #### Account Page BreadCrumbs > > The Account Page has been updated to remove breadcrumbs, as the page is not part of the main navigation. +> +> #### Share Collection and Dataset feature +> +> Links to share a collection or a dataset via LinkedIn, X or Facebook will now open in a new tab instead of a popup. diff --git a/package-lock.json b/package-lock.json index e04344ef5..b0681d081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "lodash": "^4.17.21", "moment-timezone": "0.5.43", "react-bootstrap": "2.7.2", - "react-bootstrap-icons": "1.10.3", + "react-bootstrap-icons": "1.11.4", "react-hook-form": "7.51.2", "react-i18next": "12.1.5", "react-infinite-scroll-hook": "4.1.1", @@ -39,7 +39,7 @@ "react-router-dom": "6.23.1", "react-topbar-progress-indicator": "4.1.1", "sass": "1.58.1", - "typescript": "4.9.5", + "typescript": "5.7.2", "use-deep-compare": "1.2.1", "vite-plugin-istanbul": "4.0.1", "web-vitals": "2.1.4" @@ -3701,6 +3701,18 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/@iqss/dataverse-client-javascript/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@iqss/dataverse-design-system": { "resolved": "packages/design-system", "link": true @@ -36324,9 +36336,9 @@ } }, "node_modules/react-bootstrap-icons": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.10.3.tgz", - "integrity": "sha512-j4hSby6gT9/enhl3ybB1tfr1slZNAYXDVntcRrmVjxB3//2WwqrzpESVqKhyayYVaWpEtnwf9wgUQ03cuziwrw==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.4.tgz", + "integrity": "sha512-lnkOpNEZ/Zr7mNxvjA9efuarCPSgtOuGA55XiRj7ASJnBjb1wEAdtJOd2Aiv9t07r7FLI1IgyZPg9P6jqWD/IA==", "dependencies": { "prop-types": "^15.7.2" }, @@ -42060,15 +42072,15 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/typescript-memoize": { @@ -44412,7 +44424,7 @@ "@types/react": "18.0.27", "bootstrap": "5.2.3", "react-bootstrap": "2.7.2", - "react-bootstrap-icons": "1.10.3", + "react-bootstrap-icons": "1.11.4", "sass": "1.58.1", "typescript": "4.9.5", "vite-plugin-istanbul": "4.0.1" @@ -44529,6 +44541,17 @@ "dev": true, "license": "MIT" }, + "packages/design-system/node_modules/react-bootstrap-icons": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.10.3.tgz", + "integrity": "sha512-j4hSby6gT9/enhl3ybB1tfr1slZNAYXDVntcRrmVjxB3//2WwqrzpESVqKhyayYVaWpEtnwf9wgUQ03cuziwrw==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, "packages/design-system/node_modules/supports-color": { "version": "8.1.1", "dev": true, @@ -44542,6 +44565,18 @@ "funding": { "url": "https://github.com/chalk/supports-color?sponsor=1" } + }, + "packages/design-system/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } } } } diff --git a/package.json b/package.json index d94960a77..bf283f587 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "lodash": "^4.17.21", "moment-timezone": "0.5.43", "react-bootstrap": "2.7.2", - "react-bootstrap-icons": "1.10.3", + "react-bootstrap-icons": "1.11.4", "react-hook-form": "7.51.2", "react-i18next": "12.1.5", "react-infinite-scroll-hook": "4.1.1", @@ -43,7 +43,7 @@ "react-router-dom": "6.23.1", "react-topbar-progress-indicator": "4.1.1", "sass": "1.58.1", - "typescript": "4.9.5", + "typescript": "5.7.2", "use-deep-compare": "1.2.1", "vite-plugin-istanbul": "4.0.1", "web-vitals": "2.1.4" @@ -52,7 +52,7 @@ "start": "vite --base=/spa", "build": "tsc && vite build", "preview": "vite preview", - "lint": "npm run lint:eslint && npm run lint:stylelint && npm run lint:prettier", + "lint": "npm run typecheck && npm run lint:eslint && npm run lint:stylelint && npm run lint:prettier", "lint:fix": "eslint --fix --ext .ts,.tsx ./src --ignore-path .gitignore . && stylelint --fix **/*.scss && prettier . --write", "lint:eslint": "eslint --ignore-path .gitignore .", "lint:stylelint": "stylelint **/*.scss ", @@ -77,9 +77,7 @@ "pre-commit": [ "typecheck", "lint:fix", - "git:add", - "test:unit", - "test:coverage" + "git:add" ], "eslintConfig": { "extends": [ diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 1eb3708cd..88966e1d2 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -50,7 +50,7 @@ "@types/react": "18.0.27", "bootstrap": "5.2.3", "react-bootstrap": "2.7.2", - "react-bootstrap-icons": "1.10.3", + "react-bootstrap-icons": "1.11.4", "sass": "1.58.1", "typescript": "4.9.5", "vite-plugin-istanbul": "4.0.1" diff --git a/packages/design-system/src/lib/stories/button-group/ButtonGroup.stories.tsx b/packages/design-system/src/lib/stories/button-group/ButtonGroup.stories.tsx index ddf2f812c..2fbd7422a 100644 --- a/packages/design-system/src/lib/stories/button-group/ButtonGroup.stories.tsx +++ b/packages/design-system/src/lib/stories/button-group/ButtonGroup.stories.tsx @@ -55,17 +55,12 @@ export const NestedButtonGroups: Story = { render: () => ( - + Item 1 Item 2 Item 3 - + Item 1 Item 2 Item 3 diff --git a/public/locales/en/collection.json b/public/locales/en/collection.json index 8cdf0de6d..612049c77 100644 --- a/public/locales/en/collection.json +++ b/public/locales/en/collection.json @@ -33,6 +33,10 @@ "publishedAlert": "Your collection is now public.", "addFacetFilter": "Add {{labelName}} facet filter", "removeSelectedFacet": "Remove {{labelName}} facet filter", + "share": { + "shareCollection": "Share Collection", + "helpText": "Share this collection on your favorite social media networks." + }, "editedAlert": "You have successfully updated your collection!", "editCollection": { "edit": "Edit", diff --git a/public/locales/en/dataset.json b/public/locales/en/dataset.json index 0d5c73dcd..c7907b483 100644 --- a/public/locales/en/dataset.json +++ b/public/locales/en/dataset.json @@ -59,7 +59,12 @@ "archivalZip": "Archival Format (.tab) ZIP" } }, - "uploadFiles": "Upload Files" + "uploadFiles": "Upload Files", + "share": { + "shareDataset": "Share Dataset", + "helpText": "Share this dataset on your favorite social media networks." + }, + "contactOwner": "Contact Owner" }, "alerts": { "publishInProgress": { diff --git a/public/locales/en/shared.json b/public/locales/en/shared.json index a9545df3e..67c02042e 100644 --- a/public/locales/en/shared.json +++ b/public/locales/en/shared.json @@ -3,9 +3,11 @@ "remove": "Remove", "add": "Add", "cancel": "Cancel", + "close": "Close", "continue": "Continue", "more": "More...", "less": "Less...", + "share": "Share", "pageNumberNotFound": { "heading": "Page Number Not Found", "message": "The page number you requested does not exist. Please try a different page number." diff --git a/src/sections/collection/Collection.module.scss b/src/sections/collection/Collection.module.scss index f3d91b901..adf569dde 100644 --- a/src/sections/collection/Collection.module.scss +++ b/src/sections/collection/Collection.module.scss @@ -11,10 +11,19 @@ gap: 10px; } -.action-buttons { +.metrics-actions-container { display: flex; - justify-content: flex-end; - margin-bottom: 1rem; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + margin-bottom: 2rem; + + .right-content { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + } } .subtext { diff --git a/src/sections/collection/Collection.tsx b/src/sections/collection/Collection.tsx index 4d566f19b..a5019f49f 100644 --- a/src/sections/collection/Collection.tsx +++ b/src/sections/collection/Collection.tsx @@ -1,6 +1,5 @@ import { useTranslation } from 'react-i18next' import { Alert, ButtonGroup, Col, Row } from '@iqss/dataverse-design-system' - import { CollectionRepository } from '../../collection/domain/repositories/CollectionRepository' import { useCollection } from './useCollection' import { useScrollTop } from '../../shared/hooks/useScrollTop' @@ -14,6 +13,7 @@ import { CollectionSkeleton } from './CollectionSkeleton' import { PageNotFound } from '../page-not-found/PageNotFound' import { CreatedAlert } from './CreatedAlert' import { PublishCollectionButton } from './publish-collection/PublishCollectionButton' +import { ShareCollectionButton } from './share-collection-button/ShareCollectionButton' import { EditCollectionDropdown } from './edit-collection-dropdown/EditCollectionDropdown' import styles from './Collection.module.scss' @@ -81,19 +81,28 @@ export function Collection({ {t('publishedAlert')} )} - {(showPublishButton || showEditButton) && ( -
- - {showPublishButton && ( - - )} - {showEditButton && } - + +
+
+
+ {/* 👇 Here should go Contact button also */} + {/* */} + + + + {(showPublishButton || showEditButton) && ( + + {showPublishButton && ( + + )} + {showEditButton && } + + )}
- )} +
{ + const { t } = useTranslation('collection') + const { t: tShared } = useTranslation('shared') + + const [showShareModal, setShowShareModal] = useState(false) + + const openShareModal = () => setShowShareModal(true) + const closeShareModal = () => setShowShareModal(false) + + return ( + <> + + + + + + + ) +} diff --git a/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.module.scss b/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.module.scss index fdb73c0cd..5dfd7feb5 100644 --- a/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.module.scss +++ b/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.module.scss @@ -3,3 +3,9 @@ width: 100%; margin: 0.5rem 0; } + +.contact-owner-and-share-group { + > button { + width: 50%; + } +} diff --git a/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx b/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx index ce5c3f4cd..52060d159 100644 --- a/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx +++ b/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx @@ -1,14 +1,15 @@ -import { Dataset } from '../../../dataset/domain/models/Dataset' -import { ButtonGroup } from '@iqss/dataverse-design-system' +import { useTranslation } from 'react-i18next' +import { Button, ButtonGroup } from '@iqss/dataverse-design-system' +import { Dataset } from '@/dataset/domain/models/Dataset' +import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { AccessDatasetMenu } from './access-dataset-menu/AccessDatasetMenu' import { PublishDatasetMenu } from './publish-dataset-menu/PublishDatasetMenu' -import styles from './DatasetActionButtons.module.scss' import { SubmitForReviewButton } from './submit-for-review-button/SubmitForReviewButton' import { EditDatasetMenu } from './edit-dataset-menu/EditDatasetMenu' import { LinkDatasetButton } from './link-dataset-button/LinkDatasetButton' -import { useTranslation } from 'react-i18next' -import { DatasetRepository } from '../../../dataset/domain/repositories/DatasetRepository' -import { CollectionRepository } from '../../../collection/domain/repositories/CollectionRepository' +import { ShareDatasetButton } from './share-dataset-button/ShareDatasetButton' +import styles from './DatasetActionButtons.module.scss' interface DatasetActionButtonsProps { dataset: Dataset @@ -22,6 +23,7 @@ export function DatasetActionButtons({ collectionRepository }: DatasetActionButtonsProps) { const { t } = useTranslation('dataset') + return ( + + + + ) } diff --git a/src/sections/dataset/dataset-action-buttons/share-dataset-button/ShareDatasetButton.tsx b/src/sections/dataset/dataset-action-buttons/share-dataset-button/ShareDatasetButton.tsx new file mode 100644 index 000000000..46058265c --- /dev/null +++ b/src/sections/dataset/dataset-action-buttons/share-dataset-button/ShareDatasetButton.tsx @@ -0,0 +1,29 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Button } from '@iqss/dataverse-design-system' +import { SocialShareModal } from '@/sections/shared/social-share-modal/SocialShareModal' + +export const ShareDatasetButton = () => { + const { t } = useTranslation('dataset') + const { t: tShared } = useTranslation('shared') + const [showShareModal, setShowShareModal] = useState(false) + + const openShareModal = () => setShowShareModal(true) + const closeShareModal = () => setShowShareModal(false) + + return ( + <> + + + + + ) +} diff --git a/src/sections/shared/social-share-modal/SocialShareModal.module.scss b/src/sections/shared/social-share-modal/SocialShareModal.module.scss new file mode 100644 index 000000000..2f3ebf793 --- /dev/null +++ b/src/sections/shared/social-share-modal/SocialShareModal.module.scss @@ -0,0 +1,50 @@ +@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module'; + +.help-block { + color: $dv-subtext-color; + font-size: 14px; +} + +.social-btn { + display: grid; + place-items: center; + padding: 8px 12px; + background-color: transparent; + border: 0; + border-radius: 6px; + transition: all 0.15s ease-in-out; + + &:hover { + scale: 1.1; + box-shadow: 2px 2px 10px rgb(0 0 0 / 30%); + } + + &:active { + scale: 1; + box-shadow: none; + } + + &.fb { + background-color: #3a5795; + + svg { + color: #fff; + } + } + + &.x { + background-color: #000; + + svg { + color: #fff; + } + } + + &.linkedin { + background-color: #6f5499; + + svg { + color: #fff; + } + } +} diff --git a/src/sections/shared/social-share-modal/SocialShareModal.tsx b/src/sections/shared/social-share-modal/SocialShareModal.tsx new file mode 100644 index 000000000..6915a5c39 --- /dev/null +++ b/src/sections/shared/social-share-modal/SocialShareModal.tsx @@ -0,0 +1,78 @@ +import { useTranslation } from 'react-i18next' +import { Button, Modal, Stack } from '@iqss/dataverse-design-system' +import { Facebook, Linkedin, TwitterX } from 'react-bootstrap-icons' +import styles from './SocialShareModal.module.scss' + +export const LINKEDIN_SHARE_URL = 'https://www.linkedin.com/shareArticle?url=' +export const X_SHARE_URL = 'https://x.com/intent/post?url=' +export const FACEBOOK_SHARE_URL = 'https://www.facebook.com/sharer/sharer.php?u=' + +interface SocialShareModalProps { + show: boolean + title: string + helpText: string + shareUrl: string + handleClose: () => void +} + +export const SocialShareModal = ({ + show, + title, + helpText, + shareUrl, + handleClose +}: SocialShareModalProps) => { + const { t } = useTranslation('shared') + + const shareOnLinkedInURL = `${LINKEDIN_SHARE_URL}${encodeURIComponent(shareUrl)}` + + const shareOnXURL = `${X_SHARE_URL}${encodeURIComponent(shareUrl)}` + + const shareOnFacebookURL = `${FACEBOOK_SHARE_URL}${encodeURIComponent(shareUrl)}` + + return ( + + + {title} + + +

{helpText}

+ + + + + + + + + + + + +
+ + + +
+ ) +} diff --git a/src/stories/shared/social-share-modal/SocialShareModal.stories.tsx b/src/stories/shared/social-share-modal/SocialShareModal.stories.tsx new file mode 100644 index 000000000..f378028be --- /dev/null +++ b/src/stories/shared/social-share-modal/SocialShareModal.stories.tsx @@ -0,0 +1,48 @@ +import { Meta, StoryObj } from '@storybook/react' +import { WithI18next } from '../../WithI18next' +import { SocialShareModal } from '@/sections/shared/social-share-modal/SocialShareModal' + +const meta: Meta = { + title: 'Sections/Shared/SocialShareModal', + component: SocialShareModal, + decorators: [WithI18next] +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( + {}} + /> + ) +} + +export const ShareCollection: Story = { + render: () => ( + {}} + /> + ) +} + +export const ShareDataset: Story = { + render: () => ( + {}} + /> + ) +} diff --git a/tests/component/sections/collection/Collection.spec.tsx b/tests/component/sections/collection/Collection.spec.tsx index a9c8088cf..2910e13fe 100644 --- a/tests/component/sections/collection/Collection.spec.tsx +++ b/tests/component/sections/collection/Collection.spec.tsx @@ -198,4 +198,27 @@ describe('Collection page', () => { cy.findByRole('button', { name: /Cancel/i }).click() cy.findByText('Publish Collection').should('not.exist') }) + + it('opens and close the share collection modal', () => { + cy.viewport(1200, 800) + + cy.mountAuthenticated( + + ) + cy.findByRole('button', { name: /Share/i }).should('exist') + + cy.findByRole('button', { name: /Share/i }).click() + cy.findByText('Share this collection on your favorite social media networks.').should('exist') + + cy.findAllByRole('button', { name: /Close/i }).last().click() + cy.findByText('Share this collection on your favorite social media networks.').should( + 'not.exist' + ) + }) }) diff --git a/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx index 00f2dd5c1..2fbf6ea47 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx @@ -1,3 +1,4 @@ +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { DatasetRepository } from '../../../../../src/dataset/domain/repositories/DatasetRepository' import { FileSizeUnit } from '../../../../../src/files/domain/models/FileMetadata' import { DatasetActionButtons } from '../../../../../src/sections/dataset/dataset-action-buttons/DatasetActionButtons' @@ -10,6 +11,8 @@ import { const datasetRepository: DatasetRepository = {} as DatasetRepository +const collectionRepository: CollectionRepository = {} as CollectionRepository + describe('DatasetActionButtons', () => { it('renders the DatasetActionButtons with the Publish button', () => { const dataset = DatasetMother.create({ @@ -21,7 +24,11 @@ describe('DatasetActionButtons', () => { }) cy.mountAuthenticated( - + ) cy.findByRole('group', { name: 'Dataset Action Buttons' }).should('exist') @@ -45,7 +52,11 @@ describe('DatasetActionButtons', () => { }) cy.mountAuthenticated( - + ) cy.findByRole('group', { name: 'Dataset Action Buttons' }).should('exist') diff --git a/tests/component/sections/dataset/dataset-action-buttons/share-dataset-button/ShareDatasetButton.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/share-dataset-button/ShareDatasetButton.spec.tsx new file mode 100644 index 000000000..d47613c9e --- /dev/null +++ b/tests/component/sections/dataset/dataset-action-buttons/share-dataset-button/ShareDatasetButton.spec.tsx @@ -0,0 +1,16 @@ +import { ShareDatasetButton } from '@/sections/dataset/dataset-action-buttons/share-dataset-button/ShareDatasetButton' + +describe('ShareDatasetButton', () => { + it('opens and close the share dataset modal', () => { + cy.viewport(1200, 800) + + cy.mountAuthenticated() + cy.findByRole('button', { name: /Share/i }).should('exist') + + cy.findByRole('button', { name: /Share/i }).click() + cy.findByText('Share this dataset on your favorite social media networks.').should('exist') + + cy.findAllByRole('button', { name: /Close/i }).last().click() + cy.findByText('Share this dataset on your favorite social media networks.').should('not.exist') + }) +}) diff --git a/tests/component/sections/shared/social-share-modal/SocialShareModal.spec.tsx b/tests/component/sections/shared/social-share-modal/SocialShareModal.spec.tsx new file mode 100644 index 000000000..228355890 --- /dev/null +++ b/tests/component/sections/shared/social-share-modal/SocialShareModal.spec.tsx @@ -0,0 +1,37 @@ +import { + FACEBOOK_SHARE_URL, + LINKEDIN_SHARE_URL, + SocialShareModal, + X_SHARE_URL +} from '@/sections/shared/social-share-modal/SocialShareModal' + +const urlToShare = 'some-share-url' + +describe('SocialShareModal', () => { + it('should render the component title, help text and 3 social links', () => { + cy.mount( + {}} + /> + ) + + cy.findByText('The Title').should('exist') + cy.findByText('The Help Text').should('exist') + + cy.findByLabelText('Share on LinkedIn') + .should('exist') + .should('have.attr', 'href', `${LINKEDIN_SHARE_URL}${urlToShare}`) + + cy.findByLabelText('Share on X, formerly Twitter') + .should('exist') + .should('have.attr', 'href', `${X_SHARE_URL}${urlToShare}`) + + cy.findByLabelText('Share on Facebook') + .should('exist') + .should('have.attr', 'href', `${FACEBOOK_SHARE_URL}${urlToShare}`) + }) +})