@cybertale/resolver 0.1.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/.idea/cybertale-resolver.iml +8 -0
- package/.idea/modules.xml +8 -0
- package/.idea/php.xml +19 -0
- package/README.md +92 -0
- package/package.json +12 -0
- package/src/index.ts +25 -0
- package/src/resolver/compute/label.ts +42 -0
- package/src/resolver/compute/tooltip.ts +47 -0
- package/src/resolver/compute/validation.ts +21 -0
- package/src/resolver/finalize/defaults.ts +30 -0
- package/src/resolver/finalize/template.ts +25 -0
- package/src/resolver/form/input.ts +14 -0
- package/src/resolver/form/select.ts +49 -0
- package/src/resolver/form/value.ts +54 -0
- package/src/resolver/handlers/insert.ts +33 -0
- package/src/resolver/handlers/remove.ts +52 -0
- package/src/resolver/handlers/update.ts +55 -0
- package/src/resolver/transform/json.ts +23 -0
- package/src/resolver/transform/stat.ts +32 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$" />
|
|
5
|
+
<orderEntry type="inheritedJdk" />
|
|
6
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
7
|
+
</component>
|
|
8
|
+
</module>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/cybertale-resolver.iml" filepath="$PROJECT_DIR$/.idea/cybertale-resolver.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
package/.idea/php.xml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="MessDetectorOptionsConfiguration">
|
|
4
|
+
<option name="transferred" value="true" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="PHPCSFixerOptionsConfiguration">
|
|
7
|
+
<option name="transferred" value="true" />
|
|
8
|
+
</component>
|
|
9
|
+
<component name="PHPCodeSnifferOptionsConfiguration">
|
|
10
|
+
<option name="highlightLevel" value="WARNING" />
|
|
11
|
+
<option name="transferred" value="true" />
|
|
12
|
+
</component>
|
|
13
|
+
<component name="PhpStanOptionsConfiguration">
|
|
14
|
+
<option name="transferred" value="true" />
|
|
15
|
+
</component>
|
|
16
|
+
<component name="PsalmOptionsConfiguration">
|
|
17
|
+
<option name="transferred" value="true" />
|
|
18
|
+
</component>
|
|
19
|
+
</project>
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# 📦 `@cybertale/resolver`
|
|
2
|
+
|
|
3
|
+
**A lightweight, framework-agnostic resolver engine** used to transform, compute, update, and manage `ObjectTemplate` structures from `@cybertale/interface`.
|
|
4
|
+
|
|
5
|
+
This package extracts core logic from UI components (Vue/React) into a clean, reusable, testable library.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://img.shields.io/npm/v/%40cybertale%2Fresolver.svg?style=for-the-badge" />
|
|
11
|
+
<img src="https://img.shields.io/npm/dm/%40cybertale%2Fresolver.svg?style=for-the-badge" />
|
|
12
|
+
<img src="https://img.shields.io/bundlephobia/minzip/%40cybertale%2Fresolver?style=for-the-badge" />
|
|
13
|
+
<img src="https://img.shields.io/github/license/cybertale/resolver?style=for-the-badge" />
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## 🚀 Why this exists
|
|
17
|
+
Modern UI frameworks shouldn't contain domain logic.
|
|
18
|
+
|
|
19
|
+
Before this package:
|
|
20
|
+
- Buttons processed JSON in the component
|
|
21
|
+
- Fields decided validation class
|
|
22
|
+
- SelectList parsed arrays
|
|
23
|
+
- Many duplicated helpers across components
|
|
24
|
+
|
|
25
|
+
Now all that logic is extracted into:
|
|
26
|
+
```
|
|
27
|
+
@cybertale/resolver
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Your UI becomes dumb — your resolver becomes smart.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# 🧩 Features
|
|
35
|
+
|
|
36
|
+
### ✔ JSON parsing & normalization
|
|
37
|
+
### ✔ Template stat utilities
|
|
38
|
+
### ✔ Template actions
|
|
39
|
+
### ✔ Computed helpers
|
|
40
|
+
### ✔ Finalization helpers
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# 📥 Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install @cybertale/resolver
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
# 🧱 Architecture Overview
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
resolver/
|
|
56
|
+
├── transform/
|
|
57
|
+
├── form/
|
|
58
|
+
├── compute/
|
|
59
|
+
├── finalize/
|
|
60
|
+
└── handlers/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# 📚 Usage Examples
|
|
66
|
+
|
|
67
|
+
## Extracted Field Logic (Vue)
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { getValueFromTemplate } from '@cybertale/resolver/form/value'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Updating Template Data
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { updateValueForTemplate } from '@cybertale/resolver/handlers/update'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# 🤝 Contributing
|
|
82
|
+
|
|
83
|
+
1. Clone repo
|
|
84
|
+
2. Install deps
|
|
85
|
+
3. Run tests
|
|
86
|
+
4. Submit PR
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
# 📜 License
|
|
91
|
+
|
|
92
|
+
MIT © Cybertale
|
package/package.json
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
// Barrel exports for @cybertale/template
|
|
3
|
+
|
|
4
|
+
// transform
|
|
5
|
+
export * as JsonTransform from './resolver/transform/json'
|
|
6
|
+
export * as StatTransform from './resolver/transform/stat'
|
|
7
|
+
|
|
8
|
+
// form
|
|
9
|
+
export * as FormValue from './resolver/form/value'
|
|
10
|
+
export * as FormInput from './resolver/form/input'
|
|
11
|
+
export * as FormSelect from './resolver/form/select'
|
|
12
|
+
|
|
13
|
+
// compute
|
|
14
|
+
export * as ComputeLabel from './resolver/compute/label'
|
|
15
|
+
export * as ComputeTooltip from './resolver/compute/tooltip'
|
|
16
|
+
export * as ComputeValidation from './resolver/compute/validation'
|
|
17
|
+
|
|
18
|
+
// finalize
|
|
19
|
+
export * as FinalizeTemplate from './resolver/finalize/template'
|
|
20
|
+
export * as FinalizeDefaults from './resolver/finalize/defaults'
|
|
21
|
+
|
|
22
|
+
// handlers
|
|
23
|
+
export * as HandlersRemove from './resolver/handlers/remove'
|
|
24
|
+
export * as HandlersInsert from './resolver/handlers/insert'
|
|
25
|
+
export * as HandlersUpdate from './resolver/handlers/update'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
|
|
3
|
+
import { getStringStat } from '../transform/stat'
|
|
4
|
+
import { isJSONObject, parseJSON } from '../transform/json'
|
|
5
|
+
|
|
6
|
+
export interface LabelData {
|
|
7
|
+
iconClass?: string
|
|
8
|
+
title?: string
|
|
9
|
+
styleData?: string
|
|
10
|
+
contentValue?: string
|
|
11
|
+
contentClass?: string
|
|
12
|
+
translate?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Attempts to parse Stat.Label as a LabelData object; if it is a plain string,
|
|
17
|
+
* it is returned as `title` for convenience.
|
|
18
|
+
*/
|
|
19
|
+
export function getLabelData(
|
|
20
|
+
object: ObjectTemplate,
|
|
21
|
+
labelStat: StatTypeEnum,
|
|
22
|
+
): LabelData {
|
|
23
|
+
const raw = getStringStat(object, labelStat)
|
|
24
|
+
if (!raw) return { title: '' }
|
|
25
|
+
|
|
26
|
+
if (isJSONObject(raw)) {
|
|
27
|
+
const parsed = parseJSON<LabelData>(raw)
|
|
28
|
+
if (parsed) return parsed
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { title: raw }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns the plain label text (usually LabelData.title or raw string).
|
|
36
|
+
*/
|
|
37
|
+
export function getLabelText(
|
|
38
|
+
object: ObjectTemplate,
|
|
39
|
+
labelStat: StatTypeEnum,
|
|
40
|
+
): string {
|
|
41
|
+
return getLabelData(object, labelStat).title ?? ''
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
|
|
3
|
+
import { getStringStat } from '../transform/stat'
|
|
4
|
+
import { isJSONObject, parseJSON } from '../transform/json'
|
|
5
|
+
|
|
6
|
+
export interface TooltipData {
|
|
7
|
+
toggleBy: string
|
|
8
|
+
value: string
|
|
9
|
+
translate?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tooltip is stored either as a simple string or as a JSON-encoded TooltipData.
|
|
14
|
+
*/
|
|
15
|
+
export function getTooltip(
|
|
16
|
+
object: ObjectTemplate,
|
|
17
|
+
tooltipStat: StatTypeEnum,
|
|
18
|
+
): TooltipData | string | null {
|
|
19
|
+
const raw = getStringStat(object, tooltipStat)
|
|
20
|
+
if (!raw) return null
|
|
21
|
+
|
|
22
|
+
if (isJSONObject(raw)) {
|
|
23
|
+
return parseJSON<TooltipData>(raw)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return raw
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getTooltipToggleBy(
|
|
30
|
+
object: ObjectTemplate,
|
|
31
|
+
tooltipStat: StatTypeEnum,
|
|
32
|
+
): string {
|
|
33
|
+
const tooltip = getTooltip(object, tooltipStat)
|
|
34
|
+
if (!tooltip) return 'tooltip'
|
|
35
|
+
if (typeof tooltip === 'string') return 'tooltip'
|
|
36
|
+
return tooltip.toggleBy || 'tooltip'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getTooltipText(
|
|
40
|
+
object: ObjectTemplate,
|
|
41
|
+
tooltipStat: StatTypeEnum,
|
|
42
|
+
): string {
|
|
43
|
+
const tooltip = getTooltip(object, tooltipStat)
|
|
44
|
+
if (!tooltip) return ''
|
|
45
|
+
if (typeof tooltip === 'string') return tooltip
|
|
46
|
+
return tooltip.value
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
|
|
3
|
+
import { getRawStat, getStringStat } from '../transform/stat'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Computes a Bootstrap-esque validation class ('is-valid' | 'is-invalid' | '').
|
|
7
|
+
* This mirrors the repeated logic you had in several components.
|
|
8
|
+
*/
|
|
9
|
+
export function computeValidationClass(
|
|
10
|
+
object: ObjectTemplate,
|
|
11
|
+
isValidStat: StatTypeEnum,
|
|
12
|
+
errorMessageStat: StatTypeEnum,
|
|
13
|
+
): string {
|
|
14
|
+
const isValidRaw = getRawStat(object, isValidStat)
|
|
15
|
+
const errorMessage = getStringStat(object, errorMessageStat)
|
|
16
|
+
|
|
17
|
+
if (isValidRaw === undefined || isValidRaw === '') return ''
|
|
18
|
+
if (isValidRaw) return 'is-valid'
|
|
19
|
+
if (errorMessage) return 'is-invalid'
|
|
20
|
+
return ''
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
|
|
2
|
+
import { getRawStat } from '../transform/stat'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Applies default values to stats that are currently empty / undefined.
|
|
6
|
+
* `defaults` is a map of StatTypeEnum -> default Data value.
|
|
7
|
+
*/
|
|
8
|
+
export function applyDefaults(
|
|
9
|
+
template: ObjectTemplate,
|
|
10
|
+
defaults: Partial<Record<StatTypeEnum, unknown>>,
|
|
11
|
+
): ObjectTemplate {
|
|
12
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
13
|
+
const statKey = Number(key) as StatTypeEnum
|
|
14
|
+
|
|
15
|
+
// Only apply if undefined or empty
|
|
16
|
+
if (getRawStat(template, statKey) == null && template.Stats) {
|
|
17
|
+
const stringValue = value !== undefined && value !== null
|
|
18
|
+
? String(value) // <—— FIX HERE
|
|
19
|
+
: ''
|
|
20
|
+
|
|
21
|
+
if (!template.Stats[statKey]) {
|
|
22
|
+
// Create new stat entry
|
|
23
|
+
template.Stats[statKey] = { Data: stringValue } as any
|
|
24
|
+
} else {
|
|
25
|
+
template.Stats[statKey].Data = stringValue
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return template
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate } from '@cybertale/interface'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Finalization hook for a single ObjectTemplate before it is sent to the backend
|
|
6
|
+
* or stored. For now this is a light pass-through, but centralizing this
|
|
7
|
+
* allows you to add future cleanup in one place (e.g. stripping transient stats).
|
|
8
|
+
*/
|
|
9
|
+
export function finalizeTemplate(template: ObjectTemplate): ObjectTemplate {
|
|
10
|
+
// Shallow clone to avoid accidental external mutation
|
|
11
|
+
return new (template.constructor as typeof ObjectTemplate)(
|
|
12
|
+
template.Region,
|
|
13
|
+
template.ObjectEnum,
|
|
14
|
+
template.SubObjectEnum,
|
|
15
|
+
template.ActionEnum,
|
|
16
|
+
template.Stats,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convenience function for finalizing an array of templates.
|
|
22
|
+
*/
|
|
23
|
+
export function finalizeTemplates(templates: ObjectTemplate[]): ObjectTemplate[] {
|
|
24
|
+
return templates.map(finalizeTemplate)
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
|
|
3
|
+
import { getStringStat } from '../transform/stat'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the input type for a given template.
|
|
7
|
+
* In your current code this is usually driven by StatTypeEnum.ElementType.
|
|
8
|
+
*/
|
|
9
|
+
export function getInputTypeFromTemplate(
|
|
10
|
+
object: ObjectTemplate,
|
|
11
|
+
elementTypeStat: StatTypeEnum,
|
|
12
|
+
): string {
|
|
13
|
+
return getStringStat(object, elementTypeStat)
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
|
|
3
|
+
import { getStringStat } from '../transform/stat'
|
|
4
|
+
import { parseJSON, isJSONArray } from '../transform/json'
|
|
5
|
+
|
|
6
|
+
export interface SelectItem {
|
|
7
|
+
id: string | number
|
|
8
|
+
name?: string
|
|
9
|
+
[key: string]: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parses the ItemList stat (usually JSON) into a typed array.
|
|
14
|
+
*/
|
|
15
|
+
export function getSelectItems(
|
|
16
|
+
object: ObjectTemplate,
|
|
17
|
+
itemListStat: StatTypeEnum,
|
|
18
|
+
): SelectItem[] {
|
|
19
|
+
const raw = getStringStat(object, itemListStat)
|
|
20
|
+
if (!raw || !isJSONArray(raw)) return []
|
|
21
|
+
const parsed = parseJSON<unknown[]>(raw) ?? []
|
|
22
|
+
return parsed.filter((item): item is SelectItem => {
|
|
23
|
+
return !!item && typeof (item as any).id !== 'undefined'
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets the currently selected item id from the template.
|
|
29
|
+
* This simply returns Stat.Value (or similar), but extracted in one place.
|
|
30
|
+
*/
|
|
31
|
+
export function getSelectedId(
|
|
32
|
+
object: ObjectTemplate,
|
|
33
|
+
valueStat: StatTypeEnum,
|
|
34
|
+
): string {
|
|
35
|
+
return getStringStat(object, valueStat)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the selected SelectItem, if found.
|
|
40
|
+
*/
|
|
41
|
+
export function getSelectedItem(
|
|
42
|
+
object: ObjectTemplate,
|
|
43
|
+
itemListStat: StatTypeEnum,
|
|
44
|
+
valueStat: StatTypeEnum,
|
|
45
|
+
): SelectItem | null {
|
|
46
|
+
const items = getSelectItems(object, itemListStat)
|
|
47
|
+
const selectedId = getSelectedId(object, valueStat)
|
|
48
|
+
return items.find(item => String(item.id) === selectedId) ?? null
|
|
49
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ObjectTemplate } from '@cybertale/interface'
|
|
2
|
+
import { StatTypeEnum } from '@cybertale/interface'
|
|
3
|
+
import { getStringStat, hasStat } from '../transform/stat'
|
|
4
|
+
import { isJSONArray, parseJSON } from '../transform/json'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generic value resolution from ObjectTemplate stats.
|
|
8
|
+
*
|
|
9
|
+
* This encapsulates the common pattern you used in multiple components:
|
|
10
|
+
* - Stat.Value stores an array (JSON string)
|
|
11
|
+
* - Stat.Option (or other index stat) selects one element from that array
|
|
12
|
+
* - OptionIndices may itself be a JSON array of indices
|
|
13
|
+
*/
|
|
14
|
+
export function getValueFromTemplate(
|
|
15
|
+
object: ObjectTemplate,
|
|
16
|
+
valueStat: StatTypeEnum,
|
|
17
|
+
indexStat: StatTypeEnum = (StatTypeEnum as any).Option,
|
|
18
|
+
): unknown {
|
|
19
|
+
const raw = getStringStat(object, valueStat)
|
|
20
|
+
if (!raw) return ''
|
|
21
|
+
|
|
22
|
+
if (!hasStat(object, indexStat) || !isJSONArray(raw)) {
|
|
23
|
+
return raw
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = parseJSON<unknown[]>(raw)
|
|
27
|
+
if (!data) return ''
|
|
28
|
+
|
|
29
|
+
const indexRaw = getStringStat(object, indexStat)
|
|
30
|
+
|
|
31
|
+
// JSON-array index case
|
|
32
|
+
if (isJSONArray(indexRaw)) {
|
|
33
|
+
const optionIndicesStat = (StatTypeEnum as any).OptionIndices as StatTypeEnum
|
|
34
|
+
const indices = parseJSON<number[]>(indexRaw) ?? []
|
|
35
|
+
|
|
36
|
+
const optionIndex = Number(getStringStat(object, optionIndicesStat))
|
|
37
|
+
const mappedIndex = indices[optionIndex]
|
|
38
|
+
|
|
39
|
+
// FIXED: redundant typeof removed, replaced with correct bounds + null check
|
|
40
|
+
if (mappedIndex != null && mappedIndex in data) {
|
|
41
|
+
return data[mappedIndex]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Simple numeric index
|
|
48
|
+
const index = Number(indexRaw)
|
|
49
|
+
if (Number.isNaN(index) || !(index in data)) {
|
|
50
|
+
return ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data[index]
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate } from '@cybertale/interface'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Inserts a single ObjectTemplate at the given index and returns a new array.
|
|
6
|
+
*/
|
|
7
|
+
export function insertTemplateAt(
|
|
8
|
+
templates: ObjectTemplate[],
|
|
9
|
+
index: number,
|
|
10
|
+
template: ObjectTemplate,
|
|
11
|
+
): ObjectTemplate[] {
|
|
12
|
+
return [
|
|
13
|
+
...templates.slice(0, index),
|
|
14
|
+
template,
|
|
15
|
+
...templates.slice(index),
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Inserts multiple ObjectTemplates starting at the given index and returns
|
|
21
|
+
* a new array.
|
|
22
|
+
*/
|
|
23
|
+
export function insertTemplatesAt(
|
|
24
|
+
templates: ObjectTemplate[],
|
|
25
|
+
index: number,
|
|
26
|
+
newTemplates: ObjectTemplate[],
|
|
27
|
+
): ObjectTemplate[] {
|
|
28
|
+
return [
|
|
29
|
+
...templates.slice(0, index),
|
|
30
|
+
...newTemplates,
|
|
31
|
+
...templates.slice(index),
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
|
|
3
|
+
import { getStringStat, hasStat } from '../transform/stat'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Finds the index of an ObjectTemplate by a tag-like stat. Supports the
|
|
7
|
+
* 'tag|something' pattern you use for button payloads.
|
|
8
|
+
*/
|
|
9
|
+
export function findTemplateIndexByStat(
|
|
10
|
+
templates: ObjectTemplate[],
|
|
11
|
+
value: string,
|
|
12
|
+
searchByStat: StatTypeEnum,
|
|
13
|
+
): number {
|
|
14
|
+
return templates.findIndex(t => {
|
|
15
|
+
const statValue = getStringStat(t, searchByStat)
|
|
16
|
+
return statValue === value || statValue === value.split('|')[1]
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Removes the first template whose `searchByStat` matches `value`.
|
|
22
|
+
* Returns a new array (non-mutating).
|
|
23
|
+
*/
|
|
24
|
+
export function removeByStatValue(
|
|
25
|
+
templates: ObjectTemplate[],
|
|
26
|
+
value: string,
|
|
27
|
+
searchByStat: StatTypeEnum,
|
|
28
|
+
): ObjectTemplate[] {
|
|
29
|
+
const index = findTemplateIndexByStat(templates, value, searchByStat)
|
|
30
|
+
if (index === -1) return templates
|
|
31
|
+
return [
|
|
32
|
+
...templates.slice(0, index),
|
|
33
|
+
...templates.slice(index + 1),
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Removes all templates where Stat.DependsOn.Data includes the provided key.
|
|
39
|
+
* This mirrors your old removeElementFromArray implementation, but returns
|
|
40
|
+
* a new array instead of mutating in-place.
|
|
41
|
+
*/
|
|
42
|
+
export function removeByDependsOn(
|
|
43
|
+
templates: ObjectTemplate[],
|
|
44
|
+
dependsOn: string,
|
|
45
|
+
dependsOnStat: StatTypeEnum,
|
|
46
|
+
): ObjectTemplate[] {
|
|
47
|
+
return templates.filter(t => {
|
|
48
|
+
if (!hasStat(t, dependsOnStat)) return true
|
|
49
|
+
const dep = getStringStat(t, dependsOnStat)
|
|
50
|
+
return !dep.includes(dependsOn)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate } from '@cybertale/interface'
|
|
3
|
+
import { StatTypeEnum } from '@cybertale/interface'
|
|
4
|
+
import { findTemplateIndexByStat } from './remove'
|
|
5
|
+
import { isJSONArray, parseJSON } from '../transform/json'
|
|
6
|
+
import { getStringStat } from '../transform/stat'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Updates a value-like stat on the template that matches the payload by some key stat.
|
|
10
|
+
* This mirrors your updateValueData logic, including JSON-array handling.
|
|
11
|
+
*/
|
|
12
|
+
export function updateValueForTemplate(
|
|
13
|
+
templates: ObjectTemplate[],
|
|
14
|
+
payload: ObjectTemplate,
|
|
15
|
+
options?: {
|
|
16
|
+
valueStat?: StatTypeEnum
|
|
17
|
+
searchByStat?: StatTypeEnum
|
|
18
|
+
valueIndicesStat?: StatTypeEnum
|
|
19
|
+
},
|
|
20
|
+
): ObjectTemplate[] {
|
|
21
|
+
const valueStat = options?.valueStat ?? (StatTypeEnum as any).Value
|
|
22
|
+
const searchByStat = options?.searchByStat ?? (StatTypeEnum as any).Tag
|
|
23
|
+
const valueIndicesStat = options?.valueIndicesStat ?? (StatTypeEnum as any).ValueIndices
|
|
24
|
+
|
|
25
|
+
const index = findTemplateIndexByStat(templates, getStringStat(payload, searchByStat), searchByStat)
|
|
26
|
+
if (index === -1) return templates
|
|
27
|
+
|
|
28
|
+
const target = templates[index]
|
|
29
|
+
const currentRaw = getStringStat(target, valueStat)
|
|
30
|
+
const payloadValue = getStringStat(payload, options?.valueStat ?? valueStat)
|
|
31
|
+
|
|
32
|
+
// If current value is a JSON array, update at ValueIndices
|
|
33
|
+
if (isJSONArray(currentRaw)) {
|
|
34
|
+
const arr = parseJSON<unknown[]>(currentRaw) ?? []
|
|
35
|
+
const indicesRaw = getStringStat(payload, valueIndicesStat)
|
|
36
|
+
const idx = Number(indicesRaw)
|
|
37
|
+
if (!Number.isNaN(idx)) {
|
|
38
|
+
arr[idx] = payloadValue
|
|
39
|
+
}
|
|
40
|
+
// Write back as JSON string
|
|
41
|
+
if (target.Stats) {
|
|
42
|
+
target.Stats[valueStat].Data = JSON.stringify(arr)
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
// Simple scalar assignment
|
|
46
|
+
if (target.Stats) {
|
|
47
|
+
target.Stats[valueStat].Data = payloadValue
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Return a new array instance to preserve immutability semantics
|
|
52
|
+
const clone = [...templates]
|
|
53
|
+
clone[index] = target
|
|
54
|
+
return clone
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
// JSON-related transformation utilities for ObjectTemplate stats.
|
|
3
|
+
// These are intentionally generic and environment-agnostic so they can be reused
|
|
4
|
+
// both in browser and Node contexts.
|
|
5
|
+
|
|
6
|
+
export function parseJSON<T = unknown>(value: string | null | undefined): T | null {
|
|
7
|
+
if (value == null || value === "") return null
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(value) as T
|
|
10
|
+
} catch {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isJSONArray(value: string | null | undefined): boolean {
|
|
16
|
+
const parsed = parseJSON(value)
|
|
17
|
+
return Array.isArray(parsed)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isJSONObject(value: string | null | undefined): boolean {
|
|
21
|
+
const parsed = parseJSON(value)
|
|
22
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
|
|
3
|
+
import { parseJSON } from './json'
|
|
4
|
+
|
|
5
|
+
// Safe stat accessors for ObjectTemplate. These centralize the common
|
|
6
|
+
// patterns used across UI and resolver code.
|
|
7
|
+
|
|
8
|
+
export function hasStat(object: ObjectTemplate, stat: StatTypeEnum): boolean {
|
|
9
|
+
return !!object.Stats && object.Stats[stat] !== undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getRawStat(object: ObjectTemplate, stat: StatTypeEnum): unknown {
|
|
13
|
+
return object.Stats?.[stat]?.Data
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getStringStat(object: ObjectTemplate, stat: StatTypeEnum): string {
|
|
17
|
+
const raw = getRawStat(object, stat)
|
|
18
|
+
if (raw == null) return ''
|
|
19
|
+
return String(raw)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getBooleanStat(object: ObjectTemplate, stat: StatTypeEnum): boolean {
|
|
23
|
+
const raw = getRawStat(object, stat)
|
|
24
|
+
// Treat empty string / null / undefined as false, anything else as true.
|
|
25
|
+
return !!raw
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getJSONStat<T = unknown>(object: ObjectTemplate, stat: StatTypeEnum): T | null {
|
|
29
|
+
const raw = getRawStat(object, stat)
|
|
30
|
+
if (typeof raw !== 'string') return null
|
|
31
|
+
return parseJSON<T>(raw)
|
|
32
|
+
}
|