diff --git a/app/actions/api.ts b/app/actions/api.ts index 03b28245..0d7e2dc0 100644 --- a/app/actions/api.ts +++ b/app/actions/api.ts @@ -737,3 +737,33 @@ export function fetchReadmePreview (peername: string, name: string): ApiActionTh return dispatch(action) } } + +// peername and name are the dataset to be renamed +// newName is the new dataset's name, which will be in the user's namespace +export function renameDataset (peername: string, name: string, newName: string): ApiActionThunk { + return async (dispatch, getState) => { + const { peername: newPeername } = getState().session + const whenOk = chainSuccess(dispatch, getState) + const action = { + type: 'rename', + [CALL_API]: { + endpoint: 'rename', + method: 'POST', + body: { + current: `${peername}/${name}`, + new: `${newPeername}/${newName}` + } + } + } + let response: Action + try { + response = await dispatch(action) + response = await whenOk(fetchMyDatasets(-1))(response) + dispatch(openToast('success', `Dataset renamed`)) + } catch (action) { + dispatch(openToast('error', action.payload.err.message)) + throw action + } + return response + } +} diff --git a/app/components/Dataset.tsx b/app/components/Dataset.tsx index 1ef926ba..191a9767 100644 --- a/app/components/Dataset.tsx +++ b/app/components/Dataset.tsx @@ -96,11 +96,6 @@ class Dataset extends React.Component { const { status } = workingDataset const { status: prevStatus } = prevProps.workingDataset - if ((this.props.selections.peername !== prevProps.selections.peername) || (this.props.selections.name !== prevProps.selections.name)) { - this.props.fetchWorkingDatasetDetails() - return - } - if (status) { // create an array of components that need updating const componentsToReset: SelectedComponent[] = [] @@ -321,7 +316,7 @@ class Dataset extends React.Component { sidebarContent={sidebarContent} sidebarWidth={datasetSidebarWidth} onSidebarResize={(width) => { setSidebarWidth('dataset', width) }} - maximumSidebarWidth={300} + maximumSidebarWidth={495} mainContent={mainContent} /> diff --git a/app/components/DatasetReference.tsx b/app/components/DatasetReference.tsx new file mode 100644 index 00000000..09579217 --- /dev/null +++ b/app/components/DatasetReference.tsx @@ -0,0 +1,112 @@ +// a component that displays the dataset reference including edit-in-place UI +// for dataset rename +import * as React from 'react' +import classNames from 'classnames' + +import { ApiActionThunk } from '../store/api' +import { validateDatasetName } from '../utils/formValidation' + +interface DatasetReferenceProps { + peername: string + name: string + renameDataset: (peername: string, name: string, newName: string) => ApiActionThunk +} + +const DatasetReference: React.FunctionComponent = (props) => { + const { peername, name, renameDataset } = props + const [ nameEditing, setNameEditing ] = React.useState(false) + const [ newName, setNewName ] = React.useState(name) + const [ inValid, setInvalid ] = React.useState(null) + + const commitRename = (peername: string, name: string, newName: string) => { + // cancel if no change, change invalid, or empty + if ((name === newName) || inValid || newName === '') { + cancelRename() + } else { + renameDataset(peername, name, newName) + .then(() => { + setNameEditing(false) + }) + } + } + + const cancelRename = () => { + setNewName(name) + setInvalid(null) + setNameEditing(false) + } + + const handleKeyDown = (e: any) => { + // cancel on esc + if (e.keyCode === 27) { + cancelRename() + } + + // submit on enter or tab + if ((e.keyCode === 13) || (e.keyCode === 9)) { + commitRename(peername, name, newName) + } + } + + // use a ref so we can set up a click handler + const nameRef: any = React.useRef(null) + + const handleMousedown = (e: MouseEvent) => { + const { target } = e + // allows the user to resize the sidebar when editing the dataset name + if (target.classList.contains('resize-handle')) return + + if (nameRef.current.isSameNode(target)) { + setNameEditing(true) + return + } + + if (!nameRef.current.contains(target)) { + commitRename(peername, name, newName) + } + } + + React.useEffect(() => { + document.addEventListener('keydown', handleKeyDown, false) + document.addEventListener('mousedown', handleMousedown, false) + + return () => { + document.removeEventListener('keydown', handleKeyDown, false) + document.removeEventListener('mousedown', handleMousedown, false) + } + }, [ name, newName ]) + + const handleInputChange = (e: any) => { + let { value } = e.target + setInvalid(validateDatasetName(value)) + setNewName(value) + } + + // when the input is focused, scroll all the way to the left + const onFocus = () => { + const el = document.getElementById('dataset-name-input') as HTMLInputElement + el.scrollLeft = 0 + } + + return ( +
+
{peername}/
+
+ { nameEditing && } + { !nameEditing && (<>{name})} +
+
+ ) +} + +export default DatasetReference diff --git a/app/components/DatasetSidebar.tsx b/app/components/DatasetSidebar.tsx index 5f317ca8..7bdbda36 100644 --- a/app/components/DatasetSidebar.tsx +++ b/app/components/DatasetSidebar.tsx @@ -7,6 +7,7 @@ import { faClock } from '@fortawesome/free-regular-svg-icons' import { ApiActionThunk } from '../store/api' import ComponentList from './ComponentList' +import DatasetReference from './DatasetReference' import classNames from 'classnames' import Spinner from './chrome/Spinner' @@ -71,6 +72,7 @@ export interface DatasetSidebarProps { fetchWorkingHistory: (page?: number, pageSize?: number) => ApiActionThunk discardChanges: (component: ComponentType) => ApiActionThunk setHideCommitNudge: () => Action + renameDataset: (peername: string, name: string, newName: string) => ApiActionThunk } const DatasetSidebar: React.FunctionComponent = (props) => { @@ -81,7 +83,8 @@ const DatasetSidebar: React.FunctionComponent = (props) => setSelectedListItem, fetchWorkingHistory, discardChanges, - setModal + setModal, + renameDataset } = props const { fsiPath, history, status, structure } = workingDataset @@ -118,7 +121,7 @@ const DatasetSidebar: React.FunctionComponent = (props) =>

Dataset

-

{peername}/{name}

+
diff --git a/app/containers/DatasetSidebarContainer.tsx b/app/containers/DatasetSidebarContainer.tsx index d3b988b9..e38fd55b 100644 --- a/app/containers/DatasetSidebarContainer.tsx +++ b/app/containers/DatasetSidebarContainer.tsx @@ -10,7 +10,8 @@ import { import { fetchWorkingHistory, - discardChanges + discardChanges, + renameDataset } from '../actions/api' import { @@ -49,7 +50,8 @@ const DatasetSidebarContainer = connect( setSelectedListItem, fetchWorkingHistory, discardChanges, - setHideCommitNudge + setHideCommitNudge, + renameDataset }, mergeProps )(DatasetSidebar) diff --git a/app/reducers/selections.ts b/app/reducers/selections.ts index 7ca26010..dbaa1886 100644 --- a/app/reducers/selections.ts +++ b/app/reducers/selections.ts @@ -32,6 +32,7 @@ export const [, UNPUBLISH_SUCC] = apiActionTypes('unpublish') export const [, SIGNIN_SUCC] = apiActionTypes('signin') export const [, SIGNUP_SUCC] = apiActionTypes('signup') export const [, , HISTORY_FAIL] = apiActionTypes('history') +export const [, RENAME_SUCC] = apiActionTypes('rename') export default (state = initialState, action: AnyAction) => { switch (action.type) { @@ -170,6 +171,13 @@ export default (state = initialState, action: AnyAction) => { activeTab: 'status' } + case RENAME_SUCC: + localStore().setItem('name', action.payload.data.name) + return { + ...state, + name: action.payload.data.name + } + default: return state } diff --git a/app/reducers/workingDataset.ts b/app/reducers/workingDataset.ts index 89f5bede..5431b6b1 100644 --- a/app/reducers/workingDataset.ts +++ b/app/reducers/workingDataset.ts @@ -57,6 +57,7 @@ export const [DATASET_STATUS_REQ, DATASET_STATUS_SUCC, DATASET_STATUS_FAIL] = ap const [DATASET_BODY_REQ, DATASET_BODY_SUCC, DATASET_BODY_FAIL] = apiActionTypes('body') const [, RESETOTHERCOMPONENTS_SUCC, RESETOTHERCOMPONENTS_FAIL] = apiActionTypes('resetOtherComponents') const [STATS_REQ, STATS_SUCC, STATS_FAIL] = apiActionTypes('stats') +export const [, RENAME_SUCC] = apiActionTypes('rename') export const RESET_BODY = 'RESET_BODY' @@ -274,6 +275,12 @@ const workingDatasetsReducer: Reducer = (state = initialState, action: AnyAction case SELECTIONS_SET_WORKING_DATASET: return initialState + case RENAME_SUCC: + return { + ...state, + name: action.payload.data.name + } + default: return state } diff --git a/app/scss/_dataset.scss b/app/scss/_dataset.scss index 8d6dfb7d..47da11da 100644 --- a/app/scss/_dataset.scss +++ b/app/scss/_dataset.scss @@ -208,12 +208,37 @@ $header-font-size: .9rem; font-weight: 900; } - .dataset-name { + .dataset-reference { font-size: 1.05rem; + display: flex; + } + + .dataset-name { + flex: 1; white-space: nowrap; text-overflow: ellipsis; - display: block; overflow: hidden; + + input { + background-color: #2f578c; + border: none; + border-radius: 5px; + color: #fff; + padding: 0 5px; + outline: none; + width: 100%; + transition: background 0.2s; + + &.invalid { + background: #F43535; + } + + } + + &:hover { + cursor: pointer; + text-decoration: underline; + } } } @@ -320,6 +345,13 @@ $header-font-size: .9rem; background-color: #38C629; } +.edit-icon { + font-size: 0.75rem; + opacity: 0; + transition: opacity 0.1s; + color: $light-blue-accent; +} + //////////////////////////// // Commit Details //