Form içerisindeki Workflow custom butonlar

This commit is contained in:
Sedat Öztürk 2026-07-13 20:29:53 +03:00
parent b53a4243ab
commit 1d05d24e41
5 changed files with 122 additions and 23 deletions

View file

@ -15,13 +15,14 @@ import {
} from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { GridColumnData } from '../list/GridColumnData'
import { useToolbar } from '../list/useToolbar'
import { isWorkflowToolbarItemDisabled, useToolbar } from '../list/useToolbar'
import { PermissionResults, RowMode } from './types'
import { GridDto } from '@/proxy/form/models'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { usePermission } from '@/utils/hooks/usePermission'
import { usePWA } from '@/utils/hooks/usePWA'
import { layoutTypes } from '../admin/listForm/edit/types'
import { useStoreState } from '@/store'
const FormButtons = (props: {
isSubForm?: boolean
@ -68,7 +69,7 @@ const FormButtons = (props: {
const { translate } = useLocalization()
const layout = layoutTypes.grid
const { toolbarData } = useToolbar({
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
gridDto,
listFormCode,
getSelectedRowKeys,
@ -78,6 +79,52 @@ const FormButtons = (props: {
layout,
})
const currentUser = useStoreState((state) => state.auth.user)
const selectedRowData = getSelectedRowsData()?.[0]
const rowEvent = {
row: {
data: selectedRowData,
key: id,
rowType: 'data',
isEditing: mode === 'edit',
},
}
const resolveVisible = (visible: unknown) => {
if (typeof visible !== 'function') {
return visible !== false
}
try {
return Boolean(visible(rowEvent))
} catch (error) {
console.error('Button visibility evaluation error:', error)
return false
}
}
const isToolbarButtonVisible = (item: any) =>
item.widget === 'dxButton' &&
!toolbarExcludedNames.includes(item.name) &&
resolveVisible(item.visible) &&
resolveVisible(item.options?.visible)
const isCommandButtonVisible = (item: any) =>
typeof item !== 'string' &&
!commandExcludedNames.includes(item.name) &&
resolveVisible(item.visible)
const isToolbarButtonDisabled = (item: any) => {
const workflowDisabled = isWorkflowToolbarItemDisabled(
item.name,
gridDto.gridOptions.workflowDto,
selectedRowData ? [selectedRowData] : [],
currentUser,
)
return workflowDisabled ?? Boolean(item.options?.disabled)
}
const openDynamicGridReport = () => {
if (!id) return
@ -116,22 +163,16 @@ const FormButtons = (props: {
}
const toolbarExcludedNames = ['deleteAllRecords', 'deleteSelectedRecords', 'refreshButton']
const hasVisibleToolbarButtons = toolbarData?.some(
(item) => item.widget == 'dxButton' && !toolbarExcludedNames.includes(item.name!),
)
const hasVisibleToolbarButtons = toolbarData?.some(isToolbarButtonVisible)
const commandExcludedNames = ['edit', 'delete', 'detail']
const hasVisibleCommandButtons = commandColumnData?.buttons?.some(
(item) => typeof item !== 'string' && !commandExcludedNames.includes(item.name!),
)
const hasVisibleCommandButtons = commandColumnData?.buttons?.some(isCommandButtonVisible)
return (
<>
<div className="flex flex-row items-center gap-1">
{toolbarData
?.filter(
(item) => item.widget == 'dxButton' && !toolbarExcludedNames.includes(item.name!),
)
?.filter(isToolbarButtonVisible)
.map((item, i) => {
const IconComp = navigationIcon[item.options?.icon]
const hasValidIcon =
@ -145,7 +186,8 @@ const FormButtons = (props: {
variant="default"
size="sm"
icon={hasValidIcon ? <IconComp className="text-gray-400" /> : null}
onClick={item.options?.onClick}
disabled={isToolbarButtonDisabled(item)}
onClick={(e) => item.options?.onClick?.({ ...e, ...rowEvent })}
>
{item.options?.text}
</Button>
@ -155,7 +197,7 @@ const FormButtons = (props: {
{hasVisibleToolbarButtons && <Badge innerClass="bg-blue-500" />}
{commandColumnData?.buttons
?.filter((item) => typeof item !== 'string' && !commandExcludedNames.includes(item.name!))
?.filter(isCommandButtonVisible)
.map((item: any, i) => {
return (
<Button
@ -165,9 +207,7 @@ const FormButtons = (props: {
title={item.hint}
onClick={(e: any) => {
if (item.onClick) {
const [rowData] = getSelectedRowsData()
e.row = { data: rowData }
item.onClick(e)
item.onClick({ ...e, ...rowEvent })
}
}}
>
@ -308,6 +348,13 @@ const FormButtons = (props: {
></Button>
)}
</div>
<Dialog
isOpen={toolbarModalData?.open || false}
onClose={() => setToolbarModalData(undefined)}
onRequestClose={() => setToolbarModalData(undefined)}
>
{toolbarModalData?.content}
</Dialog>
<Dialog
id="confirmDelete"
isOpen={!!deleteRowId}

View file

@ -109,7 +109,7 @@ const FormView = (
dataSource={dataSource!}
permissions={permissionResults}
handleSubmit={() => {}}
refreshData={() => {}}
refreshData={fetchData}
getSelectedRowKeys={() => [id]}
getSelectedRowsData={() => [formData]}
getFilter={() => filter}

View file

@ -655,6 +655,53 @@ function getCurrentUserWorkflowIdentities(currentUser?: {
return [currentUser?.email, currentUser?.userName, currentUser?.name].filter(Boolean) as string[]
}
export function isWorkflowToolbarItemDisabled(
itemName: string | undefined,
workflowOptions: WorkflowDto | undefined,
selectedRowsData: Record<string, unknown>[] = [],
currentUser?: {
userName?: string
email?: string
name?: string
},
) {
if (!itemName || !workflowOptions?.approvalStatusFieldName) {
return undefined
}
if (itemName === 'workflowStart') {
return (
selectedRowsData.length === 0 ||
!selectedRowsData.every((row) => isWorkflowNotStarted(row, workflowOptions))
)
}
if (!itemName.startsWith('workflowApproval_')) {
return undefined
}
const criteriaId = itemName.replace('workflowApproval_', '')
const criteria = workflowOptions.criteria?.find(
(item) => item.kind === 'Approval' && item.id === criteriaId,
)
if (!criteria) {
return true
}
const currentUserIdentities = getCurrentUserWorkflowIdentities(currentUser)
return (
selectedRowsData.length === 0 ||
!selectedRowsData.every((row) =>
isWorkflowApprovalCriteriaActive(
row,
workflowOptions,
criteria.title,
currentUserIdentities,
),
)
)
}
export function updateWorkflowApprovalToolbarItems(
component: any,
workflowOptions: WorkflowDto | undefined,

View file

@ -108,11 +108,15 @@ export const MenuManager = () => {
Design Mode
</span>
<Button
type="button"
variant="plain"
shape="none"
role="switch"
aria-checked={isDesignMode}
aria-label="Toggle design mode"
className={`
relative !inline-flex !h-6 !w-11 !min-w-11 !justify-start !rounded-full !border-0 !px-0 !items-center transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
${isDesignMode ? 'bg-blue-600 hover:!bg-blue-600 dark:bg-blue-700 dark:hover:!bg-blue-700' : 'bg-gray-200 hover:!bg-gray-200 dark:bg-gray-700 dark:hover:!bg-gray-700'}
relative !inline-flex !h-6 !w-11 !min-w-11 !items-center !justify-start !rounded-full !border-0 !px-0 shadow-inner transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
${isDesignMode ? '!bg-blue-600 hover:!bg-blue-600 dark:!bg-blue-700 dark:hover:!bg-blue-700' : '!bg-gray-300 hover:!bg-gray-300 dark:!bg-gray-700 dark:hover:!bg-gray-700'}
`}
onClick={handleToggleDesignMode}
>

View file

@ -31,9 +31,9 @@ export default defineConfig(async ({ mode }) => {
mode === 'production'
? {
globDirectory: 'dist',
// Precache only the application shell. Feature chunks and the
// many optional DevExtreme themes are cached on first use.
globPatterns: ['index.html', 'assets/css/index-*.css', 'assets/js/index-*.js'],
// Eski bir sekme yeni deploy sırasında henüz açmadığı lazy
// chunk'ları da çalıştırabilsin; tüm uygulama dosyalarını sakla.
globPatterns: ['**/*.{js,css,html,wasm}'],
// Büyük asset'leri de cache'leyebil
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
@ -52,9 +52,10 @@ export default defineConfig(async ({ mode }) => {
runtimeCaching: [
{
urlPattern: /\.(?:js|css|wasm)$/,
handler: 'CacheFirst',
handler: 'NetworkFirst',
options: {
cacheName: 'static-resources-v2',
networkTimeoutSeconds: 5,
expiration: {
maxEntries: 200,
maxAgeSeconds: 30 * 24 * 60 * 60,