UNPKG

@craftercms/studio-ui

Version:

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

303 lines (301 loc) 11.4 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, { useCallback, useEffect, useState } from 'react'; import GlobalAppToolbar from '../GlobalAppToolbar'; import { FormattedMessage, useIntl } from 'react-intl'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import AddIcon from '@mui/icons-material/Add'; import { fetchRepositories as fetchRepositoriesService, fetchStatus } from '../../services/repositories'; import RepoGrid from './RepoGrid'; import RepoStatus from './RepoStatus/RepoStatus'; import NewRemoteRepositoryDialog from '../NewRemoteRepositoryDialog'; import { showSystemNotification } from '../../state/actions/system'; import { useDispatch } from 'react-redux'; import Alert from '@mui/material/Alert'; import Typography from '@mui/material/Typography'; import useStyles from './styles'; import translations from './translations'; import { useActiveSiteId } from '../../hooks/useActiveSiteId'; import Paper from '@mui/material/Paper'; import { useEnhancedDialogState } from '../../hooks/useEnhancedDialogState'; import { useWithPendingChangesCloseRequest } from '../../hooks/useWithPendingChangesCloseRequest'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Box from '@mui/material/Box'; import WarningRounded from '@mui/icons-material/WarningRounded'; import useSpreadState from '../../hooks/useSpreadState'; import { UNDEFINED } from '../../utils/constants'; import RefreshRounded from '@mui/icons-material/RefreshRounded'; import Tooltip from '@mui/material/Tooltip'; function a11yProps(index) { return { id: `simple-tab-${index}`, 'aria-controls': `gitReposTabpanel_${index}` }; } // TODO: // - Accommodate area to display repo fetch errors // - Accommodate area to display status fetch errors // - Use/discard `loading` props export function GitManagement(props) { const { embedded, showAppsButton = !embedded } = props; const { classes } = useStyles(); const siteId = useActiveSiteId(); const dispatch = useDispatch(); const { formatMessage } = useIntl(); const [activeTab, setActiveTab] = useState(0); const [{ repositories }, setRepoState] = useSpreadState({ repositories: null, loadingRepos: false, reposFetchError: null }); const [{ loadingStatus, repoStatus, clean, hasConflicts, hasUncommitted, statusMessageKey }, setRepoStatusState] = useSpreadState({ clean: true, loadingStatus: false, hasConflicts: false, hasUncommitted: false, statusFetchError: null, repoStatus: null, statusMessageKey: null }); const fetchRepositories = useCallback(() => { setRepoState({ loadingRepos: true, reposFetchError: null }); fetchRepositoriesService(siteId).subscribe({ next: (repositories) => { setRepoState({ repositories, loadingRepos: false }); }, error: ({ response }) => { setRepoState({ loadingRepos: false, reposFetchError: response }); } }); }, [setRepoState, siteId]); const fetchRepoStatusReceiver = useCallback( (repoStatus) => { let statusMessageKey = null; if (repoStatus.clean) { statusMessageKey = 'noConflicts'; } else if (repoStatus.conflicting.length > 0) { statusMessageKey = 'conflictsExist'; } else if (repoStatus.uncommittedChanges.length > 0 && repoStatus.conflicting.length < 1) { statusMessageKey = 'pendingCommit'; } else if (repoStatus.uncommittedChanges.length < 1 && repoStatus.conflicting.length < 1) { statusMessageKey = 'unstagedFiles'; } setRepoStatusState({ loadingStatus: false, repoStatus, statusMessageKey, clean: repoStatus.clean, hasConflicts: Boolean(repoStatus.conflicting.length), hasUncommitted: Boolean(repoStatus.uncommittedChanges.length) }); }, [setRepoStatusState] ); const fetchRepoStatus = useCallback(() => { setRepoStatusState({ loadingStatus: true, statusFetchError: null }); fetchStatus(siteId).subscribe({ next: fetchRepoStatusReceiver, error(response) { setRepoStatusState({ loadingStatus: false, statusFetchError: response }); dispatch( showSystemNotification({ message: response.response.message, options: { variant: 'error' } }) ); } }); }, [dispatch, fetchRepoStatusReceiver, setRepoStatusState, siteId]); const onRepoCreatedSuccess = () => { fetchRepositories(); newRemoteRepositoryDialogState.onClose(); dispatch( showSystemNotification({ message: formatMessage(translations.remoteCreateSuccessMessage) }) ); }; const onRepoCreateError = ({ response }) => { dispatch( showSystemNotification({ message: response.response.message, options: { variant: 'error' } }) ); }; const onTabChange = (event, newValue) => { setActiveTab(newValue); }; useEffect(() => { fetchRepositories(); }, [fetchRepositories]); useEffect(() => { fetchRepoStatus(); }, [fetchRepoStatus]); const newRemoteRepositoryDialogState = useEnhancedDialogState(); const newRemoteRepositoryDialogStatePendingChangesCloseRequest = useWithPendingChangesCloseRequest( newRemoteRepositoryDialogState.onClose ); return React.createElement( Paper, { elevation: 0 }, React.createElement(GlobalAppToolbar, { title: !embedded && React.createElement(FormattedMessage, { id: 'words.git', defaultMessage: 'Git' }), leftContent: React.createElement( Button, { startIcon: React.createElement(AddIcon, null), variant: 'outlined', color: 'primary', onClick: () => newRemoteRepositoryDialogState.onOpen() }, React.createElement(FormattedMessage, { id: 'repositories.newRepository', defaultMessage: 'New Remote' }) ), rightContent: React.createElement( Tooltip, { title: React.createElement(FormattedMessage, { id: 'words.refresh', defaultMessage: 'Refresh' }) }, React.createElement( IconButton, { onClick: () => { fetchRepositories(); fetchRepoStatus(); } }, React.createElement(RefreshRounded, null) ) ), showHamburgerMenuButton: !embedded, showAppsButton: showAppsButton }), React.createElement( Box, { sx: { borderBottom: 1, borderColor: 'divider' } }, React.createElement( Tabs, { value: activeTab, onChange: onTabChange }, React.createElement( Tab, Object.assign( { label: React.createElement(FormattedMessage, { id: 'remoteRepositories.title', defaultMessage: 'Remote Repositories' }) }, a11yProps(0) ) ), React.createElement( Tab, Object.assign( { sx: { flexDirection: 'row', color: hasConflicts ? 'error.main' : hasUncommitted ? 'warning.main' : UNDEFINED }, label: React.createElement( React.Fragment, null, React.createElement(FormattedMessage, { id: 'repository.repositoryStatusLabel', defaultMessage: 'Repository Status' }), (hasConflicts || hasUncommitted) && React.createElement(WarningRounded, { sx: { ml: 1 }, color: hasConflicts ? 'error' : 'warning' }) ) }, a11yProps(1) ) ) ) ), React.createElement( 'section', null, activeTab === 0 && React.createElement( Box, { padding: 2 }, React.createElement( Alert, { severity: loadingStatus ? 'info' : clean ? 'success' : 'warning' }, formatMessage( translations[ loadingStatus ? 'fetchingStatus' : statusMessageKey !== null && statusMessageKey !== void 0 ? statusMessageKey : 'fetchingStatus' ] ) ), React.createElement(RepoGrid, { repositories: repositories, fetchStatus: fetchRepoStatus, fetchRepositories: fetchRepositories, disableActions: !repoStatus || repoStatus.conflicting.length > 0 }), React.createElement( Typography, { variant: 'caption', className: classes.statusNote }, React.createElement(FormattedMessage, { id: 'repository.statusNote', defaultMessage: 'Do not use Studio as a git merge and conflict resolution platform. All merge conflicts should be resolved upstream before getting pulled into Studio.' }) ) ), activeTab === 1 && React.createElement(RepoStatus, { status: repoStatus, onCommitSuccess: fetchRepoStatusReceiver, onConflictResolved: fetchRepoStatusReceiver, onFailedPullCancelled: fetchRepoStatusReceiver }), React.createElement(NewRemoteRepositoryDialog, { open: newRemoteRepositoryDialogState.open, isMinimized: newRemoteRepositoryDialogState.isMinimized, isSubmitting: newRemoteRepositoryDialogState.isSubmitting, hasPendingChanges: newRemoteRepositoryDialogState.hasPendingChanges, onSubmittingAndOrPendingChange: newRemoteRepositoryDialogState.onSubmittingAndOrPendingChange, onWithPendingChangesCloseRequest: newRemoteRepositoryDialogStatePendingChangesCloseRequest, onClose: newRemoteRepositoryDialogState.onClose, onCreateSuccess: onRepoCreatedSuccess, onCreateError: onRepoCreateError }) ) ); } export default GitManagement;