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

View file

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

View file

@ -655,6 +655,53 @@ function getCurrentUserWorkflowIdentities(currentUser?: {
return [currentUser?.email, currentUser?.userName, currentUser?.name].filter(Boolean) as string[] 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( export function updateWorkflowApprovalToolbarItems(
component: any, component: any,
workflowOptions: WorkflowDto | undefined, workflowOptions: WorkflowDto | undefined,

View file

@ -108,11 +108,15 @@ export const MenuManager = () => {
Design Mode Design Mode
</span> </span>
<Button <Button
type="button"
variant="plain" variant="plain"
shape="none" shape="none"
role="switch"
aria-checked={isDesignMode}
aria-label="Toggle design mode"
className={` 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 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-200 hover:!bg-gray-200 dark:bg-gray-700 dark:hover:!bg-gray-700'} ${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} onClick={handleToggleDesignMode}
> >

View file

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