diff --git a/src/components/item/publish/CCLicenseSelection.tsx b/src/components/item/publish/CCLicenseSelection.tsx index 65dc4be5b..84e3a7029 100644 --- a/src/components/item/publish/CCLicenseSelection.tsx +++ b/src/components/item/publish/CCLicenseSelection.tsx @@ -1,240 +1,43 @@ -import { ChangeEvent, useEffect, useState } from 'react'; +import { useState } from 'react'; -import { - Box, - FormControlLabel, - Radio, - RadioGroup, - Typography, -} from '@mui/material'; +import { Box, Typography } from '@mui/material'; -import { CCLicenseAdaptions, DiscriminatedItem } from '@graasp/sdk'; -import { CCSharingVariant, CreativeCommons, Loader } from '@graasp/ui'; +import { DiscriminatedItem } from '@graasp/sdk'; +import { Loader } from '@graasp/ui'; import { useBuilderTranslation } from '../../../config/i18n'; -import { mutations } 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 { BUILDER } from '../../../langs/constants'; import { useCurrentUserContext } from '../../context/CurrentUserContext'; import CCLicenseDialog from './CCLicenseDialog'; - -type CCLicenseChoice = 'yes' | 'no' | ''; -type CCSharingLicenseChoice = CCLicenseChoice | 'alike'; +import useItemLicense from './useItemLicense'; type Props = { item: DiscriminatedItem; disabled: boolean; }; -const licensePreviewStyle = { - border: '1px solid #eee', - borderRadius: 2, - minWidth: 300, -}; - const CCLicenseSelection = ({ item, disabled }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); - const { mutate: updateCCLicense } = mutations.useEditItem(); - - const [requireAttributionValue, setRequireAttributionValue] = - useState(''); - const [allowCommercialValue, setAllowCommercialValue] = - useState(''); - const [allowSharingValue, setAllowSharingValue] = - useState(''); - - const [open, setOpen] = useState(false); + const { handleSubmit, licenseForm, creativeCommons } = useItemLicense({ + item, + }); // user const { isLoading: isMemberLoading } = useCurrentUserContext(); - // itemId - const itemId = item?.id; + const [open, setOpen] = useState(false); const settings = item?.settings; - const itemName = item?.name; - - useEffect(() => { - if (settings?.ccLicenseAdaption) { - // Handles old license formats. - if (['alike', 'allow'].includes(settings?.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 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 = () => { - if (requireAttributionValue) { - updateCCLicense({ - id: itemId, - name: itemName, - settings: { ccLicenseAdaption: convertSelectionToLicense() }, - }); - } else { - console.error(`optionValue "${requireAttributionValue}" is undefined`); - } + const onSubmit = () => { + handleSubmit(); setOpen(false); }; - return ( - - {translateBuilder(BUILDER.ITEM_SETTINGS_CC_ATTRIBUTION_TITLE)} - - - - } - 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, - )} - /> - - - - )} + {licenseForm} { buttonName={translateBuilder( BUILDER.ITEM_SETTINGS_CC_LICENSE_SUBMIT_BUTTON, )} - handleSubmit={handleSubmit} + handleSubmit={onSubmit} /> {settings?.ccLicenseAdaption && ( <> @@ -250,12 +53,7 @@ const CCLicenseSelection = ({ item, disabled }: Props): JSX.Element => { {translateBuilder(BUILDER.ITEM_SETTINGS_CC_LICENSE_PREVIEW_TITLE)} - + {creativeCommons} )} diff --git a/src/components/item/publish/ConfirmLicenseDialogContent.tsx b/src/components/item/publish/ConfirmLicenseDialogContent.tsx new file mode 100644 index 000000000..cfeaff31e --- /dev/null +++ b/src/components/item/publish/ConfirmLicenseDialogContent.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +import { DialogActions, DialogContent, DialogContentText } from '@mui/material'; + +import { Button } from '@graasp/ui'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { BUILDER } from '@/langs/constants'; + +type Props = { + handleSubmit: () => void; + disableSubmission?: boolean; + handleBack: () => void; +}; + +const ConfirmLicenseDialogContent = ({ + handleSubmit, + disableSubmission, + handleBack, +}: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + return ( + <> + + + {translateBuilder(BUILDER.ITEM_SETTINGS_CC_LICENSE_MODAL_CONTENT)} + + + + + + + + ); +}; + +export default ConfirmLicenseDialogContent; diff --git a/src/components/item/publish/LicenseForm.tsx b/src/components/item/publish/LicenseForm.tsx new file mode 100644 index 000000000..06034fee0 --- /dev/null +++ b/src/components/item/publish/LicenseForm.tsx @@ -0,0 +1,171 @@ +import { ChangeEvent } from 'react'; + +import { + Box, + FormControlLabel, + Radio, + RadioGroup, + Typography, +} from '@mui/material'; + +import { useBuilderTranslation } from '@/config/i18n'; +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 { BUILDER } from '@/langs/constants'; + +import { CCLicenseChoice, CCSharingLicenseChoice } from './type'; + +interface Props { + setRequireAttributionValue: (s: CCLicenseChoice) => void; + requireAttributionValue: CCLicenseChoice; + setAllowCommercialValue: (s: CCLicenseChoice) => void; + setAllowSharingValue: (s: CCSharingLicenseChoice) => void; + allowCommercialValue: CCLicenseChoice; + allowSharingValue: CCSharingLicenseChoice; + disabled?: boolean; +} + +const LicenseForm = ({ + setRequireAttributionValue, + requireAttributionValue, + setAllowCommercialValue, + setAllowSharingValue, + allowCommercialValue, + allowSharingValue, + disabled, +}: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + 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); + }; + + return ( + <> + + {translateBuilder(BUILDER.ITEM_SETTINGS_CC_ATTRIBUTION_TITLE)} + + + + + } + 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, + )} + /> + + + + )} + + ); +}; +export default LicenseForm; diff --git a/src/components/item/publish/type.ts b/src/components/item/publish/type.ts new file mode 100644 index 000000000..7e3da0e45 --- /dev/null +++ b/src/components/item/publish/type.ts @@ -0,0 +1,2 @@ +export type CCLicenseChoice = 'yes' | 'no' | ''; +export type CCSharingLicenseChoice = CCLicenseChoice | 'alike'; diff --git a/src/components/item/publish/useItemLicense.tsx b/src/components/item/publish/useItemLicense.tsx new file mode 100644 index 000000000..1eef41ba0 --- /dev/null +++ b/src/components/item/publish/useItemLicense.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react'; + +import { SxProps } from '@mui/material'; + +import { DiscriminatedItem } from '@graasp/sdk'; +import { CCSharingVariant, CreativeCommons } from '@graasp/ui'; + +import { mutations } from '@/config/queryClient'; +import { convertLicense, convertSelectionToLicense } from '@/utils/itemLicense'; + +import LicenseForm from './LicenseForm'; +import { CCLicenseChoice, CCSharingLicenseChoice } from './type'; + +const licensePreviewStyle = { + border: '1px solid #eee', + borderRadius: 2, + minWidth: 300, +}; + +const useItemLicense = ({ + item, + disabled, + commonsSx, +}: { + item: DiscriminatedItem; + disabled?: boolean; + commonsSx?: SxProps; +}): { + handleSubmit: () => void; + licenseForm: JSX.Element; + creativeCommons: JSX.Element; + requireAttributionValue: CCLicenseChoice; +} => { + const [requireAttributionValue, setRequireAttributionValue] = + useState(''); + const [allowCommercialValue, setAllowCommercialValue] = + useState(''); + const [allowSharingValue, setAllowSharingValue] = + useState(''); + + const { mutate: updateCCLicense } = mutations.useEditItem(); + + const { id, settings } = item; + + useEffect(() => { + if (settings?.ccLicenseAdaption) { + const { allowCommercialUse, allowSharing, requireAccreditation } = + convertLicense(settings.ccLicenseAdaption); + setAllowSharingValue(allowSharing); + setAllowCommercialValue(allowCommercialUse ? 'yes' : 'no'); + setRequireAttributionValue(requireAccreditation ? 'yes' : 'no'); + } + }, [settings]); + + const handleSubmit = () => { + if (requireAttributionValue) { + updateCCLicense({ + id, + settings: { + ccLicenseAdaption: convertSelectionToLicense({ + allowCommercialValue, + allowSharingValue, + requireAttributionValue, + }), + }, + }); + } else { + console.error(`optionValue "${requireAttributionValue}" is undefined`); + } + }; + + const licenseForm = ( + + ); + + const creativeCommons = ( + + ); + return { + handleSubmit, + licenseForm, + creativeCommons, + requireAttributionValue, + }; +}; + +export default useItemLicense; diff --git a/src/components/item/settings/ItemLicenseSettings.tsx b/src/components/item/settings/ItemLicenseSettings.tsx new file mode 100644 index 000000000..44c65c38f --- /dev/null +++ b/src/components/item/settings/ItemLicenseSettings.tsx @@ -0,0 +1,76 @@ +import { useMemo, useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; + +import { Help } from '@mui/icons-material'; +import { Box, IconButton, Stack, Tooltip, Typography } from '@mui/material'; + +import { redirect } from '@graasp/sdk'; +import { Button, CCSharingVariant, CreativeCommons } from '@graasp/ui'; + +import { OutletType } from '@/components/pages/item/type'; +import { CC_LICENSE_ABOUT_URL } from '@/config/constants'; +import { useBuilderTranslation } from '@/config/i18n'; +import { BUILDER } from '@/langs/constants'; +import { convertLicense } from '@/utils/itemLicense'; + +import UpdateLicenseDialog from './UpdateLicenseDialog'; + +const ItemLicenseSettings = (): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + const [licenseDialogOpen, setLicenseDialogOpen] = useState(false); + + const { item } = useOutletContext(); + + const handleClick = () => { + const url = CC_LICENSE_ABOUT_URL; + redirect(window, url, { openInNewTab: true }); + }; + + const { allowSharing, allowCommercialUse, requireAccreditation } = useMemo( + () => convertLicense(item.settings.ccLicenseAdaption ?? ''), + [item.settings.ccLicenseAdaption], + ); + + return ( + <> + + + + {translateBuilder(BUILDER.ITEM_SETTINGS_CC_LICENSE_TITLE)} + + + + + + + + + {item.settings?.ccLicenseAdaption && ( + + + + )} + + + + + + ); +}; + +export default ItemLicenseSettings; diff --git a/src/components/item/settings/ItemSettings.tsx b/src/components/item/settings/ItemSettings.tsx index f718b5ba0..520aa6e44 100644 --- a/src/components/item/settings/ItemSettings.tsx +++ b/src/components/item/settings/ItemSettings.tsx @@ -7,6 +7,7 @@ import { OutletType } from '@/components/pages/item/type'; import CustomizedTagsEdit from '../publish/CustomizedTagsEdit'; import AdminChatSettings from './AdminChatSettings'; import GeolocationPicker from './GeolocationPicker'; +import ItemLicenseSettings from './ItemLicenseSettings'; import ItemMetadataContent from './ItemMetadataContent'; import ItemSettingsProperties from './ItemSettingsProperties'; @@ -22,6 +23,8 @@ const ItemSettings = (): JSX.Element => { + + ); diff --git a/src/components/item/settings/UpdateLicenseDialog.tsx b/src/components/item/settings/UpdateLicenseDialog.tsx new file mode 100644 index 000000000..2ddb8700d --- /dev/null +++ b/src/components/item/settings/UpdateLicenseDialog.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react'; + +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@mui/material'; + +import { DiscriminatedItem } from '@graasp/sdk'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { BUILDER } from '@/langs/constants'; + +import ConfirmLicenseDialogContent from '../publish/ConfirmLicenseDialogContent'; +import useItemLicense from '../publish/useItemLicense'; + +type Props = { + open: boolean; + setOpen: (b: boolean) => void; + item: DiscriminatedItem; +}; + +const commonsSx = { + border: '1px solid #eee', + borderRadius: 2, + minWidth: 300, + alignItems: 'center', +}; +const UpdateLicenseDialog = ({ open, setOpen, item }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + const [confirmationStep, setConfirmationStep] = useState(false); + + const { + handleSubmit, + licenseForm, + creativeCommons, + requireAttributionValue, + } = useItemLicense({ + item, + commonsSx, + }); + + const handleClose = () => { + setOpen(false); + }; + + const submitForm = () => { + handleSubmit(); + setOpen(false); + }; + + useEffect(() => { + if (!open) { + setConfirmationStep(false); + } + }, [open]); + + return ( + + + {translateBuilder(BUILDER.ITEM_SETTINGS_LICENSE_TITLE)} + + + {!confirmationStep ? ( + <> + {licenseForm} + {item.settings?.ccLicenseAdaption && ( + + {creativeCommons} + + )} + + + + + + + ) : ( + setConfirmationStep(false)} + /> + )} + + ); +}; + +export default UpdateLicenseDialog; diff --git a/src/langs/constants.ts b/src/langs/constants.ts index 28411faf2..3c0a9a413 100644 --- a/src/langs/constants.ts +++ b/src/langs/constants.ts @@ -424,6 +424,9 @@ export const BUILDER = { 'ITEM_SETTINGS_DESCRIPTION_PLACEMENT_BELOW', ITEM_SETTINGS_DESCRIPTION_PLACEMENT_TITLE: 'ITEM_SETTINGS_DESCRIPTION_PLACEMENT_TITLE', + ITEM_SETTINGS_LICENSE_TITLE: 'ITEM_SETTINGS_LICENSE_TITLE', + CANCEL_BUTTON: 'CANCEL_BUTTON', + UPDATE_LICENSE: 'UPDATE_LICENSE', ITEM_GEOLOCATION_ADVANCED_BUTTON: 'ITEM_GEOLOCATION_ADVANCED_BUTTON', ITEM_GEOLOCATION_ADVANCED_MODAL_TITLE: 'ITEM_GEOLOCATION_ADVANCED_MODAL_TITLE', diff --git a/src/langs/en.json b/src/langs/en.json index ae06893ee..5403320f6 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -359,6 +359,9 @@ "ITEM_SETTINGS_DESCRIPTION_PLACEMENT_ABOVE": "Above the content", "ITEM_SETTINGS_DESCRIPTION_PLACEMENT_BELOW": "Below the content", "ITEM_SETTINGS_DESCRIPTION_PLACEMENT_TITLE": "Placement of the description", + "ITEM_SETTINGS_LICENSE_TITLE": "License Settings", + "CANCEL_BUTTON": "Cancel", + "UPDATE_LICENSE": "Update License", "ITEM_GEOLOCATION_ADVANCED_BUTTON": "Advanced Location", "ITEM_GEOLOCATION_ADVANCED_MODAL_TITLE": "Geolocation Advanced Settings", "ITEM_GEOLOCATION_ADVANCED_MODAL_DESCRIPTION": "Any information submitted with this form won't be validated. You are responsible for the accuracy of the data.", diff --git a/src/utils/itemLicense.ts b/src/utils/itemLicense.ts new file mode 100644 index 000000000..50871bb39 --- /dev/null +++ b/src/utils/itemLicense.ts @@ -0,0 +1,59 @@ +import { CCLicenseAdaptions } from '@graasp/sdk'; + +import { + CCLicenseChoice, + CCSharingLicenseChoice, +} from '@/components/item/publish/type'; + +interface LicenseOptions { + requireAttributionValue: CCLicenseChoice; + allowCommercialValue: CCLicenseChoice; + allowSharingValue: CCSharingLicenseChoice; +} + +export const convertSelectionToLicense = ({ + requireAttributionValue, + allowCommercialValue, + allowSharingValue, +}: LicenseOptions): CCLicenseAdaptions => { + if (requireAttributionValue !== 'yes') { + return CCLicenseAdaptions.CC0; + } + + return `CC BY${allowCommercialValue === 'yes' ? '' : '-NC'}${ + allowSharingValue === 'alike' ? '-SA' : '' + }${allowSharingValue === 'no' ? '-ND' : ''}` as CCLicenseAdaptions; +}; + +interface AdaptionToLicense { + requireAccreditation: boolean; + allowCommercialUse: boolean; + allowSharing: CCSharingLicenseChoice; +} + +export const convertLicense = ( + ccLicenseAdaption: string, +): AdaptionToLicense => { + // Legacy licenses. + if (['alike', 'allow'].includes(ccLicenseAdaption)) { + return { + requireAccreditation: true, + allowCommercialUse: true, + allowSharing: ccLicenseAdaption === 'alike' ? 'alike' : 'yes', + }; + } + + return { + requireAccreditation: ccLicenseAdaption?.includes('BY'), + allowCommercialUse: !ccLicenseAdaption?.includes('NC'), + allowSharing: (() => { + if (!ccLicenseAdaption?.length) { + return ''; + } + if (ccLicenseAdaption?.includes('SA')) { + return 'alike'; + } + return ccLicenseAdaption?.includes('ND') ? 'no' : 'yes'; + })(), + }; +};