@object-ui/core 3.3.0 → 3.3.1
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/CHANGELOG.md +6 -0
- package/README.md +20 -1
- package/dist/actions/ActionRunner.d.ts +9 -0
- package/dist/actions/ActionRunner.js +41 -4
- package/dist/adapters/ValueDataSource.js +3 -1
- package/dist/utils/filter-converter.js +25 -5
- package/package.json +32 -8
- package/.turbo/turbo-build.log +0 -4
- package/src/__benchmarks__/core.bench.ts +0 -64
- package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
- package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
- package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
- package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
- package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
- package/src/actions/ActionEngine.ts +0 -268
- package/src/actions/ActionRunner.ts +0 -717
- package/src/actions/TransactionManager.ts +0 -521
- package/src/actions/UndoManager.ts +0 -215
- package/src/actions/__tests__/ActionEngine.test.ts +0 -206
- package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
- package/src/actions/__tests__/ActionRunner.test.ts +0 -711
- package/src/actions/__tests__/TransactionManager.test.ts +0 -447
- package/src/actions/__tests__/UndoManager.test.ts +0 -320
- package/src/actions/index.ts +0 -12
- package/src/adapters/ApiDataSource.ts +0 -376
- package/src/adapters/README.md +0 -180
- package/src/adapters/ValueDataSource.ts +0 -459
- package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
- package/src/adapters/__tests__/ValueDataSource.test.ts +0 -571
- package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
- package/src/adapters/index.ts +0 -15
- package/src/adapters/resolveDataSource.ts +0 -79
- package/src/builder/__tests__/schema-builder.test.ts +0 -235
- package/src/builder/schema-builder.ts +0 -584
- package/src/data-scope/DataScopeManager.ts +0 -269
- package/src/data-scope/ViewDataProvider.ts +0 -282
- package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
- package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
- package/src/data-scope/index.ts +0 -24
- package/src/errors/__tests__/errors.test.ts +0 -292
- package/src/errors/index.ts +0 -269
- package/src/evaluator/ExpressionCache.ts +0 -206
- package/src/evaluator/ExpressionContext.ts +0 -118
- package/src/evaluator/ExpressionEvaluator.ts +0 -315
- package/src/evaluator/FormulaFunctions.ts +0 -398
- package/src/evaluator/SafeExpressionParser.ts +0 -893
- package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
- package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -558
- package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
- package/src/evaluator/index.ts +0 -13
- package/src/index.ts +0 -38
- package/src/protocols/DndProtocol.ts +0 -168
- package/src/protocols/KeyboardProtocol.ts +0 -181
- package/src/protocols/NotificationProtocol.ts +0 -150
- package/src/protocols/ResponsiveProtocol.ts +0 -210
- package/src/protocols/SharingProtocol.ts +0 -185
- package/src/protocols/index.ts +0 -13
- package/src/query/__tests__/query-ast.test.ts +0 -211
- package/src/query/__tests__/window-functions.test.ts +0 -275
- package/src/query/index.ts +0 -7
- package/src/query/query-ast.ts +0 -341
- package/src/registry/PluginScopeImpl.ts +0 -259
- package/src/registry/PluginSystem.ts +0 -206
- package/src/registry/Registry.ts +0 -219
- package/src/registry/WidgetRegistry.ts +0 -316
- package/src/registry/__tests__/PluginSystem.test.ts +0 -309
- package/src/registry/__tests__/Registry.test.ts +0 -293
- package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
- package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
- package/src/theme/ThemeEngine.ts +0 -530
- package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
- package/src/theme/index.ts +0 -24
- package/src/types/index.ts +0 -21
- package/src/utils/__tests__/debug-collector.test.ts +0 -102
- package/src/utils/__tests__/debug.test.ts +0 -134
- package/src/utils/__tests__/expand-fields.test.ts +0 -120
- package/src/utils/__tests__/extract-records.test.ts +0 -50
- package/src/utils/__tests__/filter-converter.test.ts +0 -118
- package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
- package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
- package/src/utils/debug-collector.ts +0 -100
- package/src/utils/debug.ts +0 -148
- package/src/utils/expand-fields.ts +0 -76
- package/src/utils/extract-records.ts +0 -33
- package/src/utils/filter-converter.ts +0 -133
- package/src/utils/merge-views-into-objects.ts +0 -36
- package/src/utils/normalize-quick-filter.ts +0 -78
- package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
- package/src/validation/__tests__/schema-validator.test.ts +0 -118
- package/src/validation/__tests__/validation-engine.test.ts +0 -102
- package/src/validation/index.ts +0 -10
- package/src/validation/schema-validator.ts +0 -344
- package/src/validation/validation-engine.ts +0 -528
- package/src/validation/validators/index.ts +0 -25
- package/src/validation/validators/object-validation-engine.ts +0 -722
- package/tsconfig.json +0 -15
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -79,6 +79,25 @@ This allows the core types and logic to be used in:
|
|
|
79
79
|
|
|
80
80
|
See [full documentation](https://objectui.org/api/core) for detailed API reference.
|
|
81
81
|
|
|
82
|
+
<!-- release-metadata:v3.3.0 -->
|
|
83
|
+
|
|
84
|
+
## Compatibility
|
|
85
|
+
|
|
86
|
+
- **Node.js:** ≥ 18
|
|
87
|
+
- **TypeScript:** ≥ 5.0 (strict mode)
|
|
88
|
+
- **`@objectstack/spec`:** ^3.3.0
|
|
89
|
+
- **`@objectstack/client`:** ^3.3.0
|
|
90
|
+
- **Tailwind CSS:** ≥ 3.4 (for packages with UI)
|
|
91
|
+
|
|
92
|
+
## Links
|
|
93
|
+
|
|
94
|
+
- 📚 [Documentation](https://www.objectui.org/docs/core)
|
|
95
|
+
- 📦 [npm package](https://www.npmjs.com/package/@object-ui/core)
|
|
96
|
+
- 📝 [Changelog](./CHANGELOG.md)
|
|
97
|
+
- 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues)
|
|
98
|
+
- 🤝 [Contributing Guide](https://github.com/objectstack-ai/objectui/blob/main/CONTRIBUTING.md)
|
|
99
|
+
- 🗺️ [Roadmap](https://github.com/objectstack-ai/objectui/blob/main/ROADMAP.md)
|
|
100
|
+
|
|
82
101
|
## License
|
|
83
102
|
|
|
84
|
-
MIT
|
|
103
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -178,6 +178,7 @@ export interface ActionParamDef {
|
|
|
178
178
|
}
|
|
179
179
|
export declare class ActionRunner {
|
|
180
180
|
private handlers;
|
|
181
|
+
private scripts;
|
|
181
182
|
private evaluator;
|
|
182
183
|
private context;
|
|
183
184
|
private confirmHandler;
|
|
@@ -209,6 +210,14 @@ export declare class ActionRunner {
|
|
|
209
210
|
setParamCollectionHandler(handler: ParamCollectionHandler): void;
|
|
210
211
|
registerHandler(actionName: string, handler: ActionHandler): void;
|
|
211
212
|
unregisterHandler(actionName: string): void;
|
|
213
|
+
/**
|
|
214
|
+
* Register a named script handler. When a `script` action's
|
|
215
|
+
* `target`/`execute` matches the registered name, the handler runs
|
|
216
|
+
* instead of the expression evaluator. Lets dashboards/views wire
|
|
217
|
+
* symbolic action names (e.g. 'export_dashboard_pdf') to JS callbacks.
|
|
218
|
+
*/
|
|
219
|
+
registerScript(scriptName: string, handler: ActionHandler): void;
|
|
220
|
+
unregisterScript(scriptName: string): void;
|
|
212
221
|
execute(action: ActionDef): Promise<ActionResult>;
|
|
213
222
|
/**
|
|
214
223
|
* Execute multiple actions in sequence or parallel.
|
|
@@ -22,6 +22,12 @@ export class ActionRunner {
|
|
|
22
22
|
writable: true,
|
|
23
23
|
value: new Map()
|
|
24
24
|
});
|
|
25
|
+
Object.defineProperty(this, "scripts", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: new Map()
|
|
30
|
+
});
|
|
25
31
|
Object.defineProperty(this, "evaluator", {
|
|
26
32
|
enumerable: true,
|
|
27
33
|
configurable: true,
|
|
@@ -110,6 +116,18 @@ export class ActionRunner {
|
|
|
110
116
|
unregisterHandler(actionName) {
|
|
111
117
|
this.handlers.delete(actionName);
|
|
112
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Register a named script handler. When a `script` action's
|
|
121
|
+
* `target`/`execute` matches the registered name, the handler runs
|
|
122
|
+
* instead of the expression evaluator. Lets dashboards/views wire
|
|
123
|
+
* symbolic action names (e.g. 'export_dashboard_pdf') to JS callbacks.
|
|
124
|
+
*/
|
|
125
|
+
registerScript(scriptName, handler) {
|
|
126
|
+
this.scripts.set(scriptName, handler);
|
|
127
|
+
}
|
|
128
|
+
unregisterScript(scriptName) {
|
|
129
|
+
this.scripts.delete(scriptName);
|
|
130
|
+
}
|
|
113
131
|
async execute(action) {
|
|
114
132
|
try {
|
|
115
133
|
// Resolve the action type
|
|
@@ -143,14 +161,21 @@ export class ActionRunner {
|
|
|
143
161
|
}
|
|
144
162
|
// Param collection: if the action defines ActionParam[] to collect,
|
|
145
163
|
// show a dialog to gather user input before executing.
|
|
146
|
-
|
|
164
|
+
// Spec defines this as `params: ActionParam[]`; ActionRunner historically
|
|
165
|
+
// used `actionParams` to disambiguate from the static-params object that
|
|
166
|
+
// some custom handlers consume. Accept both — when `params` is an array,
|
|
167
|
+
// treat it as the input-collection definition.
|
|
168
|
+
const paramDefs = action.actionParams && Array.isArray(action.actionParams) ? action.actionParams
|
|
169
|
+
: (Array.isArray(action.params) ? action.params : undefined);
|
|
170
|
+
if (paramDefs && paramDefs.length > 0) {
|
|
147
171
|
if (this.paramCollectionHandler) {
|
|
148
|
-
const collected = await this.paramCollectionHandler(
|
|
172
|
+
const collected = await this.paramCollectionHandler(paramDefs);
|
|
149
173
|
if (collected === null) {
|
|
150
174
|
return { success: false, error: 'Action cancelled by user (params)' };
|
|
151
175
|
}
|
|
152
|
-
// Merge collected params into action.params
|
|
153
|
-
|
|
176
|
+
// Merge collected params into action.params as a values map for downstream consumers.
|
|
177
|
+
// (Replace the array form with a values object once collected.)
|
|
178
|
+
action.params = { ...(Array.isArray(action.params) ? {} : (action.params || {})), ...collected };
|
|
154
179
|
}
|
|
155
180
|
}
|
|
156
181
|
// Check for a registered custom handler first
|
|
@@ -293,6 +318,18 @@ export class ActionRunner {
|
|
|
293
318
|
if (!script) {
|
|
294
319
|
return { success: false, error: 'No script provided for script action' };
|
|
295
320
|
}
|
|
321
|
+
// Named script registry wins over the expression evaluator. This lets
|
|
322
|
+
// dashboards/views bind a symbolic action name (e.g. 'export_dashboard_pdf')
|
|
323
|
+
// to a JS callback without piping the literal through ExpressionEvaluator.
|
|
324
|
+
const named = this.scripts.get(script);
|
|
325
|
+
if (named) {
|
|
326
|
+
try {
|
|
327
|
+
return await named(action, this.context);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
return { success: false, error: `Script execution failed: ${error.message}` };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
296
333
|
try {
|
|
297
334
|
const result = this.evaluator.evaluate(`\${${script}}`);
|
|
298
335
|
return { success: true, data: result };
|
|
@@ -53,7 +53,9 @@ function matchesASTFilter(record, filterNode) {
|
|
|
53
53
|
case 'in':
|
|
54
54
|
return Array.isArray(target) && target.includes(value);
|
|
55
55
|
case 'not in':
|
|
56
|
-
case '
|
|
56
|
+
case 'not_in':
|
|
57
|
+
case 'nin': // canonical (per spec)
|
|
58
|
+
case 'notin': // legacy alias
|
|
57
59
|
return Array.isArray(target) && !target.includes(value);
|
|
58
60
|
case 'contains': {
|
|
59
61
|
const lv = typeof value === 'string' ? value.toLowerCase() : '';
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* @returns ObjectStack operator or null if not recognized
|
|
13
13
|
*/
|
|
14
14
|
export function convertOperatorToAST(operator) {
|
|
15
|
+
// Spec reference: framework/packages/spec/src/data/filter.zod.ts
|
|
16
|
+
// Canonical MongoDB-style keys are camelCase ($startsWith, $endsWith, $notContains).
|
|
17
|
+
// Lowercase aliases are accepted for tolerance.
|
|
15
18
|
const operatorMap = {
|
|
16
19
|
'$eq': '=',
|
|
17
20
|
'$ne': '!=',
|
|
@@ -20,11 +23,16 @@ export function convertOperatorToAST(operator) {
|
|
|
20
23
|
'$lt': '<',
|
|
21
24
|
'$lte': '<=',
|
|
22
25
|
'$in': 'in',
|
|
23
|
-
'$nin': '
|
|
24
|
-
'$notin': '
|
|
26
|
+
'$nin': 'nin',
|
|
27
|
+
'$notin': 'nin',
|
|
28
|
+
'$between': 'between',
|
|
25
29
|
'$contains': 'contains',
|
|
30
|
+
'$notContains': 'notcontains',
|
|
31
|
+
'$notcontains': 'notcontains',
|
|
32
|
+
'$startsWith': 'startswith',
|
|
26
33
|
'$startswith': 'startswith',
|
|
27
|
-
'$
|
|
34
|
+
'$endsWith': 'endswith',
|
|
35
|
+
'$endswith': 'endswith',
|
|
28
36
|
};
|
|
29
37
|
return operatorMap[operator] || null;
|
|
30
38
|
}
|
|
@@ -66,10 +74,21 @@ export function convertFiltersToAST(filter) {
|
|
|
66
74
|
console.warn(`[ObjectUI] Warning: $regex operator is not fully supported. ` +
|
|
67
75
|
`Converting to 'contains' which only supports substring matching, not regex patterns. ` +
|
|
68
76
|
`Field: '${field}', Value: ${JSON.stringify(operatorValue)}. ` +
|
|
69
|
-
`Consider using $contains or $
|
|
77
|
+
`Consider using $contains or $startsWith instead.`);
|
|
70
78
|
conditions.push([field, 'contains', operatorValue]);
|
|
71
79
|
continue;
|
|
72
80
|
}
|
|
81
|
+
// $null / $exists translate based on their boolean value (per spec semantics).
|
|
82
|
+
// $null: true → IS NULL | $null: false → IS NOT NULL
|
|
83
|
+
// $exists: true → IS NOT NULL | $exists: false → IS NULL
|
|
84
|
+
if (operator === '$null') {
|
|
85
|
+
conditions.push([field, operatorValue ? 'is_null' : 'is_not_null', true]);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (operator === '$exists') {
|
|
89
|
+
conditions.push([field, operatorValue ? 'is_not_null' : 'is_null', true]);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
73
92
|
const astOperator = convertOperatorToAST(operator);
|
|
74
93
|
if (astOperator) {
|
|
75
94
|
conditions.push([field, astOperator, operatorValue]);
|
|
@@ -77,7 +96,8 @@ export function convertFiltersToAST(filter) {
|
|
|
77
96
|
else {
|
|
78
97
|
// Unknown operator - throw error to avoid silent failure
|
|
79
98
|
throw new Error(`[ObjectUI] Unknown filter operator '${operator}' for field '${field}'. ` +
|
|
80
|
-
`Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $
|
|
99
|
+
`Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $between, ` +
|
|
100
|
+
`$contains, $notContains, $startsWith, $endsWith, $null, $exists. ` +
|
|
81
101
|
`If you need exact object matching, use the value directly without an operator.`);
|
|
82
102
|
}
|
|
83
103
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"description": "Core logic, types, and validation for Object UI. Zero React dependencies.",
|
|
8
|
-
"homepage": "https://www.objectui.org",
|
|
8
|
+
"homepage": "https://www.objectui.org/docs/core",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/objectstack-ai/objectui.git",
|
|
11
|
+
"url": "git+https://github.com/objectstack-ai/objectui.git",
|
|
12
12
|
"directory": "packages/core"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
@@ -25,15 +25,39 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@objectstack/spec": "^4.0.
|
|
28
|
+
"@objectstack/spec": "^4.0.4",
|
|
29
29
|
"lodash": "^4.18.1",
|
|
30
|
-
"zod": "^4.3
|
|
31
|
-
"@object-ui/types": "3.3.
|
|
30
|
+
"zod": "^4.4.3",
|
|
31
|
+
"@object-ui/types": "3.3.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"typescript": "^6.0.
|
|
35
|
-
"vitest": "^4.1.
|
|
34
|
+
"typescript": "^6.0.3",
|
|
35
|
+
"vitest": "^4.1.5"
|
|
36
36
|
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"objectui",
|
|
39
|
+
"sdui",
|
|
40
|
+
"schema-driven-ui",
|
|
41
|
+
"react",
|
|
42
|
+
"tailwind",
|
|
43
|
+
"shadcn",
|
|
44
|
+
"objectstack",
|
|
45
|
+
"core",
|
|
46
|
+
"engine",
|
|
47
|
+
"expression-engine",
|
|
48
|
+
"action-engine",
|
|
49
|
+
"registry"
|
|
50
|
+
],
|
|
51
|
+
"author": "ObjectStack Team <team@objectstack.ai>",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"dist",
|
|
57
|
+
"README.md",
|
|
58
|
+
"CHANGELOG.md",
|
|
59
|
+
"LICENSE"
|
|
60
|
+
],
|
|
37
61
|
"scripts": {
|
|
38
62
|
"build": "tsc",
|
|
39
63
|
"test": "vitest run",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Performance benchmark suite for @object-ui/core.
|
|
11
|
-
*
|
|
12
|
-
* Part of Q1 2026 roadmap §1.4 Test Coverage Improvement.
|
|
13
|
-
*
|
|
14
|
-
* Run with: npx vitest bench packages/core/src/__benchmarks__/
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { bench, describe } from 'vitest';
|
|
18
|
-
import { ExpressionEvaluator } from '@object-ui/core';
|
|
19
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
20
|
-
import { contrastRatio, meetsContrastLevel, hexToHSL } from '@object-ui/core';
|
|
21
|
-
|
|
22
|
-
describe('ExpressionEvaluator performance', () => {
|
|
23
|
-
const evaluator = new ExpressionEvaluator({ data: { name: 'Alice', age: 30, active: true } });
|
|
24
|
-
|
|
25
|
-
bench('evaluate simple string', () => {
|
|
26
|
-
evaluator.evaluate('Hello ${data.name}');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
bench('evaluate 100 expressions', () => {
|
|
30
|
-
for (let i = 0; i < 100; i++) {
|
|
31
|
-
evaluator.evaluate('Hello ${data.name}');
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('ComponentRegistry performance', () => {
|
|
37
|
-
bench('get registered component', () => {
|
|
38
|
-
ComponentRegistry.get('button');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
bench('has check', () => {
|
|
42
|
-
ComponentRegistry.has('button');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('Theme utilities performance', () => {
|
|
47
|
-
bench('hexToHSL conversion', () => {
|
|
48
|
-
hexToHSL('#336699');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
bench('contrastRatio calculation', () => {
|
|
52
|
-
contrastRatio('#000000', '#ffffff');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
bench('meetsContrastLevel check', () => {
|
|
56
|
-
meetsContrastLevel('#000000', '#ffffff', 'AA');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
bench('100 contrast checks', () => {
|
|
60
|
-
for (let i = 0; i < 100; i++) {
|
|
61
|
-
meetsContrastLevel('#336699', '#ffffff', 'AA');
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import type { DndConfig, DragItem, DropZone, DragConstraint } from '@object-ui/types';
|
|
3
|
-
import {
|
|
4
|
-
resolveDndConfig,
|
|
5
|
-
createDragItemProps,
|
|
6
|
-
createDropZoneProps,
|
|
7
|
-
resolveDragConstraints,
|
|
8
|
-
} from '../../protocols/DndProtocol';
|
|
9
|
-
|
|
10
|
-
describe('DndProtocol', () => {
|
|
11
|
-
// ==========================================================================
|
|
12
|
-
// resolveDndConfig
|
|
13
|
-
// ==========================================================================
|
|
14
|
-
describe('resolveDndConfig', () => {
|
|
15
|
-
it('should apply all defaults for minimal config', () => {
|
|
16
|
-
const config = {} as DndConfig;
|
|
17
|
-
const resolved = resolveDndConfig(config);
|
|
18
|
-
|
|
19
|
-
expect(resolved.enabled).toBe(false);
|
|
20
|
-
expect(resolved.sortable).toBe(false);
|
|
21
|
-
expect(resolved.autoScroll).toBe(true);
|
|
22
|
-
expect(resolved.touchDelay).toBe(200);
|
|
23
|
-
expect(resolved.dragItem).toBeUndefined();
|
|
24
|
-
expect(resolved.dropZone).toBeUndefined();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should preserve explicit values', () => {
|
|
28
|
-
const config = {
|
|
29
|
-
enabled: true,
|
|
30
|
-
sortable: true,
|
|
31
|
-
autoScroll: false,
|
|
32
|
-
touchDelay: 500,
|
|
33
|
-
} as DndConfig;
|
|
34
|
-
const resolved = resolveDndConfig(config);
|
|
35
|
-
|
|
36
|
-
expect(resolved.enabled).toBe(true);
|
|
37
|
-
expect(resolved.sortable).toBe(true);
|
|
38
|
-
expect(resolved.autoScroll).toBe(false);
|
|
39
|
-
expect(resolved.touchDelay).toBe(500);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should pass through dragItem and dropZone', () => {
|
|
43
|
-
const dragItem = { type: 'card' } as DragItem;
|
|
44
|
-
const dropZone = { accept: ['card'] } as DropZone;
|
|
45
|
-
const config = { dragItem, dropZone } as DndConfig;
|
|
46
|
-
const resolved = resolveDndConfig(config);
|
|
47
|
-
|
|
48
|
-
expect(resolved.dragItem).toBe(dragItem);
|
|
49
|
-
expect(resolved.dropZone).toBe(dropZone);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// ==========================================================================
|
|
54
|
-
// createDragItemProps
|
|
55
|
-
// ==========================================================================
|
|
56
|
-
describe('createDragItemProps', () => {
|
|
57
|
-
it('should return correct props for a basic drag item', () => {
|
|
58
|
-
const item = { type: 'card', label: 'Task card' } as DragItem;
|
|
59
|
-
const props = createDragItemProps(item);
|
|
60
|
-
|
|
61
|
-
expect(props.draggable).toBe(true);
|
|
62
|
-
expect(props['aria-roledescription']).toBe('draggable');
|
|
63
|
-
expect(props['aria-label']).toBe('Task card');
|
|
64
|
-
expect(props.role).toBe('listitem');
|
|
65
|
-
expect(props['data-drag-type']).toBe('card');
|
|
66
|
-
expect(props['data-drag-handle']).toBe('element');
|
|
67
|
-
expect(props['data-drag-disabled']).toBe('false');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should set draggable false when disabled', () => {
|
|
71
|
-
const item = { type: 'card', disabled: true } as DragItem;
|
|
72
|
-
const props = createDragItemProps(item);
|
|
73
|
-
|
|
74
|
-
expect(props.draggable).toBe(false);
|
|
75
|
-
expect(props['data-drag-disabled']).toBe('true');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should use ariaLabel over label when both provided', () => {
|
|
79
|
-
const item = { type: 'card', label: 'Label', ariaLabel: 'Aria Label' } as DragItem;
|
|
80
|
-
const props = createDragItemProps(item);
|
|
81
|
-
|
|
82
|
-
expect(props['aria-label']).toBe('Aria Label');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should use ariaLabel when provided as string', () => {
|
|
86
|
-
const item = {
|
|
87
|
-
type: 'card',
|
|
88
|
-
ariaLabel: 'Translated label',
|
|
89
|
-
} as unknown as DragItem;
|
|
90
|
-
const props = createDragItemProps(item);
|
|
91
|
-
|
|
92
|
-
expect(props['aria-label']).toBe('Translated label');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should use custom role and handle when provided', () => {
|
|
96
|
-
const item = { type: 'row', role: 'option', handle: 'grip' } as DragItem;
|
|
97
|
-
const props = createDragItemProps(item);
|
|
98
|
-
|
|
99
|
-
expect(props.role).toBe('option');
|
|
100
|
-
expect(props['data-drag-handle']).toBe('grip');
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// ==========================================================================
|
|
105
|
-
// createDropZoneProps
|
|
106
|
-
// ==========================================================================
|
|
107
|
-
describe('createDropZoneProps', () => {
|
|
108
|
-
it('should return correct props for a basic drop zone', () => {
|
|
109
|
-
const zone = { accept: ['card', 'task'], label: 'Column' } as unknown as DropZone;
|
|
110
|
-
const props = createDropZoneProps(zone);
|
|
111
|
-
|
|
112
|
-
expect(props['aria-dropeffect']).toBe('move');
|
|
113
|
-
expect(props['aria-label']).toBe('Column');
|
|
114
|
-
expect(props.role).toBe('list');
|
|
115
|
-
expect(props['data-drop-accept']).toBe('card,task');
|
|
116
|
-
expect(props['data-drop-highlight']).toBe('true');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should use explicit dropEffect and role', () => {
|
|
120
|
-
const zone = { accept: ['item'], dropEffect: 'copy', role: 'region' } as unknown as DropZone;
|
|
121
|
-
const props = createDropZoneProps(zone);
|
|
122
|
-
|
|
123
|
-
expect(props['aria-dropeffect']).toBe('copy');
|
|
124
|
-
expect(props.role).toBe('region');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should set maxItems when provided', () => {
|
|
128
|
-
const zone = { accept: ['card'], maxItems: 10 } as unknown as DropZone;
|
|
129
|
-
const props = createDropZoneProps(zone);
|
|
130
|
-
|
|
131
|
-
expect(props['data-drop-max-items']).toBe(10);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should use ariaLabel string when provided', () => {
|
|
135
|
-
const zone = {
|
|
136
|
-
accept: ['card'],
|
|
137
|
-
ariaLabel: 'Drop here',
|
|
138
|
-
} as unknown as DropZone;
|
|
139
|
-
const props = createDropZoneProps(zone);
|
|
140
|
-
|
|
141
|
-
expect(props['aria-label']).toBe('Drop here');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// ==========================================================================
|
|
146
|
-
// resolveDragConstraints
|
|
147
|
-
// ==========================================================================
|
|
148
|
-
describe('resolveDragConstraints', () => {
|
|
149
|
-
it('should return base styles for default axis (both)', () => {
|
|
150
|
-
const constraint = {} as DragConstraint;
|
|
151
|
-
const styles = resolveDragConstraints(constraint);
|
|
152
|
-
|
|
153
|
-
expect(styles.position).toBe('relative');
|
|
154
|
-
expect(styles.touchAction).toBe('none');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should set touchAction to pan-y for x axis', () => {
|
|
158
|
-
const constraint = { axis: 'x' } as DragConstraint;
|
|
159
|
-
const styles = resolveDragConstraints(constraint);
|
|
160
|
-
|
|
161
|
-
expect(styles.touchAction).toBe('pan-y');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should set touchAction to pan-x for y axis', () => {
|
|
165
|
-
const constraint = { axis: 'y' } as DragConstraint;
|
|
166
|
-
const styles = resolveDragConstraints(constraint);
|
|
167
|
-
|
|
168
|
-
expect(styles.touchAction).toBe('pan-x');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('should set overflow hidden for parent bounds', () => {
|
|
172
|
-
const constraint = { bounds: 'parent' } as DragConstraint;
|
|
173
|
-
const styles = resolveDragConstraints(constraint);
|
|
174
|
-
|
|
175
|
-
expect(styles.overflow).toBe('hidden');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should set grid CSS custom properties', () => {
|
|
179
|
-
const constraint = { grid: [20, 30] } as unknown as DragConstraint;
|
|
180
|
-
const styles = resolveDragConstraints(constraint);
|
|
181
|
-
|
|
182
|
-
expect(styles['--drag-grid-x']).toBe('20px');
|
|
183
|
-
expect(styles['--drag-grid-y']).toBe('30px');
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
});
|