sozsoft-platform/ui/src/views/list/useToolbar.tsx
2026-06-06 21:31:03 +03:00

675 lines
21 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 { 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'
type ToolbarModalData = {
open: boolean
content?: JSX.Element
}
// 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: () => void
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}>
Secili kayit icin workflow zaten baslamis.
</Notification>,
{ placement: 'top-end' },
)
return
}
try {
await workflowService.startWorkflow(listFormCode, keys)
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}>
Secili kayit bu onay adiminda veya onay kullanicisinda beklemiyor.
</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,
onClick() {
if (!grdOpt.deleteServiceAddress) {
return
}
dynamicFetch(grdOpt.deleteServiceAddress, 'POST', null, {
keys: getSelectedRowKeys(),
listFormCode,
}).then(() => {
refreshData()
})
},
},
})
// 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">Delete All Records</h5>
<p>Are you sure to delete all {r.data.totalCount} records?</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}>
{'Tüm kayıtlar silindi.'}
</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 {
await Promise.all(
keys.map((key) =>
workflowService.decideWorkflow(
listFormCode,
[key],
approved,
note,
criteriaId,
),
),
)
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>{keys.length} kayit icin workflow karari verilecek.</p>
<label className="mb-2 block font-semibold">Not</label>
<textarea
className="input input-textarea mb-4 min-h-[96px] w-full resize-y"
rows={4}
value={note}
placeholder="Onay veya red aciklamasi"
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)}>
Reddet
</Button>
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
Onayla
</Button>
</div>
</>
)
}
export { useToolbar }