mirror of
https://github.com/enso-org/enso.git
synced 2024-11-29 05:52:59 +03:00
Cloud backend issues (#11067)
Closes: enso-org/cloud-v2#1481
(cherry picked from commit bdadedbde5
)
This commit is contained in:
parent
e267e471be
commit
05de1a2440
@ -6,7 +6,7 @@ import * as modalProvider from '#/providers/ModalProvider'
|
|||||||
import * as aria from '#/components/aria'
|
import * as aria from '#/components/aria'
|
||||||
|
|
||||||
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { useOverlayTriggerState } from 'react-stately'
|
||||||
|
|
||||||
const PLACEHOLDER = <div />
|
const PLACEHOLDER = <div />
|
||||||
|
|
||||||
@ -14,7 +14,9 @@ const PLACEHOLDER = <div />
|
|||||||
* Props passed to the render function of a {@link DialogTrigger}.
|
* Props passed to the render function of a {@link DialogTrigger}.
|
||||||
*/
|
*/
|
||||||
export interface DialogTriggerRenderProps {
|
export interface DialogTriggerRenderProps {
|
||||||
readonly isOpened: boolean
|
readonly isOpen: boolean
|
||||||
|
readonly close: () => void
|
||||||
|
readonly open: () => void
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Props for a {@link DialogTrigger}.
|
* Props for a {@link DialogTrigger}.
|
||||||
@ -33,11 +35,10 @@ export interface DialogTriggerProps extends Omit<aria.DialogTriggerProps, 'child
|
|||||||
|
|
||||||
/** A DialogTrigger opens a dialog when a trigger element is pressed. */
|
/** A DialogTrigger opens a dialog when a trigger element is pressed. */
|
||||||
export function DialogTrigger(props: DialogTriggerProps) {
|
export function DialogTrigger(props: DialogTriggerProps) {
|
||||||
const { children, onOpenChange, onOpen = () => {}, onClose = () => {}, ...triggerProps } = props
|
const { children, onOpenChange, onOpen = () => {}, onClose = () => {} } = props
|
||||||
|
|
||||||
|
const state = useOverlayTriggerState(props)
|
||||||
|
|
||||||
const [isOpened, setIsOpened] = React.useState(
|
|
||||||
triggerProps.isOpen ?? triggerProps.defaultOpen ?? false,
|
|
||||||
)
|
|
||||||
const { setModal, unsetModal } = modalProvider.useSetModal()
|
const { setModal, unsetModal } = modalProvider.useSetModal()
|
||||||
|
|
||||||
const onOpenStableCallback = useEventCallback(onOpen)
|
const onOpenStableCallback = useEventCallback(onOpen)
|
||||||
@ -53,40 +54,29 @@ export function DialogTrigger(props: DialogTriggerProps) {
|
|||||||
onCloseStableCallback()
|
onCloseStableCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsOpened(opened)
|
state.setOpen(opened)
|
||||||
onOpenChange?.(opened)
|
onOpenChange?.(opened)
|
||||||
})
|
})
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isOpened) {
|
if (state.isOpen) {
|
||||||
onOpenStableCallback()
|
onOpenStableCallback()
|
||||||
}
|
}
|
||||||
}, [isOpened, onOpenStableCallback])
|
}, [state.isOpen, onOpenStableCallback])
|
||||||
|
|
||||||
const renderProps = {
|
|
||||||
isOpened,
|
|
||||||
} satisfies DialogTriggerRenderProps
|
|
||||||
|
|
||||||
const [trigger, dialog] = children
|
const [trigger, dialog] = children
|
||||||
|
|
||||||
|
const renderProps = {
|
||||||
|
isOpen: state.isOpen,
|
||||||
|
close: state.close.bind(state),
|
||||||
|
open: state.open.bind(state),
|
||||||
|
} satisfies DialogTriggerRenderProps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aria.DialogTrigger onOpenChange={onOpenChangeInternal} {...triggerProps}>
|
<aria.DialogTrigger {...state} onOpenChange={onOpenChangeInternal}>
|
||||||
{trigger}
|
{trigger}
|
||||||
|
|
||||||
{/* We're using AnimatePresence here to animate the dialog in and out. */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpened && (
|
|
||||||
<motion.div
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
initial={{ opacity: 1 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 1 }}
|
|
||||||
>
|
|
||||||
{typeof dialog === 'function' ? dialog(renderProps) : dialog}
|
{typeof dialog === 'function' ? dialog(renderProps) : dialog}
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</aria.DialogTrigger>
|
</aria.DialogTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,7 @@ export const DatePicker = forwardRef(function DatePicker<
|
|||||||
isRequired,
|
isRequired,
|
||||||
className,
|
className,
|
||||||
size,
|
size,
|
||||||
|
variants = DATE_PICKER_STYLES,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { fieldState, formInstance } = Form.useField({
|
const { fieldState, formInstance } = Form.useField({
|
||||||
@ -118,7 +119,7 @@ export const DatePicker = forwardRef(function DatePicker<
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
const styles = DATE_PICKER_STYLES({ size })
|
const styles = variants({ size })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Field
|
<Form.Field
|
||||||
|
@ -16,6 +16,7 @@ import { useSyncRef } from '#/hooks/syncRefHooks'
|
|||||||
import { mergeRefs } from '#/utilities/mergeRefs'
|
import { mergeRefs } from '#/utilities/mergeRefs'
|
||||||
import { forwardRef } from '#/utilities/react'
|
import { forwardRef } from '#/utilities/react'
|
||||||
import { tv } from '#/utilities/tailwindVariants'
|
import { tv } from '#/utilities/tailwindVariants'
|
||||||
|
import { DIALOG_BACKGROUND } from '../../Dialog'
|
||||||
|
|
||||||
const DROPDOWN_STYLES = tv({
|
const DROPDOWN_STYLES = tv({
|
||||||
base: 'focus-child group relative flex w-max cursor-pointer flex-col items-start whitespace-nowrap rounded-input leading-cozy',
|
base: 'focus-child group relative flex w-max cursor-pointer flex-col items-start whitespace-nowrap rounded-input leading-cozy',
|
||||||
@ -47,10 +48,12 @@ const DROPDOWN_STYLES = tv({
|
|||||||
slots: {
|
slots: {
|
||||||
container: 'absolute left-0 h-full w-full min-w-max',
|
container: 'absolute left-0 h-full w-full min-w-max',
|
||||||
options:
|
options:
|
||||||
'relative before:absolute before:top before:w-full before:rounded-input before:border-0.5 before:border-primary/20 before:backdrop-blur-default before:transition-colors',
|
'relative before:absolute before:top before:w-full before:rounded-input before:border-0.5 before:border-primary/20 before:transition-colors',
|
||||||
optionsSpacing: 'padding relative h-6',
|
optionsSpacing: 'padding relative h-6',
|
||||||
optionsContainer:
|
optionsContainer: DIALOG_BACKGROUND({
|
||||||
|
className:
|
||||||
'relative grid max-h-dropdown-items w-full overflow-auto rounded-input transition-grid-template-rows',
|
'relative grid max-h-dropdown-items w-full overflow-auto rounded-input transition-grid-template-rows',
|
||||||
|
}),
|
||||||
optionsList: 'overflow-hidden',
|
optionsList: 'overflow-hidden',
|
||||||
optionsItem:
|
optionsItem:
|
||||||
'flex h-6 items-center gap-dropdown-arrow rounded-input px-input-x transition-colors focus:cursor-default focus:bg-frame focus:font-bold focus:focus-ring not-focus:hover:bg-hover-bg not-selected:hover:bg-hover-bg',
|
'flex h-6 items-center gap-dropdown-arrow rounded-input px-input-x transition-colors focus:cursor-default focus:bg-frame focus:font-bold focus:focus-ring not-focus:hover:bg-hover-bg not-selected:hover:bg-hover-bg',
|
||||||
|
@ -1,23 +1,11 @@
|
|||||||
/** @file A context menu. */
|
/** @file A context menu. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as detect from 'enso-common/src/detect'
|
|
||||||
|
|
||||||
import Modal from '#/components/Modal'
|
import Modal from '#/components/Modal'
|
||||||
|
|
||||||
import { forwardRef } from '#/utilities/react'
|
import { forwardRef } from '#/utilities/react'
|
||||||
import * as tailwindMerge from '#/utilities/tailwindMerge'
|
import * as tailwindMerge from '#/utilities/tailwindMerge'
|
||||||
|
|
||||||
// =================
|
|
||||||
// === Constants ===
|
|
||||||
// =================
|
|
||||||
|
|
||||||
const DEFAULT_MENU_WIDTH = 256
|
|
||||||
const MACOS_MENU_WIDTH = 230
|
|
||||||
/** The width of a single context menu. */
|
|
||||||
const MENU_WIDTH = detect.isOnMacOS() ? MACOS_MENU_WIDTH : DEFAULT_MENU_WIDTH
|
|
||||||
const HALF_MENU_WIDTH = Math.floor(MENU_WIDTH / 2)
|
|
||||||
|
|
||||||
// ===================
|
// ===================
|
||||||
// === ContextMenu ===
|
// === ContextMenu ===
|
||||||
// ===================
|
// ===================
|
||||||
@ -46,7 +34,7 @@ function ContextMenus(props: ContextMenusProps, ref: React.ForwardedRef<HTMLDivE
|
|||||||
<div
|
<div
|
||||||
data-testid="context-menus"
|
data-testid="context-menus"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{ left: event.pageX - HALF_MENU_WIDTH, top: event.pageY }}
|
style={{ left: event.pageX, top: event.pageY }}
|
||||||
className={tailwindMerge.twMerge(
|
className={tailwindMerge.twMerge(
|
||||||
'pointer-events-none sticky flex w-min items-start gap-context-menus',
|
'pointer-events-none sticky flex w-min items-start gap-context-menus',
|
||||||
)}
|
)}
|
||||||
|
@ -92,7 +92,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
|
|||||||
value={typeof value === 'string' ? value : ''}
|
value={typeof value === 'string' ? value : ''}
|
||||||
size={1}
|
size={1}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'focus-child text w-60 grow rounded-input border-0.5 bg-transparent px-input-x read-only:read-only',
|
'focus-child text w-60 w-full grow rounded-input border-0.5 bg-transparent px-input-x read-only:read-only',
|
||||||
getValidator(path)(value) ? 'border-primary/20' : 'border-red-700/60',
|
getValidator(path)(value) ? 'border-primary/20' : 'border-red-700/60',
|
||||||
)}
|
)}
|
||||||
placeholder={getText('enterText')}
|
placeholder={getText('enterText')}
|
||||||
@ -139,7 +139,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
|
|||||||
value={typeof value === 'number' ? value : ''}
|
value={typeof value === 'number' ? value : ''}
|
||||||
size={1}
|
size={1}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'focus-child min-6- text40 w-60 grow rounded-input border-0.5 bg-transparent px-input-x read-only:read-only',
|
'focus-child min-6- text40 w-full grow rounded-input border-0.5 bg-transparent px-input-x read-only:read-only',
|
||||||
getValidator(path)(value) ? 'border-primary/20' : 'border-red-700/60',
|
getValidator(path)(value) ? 'border-primary/20' : 'border-red-700/60',
|
||||||
)}
|
)}
|
||||||
placeholder={getText('enterInteger')}
|
placeholder={getText('enterInteger')}
|
||||||
@ -191,7 +191,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
|
|||||||
isDisabled={!isOptional}
|
isDisabled={!isOptional}
|
||||||
isActive={!isOptional || isPresent}
|
isActive={!isOptional || isPresent}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'col-start-1 inline-block whitespace-nowrap rounded-full px-button-x',
|
'col-start-1 inline-block justify-self-start whitespace-nowrap rounded-full px-button-x',
|
||||||
isOptional && 'hover:bg-hover-bg',
|
isOptional && 'hover:bg-hover-bg',
|
||||||
)}
|
)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -216,6 +216,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
|
|||||||
>
|
>
|
||||||
{'title' in childSchema ? String(childSchema.title) : key}
|
{'title' in childSchema ? String(childSchema.title) : key}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isPresent && (
|
{isPresent && (
|
||||||
<div className="col-start-2">
|
<div className="col-start-2">
|
||||||
<JSONSchemaInput
|
<JSONSchemaInput
|
||||||
@ -302,7 +303,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
items={childSchemas}
|
items={childSchemas}
|
||||||
selectedIndex={selectedChildIndex}
|
selectedIndex={selectedChildIndex}
|
||||||
className="self-start"
|
className="w-full self-start"
|
||||||
onChange={(childSchema, index) => {
|
onChange={(childSchema, index) => {
|
||||||
setSelectedChildIndex(index)
|
setSelectedChildIndex(index)
|
||||||
const newConstantValue = constantValueOfSchema(defs, childSchema, true)
|
const newConstantValue = constantValueOfSchema(defs, childSchema, true)
|
||||||
|
@ -193,6 +193,7 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
const getDatalinkMutation = useMutation(backendMutationOptions(backend, 'getDatalink'))
|
const getDatalinkMutation = useMutation(backendMutationOptions(backend, 'getDatalink'))
|
||||||
const createPermissionMutation = useMutation(backendMutationOptions(backend, 'createPermission'))
|
const createPermissionMutation = useMutation(backendMutationOptions(backend, 'createPermission'))
|
||||||
const associateTagMutation = useMutation(backendMutationOptions(backend, 'associateTag'))
|
const associateTagMutation = useMutation(backendMutationOptions(backend, 'associateTag'))
|
||||||
|
const editDescriptionMutation = useMutation(backendMutationOptions(backend, 'updateAsset'))
|
||||||
|
|
||||||
const setSelected = useEventCallback((newSelected: boolean) => {
|
const setSelected = useEventCallback((newSelected: boolean) => {
|
||||||
const { selectedKeys } = driveStore.getState()
|
const { selectedKeys } = driveStore.getState()
|
||||||
@ -224,9 +225,11 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
}, [grabKeyboardFocusRef, isKeyboardSelected, item])
|
}, [grabKeyboardFocusRef, isKeyboardSelected, item])
|
||||||
|
|
||||||
React.useImperativeHandle(updateAssetRef, () => ({ setAsset, item }))
|
React.useImperativeHandle(updateAssetRef, () => ({ setAsset, item }))
|
||||||
|
|
||||||
if (updateAssetRef.current) {
|
if (updateAssetRef.current) {
|
||||||
updateAssetRef.current[item.item.id] = setAsset
|
updateAssetRef.current[item.item.id] = setAsset
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (updateAssetRef.current) {
|
if (updateAssetRef.current) {
|
||||||
@ -250,25 +253,25 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
[doDeleteRaw, item.item],
|
[doDeleteRaw, item.item],
|
||||||
)
|
)
|
||||||
|
|
||||||
const doTriggerDescriptionEdit = React.useCallback(() => {
|
const doTriggerDescriptionEdit = useEventCallback(() => {
|
||||||
setModal(
|
setModal(
|
||||||
<EditAssetDescriptionModal
|
<EditAssetDescriptionModal
|
||||||
doChangeDescription={async (description) => {
|
doChangeDescription={async (description) => {
|
||||||
if (description !== asset.description) {
|
if (description !== asset.description) {
|
||||||
setAsset(object.merger({ description }))
|
setAsset(object.merger({ description }))
|
||||||
|
|
||||||
await backend
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
.updateAsset(item.item.id, { parentDirectoryId: null, description }, item.item.title)
|
return editDescriptionMutation.mutateAsync([
|
||||||
.catch((error) => {
|
asset.id,
|
||||||
setAsset(object.merger({ description: asset.description }))
|
{ description, parentDirectoryId: null },
|
||||||
throw error
|
item.item.title,
|
||||||
})
|
])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
initialDescription={asset.description}
|
initialDescription={asset.description}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
}, [setModal, asset.description, setAsset, backend, item.item.id, item.item.title])
|
})
|
||||||
|
|
||||||
const clearDragState = React.useCallback(() => {
|
const clearDragState = React.useCallback(() => {
|
||||||
setIsDraggedOver(false)
|
setIsDraggedOver(false)
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
/** @file A component that renders the modal instance from the modal React Context. */
|
/** @file A component that renders the modal instance from the modal React Context. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { DialogTrigger } from '#/components/AriaComponents'
|
||||||
import * as modalProvider from '#/providers/ModalProvider'
|
import * as modalProvider from '#/providers/ModalProvider'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
|
||||||
// ================
|
// ================
|
||||||
// === TheModal ===
|
// === TheModal ===
|
||||||
@ -9,7 +11,25 @@ import * as modalProvider from '#/providers/ModalProvider'
|
|||||||
|
|
||||||
/** Renders the modal instance from the modal React Context (if any). */
|
/** Renders the modal instance from the modal React Context (if any). */
|
||||||
export default function TheModal() {
|
export default function TheModal() {
|
||||||
const { modal } = modalProvider.useModal()
|
const { modal, key } = modalProvider.useModal()
|
||||||
|
|
||||||
return <>{modal}</>
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{modal && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 1 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-magic-numbers */
|
||||||
|
exit={{ opacity: 0.8 }}
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-magic-numbers */
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<DialogTrigger key={key} defaultOpen>
|
||||||
|
<></>
|
||||||
|
{modal}
|
||||||
|
</DialogTrigger>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ export default function AssetPanel(props: AssetPanelProps) {
|
|||||||
<div
|
<div
|
||||||
data-testid="asset-panel"
|
data-testid="asset-panel"
|
||||||
className={tailwindMerge.twMerge(
|
className={tailwindMerge.twMerge(
|
||||||
'pointer-events-none absolute flex h-full w-asset-panel flex-col gap-asset-panel bg-white p-4 pl-asset-panel-l transition-[box-shadow] clip-path-left-shadow',
|
'pointer-events-none absolute flex h-full w-asset-panel flex-col gap-asset-panel bg-invert p-4 pl-asset-panel-l transition-[box-shadow] clip-path-left-shadow',
|
||||||
isVisible ? 'shadow-softer' : '',
|
isVisible ? 'shadow-softer' : '',
|
||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
@ -184,6 +184,7 @@ export default function AssetPanel(props: AssetPanelProps) {
|
|||||||
: <>
|
: <>
|
||||||
{tab === AssetPanelTab.properties && (
|
{tab === AssetPanelTab.properties && (
|
||||||
<AssetProperties
|
<AssetProperties
|
||||||
|
key={item.item.id}
|
||||||
backend={backend}
|
backend={backend}
|
||||||
isReadonly={isReadonly}
|
isReadonly={isReadonly}
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -45,14 +45,13 @@ export interface AssetPropertiesProps {
|
|||||||
|
|
||||||
/** Display and modify the properties of an asset. */
|
/** Display and modify the properties of an asset. */
|
||||||
export default function AssetProperties(props: AssetPropertiesProps) {
|
export default function AssetProperties(props: AssetPropertiesProps) {
|
||||||
const { backend, item: itemRaw, setItem: setItemRaw, category } = props
|
const { backend, item, setItem, category } = props
|
||||||
const { isReadonly = false } = props
|
const { isReadonly = false } = props
|
||||||
|
|
||||||
const { user } = authProvider.useFullUserSession()
|
const { user } = authProvider.useFullUserSession()
|
||||||
const { getText } = textProvider.useText()
|
const { getText } = textProvider.useText()
|
||||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||||
const localBackend = backendProvider.useLocalBackend()
|
const localBackend = backendProvider.useLocalBackend()
|
||||||
const [item, setItemInner] = React.useState(itemRaw)
|
|
||||||
const [isEditingDescription, setIsEditingDescription] = React.useState(false)
|
const [isEditingDescription, setIsEditingDescription] = React.useState(false)
|
||||||
const [queuedDescription, setQueuedDescripion] = React.useState<string | null>(null)
|
const [queuedDescription, setQueuedDescripion] = React.useState<string | null>(null)
|
||||||
const [description, setDescription] = React.useState('')
|
const [description, setDescription] = React.useState('')
|
||||||
@ -65,13 +64,7 @@ export default function AssetProperties(props: AssetPropertiesProps) {
|
|||||||
() => datalinkValidator.validateDatalink(datalinkValue),
|
() => datalinkValidator.validateDatalink(datalinkValue),
|
||||||
[datalinkValue],
|
[datalinkValue],
|
||||||
)
|
)
|
||||||
const setItem = React.useCallback(
|
|
||||||
(valueOrUpdater: React.SetStateAction<assetTreeNode.AnyAssetTreeNode>) => {
|
|
||||||
setItemInner(valueOrUpdater)
|
|
||||||
setItemRaw(valueOrUpdater)
|
|
||||||
},
|
|
||||||
[setItemRaw],
|
|
||||||
)
|
|
||||||
const labels = useBackendQuery(backend, 'listTags', []).data ?? []
|
const labels = useBackendQuery(backend, 'listTags', []).data ?? []
|
||||||
const self = permissions.tryFindSelfPermission(user, item.item.permissions)
|
const self = permissions.tryFindSelfPermission(user, item.item.permissions)
|
||||||
const ownsThisAsset = self?.permission === permissions.PermissionAction.own
|
const ownsThisAsset = self?.permission === permissions.PermissionAction.own
|
||||||
|
@ -72,10 +72,18 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
|
|||||||
!isCloud ||
|
!isCloud ||
|
||||||
Array.from(selectedKeys, (key) => {
|
Array.from(selectedKeys, (key) => {
|
||||||
const userPermissions = nodeMapRef.current.get(key)?.item.permissions
|
const userPermissions = nodeMapRef.current.get(key)?.item.permissions
|
||||||
|
const selfGroupPermission = userPermissions?.find(
|
||||||
|
backendModule.isUserGroupPermissionAnd(
|
||||||
|
(permission) => user.userGroups?.includes(permission.userGroup.id) ?? false,
|
||||||
|
),
|
||||||
|
)
|
||||||
const selfPermission = userPermissions?.find(
|
const selfPermission = userPermissions?.find(
|
||||||
backendModule.isUserPermissionAnd((permission) => permission.user.userId === user.userId),
|
backendModule.isUserPermissionAnd((permission) => permission.user.userId === user.userId),
|
||||||
)
|
)
|
||||||
return selfPermission?.permission === permissions.PermissionAction.own
|
|
||||||
|
return (
|
||||||
|
(selfPermission ?? selfGroupPermission)?.permission === permissions.PermissionAction.own
|
||||||
|
)
|
||||||
}).every((isOwner) => isOwner)
|
}).every((isOwner) => isOwner)
|
||||||
|
|
||||||
// This is not a React component even though it contains JSX.
|
// This is not a React component even though it contains JSX.
|
||||||
|
@ -35,7 +35,12 @@ export default function UpsertDatalinkModal(props: UpsertDatalinkModalProps) {
|
|||||||
const { getText } = useText()
|
const { getText } = useText()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog fitContent title={getText('createDatalink')} className="min-w-max">
|
<Dialog
|
||||||
|
fitContent
|
||||||
|
title={getText('createDatalink')}
|
||||||
|
className="min-w-max"
|
||||||
|
isDismissable={false}
|
||||||
|
>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
<Form
|
<Form
|
||||||
method="dialog"
|
method="dialog"
|
||||||
@ -55,7 +60,7 @@ export default function UpsertDatalinkModal(props: UpsertDatalinkModalProps) {
|
|||||||
label={getText('name')}
|
label={getText('name')}
|
||||||
placeholder={getText('datalinkNamePlaceholder')}
|
placeholder={getText('datalinkNamePlaceholder')}
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative w-full">
|
||||||
<DatalinkFormInput form={form} name="value" dropdownTitle={getText('type')} />
|
<DatalinkFormInput form={form} name="value" dropdownTitle={getText('type')} />
|
||||||
</div>
|
</div>
|
||||||
<ButtonGroup className="relative">
|
<ButtonGroup className="relative">
|
||||||
|
@ -23,7 +23,10 @@ export default function UpsertSecretModal(props: UpsertSecretModalProps) {
|
|||||||
const isNameEditable = nameRaw == null
|
const isNameEditable = nameRaw == null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={isCreatingSecret ? getText('newSecret') : getText('editSecret')}>
|
<Dialog
|
||||||
|
title={isCreatingSecret ? getText('newSecret') : getText('editSecret')}
|
||||||
|
isDismissable={false}
|
||||||
|
>
|
||||||
<Form
|
<Form
|
||||||
testId="upsert-secret-modal"
|
testId="upsert-secret-modal"
|
||||||
method="dialog"
|
method="dialog"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/** @file The React provider for modals, along with hooks to use the provider via
|
/** @file The React provider for modals, along with hooks to use the provider via
|
||||||
* the shared React context. */
|
* the shared React context. */
|
||||||
|
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
@ -17,10 +18,11 @@ interface ModalStaticContextType {
|
|||||||
|
|
||||||
/** State contained in a `ModalContext`. */
|
/** State contained in a `ModalContext`. */
|
||||||
interface ModalContextType {
|
interface ModalContextType {
|
||||||
|
readonly key: number
|
||||||
readonly modal: Modal | null
|
readonly modal: Modal | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalContext = React.createContext<ModalContextType>({ modal: null })
|
const ModalContext = React.createContext<ModalContextType>({ modal: null, key: 0 })
|
||||||
|
|
||||||
const ModalStaticContext = React.createContext<ModalStaticContextType>({
|
const ModalStaticContext = React.createContext<ModalStaticContextType>({
|
||||||
setModal: () => {
|
setModal: () => {
|
||||||
@ -37,23 +39,34 @@ export interface ModalProviderProps extends Readonly<React.PropsWithChildren> {}
|
|||||||
export default function ModalProvider(props: ModalProviderProps) {
|
export default function ModalProvider(props: ModalProviderProps) {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
const [modal, setModal] = React.useState<Modal | null>(null)
|
const [modal, setModal] = React.useState<Modal | null>(null)
|
||||||
|
// we use key to tell react to invaldidate the modal when we change it.
|
||||||
|
const [key, setKey] = React.useState(0)
|
||||||
const modalRef = React.useRef(modal)
|
const modalRef = React.useRef(modal)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
modalRef.current = modal
|
modalRef.current = modal
|
||||||
}, [modal])
|
}, [modal])
|
||||||
|
|
||||||
|
const setModalStableCallback = useEventCallback(
|
||||||
|
(nextModal: React.SetStateAction<React.JSX.Element | null>) => {
|
||||||
|
React.startTransition(() => {
|
||||||
|
setModal(nextModal)
|
||||||
|
setKey((currentKey) => currentKey + 1)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// This is NOT for optimization purposes - this is for debugging purposes,
|
// This is NOT for optimization purposes - this is for debugging purposes,
|
||||||
// so that a change of `modal` does not trigger VDOM changes everywhere in the page.
|
// so that a change of `modal` does not trigger VDOM changes everywhere in the page.
|
||||||
const setModalProvider = React.useMemo(
|
const setModalProvider = React.useMemo(
|
||||||
() => (
|
() => (
|
||||||
<ModalStaticProvider setModal={setModal} modalRef={modalRef}>
|
<ModalStaticProvider setModal={setModalStableCallback} modalRef={modalRef}>
|
||||||
{children}
|
{children}
|
||||||
</ModalStaticProvider>
|
</ModalStaticProvider>
|
||||||
),
|
),
|
||||||
[children],
|
[children, setModalStableCallback],
|
||||||
)
|
)
|
||||||
return <ModalContext.Provider value={{ modal }}>{setModalProvider}</ModalContext.Provider>
|
return <ModalContext.Provider value={{ modal, key }}>{setModalProvider}</ModalContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Props for a {@link ModalStaticProvider}. */
|
/** Props for a {@link ModalStaticProvider}. */
|
||||||
@ -79,8 +92,8 @@ function ModalStaticProvider(props: InternalModalStaticProviderProps) {
|
|||||||
|
|
||||||
/** A React context hook exposing the currently active modal, if one is currently visible. */
|
/** A React context hook exposing the currently active modal, if one is currently visible. */
|
||||||
export function useModal() {
|
export function useModal() {
|
||||||
const { modal } = React.useContext(ModalContext)
|
const { modal, key } = React.useContext(ModalContext)
|
||||||
return { modal } as const
|
return { modal, key } as const
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================
|
// ===================
|
||||||
|
Loading…
Reference in New Issue
Block a user