sozsoft-platform/ui/src/views/list/useToolbar.tsx
2026-06-07 22:42:02 +03:00

775 lines
25 KiB
TypeScript

import { Button, Notification, toast } from '@/components/ui'
import { GridDto, UiCommandButtonPositionTypeEnum, WorkflowDto } from '@/proxy/form/models'
import { dynamicFetch } from '@/services/form.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { DataGridTypes } from 'devextreme-react/data-grid'
import { ToolbarItem } from 'devextreme/ui/data_grid_types'
import { useEffect, useState } from 'react'
import { useDialogContext } from '../shared/DialogContext'
import { usePWA } from '@/utils/hooks/usePWA'
import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types'
import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service'
import type { WorkflowRunResultDto } from '@/services/workflow.service'
type ToolbarModalData = {
open: boolean
content?: JSX.Element
}
const showWorkflowToastMessages = (results: WorkflowRunResultDto | WorkflowRunResultDto[]) => {
const list = Array.isArray(results) ? results : [results]
const messages = list.flatMap((result) => result.toastMessages ?? [])
if (!messages.length) {
return
}
toast.push(
<Notification type="info" duration={7000}>
{messages.map((message, messageIndex) => (
<div key={messageIndex} className={messageIndex > 0 ? 'mt-2 border-t pt-2' : undefined}>
{message.split('\n').map((line, lineIndex) => (
<div key={lineIndex}>{line}</div>
))}
</div>
))}
</Notification>,
{ placement: 'top-end' },
)
}
// https://js.devexpress.com/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/toolbar/
// item.name > Accepted Values: 'addRowButton', 'applyFilterButton', 'columnChooserButton', 'exportButton', 'groupPanel', 'revertButton', 'saveButton', 'searchPanel'
const useToolbar = ({
gridDto,
listFormCode,
getSelectedRowKeys,
getSelectedRowsData,
refreshData,
getFilter,
layout,
expandAll,
collapseAll,
}: {
gridDto?: GridDto
listFormCode: string
getSelectedRowKeys: () => unknown[] | Promise<unknown[]>
getSelectedRowsData: () => any
refreshData: () => void
getFilter: () => void
layout: ListViewLayoutType | string
expandAll?: () => void
collapseAll?: () => void
}): {
toolbarData: ToolbarItem[]
toolbarModalData: ToolbarModalData | undefined
setToolbarModalData: (data: ToolbarModalData | undefined) => void
} => {
const dialog: any = useDialogContext()
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
const currentUser = useStoreState((state) => state.auth.user)
const [toolbarData, setToolbarData] = useState<ToolbarItem[]>([])
const [toolbarModalData, setToolbarModalData] = useState<ToolbarModalData>()
const grdOpt = gridDto?.gridOptions
function getToolbarData() {
const items: ToolbarItem[] = []
if (!gridDto || !grdOpt) {
setToolbarData(items)
return
}
// Add searchPanel
if (grdOpt.searchPanelDto?.visible) {
items.push({
locateInMenu: 'auto',
showText: 'inMenu',
name: 'searchPanel',
})
}
// Add InsertNewRecord button
if (grdOpt.editingOptionDto?.allowAdding && checkPermission(grdOpt.permissionDto?.c)) {
if (grdOpt.editingOptionDto?.addPageUrl) {
items.push({
widget: 'dxButton',
showText: 'always',
name: 'addRowButton',
location: 'after',
options: {
text: translate('::ListForms.ListForm.AddNewRecord'),
hint: translate('::ListForms.ListForm.AddNewRecord'),
onClick() {
window.open(grdOpt.editingOptionDto?.addPageUrl, isPwaMode ? '_self' : '_blank')
},
},
})
} else {
items.push({
locateInMenu: 'auto',
showText: 'always',
name: 'addRowButton',
location: 'after',
options: {
text: translate('::ListForms.ListForm.AddNewRecord'),
hint: translate('::ListForms.ListForm.AddNewRecord'),
},
})
}
}
items.push({
widget: 'dxButton',
name: 'refreshButton',
options: {
icon: 'refresh',
onClick: refreshData,
text: translate('::ListForms.ListForm.Refresh'),
},
location: 'after',
})
const workflowOptions = grdOpt.workflowDto
const approvalCriteria =
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
if (
workflowOptions?.approvalStatusFieldName &&
approvalCriteria.length > 0 &&
grdOpt.updateServiceAddress
) {
items.push({
widget: 'dxButton',
name: 'workflowStart',
location: 'after',
options: {
icon: 'play',
text: 'Workflow Start',
hint: 'Workflow Start',
visible: true,
disabled: true,
onClick: async () => {
const keys = (await Promise.resolve(getSelectedRowKeys() as any)) as unknown[]
if (!keys?.length) {
toast.push(
<Notification type="warning" duration={2000}>
{translate('::ListForms.ListForm.SelectRecord')}
</Notification>,
{ placement: 'top-end' },
)
return
}
const selectedRows = ((await Promise.resolve(getSelectedRowsData() as any)) ||
[]) as Record<string, unknown>[]
if (
selectedRows.length === 0 ||
!selectedRows.every((row) => isWorkflowNotStarted(row, workflowOptions))
) {
toast.push(
<Notification type="warning" duration={2500}>
{translate('::WorkflowAlreadyStarted')}
</Notification>,
{ placement: 'top-end' },
)
return
}
try {
const result = await workflowService.startWorkflow(listFormCode, keys)
showWorkflowToastMessages(result)
refreshData()
} catch (error: any) {
toast.push(
<Notification type="danger" duration={3000}>
{error?.response?.data?.error?.message ||
error?.response?.data?.message ||
error?.message ||
'Workflow baslatilamadi.'}
</Notification>,
{ placement: 'top-end' },
)
}
},
},
})
approvalCriteria.forEach((criteria) => {
items.push({
widget: 'dxButton',
name: `workflowApproval_${criteria.id}`,
location: 'after',
options: {
icon: 'check',
text: criteria.title,
hint: criteria.title,
visible: true,
disabled: true,
onClick: async () => {
const keys = (await Promise.resolve(getSelectedRowKeys() as any)) as unknown[]
if (!keys?.length) {
toast.push(
<Notification type="warning" duration={2000}>
{translate('::ListForms.ListForm.SelectRecord')}
</Notification>,
{ placement: 'top-end' },
)
return
}
const selectedRows = ((await Promise.resolve(getSelectedRowsData() as any)) ||
[]) as Record<string, unknown>[]
const activeRows = selectedRows.filter((row) =>
isWorkflowApprovalCriteriaActive(
row,
workflowOptions,
criteria.title,
getCurrentUserWorkflowIdentities(currentUser),
),
)
if (activeRows.length !== selectedRows.length) {
toast.push(
<Notification type="warning" duration={2500}>
{translate('::SeciliKayitBekliyor')}
</Notification>,
{ placement: 'top-end' },
)
return
}
setToolbarModalData({
open: true,
content: (
<>
<WorkflowApprovalDecisionDialog
criteriaTitle={criteria.title}
keys={keys}
listFormCode={listFormCode}
criteriaId={criteria.id}
onCancel={() => setToolbarModalData(undefined)}
onCompleted={() => {
refreshData()
setToolbarModalData(undefined)
}}
/>
</>
),
})
},
},
})
})
}
// Add Expand All button for TreeList
if (layout === layoutTypes.tree && grdOpt.treeOptionDto?.parentIdExpr) {
items.push({
widget: 'dxButton',
name: 'expandAllButton',
options: {
icon: 'plus',
text: translate('::ListForms.ListFormEdit.ExpandAll'),
onClick: expandAll,
},
location: 'after',
})
// Add Collapse All button for TreeList
items.push({
widget: 'dxButton',
name: 'collapseAllButton',
options: {
icon: 'minus',
text: translate('::ListForms.ListFormEdit.CollapseAll'),
onClick: collapseAll,
},
location: 'after',
})
}
// field chooser panel
if (grdOpt.columnOptionDto?.columnChooserEnabled) {
items.push({
locateInMenu: 'auto',
showText: 'inMenu',
name: 'columnChooserButton',
options: {
hint: translate('::ListForms.ListForm.ColumnChooser'),
},
})
}
// Add group panel
if (grdOpt.groupPanelDto?.visible) {
items.push({
locateInMenu: 'auto',
showText: 'inMenu',
name: 'groupPanel',
})
}
// Add DeleteSelectedRecords button
// coklu silme icin
if (grdOpt.editingOptionDto?.allowDeleting && checkPermission(grdOpt.permissionDto?.d)) {
items.push({
location: 'after',
widget: 'dxButton',
locateInMenu: 'auto',
showText: 'inMenu',
name: 'deleteSelectedRecords',
options: {
text: translate('::ListForms.ListForm.DeleteSelectedRecords'),
icon: 'trash',
visible: false,
async onClick() {
if (!grdOpt.deleteServiceAddress) {
return
}
const selectedKeys = await Promise.resolve(getSelectedRowKeys())
const keys = Array.isArray(selectedKeys) ? [...selectedKeys] : []
if (!keys.length) {
toast.push(
<Notification type="warning" duration={2000}>
{translate('::ListForms.ListForm.SelectRecord')}
</Notification>,
{ placement: 'top-end' },
)
return
}
setToolbarModalData({
open: true,
content: (
<>
<h5 className="mb-4">
{translate('::ListForms.ListForm.DeleteSelectedRecords')}
</h5>
<p>
{translate('::SeciliKayitlarSilmekIstiyormusunuz', {
0: keys.length,
})}
</p>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={() => setToolbarModalData(undefined)}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={() => {
dynamicFetch(grdOpt.deleteServiceAddress!, 'POST', null, {
keys,
listFormCode,
})
.then(() => {
refreshData()
setToolbarModalData(undefined)
})
.catch((error: any) => {
toast.push(
<Notification type="danger" duration={3000}>
{error?.response?.data?.error?.message ||
error?.response?.data?.message ||
error?.message ||
translate('::SilmeIslemiBasarisiz')}
</Notification>,
{ placement: 'top-end' },
)
})
}}
>
{translate('::Delete')}
</Button>
</div>
</>
),
})
},
},
})
// Add DeleteAllRecords button
// butun kayitlari (filtreli) icin
if (grdOpt.editingOptionDto?.allowAllDeleting) {
const buttonDeleteAll: DataGridTypes.ToolbarItem = {
location: 'after',
widget: 'dxButton',
name: 'deleteAllRecords',
options: {
text: translate('::ListForms.ListForm.DeleteAllRecords'),
hint: translate('::ListForms.ListForm.DeleteAllRecords'),
icon: 'trash',
visible: true,
onClick() {
const parameters = {
listFormCode,
filter: JSON.stringify(getFilter()),
onlyTotalCountQuery: true,
createDeleteQuery: false,
}
dynamicFetch('list-form-select/select', 'GET', parameters).then((r: any) => {
setToolbarModalData({
open: true,
content: (
<>
<h5 className="mb-4">{translate('::ListForms.ListForm.DeleteAllRecords')}</h5>
<p>
{translate('::TumKayitlariSilmekIstiyormusunuz', {
0: r.data.totalCount,
})}
</p>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={() => setToolbarModalData(undefined)}
>
Cancel
</Button>
<Button
variant="solid"
onClick={() => {
//delete parameters.onlyTotalCountQuery
parameters.createDeleteQuery = true // tumunu silme islemi parametresi > set
dynamicFetch('list-form-select/select', 'GET', parameters).then(() => {
toast.push(
<Notification type="success" duration={2000}>
{translate('::TumKayitlarSilindi')}
</Notification>,
{
placement: 'top-end',
},
)
refreshData()
setToolbarModalData(undefined)
})
}}
>
Save
</Button>
</div>
</>
),
})
})
},
},
}
items.push(buttonDeleteAll)
}
}
// #region Toolbar icin kullanici tanimli dinamik butonlari ekler
for (let i = 0; i < grdOpt.commandColumnDto.length; i++) {
const action = grdOpt.commandColumnDto[i]
// action.buttonPosition == 1 ise Toolbar butonudur, burada sadece Toolbar butonunu eklenir
if (action.buttonPosition !== UiCommandButtonPositionTypeEnum.Toolbar) {
continue
}
if (checkPermission(action.authName)) {
const buttonCustom: DataGridTypes.ToolbarItem = {
location: 'after',
widget: 'dxButton',
name: action.hint,
options: {
hint: translate('::' + action.hint),
text: translate('::' + action.text),
icon: action.icon,
visible: true,
onClick(e: any) {
if (typeof e.event?.preventDefault === 'function') {
e?.event?.preventDefault()
}
if (action.url) {
let url = action.url
// griddeki secili satirlari al
const selectedRowsData = getSelectedRowsData()
// constsa her bir secili satir icin donguye gir
for (let i = 0; i < selectedRowsData.length; i++) {
// secili satirin objesine ait property verilerini al
const keys = Object.keys(selectedRowsData[i])
// secili satirin her bir property si icin donguye gir
for (let j = 0; j < keys.length; j++) {
// secili satirin j indexine sahip property ismini al
const fieldName = keys[j]
// secili satirin j indexine sahip property isminin degerini al
const fieldValue = selectedRowsData[i][fieldName]
// url icerisindeki {PropertyName} seklindeki anahtarlari secili satirdaki uyusan propertyler ile degistir
url = url.replace(`@${fieldName}`, fieldValue)
}
break // Url cagirmak icin kullanilacak parametreler sadece secili olan ilk satirdan alinir!
}
window.open(url, isPwaMode ? '_self' : action.urlTarget)
} else if (action.dialogName) {
if (action.dialogParameters) {
var dynamicMap = JSON.parse(action.dialogParameters)
for (const [key, value] of Object.entries<string>(dynamicMap)) {
dynamicMap[key] = value.startsWith('@')
? e.row.data[value.replace('@', '')]
: value
}
dialog.setConfig({
component: action.dialogName,
props: dynamicMap,
})
}
} else if (action.onClick) {
eval(action.onClick)
}
},
},
}
items.push(buttonCustom)
}
}
// #endregion
// batch editing icin kaydet ve geri al butonu
if (
grdOpt.editingOptionDto?.allowUpdating &&
grdOpt.editingOptionDto?.mode == 'batch' &&
checkPermission(grdOpt.permissionDto?.u)
) {
items.push({
locateInMenu: 'auto',
showText: 'inMenu',
name: 'saveButton',
options: { hint: translate('::App.SaveChanges') },
})
items.push({
locateInMenu: 'auto',
showText: 'inMenu',
name: 'revertButton',
options: { hint: translate('::App.UndoChanges') },
})
}
// #endregion
setToolbarData(items)
}
useEffect(() => {
if (!gridDto && !listFormCode) return
getToolbarData()
}, [gridDto, listFormCode, currentUser])
return {
toolbarData,
toolbarModalData,
setToolbarModalData,
}
}
function isWorkflowApprovalCriteriaActive(
row: Record<string, unknown>,
workflowOptions: WorkflowDto,
criteriaTitle: string,
currentUserIdentities: string[] = [],
) {
if (!workflowOptions.approvalStatusFieldName || !criteriaTitle) {
return false
}
const statusMatches =
normalizeWorkflowValue(row?.[workflowOptions.approvalStatusFieldName]) ===
normalizeWorkflowValue(criteriaTitle)
if (!statusMatches) {
return false
}
if (!workflowOptions.approvalUserFieldName) {
return true
}
const approver = normalizeWorkflowValue(row?.[workflowOptions.approvalUserFieldName])
return currentUserIdentities.some((identity) => normalizeWorkflowValue(identity) === approver)
}
function normalizeWorkflowValue(value: unknown) {
return String(value ?? '')
.trim()
.toLocaleLowerCase('tr-TR')
}
function isWorkflowNotStarted(row: Record<string, unknown>, workflowOptions: WorkflowDto) {
return normalizeWorkflowValue(row?.[workflowOptions.approvalStatusFieldName]) === ''
}
function getCurrentUserWorkflowIdentities(currentUser?: {
userName?: string
email?: string
name?: string
}) {
return [currentUser?.email, currentUser?.userName, currentUser?.name].filter(Boolean) as string[]
}
export function updateWorkflowApprovalToolbarItems(
component: any,
workflowOptions: WorkflowDto | undefined,
selectedRowsData: Record<string, unknown>[] = [],
currentUser?: {
userName?: string
email?: string
name?: string
},
) {
const approvalCriteria =
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
if (!component || !workflowOptions?.approvalStatusFieldName || !approvalCriteria.length) {
return
}
const toolbarOptions = component.option('toolbar')
if (!toolbarOptions?.items || !Array.isArray(toolbarOptions.items)) {
return
}
const workflowStartItemIndex = toolbarOptions.items
.map((item: any) => item.name)
.indexOf('workflowStart')
if (workflowStartItemIndex >= 0) {
const startEnabled =
selectedRowsData.length > 0 &&
selectedRowsData.every((row) => isWorkflowNotStarted(row, workflowOptions))
const startOptionPath = `toolbar.items[${workflowStartItemIndex}].options.disabled`
const nextStartDisabled = !startEnabled
if (component.option(startOptionPath) !== nextStartDisabled) {
component.option(startOptionPath, nextStartDisabled)
}
}
const currentUserIdentities = getCurrentUserWorkflowIdentities(currentUser)
approvalCriteria.forEach((criteria) => {
const toolbarItemIndex = toolbarOptions.items
.map((item: any) => item.name)
.indexOf(`workflowApproval_${criteria.id}`)
if (toolbarItemIndex < 0) {
return
}
const enabled =
selectedRowsData.length > 0 &&
selectedRowsData.every((row) =>
isWorkflowApprovalCriteriaActive(
row,
workflowOptions,
criteria.title,
currentUserIdentities,
),
)
const optionPath = `toolbar.items[${toolbarItemIndex}].options.disabled`
const nextDisabled = !enabled
if (component.option(optionPath) !== nextDisabled) {
component.option(optionPath, nextDisabled)
}
})
}
function WorkflowApprovalDecisionDialog({
criteriaTitle,
keys,
listFormCode,
criteriaId,
onCancel,
onCompleted,
}: {
criteriaTitle: string
keys: unknown[]
listFormCode: string
criteriaId: string
onCancel: () => void
onCompleted: () => void
}) {
const { translate } = useLocalization()
const [note, setNote] = useState('')
const [submitting, setSubmitting] = useState(false)
const decide = async (approved: boolean) => {
setSubmitting(true)
try {
const results = await Promise.all(
keys.map((key) =>
workflowService.decideWorkflow(listFormCode, [key], approved, note, criteriaId),
),
)
showWorkflowToastMessages(results)
onCompleted()
} catch (error: any) {
toast.push(
<Notification type="danger" duration={3000}>
{error?.response?.data?.error?.message ||
error?.response?.data?.message ||
error?.message ||
'Workflow karari verilemedi.'}
</Notification>,
{ placement: 'top-end' },
)
} finally {
setSubmitting(false)
}
}
return (
<>
<h5 className="mb-4">{criteriaTitle}</h5>
<p className="mb-4">
{translate('::App.Listform.ListformField.WorkflowDecisionMessage', {
0: keys.length,
})}
</p>
<textarea
className="input input-textarea mb-4 min-h-[96px] w-full resize-y"
rows={4}
value={note}
autoFocus
placeholder={translate('::App.Listform.ListformField.ApprovalComment')}
onChange={(event) => setNote(event.target.value)}
/>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
disabled={submitting}
onClick={onCancel}
>
{translate('::Cancel')}
</Button>
<Button className="ltr:mr-2 rtl:ml-2" disabled={submitting} onClick={() => decide(false)}>
{translate('::App.Listform.ListformField.Rejecter')}
</Button>
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
{translate('::App.Listform.ListformField.Approver')}
</Button>
</div>
</>
)
}
export { useToolbar }