erp-platform/ui/src/views/admin/organization-unit/OrganizationUnits.tsx
2025-08-13 23:29:27 +03:00

1069 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ActionLink, ColumnDef, DataTable, Loading } from '@/components/shared'
import AdaptableCard from '@/components/shared/AdaptableCard'
import Container from '@/components/shared/Container'
import {
Button,
Card,
Dialog,
FormContainer,
FormItem,
Input,
Notification,
Select,
Table,
Tabs,
toast,
} from '@/components/ui'
import TBody from '@/components/ui/Table/TBody'
import Td from '@/components/ui/Table/Td'
import Th from '@/components/ui/Table/Th'
import THead from '@/components/ui/Table/THead'
import Tr from '@/components/ui/Table/Tr'
import TabContent from '@/components/ui/Tabs/TabContent'
import TabList from '@/components/ui/Tabs/TabList'
import TabNav from '@/components/ui/Tabs/TabNav'
import { getRoles, getUsers } from '@/services/identity.service'
import { IdentityRoleDto, IdentityUserDto } from '@/proxy/admin/models'
import {
CreateUpdateOrganizationUnitDto,
OrganizationUnitDto,
} from '@/proxy/admin/organization-unit/models'
import {
ouDelete,
ouDeleteMembers,
ouDeleteRoles,
ouGetAll,
ouGetMembers,
ouGetRoles,
ouMoveAllUsers,
ouPost,
ouPut,
ouPutMembers,
ouPutMove,
ouPutRoles,
} from '@/services/organization-unit.service'
import { SelectBoxOption } from '@/shared/types'
import { useLocalization } from '@/utils/hooks/useLocalization'
import TableNoRecords from '@/views/shared/TableNoRecords'
import classNames from 'classnames'
import { Field, FieldProps, Form, Formik, FormikHelpers } from 'formik'
import isEmpty from 'lodash/isEmpty'
import { memo, Suspense, useEffect, useMemo, useState } from 'react'
import { NodeApi, NodeRendererProps } from 'react-arborist'
import { Tree } from 'react-arborist/dist/module/components/tree'
import { Helmet } from 'react-helmet'
import { FaFolder, FaMinusCircle, FaPlusCircle, FaTrash } from 'react-icons/fa'
import { HiBadgeCheck, HiUser } from 'react-icons/hi'
import {
MdAccountTree,
MdAddModerator,
MdAdUnits,
MdDelete,
MdEdit,
MdGroup,
MdPersonAdd,
MdSupervisedUserCircle,
} from 'react-icons/md'
import { object, string } from 'yup'
const schema = object().shape({
id: string(),
parentId: string(),
displayName: string().required(),
})
const schemaMoveAllUsers = object().shape({
id: string().required(),
newId: string().required(),
})
interface TreeViewData {
id: string
name: string
children?: TreeViewData[]
}
const convertToTreeViewData = (
items: (OrganizationUnitDto & { isAdded?: boolean })[],
parentId?: string,
) => {
const levelItems = items.filter((a) => a.parentId == parentId)
if (!levelItems?.length) {
return
}
const data: TreeViewData[] = []
for (let item of levelItems) {
if (!item.id || item.isAdded) {
continue
}
item.isAdded = true
data.push({
id: item.id,
name: item.displayName,
children: convertToTreeViewData(items, item.id),
})
}
return data
}
interface DeleteTypeData {
id: string
name: 'Organization Unit' | 'User' | 'Role'
}
const OrganizationUnits = () => {
const [loading, setLoading] = useState(true)
const [ous, setOus] = useState<SelectBoxOption[]>([])
const [list, setList] = useState<TreeViewData[]>([])
const [activeOu, setActiveOu] = useState<string>()
const [dialogItem, setDialogItem] = useState<CreateUpdateOrganizationUnitDto | undefined>()
const [ouMembers, setOuMembers] = useState<IdentityUserDto[]>([])
const [userSelectionList, setUserSelectionList] = useState<IdentityUserDto[]>([])
const [userSelectedRows, setUserSelectedRows] = useState<string[]>([])
const [roleSelectionList, setRoleSelectionList] = useState<IdentityRoleDto[]>([])
const [ouRoles, setOuRoles] = useState<IdentityRoleDto[]>([])
const [roleSelectedRows, setRoleSelectedRows] = useState<string[]>([])
const [deleteRow, setDeleteRow] = useState<DeleteTypeData | null>()
const [isMoveAllUsersOpen, setIsMoveAllUsersOpen] = useState(false)
const [treeSearch, setTreeSearch] = useState('')
const { translate } = useLocalization()
const userSelectionColumns: ColumnDef<IdentityUserDto>[] = useMemo(
() => [
{
header: 'Name',
accessorKey: 'fullName',
cell: ({ row }) => (
<>
{row.original.name} {row.original.surname}
</>
),
},
{
header: 'Email',
accessorKey: 'email',
},
],
[],
)
const roleSelectionColumns: ColumnDef<IdentityRoleDto>[] = useMemo(
() => [
{
header: 'Name',
accessorKey: 'Name',
cell: ({ row }) => <>{row.original.name}</>,
},
],
[],
)
const handleUserRowSelect = (checked: boolean, row: IdentityUserDto) => {
if (checked) {
const newState = [...userSelectedRows, row.id!]
setUserSelectedRows(newState)
} else {
const newState = [...userSelectedRows.filter((id) => id !== row.id)]
setUserSelectedRows(newState)
}
}
const handleRoleRowSelect = (checked: boolean, row: IdentityRoleDto) => {
if (checked) {
const newState = [...roleSelectedRows, row.id!]
setRoleSelectedRows(newState)
} else {
const newState = [...roleSelectedRows.filter((id) => id !== row.id)]
setRoleSelectedRows(newState)
}
}
const fetchData = async () => {
setLoading(true)
const response = await ouGetAll()
if (response.data?.items) {
setOus(
response.data.items.map((a) => ({
value: a.id,
label: a.displayName,
})),
)
var list = convertToTreeViewData(response.data.items) ?? []
setList(list)
}
setLoading(false)
}
useEffect(() => {
if (isEmpty(list)) {
fetchData()
}
}, [])
const handleDialogSubmit = async (
values: CreateUpdateOrganizationUnitDto,
{ setSubmitting }: FormikHelpers<CreateUpdateOrganizationUnitDto>,
) => {
setLoading(true)
setSubmitting(true)
try {
if (values.id) {
await ouPut(values)
} else {
await ouPost(values)
}
toast.push(
<Notification type="success" duration={2000}>
{translate('::Kaydet')}
</Notification>,
{
placement: 'top-center',
},
)
fetchData()
onDialogClose()
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
setSubmitting(false)
}
}
const onDialogClose = () => {
setDialogItem(undefined)
}
const onDropdownItemClick = (node: NodeApi<TreeViewData>, eventKey: string) => {
if (eventKey == 'edit') {
setDialogItem({
id: node.data.id,
displayName: node.data.name,
parentId: node.parent?.isRoot ? undefined : node.parent?.data.id,
})
} else if (eventKey == 'add-sub-unit') {
setDialogItem({
displayName: '',
parentId: node.data.id,
})
} else if (eventKey == 'delete') {
setDeleteRow({ id: node.data.id, name: 'Organization Unit' })
}
}
const onSelect = async (nodes: NodeApi<TreeViewData>[]) => {
if (nodes.length === 0) {
setActiveOu(undefined)
} else {
const node = nodes[0]
setActiveOu(node.data.id)
}
}
const fetchUsersAndRoles = async (id: string) => {
try {
setLoading(true)
const memberList = await ouGetMembers(id)
const members = memberList.data?.items ?? []
setOuMembers(members)
setUserSelectedRows(members.map((a) => a.id!))
const roleList = await ouGetRoles(id)
const roles = roleList.data?.items ?? []
setOuRoles(roles)
setRoleSelectedRows(roles.map((a) => a.id!))
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
}
}
useEffect(() => {
const fn = async () => {
setOuMembers([])
setUserSelectedRows([])
setOuRoles([])
setRoleSelectedRows([])
if (activeOu) {
await fetchUsersAndRoles(activeOu)
}
}
fn()
}, [activeOu])
const handleEdit = async (id: string, name: string) => {
setLoading(true)
try {
if (!id) {
return
}
await ouPut({ id, displayName: name })
toast.push(
<Notification type="success" duration={2000}>
{translate('::Kaydet')}
</Notification>,
{
placement: 'top-center',
},
)
fetchData()
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
}
}
const handleMove = async (id: string, newParentId?: string) => {
setLoading(true)
try {
if (!id) {
return
}
await ouPutMove(id, newParentId)
toast.push(
<Notification type="success" duration={2000}>
{translate('::Kaydet')}
</Notification>,
{
placement: 'top-center',
},
)
fetchData()
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
}
}
const Node = memo(({ node, style, dragHandle }: NodeRendererProps<any>) => {
return (
<div
className={classNames(
'h-6 text-md flex gap-2 items-center',
node.isSelected ? 'bg-indigo-200' : '',
)}
style={style}
ref={dragHandle}
>
<div
className="flex items-center h-full w-full"
onClick={() => node.isInternal && node.toggle()}
>
<div className="w-3">
{node.isInternal && (node.isOpen ? <FaMinusCircle /> : <FaPlusCircle />)}
</div>
<FaFolder className="ml-2 w-3" color="#337ab7" />
<span className="node-text m-1">
{node.isEditing ? (
<Input
className="h-4"
type="text"
defaultValue={node.data.name}
onBlur={() => node.reset()}
onKeyDown={(e) => {
if (e.key === 'Escape') {
node.reset()
}
if (e.key === 'Enter') {
node.submit(e.currentTarget.value)
}
}}
autoFocus
/>
) : (
<span>{node.data.name}</span>
)}
</span>
</div>
<div className="file-actions">
<div className="flex gap-1 folderFileActions">
<button
onClick={() => setIsMoveAllUsersOpen(true)}
title={translate('::Abp.Identity.OrganizationUnit.MoveAllUsers')}
>
<MdSupervisedUserCircle size="20" color="#2d6da3" />
</button>
<button
onClick={() => node.edit()}
title={translate('::Abp.Identity.OrganizationUnit.Rename')}
>
<MdEdit size="20" className="text-teal-900" />
</button>
<button
onClick={() => setDeleteRow({ id: node.data.id, name: 'Organization Unit' })}
title={translate('::Delete')}
>
<MdDelete size="20" className="text-red-500" />
</button>
</div>
</div>
</div>
)
})
return (
<>
<Helmet
titleTemplate="%s | Kurs Platform"
title={translate('::Abp.Identity.OrganizationUnits')}
defaultTitle="Kurs Platform"
></Helmet>
<Loading type="cover" loading={loading}>
<Container>
<div className="flex flex-col lg:flex-row gap-4">
<Card
className="md:w-3/12 min-w-fit"
header={translate('::Abp.Identity.OrganizationUnits')}
headerExtra={
<div className="flex gap-1">
<Button
variant="solid"
size="xs"
title={translate('::Abp.Identity.OrganizationUnit.AddUnit')}
onClick={(e) => {
e.preventDefault()
setDialogItem({
parentId: activeOu,
displayName: '',
})
}}
>
{activeOu ? <MdAccountTree /> : <MdAdUnits />}
</Button>
<Button
variant="solid"
size="xs"
title={translate('::Abp.Identity.OrganizationUnit.MoveAllUsers')}
onClick={(e) => {
e.preventDefault()
setIsMoveAllUsersOpen(true)
}}
>
<MdGroup />
</Button>
</div>
}
>
<Input
className="h-8"
type="text"
placeholder={translate('::App.Search')}
value={treeSearch}
onChange={(e) => {
setTreeSearch(e.target.value)
}}
/>
<Tree
openByDefault={false}
data={list}
onSelect={onSelect}
onMove={({ dragIds, parentId }) => {
if (dragIds.length > 0) {
handleMove(dragIds[0], parentId ?? '')
}
}}
onRename={({ id, name }) => {
handleEdit(id, name)
}}
className="mt-2 cursor-pointer"
searchTerm={treeSearch}
indent={10}
searchMatch={(node, term) =>
node.data.name.toLowerCase().includes(term.toLowerCase())
}
width={'100%'}
>
{Node}
</Tree>
</Card>
<Card className="md:w-9/12 w-full">
<Tabs defaultValue="users">
<TabList>
<TabNav value="users" icon={<HiUser />}>
{translate('::AbpIdentity.Users')}
</TabNav>
<TabNav value="roles" icon={<HiBadgeCheck />}>
{translate('::AbpIdentity.Roles')}
</TabNav>
</TabList>
<TabContent value="users">
{activeOu ? (
<AdaptableCard
headerClass="text-right"
header={
<Button
variant="solid"
size="sm"
onClick={async (e) => {
e.preventDefault()
const response = await getUsers(0, 1000)
setUserSelectionList(response.data?.items ?? [])
}}
>
<MdPersonAdd />
</Button>
}
>
<Table compact>
{!!ouMembers.length && (
<THead>
<Tr>
<Th></Th>
<Th>Adı Soyadı</Th>
<Th>E-Posta</Th>
<Th>Durum</Th>
</Tr>
</THead>
)}
<TBody>
{ouMembers.map((user) => (
<Tr key={user.id}>
<Td>
<Button
className="mr-auto"
type="button"
size="xs"
onClick={() => {
setDeleteRow({
id: user.id ?? '',
name: 'User',
})
}}
>
<FaTrash />
</Button>
</Td>
<Td>
<ActionLink
href={`/admin/identity/users/${user.id}/details`}
className="font-bold"
target="_blank"
>
{user.name} {user.surname}
</ActionLink>
</Td>
<Td>{user.email}</Td>
<Td>{user.isActive ? 'Aktif' : 'Pasif'}</Td>
</Tr>
))}
<TableNoRecords show={!ouMembers.length} />
</TBody>
</Table>
</AdaptableCard>
) : (
<div className="p-3">
{translate('::Abp.Identity.OrganizationUnit.Users.Description')}
</div>
)}
</TabContent>
<TabContent value="roles">
{activeOu ? (
<AdaptableCard
headerClass="text-right"
header={
<Button
variant="solid"
size="sm"
onClick={async (e) => {
e.preventDefault()
const response = await getRoles(0, 1000)
setRoleSelectionList(response.data?.items ?? [])
}}
>
<MdPersonAdd />
</Button>
}
>
<Table compact>
{!!ouRoles.length && (
<THead>
<Tr>
<Th></Th>
<Th>Rol</Th>
</Tr>
</THead>
)}
<TBody>
{ouRoles.map((role) => (
<Tr key={role.id}>
<Td>
<Button
className="mr-auto"
type="button"
size="xs"
onClick={() => {
setDeleteRow({
id: role.id ?? '',
name: 'Role',
})
}}
>
<FaTrash />
</Button>
</Td>
<Td>{role.name}</Td>
</Tr>
))}
<TableNoRecords show={!ouRoles.length} />
</TBody>
</Table>
</AdaptableCard>
) : (
<div className="p-3">
{translate('::Abp.Identity.OrganizationUnit.Roles.Description')}
</div>
)}
</TabContent>
</Tabs>
</Card>
</div>
</Container>
</Loading>
<Dialog isOpen={!!dialogItem} onClose={onDialogClose} onRequestClose={onDialogClose}>
<h5 className="mb-4">{translate('::Abp.Identity.OrganizationUnit.NewUnit')}</h5>
<hr className="my-2"></hr>
<Suspense fallback={<></>}>
<Formik
initialValues={dialogItem ?? ({ displayName: '' } as CreateUpdateOrganizationUnitDto)}
validationSchema={schema}
onSubmit={handleDialogSubmit}
>
{({ touched, errors, isSubmitting }) => {
return (
<Form>
<FormContainer size="sm">
<FormItem
label={translate('::Abp.Identity.OrganizationUnit.Parent')}
invalid={errors.parentId && touched.parentId}
errorMessage={errors.parentId}
>
{/* Parent Name göster */}
<Field
type="text"
disabled
autoComplete="off"
name="parentId"
component={Input}
/>
</FormItem>
<FormItem
label={translate('::Abp.Identity.OrganizationUnit.DisplayName')}
invalid={errors.displayName && touched.displayName}
errorMessage={errors.displayName}
>
<Field
type="text"
autoComplete="off"
name="displayName"
component={Input}
autoFocus
/>
</FormItem>
<div className="mt-6 flex flex-row justify-end gap-3">
<Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::...') : translate('::Save')}
</Button>
<Button type="button" variant="plain" onClick={onDialogClose}>
{translate('::Cancel')}
</Button>
</div>
</FormContainer>
</Form>
)
}}
</Formik>
</Suspense>
</Dialog>
<Dialog
id="userSelection"
isOpen={userSelectionList.length > 0}
onClose={() => setUserSelectionList([])}
onRequestClose={() => setUserSelectionList([])}
>
<h5 className="mb-4">{translate('::Abp.Identity.OrganizationUnit.SelectMembers')}</h5>
<DataTable<IdentityUserDto>
selectable
selectedRows={[...userSelectedRows]}
columns={userSelectionColumns}
data={userSelectionList}
loading={loading}
// pagingData={tableData}
// onPaginationChange={handlePaginationChange}
// onSelectChange={handleSelectChange} // Items per page
// onSort={handleSort}
onCheckBoxChange={handleUserRowSelect}
// onIndeterminateCheckBoxChange={handleAllRowSelect}
/>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={() => {
setUserSelectionList([])
}}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={async () => {
if (!activeOu) {
return
}
try {
setLoading(true)
await ouPutMembers(activeOu, userSelectedRows)
toast.push(
<Notification type="success" duration={2000}>
Updated
</Notification>,
{
placement: 'top-center',
},
)
await fetchUsersAndRoles(activeOu)
setUserSelectionList([])
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
}
}}
>
{translate('::Abp.Identity.OrganizationUnit.AddMembers')}
</Button>
</div>
</Dialog>
<Dialog
id="roleSelection"
isOpen={roleSelectionList.length > 0}
onClose={() => setRoleSelectionList([])}
onRequestClose={() => setRoleSelectionList([])}
>
<h5 className="mb-4">{translate('::Abp.Identity.OrganizationUnit.SelectRoles')}</h5>
<DataTable<IdentityRoleDto>
selectable
selectedRows={[...roleSelectedRows]}
columns={roleSelectionColumns}
data={roleSelectionList}
loading={loading}
// pagingData={tableData}
// onPaginationChange={handlePaginationChange}
// onSelectChange={handleSelectChange} // Items per page
// onSort={handleSort}
onCheckBoxChange={handleRoleRowSelect}
// onIndeterminateCheckBoxChange={handleAllRowSelect}
/>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={() => {
setRoleSelectionList([])
}}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={async () => {
if (!activeOu) {
return
}
try {
setLoading(true)
await ouPutRoles(activeOu, roleSelectedRows)
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.FormBilgileriKaydedildi')}
</Notification>,
{
placement: 'top-center',
},
)
await fetchUsersAndRoles(activeOu)
setRoleSelectionList([])
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
}
}}
>
{translate('::Abp.Identity.OrganizationUnit.AddRoles')}
</Button>
</div>
</Dialog>
<Dialog
id="confirmDelete"
isOpen={!!deleteRow}
onClose={() => setDeleteRow(null)}
onRequestClose={() => setDeleteRow(null)}
>
<h5 className="mb-4">{translate('::Delete')}</h5>
<p>{translate('::DeleteConfirmation')}</p>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={() => {
setDeleteRow(null)
}}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={async () => {
if (!deleteRow) {
return
}
try {
setLoading(true)
switch (deleteRow.name) {
case 'Organization Unit':
await ouDelete(deleteRow.id)
await fetchData()
break
case 'Role':
if (activeOu) {
await ouDeleteRoles(activeOu, deleteRow.id)
await fetchUsersAndRoles(activeOu)
}
break
case 'User':
if (activeOu) {
await ouDeleteMembers(activeOu, deleteRow.id)
await fetchUsersAndRoles(activeOu)
}
break
default:
break
}
setDeleteRow(null)
toast.push(
<Notification type="success" duration={2000}>
{translate('::KayitSilindi')}
</Notification>,
{
placement: 'top-center',
},
)
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
}
}}
>
{translate('::Delete')}
</Button>
</div>
</Dialog>
<Dialog
id="moveAllUsers"
isOpen={isMoveAllUsersOpen}
onClose={() => setIsMoveAllUsersOpen(false)}
onRequestClose={() => setIsMoveAllUsersOpen(false)}
>
<h5 className="mb-4">{translate('::Abp.Identity.OrganizationUnit.MoveAllUsers')}</h5>
<Formik
initialValues={{ id: activeOu, newId: '' }}
validationSchema={schemaMoveAllUsers}
onSubmit={async (values, { setSubmitting }) => {
if (!values.id || values.id === values.newId) {
return
}
setSubmitting(true)
try {
setLoading(true)
await ouMoveAllUsers(values.id, values.newId)
toast.push(
<Notification type="success" duration={2000}>
{translate('::Abp.Identity.OrganizationUnit.MoveAllUsersMessage')}
</Notification>,
{
placement: 'top-center',
},
)
await fetchUsersAndRoles(values.id)
setIsMoveAllUsersOpen(false)
} catch (error) {
toast.push(
<Notification type="danger" duration={2000}>
Hata
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setSubmitting(false)
}
}}
>
{({ touched, errors, values, isSubmitting }) => {
return (
<Form>
<FormContainer size="sm">
<FormItem
label={translate('::Abp.Identity.OrganizationUnit.CurrentOrganizationUnit')}
invalid={errors.id && touched.id}
errorMessage={errors.id}
>
<Field
type="text"
autoComplete="off"
name="id"
placeholder={translate(
'::Abp.Identity.OrganizationUnit.CurrentOrganizationUnit',
)}
>
{({ field, form }: FieldProps<OrganizationUnitDto>) => (
<Select
field={field}
form={form}
isClearable={true}
options={ous}
value={ous?.filter((option) => option.value === values.id)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::Abp.Identity.OrganizationUnit.NewOrganizationUnit')}
invalid={errors.newId && touched.newId}
errorMessage={errors.newId}
>
<Field
type="text"
autoComplete="off"
name="newId"
placeholder={translate('::Abp.Identity.OrganizationUnit.NewOrganizationUnit')}
>
{({ field, form }: FieldProps<OrganizationUnitDto>) => (
<Select
field={field}
form={form}
isClearable={true}
options={ous}
value={ous?.filter((option) => option.value === values.newId)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<div className="mt-6 flex flex-row justify-end gap-3">
<Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::...') : translate('::Save')}
</Button>
<Button
type="button"
variant="plain"
onClick={() => setIsMoveAllUsersOpen(false)}
>
{translate('::Cancel')}
</Button>
</div>
</FormContainer>
</Form>
)
}}
</Formik>
</Dialog>
</>
)
}
export default OrganizationUnits