diff --git a/cypress/e2e/item/publish/ccLicense.cy.js b/cypress/e2e/item/publish/ccLicense.cy.js new file mode 100644 index 000000000..c8aff0f5a --- /dev/null +++ b/cypress/e2e/item/publish/ccLicense.cy.js @@ -0,0 +1,61 @@ +import { buildItemPath } from "../../../../src/config/paths"; +import { + buildPublishButtonId, + CC_ALLOW_COMMERCIAL_CONTROL_ID, + CC_CC0_CONTROL_ID, + CC_DERIVATIVE_CONTROL_ID, + CC_DISALLOW_COMMERCIAL_CONTROL_ID, + CC_NO_DERIVATIVE_CONTROL_ID, + CC_REQUIRE_ATTRIBUTION_CONTROL_ID, + CC_SHARE_ALIKE_CONTROL_ID, +} from "../../../../src/config/selectors"; +import { PUBLISHED_ITEMS } from "../../../fixtures/items"; +import { DEFAULT_TAGS } from "../../../fixtures/itemTags"; + +const openPublishItemTab = (id) => { + cy.get(`#${buildPublishButtonId(id)}`).click(); +}; + +const visitItemPage = (item) => { + cy.setUpApi({ items: [item], tags: DEFAULT_TAGS }); + cy.visit(buildItemPath(item.id)); + openPublishItemTab(item.id); +}; + +const ensureRadioCheckedState = (parentId, shouldBeChecked) => + cy.get(`#${parentId}`) + // MUI doesn't update the `checked` attribute of checkboxes. + .find('svg[data-testid=RadioButtonCheckedIcon]') + .should('have.css', 'transform', `matrix(${shouldBeChecked ? '1, 0, 0, 1, 0, 0' : '0, 0, 0, 0, 0, 0'})`); + +describe('Creative Commons License', () => { + it('Current license is selected', () => { + for (const publishedItem of PUBLISHED_ITEMS) { + visitItemPage(publishedItem); + + const requireAttribution = publishedItem.settings.ccLicenseAdaption.includes('BY'); + const noncommercial = publishedItem.settings.ccLicenseAdaption.includes('NC'); + const shareAlike = publishedItem.settings.ccLicenseAdaption.includes('SA'); + const noDerivative = publishedItem.settings.ccLicenseAdaption.includes('ND'); + + ensureRadioCheckedState(CC_REQUIRE_ATTRIBUTION_CONTROL_ID, requireAttribution); + ensureRadioCheckedState(CC_CC0_CONTROL_ID, !requireAttribution); + + if (requireAttribution) { + ensureRadioCheckedState(CC_ALLOW_COMMERCIAL_CONTROL_ID, !noncommercial); + ensureRadioCheckedState(CC_DISALLOW_COMMERCIAL_CONTROL_ID, noncommercial); + + ensureRadioCheckedState(CC_NO_DERIVATIVE_CONTROL_ID, noDerivative); + ensureRadioCheckedState(CC_SHARE_ALIKE_CONTROL_ID, shareAlike); + ensureRadioCheckedState(CC_DERIVATIVE_CONTROL_ID, !shareAlike && !noDerivative); + } + else { + cy.get(`#${CC_ALLOW_COMMERCIAL_CONTROL_ID}`).should('not.exist'); + cy.get(`#${CC_DISALLOW_COMMERCIAL_CONTROL_ID}`).should('not.exist'); + cy.get(`#${CC_NO_DERIVATIVE_CONTROL_ID}`).should('not.exist'); + cy.get(`#${CC_SHARE_ALIKE_CONTROL_ID}`).should('not.exist'); + cy.get(`#${CC_DERIVATIVE_CONTROL_ID}`).should('not.exist'); + } + } + }); +}); diff --git a/cypress/fixtures/items.js b/cypress/fixtures/items.js index e462ec0e7..81787def8 100644 --- a/cypress/fixtures/items.js +++ b/cypress/fixtures/items.js @@ -17,6 +17,9 @@ export const DEFAULT_FOLDER_ITEM = { type: ITEM_TYPES.FOLDER, createdAt: new Date('2020-01-01T01:01:01Z'), updatedAt: new Date('2020-01-02T01:01:01Z'), + settings: { + ccLicenseAdaption: 'CC BY', + } }; export const CREATED_ITEM = { @@ -73,6 +76,9 @@ export const SAMPLE_ITEMS = { memberId: MEMBERS.BOB.id, }, ], + settings: { + ccLicenseAdaption: 'CC BY-NC', + } }, { ...DEFAULT_FOLDER_ITEM, @@ -81,6 +87,7 @@ export const SAMPLE_ITEMS = { path: 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130003', extra: { image: 'someimageurl', + ccLicenseAdaption: 'CC BY-NC', }, createdAt: '2022-12-16T16:00:50.968Z', updatedAt: '2022-12-18T16:00:52.655Z', @@ -107,6 +114,9 @@ export const SAMPLE_ITEMS = { memberId: MEMBERS.ANNA.id, }, ], + settings: { + ccLicenseAdaption: 'CC0', + } }, { ...DEFAULT_FOLDER_ITEM, @@ -123,6 +133,9 @@ export const SAMPLE_ITEMS = { memberId: MEMBERS.ANNA.id, }, ], + settings: { + ccLicenseAdaption: 'CC BY-NC-SA', + } }, { ...DEFAULT_FOLDER_ITEM, @@ -141,6 +154,9 @@ export const SAMPLE_ITEMS = { memberId: MEMBERS.ANNA.id, }, ], + settings: { + ccLicenseAdaption: 'CC BY-NC-ND', + } }, ], memberships: [], @@ -597,6 +613,22 @@ export const PUBLISHED_ITEM = { ], }; +export const PUBLISHED_ITEMS = [ + PUBLISHED_ITEM, + { + ...PUBLISHED_ITEM, + settings: { + ccLicenseAdaption: 'CC0', + } + }, + { + ...PUBLISHED_ITEM, + settings: { + ccLicenseAdaption: 'CC BY-NC-ND', + } + } +] + export const HIDDEN_ITEM = { ...DEFAULT_FOLDER_ITEM, id: 'ecafbd2a-5688-11eb-ae93-0242ac130001', diff --git a/package.json b/package.json index 599ea392a..010aa8b97 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "@graasp/chatbox": "1.2.1", "@graasp/query-client": "0.4.1", "@graasp/sdk": "0.12.0", - "@graasp/translations": "1.11.0", - "@graasp/ui": "2.4.3", + "@graasp/translations": "1.12.0", + "@graasp/ui": "2.5.0", "@mui/icons-material": "5.11.16", "@mui/lab": "5.0.0-alpha.117", "@mui/material": "5.12.0", diff --git a/src/components/item/publish/CCLicenseSelection.tsx b/src/components/item/publish/CCLicenseSelection.tsx index f30371eaf..acc79f1d2 100644 --- a/src/components/item/publish/CCLicenseSelection.tsx +++ b/src/components/item/publish/CCLicenseSelection.tsx @@ -1,31 +1,61 @@ -import { FormControlLabel, Radio, RadioGroup, Typography } from '@mui/material'; +import { + Box, + FormControlLabel, + Radio, + RadioGroup, + Typography, +} from '@mui/material'; import { ChangeEvent, FC, useEffect, useState } from 'react'; import { MUTATION_KEYS } from '@graasp/query-client'; import { ItemRecord } from '@graasp/sdk/frontend'; import { BUILDER } from '@graasp/translations'; -import { CCLicenseIcon, Loader } from '@graasp/ui'; +import { CCSharingVariant, CreativeCommons, Loader } from '@graasp/ui'; -import { CC_LICENSE_ADAPTION_OPTIONS } from '../../../config/constants'; import { useBuilderTranslation } from '../../../config/i18n'; import { useMutation } from '../../../config/queryClient'; +import { + CC_ALLOW_COMMERCIAL_CONTROL_ID, + CC_CC0_CONTROL_ID, + CC_DERIVATIVE_CONTROL_ID, + CC_DISALLOW_COMMERCIAL_CONTROL_ID, + CC_NO_DERIVATIVE_CONTROL_ID, + CC_REQUIRE_ATTRIBUTION_CONTROL_ID, + CC_SHARE_ALIKE_CONTROL_ID, +} from '../../../config/selectors'; import { useCurrentUserContext } from '../../context/CurrentUserContext'; import CCLicenseDialog from './CCLicenseDialog'; const { EDIT_ITEM } = MUTATION_KEYS; // TODO: export in graasp sdk -enum CCLicenseAdaption { - ALLOW = 'allow', - ALIKE = 'alike', +enum CCLicenseAdaptions { + CC_BY = 'CC BY', + CC_BY_NC = 'CC BY-NC', + CC_BY_SA = 'CC BY-SA', + CC_BY_NC_SA = 'CC BY-NC-SA', + CC_BY_ND = 'CC BY-ND', + CC_BY_NC_ND = 'CC BY-NC-ND', + CC0 = 'CC0', } +export type CCLicenseAdaption = CCLicenseAdaptions | `${CCLicenseAdaptions}`; + +type CCLicenseChoice = 'yes' | 'no' | ''; +type CCSharingLicenseChoice = CCLicenseChoice | 'alike'; + type Props = { item: ItemRecord; disabled: boolean; }; +const licensePreviewStyle = { + border: '1px solid #eee', + borderRadius: 2, + minWidth: 300, +}; + const CCLicenseSelection: FC = ({ item, disabled }) => { const { t: translateBuilder } = useBuilderTranslation(); const { mutate: updateCCLicense } = useMutation< @@ -37,7 +67,13 @@ const CCLicenseSelection: FC = ({ item, disabled }) => { settings: { ccLicenseAdaption: CCLicenseAdaption }; } >(EDIT_ITEM); - const [optionValue, setOptionValue] = useState(); + const [requireAttributionValue, setRequireAttributionValue] = + useState(''); + const [allowCommercialValue, setAllowCommercialValue] = + useState(''); + const [allowSharingValue, setAllowSharingValue] = + useState(''); + const [open, setOpen] = useState(false); // user @@ -51,60 +87,182 @@ const CCLicenseSelection: FC = ({ item, disabled }) => { useEffect(() => { if (settings?.ccLicenseAdaption) { - setOptionValue(settings.ccLicenseAdaption as CCLicenseAdaption); + // Handles old license formats. + if ( + ['alike', 'allow'].includes( + settings?.ccLicenseAdaption as CCLicenseAdaption, + ) + ) { + setRequireAttributionValue('yes'); + setAllowCommercialValue('no'); + setAllowSharingValue( + settings?.ccLicenseAdaption === 'alike' ? 'alike' : 'yes', + ); + } else if (typeof settings?.ccLicenseAdaption === 'string') { + setRequireAttributionValue( + settings.ccLicenseAdaption !== CCLicenseAdaptions.CC0 ? 'yes' : 'no', + ); + setAllowCommercialValue( + settings.ccLicenseAdaption.includes('NC') ? 'no' : 'yes', + ); + if (settings.ccLicenseAdaption.includes('SA')) { + setAllowSharingValue('alike'); + } else if (settings.ccLicenseAdaption.includes('ND')) { + setAllowSharingValue('no'); + } else { + setAllowSharingValue('yes'); + } + } } }, [settings]); if (isMemberLoading) return ; - const handleChange = (event: ChangeEvent<{ value: string }>) => { - setOptionValue(event.target.value as CCLicenseAdaption); + const convertSelectionToLicense = (): CCLicenseAdaptions => { + if (requireAttributionValue !== 'yes') { + return CCLicenseAdaptions.CC0; + } + + return `CC BY${allowCommercialValue === 'yes' ? '' : '-NC'}${ + allowSharingValue === 'alike' ? '-SA' : '' + }${allowSharingValue === 'no' ? '-ND' : ''}` as CCLicenseAdaptions; + }; + + const handleAttributionChange = (event: ChangeEvent<{ value: string }>) => { + setRequireAttributionValue(event.target.value as CCLicenseChoice); + }; + + const handleCommercialChange = (event: ChangeEvent<{ value: string }>) => { + setAllowCommercialValue(event.target.value as CCLicenseChoice); + }; + + const handleSharingChange = (event: ChangeEvent<{ value: string }>) => { + setAllowSharingValue(event.target.value as CCSharingLicenseChoice); }; const handleSubmit = (event: SubmitEvent) => { event.preventDefault(); - if (optionValue) { + if (requireAttributionValue) { updateCCLicense({ id: itemId, name: itemName, - settings: { ccLicenseAdaption: optionValue }, + settings: { ccLicenseAdaption: convertSelectionToLicense() }, }); } else { - console.error(`optionValue "${optionValue}" is undefined`); + console.error(`optionValue "${requireAttributionValue}" is undefined`); } setOpen(false); }; return ( - <> + - {translateBuilder(BUILDER.ITEM_SETTINGS_CC_LICENSE_INFORMATIONS)} + {translateBuilder(BUILDER.ITEM_SETTINGS_CC_ATTRIBUTION_TITLE)} - - } - disabled={disabled} - label={translateBuilder(BUILDER.ITEM_SETTINGS_CC_LICENSE_ALLOW_LABEL)} - /> - } - disabled={disabled} - label={translateBuilder(BUILDER.ITEM_SETTINGS_CC_LICENSE_ALIKE_LABEL)} - /> - } - disabled={disabled} - label={translateBuilder(BUILDER.ITEM_SETTINGS_CC_LICENSE_NONE_LABEL)} - /> - + + + } + disabled={disabled} + label={translateBuilder( + BUILDER.ITEM_SETTINGS_CC_REQUIRE_ATTRIBUTION_OPTION_LABEL, + )} + /> + } + disabled={disabled} + label={translateBuilder(BUILDER.ITEM_SETTINGS_CC_CC0_OPTION_LABEL)} + /> + + + {requireAttributionValue === 'yes' && ( + <> + + {translateBuilder(BUILDER.ITEM_SETTINGS_CC_COMMERCIAL_TITLE)} + + + + } + disabled={disabled} + label={translateBuilder( + BUILDER.ITEM_SETTINGS_CC_ALLOW_COMMERCIAL_OPTION_LABEL, + )} + /> + } + disabled={disabled} + label={translateBuilder( + BUILDER.ITEM_SETTINGS_CC_DISALLOW_COMMERCIAL_OPTION_LABEL, + )} + /> + + + + {translateBuilder(BUILDER.ITEM_SETTINGS_CC_REMIX_TITLE)} + + + + } + disabled={disabled} + label={translateBuilder( + BUILDER.ITEM_SETTINGS_CC_ALLOW_REMIX_OPTION_LABEL, + )} + /> + } + disabled={disabled} + label={translateBuilder( + BUILDER.ITEM_SETTINGS_CC_ALLOW_SHARE_ALIKE_REMIX_OPTION_LABEL, + )} + /> + } + disabled={disabled} + label={translateBuilder( + BUILDER.ITEM_SETTINGS_CC_DISALLOW_REMIX_OPTION_LABEL, + )} + /> + + + + )} = ({ item, disabled }) => { {translateBuilder(BUILDER.ITEM_SETTINGS_CC_LICENSE_PREVIEW_TITLE)} - + + + )} - + ); }; diff --git a/src/config/selectors.ts b/src/config/selectors.ts index 0363231d8..9b38a0cfc 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -292,3 +292,11 @@ export const buildLanguageOptionId = (value: string): string => `languageOption-${value}`; export const buildEmailFrequencyOptionId = (value: string): string => `emailFrequencyOption-${value}`; + +export const CC_ALLOW_COMMERCIAL_CONTROL_ID = 'allowCommercialCCSelector'; +export const CC_DISALLOW_COMMERCIAL_CONTROL_ID = 'disallowCommercialCCSelector'; +export const CC_REQUIRE_ATTRIBUTION_CONTROL_ID = 'requireAttributionSelector'; +export const CC_CC0_CONTROL_ID = 'cc0Selector'; +export const CC_SHARE_ALIKE_CONTROL_ID = 'shareAlikeSelector'; +export const CC_NO_DERIVATIVE_CONTROL_ID = 'noDerivativeSelector'; +export const CC_DERIVATIVE_CONTROL_ID = 'derivativeSelector'; diff --git a/yarn.lock b/yarn.lock index 1b3fa0e74..ef671eabf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2445,12 +2445,12 @@ __metadata: languageName: node linkType: hard -"@graasp/translations@npm:1.11.0": - version: 1.11.0 - resolution: "@graasp/translations@npm:1.11.0" +"@graasp/translations@npm:1.12.0": + version: 1.12.0 + resolution: "@graasp/translations@npm:1.12.0" dependencies: i18next: 21.8.1 - checksum: 83bba8432cc5b2f83cfa88278fc61387821d421e8a3e9f6a3a1b28ce8c2789e9c8f20832305c13aa0d554d8a20824644ce968d7e6fa5b643676164eb1a3c230c + checksum: 0c8f21bd9116b9b248575e3b94b0bcfaa431e5ae4af9310423f23341df613a409874dc8f96961781090f018c659e6263b72f75d7b0b55127e62b164425669950 languageName: node linkType: hard @@ -2463,9 +2463,9 @@ __metadata: languageName: node linkType: hard -"@graasp/ui@npm:2.4.3": - version: 2.4.3 - resolution: "@graasp/ui@npm:2.4.3" +"@graasp/ui@npm:2.5.0": + version: 2.5.0 + resolution: "@graasp/ui@npm:2.5.0" dependencies: "@graasp/sdk": 0.12.0 clsx: 1.1.1 @@ -2498,7 +2498,7 @@ __metadata: optional: true ag-grid-react: optional: true - checksum: 2c1b08152f668116d2654d6ad913722b563f32762b3cf00b343250999b8857ba88740ea1d6d52929133ff50e43edc94eb74780e82febed82ec5e4bbc232ea1e4 + checksum: 7663e2cf56c1cd3c83faecb2f6565f2738fccc2f472d90cf00e6408654d5ad31bd8ff907eb15820f458b8324373750f8bc45b444d51f61bf5ac66065d9e7ec93 languageName: node linkType: hard @@ -9665,8 +9665,8 @@ __metadata: "@graasp/plugin-websockets": 1.0.0 "@graasp/query-client": 0.4.1 "@graasp/sdk": 0.12.0 - "@graasp/translations": 1.11.0 - "@graasp/ui": 2.4.3 + "@graasp/translations": 1.12.0 + "@graasp/ui": 2.5.0 "@mui/icons-material": 5.11.16 "@mui/lab": 5.0.0-alpha.117 "@mui/material": 5.12.0