UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

510 lines (508 loc) 18.9 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { useEffect, useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { darken, lighten } from '@mui/material/styles'; import { makeStyles } from 'tss-react/mui'; import { withStyles } from 'tss-react/mui'; import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/DeleteOutline'; import { AsDayMonthDateTime } from '../VersionList'; import { deleteToken, fetchTokens as fetchTokensService, updateToken } from '../../services/tokens'; import { useDispatch } from 'react-redux'; import CreateTokenDialog from '../CreateTokenDialog'; import { showSystemNotification } from '../../state/actions/system'; import ConfirmDropdown from '../ConfirmDropdown'; import ActionsBar from '../ActionsBar'; import { ConditionalLoadingState } from '../LoadingState/LoadingState'; import EmptyState from '../EmptyState/EmptyState'; import CopyTokenDialog from '../CopyTokenDialog/CopyTokenDialog'; import moment from 'moment-timezone'; import { forkJoin } from 'rxjs'; import Typography from '@mui/material/Typography'; import TableContainer from '@mui/material/TableContainer'; import Table from '@mui/material/Table'; import TableRow from '@mui/material/TableRow'; import Checkbox from '@mui/material/Checkbox'; import TableCell from '@mui/material/TableCell'; import TableHead from '@mui/material/TableHead'; import TableBody from '@mui/material/TableBody'; import Chip from '@mui/material/Chip'; import Switch from '@mui/material/Switch'; import { showErrorDialog } from '../../state/reducers/dialogs/error'; import GlobalAppToolbar from '../GlobalAppToolbar'; import Button from '@mui/material/Button'; import Paper from '@mui/material/Paper'; import { useEnhancedDialogState } from '../../hooks/useEnhancedDialogState'; import { useWithPendingChangesCloseRequest } from '../../hooks/useWithPendingChangesCloseRequest'; const styles = makeStyles()((theme) => ({ table: { minWidth: 650 }, actions: { width: '150px', padding: '5px 20px' }, actionsBar: { position: 'absolute', left: 0, right: 0, zIndex: 1 }, chip: { backgroundColor: theme.palette.mode === 'light' ? lighten(theme.palette.success.main, 0.9) : darken(theme.palette.success.main, 0.9), height: 'auto', padding: '4px 6.5px', '&.disabled': { backgroundColor: theme.palette.mode === 'light' ? lighten(theme.palette.warning.main, 0.9) : darken(theme.palette.warning.main, 0.9) }, '&.expired': { backgroundColor: theme.palette.mode === 'light' ? lighten(theme.palette.error.main, 0.9) : darken(theme.palette.error.main, 0.9) } } })); const StyledTableCell = withStyles(TableCell, () => ({ root: { padding: '5px' } })); const translations = defineMessages({ tokenCreated: { id: 'tokenManagement.created', defaultMessage: 'Token created and copied to clipboard' }, tokenCopied: { id: 'tokenManagement.copied', defaultMessage: 'Token copied to clipboard' }, tokenDeleted: { id: 'tokenManagement.deleted', defaultMessage: '{count, plural, one {Token deleted} other {The selected tokens were deleted}}' }, tokenUpdated: { id: 'tokenManagement.updated', defaultMessage: 'Token updated' }, never: { id: 'words.never', defaultMessage: 'Never' }, confirmHelperText: { id: 'tokenManagement.helperText', defaultMessage: 'Delete "{label}" token?' }, confirmOk: { id: 'words.yes', defaultMessage: 'Yes' }, confirmCancel: { id: 'words.no', defaultMessage: 'No' }, deletedSelected: { id: 'tokenManagement.deletedSelected', defaultMessage: 'Delete Selected' }, clearSelected: { id: 'tokenManagement.clearSelected', defaultMessage: 'Clear Selected ({count})' }, emptyTokens: { id: 'tokenManagement.emptyTokens', defaultMessage: 'There are no tokens, click on Create Token to add a new one' } }); export function TokenManagement() { const { classes, cx } = styles(); const dispatch = useDispatch(); const { formatMessage } = useIntl(); const [tokens, setTokens] = useState(null); const [checkedLookup, setCheckedLookup] = useState({}); const [createdToken, setCreatedToken] = useState(null); const checkedCount = useMemo(() => Object.values(checkedLookup).filter(Boolean).length, [checkedLookup]); const options = useMemo( () => [ { id: 'delete', label: formatMessage(translations.deletedSelected) }, { id: 'clear', label: formatMessage(translations.clearSelected, { count: checkedCount }) } ], [checkedCount, formatMessage] ); const fetchTokens = () => { fetchTokensService().subscribe((tokens) => { setTokens(tokens); }); }; useEffect(() => { fetchTokens(); }, []); const createTokenDialogState = useEnhancedDialogState(); const createTokenDialogPendingChangesCloseRequest = useWithPendingChangesCloseRequest(createTokenDialogState.onClose); const copyTokenDialogState = useEnhancedDialogState(); const copyTokenDialogPendingChangesCloseRequest = useWithPendingChangesCloseRequest(copyTokenDialogState.onClose); const onCreateToken = () => { createTokenDialogState.onOpen(); }; const onCopyTokenDialogClosed = () => { setCreatedToken(null); }; const onTokenCreated = (token) => { fetchTokens(); dispatch( showSystemNotification({ message: formatMessage(translations.tokenCreated) }) ); createTokenDialogState.onClose(); copyTokenDialogState.onOpen(); setCreatedToken(token); }; const onTokenCopied = () => { dispatch( showSystemNotification({ message: formatMessage(translations.tokenCopied) }) ); }; const onSetEnabled = (id, checked) => { const _tokens = Object.assign({}, tokens); setTokens( tokens.map((token) => { if (token.id === id) { return Object.assign(Object.assign({}, token), { enabled: checked }); } else { return token; } }) ); updateToken(id, { enabled: checked }).subscribe( (token) => { fetchTokens(); dispatch( showSystemNotification({ message: formatMessage(translations.tokenUpdated) }) ); }, (response) => { setTokens(_tokens); dispatch(showErrorDialog({ error: response })); } ); }; const onDeleteToken = (id) => { const _tokens = Object.assign({}, tokens); setTokens(tokens.filter((token) => token.id !== id)); setCheckedLookup(Object.assign(Object.assign({}, checkedLookup), { [id]: false })); deleteToken(id).subscribe( (token) => { fetchTokens(); dispatch( showSystemNotification({ message: formatMessage(translations.tokenDeleted, { count: 1 }) }) ); }, (response) => { setTokens(_tokens); setCheckedLookup(Object.assign(Object.assign({}, checkedLookup), { [id]: false })); dispatch(showErrorDialog({ error: response })); } ); }; const onOptionClicked = (action) => { switch (action) { case 'delete': { const requests = []; let checkedIds = []; let _checkedLookup = Object.assign({}, checkedLookup); Object.keys(checkedLookup).forEach((id, i) => { if (checkedLookup[id]) { checkedIds.push(parseInt(id)); _checkedLookup[id] = false; requests.push(deleteToken(parseInt(id))); } }); setCheckedLookup(_checkedLookup); setTokens(tokens.filter((token) => !checkedIds.includes(token.id))); forkJoin(requests).subscribe( (responses) => { dispatch( showSystemNotification({ message: formatMessage(translations.tokenDeleted, { count: checkedIds.length }) }) ); }, (response) => { fetchTokens(); dispatch(showErrorDialog({ error: response })); } ); break; } case 'clear': { onToggleSelectAll(false); break; } } }; const onItemChecked = (id, checked) => { setCheckedLookup(Object.assign(Object.assign({}, checkedLookup), { [id]: checked })); }; const onToggleSelectAll = (check) => { const _checkedLookup = {}; tokens.forEach((token) => { _checkedLookup[token.id] = check; }); setCheckedLookup(_checkedLookup); }; return React.createElement( Paper, { elevation: 0 }, React.createElement(GlobalAppToolbar, { title: React.createElement(FormattedMessage, { id: 'globalMenu.tokenManagement', defaultMessage: 'Token Management' }), leftContent: React.createElement( Button, { startIcon: React.createElement(AddIcon, null), variant: 'outlined', color: 'primary', onClick: onCreateToken }, React.createElement(FormattedMessage, { id: 'tokenManagement.createToken', defaultMessage: 'Create Token' }) ) }), React.createElement( ConditionalLoadingState, { isLoading: tokens === null }, (tokens === null || tokens === void 0 ? void 0 : tokens.length) ? React.createElement( TableContainer, { style: { position: 'relative' } }, checkedCount > 0 && React.createElement(ActionsBar, { onOptionClicked: onOptionClicked, options: options, isIndeterminate: checkedCount > 0 && checkedCount < tokens.length, isChecked: checkedCount === tokens.length, onCheckboxChange: () => onToggleSelectAll(checkedCount !== tokens.length), classes: { root: classes.actionsBar } }), React.createElement( Table, { className: classes.table }, React.createElement( TableHead, null, React.createElement( TableRow, null, React.createElement( TableCell, { padding: 'checkbox' }, React.createElement(Checkbox, { disabled: checkedCount > 0, checked: false, color: 'primary', onClick: () => { onToggleSelectAll(true); } }) ), React.createElement( TableCell, { align: 'left', padding: 'none' }, React.createElement( Typography, { variant: 'subtitle2' }, React.createElement(FormattedMessage, { id: 'words.status', defaultMessage: 'Status' }) ) ), React.createElement( StyledTableCell, { align: 'left' }, React.createElement( Typography, { variant: 'subtitle2' }, React.createElement(FormattedMessage, { id: 'words.label', defaultMessage: 'Label' }) ) ), React.createElement( StyledTableCell, { align: 'left' }, React.createElement( Typography, { variant: 'subtitle2' }, React.createElement(FormattedMessage, { id: 'words.expiration', defaultMessage: 'Expiration' }) ) ), React.createElement( StyledTableCell, { align: 'left' }, React.createElement( Typography, { variant: 'subtitle2' }, React.createElement(FormattedMessage, { id: 'words.created', defaultMessage: 'Created' }) ) ), React.createElement(TableCell, { align: 'center', className: classes.actions }) ) ), React.createElement( TableBody, null, tokens.map((token) => React.createElement( TableRow, { key: token.id }, React.createElement( TableCell, { padding: 'checkbox' }, React.createElement(Checkbox, { checked: !!checkedLookup[token.id], color: 'primary', onChange: (event, checked) => { onItemChecked(token.id, checked); } }) ), React.createElement( TableCell, { component: 'th', id: token.id.toString(), scope: 'row', padding: 'none' }, React.createElement(Chip, { label: Date.parse(token.expiresAt) < Date.now() ? React.createElement(FormattedMessage, { id: 'words.expired', defaultMessage: 'Expired' }) : token.enabled ? React.createElement(FormattedMessage, { id: 'words.enabled', defaultMessage: 'Enabled' }) : React.createElement(FormattedMessage, { id: 'words.disabled', defaultMessage: 'Disabled' }), className: cx( classes.chip, !token.enabled && 'disabled', moment(token.expiresAt) < moment() && 'expired' ) }) ), React.createElement(StyledTableCell, { align: 'left' }, token.label), React.createElement( StyledTableCell, { align: 'left' }, token.expiresAt ? React.createElement(AsDayMonthDateTime, { date: token.expiresAt }) : React.createElement( Typography, { color: 'textSecondary', variant: 'body2' }, '(', formatMessage(translations.never).toLowerCase(), ')' ) ), React.createElement( StyledTableCell, { align: 'left' }, React.createElement(AsDayMonthDateTime, { date: token.createdOn }) ), React.createElement( TableCell, { align: 'right', className: classes.actions }, (token.expiresAt === null || moment(token.expiresAt) > moment()) && React.createElement(Switch, { checked: token.enabled, onChange: (e, checked) => { onSetEnabled(token.id, checked); }, color: 'primary' }), React.createElement(ConfirmDropdown, { cancelText: formatMessage(translations.confirmCancel), confirmText: formatMessage(translations.confirmOk), confirmHelperText: formatMessage(translations.confirmHelperText, { label: token.label }), icon: DeleteIcon, onConfirm: () => { onDeleteToken(token.id); } }) ) ) ) ) ) ) : React.createElement(EmptyState, { title: formatMessage(translations.emptyTokens) }) ), React.createElement(CreateTokenDialog, { open: createTokenDialogState.open, hasPendingChanges: createTokenDialogState.hasPendingChanges, isSubmitting: createTokenDialogState.isSubmitting, isMinimized: createTokenDialogState.isMinimized, onSubmittingAndOrPendingChange: createTokenDialogState.onSubmittingAndOrPendingChange, onWithPendingChangesCloseRequest: createTokenDialogPendingChangesCloseRequest, onCreated: onTokenCreated, onClose: createTokenDialogState.onClose }), React.createElement(CopyTokenDialog, { open: copyTokenDialogState.open, token: createdToken, hasPendingChanges: copyTokenDialogState.hasPendingChanges, isSubmitting: copyTokenDialogState.isSubmitting, isMinimized: copyTokenDialogState.isMinimized, onWithPendingChangesCloseRequest: copyTokenDialogPendingChangesCloseRequest, onClose: copyTokenDialogState.onClose, onClosed: onCopyTokenDialogClosed, onCopy: onTokenCopied }) ); } export default TokenManagement;