mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 13:15:52 +03:00
Check asset name uniqueness (#8018)
- Implements frontend component of https://github.com/enso-org/cloud-v2/issues/702 - Ensures that the new name is not already present in a sibling in the directory - Only compares between directories when renaming a directory - Only compares between files/projects/connectors when renaming one of those # Important Notes - This has not been implemented for connectors and files as currently there is no backend endpoint to rename those. - Secrets are also not implemented yet, AFAIK, so there is no behavior related to secrets.
This commit is contained in:
parent
826127d8ff
commit
94d3a05905
@ -141,6 +141,10 @@ export interface AssetsTableState {
|
||||
dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void
|
||||
assetEvents: assetEventModule.AssetEvent[]
|
||||
dispatchAssetEvent: (event: assetEventModule.AssetEvent) => void
|
||||
topLevelAssets: Readonly<React.MutableRefObject<assetTreeNode.AssetTreeNode[]>>
|
||||
nodeMap: Readonly<
|
||||
React.MutableRefObject<ReadonlyMap<backendModule.AssetId, assetTreeNode.AssetTreeNode>>
|
||||
>
|
||||
doToggleDirectoryExpansion: (
|
||||
directoryId: backendModule.DirectoryId,
|
||||
key: backendModule.AssetId,
|
||||
@ -228,21 +232,18 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const [sortColumn, setSortColumn] = React.useState<columnModule.SortableColumn | null>(null)
|
||||
const [sortDirection, setSortDirection] = React.useState<sorting.SortDirection | null>(null)
|
||||
const [selectedKeys, setSelectedKeys] = React.useState(() => new Set<backendModule.AssetId>())
|
||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
const headerRowRef = React.useRef<HTMLTableRowElement>(null)
|
||||
const [, setQueuedAssetEvents] = React.useState<assetEventModule.AssetEvent[]>([])
|
||||
const [, setNameOfProjectToImmediatelyOpen] = React.useState(initialProjectName)
|
||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
const headerRowRef = React.useRef<HTMLTableRowElement>(null)
|
||||
const assetTreeRef = React.useRef<assetTreeNode.AssetTreeNode[]>([])
|
||||
const nodeMapRef = React.useRef<
|
||||
ReadonlyMap<backendModule.AssetId, assetTreeNode.AssetTreeNode>
|
||||
>(new Map<backendModule.AssetId, assetTreeNode.AssetTreeNode>())
|
||||
const rootDirectoryId = React.useMemo(
|
||||
() => backend.rootDirectoryId(organization),
|
||||
[backend, organization]
|
||||
)
|
||||
const nodeMap = React.useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
assetTreeNode.assetTreePreorderTraversal(assetTree).map(asset => [asset.key, asset])
|
||||
),
|
||||
[assetTree]
|
||||
)
|
||||
const filter = React.useMemo(() => {
|
||||
if (query === '') {
|
||||
return null
|
||||
@ -302,6 +303,13 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
}, [loadingProjectManagerDidFail, backend.type])
|
||||
|
||||
React.useEffect(() => {
|
||||
assetTreeRef.current = assetTree
|
||||
nodeMapRef.current = new Map(
|
||||
assetTreeNode.assetTreePreorderTraversal(assetTree).map(asset => [asset.key, asset])
|
||||
)
|
||||
}, [assetTree])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isLoading) {
|
||||
setNameOfProjectToImmediatelyOpen(initialProjectName)
|
||||
@ -502,7 +510,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
)
|
||||
const doToggleDirectoryExpansion = React.useCallback(
|
||||
(directoryId: backendModule.DirectoryId, key: backendModule.AssetId, title?: string) => {
|
||||
const directory = nodeMap.get(key)
|
||||
const directory = nodeMapRef.current.get(key)
|
||||
if (directory?.children != null) {
|
||||
const abortController = directoryListAbortControllersRef.current.get(directoryId)
|
||||
if (abortController != null) {
|
||||
@ -606,21 +614,22 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
})()
|
||||
}
|
||||
},
|
||||
[category, nodeMap, backend]
|
||||
[category, nodeMapRef, backend]
|
||||
)
|
||||
|
||||
const getNewProjectName = React.useCallback(
|
||||
(templateId: string | null, parentKey: backendModule.DirectoryId | null) => {
|
||||
const prefix = `${templateId ?? 'New_Project'}_`
|
||||
const projectNameTemplate = new RegExp(`^${prefix}(?<projectIndex>\\d+)$`)
|
||||
const siblings = parentKey == null ? assetTree : nodeMap.get(parentKey)?.children ?? []
|
||||
const siblings =
|
||||
parentKey == null ? assetTree : nodeMapRef.current.get(parentKey)?.children ?? []
|
||||
const projectIndices = siblings
|
||||
.filter(node => backendModule.assetIsProject(node.item))
|
||||
.map(node => projectNameTemplate.exec(node.item.title)?.groups?.projectIndex)
|
||||
.map(maybeIndex => (maybeIndex != null ? parseInt(maybeIndex, 10) : 0))
|
||||
return `${prefix}${Math.max(0, ...projectIndices) + 1}`
|
||||
},
|
||||
[assetTree, nodeMap]
|
||||
[assetTree, nodeMapRef]
|
||||
)
|
||||
|
||||
hooks.useEventHandler(assetListEvents, event => {
|
||||
@ -629,7 +638,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const siblings =
|
||||
event.parentKey == null
|
||||
? assetTree
|
||||
: nodeMap.get(event.parentKey)?.children ?? []
|
||||
: nodeMapRef.current.get(event.parentKey)?.children ?? []
|
||||
const directoryIndices = siblings
|
||||
.filter(node => backendModule.assetIsDirectory(node.item))
|
||||
.map(node => DIRECTORY_NAME_REGEX.exec(node.item.title))
|
||||
@ -650,7 +659,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
if (
|
||||
event.parentId != null &&
|
||||
event.parentKey != null &&
|
||||
nodeMap.get(event.parentKey)?.children == null
|
||||
nodeMapRef.current.get(event.parentKey)?.children == null
|
||||
) {
|
||||
doToggleDirectoryExpansion(event.parentId, event.parentKey)
|
||||
}
|
||||
@ -701,7 +710,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
if (
|
||||
event.parentId != null &&
|
||||
event.parentKey != null &&
|
||||
nodeMap.get(event.parentKey)?.children == null
|
||||
nodeMapRef.current.get(event.parentKey)?.children == null
|
||||
) {
|
||||
doToggleDirectoryExpansion(event.parentId, event.parentKey)
|
||||
}
|
||||
@ -767,7 +776,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
if (
|
||||
event.parentId != null &&
|
||||
event.parentKey != null &&
|
||||
nodeMap.get(event.parentKey)?.children == null
|
||||
nodeMapRef.current.get(event.parentKey)?.children == null
|
||||
) {
|
||||
doToggleDirectoryExpansion(event.parentId, event.parentKey)
|
||||
}
|
||||
@ -829,7 +838,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
if (
|
||||
event.parentId != null &&
|
||||
event.parentKey != null &&
|
||||
nodeMap.get(event.parentKey)?.children == null
|
||||
nodeMapRef.current.get(event.parentKey)?.children == null
|
||||
) {
|
||||
doToggleDirectoryExpansion(event.parentId, event.parentKey)
|
||||
}
|
||||
@ -884,7 +893,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
break
|
||||
}
|
||||
case assetListEventModule.AssetListEventType.closeFolder: {
|
||||
if (nodeMap.get(event.key)?.children != null) {
|
||||
if (nodeMapRef.current.get(event.key)?.children != null) {
|
||||
doToggleDirectoryExpansion(event.id, event.key)
|
||||
}
|
||||
break
|
||||
@ -928,6 +937,8 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
assetEvents,
|
||||
dispatchAssetEvent,
|
||||
dispatchAssetListEvent,
|
||||
topLevelAssets: assetTreeRef,
|
||||
nodeMap: nodeMapRef,
|
||||
doToggleDirectoryExpansion,
|
||||
doOpenManually,
|
||||
doOpenIde,
|
||||
@ -1015,7 +1026,8 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
backend.type === backendModule.BackendType.local ||
|
||||
(organization != null &&
|
||||
Array.from(innerSelectedKeys, key => {
|
||||
const userPermissions = nodeMap.get(key)?.item.permissions
|
||||
const userPermissions =
|
||||
nodeMapRef.current.get(key)?.item.permissions
|
||||
const selfPermission = userPermissions?.find(
|
||||
permission =>
|
||||
permission.user.user_email === organization.email
|
||||
|
@ -40,6 +40,8 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
numberOfSelectedItems,
|
||||
assetEvents,
|
||||
dispatchAssetListEvent,
|
||||
topLevelAssets,
|
||||
nodeMap,
|
||||
doToggleDirectoryExpansion,
|
||||
},
|
||||
rowState,
|
||||
@ -182,6 +184,20 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
)}
|
||||
<EditableSpan
|
||||
editable={rowState.isEditingName}
|
||||
checkSubmittable={newTitle =>
|
||||
(item.directoryKey != null
|
||||
? nodeMap.current.get(item.directoryKey)?.children ?? []
|
||||
: topLevelAssets.current
|
||||
).every(
|
||||
child =>
|
||||
// All siblings,
|
||||
child.key === item.key ||
|
||||
// that are directories,
|
||||
!backendModule.assetIsDirectory(child.item) ||
|
||||
// must have a different name.
|
||||
child.item.title !== newTitle
|
||||
)
|
||||
}
|
||||
onSubmit={async newTitle => {
|
||||
setRowState(oldRowState => ({
|
||||
...oldRowState,
|
||||
|
@ -17,6 +17,7 @@ type EditableSpanPassthroughProps = JSX.IntrinsicElements['input'] & JSX.Intrins
|
||||
/** Props for an {@link EditableSpan}. */
|
||||
export interface EditableSpanProps extends Omit<EditableSpanPassthroughProps, 'onSubmit'> {
|
||||
editable?: boolean
|
||||
checkSubmittable?: (value: string) => boolean
|
||||
onSubmit: (value: string) => void
|
||||
onCancel: () => void
|
||||
inputPattern?: string
|
||||
@ -28,6 +29,7 @@ export interface EditableSpanProps extends Omit<EditableSpanPassthroughProps, 'o
|
||||
export default function EditableSpan(props: EditableSpanProps) {
|
||||
const {
|
||||
editable = false,
|
||||
checkSubmittable,
|
||||
children,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
@ -36,9 +38,15 @@ export default function EditableSpan(props: EditableSpanProps) {
|
||||
...passthroughProps
|
||||
} = props
|
||||
const { shortcuts } = shortcutsProvider.useShortcuts()
|
||||
|
||||
const [isSubmittable, setIsSubmittable] = React.useState(true)
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsSubmittable(checkSubmittable?.(inputRef.current?.value ?? '') ?? true)
|
||||
// This effect MUST only run on mount.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (editable) {
|
||||
return shortcuts.registerKeyboardHandlers({
|
||||
@ -58,8 +66,10 @@ export default function EditableSpan(props: EditableSpanProps) {
|
||||
className="flex grow"
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
if (inputRef.current != null) {
|
||||
onSubmit(inputRef.current.value)
|
||||
if (isSubmittable) {
|
||||
if (inputRef.current != null) {
|
||||
onSubmit(inputRef.current.value)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -70,14 +80,24 @@ export default function EditableSpan(props: EditableSpanProps) {
|
||||
size={1}
|
||||
defaultValue={children}
|
||||
onBlur={event => event.currentTarget.form?.requestSubmit()}
|
||||
{...(inputPattern != null ? { pattern: inputPattern } : {})}
|
||||
{...(inputTitle != null ? { title: inputTitle } : {})}
|
||||
{...(inputPattern == null ? {} : { pattern: inputPattern })}
|
||||
{...(inputTitle == null ? {} : { title: inputTitle })}
|
||||
{...(checkSubmittable == null
|
||||
? {}
|
||||
: {
|
||||
onInput: event => {
|
||||
setIsSubmittable(checkSubmittable(event.currentTarget.value))
|
||||
},
|
||||
})}
|
||||
{...passthroughProps}
|
||||
/>
|
||||
<button type="submit" className="mx-0.5">
|
||||
<img src={TickIcon} />
|
||||
</button>
|
||||
{isSubmittable && (
|
||||
<button type="submit" className="mx-0.5">
|
||||
<img src={TickIcon} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="mx-0.5"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
|
@ -46,6 +46,8 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
assetEvents,
|
||||
dispatchAssetEvent,
|
||||
dispatchAssetListEvent,
|
||||
topLevelAssets,
|
||||
nodeMap,
|
||||
doOpenManually,
|
||||
doOpenIde,
|
||||
doCloseIde,
|
||||
@ -288,6 +290,20 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
)}
|
||||
<EditableSpan
|
||||
editable={rowState.isEditingName}
|
||||
checkSubmittable={newTitle =>
|
||||
(item.directoryKey != null
|
||||
? nodeMap.current.get(item.directoryKey)?.children ?? []
|
||||
: topLevelAssets.current
|
||||
).every(
|
||||
child =>
|
||||
// All siblings,
|
||||
child.key === item.key ||
|
||||
// that are not directories,
|
||||
backendModule.assetIsDirectory(child.item) ||
|
||||
// must have a different name.
|
||||
child.item.title !== newTitle
|
||||
)
|
||||
}
|
||||
onSubmit={async newTitle => {
|
||||
setRowState(oldRowState => ({
|
||||
...oldRowState,
|
||||
|
Loading…
Reference in New Issue
Block a user