+
{[
['approvalUserFieldName', '::App.ListFormEditWorkflow.ApprovalUserFieldName'],
[
@@ -484,6 +484,26 @@ function WizardStepWorkflow({
)}
+
+
+ {({ field, form }: FieldProps) => (
+ {
+ form.setFieldValue(field.name, checked)
+ onWorkflowChange({
+ ...values,
+ approvalIsManualStart: checked,
+ criteria,
+ })
+ }}
+ />
+ )}
+
+
diff --git a/ui/src/views/form/FormDevExpress.tsx b/ui/src/views/form/FormDevExpress.tsx
index 3dc66e8b..61c21eba 100644
--- a/ui/src/views/form/FormDevExpress.tsx
+++ b/ui/src/views/form/FormDevExpress.tsx
@@ -14,6 +14,7 @@ import { Dispatch, ReactNode, RefObject, useCallback, useEffect, useRef, useStat
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
+import { RatingEditorComponent } from './editors/RatingEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { RowMode, SimpleItemWithColData } from './types'
import { PlatformEditorTypes } from '@/proxy/form/models'
@@ -47,6 +48,7 @@ const CUSTOM_EDITOR_TYPES = [
PlatformEditorTypes.dxGridBox,
PlatformEditorTypes.dxImageUpload,
PlatformEditorTypes.dxImageViewer,
+ PlatformEditorTypes.dxRating,
] as const
const FormDevExpress = (props: {
@@ -402,6 +404,14 @@ const FormDevExpress = (props: {
editorOptions={editorOptions}
/>
)
+ case PlatformEditorTypes.dxRating:
+ return (
+
+ )
default:
return null
}
@@ -437,12 +447,13 @@ const FormDevExpress = (props: {
)
}
- // Görsel editörleri DevExtreme'in bilmediği alan tiplerini yaymamak için
+ // Görsel ve puan editörleri DevExtreme'in bilmediği alan tiplerini yaymamak için
// yalnızca gerekli alanlarla oluşturulur.
- const isImageEditor =
+ const isStandaloneEditor =
formItem.editorType2 === PlatformEditorTypes.dxImageUpload ||
- formItem.editorType2 === PlatformEditorTypes.dxImageViewer
- const itemProps = isImageEditor
+ formItem.editorType2 === PlatformEditorTypes.dxImageViewer ||
+ formItem.editorType2 === PlatformEditorTypes.dxRating
+ const itemProps = isStandaloneEditor
? {
dataField: formItem.dataField,
name: formItem.name,
diff --git a/ui/src/views/form/editors/RatingEditorComponent.tsx b/ui/src/views/form/editors/RatingEditorComponent.tsx
new file mode 100644
index 00000000..2dfee5c9
--- /dev/null
+++ b/ui/src/views/form/editors/RatingEditorComponent.tsx
@@ -0,0 +1,44 @@
+import { useEffect, useState, type ReactElement } from 'react'
+import { Rate } from '@/components/ui'
+import { normalizeRatingValue, resolveRatingCount } from '../../list/shared/ratingValue'
+
+/** Form sayfası ve liste düzenleme hücresinin ortak yıldızlı puan editörü; değer 1..count tam sayıdır. */
+const RatingEditorComponent = ({
+ value,
+ onValueChanged,
+ editorOptions,
+}: {
+ value: any
+ onValueChanged: (value: number | null) => void
+ editorOptions?: any
+}): ReactElement => {
+ const count = resolveRatingCount(editorOptions)
+ const valueRating = normalizeRatingValue(value, count)
+
+ // Grid/Tree hücre şablonu bir kez çizilir ve setValue sonrası `value` prop'u güncellenmez;
+ // seçim yerel durumda tutulmazsa yıldızlar tıklamadan sonra eski değere döner.
+ const [rating, setRating] = useState(valueRating)
+
+ useEffect(() => {
+ setRating(valueRating)
+ }, [valueRating])
+
+ return (
+
{
+ setRating(next)
+ // Temizlenen puan 0 değil boş kaydedilir; 0 geçerli bir puan değildir.
+ onValueChanged(next || null)
+ }}
+ />
+ )
+}
+
+export { RatingEditorComponent }
diff --git a/ui/src/views/form/types.ts b/ui/src/views/form/types.ts
index 659e0b77..62bd2c58 100644
--- a/ui/src/views/form/types.ts
+++ b/ui/src/views/form/types.ts
@@ -14,6 +14,7 @@ export type EditorType2 =
| PlatformEditorTypes.dxGridBox
| PlatformEditorTypes.dxImageViewer
| PlatformEditorTypes.dxImageUpload
+ | PlatformEditorTypes.dxRating
export type SimpleItemWithColData = Overwrite<
SimpleItem,
{
diff --git a/ui/src/views/list/CardView.tsx b/ui/src/views/list/CardView.tsx
index 862feb66..059585c0 100644
--- a/ui/src/views/list/CardView.tsx
+++ b/ui/src/views/list/CardView.tsx
@@ -34,14 +34,18 @@ import DxCardView, {
} from 'devextreme-react/card-view'
import CustomStore from 'devextreme/data/custom_store'
import type { DataType } from 'devextreme/common'
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
import { createRoot } from 'react-dom/client'
import PageTitle from '@/components/shared/PageTitle'
import { NotePanel } from '../form/notes/NotePanel'
import SubForms from '../form/SubForms'
import GridFilterDialogs from './GridFilterDialogs'
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
-import { ImageUploadEditorComponent, ImageViewerEditorComponent } from './editors/cellEditors'
+import {
+ ImageUploadEditorComponent,
+ ImageViewerEditorComponent,
+ RatingEditorComponent,
+} from './editors/cellEditors'
import {
autoNumber,
extractSearchParamsFields,
@@ -93,9 +97,11 @@ const toCardColumn = (
const dataField = column.dataField
const originalCustomizeText =
typeof column.customizeText === 'function' ? column.customizeText : undefined
- const isImageColumn =
+ // Görsel ve puan kolonlarının hücre şablonu kartta da aynı çizimi yapar.
+ const hasCellTemplate =
typeof column.cellTemplate === 'function' &&
- Object.prototype.hasOwnProperty.call(column.extras ?? {}, 'imageUploadOptions')
+ (Object.prototype.hasOwnProperty.call(column.extras ?? {}, 'imageUploadOptions') ||
+ column.colData?.editorType2 === PlatformEditorTypes.dxRating)
const isHtmlColumn = column.colData?.editorType2 === 'dxHtmlEditor'
return Object.fromEntries(
@@ -129,27 +135,26 @@ const toCardColumn = (
dataType: column.dataType,
editorOptions: column.editorOptions,
filterValue: column.filterValue,
- fieldValueTemplate:
- isImageColumn && typeof column.cellTemplate === 'function'
+ fieldValueTemplate: hasCellTemplate
+ ? (templateOptions: any) => {
+ const templateData = templateOptions?.model ?? templateOptions
+ const container = document.createElement('div')
+ column.cellTemplate(container, {
+ column,
+ data: templateData.field?.card?.data,
+ displayValue: templateData.field?.displayValue,
+ value: templateData.field?.value,
+ })
+ return container
+ }
+ : isHtmlColumn
? (templateOptions: any) => {
const templateData = templateOptions?.model ?? templateOptions
const container = document.createElement('div')
- column.cellTemplate(container, {
- column,
- data: templateData.field?.card?.data,
- displayValue: templateData.field?.displayValue,
- value: templateData.field?.value,
- })
+ container.innerHTML = String(templateData.field?.value ?? '')
return container
}
- : isHtmlColumn
- ? (templateOptions: any) => {
- const templateData = templateOptions?.model ?? templateOptions
- const container = document.createElement('div')
- container.innerHTML = String(templateData.field?.value ?? '')
- return container
- }
- : undefined,
+ : undefined,
format: column.format,
headerFilter: column.headerFilter,
lookup: column.lookup,
@@ -545,39 +550,43 @@ const CardView = (props: CardViewProps) => {
const commandButtonsRef = useRef(commandButtons)
commandButtonsRef.current = commandButtons
- const renderImageUploadEditor = useCallback((templateData: any) => {
- const configuredDataField =
- templateData?.dataField ??
- templateData?.item?.dataField ??
- templateData?.editorOptions?.name ??
- templateData?.name
- const dataField = getEditingFieldName(configuredDataField)
- const column = listColumnsCacheRef.current.columns.find(
- (candidate) => candidate.dataField?.toLowerCase() === String(dataField ?? '').toLowerCase(),
- )
- const component = templateData?.component
- const currentFormData = component?.option?.('formData') ?? formDataRef.current ?? {}
- const editorValue = templateData?.editorOptions?.value
+ /** Değer yazan platform editörünü (görsel yükleme, puan) kart formuna bağlar. */
+ const renderValueEditor = useCallback(
+ (templateData: any, Editor: (cell: any) => ReactElement) => {
+ const configuredDataField =
+ templateData?.dataField ??
+ templateData?.item?.dataField ??
+ templateData?.editorOptions?.name ??
+ templateData?.name
+ const dataField = getEditingFieldName(configuredDataField)
+ const column = listColumnsCacheRef.current.columns.find(
+ (candidate) => candidate.dataField?.toLowerCase() === String(dataField ?? '').toLowerCase(),
+ )
+ const component = templateData?.component
+ const currentFormData = component?.option?.('formData') ?? formDataRef.current ?? {}
+ const editorValue = templateData?.editorOptions?.value
- return (
- {
- if (!dataField) return
- if (typeof templateData?.editorOptions?.onValueChanged === 'function') {
- templateData.editorOptions.onValueChanged({ value })
- } else {
- component?.updateData?.(dataField, value)
+ return (
+
- )
- }, [])
+ setValue={(value: any) => {
+ if (!dataField) return
+ if (typeof templateData?.editorOptions?.onValueChanged === 'function') {
+ templateData.editorOptions.onValueChanged({ value })
+ } else {
+ component?.updateData?.(dataField, value)
+ }
+ formDataRef.current = { ...currentFormData, [dataField]: value }
+ }}
+ />
+ )
+ },
+ [],
+ )
const renderImageViewerEditor = useCallback(
(templateData: any) => (
@@ -590,11 +599,22 @@ const CardView = (props: CardViewProps) => {
(templateOptions: any) => {
const container = document.createElement('div')
createRoot(container).render(
- renderImageUploadEditor(templateOptions?.model ?? templateOptions),
+ renderValueEditor(templateOptions?.model ?? templateOptions, ImageUploadEditorComponent),
)
return container
},
- [renderImageUploadEditor],
+ [renderValueEditor],
+ )
+
+ const ratingFormTemplate = useCallback(
+ (templateOptions: any) => {
+ const container = document.createElement('div')
+ createRoot(container).render(
+ renderValueEditor(templateOptions?.model ?? templateOptions, RatingEditorComponent),
+ )
+ return container
+ },
+ [renderValueEditor],
)
const imageViewerFormTemplate = useCallback(
@@ -893,10 +913,19 @@ const CardView = (props: CardViewProps) => {
? imageUploadFormTemplate
: item.editorType2 === PlatformEditorTypes.dxImageViewer
? imageViewerFormTemplate
- : undefined,
+ : item.editorType2 === PlatformEditorTypes.dxRating
+ ? ratingFormTemplate
+ : undefined,
}
},
- [gridDto, imageUploadFormTemplate, imageViewerFormTemplate, mode, translate],
+ [
+ gridDto,
+ imageUploadFormTemplate,
+ imageViewerFormTemplate,
+ ratingFormTemplate,
+ mode,
+ translate,
+ ],
)
const editingFormItems = useMemo(() => {
diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx
index 6a8bdf64..c09f54a1 100644
--- a/ui/src/views/list/Grid.tsx
+++ b/ui/src/views/list/Grid.tsx
@@ -37,7 +37,13 @@ import SubForms from '../form/SubForms'
import { RowMode } from '../form/types'
import { GridColumnData } from './GridColumnData'
import GridFilterDialogs from './GridFilterDialogs'
-import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors'
+import {
+ GridBoxEditorComponent,
+ ImageUploadEditorComponent,
+ ImageViewerEditorComponent,
+ RatingEditorComponent,
+ TagBoxEditorComponent,
+} from './editors/cellEditors'
import { useFilters } from './useFilters'
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
@@ -638,6 +644,10 @@ const Grid = (props: GridProps) => {
name="cellEditImageUpload"
render={(cell: any) => ImageUploadEditorComponent(adaptCustomEditorCell(cell))}
/>
+ RatingEditorComponent(adaptCustomEditorCell(cell))}
+ />
(
diff --git a/ui/src/views/list/Tree.tsx b/ui/src/views/list/Tree.tsx
index 86e8f970..bbaee020 100644
--- a/ui/src/views/list/Tree.tsx
+++ b/ui/src/views/list/Tree.tsx
@@ -31,7 +31,13 @@ import PageTitle from '@/components/shared/PageTitle'
import { RowMode } from '../form/types'
import { GridColumnData } from './GridColumnData'
import GridFilterDialogs from './GridFilterDialogs'
-import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors'
+import {
+ GridBoxEditorComponent,
+ ImageUploadEditorComponent,
+ ImageViewerEditorComponent,
+ RatingEditorComponent,
+ TagBoxEditorComponent,
+} from './editors/cellEditors'
import { useFilters } from './useFilters'
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
@@ -627,6 +633,10 @@ const Tree = (props: TreeProps) => {
name="cellEditImageUpload"
render={(cell: any) => ImageUploadEditorComponent(adaptCustomEditorCell(cell))}
/>
+ RatingEditorComponent(adaptCustomEditorCell(cell))}
+ />
(
diff --git a/ui/src/views/list/editors/cellEditors.tsx b/ui/src/views/list/editors/cellEditors.tsx
index 2d3fd0ae..b1b8e76d 100644
--- a/ui/src/views/list/editors/cellEditors.tsx
+++ b/ui/src/views/list/editors/cellEditors.tsx
@@ -3,6 +3,7 @@ import type { ImageUploadOptionsDto } from '@/proxy/form/models'
import { GridBoxEditorComponent as FormGridBoxEditor } from '../../form/editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent as FormImageUploadEditor } from '../../form/editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent as FormImageViewerEditor } from '../../form/editors/ImageViewerEditorComponent'
+import { RatingEditorComponent as FormRatingEditor } from '../../form/editors/RatingEditorComponent'
import { TagBoxEditorComponent as FormTagBoxEditor } from '../../form/editors/TagBoxEditorComponent'
import { parseJsonObject } from '../shared/imageValue'
@@ -12,7 +13,6 @@ import { parseJsonObject } from '../shared/imageValue'
* `form/editors/*`tir; burada yalnızca hücre verisi bileşen prop'larına çevrilir.
*/
-
/**
* Ekranın editorOptions'ı + hücre bağlamının salt-okunur/devre dışı kararı. Bağlamın geri
* kalanı (`value`, `onValueChanged`…) editörün kendi değer akışını ezeceği için alınmaz.
@@ -104,6 +104,15 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
)
}
+/** Liste düzenleme hücresi (Grid, Tree, CardView); editör form sayfasıyla aynı bileşendir. */
+const RatingEditorComponent = (cellElement: any): ReactElement => (
+ cellElement.setValue(value)}
+ />
+)
+
const resolveTemplateValue = (templateData: any) => {
const dataField =
templateData?.dataField ||
@@ -168,5 +177,6 @@ export {
GridBoxEditorComponent,
ImageUploadEditorComponent,
ImageViewerEditorComponent,
+ RatingEditorComponent,
TagBoxEditorComponent,
}
diff --git a/ui/src/views/list/shared/editingForm.ts b/ui/src/views/list/shared/editingForm.ts
index 7300347a..c4e833ba 100644
--- a/ui/src/views/list/shared/editingForm.ts
+++ b/ui/src/views/list/shared/editingForm.ts
@@ -468,6 +468,8 @@ const getDateEditorDefaults = (sourceDbType?: DbTypeEnum) => {
/** PlatformEditorTypes -> DevExtreme editorType eşlemesi. */
export const resolveEditorType = (editorType2?: EditorType2) => {
if (editorType2 === PlatformEditorTypes.dxGridBox) return 'dxDropDownBox'
+ // Yıldız şablonu olmayan görünümlerde (Scheduler…) puan sayı olarak girilir.
+ if (editorType2 === PlatformEditorTypes.dxRating) return 'dxNumberBox'
if (
editorType2 === PlatformEditorTypes.dxImageUpload ||
editorType2 === PlatformEditorTypes.dxImageViewer
diff --git a/ui/src/views/list/shared/ratingValue.ts b/ui/src/views/list/shared/ratingValue.ts
new file mode 100644
index 00000000..88173ad1
--- /dev/null
+++ b/ui/src/views/list/shared/ratingValue.ts
@@ -0,0 +1,35 @@
+import { RATE_STAR_POINTS } from '@/components/ui/Rate/starShape'
+
+const DEFAULT_RATING_COUNT = 5
+
+/** Yıldız sayısı editorOptions.count'tan gelir; geçersizse 5. */
+export const resolveRatingCount = (editorOptions?: { count?: unknown }) => {
+ const count = Number(editorOptions?.count)
+ return Number.isInteger(count) && count > 0 ? count : DEFAULT_RATING_COUNT
+}
+
+/** Kolon değeri (sayı ya da metin) -> 0..count arası tam puan; boş değer 0. */
+export const normalizeRatingValue = (value: unknown, count: number) => {
+ const rating = Math.round(Number(value))
+ return Number.isFinite(rating) ? Math.min(Math.max(rating, 0), count) : 0
+}
+
+const STAR_FILLED_COLOR = '#fbbf24'
+const STAR_EMPTY_COLOR = '#d1d5db'
+
+/**
+ * Liste hücresi (Grid, Tree, Card) DOM ile çizilir; React kökü açmamak için yıldızlar
+ * `Rate` bileşeniyle aynı çokgenle SVG olarak yazılır.
+ */
+export const renderRatingCell = (cellElement: HTMLElement, value: unknown, count: number) => {
+ const rating = normalizeRatingValue(value, count)
+ const stars = Array.from({ length: count }, (_, index) => {
+ const color = index < rating ? STAR_FILLED_COLOR : STAR_EMPTY_COLOR
+ const fill = index < rating ? color : 'none'
+ return ``
+ })
+
+ cellElement.style.cssText += 'display:flex;align-items:center;gap:2px;'
+ cellElement.innerHTML = stars.join('')
+ cellElement.title = rating ? `${rating} / ${count}` : ''
+}
diff --git a/ui/src/views/list/shared/workflowRows.tsx b/ui/src/views/list/shared/workflowRows.tsx
index 8f026e3a..4395d7c4 100644
--- a/ui/src/views/list/shared/workflowRows.tsx
+++ b/ui/src/views/list/shared/workflowRows.tsx
@@ -26,7 +26,8 @@ const showWorkflowMessages = (messages: string[] = []) => {
/**
* Liste formunda workflow onay alanı tanımlıysa eklenen kayıt için
- * workflow'u başlatır ve görünümü yeniler.
+ * workflow'u başlatır ve görünümü yeniler. `approvalIsManualStart` açıksa kayıt
+ * taslak kalır; alt form satırları girildikten sonra akış başlat düğmesiyle başlar.
*/
export const startWorkflowForInsertedRow = (
event: any,
@@ -34,7 +35,8 @@ export const startWorkflowForInsertedRow = (
listFormCode: string,
refresh: () => void,
) => {
- if (!gridDto.gridOptions.workflowDto?.approvalStatusFieldName) return
+ const workflowDto = gridDto.gridOptions.workflowDto
+ if (!workflowDto?.approvalStatusFieldName || workflowDto.approvalIsManualStart) return
const insertedKey = getPersistedInsertedKey(event, gridDto.gridOptions.keyFieldName)
if (insertedKey === undefined) return
diff --git a/ui/src/views/list/useListFormColumns.ts b/ui/src/views/list/useListFormColumns.ts
index e7eeb8d5..9cc26c99 100644
--- a/ui/src/views/list/useListFormColumns.ts
+++ b/ui/src/views/list/useListFormColumns.ts
@@ -32,6 +32,7 @@ import {
showImageHoverPreview,
thumbnailSource,
} from '../form/editors/imageHoverPreview'
+import { renderRatingCell, resolveRatingCount } from './shared/ratingValue'
import { translateWorkflowLabel } from '@/utils/workflow/workflowHelpers'
import { findLookupColor, getLookupItemColor, tagBadgeColors } from '../shared/lookupColor'
import { applyPhoneColumnFormat, needsPhoneCountries } from './shared/columnFormatters'
@@ -158,6 +159,14 @@ const cellTemplateImage = (
})
}
+const cellTemplateRating = (
+ cellElement: HTMLElement,
+ cellInfo: DataGridTypes.ColumnCellTemplateData,
+) => {
+ const column = cellInfo?.column as GridColumnData | undefined
+ renderRatingCell(cellElement, cellInfo?.value, resolveRatingCount(column?.extras?.editorOptions))
+}
+
function calculateFilterExpressionMultiValue(
this: DataGridTypes.Column,
filterValue: any,
@@ -810,6 +819,16 @@ const useListFormColumns = ({
}
// #endregion image upload editor
+ if (
+ colData.editorType2 === PlatformEditorTypes.dxRating &&
+ !colData.lookupDto?.dataSourceType
+ ) {
+ column.extras = { multiValue: false, editorOptions }
+ column.editCellTemplate = 'cellEditRating'
+ column.cellTemplate = cellTemplateRating as any
+ column.alignment = 'left'
+ }
+
if (colData.validationRuleDto) {
// for server side validation : https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/DataValidation/jQuery/Light/
column.validationRules = colData.validationRuleDto as ValidationRule[]