react-dom#unstable_batchedUpdates TypeScript Examples
The following examples show how to use
react-dom#unstable_batchedUpdates.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts From excalidraw with MIT License | 7 votes |
withBatchedUpdatesThrottled = <
TFunction extends ((event: any) => void) | (() => void),
>(
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
) => {
// @ts-ignore
return throttleRAF<Parameters<TFunction>>(((event) => {
unstable_batchedUpdates(func, event);
}) as TFunction);
}
Example #2
Source File: atom-with-pub-sub.ts From utopia with MIT License | 6 votes |
export function atomWithPubSub<T>(options: { key: string; defaultValue: T }): AtomWithPubSub<T> {
const { key, defaultValue } = options
if (key in GlobalAtomMap && !HMR) {
throw new Error(`Tried to create multiple atoms with the same key: ${key}`)
}
const newAtom: AtomWithPubSub<T> = {
key: key,
currentValue: defaultValue,
Provider: ({ children, value }) => {
const updateAtomSynchronously = usePubSubAtomWriteOnlyInner(newAtom, 'sync')
newAtom.currentValue = value // TODO this is sneaky and we should use API instead
useIsomorphicLayoutEffect(() => {
unstable_batchedUpdates(() => {
updateAtomSynchronously(() => value)
})
})
return React.createElement(React.Fragment, {}, children)
},
}
GlobalAtomMap[key] = newAtom
return newAtom
}
Example #3
Source File: tag-version-chooser.tsx From bedrock-dot-dev with GNU General Public License v3.0 | 6 votes |
TagVersionChooser: FunctionComponent<VersionSelectorProps> = ({ tags, setMajor, setMinor }) => {
const { t } = useTranslation('common')
const [ version, setVersion ] = useState(Tags.Stable)
const updateVersion = () => {
const [ major, minor ] = tags[version]
unstable_batchedUpdates(() => {
setMajor(major)
setMinor(minor)
})
}
useEffect(updateVersion, [ version ])
return (
<div className='w-full mb-2'>
<label className='block text-sm font-bold mb-2' htmlFor='tag'>
{t('component.version_chooser.tagged_version_title')}
</label>
<select id='tag' className='border-gray-300 rounded-md focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50 dark:text-gray-200 dark:bg-dark-gray-900 dark:border-dark-gray-800 leading-5 w-full ' value={version} onChange={({ target: { value } }) => setVersion(value as Tags)}>
<option value={Tags.Stable}>{tags.stable[1]} ({t('component.version_chooser.stable_string')})</option>
<option value={Tags.Beta}>{tags.beta[1]} ({t('component.version_chooser.beta_string')})</option>
</select>
</div>
)
}
Example #4
Source File: utils.ts From excalidraw with MIT License | 6 votes |
withBatchedUpdates = <
TFunction extends ((event: any) => void) | (() => void),
>(
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
) =>
((event) => {
unstable_batchedUpdates(func as TFunction, event);
}) as TFunction
Example #5
Source File: batchedUpdate.ts From redux-with-domain with MIT License | 5 votes |
export default function batchedUpdateMiddleware() {
return (next: Function) => (action: object) =>
unstable_batchedUpdates(() => next(action))
}
Example #6
Source File: Popover.tsx From excalidraw with MIT License | 5 votes |
Popover = ({
children,
left,
top,
onCloseRequest,
fitInViewport = false,
offsetLeft = 0,
offsetTop = 0,
viewportWidth = window.innerWidth,
viewportHeight = window.innerHeight,
}: Props) => {
const popoverRef = useRef<HTMLDivElement>(null);
// ensure the popover doesn't overflow the viewport
useLayoutEffect(() => {
if (fitInViewport && popoverRef.current) {
const element = popoverRef.current;
const { x, y, width, height } = element.getBoundingClientRect();
if (x + width - offsetLeft > viewportWidth) {
element.style.left = `${viewportWidth - width}px`;
}
if (y + height - offsetTop > viewportHeight) {
element.style.top = `${viewportHeight - height}px`;
}
}
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);
useEffect(() => {
if (onCloseRequest) {
const handler = (event: PointerEvent) => {
if (!popoverRef.current?.contains(event.target as Node)) {
unstable_batchedUpdates(() => onCloseRequest(event));
}
};
document.addEventListener("pointerdown", handler, false);
return () => document.removeEventListener("pointerdown", handler, false);
}
}, [onCloseRequest]);
return (
<div className="popover" style={{ top, left }} ref={popoverRef}>
{children}
</div>
);
}
Example #7
Source File: Popover.tsx From excalidraw-embed with MIT License | 5 votes |
Popover = ({
children,
left,
top,
onCloseRequest,
fitInViewport = false,
}: Props) => {
const popoverRef = useRef<HTMLDivElement>(null);
// ensure the popover doesn't overflow the viewport
useLayoutEffect(() => {
if (fitInViewport && popoverRef.current) {
const element = popoverRef.current;
const { x, y, width, height } = element.getBoundingClientRect();
const viewportWidth = window.innerWidth;
if (x + width > viewportWidth) {
element.style.left = `${viewportWidth - width}px`;
}
const viewportHeight = window.innerHeight;
if (y + height > viewportHeight) {
element.style.top = `${viewportHeight - height}px`;
}
}
}, [fitInViewport]);
useEffect(() => {
if (onCloseRequest) {
const handler = (event: PointerEvent) => {
if (!popoverRef.current?.contains(event.target as Node)) {
unstable_batchedUpdates(() => onCloseRequest(event));
}
};
document.addEventListener("pointerdown", handler, false);
return () => document.removeEventListener("pointerdown", handler, false);
}
}, [onCloseRequest]);
return (
<div className="popover" style={{ top: top, left: left }} ref={popoverRef}>
{children}
</div>
);
}
Example #8
Source File: patch.ts From store with MIT License | 5 votes |
Scheduler.batch = ( fn: Function ) => unstable_batchedUpdates ( () => batch ( fn ) );
Example #9
Source File: uplot-react-example.tsx From uplot-wrappers with MIT License | 5 votes |
HooksApp = () => {
const [options, setOptions] = useState<uPlot.Options>(useMemo(() => ({
title: 'Chart',
width: 400,
height: 300,
series: [{
label: 'Date'
}, {
label: '',
points: {show: false},
stroke: 'blue',
fill: 'blue'
}],
plugins: [dummyPlugin()],
scales: {x: {time: false}}
}), []));
const initialState = useMemo<uPlot.AlignedData>(() =>
([[...new Array(100000)].map((_, i) => i), [...new Array(100000)].map((_, i) => i % 1000)])
, []);
const [data, setData] = useState<uPlot.AlignedData>(initialState);
setTimeout(() => {
const newOptions = {
...options,
title: 'Rendered with hooks'
};
const newData: uPlot.AlignedData = [
[...data[0], data[0].length],
[...data[1], data[0].length % 1000]
];
unstable_batchedUpdates(() => {
setData(newData);
setOptions(newOptions);
});
}, 100);
return (<UPlotReact
key="hooks-key"
options={options}
data={data}
target={root}
onDelete={(/* chart: uPlot */) => console.log('Deleted from hooks')}
onCreate={(/* chart: uPlot */) => console.log('Created from hooks')}
/>);
}
Example #10
Source File: DndContext.tsx From dnd-kit with MIT License | 4 votes |
DndContext = memo(function DndContext({
id,
accessibility,
autoScroll = true,
children,
sensors = defaultSensors,
collisionDetection = rectIntersection,
measuring,
modifiers,
...props
}: Props) {
const store = useReducer(reducer, undefined, getInitialState);
const [state, dispatch] = store;
const [dispatchMonitorEvent, registerMonitorListener] =
useDndMonitorProvider();
const [status, setStatus] = useState<Status>(Status.Uninitialized);
const isInitialized = status === Status.Initialized;
const {
draggable: {active: activeId, nodes: draggableNodes, translate},
droppable: {containers: droppableContainers},
} = state;
const node = activeId ? draggableNodes.get(activeId) : null;
const activeRects = useRef<Active['rect']['current']>({
initial: null,
translated: null,
});
const active = useMemo<Active | null>(
() =>
activeId != null
? {
id: activeId,
// It's possible for the active node to unmount while dragging
data: node?.data ?? defaultData,
rect: activeRects,
}
: null,
[activeId, node]
);
const activeRef = useRef<UniqueIdentifier | null>(null);
const [activeSensor, setActiveSensor] = useState<SensorInstance | null>(null);
const [activatorEvent, setActivatorEvent] = useState<Event | null>(null);
const latestProps = useLatestValue(props, Object.values(props));
const draggableDescribedById = useUniqueId(`DndDescribedBy`, id);
const enabledDroppableContainers = useMemo(
() => droppableContainers.getEnabled(),
[droppableContainers]
);
const measuringConfiguration = useMeasuringConfiguration(measuring);
const {droppableRects, measureDroppableContainers, measuringScheduled} =
useDroppableMeasuring(enabledDroppableContainers, {
dragging: isInitialized,
dependencies: [translate.x, translate.y],
config: measuringConfiguration.droppable,
});
const activeNode = useCachedNode(draggableNodes, activeId);
const activationCoordinates = useMemo(
() => (activatorEvent ? getEventCoordinates(activatorEvent) : null),
[activatorEvent]
);
const autoScrollOptions = getAutoScrollerOptions();
const initialActiveNodeRect = useInitialRect(
activeNode,
measuringConfiguration.draggable.measure
);
useLayoutShiftScrollCompensation({
activeNode: activeId ? draggableNodes.get(activeId) : null,
config: autoScrollOptions.layoutShiftCompensation,
initialRect: initialActiveNodeRect,
measure: measuringConfiguration.draggable.measure,
});
const activeNodeRect = useRect(
activeNode,
measuringConfiguration.draggable.measure,
initialActiveNodeRect
);
const containerNodeRect = useRect(
activeNode ? activeNode.parentElement : null
);
const sensorContext = useRef<SensorContext>({
activatorEvent: null,
active: null,
activeNode,
collisionRect: null,
collisions: null,
droppableRects,
draggableNodes,
draggingNode: null,
draggingNodeRect: null,
droppableContainers,
over: null,
scrollableAncestors: [],
scrollAdjustedTranslate: null,
});
const overNode = droppableContainers.getNodeFor(
sensorContext.current.over?.id
);
const dragOverlay = useDragOverlayMeasuring({
measure: measuringConfiguration.dragOverlay.measure,
});
// Use the rect of the drag overlay if it is mounted
const draggingNode = dragOverlay.nodeRef.current ?? activeNode;
const draggingNodeRect = isInitialized
? dragOverlay.rect ?? activeNodeRect
: null;
const usesDragOverlay = Boolean(
dragOverlay.nodeRef.current && dragOverlay.rect
);
// The delta between the previous and new position of the draggable node
// is only relevant when there is no drag overlay
const nodeRectDelta = useRectDelta(usesDragOverlay ? null : activeNodeRect);
// Get the window rect of the dragging node
const windowRect = useWindowRect(
draggingNode ? getWindow(draggingNode) : null
);
// Get scrollable ancestors of the dragging node
const scrollableAncestors = useScrollableAncestors(
isInitialized ? overNode ?? activeNode : null
);
const scrollableAncestorRects = useRects(scrollableAncestors);
// Apply modifiers
const modifiedTranslate = applyModifiers(modifiers, {
transform: {
x: translate.x - nodeRectDelta.x,
y: translate.y - nodeRectDelta.y,
scaleX: 1,
scaleY: 1,
},
activatorEvent,
active,
activeNodeRect,
containerNodeRect,
draggingNodeRect,
over: sensorContext.current.over,
overlayNodeRect: dragOverlay.rect,
scrollableAncestors,
scrollableAncestorRects,
windowRect,
});
const pointerCoordinates = activationCoordinates
? add(activationCoordinates, translate)
: null;
const scrollOffsets = useScrollOffsets(scrollableAncestors);
// Represents the scroll delta since dragging was initiated
const scrollAdjustment = useScrollOffsetsDelta(scrollOffsets);
// Represents the scroll delta since the last time the active node rect was measured
const activeNodeScrollDelta = useScrollOffsetsDelta(scrollOffsets, [
activeNodeRect,
]);
const scrollAdjustedTranslate = add(modifiedTranslate, scrollAdjustment);
const collisionRect = draggingNodeRect
? getAdjustedRect(draggingNodeRect, modifiedTranslate)
: null;
const collisions =
active && collisionRect
? collisionDetection({
active,
collisionRect,
droppableRects,
droppableContainers: enabledDroppableContainers,
pointerCoordinates,
})
: null;
const overId = getFirstCollision(collisions, 'id');
const [over, setOver] = useState<Over | null>(null);
// When there is no drag overlay used, we need to account for the
// window scroll delta
const appliedTranslate = usesDragOverlay
? modifiedTranslate
: add(modifiedTranslate, activeNodeScrollDelta);
const transform = adjustScale(
appliedTranslate,
over?.rect ?? null,
activeNodeRect
);
const instantiateSensor = useCallback(
(
event: React.SyntheticEvent,
{sensor: Sensor, options}: SensorDescriptor<any>
) => {
if (activeRef.current == null) {
return;
}
const activeNode = draggableNodes.get(activeRef.current);
if (!activeNode) {
return;
}
const activatorEvent = event.nativeEvent;
const sensorInstance = new Sensor({
active: activeRef.current,
activeNode,
event: activatorEvent,
options,
// Sensors need to be instantiated with refs for arguments that change over time
// otherwise they are frozen in time with the stale arguments
context: sensorContext,
onStart(initialCoordinates) {
const id = activeRef.current;
if (id == null) {
return;
}
const draggableNode = draggableNodes.get(id);
if (!draggableNode) {
return;
}
const {onDragStart} = latestProps.current;
const event: DragStartEvent = {
active: {id, data: draggableNode.data, rect: activeRects},
};
unstable_batchedUpdates(() => {
onDragStart?.(event);
setStatus(Status.Initializing);
dispatch({
type: Action.DragStart,
initialCoordinates,
active: id,
});
dispatchMonitorEvent({type: 'onDragStart', event});
});
},
onMove(coordinates) {
dispatch({
type: Action.DragMove,
coordinates,
});
},
onEnd: createHandler(Action.DragEnd),
onCancel: createHandler(Action.DragCancel),
});
unstable_batchedUpdates(() => {
setActiveSensor(sensorInstance);
setActivatorEvent(event.nativeEvent);
});
function createHandler(type: Action.DragEnd | Action.DragCancel) {
return async function handler() {
const {active, collisions, over, scrollAdjustedTranslate} =
sensorContext.current;
let event: DragEndEvent | null = null;
if (active && scrollAdjustedTranslate) {
const {cancelDrop} = latestProps.current;
event = {
activatorEvent,
active: active,
collisions,
delta: scrollAdjustedTranslate,
over,
};
if (type === Action.DragEnd && typeof cancelDrop === 'function') {
const shouldCancel = await Promise.resolve(cancelDrop(event));
if (shouldCancel) {
type = Action.DragCancel;
}
}
}
activeRef.current = null;
unstable_batchedUpdates(() => {
dispatch({type});
setStatus(Status.Uninitialized);
setOver(null);
setActiveSensor(null);
setActivatorEvent(null);
const eventName =
type === Action.DragEnd ? 'onDragEnd' : 'onDragCancel';
if (event) {
const handler = latestProps.current[eventName];
handler?.(event);
dispatchMonitorEvent({type: eventName, event});
}
});
};
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[draggableNodes]
);
const bindActivatorToSensorInstantiator = useCallback(
(
handler: SensorActivatorFunction<any>,
sensor: SensorDescriptor<any>
): SyntheticListener['handler'] => {
return (event, active) => {
const nativeEvent = event.nativeEvent as DndEvent;
const activeDraggableNode = draggableNodes.get(active);
if (
// Another sensor is already instantiating
activeRef.current !== null ||
// No active draggable
!activeDraggableNode ||
// Event has already been captured
nativeEvent.dndKit ||
nativeEvent.defaultPrevented
) {
return;
}
const activationContext = {
active: activeDraggableNode,
};
const shouldActivate = handler(
event,
sensor.options,
activationContext
);
if (shouldActivate === true) {
nativeEvent.dndKit = {
capturedBy: sensor.sensor,
};
activeRef.current = active;
instantiateSensor(event, sensor);
}
};
},
[draggableNodes, instantiateSensor]
);
const activators = useCombineActivators(
sensors,
bindActivatorToSensorInstantiator
);
useSensorSetup(sensors);
useIsomorphicLayoutEffect(() => {
if (activeNodeRect && status === Status.Initializing) {
setStatus(Status.Initialized);
}
}, [activeNodeRect, status]);
useEffect(
() => {
const {onDragMove} = latestProps.current;
const {active, activatorEvent, collisions, over} = sensorContext.current;
if (!active || !activatorEvent) {
return;
}
const event: DragMoveEvent = {
active,
activatorEvent,
collisions,
delta: {
x: scrollAdjustedTranslate.x,
y: scrollAdjustedTranslate.y,
},
over,
};
unstable_batchedUpdates(() => {
onDragMove?.(event);
dispatchMonitorEvent({type: 'onDragMove', event});
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[scrollAdjustedTranslate.x, scrollAdjustedTranslate.y]
);
useEffect(
() => {
const {
active,
activatorEvent,
collisions,
droppableContainers,
scrollAdjustedTranslate,
} = sensorContext.current;
if (
!active ||
activeRef.current == null ||
!activatorEvent ||
!scrollAdjustedTranslate
) {
return;
}
const {onDragOver} = latestProps.current;
const overContainer = droppableContainers.get(overId);
const over =
overContainer && overContainer.rect.current
? {
id: overContainer.id,
rect: overContainer.rect.current,
data: overContainer.data,
disabled: overContainer.disabled,
}
: null;
const event: DragOverEvent = {
active,
activatorEvent,
collisions,
delta: {
x: scrollAdjustedTranslate.x,
y: scrollAdjustedTranslate.y,
},
over,
};
unstable_batchedUpdates(() => {
setOver(over);
onDragOver?.(event);
dispatchMonitorEvent({type: 'onDragOver', event});
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[overId]
);
useIsomorphicLayoutEffect(() => {
sensorContext.current = {
activatorEvent,
active,
activeNode,
collisionRect,
collisions,
droppableRects,
draggableNodes,
draggingNode,
draggingNodeRect,
droppableContainers,
over,
scrollableAncestors,
scrollAdjustedTranslate,
};
activeRects.current = {
initial: draggingNodeRect,
translated: collisionRect,
};
}, [
active,
activeNode,
collisions,
collisionRect,
draggableNodes,
draggingNode,
draggingNodeRect,
droppableRects,
droppableContainers,
over,
scrollableAncestors,
scrollAdjustedTranslate,
]);
useAutoScroller({
...autoScrollOptions,
delta: translate,
draggingRect: collisionRect,
pointerCoordinates,
scrollableAncestors,
scrollableAncestorRects,
});
const publicContext = useMemo(() => {
const context: PublicContextDescriptor = {
active,
activeNode,
activeNodeRect,
activatorEvent,
collisions,
containerNodeRect,
dragOverlay,
draggableNodes,
droppableContainers,
droppableRects,
over,
measureDroppableContainers,
scrollableAncestors,
scrollableAncestorRects,
measuringConfiguration,
measuringScheduled,
windowRect,
};
return context;
}, [
active,
activeNode,
activeNodeRect,
activatorEvent,
collisions,
containerNodeRect,
dragOverlay,
draggableNodes,
droppableContainers,
droppableRects,
over,
measureDroppableContainers,
scrollableAncestors,
scrollableAncestorRects,
measuringConfiguration,
measuringScheduled,
windowRect,
]);
const internalContext = useMemo(() => {
const context: InternalContextDescriptor = {
activatorEvent,
activators,
active,
activeNodeRect,
ariaDescribedById: {
draggable: draggableDescribedById,
},
dispatch,
draggableNodes,
over,
measureDroppableContainers,
};
return context;
}, [
activatorEvent,
activators,
active,
activeNodeRect,
dispatch,
draggableDescribedById,
draggableNodes,
over,
measureDroppableContainers,
]);
return (
<DndMonitorContext.Provider value={registerMonitorListener}>
<InternalContext.Provider value={internalContext}>
<PublicContext.Provider value={publicContext}>
<ActiveDraggableContext.Provider value={transform}>
{children}
</ActiveDraggableContext.Provider>
</PublicContext.Provider>
<RestoreFocus disabled={accessibility?.restoreFocus === false} />
</InternalContext.Provider>
<Accessibility
{...accessibility}
hiddenTextDescribedById={draggableDescribedById}
/>
</DndMonitorContext.Provider>
);
function getAutoScrollerOptions() {
const activeSensorDisablesAutoscroll =
activeSensor?.autoScrollEnabled === false;
const autoScrollGloballyDisabled =
typeof autoScroll === 'object'
? autoScroll.enabled === false
: autoScroll === false;
const enabled =
isInitialized &&
!activeSensorDisablesAutoscroll &&
!autoScrollGloballyDisabled;
if (typeof autoScroll === 'object') {
return {
...autoScroll,
enabled,
};
}
return {enabled};
}
})
Example #11
Source File: MultipleContainers.tsx From dnd-kit with MIT License | 4 votes |
export function MultipleContainers({
adjustScale = false,
itemCount = 3,
cancelDrop,
columns,
handle = false,
items: initialItems,
containerStyle,
coordinateGetter = multipleContainersCoordinateGetter,
getItemStyles = () => ({}),
wrapperStyle = () => ({}),
minimal = false,
modifiers,
renderItem,
strategy = verticalListSortingStrategy,
trashable = false,
vertical = false,
scrollable,
}: Props) {
const [items, setItems] = useState<Items>(
() =>
initialItems ?? {
A: createRange(itemCount, (index) => `A${index + 1}`),
B: createRange(itemCount, (index) => `B${index + 1}`),
C: createRange(itemCount, (index) => `C${index + 1}`),
D: createRange(itemCount, (index) => `D${index + 1}`),
}
);
const [containers, setContainers] = useState(
Object.keys(items) as UniqueIdentifier[]
);
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
const lastOverId = useRef<UniqueIdentifier | null>(null);
const recentlyMovedToNewContainer = useRef(false);
const isSortingContainer = activeId ? containers.includes(activeId) : false;
/**
* Custom collision detection strategy optimized for multiple containers
*
* - First, find any droppable containers intersecting with the pointer.
* - If there are none, find intersecting containers with the active draggable.
* - If there are no intersecting containers, return the last matched intersection
*
*/
const collisionDetectionStrategy: CollisionDetection = useCallback(
(args) => {
if (activeId && activeId in items) {
return closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container) => container.id in items
),
});
}
// Start by finding any intersecting droppable
const pointerIntersections = pointerWithin(args);
const intersections =
pointerIntersections.length > 0
? // If there are droppables intersecting with the pointer, return those
pointerIntersections
: rectIntersection(args);
let overId = getFirstCollision(intersections, 'id');
if (overId != null) {
if (overId === TRASH_ID) {
// If the intersecting droppable is the trash, return early
// Remove this if you're not using trashable functionality in your app
return intersections;
}
if (overId in items) {
const containerItems = items[overId];
// If a container is matched and it contains items (columns 'A', 'B', 'C')
if (containerItems.length > 0) {
// Return the closest droppable within that container
overId = closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container) =>
container.id !== overId &&
containerItems.includes(container.id)
),
})[0]?.id;
}
}
lastOverId.current = overId;
return [{id: overId}];
}
// When a draggable item moves to a new container, the layout may shift
// and the `overId` may become `null`. We manually set the cached `lastOverId`
// to the id of the draggable item that was moved to the new container, otherwise
// the previous `overId` will be returned which can cause items to incorrectly shift positions
if (recentlyMovedToNewContainer.current) {
lastOverId.current = activeId;
}
// If no droppable is matched, return the last match
return lastOverId.current ? [{id: lastOverId.current}] : [];
},
[activeId, items]
);
const [clonedItems, setClonedItems] = useState<Items | null>(null);
const sensors = useSensors(
useSensor(MouseSensor),
useSensor(TouchSensor),
useSensor(KeyboardSensor, {
coordinateGetter,
})
);
const findContainer = (id: UniqueIdentifier) => {
if (id in items) {
return id;
}
return Object.keys(items).find((key) => items[key].includes(id));
};
const getIndex = (id: UniqueIdentifier) => {
const container = findContainer(id);
if (!container) {
return -1;
}
const index = items[container].indexOf(id);
return index;
};
const onDragCancel = () => {
if (clonedItems) {
// Reset items to their original state in case items have been
// Dragged across containers
setItems(clonedItems);
}
setActiveId(null);
setClonedItems(null);
};
useEffect(() => {
requestAnimationFrame(() => {
recentlyMovedToNewContainer.current = false;
});
}, [items]);
return (
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
measuring={{
droppable: {
strategy: MeasuringStrategy.Always,
},
}}
onDragStart={({active}) => {
setActiveId(active.id);
setClonedItems(items);
}}
onDragOver={({active, over}) => {
const overId = over?.id;
if (overId == null || overId === TRASH_ID || active.id in items) {
return;
}
const overContainer = findContainer(overId);
const activeContainer = findContainer(active.id);
if (!overContainer || !activeContainer) {
return;
}
if (activeContainer !== overContainer) {
setItems((items) => {
const activeItems = items[activeContainer];
const overItems = items[overContainer];
const overIndex = overItems.indexOf(overId);
const activeIndex = activeItems.indexOf(active.id);
let newIndex: number;
if (overId in items) {
newIndex = overItems.length + 1;
} else {
const isBelowOverItem =
over &&
active.rect.current.translated &&
active.rect.current.translated.top >
over.rect.top + over.rect.height;
const modifier = isBelowOverItem ? 1 : 0;
newIndex =
overIndex >= 0 ? overIndex + modifier : overItems.length + 1;
}
recentlyMovedToNewContainer.current = true;
return {
...items,
[activeContainer]: items[activeContainer].filter(
(item) => item !== active.id
),
[overContainer]: [
...items[overContainer].slice(0, newIndex),
items[activeContainer][activeIndex],
...items[overContainer].slice(
newIndex,
items[overContainer].length
),
],
};
});
}
}}
onDragEnd={({active, over}) => {
if (active.id in items && over?.id) {
setContainers((containers) => {
const activeIndex = containers.indexOf(active.id);
const overIndex = containers.indexOf(over.id);
return arrayMove(containers, activeIndex, overIndex);
});
}
const activeContainer = findContainer(active.id);
if (!activeContainer) {
setActiveId(null);
return;
}
const overId = over?.id;
if (overId == null) {
setActiveId(null);
return;
}
if (overId === TRASH_ID) {
setItems((items) => ({
...items,
[activeContainer]: items[activeContainer].filter(
(id) => id !== activeId
),
}));
setActiveId(null);
return;
}
if (overId === PLACEHOLDER_ID) {
const newContainerId = getNextContainerId();
unstable_batchedUpdates(() => {
setContainers((containers) => [...containers, newContainerId]);
setItems((items) => ({
...items,
[activeContainer]: items[activeContainer].filter(
(id) => id !== activeId
),
[newContainerId]: [active.id],
}));
setActiveId(null);
});
return;
}
const overContainer = findContainer(overId);
if (overContainer) {
const activeIndex = items[activeContainer].indexOf(active.id);
const overIndex = items[overContainer].indexOf(overId);
if (activeIndex !== overIndex) {
setItems((items) => ({
...items,
[overContainer]: arrayMove(
items[overContainer],
activeIndex,
overIndex
),
}));
}
}
setActiveId(null);
}}
cancelDrop={cancelDrop}
onDragCancel={onDragCancel}
modifiers={modifiers}
>
<div
style={{
display: 'inline-grid',
boxSizing: 'border-box',
padding: 20,
gridAutoFlow: vertical ? 'row' : 'column',
}}
>
<SortableContext
items={[...containers, PLACEHOLDER_ID]}
strategy={
vertical
? verticalListSortingStrategy
: horizontalListSortingStrategy
}
>
{containers.map((containerId) => (
<DroppableContainer
key={containerId}
id={containerId}
label={minimal ? undefined : `Column ${containerId}`}
columns={columns}
items={items[containerId]}
scrollable={scrollable}
style={containerStyle}
unstyled={minimal}
onRemove={() => handleRemove(containerId)}
>
<SortableContext items={items[containerId]} strategy={strategy}>
{items[containerId].map((value, index) => {
return (
<SortableItem
disabled={isSortingContainer}
key={value}
id={value}
index={index}
handle={handle}
style={getItemStyles}
wrapperStyle={wrapperStyle}
renderItem={renderItem}
containerId={containerId}
getIndex={getIndex}
/>
);
})}
</SortableContext>
</DroppableContainer>
))}
{minimal ? undefined : (
<DroppableContainer
id={PLACEHOLDER_ID}
disabled={isSortingContainer}
items={empty}
onClick={handleAddColumn}
placeholder
>
+ Add column
</DroppableContainer>
)}
</SortableContext>
</div>
{createPortal(
<DragOverlay adjustScale={adjustScale} dropAnimation={dropAnimation}>
{activeId
? containers.includes(activeId)
? renderContainerDragOverlay(activeId)
: renderSortableItemDragOverlay(activeId)
: null}
</DragOverlay>,
document.body
)}
{trashable && activeId && !containers.includes(activeId) ? (
<Trash id={TRASH_ID} />
) : null}
</DndContext>
);
function renderSortableItemDragOverlay(id: UniqueIdentifier) {
return (
<Item
value={id}
handle={handle}
style={getItemStyles({
containerId: findContainer(id) as UniqueIdentifier,
overIndex: -1,
index: getIndex(id),
value: id,
isSorting: true,
isDragging: true,
isDragOverlay: true,
})}
color={getColor(id)}
wrapperStyle={wrapperStyle({index: 0})}
renderItem={renderItem}
dragOverlay
/>
);
}
function renderContainerDragOverlay(containerId: UniqueIdentifier) {
return (
<Container
label={`Column ${containerId}`}
columns={columns}
style={{
height: '100%',
}}
shadow
unstyled={false}
>
{items[containerId].map((item, index) => (
<Item
key={item}
value={item}
handle={handle}
style={getItemStyles({
containerId,
overIndex: -1,
index: getIndex(item),
value: item,
isDragging: false,
isSorting: false,
isDragOverlay: false,
})}
color={getColor(item)}
wrapperStyle={wrapperStyle({index})}
renderItem={renderItem}
/>
))}
</Container>
);
}
function handleRemove(containerID: UniqueIdentifier) {
setContainers((containers) =>
containers.filter((id) => id !== containerID)
);
}
function handleAddColumn() {
const newContainerId = getNextContainerId();
unstable_batchedUpdates(() => {
setContainers((containers) => [...containers, newContainerId]);
setItems((items) => ({
...items,
[newContainerId]: [],
}));
});
}
function getNextContainerId() {
const containerIds = Object.keys(items);
const lastContainerId = containerIds[containerIds.length - 1];
return String.fromCharCode(lastContainerId.charCodeAt(0) + 1);
}
}
Example #12
Source File: sidebar.tsx From bedrock-dot-dev with GNU General Public License v3.0 | 4 votes |
Sidebar: FunctionComponent<Props> = ({ sidebar, file, loading }) => {
if (!sidebar) return null
const [filter, setFilter] = useState('')
const [mounted, setMounted] = useState(false)
const mobile = useIsMobile()
const { open } = useContext(SidebarContext)
// reset when the page changes
useEffect(() => {
unstable_batchedUpdates(() => {
setFilter('')
setMounted(true)
})
}, [ file ])
useEffect(() => {
if (mobile) document.body.style.overflow = open ? 'hidden' : 'initial'
}, [ open ])
// on unmount ensure scrolling
useEffect(() => {
return () => {
document.body.style.overflow = 'initial'
}
}, [])
const loadingContent = (
<div className='flex-1 flex px-4 py-4'>
<div className='animate-pulse w-full'>
<div className='w-4/5 bg-gray-100 dark:bg-dark-gray-800 h-8' />
<div className='w-2/3 bg-gray-100 dark:bg-dark-gray-800 h-3 mt-4' />
<div className='w-5/6 bg-gray-100 dark:bg-dark-gray-800 h-3 mt-4' />
<div className='w-1/2 bg-gray-100 dark:bg-dark-gray-800 h-3 mt-4' />
<div className='w-3/5 bg-gray-100 dark:bg-dark-gray-800 h-8 mt-10' />
<div className='w-2/4 bg-gray-100 dark:bg-dark-gray-800 h-s mt-4' />
<div className='w-2/3 bg-gray-100 dark:bg-dark-gray-800 h-3 mt-4' />
<div className='w-3/4 bg-gray-100 dark:bg-dark-gray-800 h-3 mt-4' />
<div className='w-2/3 bg-gray-100 dark:bg-dark-gray-800 h-3 mt-4' />
<div className='w-3/5 bg-gray-100 dark:bg-dark-gray-800 h-3 mt-4' />
</div>
</div>
)
const loadingSelectors = (
<div className='animate-pulse w-full'>
<div className='flex flex-row'>
<div className='w-2/4 bg-gray-100 dark:bg-dark-gray-800 h-8' />
<div className='w-2/4 bg-gray-100 dark:bg-dark-gray-800 h-8 ml-2' />
</div>
<div className='w-4/5 bg-gray-100 dark:bg-dark-gray-800 h-8 mt-4' />
</div>
)
return (
<>
{ open && mobile && <SidebarMask /> }
<aside className={cn('sidebar', { open, mounted })}>
<div className='w-full p-4 border-b border-gray-200 dark:border-dark-gray-800'>
{loading || !Object.keys(sidebar).length ? loadingSelectors : (
<>
<Selectors />
<SidebarFilter setValue={setFilter} value={filter} />
</>
)}
</div>
{ loading ? loadingContent : (
<>
<SidebarContent search={filter} sidebar={sidebar} file={file} />
<div className='flex flex-row justify-end items-center bg-white dark:bg-dark-gray-950 w-full px-4 py-2 border-t border-gray-200 dark:border-dark-gray-800 bottom-safe-area-inset inset-2'>
<LanguageSelect />
<ModeSelect className='ml-2' />
</div>
</>
) }
</aside>
</>
)
}
Example #13
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
Material: React.FC<MaterialProps> = (props) => {
const [materalList, setMateralList] = useState<Material.Item[]>([])
const [filterVisible, setFilterVisible] = useState<boolean>(false)
const [modalFormVisible, setModalFormVisible] = useState<boolean>(false)
const [targetUpdateMaterial, setTargetUpdateMaterial] = useState<Material.Item>({} as Material.Item)
const [targetDeleteMaterial, setTargetDeleteMaterial] = useState<Material.Item>({} as Material.Item)
const [tagModalVisible, setTagModalVisible] = useState(false)
const [choosedTags, setChoosedTags] = useState<MaterialTag.Item[]>([])// 新增&修改素材时选择的标签
const [searchTags, setSearchTags] = useState<MaterialTag.Item[]>([])
const realSearchTags = useRef<MaterialTag.Item[]>([])
const [tagsForFilter, setTagsForFilter] = useState<MaterialTag.Item[]>([]) // 筛选标签的dropdown里的所有标签
const [keyword, setKeyword] = useState<string>('');
const [showSearchTags, setShowSearchTags] = useState(false)
const [linkFetching, setLinkFetching] = useState(false);
const [fileInfo, setFileInfo] = useState({fileName: '', fileSize: ''})
const [materialLoading, setMaterialLoading] = useState(true)
const modalFormRef = useRef<FormInstance>()
const {allTags, allTagsMap} = useContext(TagContext)
useEffect(() => {
const filteredTags = allTags.filter((tag) => {
if (keyword.trim() === '') {
return true;
}
if (tag.name?.includes(keyword)) {
return true;
}
return false;
});
setTagsForFilter(filteredTags);
}, [allTags, keyword])
// 查询素材列表
const queryMaterialList = (material_tag_list?: string[], title?: string) => {
QueryMaterialList({
page_size: 5000,
material_type: fileTypeMap[props.fileType].material_type,
material_tag_list,
title
}).then(res => {
setMaterialLoading(false)
if (res?.code === 0 && res?.data) {
setMateralList(res?.data?.items || [])
} else {
message.error(res?.message)
}
})
}
useEffect(() => {
setMaterialLoading(true)
queryMaterialList()
realSearchTags.current = []
setSearchTags([])
}, [props.fileType, allTags])
// 处理修改 删除的目标素材
const operateMaterial = (targetMaterial: Material.Item, operation: string) => {
setFileInfo({fileName: targetMaterial.title, fileSize: targetMaterial.file_size})
if (operation === 'update') {
setModalFormVisible(true)
setTargetUpdateMaterial(targetMaterial)
setChoosedTags(targetMaterial?.material_tag_list?.map((tagId: string) =>
allTagsMap[tagId]
))
setTimeout(() => {
modalFormRef.current?.setFieldsValue({
...targetMaterial,
file_url: targetMaterial.url,
file_size: targetMaterial.file_size
})
}, 100)
}
if (operation === 'delete') {
setTargetDeleteMaterial(targetMaterial)
}
}
return (
<TagContext.Consumer>
{
(contextValue) => (
<div>
<div>
<div className={styles.topNav}>
<div className={styles.topNavTitle}>
{props.fileType}素材(共{materalList?.length}篇)
</div>
<div className={styles.topNavOperator}>
<Input.Search placeholder={`搜索${props.fileType}标题`} style={{width: 300}} onSearch={(value) => {
queryMaterialList(realSearchTags.current.map(tag => tag?.id), value)
}}/>
<Dropdown
visible={filterVisible}
overlay={
<div className={styles.overlay}>
<div className={styles.overlayTitle}>素材标签 ( {contextValue.allTags.length} )</div>
{/* 筛选标签 */}
<Form
layout={'horizontal'}
>
<Input
allowClear={true}
placeholder={'输入关键词搜索标签'}
value={keyword}
onChange={(e) => {
setKeyword(e.currentTarget.value)
}}
style={{width: 320, marginLeft: 10}}
/>
<div style={{padding: "14px 4px"}}>
{tagsForFilter?.map((tag) => {
const isSelected = searchTags.map((searchTag) => searchTag.id)?.includes(tag?.id);
return (
<Space direction={'horizontal'} wrap={true}>
<Tag
className={`tag-item ${isSelected ? ' selected-tag-item' : ''}`}
style={{cursor: 'pointer', margin: '6px'}}
key={tag.id}
onClick={() => {
if (tag?.id && isSelected) {
setSearchTags(searchTags.filter((searchTag) => {
return searchTag.id !== tag?.id
}))
} else {
setSearchTags([...searchTags, tag])
}
}}
>
{tag.name}
</Tag>
</Space>
)
})}
</div>
{contextValue.allTags?.length === 0 &&
<Empty style={{marginTop: 36, marginBottom: 36}} image={Empty.PRESENTED_IMAGE_SIMPLE}/>}
<div style={{display: 'flex', justifyContent: 'flex-end'}}>
<Button onClick={() => setFilterVisible(false)}>取消</Button>
<Button
style={{marginLeft: 6}}
type='primary'
htmlType="submit"
onClick={() => {
setFilterVisible(false)
setShowSearchTags(true)
realSearchTags.current = searchTags
queryMaterialList(realSearchTags.current.map(tag => tag?.id) || [])
}}>完成</Button>
</div>
</Form>
</div>
} trigger={['click']}>
<div>
<Button
style={{margin: '0px 6px'}}
onClick={() => {
setFilterVisible(!filterVisible)
}}>筛选</Button>
</div>
</Dropdown>
<Button type={'primary'} onClick={() => {
setModalFormVisible(true)
}}>添加{props.fileType}</Button>
</div>
</div>
</div>
<div>
{
realSearchTags.current.length > 0 && showSearchTags ? <div className={styles.filterTagBox}>
{
realSearchTags.current.map(tag =>
<Tag
key={tag.id}
className={'tag-item selected-tag-item'}
>
{tag.name}
<span>
<CloseOutlined
style={{fontSize: '12px', cursor: 'pointer'}}
onClick={() => {
realSearchTags.current = realSearchTags.current.filter((t) => {
return t.id !== tag?.id
})
setSearchTags(realSearchTags.current)
queryMaterialList(realSearchTags.current.map(t => t?.id) || [])
}}
/>
</span>
</Tag>
)
}
<Button
type={'link'}
icon={<ClearOutlined/>}
style={{display: (showSearchTags && realSearchTags.current.length > 0) ? 'inline-block' : 'none'}}
onClick={() => {
setShowSearchTags(false)
setSearchTags([])
queryMaterialList()
}}>清空筛选</Button>
</div>
:
<div style={{margin: 0}}/>
}
{/* 素材列表 */}
<Spin spinning={materialLoading} style={{marginTop:50}}>
<div className={styles.articles}>
{
materalList?.map((item) => <MaterialCard {...item} callback={operateMaterial}/>)
}
</div>
</Spin>
{
materalList?.length === 0 && !materialLoading && <Empty style={{marginTop: 100}} image={Empty.PRESENTED_IMAGE_SIMPLE}/>
}
</div>
{/* 修改素材弹窗 */}
<ModalForm
formRef={modalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
visible={modalFormVisible}
onVisibleChange={(visible) => {
if (!visible) {
setTargetUpdateMaterial({} as Material.Item)
setChoosedTags([])
}
setModalFormVisible(visible)
modalFormRef.current?.resetFields()
}}
// @ts-ignore
onFinish={(params) => {
if (targetUpdateMaterial.id) {
const tagIdList = choosedTags.map(tag => {
return tag?.id
})
UpdateMaterial({
material_type: fileTypeMap[props.fileType].material_type, ...params,
id: targetUpdateMaterial.id,
material_tag_list: choosedTags.length > 0 ? tagIdList : [],
})
.then(res => {
if (res?.code === 0) {
message.success(`修改${props.fileType}成功`)
unstable_batchedUpdates(() => {
queryMaterialList(searchTags.map(tag => tag?.id) || [])
setModalFormVisible(false)
setChoosedTags([])
setTargetUpdateMaterial({} as Material.Item)
})
} else {
message.error(res.message)
}
})
} else {
const tagIdList = choosedTags.map(tag => {
return tag?.id
})
CreateMaterial({
material_type: fileTypeMap[props.fileType].material_type, ...params,
material_tag_list: choosedTags.length > 0 ? tagIdList : [],
})
.then(res => {
if (res?.code === 0) {
message.success(`新增${props.fileType}成功`)
unstable_batchedUpdates(() => {
queryMaterialList(searchTags.map(tag => tag?.id) || [])
setModalFormVisible(false)
setChoosedTags([])
})
} else {
message.error(res.message)
}
})
}
}}
>
{
// 修改链接素材 弹窗内容
props.fileType === '链接' && <div key={props.fileType}>
<Spin spinning={linkFetching}>
<h2 className='dialog-title'> {targetUpdateMaterial.id ? '修改链接' : '添加链接'} </h2>
<ProForm.Item initialValue={'link'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='link'
label='链接地址'
placeholder="链接地址以http(s)开头"
width='md'
fieldProps={{
disabled: linkFetching,
addonAfter: (
<Tooltip title="点击抓取远程链接,自动填充标题,描述,图片">
<div
onClick={async () => {
setLinkFetching(true);
const res = await ParseURL(modalFormRef.current?.getFieldValue('link'))
setLinkFetching(false);
if (res.code !== 0) {
message.error(res.message);
} else {
message.success('解析链接成功');
modalFormRef?.current?.setFieldsValue({
customer_link_enable: 1,
title: res.data.title,
digest: res.data.desc,
file_url: res.data.img_url,
})
}
}}
style={{
cursor: "pointer",
width: 32,
height: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<SyncOutlined/>
</div>
</Tooltip>
)
}}
rules={[
{
required: true,
message: '请输入链接地址',
},
{
type: 'url',
message: '请填写正确的的URL,必须是http或https开头',
},
]}
/>
<ProFormText
name='title'
label='链接标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormTextArea
name='digest'
label='链接描述'
width='md'
/>
<Form.Item
label='链接封面'
name='file_url'
rules={[
{
required: true,
message: '请上传链接图片!',
},
]}
>
<Uploader
fileType='formImage'
fileInfo={fileInfo}
setFileInfo={setFileInfo}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
modalFormRef?.current?.setFieldsValue({
file_url: getUploadUrlRes?.data?.download_url,
file_size: String(file.size)
})
return;
}
message.error('上传图片失败');
return;
} catch (e) {
message.error('上传图片失败');
}
}}
/>
</Form.Item>
<div style={{display: 'none'}}>
<ProFormText name='file_size'/>
</div>
</Spin>
</div>
}
{
// 修改海报素材 弹窗内容
props.fileType === '海报' && <div key={props.fileType}>
<h2 className='dialog-title'> {targetUpdateMaterial.id ? '修改海报' : '添加海报'} </h2>
<ProForm.Item
name='file_url'
>
<Uploader
fileType='海报'
fileInfo={fileInfo}
setFileInfo={setFileInfo}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
modalFormRef?.current?.setFieldsValue({
file_url: getUploadUrlRes?.data?.download_url,
file_size: String(file.size),
title: file.name
})
return;
}
message.error('上传图片失败');
return;
} catch (e) {
message.error('上传图片失败');
}
}}
/>
</ProForm.Item>
<div style={{display: 'none'}}>
<ProFormText name='file_size'/>
</div>
<div style={{display: 'none'}}>
{/* 文件名 */}
<ProFormText name='title'/>
</div>
</div>
}
{
// 修改视频素材 弹窗内容
props.fileType === '视频' && <div key={props.fileType}>
<h2 className='dialog-title'> {targetUpdateMaterial.id ? '修改视频' : '添加视频'} </h2>
<ProForm.Item
name='file_url'
>
<Uploader
fileType='视频'
fileInfo={fileInfo}
setFileInfo={setFileInfo}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
modalFormRef?.current?.setFieldsValue({
file_url: getUploadUrlRes?.data?.download_url,
file_size: String(file.size),
title: file.name
})
return;
}
message.error('上传视频失败');
return;
} catch (e) {
message.error('上传视频失败');
}
}}
/>
</ProForm.Item>
<div style={{display: 'none'}}>
<ProFormText name='file_size'/>
</div>
<div style={{display: 'none'}}>
{/* 文件名 */}
<ProFormText name='title'/>
</div>
</div>
}
{
// 修改文件类素材 弹窗内容
(props.fileType === 'PDF' || props.fileType === 'PPT' || props.fileType === '文档' || props.fileType === '表格') &&
<div key={props.fileType}>
<h2
className='dialog-title'> {targetUpdateMaterial.id ? `修改${props.fileType}` : `添加${props.fileType}`} </h2>
<ProForm.Item
name='file_url'
>
<Uploader
fileType={props.fileType}
fileInfo={fileInfo}
setFileInfo={setFileInfo}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
modalFormRef?.current?.setFieldsValue({
file_url: getUploadUrlRes?.data?.download_url,
file_size: String(file.size),
title: file.name
})
return;
}
message.error(`上传${props.fileType}失败`);
return;
} catch (e) {
message.error(`上传${props.fileType}失败`);
}
}}
/>
</ProForm.Item>
<div style={{display: 'none'}}>
<ProFormText name='file_size'/>
</div>
<div style={{display: 'none'}}>
{/* 文件名 */}
<ProFormText name='title'/>
</div>
</div>
}
<div className={styles.modalTagBox}>
<Space direction={'horizontal'} wrap={true}>
<Button icon={<PlusOutlined/>} onClick={() => setTagModalVisible(true)}>选择标签</Button>
{
choosedTags?.length > 0 && choosedTags?.map((tag) =>
<Tag
key={tag?.id}
className={'tag-item selected-tag-item'}
>
{tag?.name}
<span>
<CloseOutlined
style={{fontSize: '12px', cursor: 'pointer'}}
onClick={() => {
setChoosedTags(choosedTags?.filter((choosedTag) => {
return choosedTag?.id !== tag?.id
}))
}}
/>
</span>
</Tag>
)}
</Space>
</div>
</ModalForm>
{/* 删除素材 */}
<Modal
visible={!!targetDeleteMaterial.id}
onOk={() => {
DeleteMaterial({ids: [targetDeleteMaterial.id]}).then(res => {
if (res?.code === 0) {
message.success('删除素材标签成功')
// setListTimestamp(Date.now)
queryMaterialList(searchTags?.map(tag => tag?.id) || [])
} else {
message.success('删除失败')
}
setTargetDeleteMaterial({} as Material.Item)
})
}}
onCancel={() => {
setTargetDeleteMaterial({} as Material.Item)
}}
>
<h3>提示</h3>
<h4>确定删除「{(targetDeleteMaterial as Material.Item).title}」这个素材吗?删除后不可恢复</h4>
</Modal>
{/* 选择素材标签弹窗 */}
<TagModal
width={560}
isEditable={false}
defaultCheckedTags={() => {
if (choosedTags?.length > 0) {
return choosedTags
}
const tempArr: MaterialTag.Item[] = []
targetUpdateMaterial?.material_tag_list?.forEach((tagId: string) => {
tempArr.push(contextValue.allTagsMap[tagId])
});
return tempArr || []
}}
allTags={contextValue.allTags}
setAllTags={contextValue.setAllTags}
visible={tagModalVisible}
setVisible={setTagModalVisible}
onCancel={() => {
setChoosedTags([])
}}
reloadTags={contextValue.setTagsItemsTimestamp}
onFinish={async (values) => {
setChoosedTags(values)
}}
/>
</div>
)
}
</TagContext.Consumer>
);
}