@ayasofyazilim/ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__mocks__/canvas.ts +8 -0
- package/components.json +21 -0
- package/eslint.config.js +4 -0
- package/jest-environment.js +37 -0
- package/jest.config.ts +47 -0
- package/jest.setup.ts +69 -0
- package/package.json +124 -0
- package/postcss.config.mjs +6 -0
- package/src/aria/index.tsx +1 -0
- package/src/aria/number-field.tsx +41 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/accordion.tsx +66 -0
- package/src/components/alert-dialog.tsx +157 -0
- package/src/components/alert.tsx +70 -0
- package/src/components/aspect-ratio.tsx +11 -0
- package/src/components/avatar.tsx +53 -0
- package/src/components/badge.tsx +67 -0
- package/src/components/breadcrumb.tsx +109 -0
- package/src/components/button-group.tsx +83 -0
- package/src/components/button.tsx +68 -0
- package/src/components/calendar.tsx +219 -0
- package/src/components/card.tsx +92 -0
- package/src/components/carousel.tsx +241 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +32 -0
- package/src/components/collapsible.tsx +33 -0
- package/src/components/command.tsx +184 -0
- package/src/components/context-menu.tsx +252 -0
- package/src/components/dialog.tsx +144 -0
- package/src/components/drawer.tsx +135 -0
- package/src/components/dropdown-menu.tsx +258 -0
- package/src/components/empty.tsx +100 -0
- package/src/components/field.tsx +248 -0
- package/src/components/form.tsx +169 -0
- package/src/components/hover-card.tsx +44 -0
- package/src/components/input-group.tsx +170 -0
- package/src/components/input-otp.tsx +77 -0
- package/src/components/input.tsx +21 -0
- package/src/components/item.tsx +193 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.tsx +24 -0
- package/src/components/menubar.tsx +276 -0
- package/src/components/navigation-menu.tsx +168 -0
- package/src/components/pagination.tsx +130 -0
- package/src/components/popover.tsx +88 -0
- package/src/components/progress.tsx +31 -0
- package/src/components/radio-group.tsx +45 -0
- package/src/components/resizable.tsx +56 -0
- package/src/components/scroll-area.tsx +58 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +28 -0
- package/src/components/sheet.tsx +140 -0
- package/src/components/sidebar.tsx +862 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +63 -0
- package/src/components/sonner.tsx +40 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/stepper.tsx +291 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +133 -0
- package/src/components/tabs.tsx +66 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.tsx +83 -0
- package/src/components/toggle.tsx +47 -0
- package/src/components/tooltip.tsx +66 -0
- package/src/custom/action-button.tsx +48 -0
- package/src/custom/async-select.tsx +287 -0
- package/src/custom/awesome-not-found.tsx +116 -0
- package/src/custom/charts/area-chart.tsx +147 -0
- package/src/custom/charts/bar-chart.tsx +233 -0
- package/src/custom/charts/chart-card.tsx +103 -0
- package/src/custom/charts/index.tsx +16 -0
- package/src/custom/charts/pie-chart.tsx +168 -0
- package/src/custom/charts/radar-chart.tsx +126 -0
- package/src/custom/checkbox-tree.tsx +100 -0
- package/src/custom/combobox.tsx +296 -0
- package/src/custom/confirm-dialog.tsx +102 -0
- package/src/custom/country-selector.tsx +204 -0
- package/src/custom/date-picker/calendar-rac.tsx +109 -0
- package/src/custom/date-picker/datefield-rac.tsx +84 -0
- package/src/custom/date-picker/index.tsx +273 -0
- package/src/custom/date-picker/types/index.ts +4 -0
- package/src/custom/date-picker/utils/index.ts +42 -0
- package/src/custom/date-picker-old.tsx +50 -0
- package/src/custom/date-tooltip.tsx +98 -0
- package/src/custom/document-scanner/consts.ts +5 -0
- package/src/custom/document-scanner/corner-adjustment/action-buttons.tsx +33 -0
- package/src/custom/document-scanner/corner-adjustment/corner-handle.tsx +43 -0
- package/src/custom/document-scanner/corner-adjustment/hooks/use-corner-drag.ts +85 -0
- package/src/custom/document-scanner/corner-adjustment/index.tsx +125 -0
- package/src/custom/document-scanner/corner-adjustment/types.ts +53 -0
- package/src/custom/document-scanner/corner-adjustment/utils/clip-path.ts +22 -0
- package/src/custom/document-scanner/corner-adjustment/zoom-magnifier.tsx +115 -0
- package/src/custom/document-scanner/hooks/use-document-capture.ts +81 -0
- package/src/custom/document-scanner/hooks/use-document-scanner.ts +80 -0
- package/src/custom/document-scanner/hooks/use-perspective-crop.ts +38 -0
- package/src/custom/document-scanner/index.tsx +255 -0
- package/src/custom/document-scanner/lib.ts +407 -0
- package/src/custom/document-scanner/types.ts +205 -0
- package/src/custom/document-scanner/utils/perspective-correction.ts +139 -0
- package/src/custom/document-viewer/controllers.tsx +98 -0
- package/src/custom/document-viewer/index.tsx +43 -0
- package/src/custom/document-viewer/renderers/image.tsx +37 -0
- package/src/custom/document-viewer/renderers/index.tsx +2 -0
- package/src/custom/document-viewer/renderers/pdf.tsx +105 -0
- package/src/custom/email-input/domains.json +159 -0
- package/src/custom/email-input/email.tsx +229 -0
- package/src/custom/email-input/index.tsx +4 -0
- package/src/custom/email-input/types.ts +104 -0
- package/src/custom/file-uploader.tsx +541 -0
- package/src/custom/filter-component/fields/async-select.tsx +33 -0
- package/src/custom/filter-component/fields/date.tsx +60 -0
- package/src/custom/filter-component/fields/multi-select.tsx +30 -0
- package/src/custom/filter-component/index.tsx +217 -0
- package/src/custom/image-canvas.tsx +260 -0
- package/src/custom/json-editor.tsx +22 -0
- package/src/custom/master-data-grid/components/dialogs/column-settings-dialog.tsx +100 -0
- package/src/custom/master-data-grid/components/dialogs/index.ts +1 -0
- package/src/custom/master-data-grid/components/filters/client-filter.tsx +368 -0
- package/src/custom/master-data-grid/components/filters/filter-input.tsx +256 -0
- package/src/custom/master-data-grid/components/filters/index.ts +3 -0
- package/src/custom/master-data-grid/components/filters/inline-column-filter.tsx +233 -0
- package/src/custom/master-data-grid/components/filters/multi-filter-dialog.tsx +90 -0
- package/src/custom/master-data-grid/components/filters/server-filter.tsx +255 -0
- package/src/custom/master-data-grid/components/master-data-grid.tsx +472 -0
- package/src/custom/master-data-grid/components/pagination/index.ts +1 -0
- package/src/custom/master-data-grid/components/pagination/pagination.tsx +178 -0
- package/src/custom/master-data-grid/components/table/cell-renderer.tsx +634 -0
- package/src/custom/master-data-grid/components/table/header-cell.tsx +162 -0
- package/src/custom/master-data-grid/components/table/index.ts +4 -0
- package/src/custom/master-data-grid/components/table/table-body-renderer.tsx +113 -0
- package/src/custom/master-data-grid/components/table/virtual-body.tsx +138 -0
- package/src/custom/master-data-grid/components/toolbar/index.ts +1 -0
- package/src/custom/master-data-grid/components/toolbar/toolbar.tsx +314 -0
- package/src/custom/master-data-grid/hooks/index.ts +3 -0
- package/src/custom/master-data-grid/hooks/use-columns.tsx +332 -0
- package/src/custom/master-data-grid/hooks/use-editing.ts +106 -0
- package/src/custom/master-data-grid/hooks/use-table-state-reducer.ts +157 -0
- package/src/custom/master-data-grid/hooks/use-table-state.ts +31 -0
- package/src/custom/master-data-grid/index.ts +16 -0
- package/src/custom/master-data-grid/types.ts +466 -0
- package/src/custom/master-data-grid/utils/column-generator.tsx +306 -0
- package/src/custom/master-data-grid/utils/export-utils.ts +67 -0
- package/src/custom/master-data-grid/utils/filter-fns.ts +290 -0
- package/src/custom/master-data-grid/utils/index.ts +8 -0
- package/src/custom/master-data-grid/utils/pinning-utils.ts +88 -0
- package/src/custom/master-data-grid/utils/translation-utils.ts +42 -0
- package/src/custom/multi-select.tsx +432 -0
- package/src/custom/password-input.tsx +194 -0
- package/src/custom/phone-input.tsx +172 -0
- package/src/custom/schema-form/custom/index.tsx +1 -0
- package/src/custom/schema-form/custom/label.tsx +53 -0
- package/src/custom/schema-form/fields/base-input-field.tsx +82 -0
- package/src/custom/schema-form/fields/field.tsx +67 -0
- package/src/custom/schema-form/fields/index.tsx +5 -0
- package/src/custom/schema-form/fields/object.tsx +12 -0
- package/src/custom/schema-form/fields/table-array/array-field-item.tsx +90 -0
- package/src/custom/schema-form/fields/table-array/array-field-template.tsx +115 -0
- package/src/custom/schema-form/index.tsx +259 -0
- package/src/custom/schema-form/templates/description.tsx +20 -0
- package/src/custom/schema-form/templates/index.tsx +2 -0
- package/src/custom/schema-form/templates/submit.tsx +32 -0
- package/src/custom/schema-form/types.ts +64 -0
- package/src/custom/schema-form/utils/index.ts +4 -0
- package/src/custom/schema-form/utils/schema-dependency.ts +655 -0
- package/src/custom/schema-form/utils/schemas.ts +289 -0
- package/src/custom/schema-form/utils/validation.ts +23 -0
- package/src/custom/schema-form/widgets/boolean.tsx +77 -0
- package/src/custom/schema-form/widgets/combobox.tsx +274 -0
- package/src/custom/schema-form/widgets/date.tsx +59 -0
- package/src/custom/schema-form/widgets/email.tsx +34 -0
- package/src/custom/schema-form/widgets/index.tsx +10 -0
- package/src/custom/schema-form/widgets/password.tsx +40 -0
- package/src/custom/schema-form/widgets/phone.tsx +40 -0
- package/src/custom/schema-form/widgets/select.tsx +105 -0
- package/src/custom/schema-form/widgets/selectable.tsx +25 -0
- package/src/custom/schema-form/widgets/string-array.tsx +296 -0
- package/src/custom/schema-form/widgets/url.tsx +56 -0
- package/src/custom/section-layout-v2.tsx +212 -0
- package/src/custom/select-tabs.tsx +109 -0
- package/src/custom/selectable.tsx +316 -0
- package/src/custom/stepper.tsx +236 -0
- package/src/custom/tab-layout.tsx +213 -0
- package/src/custom/tanstack-table/fields/index.tsx +12 -0
- package/src/custom/tanstack-table/fields/tanstack-table-action-dialogs.tsx +89 -0
- package/src/custom/tanstack-table/fields/tanstack-table-column-header.tsx +66 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-date.tsx +180 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-faceted.tsx +158 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-text.tsx +76 -0
- package/src/custom/tanstack-table/fields/tanstack-table-pagination.tsx +136 -0
- package/src/custom/tanstack-table/fields/tanstack-table-plain-table.tsx +142 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions-confirmation.tsx +77 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions-custom-dialog.tsx +87 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions.tsx +151 -0
- package/src/custom/tanstack-table/fields/tanstack-table-table-actions-custom-dialog.tsx +88 -0
- package/src/custom/tanstack-table/fields/tanstack-table-table-actions-schemaform-dialog.tsx +47 -0
- package/src/custom/tanstack-table/fields/tanstack-table-toolbar.tsx +143 -0
- package/src/custom/tanstack-table/fields/tanstack-table-view-options.tsx +171 -0
- package/src/custom/tanstack-table/index.tsx +244 -0
- package/src/custom/tanstack-table/types/index.ts +328 -0
- package/src/custom/tanstack-table/utils/cell-with-actions.tsx +21 -0
- package/src/custom/tanstack-table/utils/column-names.ts +26 -0
- package/src/custom/tanstack-table/utils/columns-by-row-data.tsx +312 -0
- package/src/custom/tanstack-table/utils/editable-columns-by-row-data.tsx +219 -0
- package/src/custom/tanstack-table/utils/faceted-boolean-options.tsx +22 -0
- package/src/custom/tanstack-table/utils/index.tsx +10 -0
- package/src/custom/tanstack-table/utils/pinning-styles.ts +57 -0
- package/src/custom/tanstack-table/utils/table.tsx +83 -0
- package/src/custom/tanstack-table/utils/test-conditions.ts +17 -0
- package/src/custom/timeline.tsx +208 -0
- package/src/custom/tree.tsx +200 -0
- package/src/custom/tscanify/browser.ts +66 -0
- package/src/custom/tscanify/index.ts +51 -0
- package/src/custom/tscanify/tscanify-browser.ts +522 -0
- package/src/custom/tscanify/tscanify.ts +262 -0
- package/src/custom/tscanify/types.ts +22 -0
- package/src/custom/webcam.tsx +737 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-callback-ref.ts +27 -0
- package/src/hooks/use-controllable-state.ts +67 -0
- package/src/hooks/use-debounce.ts +19 -0
- package/src/hooks/use-is-visible.ts +23 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/use-on-window-resize.ts +15 -0
- package/src/hooks/use-scroll.tsx +22 -0
- package/src/lib/utils.ts +61 -0
- package/src/lib/zod.ts +2 -0
- package/src/styles/core.css +57 -0
- package/src/styles/globals.css +130 -0
- package/src/test/email-input.test.tsx +217 -0
- package/src/test/password-input.test.tsx +92 -0
- package/src/test/select-tabs.test.tsx +302 -0
- package/src/test/selectable.test.tsx +1093 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lint.json +8 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FormValidation,
|
|
3
|
+
GenericObjectType,
|
|
4
|
+
RJSFValidationError,
|
|
5
|
+
} from "@rjsf/utils";
|
|
6
|
+
|
|
7
|
+
interface JSONSchema {
|
|
8
|
+
type?: string;
|
|
9
|
+
properties?: Record<string, JSONSchema>;
|
|
10
|
+
required?: string[];
|
|
11
|
+
dependencies?: Record<string, JSONSchemaDependency>;
|
|
12
|
+
items?: JSONSchema | JSONSchema[];
|
|
13
|
+
additionalItems?: JSONSchema;
|
|
14
|
+
definitions?: Record<string, JSONSchema>;
|
|
15
|
+
$ref?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
enum?: unknown[];
|
|
19
|
+
default?: unknown;
|
|
20
|
+
oneOf?: JSONSchema[];
|
|
21
|
+
nullable?: boolean;
|
|
22
|
+
format?: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JSONSchemaDependency {
|
|
27
|
+
properties?: Record<string, JSONSchema>;
|
|
28
|
+
required?: string[];
|
|
29
|
+
oneOf?: JSONSchema[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FieldDependencyRule {
|
|
33
|
+
when: (value: unknown) => boolean;
|
|
34
|
+
targets: string[];
|
|
35
|
+
fieldCondition?: JSONSchema;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface FieldDependencies {
|
|
39
|
+
HIDES?: FieldDependencyRule[];
|
|
40
|
+
REQUIRES?: FieldDependencyRule[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type DependencyConfig = Record<string, FieldDependencies>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Helper to safely get a nested property in a JSON Schema using dot notation path.
|
|
47
|
+
*/
|
|
48
|
+
function getNestedProperty(
|
|
49
|
+
obj: JSONSchema,
|
|
50
|
+
path: string
|
|
51
|
+
): JSONSchema | undefined {
|
|
52
|
+
const parts = path.split(".");
|
|
53
|
+
let current: JSONSchema | undefined = obj;
|
|
54
|
+
|
|
55
|
+
for (const part of parts) {
|
|
56
|
+
if (!current?.properties?.[part]) return undefined;
|
|
57
|
+
current = current.properties[part];
|
|
58
|
+
}
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all possible values for a controlling field to create conditional branches.
|
|
64
|
+
*/
|
|
65
|
+
function getFieldValues(schema: JSONSchema, fieldPath: string): unknown[] {
|
|
66
|
+
const field = getNestedProperty(schema, fieldPath);
|
|
67
|
+
if (!field) return [];
|
|
68
|
+
if (field.enum) return field.enum;
|
|
69
|
+
if (field.type === "boolean") return [true, false];
|
|
70
|
+
if (field.type === "integer" || field.type === "number")
|
|
71
|
+
return ["__NUMERIC_RANGE__"];
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Main function to apply HIDES and REQUIRES field dependencies on a JSON Schema.
|
|
77
|
+
*/
|
|
78
|
+
function applyFieldDependencies(
|
|
79
|
+
originalSchema: GenericObjectType,
|
|
80
|
+
dependencies: DependencyConfig
|
|
81
|
+
): JSONSchema {
|
|
82
|
+
const schema: JSONSchema = JSON.parse(JSON.stringify(originalSchema));
|
|
83
|
+
if (!schema.properties) return schema;
|
|
84
|
+
|
|
85
|
+
for (const [fieldPath, fieldDeps] of Object.entries(dependencies)) {
|
|
86
|
+
const fieldValues = getFieldValues(schema, fieldPath);
|
|
87
|
+
if (fieldValues.length > 0) {
|
|
88
|
+
const parentPath = fieldPath.split(".").slice(0, -1).join(".");
|
|
89
|
+
const parentSchema = parentPath
|
|
90
|
+
? getNestedProperty(schema, parentPath)
|
|
91
|
+
: schema;
|
|
92
|
+
const fieldName = fieldPath.split(".").pop()!;
|
|
93
|
+
|
|
94
|
+
if (parentSchema) {
|
|
95
|
+
// Collect all HIDES targets globally, to remove from root properties
|
|
96
|
+
const allHidesTargets = new Set<string>();
|
|
97
|
+
if (fieldDeps.HIDES) {
|
|
98
|
+
for (const rule of fieldDeps.HIDES) {
|
|
99
|
+
rule.targets.forEach((t) => allHidesTargets.add(t));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Remove all HIDES targets from root properties immediately
|
|
104
|
+
for (const hiddenTarget of Array.from(allHidesTargets)) {
|
|
105
|
+
if (schema.properties[hiddenTarget]) {
|
|
106
|
+
delete schema.properties[hiddenTarget];
|
|
107
|
+
if (schema.required?.includes(hiddenTarget)) {
|
|
108
|
+
schema.required = schema.required.filter(
|
|
109
|
+
(r) => r !== hiddenTarget
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const conditionalSchemas: JSONSchema[] = [];
|
|
116
|
+
|
|
117
|
+
for (const value of fieldValues) {
|
|
118
|
+
const requiredFields: string[] = [];
|
|
119
|
+
const visibleFields: string[] = [];
|
|
120
|
+
|
|
121
|
+
if (fieldDeps.REQUIRES) {
|
|
122
|
+
for (const rule of fieldDeps.REQUIRES) {
|
|
123
|
+
if (rule.when(value)) {
|
|
124
|
+
requiredFields.push(...rule.targets);
|
|
125
|
+
visibleFields.push(...rule.targets);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (fieldDeps.HIDES) {
|
|
131
|
+
for (const rule of fieldDeps.HIDES) {
|
|
132
|
+
if (rule.when(value)) {
|
|
133
|
+
// Hidden fields: do NOT add to visibleFields
|
|
134
|
+
// But fields not hidden should be visible
|
|
135
|
+
// So we skip these targets here
|
|
136
|
+
} else {
|
|
137
|
+
// For other values where fields are not hidden, add them visible
|
|
138
|
+
visibleFields.push(...rule.targets);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Also add controlling field itself visible
|
|
144
|
+
visibleFields.push(fieldName);
|
|
145
|
+
|
|
146
|
+
// Deduplicate visibleFields
|
|
147
|
+
const visibleFieldsUnique = Array.from(new Set(visibleFields));
|
|
148
|
+
|
|
149
|
+
// Build properties for conditional schema
|
|
150
|
+
const conditionalProperties: Record<string, JSONSchema> = {};
|
|
151
|
+
|
|
152
|
+
visibleFieldsUnique.forEach((targetField) => {
|
|
153
|
+
// If targetField === fieldName, add enum with current value
|
|
154
|
+
if (targetField === fieldName) {
|
|
155
|
+
conditionalProperties[targetField] = {
|
|
156
|
+
enum: [value],
|
|
157
|
+
};
|
|
158
|
+
} else {
|
|
159
|
+
// Else get original schema for that target field
|
|
160
|
+
const originalFieldSchema = getNestedProperty(
|
|
161
|
+
originalSchema,
|
|
162
|
+
targetField
|
|
163
|
+
);
|
|
164
|
+
if (originalFieldSchema) {
|
|
165
|
+
conditionalProperties[targetField] = { ...originalFieldSchema };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const conditionalSchema: JSONSchema = {
|
|
171
|
+
properties: conditionalProperties,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (requiredFields.length > 0) {
|
|
175
|
+
conditionalSchema.required = requiredFields.filter((r) =>
|
|
176
|
+
visibleFieldsUnique.includes(r)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
conditionalSchemas.push(conditionalSchema);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!parentSchema.dependencies) parentSchema.dependencies = {};
|
|
184
|
+
parentSchema.dependencies[fieldName] = { oneOf: conditionalSchemas };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return schema;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getFormDataValue(
|
|
193
|
+
formData: Record<string, unknown>,
|
|
194
|
+
path: string
|
|
195
|
+
): unknown {
|
|
196
|
+
const parts = path.split(".");
|
|
197
|
+
let current: unknown = formData;
|
|
198
|
+
for (const part of parts) {
|
|
199
|
+
if (current === null || current === undefined) return undefined;
|
|
200
|
+
current = (current as Record<string, unknown>)[part];
|
|
201
|
+
}
|
|
202
|
+
return current;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function deleteFormDataProperty(
|
|
206
|
+
formData: Record<string, unknown>,
|
|
207
|
+
path: string
|
|
208
|
+
): void {
|
|
209
|
+
const parts = path.split(".");
|
|
210
|
+
if (parts.length === 1) {
|
|
211
|
+
delete formData[path];
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const parentPath = parts.slice(0, -1);
|
|
215
|
+
const fieldName = parts.at(-1);
|
|
216
|
+
if (!fieldName) return;
|
|
217
|
+
let current: unknown = formData;
|
|
218
|
+
for (const part of parentPath) {
|
|
219
|
+
if (current === null || current === undefined) return;
|
|
220
|
+
current = (current as Record<string, unknown>)[part];
|
|
221
|
+
}
|
|
222
|
+
if (current && typeof current === "object") {
|
|
223
|
+
delete (current as Record<string, unknown>)[fieldName];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function cleanHiddenFieldsFromFormData<T extends Record<string, unknown>>(
|
|
228
|
+
formData: T,
|
|
229
|
+
dependencies: DependencyConfig
|
|
230
|
+
): T {
|
|
231
|
+
const cleanedData = JSON.parse(JSON.stringify(formData)) as T;
|
|
232
|
+
|
|
233
|
+
for (const [fieldPath, fieldDeps] of Object.entries(dependencies)) {
|
|
234
|
+
if (!fieldDeps.HIDES) continue;
|
|
235
|
+
|
|
236
|
+
const controllingValue = getFormDataValue(cleanedData, fieldPath);
|
|
237
|
+
|
|
238
|
+
for (const rule of fieldDeps.HIDES) {
|
|
239
|
+
if (rule.when(controllingValue)) {
|
|
240
|
+
for (const target of rule.targets) {
|
|
241
|
+
deleteFormDataProperty(cleanedData, target);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return cleanedData;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { applyFieldDependencies, cleanHiddenFieldsFromFormData };
|
|
251
|
+
export type {
|
|
252
|
+
JSONSchema,
|
|
253
|
+
DependencyConfig,
|
|
254
|
+
FieldDependencies,
|
|
255
|
+
FieldDependencyRule,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// RUNTIME CONDITIONAL REQUIREMENTS
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// For fields that need runtime validation (e.g., UUID comparisons, complex conditions)
|
|
262
|
+
// Use this when JSON Schema dependencies can't express the condition.
|
|
263
|
+
|
|
264
|
+
const EMPTY_GUID = "00000000-0000-0000-0000-000000000000";
|
|
265
|
+
|
|
266
|
+
type EmptyCheckFn = (value: unknown) => boolean;
|
|
267
|
+
|
|
268
|
+
interface ConditionalFieldConfig {
|
|
269
|
+
/** Fields that trigger the requirement check when filled */
|
|
270
|
+
triggerFields: string[];
|
|
271
|
+
/** Fields that become required when trigger condition is met */
|
|
272
|
+
requiredFields: string[];
|
|
273
|
+
/** Optional custom empty check per field (default: checks empty string, null, undefined, EMPTY_GUID) */
|
|
274
|
+
emptyChecks?: Record<string, EmptyCheckFn>;
|
|
275
|
+
/** Error message for required fields (default: "Required") */
|
|
276
|
+
errorMessage?: string;
|
|
277
|
+
/** Minimum number of trigger fields that must be filled to activate requirements (default: 2) */
|
|
278
|
+
minFilledCount?: number;
|
|
279
|
+
/** If true, remove this object from form data on submit when requirements are not triggered */
|
|
280
|
+
removeFromSubmit?: boolean;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface RuntimeDependencyConfig {
|
|
284
|
+
/** Path to the nested object (e.g., "address", "telephone") */
|
|
285
|
+
[objectPath: string]: ConditionalFieldConfig;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Default check for whether a value is considered "empty"
|
|
290
|
+
*/
|
|
291
|
+
function isValueEmpty(value: unknown): boolean {
|
|
292
|
+
if (value === null || value === undefined) return true;
|
|
293
|
+
if (typeof value === "string") {
|
|
294
|
+
return value.trim() === "" || value === EMPTY_GUID;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get a nested value from an object using dot notation path
|
|
301
|
+
*/
|
|
302
|
+
function getNestedValue(obj: GenericObjectType, path: string): unknown {
|
|
303
|
+
const parts = path.split(".");
|
|
304
|
+
let current: unknown = obj;
|
|
305
|
+
for (const part of parts) {
|
|
306
|
+
if (current === null || current === undefined) return undefined;
|
|
307
|
+
current = (current as GenericObjectType)[part];
|
|
308
|
+
}
|
|
309
|
+
return current;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Creates a modified schema where specified nested object fields are not required.
|
|
314
|
+
* The required fields will be validated at runtime instead.
|
|
315
|
+
*/
|
|
316
|
+
function applyRuntimeDependencies<T extends GenericObjectType>(
|
|
317
|
+
originalSchema: T,
|
|
318
|
+
config: RuntimeDependencyConfig
|
|
319
|
+
): T {
|
|
320
|
+
const schema = JSON.parse(JSON.stringify(originalSchema)) as T;
|
|
321
|
+
const schemaAsJson = schema as unknown as JSONSchema;
|
|
322
|
+
|
|
323
|
+
for (const [objectPath, fieldConfig] of Object.entries(config)) {
|
|
324
|
+
const nestedSchema = getNestedProperty(schemaAsJson, objectPath);
|
|
325
|
+
if (nestedSchema) {
|
|
326
|
+
// Remove all conditionally required fields from the nested object's required array
|
|
327
|
+
const fieldsToMakeOptional = [
|
|
328
|
+
...fieldConfig.triggerFields,
|
|
329
|
+
...fieldConfig.requiredFields,
|
|
330
|
+
];
|
|
331
|
+
if (nestedSchema.required) {
|
|
332
|
+
nestedSchema.required = nestedSchema.required.filter(
|
|
333
|
+
(field) => !fieldsToMakeOptional.includes(field)
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Also remove the nested object itself from root required if all its fields are optional
|
|
340
|
+
if (schemaAsJson.required) {
|
|
341
|
+
for (const objectPath of Object.keys(config)) {
|
|
342
|
+
const topLevelField = objectPath.split(".")[0];
|
|
343
|
+
if (topLevelField && schemaAsJson.required.includes(topLevelField)) {
|
|
344
|
+
schemaAsJson.required = schemaAsJson.required.filter(
|
|
345
|
+
(field: string) => field !== topLevelField
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return schema;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Creates a customValidate function for runtime conditional requirements.
|
|
356
|
+
*
|
|
357
|
+
* Logic: If ANY trigger field is filled, ALL required fields become required.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* // Address validation: if countryId filled AND any other field filled, all required
|
|
361
|
+
* const validateAddress = createRuntimeValidator({
|
|
362
|
+
* address: {
|
|
363
|
+
* triggerFields: ["countryId", "adminAreaLevel1Id", "adminAreaLevel2Id", "addressLine"],
|
|
364
|
+
* requiredFields: ["countryId", "adminAreaLevel1Id", "adminAreaLevel2Id", "addressLine", "type"],
|
|
365
|
+
* }
|
|
366
|
+
* });
|
|
367
|
+
*/
|
|
368
|
+
function createRuntimeValidator<TData extends GenericObjectType>(
|
|
369
|
+
config: RuntimeDependencyConfig
|
|
370
|
+
) {
|
|
371
|
+
return function validate(
|
|
372
|
+
formData: TData | undefined,
|
|
373
|
+
errors: FormValidation<TData>
|
|
374
|
+
): FormValidation<TData> {
|
|
375
|
+
if (!formData) return errors;
|
|
376
|
+
|
|
377
|
+
for (const [objectPath, fieldConfig] of Object.entries(config)) {
|
|
378
|
+
const nestedObject = getNestedValue(formData, objectPath) as
|
|
379
|
+
| GenericObjectType
|
|
380
|
+
| undefined;
|
|
381
|
+
if (!nestedObject) continue;
|
|
382
|
+
|
|
383
|
+
const errorMessage = fieldConfig.errorMessage || "Required";
|
|
384
|
+
|
|
385
|
+
// Check if any trigger field is filled
|
|
386
|
+
const filledTriggerFields = fieldConfig.triggerFields.filter((field) => {
|
|
387
|
+
const value = nestedObject[field];
|
|
388
|
+
const emptyCheck = fieldConfig.emptyChecks?.[field] || isValueEmpty;
|
|
389
|
+
return !emptyCheck(value);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// If at least 2 trigger fields are filled (not just one), all required fields become required
|
|
393
|
+
// This handles the "only countryId filled = optional" case
|
|
394
|
+
if (filledTriggerFields.length >= 2) {
|
|
395
|
+
const nestedErrors = getNestedValue(
|
|
396
|
+
errors as unknown as GenericObjectType,
|
|
397
|
+
objectPath
|
|
398
|
+
) as GenericObjectType | undefined;
|
|
399
|
+
|
|
400
|
+
for (const requiredField of fieldConfig.requiredFields) {
|
|
401
|
+
const value = nestedObject[requiredField];
|
|
402
|
+
const emptyCheck =
|
|
403
|
+
fieldConfig.emptyChecks?.[requiredField] || isValueEmpty;
|
|
404
|
+
|
|
405
|
+
if (emptyCheck(value)) {
|
|
406
|
+
const fieldError = nestedErrors?.[requiredField] as
|
|
407
|
+
| { addError?: (msg: string) => void }
|
|
408
|
+
| undefined;
|
|
409
|
+
fieldError?.addError?.(errorMessage);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return errors;
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Convenience function that applies both schema modifications and returns a validator.
|
|
421
|
+
* Use this for the complete solution.
|
|
422
|
+
*
|
|
423
|
+
* @typeParam TSchema - The schema object type
|
|
424
|
+
* @typeParam TData - The form data type (optional, defaults to GenericObjectType)
|
|
425
|
+
*/
|
|
426
|
+
function createConditionalRequirements<
|
|
427
|
+
TSchema extends GenericObjectType,
|
|
428
|
+
TData extends GenericObjectType = GenericObjectType,
|
|
429
|
+
>(
|
|
430
|
+
originalSchema: TSchema,
|
|
431
|
+
config: RuntimeDependencyConfig
|
|
432
|
+
): {
|
|
433
|
+
schema: TSchema;
|
|
434
|
+
validate: (
|
|
435
|
+
formData: TData | undefined,
|
|
436
|
+
errors: FormValidation<TData>
|
|
437
|
+
) => FormValidation<TData>;
|
|
438
|
+
} {
|
|
439
|
+
return {
|
|
440
|
+
schema: applyRuntimeDependencies(originalSchema, config),
|
|
441
|
+
validate: createRuntimeValidator<TData>(config),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Checks if requirements should be active for a given object path.
|
|
447
|
+
* Returns true if 2+ trigger fields are filled.
|
|
448
|
+
*/
|
|
449
|
+
function shouldRequireFields<TData extends GenericObjectType>(
|
|
450
|
+
formData: TData | undefined,
|
|
451
|
+
objectPath: string,
|
|
452
|
+
fieldConfig: ConditionalFieldConfig
|
|
453
|
+
): boolean {
|
|
454
|
+
if (!formData) return false;
|
|
455
|
+
|
|
456
|
+
const nestedObject = getNestedValue(formData, objectPath) as
|
|
457
|
+
| GenericObjectType
|
|
458
|
+
| undefined;
|
|
459
|
+
if (!nestedObject) return false;
|
|
460
|
+
|
|
461
|
+
const filledTriggerFields = fieldConfig.triggerFields.filter((field) => {
|
|
462
|
+
const value = nestedObject[field];
|
|
463
|
+
const emptyCheck = fieldConfig.emptyChecks?.[field] || isValueEmpty;
|
|
464
|
+
return !emptyCheck(value);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const minCount = fieldConfig.minFilledCount ?? 2;
|
|
468
|
+
return filledTriggerFields.length >= minCount;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Creates a schema with required fields dynamically based on current form state.
|
|
473
|
+
* Use this in useMemo to recompute schema when form data changes.
|
|
474
|
+
*
|
|
475
|
+
* @param originalSchema - The original schema
|
|
476
|
+
* @param config - The runtime dependency configuration
|
|
477
|
+
* @param formData - Current form data to determine required state
|
|
478
|
+
* @returns Schema with correct required fields based on form state
|
|
479
|
+
*/
|
|
480
|
+
function createDynamicSchema<
|
|
481
|
+
TSchema extends GenericObjectType,
|
|
482
|
+
TData extends GenericObjectType = GenericObjectType,
|
|
483
|
+
>(
|
|
484
|
+
originalSchema: TSchema,
|
|
485
|
+
config: RuntimeDependencyConfig,
|
|
486
|
+
formData: TData | undefined
|
|
487
|
+
): TSchema {
|
|
488
|
+
const schema = JSON.parse(JSON.stringify(originalSchema)) as TSchema;
|
|
489
|
+
const schemaAsJson = schema as unknown as JSONSchema;
|
|
490
|
+
|
|
491
|
+
for (const [objectPath, fieldConfig] of Object.entries(config)) {
|
|
492
|
+
const nestedSchema = getNestedProperty(schemaAsJson, objectPath);
|
|
493
|
+
if (!nestedSchema) continue;
|
|
494
|
+
|
|
495
|
+
const requirementsActive = shouldRequireFields(
|
|
496
|
+
formData,
|
|
497
|
+
objectPath,
|
|
498
|
+
fieldConfig
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
if (requirementsActive) {
|
|
502
|
+
// Add required fields to the nested object's required array
|
|
503
|
+
const currentRequired = nestedSchema.required || [];
|
|
504
|
+
const newRequired = new Set([
|
|
505
|
+
...currentRequired,
|
|
506
|
+
...fieldConfig.requiredFields,
|
|
507
|
+
]);
|
|
508
|
+
nestedSchema.required = Array.from(newRequired);
|
|
509
|
+
} else {
|
|
510
|
+
// Remove conditionally required fields from required array
|
|
511
|
+
const fieldsToMakeOptional = [
|
|
512
|
+
...fieldConfig.triggerFields,
|
|
513
|
+
...fieldConfig.requiredFields,
|
|
514
|
+
];
|
|
515
|
+
if (nestedSchema.required) {
|
|
516
|
+
nestedSchema.required = nestedSchema.required.filter(
|
|
517
|
+
(field) => !fieldsToMakeOptional.includes(field)
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Handle top-level required for nested objects
|
|
524
|
+
if (schemaAsJson.required) {
|
|
525
|
+
for (const [objectPath, fieldConfig] of Object.entries(config)) {
|
|
526
|
+
const topLevelField = objectPath.split(".")[0];
|
|
527
|
+
if (!topLevelField) continue;
|
|
528
|
+
|
|
529
|
+
const requirementsActive = shouldRequireFields(
|
|
530
|
+
formData,
|
|
531
|
+
objectPath,
|
|
532
|
+
fieldConfig
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
if (requirementsActive) {
|
|
536
|
+
// Add to required if not already present
|
|
537
|
+
if (!schemaAsJson.required.includes(topLevelField)) {
|
|
538
|
+
schemaAsJson.required.push(topLevelField);
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
// Remove from required
|
|
542
|
+
schemaAsJson.required = schemaAsJson.required.filter(
|
|
543
|
+
(field: string) => field !== topLevelField
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return schema;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Creates a transformErrors function that filters out format validation errors
|
|
554
|
+
* when the field value is empty. This prevents errors like "must match email format"
|
|
555
|
+
* when an optional email field is empty.
|
|
556
|
+
*
|
|
557
|
+
* @param formData - Current form data to check for empty values
|
|
558
|
+
* @returns TransformErrors function for SchemaForm
|
|
559
|
+
*/
|
|
560
|
+
function createEmptyValueTransformErrors<TData extends GenericObjectType>(
|
|
561
|
+
formData: TData | undefined
|
|
562
|
+
) {
|
|
563
|
+
return function transformErrors(
|
|
564
|
+
errors: RJSFValidationError[]
|
|
565
|
+
): RJSFValidationError[] {
|
|
566
|
+
return errors.filter((error) => {
|
|
567
|
+
// Only filter format errors
|
|
568
|
+
if (error.name !== "format") return true;
|
|
569
|
+
|
|
570
|
+
// Get the field path from the property (e.g., ".email.emailAddress" -> ["email", "emailAddress"])
|
|
571
|
+
const path = (error.property || "")
|
|
572
|
+
.replace(/^\./, "")
|
|
573
|
+
.split(".")
|
|
574
|
+
.filter(Boolean);
|
|
575
|
+
|
|
576
|
+
if (path.length === 0 || !formData) return true;
|
|
577
|
+
|
|
578
|
+
// Navigate to get the value
|
|
579
|
+
let value: unknown = formData;
|
|
580
|
+
for (const key of path) {
|
|
581
|
+
if (value && typeof value === "object" && key in value) {
|
|
582
|
+
value = (value as Record<string, unknown>)[key];
|
|
583
|
+
} else {
|
|
584
|
+
return true; // Path doesn't exist, keep the error
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// If value is empty, filter out the format error
|
|
589
|
+
return !isValueEmpty(value);
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Cleans form data for submission by removing objects where requirements are not triggered
|
|
596
|
+
* and removeFromSubmit is true.
|
|
597
|
+
*
|
|
598
|
+
* @param formData - The form data to clean
|
|
599
|
+
* @param config - The runtime dependency configuration
|
|
600
|
+
* @returns Cleaned form data ready for submission
|
|
601
|
+
*/
|
|
602
|
+
function cleanFormDataForSubmit<TData extends GenericObjectType>(
|
|
603
|
+
formData: TData,
|
|
604
|
+
config: RuntimeDependencyConfig
|
|
605
|
+
): Partial<TData> {
|
|
606
|
+
const result = { ...formData } as Record<string, unknown>;
|
|
607
|
+
|
|
608
|
+
for (const [objectPath, fieldConfig] of Object.entries(config)) {
|
|
609
|
+
if (!fieldConfig.removeFromSubmit) continue;
|
|
610
|
+
|
|
611
|
+
// Check if requirements are triggered
|
|
612
|
+
const requirementsActive = shouldRequireFields(
|
|
613
|
+
formData,
|
|
614
|
+
objectPath,
|
|
615
|
+
fieldConfig
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
// If requirements are NOT active and removeFromSubmit is true, remove the object
|
|
619
|
+
if (!requirementsActive) {
|
|
620
|
+
// Handle nested paths (e.g., "contact.address")
|
|
621
|
+
const pathParts = objectPath.split(".");
|
|
622
|
+
if (pathParts.length === 1) {
|
|
623
|
+
delete result[objectPath];
|
|
624
|
+
} else {
|
|
625
|
+
// Navigate to parent and delete the leaf
|
|
626
|
+
let current: Record<string, unknown> = result;
|
|
627
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
628
|
+
const part = pathParts[i] as string;
|
|
629
|
+
if (current[part] && typeof current[part] === "object") {
|
|
630
|
+
current = current[part] as Record<string, unknown>;
|
|
631
|
+
} else {
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const leafKey = pathParts[pathParts.length - 1] as string;
|
|
636
|
+
delete current[leafKey];
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return result as Partial<TData>;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export {
|
|
645
|
+
applyRuntimeDependencies,
|
|
646
|
+
createRuntimeValidator,
|
|
647
|
+
createConditionalRequirements,
|
|
648
|
+
createDynamicSchema,
|
|
649
|
+
createEmptyValueTransformErrors,
|
|
650
|
+
cleanFormDataForSubmit,
|
|
651
|
+
shouldRequireFields,
|
|
652
|
+
isValueEmpty,
|
|
653
|
+
EMPTY_GUID,
|
|
654
|
+
};
|
|
655
|
+
export type { RuntimeDependencyConfig, ConditionalFieldConfig };
|