Skip to content

Commit e7bc796

Browse files
authored
Make Asset Table selection backend specific (#11288)
- Fix enso-org/cloud-v2#1542 - Ignore selection (and clear target directory) when the selection is for a different backend # Important Notes None
1 parent d75e20c commit e7bc796

File tree

11 files changed

+383
-208
lines changed

11 files changed

+383
-208
lines changed

app/common/src/text/english.json

+2
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,8 @@
629629
"slsaLicenseAgreementDescription1": "This Order is governed by the Software License and Service Agreement found at",
630630
"slsaLicenseAgreementDescription2": ", (the “Agreement”). All capitalized terms used in this Customer Order but not otherwise defined herein shall have the meanings set forth in the Agreement.\n Except as expressly provided in the Agreement, Products and Services purchased under this Customer Order are non-cancelable and non-refundable.",
631631
"downgradeInfo": "to downgrade",
632+
"xItemsCopied": "$0 item(s) copied",
633+
"xItemsCut": "$0 item(s) cut",
632634

633635
"someAgreementsHaveBeenUpdated": "Some agreements have been updated. Please re-read and agree to continue.",
634636
"licenseAgreementTitle": "Enso Terms of Service",

app/common/src/text/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ interface PlaceholderOverrides {
139139
readonly trialDescription: [days: number]
140140
readonly groupNameSettingsInputDescription: [howLong: number]
141141
readonly xIsUsingTheProject: [userName: string]
142+
readonly xItemsCopied: [count: number]
143+
readonly xItemsCut: [count: number]
142144

143145
readonly arbitraryFieldTooLarge: [maxSize: string]
144146
readonly arbitraryFieldTooSmall: [minSize: string]

app/gui/src/dashboard/layouts/AssetContextMenu.tsx

+29-25
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ import ManagePermissionsModal from '#/modals/ManagePermissionsModal'
3636
import * as backendModule from '#/services/Backend'
3737
import * as localBackendModule from '#/services/LocalBackend'
3838

39+
import { backendMutationOptions } from '#/hooks/backendHooks'
3940
import {
41+
usePasteData,
4042
useSetAssetPanelProps,
4143
useSetIsAssetPanelTemporarilyVisible,
4244
} from '#/providers/DriveProvider'
@@ -70,7 +72,7 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
7072
const { innerProps, rootDirectoryId, event, eventTarget, hidden = false } = props
7173
const { doCopy, doCut, doPaste, doDelete } = props
7274
const { item, setItem, state, setRowState } = innerProps
73-
const { backend, category, hasPasteData, pasteData, nodeMap } = state
75+
const { backend, category, nodeMap } = state
7476

7577
const { user } = authProvider.useFullUserSession()
7678
const { setModal } = modalProvider.useSetModal()
@@ -99,6 +101,9 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
99101
: isCloud ? encodeURI(pathRaw)
100102
: pathRaw
101103
const copyMutation = copyHooks.useCopy({ copyText: path ?? '' })
104+
const uploadFileMutation = reactQuery.useMutation(
105+
backendMutationOptions(remoteBackend, 'uploadFile'),
106+
)
102107

103108
const { isFeatureUnderPaywall } = billingHooks.usePaywall({ plan: user.plan })
104109
const isUnderPaywall = isFeatureUnderPaywall('share')
@@ -112,8 +117,10 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
112117
category.type !== 'recent' &&
113118
asset.type === backendModule.AssetType.directory &&
114119
canEditThisAsset
120+
const pasteData = usePasteData()
121+
const hasPasteData = (pasteData?.data.ids.size ?? 0) > 0
115122
const pasteDataParentKeys =
116-
!pasteData.current ? null : (
123+
!pasteData ? null : (
117124
new Map(
118125
Array.from(nodeMap.current.entries()).map(([id, otherAsset]) => [
119126
id,
@@ -122,9 +129,9 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
122129
)
123130
)
124131
const canPaste =
125-
!pasteData.current || !pasteDataParentKeys || !isCloud ?
132+
!pasteData || !pasteDataParentKeys || !isCloud ?
126133
true
127-
: !Array.from(pasteData.current.data).some((assetId) => {
134+
: !Array.from(pasteData.data.ids).some((assetId) => {
128135
const parentKey = pasteDataParentKeys.get(assetId)
129136
const parent = parentKey == null ? null : nodeMap.current.get(parentKey)
130137
return !parent ? true : (
@@ -156,6 +163,20 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
156163

157164
const setAsset = setAssetHooks.useSetAsset(asset, setItem)
158165

166+
const pasteMenuEntry = hasPasteData && canPaste && (
167+
<ContextMenuEntry
168+
hidden={hidden}
169+
action="paste"
170+
doAction={() => {
171+
const [directoryKey, directoryId] =
172+
item.type === backendModule.AssetType.directory ?
173+
[item.key, item.item.id]
174+
: [item.directoryKey, item.directoryId]
175+
doPaste(directoryKey, directoryId)
176+
}}
177+
/>
178+
)
179+
159180
return (
160181
category.type === 'trash' ?
161182
!ownsThisAsset ? null
@@ -186,6 +207,7 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
186207
)
187208
}}
188209
/>
210+
{pasteMenuEntry}
189211
</ContextMenu>
190212
</ContextMenus>
191213
: <ContextMenus hidden={hidden} key={asset.id} event={event}>
@@ -276,19 +298,14 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
276298
const projectResponse = await fetch(
277299
`./api/project-manager/projects/${localBackendModule.extractTypeAndId(asset.id).id}/enso-project`,
278300
)
279-
// This DOES NOT update the cloud assets list when it
280-
// completes, as the current backend is not the remote
281-
// (cloud) backend. The user may change to the cloud backend
282-
// while this request is in progress, however this is
283-
// uncommon enough that it is not worth the added complexity.
284-
await remoteBackend.uploadFile(
301+
await uploadFileMutation.mutateAsync([
285302
{
286303
fileName: `${asset.title}.enso-project`,
287304
fileId: null,
288305
parentDirectoryId: null,
289306
},
290307
await projectResponse.blob(),
291-
)
308+
])
292309
toast.toast.success(getText('uploadProjectToCloudSuccess'))
293310
} catch (error) {
294311
toastAndLog('uploadProjectToCloudError', error)
@@ -467,25 +484,12 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
467484
}}
468485
/>
469486
)}
470-
{hasPasteData && canPaste && (
471-
<ContextMenuEntry
472-
hidden={hidden}
473-
action="paste"
474-
doAction={() => {
475-
const [directoryKey, directoryId] =
476-
item.type === backendModule.AssetType.directory ?
477-
[item.key, item.item.id]
478-
: [item.directoryKey, item.directoryId]
479-
doPaste(directoryKey, directoryId)
480-
}}
481-
/>
482-
)}
487+
{pasteMenuEntry}
483488
</ContextMenu>
484489
{canAddToThisDirectory && (
485490
<GlobalContextMenu
486491
hidden={hidden}
487492
backend={backend}
488-
hasPasteData={hasPasteData}
489493
rootDirectoryId={rootDirectoryId}
490494
directoryKey={
491495
// This is SAFE, as both branches are guaranteed to be `DirectoryId`s

app/gui/src/dashboard/layouts/AssetsTable.tsx

+36-32
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,12 @@ import useOnScroll from '#/hooks/useOnScroll'
6464
import type * as assetSearchBar from '#/layouts/AssetSearchBar'
6565
import * as eventListProvider from '#/layouts/AssetsTable/EventListProvider'
6666
import AssetsTableContextMenu from '#/layouts/AssetsTableContextMenu'
67-
import { isLocalCategory, type Category } from '#/layouts/CategorySwitcher/Category'
67+
import {
68+
canTransferBetweenCategories,
69+
isLocalCategory,
70+
useTransferBetweenCategories,
71+
type Category,
72+
} from '#/layouts/CategorySwitcher/Category'
6873
import DragModal from '#/modals/DragModal'
6974
import DuplicateAssetsModal from '#/modals/DuplicateAssetsModal'
7075
import UpsertSecretModal from '#/modals/UpsertSecretModal'
@@ -81,6 +86,7 @@ import {
8186
useSetCanDownload,
8287
useSetIsAssetPanelTemporarilyVisible,
8388
useSetNewestFolderId,
89+
useSetPasteData,
8490
useSetSelectedKeys,
8591
useSetSuggestions,
8692
useSetTargetDirectory,
@@ -141,7 +147,6 @@ import { fileExtension } from '#/utilities/fileInfo'
141147
import type { DetailedRectangle } from '#/utilities/geometry'
142148
import { DEFAULT_HANDLER } from '#/utilities/inputBindings'
143149
import LocalStorage from '#/utilities/LocalStorage'
144-
import type { PasteData } from '#/utilities/pasteData'
145150
import PasteType from '#/utilities/PasteType'
146151
import {
147152
canPermissionModifyDirectoryContents,
@@ -309,14 +314,11 @@ export interface AssetsTableState {
309314
readonly scrollContainerRef: RefObject<HTMLElement>
310315
readonly visibilities: ReadonlyMap<AssetId, Visibility>
311316
readonly category: Category
312-
readonly hasPasteData: boolean
313-
readonly setPasteData: (pasteData: PasteData<Set<AssetId>>) => void
314317
readonly sortInfo: SortInfo<SortableColumn> | null
315318
readonly setSortInfo: (sortInfo: SortInfo<SortableColumn> | null) => void
316319
readonly query: AssetQuery
317320
readonly setQuery: Dispatch<SetStateAction<AssetQuery>>
318321
readonly nodeMap: Readonly<MutableRefObject<ReadonlyMap<AssetId, AnyAssetTreeNode>>>
319-
readonly pasteData: Readonly<MutableRefObject<PasteData<ReadonlySet<AssetId>> | null>>
320322
readonly hideColumn: (column: Column) => void
321323
readonly doToggleDirectoryExpansion: (
322324
directoryId: DirectoryId,
@@ -396,7 +398,7 @@ export default function AssetsTable(props: AssetsTableProps) {
396398
const setSelectedKeys = useSetSelectedKeys()
397399
const setVisuallySelectedKeys = useSetVisuallySelectedKeys()
398400
const updateAssetRef = useRef<Record<AnyAsset['id'], (asset: AnyAsset) => void>>({})
399-
const [pasteData, setPasteData] = useState<PasteData<ReadonlySet<AssetId>> | null>(null)
401+
const setPasteData = useSetPasteData()
400402

401403
const { data: users } = useBackendQuery(backend, 'listUsers', [])
402404
const { data: userGroups } = useBackendQuery(backend, 'listUserGroups', [])
@@ -864,7 +866,6 @@ export default function AssetsTable(props: AssetsTableProps) {
864866
const lastSelectedIdsRef = useRef<AssetId | ReadonlySet<AssetId> | null>(null)
865867
const headerRowRef = useRef<HTMLTableRowElement>(null)
866868
const assetTreeRef = useRef<AnyAssetTreeNode>(assetTree)
867-
const pasteDataRef = useRef<PasteData<ReadonlySet<AssetId>> | null>(null)
868869
const nodeMapRef = useRef<ReadonlyMap<AssetId, AnyAssetTreeNode>>(
869870
new Map<AssetId, AnyAssetTreeNode>(),
870871
)
@@ -1141,25 +1142,22 @@ export default function AssetsTable(props: AssetsTableProps) {
11411142
nodeMapRef.current = newNodeMap
11421143
}, [assetTree])
11431144

1144-
useEffect(() => {
1145-
pasteDataRef.current = pasteData
1146-
}, [pasteData])
1147-
11481145
useEffect(() => {
11491146
if (!hidden) {
11501147
return inputBindings.attach(document.body, 'keydown', {
11511148
cancelCut: () => {
1152-
if (pasteDataRef.current == null) {
1149+
const { pasteData } = driveStore.getState()
1150+
if (pasteData == null) {
11531151
return false
11541152
} else {
1155-
dispatchAssetEvent({ type: AssetEventType.cancelCut, ids: pasteDataRef.current.data })
1153+
dispatchAssetEvent({ type: AssetEventType.cancelCut, ids: pasteData.data.ids })
11561154
setPasteData(null)
11571155
return
11581156
}
11591157
},
11601158
})
11611159
}
1162-
}, [hidden, inputBindings, dispatchAssetEvent])
1160+
}, [dispatchAssetEvent, driveStore, hidden, inputBindings, setPasteData])
11631161

11641162
useEffect(
11651163
() =>
@@ -2145,29 +2143,40 @@ export default function AssetsTable(props: AssetsTableProps) {
21452143
const doCopy = useEventCallback(() => {
21462144
unsetModal()
21472145
const { selectedKeys } = driveStore.getState()
2148-
setPasteData({ type: PasteType.copy, data: selectedKeys })
2146+
setPasteData({
2147+
type: PasteType.copy,
2148+
data: { backendType: backend.type, category, ids: selectedKeys },
2149+
})
21492150
})
21502151

21512152
const doCut = useEventCallback(() => {
21522153
unsetModal()
2154+
const { selectedKeys, pasteData } = driveStore.getState()
21532155
if (pasteData != null) {
2154-
dispatchAssetEvent({ type: AssetEventType.cancelCut, ids: pasteData.data })
2156+
dispatchAssetEvent({ type: AssetEventType.cancelCut, ids: pasteData.data.ids })
21552157
}
2156-
const { selectedKeys } = driveStore.getState()
2157-
setPasteData({ type: PasteType.move, data: selectedKeys })
2158+
setPasteData({
2159+
type: PasteType.move,
2160+
data: { backendType: backend.type, category, ids: selectedKeys },
2161+
})
21582162
dispatchAssetEvent({ type: AssetEventType.cut, ids: selectedKeys })
21592163
setSelectedKeys(EMPTY_SET)
21602164
})
21612165

2166+
const transferBetweenCategories = useTransferBetweenCategories(category)
21622167
const doPaste = useEventCallback((newParentKey: DirectoryId, newParentId: DirectoryId) => {
21632168
unsetModal()
2164-
if (pasteData != null) {
2165-
if (pasteData.data.has(newParentKey)) {
2169+
const { pasteData } = driveStore.getState()
2170+
if (
2171+
pasteData?.data.backendType === backend.type &&
2172+
canTransferBetweenCategories(pasteData.data.category, category)
2173+
) {
2174+
if (pasteData.data.ids.has(newParentKey)) {
21662175
toast.error('Cannot paste a folder into itself.')
21672176
} else {
21682177
doToggleDirectoryExpansion(newParentId, newParentKey, true)
21692178
if (pasteData.type === PasteType.copy) {
2170-
const assets = Array.from(pasteData.data, (id) => nodeMapRef.current.get(id)).flatMap(
2179+
const assets = Array.from(pasteData.data.ids, (id) => nodeMapRef.current.get(id)).flatMap(
21712180
(asset) => (asset ? [asset.item] : []),
21722181
)
21732182
dispatchAssetListEvent({
@@ -2177,12 +2186,13 @@ export default function AssetsTable(props: AssetsTableProps) {
21772186
newParentKey,
21782187
})
21792188
} else {
2180-
dispatchAssetEvent({
2181-
type: AssetEventType.move,
2182-
ids: pasteData.data,
2189+
transferBetweenCategories(
2190+
pasteData.data.category,
2191+
category,
2192+
pasteData.data.ids,
21832193
newParentKey,
21842194
newParentId,
2185-
})
2195+
)
21862196
}
21872197
setPasteData(null)
21882198
}
@@ -2207,7 +2217,6 @@ export default function AssetsTable(props: AssetsTableProps) {
22072217
hidden
22082218
backend={backend}
22092219
category={category}
2210-
pasteData={pasteData}
22112220
nodeMapRef={nodeMapRef}
22122221
rootDirectoryId={rootDirectoryId}
22132222
event={{ pageX: 0, pageY: 0 }}
@@ -2217,7 +2226,7 @@ export default function AssetsTable(props: AssetsTableProps) {
22172226
doDelete={doDeleteById}
22182227
/>
22192228
),
2220-
[backend, category, pasteData, rootDirectoryId, doCopy, doCut, doPaste, doDeleteById],
2229+
[backend, category, rootDirectoryId, doCopy, doCut, doPaste, doDeleteById],
22212230
)
22222231

22232232
const onDropzoneDragOver = (event: DragEvent<Element>) => {
@@ -2260,14 +2269,11 @@ export default function AssetsTable(props: AssetsTableProps) {
22602269
visibilities,
22612270
scrollContainerRef: rootRef,
22622271
category,
2263-
hasPasteData: pasteData != null,
2264-
setPasteData,
22652272
sortInfo,
22662273
setSortInfo,
22672274
query,
22682275
setQuery,
22692276
nodeMap: nodeMapRef,
2270-
pasteData: pasteDataRef,
22712277
hideColumn,
22722278
doToggleDirectoryExpansion,
22732279
doCopy,
@@ -2283,7 +2289,6 @@ export default function AssetsTable(props: AssetsTableProps) {
22832289
rootDirectoryId,
22842290
visibilities,
22852291
category,
2286-
pasteData,
22872292
sortInfo,
22882293
query,
22892294
doToggleDirectoryExpansion,
@@ -2762,7 +2767,6 @@ export default function AssetsTable(props: AssetsTableProps) {
27622767
<AssetsTableContextMenu
27632768
backend={backend}
27642769
category={category}
2765-
pasteData={pasteData}
27662770
nodeMapRef={nodeMapRef}
27672771
event={event}
27682772
rootDirectoryId={rootDirectoryId}

0 commit comments

Comments
 (0)