refactor: ♻️ Rename step to block
This commit is contained in:
@ -13,23 +13,23 @@ export enum RightPanel {
|
||||
const editorContext = createContext<{
|
||||
rightPanel?: RightPanel
|
||||
setRightPanel: Dispatch<SetStateAction<RightPanel | undefined>>
|
||||
startPreviewAtBlock: string | undefined
|
||||
setStartPreviewAtBlock: Dispatch<SetStateAction<string | undefined>>
|
||||
startPreviewAtGroup: string | undefined
|
||||
setStartPreviewAtGroup: Dispatch<SetStateAction<string | undefined>>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const EditorContext = ({ children }: { children: ReactNode }) => {
|
||||
const [rightPanel, setRightPanel] = useState<RightPanel>()
|
||||
const [startPreviewAtBlock, setStartPreviewAtBlock] = useState<string>()
|
||||
const [startPreviewAtGroup, setStartPreviewAtGroup] = useState<string>()
|
||||
|
||||
return (
|
||||
<editorContext.Provider
|
||||
value={{
|
||||
rightPanel,
|
||||
setRightPanel,
|
||||
startPreviewAtBlock,
|
||||
setStartPreviewAtBlock,
|
||||
startPreviewAtGroup,
|
||||
setStartPreviewAtGroup,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Block, Edge, IdMap, Source, Step, Target } from 'models'
|
||||
import { Group, Edge, IdMap, Source, Block, Target } from 'models'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
@ -35,8 +35,8 @@ export type Anchor = {
|
||||
coordinates: Coordinates
|
||||
}
|
||||
|
||||
export type Node = Omit<Block, 'steps'> & {
|
||||
steps: (Step & {
|
||||
export type Node = Omit<Group, 'blocks'> & {
|
||||
blocks: (Block & {
|
||||
sourceAnchorsPosition: { left: Coordinates; right: Coordinates }
|
||||
})[]
|
||||
}
|
||||
@ -48,18 +48,18 @@ export type ConnectingIds = {
|
||||
target?: Target
|
||||
}
|
||||
|
||||
type StepId = string
|
||||
type BlockId = string
|
||||
type ButtonId = string
|
||||
export type Endpoint = {
|
||||
id: StepId | ButtonId
|
||||
id: BlockId | ButtonId
|
||||
ref: MutableRefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
export type BlocksCoordinates = IdMap<Coordinates>
|
||||
export type GroupsCoordinates = IdMap<Coordinates>
|
||||
|
||||
const graphContext = createContext<{
|
||||
blocksCoordinates: BlocksCoordinates
|
||||
updateBlockCoordinates: (blockId: string, newCoord: Coordinates) => void
|
||||
groupsCoordinates: GroupsCoordinates
|
||||
updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void
|
||||
graphPosition: Position
|
||||
setGraphPosition: Dispatch<SetStateAction<Position>>
|
||||
connectingIds: ConnectingIds | null
|
||||
@ -70,11 +70,11 @@ const graphContext = createContext<{
|
||||
addSourceEndpoint: (endpoint: Endpoint) => void
|
||||
targetEndpoints: IdMap<Endpoint>
|
||||
addTargetEndpoint: (endpoint: Endpoint) => void
|
||||
openedStepId?: string
|
||||
setOpenedStepId: Dispatch<SetStateAction<string | undefined>>
|
||||
openedBlockId?: string
|
||||
setOpenedBlockId: Dispatch<SetStateAction<string | undefined>>
|
||||
isReadOnly: boolean
|
||||
focusedBlockId?: string
|
||||
setFocusedBlockId: Dispatch<SetStateAction<string | undefined>>
|
||||
focusedGroupId?: string
|
||||
setFocusedGroupId: Dispatch<SetStateAction<string | undefined>>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({
|
||||
@ -84,11 +84,11 @@ const graphContext = createContext<{
|
||||
|
||||
export const GraphProvider = ({
|
||||
children,
|
||||
blocks,
|
||||
groups,
|
||||
isReadOnly = false,
|
||||
}: {
|
||||
children: ReactNode
|
||||
blocks: Block[]
|
||||
groups: Group[]
|
||||
isReadOnly?: boolean
|
||||
}) => {
|
||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||
@ -96,15 +96,15 @@ export const GraphProvider = ({
|
||||
const [previewingEdge, setPreviewingEdge] = useState<Edge>()
|
||||
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [openedStepId, setOpenedStepId] = useState<string>()
|
||||
const [blocksCoordinates, setBlocksCoordinates] = useState<BlocksCoordinates>(
|
||||
const [openedBlockId, setOpenedBlockId] = useState<string>()
|
||||
const [groupsCoordinates, setGroupsCoordinates] = useState<GroupsCoordinates>(
|
||||
{}
|
||||
)
|
||||
const [focusedBlockId, setFocusedBlockId] = useState<string>()
|
||||
const [focusedGroupId, setFocusedGroupId] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
setBlocksCoordinates(
|
||||
blocks.reduce(
|
||||
setGroupsCoordinates(
|
||||
groups.reduce(
|
||||
(coords, block) => ({
|
||||
...coords,
|
||||
[block.id]: block.graphCoordinates,
|
||||
@ -113,7 +113,7 @@ export const GraphProvider = ({
|
||||
)
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blocks])
|
||||
}, [groups])
|
||||
|
||||
const addSourceEndpoint = (endpoint: Endpoint) => {
|
||||
setSourceEndpoints((endpoints) => ({
|
||||
@ -129,10 +129,10 @@ export const GraphProvider = ({
|
||||
}))
|
||||
}
|
||||
|
||||
const updateBlockCoordinates = (blockId: string, newCoord: Coordinates) =>
|
||||
setBlocksCoordinates((blocksCoordinates) => ({
|
||||
...blocksCoordinates,
|
||||
[blockId]: newCoord,
|
||||
const updateGroupCoordinates = (groupId: string, newCoord: Coordinates) =>
|
||||
setGroupsCoordinates((groupsCoordinates) => ({
|
||||
...groupsCoordinates,
|
||||
[groupId]: newCoord,
|
||||
}))
|
||||
|
||||
return (
|
||||
@ -148,13 +148,13 @@ export const GraphProvider = ({
|
||||
targetEndpoints,
|
||||
addSourceEndpoint,
|
||||
addTargetEndpoint,
|
||||
openedStepId,
|
||||
setOpenedStepId,
|
||||
blocksCoordinates,
|
||||
updateBlockCoordinates,
|
||||
openedBlockId,
|
||||
setOpenedBlockId,
|
||||
groupsCoordinates,
|
||||
updateGroupCoordinates,
|
||||
isReadOnly,
|
||||
focusedBlockId,
|
||||
setFocusedBlockId,
|
||||
focusedGroupId,
|
||||
setFocusedGroupId,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEventListener } from '@chakra-ui/react'
|
||||
import { ButtonItem, DraggableStep, DraggableStepType } from 'models'
|
||||
import { ButtonItem, DraggableBlock, DraggableBlockType } from 'models'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
@ -11,20 +11,20 @@ import {
|
||||
} from 'react'
|
||||
import { Coordinates } from './GraphContext'
|
||||
|
||||
type BlockInfo = {
|
||||
type GroupInfo = {
|
||||
id: string
|
||||
ref: React.MutableRefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
const graphDndContext = createContext<{
|
||||
draggedStepType?: DraggableStepType
|
||||
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
|
||||
draggedStep?: DraggableStep
|
||||
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
|
||||
draggedBlockType?: DraggableBlockType
|
||||
setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>>
|
||||
draggedBlock?: DraggableBlock
|
||||
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
|
||||
draggedItem?: ButtonItem
|
||||
setDraggedItem: Dispatch<SetStateAction<ButtonItem | undefined>>
|
||||
mouseOverBlock?: BlockInfo
|
||||
setMouseOverBlock: Dispatch<SetStateAction<BlockInfo | undefined>>
|
||||
mouseOverGroup?: GroupInfo
|
||||
setMouseOverGroup: Dispatch<SetStateAction<GroupInfo | undefined>>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
@ -32,24 +32,24 @@ const graphDndContext = createContext<{
|
||||
export type NodePosition = { absolute: Coordinates; relative: Coordinates }
|
||||
|
||||
export const GraphDndContext = ({ children }: { children: ReactNode }) => {
|
||||
const [draggedStep, setDraggedStep] = useState<DraggableStep>()
|
||||
const [draggedStepType, setDraggedStepType] = useState<
|
||||
DraggableStepType | undefined
|
||||
const [draggedBlock, setDraggedBlock] = useState<DraggableBlock>()
|
||||
const [draggedBlockType, setDraggedBlockType] = useState<
|
||||
DraggableBlockType | undefined
|
||||
>()
|
||||
const [draggedItem, setDraggedItem] = useState<ButtonItem | undefined>()
|
||||
const [mouseOverBlock, setMouseOverBlock] = useState<BlockInfo>()
|
||||
const [mouseOverGroup, setMouseOverGroup] = useState<GroupInfo>()
|
||||
|
||||
return (
|
||||
<graphDndContext.Provider
|
||||
value={{
|
||||
draggedStep,
|
||||
setDraggedStep,
|
||||
draggedStepType,
|
||||
setDraggedStepType,
|
||||
draggedBlock,
|
||||
setDraggedBlock,
|
||||
draggedBlockType,
|
||||
setDraggedBlockType,
|
||||
draggedItem,
|
||||
setDraggedItem,
|
||||
mouseOverBlock,
|
||||
setMouseOverBlock,
|
||||
mouseOverGroup,
|
||||
setMouseOverGroup,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@ -124,4 +124,4 @@ export const computeNearestPlaceholderIndex = (
|
||||
return closestIndex
|
||||
}
|
||||
|
||||
export const useStepDnd = () => useContext(graphDndContext)
|
||||
export const useBlockDnd = () => useContext(graphDndContext)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {
|
||||
LogicStepType,
|
||||
LogicBlockType,
|
||||
PublicTypebot,
|
||||
Settings,
|
||||
Theme,
|
||||
@ -30,8 +30,8 @@ import {
|
||||
import { fetcher, preventUserFromRefreshing } from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { isDefined, isEmpty, isNotDefined, omit } from 'utils'
|
||||
import { BlocksActions, blocksActions } from './actions/blocks'
|
||||
import { stepsAction, StepsActions } from './actions/steps'
|
||||
import { GroupsActions, groupsActions } from './actions/groups'
|
||||
import { blocksAction, BlocksActions } from './actions/blocks'
|
||||
import { variablesAction, VariablesActions } from './actions/variables'
|
||||
import { edgesAction, EdgesActions } from './actions/edges'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
@ -80,8 +80,8 @@ const typebotContext = createContext<
|
||||
updateTypebot: (updates: UpdateTypebotPayload) => void
|
||||
publishTypebot: () => void
|
||||
restorePublishedTypebot: () => void
|
||||
} & BlocksActions &
|
||||
StepsActions &
|
||||
} & GroupsActions &
|
||||
BlocksActions &
|
||||
ItemsActions &
|
||||
VariablesActions &
|
||||
EdgesActions
|
||||
@ -122,13 +122,13 @@ export const TypebotContext = ({
|
||||
},
|
||||
] = useUndo<Typebot | undefined>(undefined)
|
||||
|
||||
const linkedTypebotIds = localTypebot?.blocks
|
||||
.flatMap((b) => b.steps)
|
||||
const linkedTypebotIds = localTypebot?.groups
|
||||
.flatMap((b) => b.blocks)
|
||||
.reduce<string[]>(
|
||||
(typebotIds, step) =>
|
||||
step.type === LogicStepType.TYPEBOT_LINK &&
|
||||
isDefined(step.options.typebotId)
|
||||
? [...typebotIds, step.options.typebotId]
|
||||
(typebotIds, block) =>
|
||||
block.type === LogicBlockType.TYPEBOT_LINK &&
|
||||
isDefined(block.options.typebotId)
|
||||
? [...typebotIds, block.options.typebotId]
|
||||
: typebotIds,
|
||||
[]
|
||||
)
|
||||
@ -360,8 +360,8 @@ export const TypebotContext = ({
|
||||
updateTypebot: updateLocalTypebot,
|
||||
restorePublishedTypebot,
|
||||
updateWebhook,
|
||||
...blocksActions(setLocalTypebot as SetTypebot),
|
||||
...stepsAction(setLocalTypebot as SetTypebot),
|
||||
...groupsActions(setLocalTypebot as SetTypebot),
|
||||
...blocksAction(setLocalTypebot as SetTypebot),
|
||||
...variablesAction(setLocalTypebot as SetTypebot),
|
||||
...edgesAction(setLocalTypebot as SetTypebot),
|
||||
...itemsAction(setLocalTypebot as SetTypebot),
|
||||
|
@ -1,102 +1,155 @@
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import cuid from 'cuid'
|
||||
import { produce } from 'immer'
|
||||
import { WritableDraft } from 'immer/dist/internal'
|
||||
import {
|
||||
Block,
|
||||
DraggableStep,
|
||||
DraggableStepType,
|
||||
StepIndices,
|
||||
Typebot,
|
||||
DraggableBlock,
|
||||
DraggableBlockType,
|
||||
BlockIndices,
|
||||
} from 'models'
|
||||
import { parseNewBlock } from 'services/typebots/typebots'
|
||||
import { removeEmptyGroups } from './groups'
|
||||
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||
import { SetTypebot } from '../TypebotContext'
|
||||
import { cleanUpEdgeDraft } from './edges'
|
||||
import { createStepDraft, duplicateStepDraft } from './steps'
|
||||
import produce from 'immer'
|
||||
import { cleanUpEdgeDraft, deleteEdgeDraft } from './edges'
|
||||
import cuid from 'cuid'
|
||||
import { byId, isWebhookBlock, blockHasItems } from 'utils'
|
||||
import { duplicateItemDraft } from './items'
|
||||
|
||||
export type BlocksActions = {
|
||||
createBlock: (
|
||||
props: Coordinates & {
|
||||
id: string
|
||||
step: DraggableStep | DraggableStepType
|
||||
indices: StepIndices
|
||||
}
|
||||
groupId: string,
|
||||
block: DraggableBlock | DraggableBlockType,
|
||||
indices: BlockIndices
|
||||
) => void
|
||||
updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) => void
|
||||
duplicateBlock: (blockIndex: number) => void
|
||||
deleteBlock: (blockIndex: number) => void
|
||||
updateBlock: (
|
||||
indices: BlockIndices,
|
||||
updates: Partial<Omit<Block, 'id' | 'type'>>
|
||||
) => void
|
||||
duplicateBlock: (indices: BlockIndices) => void
|
||||
detachBlockFromGroup: (indices: BlockIndices) => void
|
||||
deleteBlock: (indices: BlockIndices) => void
|
||||
}
|
||||
|
||||
const blocksActions = (setTypebot: SetTypebot): BlocksActions => ({
|
||||
createBlock: ({
|
||||
id,
|
||||
step,
|
||||
indices,
|
||||
...graphCoordinates
|
||||
}: Coordinates & {
|
||||
id: string
|
||||
step: DraggableStep | DraggableStepType
|
||||
indices: StepIndices
|
||||
}) =>
|
||||
const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
|
||||
createBlock: (
|
||||
groupId: string,
|
||||
block: DraggableBlock | DraggableBlockType,
|
||||
indices: BlockIndices
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const newBlock: Block = {
|
||||
id,
|
||||
graphCoordinates,
|
||||
title: `Group #${typebot.blocks.length}`,
|
||||
steps: [],
|
||||
}
|
||||
typebot.blocks.push(newBlock)
|
||||
createStepDraft(typebot, step, newBlock.id, indices)
|
||||
createBlockDraft(typebot, block, groupId, indices)
|
||||
})
|
||||
),
|
||||
updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) =>
|
||||
updateBlock: (
|
||||
{ groupIndex, blockIndex }: BlockIndices,
|
||||
updates: Partial<Omit<Block, 'id' | 'type'>>
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const block = typebot.blocks[blockIndex]
|
||||
typebot.blocks[blockIndex] = { ...block, ...updates }
|
||||
const block = typebot.groups[groupIndex].blocks[blockIndex]
|
||||
typebot.groups[groupIndex].blocks[blockIndex] = { ...block, ...updates }
|
||||
})
|
||||
),
|
||||
duplicateBlock: (blockIndex: number) =>
|
||||
duplicateBlock: ({ groupIndex, blockIndex }: BlockIndices) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const block = typebot.blocks[blockIndex]
|
||||
const id = cuid()
|
||||
const newBlock: Block = {
|
||||
...block,
|
||||
title: `${block.title} copy`,
|
||||
id,
|
||||
steps: block.steps.map(duplicateStepDraft(id)),
|
||||
graphCoordinates: {
|
||||
x: block.graphCoordinates.x + 200,
|
||||
y: block.graphCoordinates.y + 100,
|
||||
},
|
||||
}
|
||||
typebot.blocks.splice(blockIndex + 1, 0, newBlock)
|
||||
const block = { ...typebot.groups[groupIndex].blocks[blockIndex] }
|
||||
const newBlock = duplicateBlockDraft(block.groupId)(block)
|
||||
typebot.groups[groupIndex].blocks.splice(blockIndex + 1, 0, newBlock)
|
||||
})
|
||||
),
|
||||
deleteBlock: (blockIndex: number) =>
|
||||
detachBlockFromGroup: (indices: BlockIndices) =>
|
||||
setTypebot((typebot) => produce(typebot, removeBlockFromGroup(indices))),
|
||||
deleteBlock: ({ groupIndex, blockIndex }: BlockIndices) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
deleteBlockDraft(typebot)(blockIndex)
|
||||
const removingBlock = typebot.groups[groupIndex].blocks[blockIndex]
|
||||
removeBlockFromGroup({ groupIndex, blockIndex })(typebot)
|
||||
cleanUpEdgeDraft(typebot, removingBlock.id)
|
||||
removeEmptyGroups(typebot)
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const deleteBlockDraft =
|
||||
(typebot: WritableDraft<Typebot>) => (blockIndex: number) => {
|
||||
cleanUpEdgeDraft(typebot, typebot.blocks[blockIndex].id)
|
||||
typebot.blocks.splice(blockIndex, 1)
|
||||
const removeBlockFromGroup =
|
||||
({ groupIndex, blockIndex }: BlockIndices) =>
|
||||
(typebot: WritableDraft<Typebot>) => {
|
||||
typebot.groups[groupIndex].blocks.splice(blockIndex, 1)
|
||||
}
|
||||
|
||||
const removeEmptyBlocks = (typebot: WritableDraft<Typebot>) => {
|
||||
const emptyBlocksIndices = typebot.blocks.reduce<number[]>(
|
||||
(arr, block, idx) => {
|
||||
block.steps.length === 0 && arr.push(idx)
|
||||
return arr
|
||||
},
|
||||
[]
|
||||
const createBlockDraft = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
block: DraggableBlock | DraggableBlockType,
|
||||
groupId: string,
|
||||
{ groupIndex, blockIndex }: BlockIndices
|
||||
) => {
|
||||
const blocks = typebot.groups[groupIndex].blocks
|
||||
if (
|
||||
blockIndex === blocks.length &&
|
||||
blockIndex > 0 &&
|
||||
blocks[blockIndex - 1].outgoingEdgeId
|
||||
)
|
||||
emptyBlocksIndices.forEach(deleteBlockDraft(typebot))
|
||||
deleteEdgeDraft(typebot, blocks[blockIndex - 1].outgoingEdgeId as string)
|
||||
typeof block === 'string'
|
||||
? createNewBlock(typebot, block, groupId, { groupIndex, blockIndex })
|
||||
: moveBlockToGroup(typebot, block, groupId, { groupIndex, blockIndex })
|
||||
removeEmptyGroups(typebot)
|
||||
}
|
||||
|
||||
export { blocksActions, removeEmptyBlocks }
|
||||
const createNewBlock = async (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
type: DraggableBlockType,
|
||||
groupId: string,
|
||||
{ groupIndex, blockIndex }: BlockIndices
|
||||
) => {
|
||||
const newBlock = parseNewBlock(type, groupId)
|
||||
typebot.groups[groupIndex].blocks.splice(blockIndex ?? 0, 0, newBlock)
|
||||
}
|
||||
|
||||
const moveBlockToGroup = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
block: DraggableBlock,
|
||||
groupId: string,
|
||||
{ groupIndex, blockIndex }: BlockIndices
|
||||
) => {
|
||||
const newBlock = { ...block, groupId }
|
||||
const items = blockHasItems(block) ? block.items : []
|
||||
items.forEach((item) => {
|
||||
const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId))
|
||||
if (edgeIndex === -1) return
|
||||
typebot.edges[edgeIndex].from.groupId = groupId
|
||||
})
|
||||
if (block.outgoingEdgeId) {
|
||||
if (typebot.groups[groupIndex].blocks.length > blockIndex ?? 0) {
|
||||
deleteEdgeDraft(typebot, block.outgoingEdgeId)
|
||||
newBlock.outgoingEdgeId = undefined
|
||||
} else {
|
||||
const edgeIndex = typebot.edges.findIndex(byId(block.outgoingEdgeId))
|
||||
edgeIndex !== -1
|
||||
? (typebot.edges[edgeIndex].from.groupId = groupId)
|
||||
: (newBlock.outgoingEdgeId = undefined)
|
||||
}
|
||||
}
|
||||
typebot.groups[groupIndex].blocks.splice(blockIndex ?? 0, 0, newBlock)
|
||||
}
|
||||
|
||||
const duplicateBlockDraft =
|
||||
(groupId: string) =>
|
||||
(block: Block): Block => {
|
||||
const blockId = cuid()
|
||||
return {
|
||||
...block,
|
||||
groupId,
|
||||
id: blockId,
|
||||
items: blockHasItems(block)
|
||||
? block.items.map(duplicateItemDraft(blockId))
|
||||
: undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
webhookId: isWebhookBlock(block) ? cuid() : undefined,
|
||||
outgoingEdgeId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export { blocksAction, createBlockDraft, duplicateBlockDraft }
|
||||
|
@ -1,15 +1,15 @@
|
||||
import {
|
||||
Typebot,
|
||||
Edge,
|
||||
StepWithItems,
|
||||
StepIndices,
|
||||
BlockWithItems,
|
||||
BlockIndices,
|
||||
ItemIndices,
|
||||
Step,
|
||||
Block,
|
||||
} from 'models'
|
||||
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||
import { SetTypebot } from '../TypebotContext'
|
||||
import { produce } from 'immer'
|
||||
import { byId, isDefined, stepHasItems } from 'utils'
|
||||
import { byId, isDefined, blockHasItems } from 'utils'
|
||||
import cuid from 'cuid'
|
||||
|
||||
export type EdgesActions = {
|
||||
@ -28,25 +28,25 @@ export const edgesAction = (setTypebot: SetTypebot): EdgesActions => ({
|
||||
}
|
||||
removeExistingEdge(typebot, edge)
|
||||
typebot.edges.push(newEdge)
|
||||
const blockIndex = typebot.blocks.findIndex(byId(edge.from.blockId))
|
||||
const stepIndex = typebot.blocks[blockIndex].steps.findIndex(
|
||||
byId(edge.from.stepId)
|
||||
const groupIndex = typebot.groups.findIndex(byId(edge.from.groupId))
|
||||
const blockIndex = typebot.groups[groupIndex].blocks.findIndex(
|
||||
byId(edge.from.blockId)
|
||||
)
|
||||
const itemIndex = edge.from.itemId
|
||||
? (
|
||||
typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems
|
||||
typebot.groups[groupIndex].blocks[blockIndex] as BlockWithItems
|
||||
).items.findIndex(byId(edge.from.itemId))
|
||||
: null
|
||||
|
||||
isDefined(itemIndex) && itemIndex !== -1
|
||||
? addEdgeIdToItem(typebot, newEdge.id, {
|
||||
groupIndex,
|
||||
blockIndex,
|
||||
stepIndex,
|
||||
itemIndex,
|
||||
})
|
||||
: addEdgeIdToStep(typebot, newEdge.id, {
|
||||
: addEdgeIdToBlock(typebot, newEdge.id, {
|
||||
groupIndex,
|
||||
blockIndex,
|
||||
stepIndex,
|
||||
})
|
||||
})
|
||||
),
|
||||
@ -68,20 +68,20 @@ export const edgesAction = (setTypebot: SetTypebot): EdgesActions => ({
|
||||
),
|
||||
})
|
||||
|
||||
const addEdgeIdToStep = (
|
||||
const addEdgeIdToBlock = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
edgeId: string,
|
||||
{ blockIndex, stepIndex }: StepIndices
|
||||
{ groupIndex, blockIndex }: BlockIndices
|
||||
) => {
|
||||
typebot.blocks[blockIndex].steps[stepIndex].outgoingEdgeId = edgeId
|
||||
typebot.groups[groupIndex].blocks[blockIndex].outgoingEdgeId = edgeId
|
||||
}
|
||||
|
||||
const addEdgeIdToItem = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
edgeId: string,
|
||||
{ blockIndex, stepIndex, itemIndex }: ItemIndices
|
||||
{ groupIndex, blockIndex, itemIndex }: ItemIndices
|
||||
) =>
|
||||
((typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems).items[
|
||||
((typebot.groups[groupIndex].blocks[blockIndex] as BlockWithItems).items[
|
||||
itemIndex
|
||||
].outgoingEdgeId = edgeId)
|
||||
|
||||
@ -101,23 +101,23 @@ const deleteOutgoingEdgeIdProps = (
|
||||
) => {
|
||||
const edge = typebot.edges.find(byId(edgeId))
|
||||
if (!edge) return
|
||||
const fromBlockIndex = typebot.blocks.findIndex(byId(edge.from.blockId))
|
||||
const fromStepIndex = typebot.blocks[fromBlockIndex].steps.findIndex(
|
||||
byId(edge.from.stepId)
|
||||
const fromGroupIndex = typebot.groups.findIndex(byId(edge.from.groupId))
|
||||
const fromBlockIndex = typebot.groups[fromGroupIndex].blocks.findIndex(
|
||||
byId(edge.from.blockId)
|
||||
)
|
||||
const step = typebot.blocks[fromBlockIndex].steps[fromStepIndex] as
|
||||
| Step
|
||||
const block = typebot.groups[fromGroupIndex].blocks[fromBlockIndex] as
|
||||
| Block
|
||||
| undefined
|
||||
const fromItemIndex =
|
||||
edge.from.itemId && step && stepHasItems(step)
|
||||
? step.items.findIndex(byId(edge.from.itemId))
|
||||
edge.from.itemId && block && blockHasItems(block)
|
||||
? block.items.findIndex(byId(edge.from.itemId))
|
||||
: -1
|
||||
if (fromStepIndex !== -1)
|
||||
typebot.blocks[fromBlockIndex].steps[fromStepIndex].outgoingEdgeId =
|
||||
if (fromBlockIndex !== -1)
|
||||
typebot.groups[fromGroupIndex].blocks[fromBlockIndex].outgoingEdgeId =
|
||||
undefined
|
||||
if (fromItemIndex !== -1)
|
||||
(
|
||||
typebot.blocks[fromBlockIndex].steps[fromStepIndex] as StepWithItems
|
||||
typebot.groups[fromGroupIndex].blocks[fromBlockIndex] as BlockWithItems
|
||||
).items[fromItemIndex].outgoingEdgeId = undefined
|
||||
}
|
||||
|
||||
@ -127,11 +127,11 @@ export const cleanUpEdgeDraft = (
|
||||
) => {
|
||||
const edgesToDelete = typebot.edges.filter((edge) =>
|
||||
[
|
||||
edge.from.groupId,
|
||||
edge.from.blockId,
|
||||
edge.from.stepId,
|
||||
edge.from.itemId,
|
||||
edge.to.groupId,
|
||||
edge.to.blockId,
|
||||
edge.to.stepId,
|
||||
].includes(deletedNodeId)
|
||||
)
|
||||
edgesToDelete.forEach((edge) => deleteEdgeDraft(typebot, edge.id))
|
||||
@ -144,6 +144,6 @@ const removeExistingEdge = (
|
||||
typebot.edges = typebot.edges.filter((e) =>
|
||||
edge.from.itemId
|
||||
? e.from.itemId !== edge.from.itemId
|
||||
: isDefined(e.from.itemId) || e.from.stepId !== edge.from.stepId
|
||||
: isDefined(e.from.itemId) || e.from.blockId !== edge.from.blockId
|
||||
)
|
||||
}
|
||||
|
102
apps/builder/contexts/TypebotContext/actions/groups.ts
Normal file
102
apps/builder/contexts/TypebotContext/actions/groups.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import cuid from 'cuid'
|
||||
import { produce } from 'immer'
|
||||
import { WritableDraft } from 'immer/dist/internal'
|
||||
import {
|
||||
Group,
|
||||
DraggableBlock,
|
||||
DraggableBlockType,
|
||||
BlockIndices,
|
||||
Typebot,
|
||||
} from 'models'
|
||||
import { SetTypebot } from '../TypebotContext'
|
||||
import { cleanUpEdgeDraft } from './edges'
|
||||
import { createBlockDraft, duplicateBlockDraft } from './blocks'
|
||||
|
||||
export type GroupsActions = {
|
||||
createGroup: (
|
||||
props: Coordinates & {
|
||||
id: string
|
||||
block: DraggableBlock | DraggableBlockType
|
||||
indices: BlockIndices
|
||||
}
|
||||
) => void
|
||||
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) => void
|
||||
duplicateGroup: (groupIndex: number) => void
|
||||
deleteGroup: (groupIndex: number) => void
|
||||
}
|
||||
|
||||
const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
|
||||
createGroup: ({
|
||||
id,
|
||||
block,
|
||||
indices,
|
||||
...graphCoordinates
|
||||
}: Coordinates & {
|
||||
id: string
|
||||
block: DraggableBlock | DraggableBlockType
|
||||
indices: BlockIndices
|
||||
}) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const newGroup: Group = {
|
||||
id,
|
||||
graphCoordinates,
|
||||
title: `Group #${typebot.groups.length}`,
|
||||
blocks: [],
|
||||
}
|
||||
typebot.groups.push(newGroup)
|
||||
createBlockDraft(typebot, block, newGroup.id, indices)
|
||||
})
|
||||
),
|
||||
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const block = typebot.groups[groupIndex]
|
||||
typebot.groups[groupIndex] = { ...block, ...updates }
|
||||
})
|
||||
),
|
||||
duplicateGroup: (groupIndex: number) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const group = typebot.groups[groupIndex]
|
||||
const id = cuid()
|
||||
const newGroup: Group = {
|
||||
...group,
|
||||
title: `${group.title} copy`,
|
||||
id,
|
||||
blocks: group.blocks.map(duplicateBlockDraft(id)),
|
||||
graphCoordinates: {
|
||||
x: group.graphCoordinates.x + 200,
|
||||
y: group.graphCoordinates.y + 100,
|
||||
},
|
||||
}
|
||||
typebot.groups.splice(groupIndex + 1, 0, newGroup)
|
||||
})
|
||||
),
|
||||
deleteGroup: (groupIndex: number) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
deleteGroupDraft(typebot)(groupIndex)
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const deleteGroupDraft =
|
||||
(typebot: WritableDraft<Typebot>) => (groupIndex: number) => {
|
||||
cleanUpEdgeDraft(typebot, typebot.groups[groupIndex].id)
|
||||
typebot.groups.splice(groupIndex, 1)
|
||||
}
|
||||
|
||||
const removeEmptyGroups = (typebot: WritableDraft<Typebot>) => {
|
||||
const emptyGroupsIndices = typebot.groups.reduce<number[]>(
|
||||
(arr, group, idx) => {
|
||||
group.blocks.length === 0 && arr.push(idx)
|
||||
return arr
|
||||
},
|
||||
[]
|
||||
)
|
||||
emptyGroupsIndices.forEach(deleteGroupDraft(typebot))
|
||||
}
|
||||
|
||||
export { groupsActions, removeEmptyGroups }
|
@ -1,14 +1,14 @@
|
||||
import {
|
||||
ItemIndices,
|
||||
Item,
|
||||
InputStepType,
|
||||
StepWithItems,
|
||||
InputBlockType,
|
||||
BlockWithItems,
|
||||
ButtonItem,
|
||||
} from 'models'
|
||||
import { SetTypebot } from '../TypebotContext'
|
||||
import produce from 'immer'
|
||||
import { cleanUpEdgeDraft } from './edges'
|
||||
import { byId, stepHasItems } from 'utils'
|
||||
import { byId, blockHasItems } from 'utils'
|
||||
import cuid from 'cuid'
|
||||
|
||||
export type ItemsActions = {
|
||||
@ -17,79 +17,79 @@ export type ItemsActions = {
|
||||
indices: ItemIndices
|
||||
) => void
|
||||
updateItem: (indices: ItemIndices, updates: Partial<Omit<Item, 'id'>>) => void
|
||||
detachItemFromStep: (indices: ItemIndices) => void
|
||||
detachItemFromBlock: (indices: ItemIndices) => void
|
||||
deleteItem: (indices: ItemIndices) => void
|
||||
}
|
||||
|
||||
const itemsAction = (setTypebot: SetTypebot): ItemsActions => ({
|
||||
createItem: (
|
||||
item: ButtonItem | Omit<ButtonItem, 'id'>,
|
||||
{ blockIndex, stepIndex, itemIndex }: ItemIndices
|
||||
{ groupIndex, blockIndex, itemIndex }: ItemIndices
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[stepIndex]
|
||||
if (step.type !== InputStepType.CHOICE) return
|
||||
const block = typebot.groups[groupIndex].blocks[blockIndex]
|
||||
if (block.type !== InputBlockType.CHOICE) return
|
||||
const newItem = {
|
||||
...item,
|
||||
stepId: step.id,
|
||||
blockId: block.id,
|
||||
id: 'id' in item ? item.id : cuid(),
|
||||
}
|
||||
if (item.outgoingEdgeId) {
|
||||
const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId))
|
||||
edgeIndex !== -1
|
||||
? (typebot.edges[edgeIndex].from = {
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
groupId: block.groupId,
|
||||
blockId: block.id,
|
||||
itemId: newItem.id,
|
||||
})
|
||||
: (newItem.outgoingEdgeId = undefined)
|
||||
}
|
||||
step.items.splice(itemIndex, 0, newItem)
|
||||
block.items.splice(itemIndex, 0, newItem)
|
||||
})
|
||||
),
|
||||
updateItem: (
|
||||
{ blockIndex, stepIndex, itemIndex }: ItemIndices,
|
||||
{ groupIndex, blockIndex, itemIndex }: ItemIndices,
|
||||
updates: Partial<Omit<Item, 'id'>>
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[stepIndex]
|
||||
if (!stepHasItems(step)) return
|
||||
;(typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems).items[
|
||||
itemIndex
|
||||
] = {
|
||||
...step.items[itemIndex],
|
||||
const block = typebot.groups[groupIndex].blocks[blockIndex]
|
||||
if (!blockHasItems(block)) return
|
||||
;(
|
||||
typebot.groups[groupIndex].blocks[blockIndex] as BlockWithItems
|
||||
).items[itemIndex] = {
|
||||
...block.items[itemIndex],
|
||||
...updates,
|
||||
} as Item
|
||||
})
|
||||
),
|
||||
detachItemFromStep: ({ blockIndex, stepIndex, itemIndex }: ItemIndices) =>
|
||||
detachItemFromBlock: ({ groupIndex, blockIndex, itemIndex }: ItemIndices) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[
|
||||
stepIndex
|
||||
] as StepWithItems
|
||||
step.items.splice(itemIndex, 1)
|
||||
const block = typebot.groups[groupIndex].blocks[
|
||||
blockIndex
|
||||
] as BlockWithItems
|
||||
block.items.splice(itemIndex, 1)
|
||||
})
|
||||
),
|
||||
deleteItem: ({ blockIndex, stepIndex, itemIndex }: ItemIndices) =>
|
||||
deleteItem: ({ groupIndex, blockIndex, itemIndex }: ItemIndices) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[
|
||||
stepIndex
|
||||
] as StepWithItems
|
||||
const removingItem = step.items[itemIndex]
|
||||
step.items.splice(itemIndex, 1)
|
||||
const block = typebot.groups[groupIndex].blocks[
|
||||
blockIndex
|
||||
] as BlockWithItems
|
||||
const removingItem = block.items[itemIndex]
|
||||
block.items.splice(itemIndex, 1)
|
||||
cleanUpEdgeDraft(typebot, removingItem.id)
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const duplicateItemDraft = (stepId: string) => (item: Item) => ({
|
||||
const duplicateItemDraft = (blockId: string) => (item: Item) => ({
|
||||
...item,
|
||||
id: cuid(),
|
||||
stepId,
|
||||
blockId,
|
||||
outgoingEdgeId: undefined,
|
||||
})
|
||||
|
||||
|
@ -1,155 +0,0 @@
|
||||
import {
|
||||
Step,
|
||||
Typebot,
|
||||
DraggableStep,
|
||||
DraggableStepType,
|
||||
StepIndices,
|
||||
} from 'models'
|
||||
import { parseNewStep } from 'services/typebots/typebots'
|
||||
import { removeEmptyBlocks } from './blocks'
|
||||
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||
import { SetTypebot } from '../TypebotContext'
|
||||
import produce from 'immer'
|
||||
import { cleanUpEdgeDraft, deleteEdgeDraft } from './edges'
|
||||
import cuid from 'cuid'
|
||||
import { byId, isWebhookStep, stepHasItems } from 'utils'
|
||||
import { duplicateItemDraft } from './items'
|
||||
|
||||
export type StepsActions = {
|
||||
createStep: (
|
||||
blockId: string,
|
||||
step: DraggableStep | DraggableStepType,
|
||||
indices: StepIndices
|
||||
) => void
|
||||
updateStep: (
|
||||
indices: StepIndices,
|
||||
updates: Partial<Omit<Step, 'id' | 'type'>>
|
||||
) => void
|
||||
duplicateStep: (indices: StepIndices) => void
|
||||
detachStepFromBlock: (indices: StepIndices) => void
|
||||
deleteStep: (indices: StepIndices) => void
|
||||
}
|
||||
|
||||
const stepsAction = (setTypebot: SetTypebot): StepsActions => ({
|
||||
createStep: (
|
||||
blockId: string,
|
||||
step: DraggableStep | DraggableStepType,
|
||||
indices: StepIndices
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
createStepDraft(typebot, step, blockId, indices)
|
||||
})
|
||||
),
|
||||
updateStep: (
|
||||
{ blockIndex, stepIndex }: StepIndices,
|
||||
updates: Partial<Omit<Step, 'id' | 'type'>>
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[stepIndex]
|
||||
typebot.blocks[blockIndex].steps[stepIndex] = { ...step, ...updates }
|
||||
})
|
||||
),
|
||||
duplicateStep: ({ blockIndex, stepIndex }: StepIndices) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = { ...typebot.blocks[blockIndex].steps[stepIndex] }
|
||||
const newStep = duplicateStepDraft(step.blockId)(step)
|
||||
typebot.blocks[blockIndex].steps.splice(stepIndex + 1, 0, newStep)
|
||||
})
|
||||
),
|
||||
detachStepFromBlock: (indices: StepIndices) =>
|
||||
setTypebot((typebot) => produce(typebot, removeStepFromBlock(indices))),
|
||||
deleteStep: ({ blockIndex, stepIndex }: StepIndices) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const removingStep = typebot.blocks[blockIndex].steps[stepIndex]
|
||||
removeStepFromBlock({ blockIndex, stepIndex })(typebot)
|
||||
cleanUpEdgeDraft(typebot, removingStep.id)
|
||||
removeEmptyBlocks(typebot)
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const removeStepFromBlock =
|
||||
({ blockIndex, stepIndex }: StepIndices) =>
|
||||
(typebot: WritableDraft<Typebot>) => {
|
||||
typebot.blocks[blockIndex].steps.splice(stepIndex, 1)
|
||||
}
|
||||
|
||||
const createStepDraft = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
step: DraggableStep | DraggableStepType,
|
||||
blockId: string,
|
||||
{ blockIndex, stepIndex }: StepIndices
|
||||
) => {
|
||||
const steps = typebot.blocks[blockIndex].steps
|
||||
if (
|
||||
stepIndex === steps.length &&
|
||||
stepIndex > 0 &&
|
||||
steps[stepIndex - 1].outgoingEdgeId
|
||||
)
|
||||
deleteEdgeDraft(typebot, steps[stepIndex - 1].outgoingEdgeId as string)
|
||||
typeof step === 'string'
|
||||
? createNewStep(typebot, step, blockId, { blockIndex, stepIndex })
|
||||
: moveStepToBlock(typebot, step, blockId, { blockIndex, stepIndex })
|
||||
removeEmptyBlocks(typebot)
|
||||
}
|
||||
|
||||
const createNewStep = async (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
type: DraggableStepType,
|
||||
blockId: string,
|
||||
{ blockIndex, stepIndex }: StepIndices
|
||||
) => {
|
||||
const newStep = parseNewStep(type, blockId)
|
||||
typebot.blocks[blockIndex].steps.splice(stepIndex ?? 0, 0, newStep)
|
||||
}
|
||||
|
||||
const moveStepToBlock = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
step: DraggableStep,
|
||||
blockId: string,
|
||||
{ blockIndex, stepIndex }: StepIndices
|
||||
) => {
|
||||
const newStep = { ...step, blockId }
|
||||
const items = stepHasItems(step) ? step.items : []
|
||||
items.forEach((item) => {
|
||||
const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId))
|
||||
if (edgeIndex === -1) return
|
||||
typebot.edges[edgeIndex].from.blockId = blockId
|
||||
})
|
||||
if (step.outgoingEdgeId) {
|
||||
if (typebot.blocks[blockIndex].steps.length > stepIndex ?? 0) {
|
||||
deleteEdgeDraft(typebot, step.outgoingEdgeId)
|
||||
newStep.outgoingEdgeId = undefined
|
||||
} else {
|
||||
const edgeIndex = typebot.edges.findIndex(byId(step.outgoingEdgeId))
|
||||
edgeIndex !== -1
|
||||
? (typebot.edges[edgeIndex].from.blockId = blockId)
|
||||
: (newStep.outgoingEdgeId = undefined)
|
||||
}
|
||||
}
|
||||
typebot.blocks[blockIndex].steps.splice(stepIndex ?? 0, 0, newStep)
|
||||
}
|
||||
|
||||
const duplicateStepDraft =
|
||||
(blockId: string) =>
|
||||
(step: Step): Step => {
|
||||
const stepId = cuid()
|
||||
return {
|
||||
...step,
|
||||
blockId,
|
||||
id: stepId,
|
||||
items: stepHasItems(step)
|
||||
? step.items.map(duplicateItemDraft(stepId))
|
||||
: undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
webhookId: isWebhookStep(step) ? cuid() : undefined,
|
||||
outgoingEdgeId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export { stepsAction, createStepDraft, duplicateStepDraft }
|
Reference in New Issue
Block a user