mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 21:12:44 +03:00
Fix dashboard tests (#9050)
Fixes issues that were causing CI to fail. # Important Notes None
This commit is contained in:
parent
a25d716932
commit
50d5f32570
@ -589,6 +589,17 @@ export function getAssetRowLeftPx(locator: test.Locator) {
|
||||
return locator.evaluate(el => el.children[0]?.children[0]?.getBoundingClientRect().left ?? 0)
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// === expect functions for themes ===
|
||||
// ===================================
|
||||
|
||||
/** A test assertion to confirm that the element has the class `selected`. */
|
||||
export async function expectClassSelected(locator: test.Locator) {
|
||||
await test.test.step('Expect `selected`', async () => {
|
||||
await test.expect(locator).toHaveClass(/(?:^| )selected(?: |$)/)
|
||||
})
|
||||
}
|
||||
|
||||
// ============================
|
||||
// === expectPlaceholderRow ===
|
||||
// ============================
|
||||
|
@ -137,9 +137,14 @@ test.test('cut (keyboard)', async ({ page }) => {
|
||||
await actions.locateNewFolderIcon(page).click()
|
||||
await assetRows.nth(0).click()
|
||||
await actions.press(page, 'Mod+X')
|
||||
test
|
||||
.expect(await assetRows.nth(0).evaluate(el => Number(getComputedStyle(el).opacity)))
|
||||
.toBeLessThan(1)
|
||||
// This action is not a builtin `expect` action, so it needs to be manually retried.
|
||||
await test
|
||||
.expect(async () => {
|
||||
test
|
||||
.expect(await assetRows.nth(0).evaluate(el => Number(getComputedStyle(el).opacity)))
|
||||
.toBeLessThan(1)
|
||||
})
|
||||
.toPass()
|
||||
})
|
||||
|
||||
test.test('duplicate', async ({ page }) => {
|
||||
|
@ -18,6 +18,7 @@ export default test.defineConfig({
|
||||
toHaveScreenshot: { threshold: 0 },
|
||||
timeout: 30_000,
|
||||
},
|
||||
timeout: 30_000,
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080',
|
||||
launchOptions: {
|
||||
|
@ -56,14 +56,14 @@ export default function DragModal(props: DragModalProps) {
|
||||
unsetModal()
|
||||
}
|
||||
// Update position (non-FF)
|
||||
document.addEventListener('drag', onDrag)
|
||||
document.addEventListener('drag', onDrag, { capture: true })
|
||||
// Update position (FF)
|
||||
document.addEventListener('dragover', onDrag)
|
||||
document.addEventListener('dragend', onDragEnd)
|
||||
document.addEventListener('dragover', onDrag, { capture: true })
|
||||
document.addEventListener('dragend', onDragEnd, { capture: true })
|
||||
return () => {
|
||||
document.removeEventListener('drag', onDrag)
|
||||
document.removeEventListener('dragover', onDrag)
|
||||
document.removeEventListener('dragend', onDragEnd)
|
||||
document.removeEventListener('drag', onDrag, { capture: true })
|
||||
document.removeEventListener('dragover', onDrag, { capture: true })
|
||||
document.removeEventListener('dragend', onDragEnd, { capture: true })
|
||||
}
|
||||
// `doCleanup` is a callback, not a dependency.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -39,7 +39,7 @@ export interface DirectoryNameColumnProps extends column.AssetColumnProps {}
|
||||
* This should never happen. */
|
||||
export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
const { item, setItem, selected, state, rowState, setRowState } = props
|
||||
const { numberOfSelectedItems, assetEvents, dispatchAssetListEvent, nodeMap } = state
|
||||
const { selectedKeys, assetEvents, dispatchAssetListEvent, nodeMap } = state
|
||||
const { doToggleDirectoryExpansion } = state
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
@ -137,7 +137,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
onClick={event => {
|
||||
if (
|
||||
eventModule.isSingleClick(event) &&
|
||||
((selected && numberOfSelectedItems === 1) ||
|
||||
((selected && selectedKeys.current.size === 1) ||
|
||||
shortcutManager.matchesMouseAction(shortcutManagerModule.MouseAction.editName, event))
|
||||
) {
|
||||
event.stopPropagation()
|
||||
|
@ -42,7 +42,7 @@ export interface ProjectNameColumnProps extends column.AssetColumnProps {}
|
||||
* This should never happen. */
|
||||
export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
const { item, setItem, selected, rowState, setRowState, state } = props
|
||||
const { numberOfSelectedItems, assetEvents, dispatchAssetEvent, dispatchAssetListEvent } = state
|
||||
const { selectedKeys, assetEvents, dispatchAssetEvent, dispatchAssetListEvent } = state
|
||||
const { nodeMap, doOpenManually, doOpenIde, doCloseIde } = state
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
@ -277,7 +277,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
} else if (
|
||||
!isRunning &&
|
||||
eventModule.isSingleClick(event) &&
|
||||
((selected && numberOfSelectedItems === 1) ||
|
||||
((selected && selectedKeys.current.size === 1) ||
|
||||
shortcutManager.matchesMouseAction(shortcutManagerModule.MouseAction.editName, event))
|
||||
) {
|
||||
setRowState(object.merger({ isEditingName: true }))
|
||||
|
@ -73,12 +73,12 @@ export interface AssetNewFolderEvent extends AssetBaseEvent<AssetEventType.newFo
|
||||
|
||||
/** A signal to upload files. */
|
||||
export interface AssetUploadFilesEvent extends AssetBaseEvent<AssetEventType.uploadFiles> {
|
||||
readonly files: Map<backendModule.AssetId, File>
|
||||
readonly files: ReadonlyMap<backendModule.AssetId, File>
|
||||
}
|
||||
|
||||
/** A signal to update files with new versions. */
|
||||
export interface AssetUpdateFilesEvent extends AssetBaseEvent<AssetEventType.updateFiles> {
|
||||
readonly files: Map<backendModule.AssetId, File>
|
||||
readonly files: ReadonlyMap<backendModule.AssetId, File>
|
||||
}
|
||||
|
||||
/** A signal to create a Data Link. */
|
||||
@ -112,41 +112,41 @@ export interface AssetCancelOpeningAllProjectsEvent
|
||||
/** A signal that multiple assets should be copied. `ids` are the `Id`s of the newly created
|
||||
* placeholder items. */
|
||||
export interface AssetCopyEvent extends AssetBaseEvent<AssetEventType.copy> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly newParentKey: backendModule.AssetId
|
||||
readonly newParentId: backendModule.DirectoryId
|
||||
}
|
||||
|
||||
/** A signal to cut multiple assets. */
|
||||
export interface AssetCutEvent extends AssetBaseEvent<AssetEventType.cut> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
}
|
||||
|
||||
/** A signal that a cut operation has been cancelled. */
|
||||
export interface AssetCancelCutEvent extends AssetBaseEvent<AssetEventType.cancelCut> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to move multiple assets. */
|
||||
export interface AssetMoveEvent extends AssetBaseEvent<AssetEventType.move> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly newParentKey: backendModule.AssetId
|
||||
readonly newParentId: backendModule.DirectoryId
|
||||
}
|
||||
|
||||
/** A signal to delete assets. */
|
||||
export interface AssetDeleteEvent extends AssetBaseEvent<AssetEventType.delete> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to restore assets from trash. */
|
||||
export interface AssetRestoreEvent extends AssetBaseEvent<AssetEventType.restore> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to download assets. */
|
||||
export interface AssetDownloadEvent extends AssetBaseEvent<AssetEventType.download> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to download the currently selected assets. */
|
||||
@ -161,26 +161,26 @@ export interface AssetRemoveSelfEvent extends AssetBaseEvent<AssetEventType.remo
|
||||
/** A signal to temporarily add labels to the selected assetss. */
|
||||
export interface AssetTemporarilyAddLabelsEvent
|
||||
extends AssetBaseEvent<AssetEventType.temporarilyAddLabels> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly labelNames: ReadonlySet<backendModule.LabelName>
|
||||
}
|
||||
|
||||
/** A signal to temporarily remove labels from the selected assetss. */
|
||||
export interface AssetTemporarilyRemoveLabelsEvent
|
||||
extends AssetBaseEvent<AssetEventType.temporarilyRemoveLabels> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly labelNames: ReadonlySet<backendModule.LabelName>
|
||||
}
|
||||
|
||||
/** A signal to add labels to the selected assetss. */
|
||||
export interface AssetAddLabelsEvent extends AssetBaseEvent<AssetEventType.addLabels> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly labelNames: ReadonlySet<backendModule.LabelName>
|
||||
}
|
||||
|
||||
/** A signal to remove labels from the selected assetss. */
|
||||
export interface AssetRemoveLabelsEvent extends AssetBaseEvent<AssetEventType.removeLabels> {
|
||||
readonly ids: Set<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly labelNames: ReadonlySet<backendModule.LabelName>
|
||||
}
|
||||
|
||||
|
@ -250,7 +250,7 @@ const CATEGORY_TO_FILTER_BY: Readonly<Record<Category, backendModule.FilterBy |
|
||||
|
||||
/** State passed through from a {@link AssetsTable} to every cell. */
|
||||
export interface AssetsTableState {
|
||||
readonly numberOfSelectedItems: number
|
||||
readonly selectedKeys: React.MutableRefObject<ReadonlySet<backendModule.AssetId>>
|
||||
readonly visibilities: ReadonlyMap<backendModule.AssetId, Visibility>
|
||||
readonly category: Category
|
||||
readonly labels: Map<backendModule.LabelName, backendModule.Label>
|
||||
@ -352,9 +352,12 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const [extraColumns, setExtraColumns] = React.useState(() => new Set<columnUtils.ExtraColumn>())
|
||||
const [sortColumn, setSortColumn] = React.useState<columnUtils.SortableColumn | null>(null)
|
||||
const [sortDirection, setSortDirection] = React.useState<SortDirection | null>(null)
|
||||
const [selectedKeys, setSelectedKeys] = React.useState(() => new Set<backendModule.AssetId>())
|
||||
const [selectedKeys, setSelectedKeysRaw] = React.useState<ReadonlySet<backendModule.AssetId>>(
|
||||
() => new Set()
|
||||
)
|
||||
const selectedKeysRef = React.useRef(selectedKeys)
|
||||
const [pasteData, setPasteData] = React.useState<pasteDataModule.PasteData<
|
||||
Set<backendModule.AssetId>
|
||||
ReadonlySet<backendModule.AssetId>
|
||||
> | null>(null)
|
||||
const [, setQueuedAssetEvents] = React.useState<assetEvent.AssetEvent[]>([])
|
||||
const [, setNameOfProjectToImmediatelyOpen] = React.useState(initialProjectName)
|
||||
@ -381,9 +384,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
const headerRowRef = React.useRef<HTMLTableRowElement>(null)
|
||||
const assetTreeRef = React.useRef<AssetTreeNode>(assetTree)
|
||||
const pasteDataRef = React.useRef<pasteDataModule.PasteData<Set<backendModule.AssetId>> | null>(
|
||||
null
|
||||
)
|
||||
const pasteDataRef = React.useRef<pasteDataModule.PasteData<
|
||||
ReadonlySet<backendModule.AssetId>
|
||||
> | null>(null)
|
||||
const nodeMapRef = React.useRef<ReadonlyMap<backendModule.AssetId, AssetTreeNode>>(
|
||||
new Map<backendModule.AssetId, AssetTreeNode>()
|
||||
)
|
||||
@ -575,17 +578,17 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
if (category === Category.trash) {
|
||||
setCanDownloadFiles(false)
|
||||
} else if (!isCloud) {
|
||||
setCanDownloadFiles(selectedKeys.size !== 0)
|
||||
setCanDownloadFiles(selectedKeysRef.current.size !== 0)
|
||||
} else {
|
||||
setCanDownloadFiles(
|
||||
selectedKeys.size !== 0 &&
|
||||
Array.from(selectedKeys).every(key => {
|
||||
selectedKeysRef.current.size !== 0 &&
|
||||
Array.from(selectedKeysRef.current).every(key => {
|
||||
const node = nodeMapRef.current.get(key)
|
||||
return node?.item.type === backendModule.AssetType.file
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [category, selectedKeys, isCloud, /* should never change */ setCanDownloadFiles])
|
||||
}, [category, isCloud, /* should never change */ setCanDownloadFiles])
|
||||
|
||||
React.useEffect(() => {
|
||||
const nodeToSuggestion = (
|
||||
@ -792,10 +795,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
if (pasteDataRef.current == null) {
|
||||
return false
|
||||
} else {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.cancelCut,
|
||||
ids: pasteDataRef.current.data,
|
||||
})
|
||||
dispatchAssetEvent({ type: AssetEventType.cancelCut, ids: pasteDataRef.current.data })
|
||||
setPasteData(null)
|
||||
return
|
||||
}
|
||||
@ -833,6 +833,18 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialProjectName])
|
||||
|
||||
const setSelectedKeys = React.useCallback(
|
||||
(newSelectedKeys: ReadonlySet<backendModule.AssetId>) => {
|
||||
selectedKeysRef.current = newSelectedKeys
|
||||
setSelectedKeysRaw(newSelectedKeys)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const clearSelectedKeys = React.useCallback(() => {
|
||||
setSelectedKeys(new Set())
|
||||
}, [/* should never change */ setSelectedKeys])
|
||||
|
||||
const overwriteNodes = React.useCallback(
|
||||
(newAssets: backendModule.AnyAsset[]) => {
|
||||
// This is required, otherwise we are using an outdated
|
||||
@ -1057,10 +1069,10 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}, [extraColumns, initialized, /* should never change */ localStorage])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedKeys.size !== 1) {
|
||||
if (selectedKeysRef.current.size !== 1) {
|
||||
setAssetPanelProps(null)
|
||||
}
|
||||
}, [selectedKeys.size, /* should never change */ setAssetPanelProps])
|
||||
}, [selectedKeysRef.current.size, /* should never change */ setAssetPanelProps])
|
||||
|
||||
const directoryListAbortControllersRef = React.useRef(
|
||||
new Map<backendModule.DirectoryId, AbortController>()
|
||||
@ -1474,12 +1486,10 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
break
|
||||
}
|
||||
case AssetListEventType.willDelete: {
|
||||
if (selectedKeys.has(event.key)) {
|
||||
setSelectedKeys(oldSelectedKeys => {
|
||||
const newSelectedKeys = new Set(oldSelectedKeys)
|
||||
newSelectedKeys.delete(event.key)
|
||||
return newSelectedKeys
|
||||
})
|
||||
if (selectedKeysRef.current.has(event.key)) {
|
||||
const newSelectedKeys = new Set(selectedKeysRef.current)
|
||||
newSelectedKeys.delete(event.key)
|
||||
setSelectedKeys(newSelectedKeys)
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -1548,34 +1558,20 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
|
||||
const doCopy = React.useCallback(() => {
|
||||
unsetModal()
|
||||
setSelectedKeys(oldSelectedKeys => {
|
||||
queueMicrotask(() => {
|
||||
setPasteData({ type: PasteType.copy, data: oldSelectedKeys })
|
||||
})
|
||||
return oldSelectedKeys
|
||||
})
|
||||
setPasteData({ type: PasteType.copy, data: selectedKeysRef.current })
|
||||
}, [/* should never change */ unsetModal])
|
||||
|
||||
const doCut = React.useCallback(() => {
|
||||
unsetModal()
|
||||
setSelectedKeys(oldSelectedKeys => {
|
||||
queueMicrotask(() => {
|
||||
if (pasteData != null) {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.cancelCut,
|
||||
ids: pasteData.data,
|
||||
})
|
||||
}
|
||||
setPasteData({ type: PasteType.move, data: oldSelectedKeys })
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.cut,
|
||||
ids: oldSelectedKeys,
|
||||
})
|
||||
})
|
||||
return new Set()
|
||||
})
|
||||
if (pasteData != null) {
|
||||
dispatchAssetEvent({ type: AssetEventType.cancelCut, ids: pasteData.data })
|
||||
}
|
||||
setPasteData({ type: PasteType.move, data: selectedKeysRef.current })
|
||||
dispatchAssetEvent({ type: AssetEventType.cut, ids: selectedKeysRef.current })
|
||||
setSelectedKeys(new Set())
|
||||
}, [
|
||||
pasteData,
|
||||
/* should never change */ setSelectedKeys,
|
||||
/* should never change */ unsetModal,
|
||||
/* should never change */ dispatchAssetEvent,
|
||||
])
|
||||
@ -1626,9 +1622,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
category={category}
|
||||
pasteData={pasteData}
|
||||
selectedKeys={selectedKeys}
|
||||
clearSelectedKeys={clearSelectedKeys}
|
||||
nodeMapRef={nodeMapRef}
|
||||
event={{ pageX: 0, pageY: 0 }}
|
||||
setSelectedKeys={setSelectedKeys}
|
||||
dispatchAssetEvent={dispatchAssetEvent}
|
||||
dispatchAssetListEvent={dispatchAssetListEvent}
|
||||
doCopy={doCopy}
|
||||
@ -1638,11 +1634,12 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
),
|
||||
[
|
||||
category,
|
||||
pasteData,
|
||||
selectedKeys,
|
||||
pasteData,
|
||||
doCopy,
|
||||
doCut,
|
||||
doPaste,
|
||||
/* should never change */ clearSelectedKeys,
|
||||
/* should never change */ dispatchAssetEvent,
|
||||
/* should never change */ dispatchAssetListEvent,
|
||||
]
|
||||
@ -1660,7 +1657,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
// The type MUST be here to trigger excess property errors at typecheck time.
|
||||
(): AssetsTableState => ({
|
||||
visibilities,
|
||||
numberOfSelectedItems: selectedKeys.size,
|
||||
selectedKeys: selectedKeysRef,
|
||||
category,
|
||||
labels: allLabels,
|
||||
deletedLabelNames,
|
||||
@ -1688,7 +1685,6 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}),
|
||||
[
|
||||
visibilities,
|
||||
selectedKeys.size,
|
||||
category,
|
||||
allLabels,
|
||||
deletedLabelNames,
|
||||
@ -1755,7 +1751,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
shortcutManagerModule.MouseAction.selectAdditionalRange,
|
||||
event
|
||||
) &&
|
||||
selectedKeys.size !== 0
|
||||
selectedKeysRef.current.size !== 0
|
||||
) {
|
||||
setSelectedKeys(new Set())
|
||||
}
|
||||
@ -1764,7 +1760,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
return () => {
|
||||
document.removeEventListener('click', onDocumentClick)
|
||||
}
|
||||
}, [selectedKeys, /* should never change */ setSelectedKeys, shortcutManager])
|
||||
}, [shortcutManager, /* should never change */ setSelectedKeys])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isLoading) {
|
||||
@ -1809,24 +1805,20 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
event
|
||||
)
|
||||
) {
|
||||
setSelectedKeys(
|
||||
oldSelectedItems => new Set([...oldSelectedItems, ...getNewlySelectedKeys()])
|
||||
)
|
||||
setSelectedKeys(new Set([...selectedKeysRef.current, ...getNewlySelectedKeys()]))
|
||||
} else if (
|
||||
shortcutManager.matchesMouseAction(
|
||||
shortcutManagerModule.MouseAction.selectAdditional,
|
||||
event
|
||||
)
|
||||
) {
|
||||
setSelectedKeys(oldSelectedItems => {
|
||||
const newItems = new Set(oldSelectedItems)
|
||||
if (oldSelectedItems.has(key)) {
|
||||
newItems.delete(key)
|
||||
} else {
|
||||
newItems.add(key)
|
||||
}
|
||||
return newItems
|
||||
})
|
||||
const newSelectedKeys = new Set(selectedKeysRef.current)
|
||||
if (selectedKeysRef.current.has(key)) {
|
||||
newSelectedKeys.delete(key)
|
||||
} else {
|
||||
newSelectedKeys.add(key)
|
||||
}
|
||||
setSelectedKeys(newSelectedKeys)
|
||||
} else {
|
||||
setSelectedKeys(new Set([key]))
|
||||
}
|
||||
@ -1882,10 +1874,10 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
hidden={visibilities.get(item.key) === Visibility.hidden}
|
||||
selected={isSelected}
|
||||
setSelected={selected => {
|
||||
setSelectedKeys(oldSelectedKeys => set.withPresence(oldSelectedKeys, key, selected))
|
||||
setSelectedKeys(set.withPresence(selectedKeysRef.current, key, selected))
|
||||
}}
|
||||
isSoleSelectedItem={isSoleSelectedItem}
|
||||
allowContextMenu={selectedKeys.size === 0 || !isSelected || isSoleSelectedItem}
|
||||
allowContextMenu={selectedKeysRef.current.size === 0 || !isSelected || isSoleSelectedItem}
|
||||
onClick={onRowClick}
|
||||
onContextMenu={(_innerProps, event) => {
|
||||
if (!isSelected) {
|
||||
@ -1896,106 +1888,56 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
}}
|
||||
onDragStart={event => {
|
||||
if (!selectedKeys.has(key)) {
|
||||
let newSelectedKeys = selectedKeysRef.current
|
||||
if (!selectedKeysRef.current.has(key)) {
|
||||
setPreviouslySelectedKey(key)
|
||||
setSelectedKeys(new Set([key]))
|
||||
newSelectedKeys = new Set([key])
|
||||
setSelectedKeys(newSelectedKeys)
|
||||
}
|
||||
setSelectedKeys(oldSelectedKeys => {
|
||||
const nodes = assetTree
|
||||
.preorderTraversal()
|
||||
.filter(node => oldSelectedKeys.has(node.key))
|
||||
const payload: drag.AssetRowsDragPayload = nodes.map(node => ({
|
||||
key: node.key,
|
||||
asset: node.item,
|
||||
}))
|
||||
drag.setDragImageToBlank(event)
|
||||
drag.ASSET_ROWS.bind(event, payload)
|
||||
queueMicrotask(() => {
|
||||
setModal(
|
||||
<DragModal
|
||||
event={event}
|
||||
className="flex flex-col rounded-2xl bg-frame-selected backdrop-blur-3xl"
|
||||
doCleanup={() => {
|
||||
drag.ASSET_ROWS.unbind(payload)
|
||||
}}
|
||||
>
|
||||
{nodes.map(node => (
|
||||
<NameColumn
|
||||
key={node.key}
|
||||
keyProp={node.key}
|
||||
item={node.with({ depth: 0 })}
|
||||
state={state}
|
||||
// Default states.
|
||||
isSoleSelectedItem={false}
|
||||
selected={false}
|
||||
rowState={assetRowUtils.INITIAL_ROW_STATE}
|
||||
// The drag placeholder cannot be interacted with.
|
||||
setSelected={() => {}}
|
||||
setItem={() => {}}
|
||||
setRowState={() => {}}
|
||||
/>
|
||||
))}
|
||||
</DragModal>
|
||||
)
|
||||
})
|
||||
return oldSelectedKeys
|
||||
})
|
||||
const nodes = assetTree
|
||||
.preorderTraversal()
|
||||
.filter(node => newSelectedKeys.has(node.key))
|
||||
const payload: drag.AssetRowsDragPayload = nodes.map(node => ({
|
||||
key: node.key,
|
||||
asset: node.item,
|
||||
}))
|
||||
drag.setDragImageToBlank(event)
|
||||
drag.ASSET_ROWS.bind(event, payload)
|
||||
setModal(
|
||||
<DragModal
|
||||
event={event}
|
||||
className="flex flex-col rounded-2xl bg-frame-selected backdrop-blur-3xl"
|
||||
doCleanup={() => {
|
||||
drag.ASSET_ROWS.unbind(payload)
|
||||
}}
|
||||
>
|
||||
{nodes.map(node => (
|
||||
<NameColumn
|
||||
key={node.key}
|
||||
keyProp={node.key}
|
||||
item={node.with({ depth: 0 })}
|
||||
state={state}
|
||||
// Default states.
|
||||
isSoleSelectedItem={false}
|
||||
selected={false}
|
||||
rowState={assetRowUtils.INITIAL_ROW_STATE}
|
||||
// The drag placeholder cannot be interacted with.
|
||||
setSelected={() => {}}
|
||||
setItem={() => {}}
|
||||
setRowState={() => {}}
|
||||
/>
|
||||
))}
|
||||
</DragModal>
|
||||
)
|
||||
}}
|
||||
onDragOver={event => {
|
||||
setSelectedKeys(oldSelectedKeys => {
|
||||
const payload = drag.LABELS.lookup(event)
|
||||
if (payload != null) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const ids = oldSelectedKeys.has(key) ? oldSelectedKeys : new Set([key])
|
||||
// Expand ids to include ids of children as well.
|
||||
for (const node of assetTree.preorderTraversal()) {
|
||||
if (ids.has(node.key) && node.children != null) {
|
||||
for (const child of node.children) {
|
||||
ids.add(child.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
let labelsPresent = 0
|
||||
for (const selectedKey of ids) {
|
||||
const labels = nodeMapRef.current.get(selectedKey)?.item.labels
|
||||
if (labels != null) {
|
||||
for (const label of labels) {
|
||||
if (payload.has(label)) {
|
||||
labelsPresent += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const shouldAdd = labelsPresent * 2 < ids.size * payload.size
|
||||
window.setTimeout(() => {
|
||||
dispatchAssetEvent({
|
||||
type: shouldAdd
|
||||
? AssetEventType.temporarilyAddLabels
|
||||
: AssetEventType.temporarilyRemoveLabels,
|
||||
ids,
|
||||
labelNames: payload,
|
||||
})
|
||||
})
|
||||
}
|
||||
return oldSelectedKeys
|
||||
})
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setSelectedKeys(oldSelectedKeys => {
|
||||
window.setTimeout(() => {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids: oldSelectedKeys,
|
||||
labelNames: set.EMPTY,
|
||||
})
|
||||
})
|
||||
return oldSelectedKeys
|
||||
})
|
||||
}}
|
||||
onDrop={event => {
|
||||
setSelectedKeys(oldSelectedKeys => {
|
||||
const ids = oldSelectedKeys.has(key) ? new Set(oldSelectedKeys) : new Set([key])
|
||||
const payload = drag.LABELS.lookup(event)
|
||||
if (payload != null) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const ids = new Set(
|
||||
selectedKeysRef.current.has(key) ? selectedKeysRef.current : [key]
|
||||
)
|
||||
// Expand ids to include ids of children as well.
|
||||
for (const node of assetTree.preorderTraversal()) {
|
||||
if (ids.has(node.key) && node.children != null) {
|
||||
@ -2004,41 +1946,75 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const payload = drag.LABELS.lookup(event)
|
||||
if (payload != null) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
let labelsPresent = 0
|
||||
for (const selectedKey of ids) {
|
||||
const labels = nodeMapRef.current.get(selectedKey)?.item.labels
|
||||
if (labels != null) {
|
||||
for (const label of labels) {
|
||||
if (payload.has(label)) {
|
||||
labelsPresent += 1
|
||||
}
|
||||
let labelsPresent = 0
|
||||
for (const selectedKey of ids) {
|
||||
const labels = nodeMapRef.current.get(selectedKey)?.item.labels
|
||||
if (labels != null) {
|
||||
for (const label of labels) {
|
||||
if (payload.has(label)) {
|
||||
labelsPresent += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
const shouldAdd = labelsPresent * 2 < ids.size * payload.size
|
||||
window.setTimeout(() => {
|
||||
dispatchAssetEvent({
|
||||
type: shouldAdd ? AssetEventType.addLabels : AssetEventType.removeLabels,
|
||||
ids,
|
||||
labelNames: payload,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
window.setTimeout(() => {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids,
|
||||
labelNames: set.EMPTY,
|
||||
})
|
||||
})
|
||||
}
|
||||
return oldSelectedKeys
|
||||
const shouldAdd = labelsPresent * 2 < ids.size * payload.size
|
||||
window.setTimeout(() => {
|
||||
dispatchAssetEvent({
|
||||
type: shouldAdd
|
||||
? AssetEventType.temporarilyAddLabels
|
||||
: AssetEventType.temporarilyRemoveLabels,
|
||||
ids,
|
||||
labelNames: payload,
|
||||
})
|
||||
})
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids: selectedKeysRef.current,
|
||||
labelNames: set.EMPTY,
|
||||
})
|
||||
}}
|
||||
onDrop={event => {
|
||||
const ids = new Set(selectedKeysRef.current.has(key) ? selectedKeysRef.current : [key])
|
||||
// Expand ids to include ids of descendants as well.
|
||||
for (const node of assetTree.preorderTraversal()) {
|
||||
if (ids.has(node.key) && node.children != null) {
|
||||
for (const child of node.children) {
|
||||
ids.add(child.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
const payload = drag.LABELS.lookup(event)
|
||||
if (payload != null) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
let labelsPresent = 0
|
||||
for (const selectedKey of ids) {
|
||||
const labels = nodeMapRef.current.get(selectedKey)?.item.labels
|
||||
if (labels != null) {
|
||||
for (const label of labels) {
|
||||
if (payload.has(label)) {
|
||||
labelsPresent += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const shouldAdd = labelsPresent * 2 < ids.size * payload.size
|
||||
dispatchAssetEvent({
|
||||
type: shouldAdd ? AssetEventType.addLabels : AssetEventType.removeLabels,
|
||||
ids,
|
||||
labelNames: payload,
|
||||
})
|
||||
} else {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids,
|
||||
labelNames: set.EMPTY,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -2057,7 +2033,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
selectedKeys={selectedKeys}
|
||||
nodeMapRef={nodeMapRef}
|
||||
event={event}
|
||||
setSelectedKeys={setSelectedKeys}
|
||||
clearSelectedKeys={clearSelectedKeys}
|
||||
dispatchAssetEvent={dispatchAssetEvent}
|
||||
dispatchAssetListEvent={dispatchAssetListEvent}
|
||||
doCopy={doCopy}
|
||||
@ -2073,15 +2049,10 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
event.relatedTarget instanceof Node &&
|
||||
!event.currentTarget.contains(event.relatedTarget)
|
||||
) {
|
||||
setSelectedKeys(oldSelectedKeys => {
|
||||
window.setTimeout(() => {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids: oldSelectedKeys,
|
||||
labelNames: set.EMPTY,
|
||||
})
|
||||
})
|
||||
return oldSelectedKeys
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids: selectedKeysRef.current,
|
||||
labelNames: set.EMPTY,
|
||||
})
|
||||
}
|
||||
}}
|
||||
@ -2100,6 +2071,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
|
||||
<div
|
||||
className="grow"
|
||||
onClick={() => {
|
||||
setSelectedKeys(new Set())
|
||||
}}
|
||||
onDragEnter={onDragOver}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={event => {
|
||||
|
@ -43,9 +43,9 @@ const pluralize = string.makePluralize('item', 'items')
|
||||
export interface AssetsTableContextMenuProps {
|
||||
readonly hidden?: boolean
|
||||
readonly category: Category
|
||||
readonly pasteData: pasteDataModule.PasteData<Set<backendModule.AssetId>> | null
|
||||
readonly selectedKeys: Set<backendModule.AssetId>
|
||||
readonly setSelectedKeys: (items: Set<backendModule.AssetId>) => void
|
||||
readonly pasteData: pasteDataModule.PasteData<ReadonlySet<backendModule.AssetId>> | null
|
||||
readonly selectedKeys: ReadonlySet<backendModule.AssetId>
|
||||
readonly clearSelectedKeys: () => void
|
||||
readonly nodeMapRef: React.MutableRefObject<ReadonlyMap<backendModule.AssetId, AssetTreeNode>>
|
||||
readonly event: Pick<React.MouseEvent<Element, MouseEvent>, 'pageX' | 'pageY'>
|
||||
readonly dispatchAssetEvent: (event: assetEvent.AssetEvent) => void
|
||||
@ -61,7 +61,7 @@ export interface AssetsTableContextMenuProps {
|
||||
/** A context menu for an `AssetsTable`, when no row is selected, or multiple rows
|
||||
* are selected. */
|
||||
export default function AssetsTableContextMenu(props: AssetsTableContextMenuProps) {
|
||||
const { category, pasteData, selectedKeys, setSelectedKeys, nodeMapRef, event } = props
|
||||
const { category, pasteData, selectedKeys, clearSelectedKeys, nodeMapRef, event } = props
|
||||
const { dispatchAssetEvent, dispatchAssetListEvent, hidden = false } = props
|
||||
const { doCopy, doCut, doPaste } = props
|
||||
const { backend } = backendProvider.useBackend()
|
||||
@ -91,20 +91,14 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
|
||||
const doDeleteAll = () => {
|
||||
if (isCloud) {
|
||||
unsetModal()
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.delete,
|
||||
ids: selectedKeys,
|
||||
})
|
||||
dispatchAssetEvent({ type: AssetEventType.delete, ids: selectedKeys })
|
||||
} else {
|
||||
setModal(
|
||||
<ConfirmDeleteModal
|
||||
description={`${selectedKeys.size} selected ${pluralized}`}
|
||||
doDelete={() => {
|
||||
setSelectedKeys(new Set())
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.delete,
|
||||
ids: selectedKeys,
|
||||
})
|
||||
clearSelectedKeys()
|
||||
dispatchAssetEvent({ type: AssetEventType.delete, ids: selectedKeys })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ import * as jsonSchema from '#/utilities/jsonSchema'
|
||||
// =============
|
||||
|
||||
fc.test.prop({
|
||||
value: fc.fc.anything(),
|
||||
value: fc.fc.anything({ withNullPrototype: true }),
|
||||
})('converting between constant value and schema', ({ value }) => {
|
||||
const schema = jsonSchema.constantValueToSchema(value)
|
||||
if (schema != null) {
|
||||
@ -25,6 +25,25 @@ fc.test.prop({
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
v.test.each([{ value: JSON.parse('{"__proto__":{}}') }])(
|
||||
'converting between constant value and schema',
|
||||
({ value }) => {
|
||||
const schema = jsonSchema.constantValueToSchema(value)
|
||||
if (schema != null) {
|
||||
const extractedValue = jsonSchema.constantValue({}, schema)[0]
|
||||
v.expect(
|
||||
extractedValue,
|
||||
`\`${JSON.stringify(value)}\` should round trip to schema and back`
|
||||
).toEqual(value)
|
||||
v.expect(
|
||||
jsonSchema.isMatch({}, schema, value),
|
||||
`\`${JSON.stringify(value)}\` should match its converted schema`
|
||||
).toBe(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const STRING_SCHEMA = { type: 'string' } as const
|
||||
fc.test.prop({ value: fc.fc.string() })('string schema', ({ value }) => {
|
||||
const constSchema = { const: value, type: 'string' }
|
||||
|
@ -56,7 +56,7 @@ export function constantValueToSchema(value: unknown): object | null {
|
||||
result = null
|
||||
break
|
||||
}
|
||||
properties[key] = schema
|
||||
Object.defineProperty(properties, key, { value: schema, enumerable: true })
|
||||
}
|
||||
}
|
||||
break
|
||||
@ -144,7 +144,7 @@ function constantValueHelper(
|
||||
result = []
|
||||
break
|
||||
} else {
|
||||
object[key] = value[0] ?? null
|
||||
Object.defineProperty(object, key, { value: value[0] ?? null, enumerable: true })
|
||||
}
|
||||
}
|
||||
break
|
||||
|
@ -23,7 +23,7 @@ export function setPresence<T>(set: Set<T>, value: T, presence: boolean) {
|
||||
*
|
||||
* This is an immutable version of {@link setPresence}, so it returns a new set if the old set
|
||||
* would have been mutated, and returns the original set if it would not have been mutated. */
|
||||
export function withPresence<T>(set: Set<T>, value: T, presence: boolean) {
|
||||
export function withPresence<T>(set: ReadonlySet<T>, value: T, presence: boolean) {
|
||||
if (presence === set.has(value)) {
|
||||
return set
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user