Skip to content

Commit 4123933

Browse files
committed
feat: add user configurable decision inputs
1 parent cf11aeb commit 4123933

File tree

9 files changed

+285
-252
lines changed

9 files changed

+285
-252
lines changed

dev/test-studio/components/AudienceSelectInput.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

dev/test-studio/sanity.config.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,12 @@ const defaultWorkspace = defineConfig({
245245
enabled: true,
246246
},
247247
[QUOTA_EXCLUDED_RELEASES_ENABLED]: true,
248-
[DECISION_PARAMETERS_SCHEMA]: {
249-
audiences: ['aud-a', 'aud-b', 'aud-c'],
250-
locales: ['en-GB', 'en-US'],
251-
ages: ['20-29', '30-39'],
252-
},
248+
[DECISION_PARAMETERS_SCHEMA]: () => ({
249+
audiences: {title: 'Audiences', type: 'string', options: ['aud-a', 'aud-b', 'aud-c']},
250+
locales: {title: 'Locales', type: 'string', options: ['en-GB', 'en-US']},
251+
age: {title: 'Age', type: 'number'},
252+
gender: {title: 'Gender', type: 'string', options: ['male', 'female']},
253+
}),
253254
document: {
254255
actions: (prev, ctx) => {
255256
if (ctx.schemaType === 'book' && ctx.releaseId) {

dev/test-studio/schema/author.ts

Lines changed: 2 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -1,205 +1,8 @@
11
import {UserIcon as icon} from '@sanity/icons'
2-
import {type PreviewConfig, type StringRule} from '@sanity/types'
2+
import {type StringRule} from '@sanity/types'
33
import {defineField, defineType} from 'sanity'
44

5-
const rulePreview: PreviewConfig = {
6-
select: {
7-
property: 'property',
8-
operator: 'operator',
9-
targetValue: 'targetValue',
10-
and: 'and',
11-
},
12-
prepare(context) {
13-
const property = context.property
14-
const operator = context.operator
15-
const targetValue = context.targetValue
16-
const and = context.and as Rule[]
17-
return {
18-
title: `${property} ${operator} ${targetValue} ${and ? `& ${and.map((a) => `${a.property} ${a.operator} ${a.targetValue}`).join(' & ')}` : ''}`,
19-
}
20-
},
21-
}
22-
const stringRule = defineField({
23-
name: 'stringRule',
24-
title: 'String Rule',
25-
type: 'object',
26-
fields: [
27-
defineField({
28-
name: 'property',
29-
title: 'Property',
30-
type: 'string',
31-
options: {
32-
// User configurable list
33-
list: ['audience', 'language'],
34-
},
35-
}),
36-
defineField({
37-
name: 'operator',
38-
title: 'Operator',
39-
type: 'string',
40-
options: {
41-
list: [
42-
{title: 'is equal to', value: 'equals'},
43-
{title: 'is not equal to', value: 'not-equals'},
44-
{title: 'contains', value: 'contains'},
45-
{title: 'does not contain', value: 'not-contains'},
46-
{title: 'is empty', value: 'is-empty'},
47-
{title: 'is not empty', value: 'is-not-empty'},
48-
],
49-
},
50-
}),
51-
defineField({
52-
name: 'targetValue',
53-
title: 'Target Value',
54-
type: 'string',
55-
// components: {
56-
// input: AudienceSelectInput,
57-
// },
58-
}),
59-
],
60-
preview: rulePreview,
61-
})
62-
const numberRule = defineField({
63-
name: 'numberRule',
64-
title: 'Number Rule',
65-
type: 'object',
66-
preview: rulePreview,
67-
68-
fields: [
69-
defineField({
70-
name: 'property',
71-
title: 'Property',
72-
type: 'string',
73-
options: {
74-
list: ['born', 'age'],
75-
},
76-
}),
77-
defineField({
78-
name: 'operator',
79-
title: 'Operator',
80-
type: 'string',
81-
options: {
82-
list: [
83-
{title: 'is equal to', value: 'equals'},
84-
{title: 'is not equal to', value: 'not-equals'},
85-
{title: 'is empty', value: 'is-empty'},
86-
{title: 'is not empty', value: 'is-not-empty'},
87-
{title: 'is greater than', value: '>'},
88-
{title: 'is less than', value: '<'},
89-
{title: 'is greater than or equal to', value: '>='},
90-
{title: 'is less than or equal to', value: '<='},
91-
],
92-
},
93-
}),
94-
defineField({
95-
name: 'targetValue',
96-
title: 'Target Value',
97-
type: 'number',
98-
}),
99-
],
100-
})
101-
102-
interface Rule {
103-
property: string
104-
operator: string
105-
targetValue: string
106-
and?: Rule[]
107-
}
108-
109-
// Generic decide field implementation that works for all types
110-
const defineLocalDecideField = (config: any) => {
111-
const {name, title, description, type, ...otherConfig} = config
112-
113-
const valueFieldConfig = {
114-
type,
115-
// ...(to && {to}),
116-
// ...(validation && {validation}),
117-
// ...(description && {description}),
118-
// ...(readOnly && {readOnly}),
119-
// ...(hidden && {hidden}),
120-
...otherConfig,
121-
}
122-
123-
return defineField({
124-
name,
125-
title,
126-
description,
127-
type: 'object',
128-
fields: [
129-
defineField({
130-
name: 'default',
131-
title: 'Default Value',
132-
...valueFieldConfig,
133-
}),
134-
defineField({
135-
name: 'conditions',
136-
title: 'Conditions',
137-
type: 'array',
138-
of: [
139-
defineField({
140-
type: 'object',
141-
name: 'condition',
142-
title: 'Condition',
143-
preview: {
144-
select: {
145-
rules: 'anyOf',
146-
value: 'value',
147-
},
148-
prepare(context) {
149-
const value = context.value
150-
const rules = context.rules as Rule[]
151-
152-
return {
153-
title: value,
154-
subtitle: `${rules.map((rule) => `${rule.property} ${rule.operator} ${rule.targetValue} ${rule.and ? `& ${rule.and.map((and) => `${and.property} ${and.operator} ${and.targetValue}`).join(' & ')}` : ''}`).join(' | ')}`,
155-
}
156-
},
157-
},
158-
fields: [
159-
defineField({
160-
name: 'anyOf',
161-
title: 'Any of',
162-
description: 'If any of the rules are true, the condition is true',
163-
type: 'array',
164-
of: [
165-
{
166-
...stringRule,
167-
fields: [
168-
...stringRule.fields,
169-
defineField({
170-
name: 'and',
171-
title: 'And',
172-
type: 'array',
173-
of: [stringRule, numberRule],
174-
}),
175-
],
176-
},
177-
{
178-
...numberRule,
179-
fields: [
180-
...numberRule.fields,
181-
defineField({
182-
name: 'and',
183-
title: 'And',
184-
type: 'array',
185-
of: [stringRule, numberRule],
186-
}),
187-
],
188-
},
189-
],
190-
}),
191-
defineField({
192-
name: 'value',
193-
title: 'Value',
194-
...valueFieldConfig,
195-
}),
196-
],
197-
}),
198-
],
199-
}),
200-
],
201-
})
202-
}
5+
import {defineLocalDecideField} from './decideSchema/decideSchema'
2036

2047
const AUTHOR_ROLES = [
2058
{value: 'developer', title: 'Developer'},
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
DECISION_PARAMETERS_SCHEMA,
3+
type StringInputProps,
4+
type TitledListValue,
5+
useFormValue,
6+
useWorkspace,
7+
} from 'sanity'
8+
9+
const stringOperators: TitledListValue<string>[] = [
10+
{title: 'is equal to', value: 'equals'},
11+
{title: 'is not equal to', value: 'not-equals'},
12+
{title: 'contains', value: 'contains'},
13+
{title: 'does not contain', value: 'not-contains'},
14+
{title: 'is empty', value: 'is-empty'},
15+
{title: 'is not empty', value: 'is-not-empty'},
16+
]
17+
18+
const numberOperators: TitledListValue<string>[] = [
19+
{title: 'is equal to', value: 'equals'},
20+
{title: 'is not equal to', value: 'not-equals'},
21+
{title: 'is empty', value: 'is-empty'},
22+
{title: 'is not empty', value: 'is-not-empty'},
23+
{title: 'is greater than', value: '>'},
24+
{title: 'is less than', value: '<'},
25+
{title: 'is greater than or equal to', value: '>='},
26+
{title: 'is less than or equal to', value: '<='},
27+
]
28+
29+
/**
30+
* Custom input component for operator selection that reads from sanity.config and
31+
* returns a list of operators according to the property type
32+
*/
33+
export function OperatorSelectInput(props: StringInputProps) {
34+
const propertyValue = useFormValue(props.path.slice(0, -1).concat('property')) as string
35+
const decisionParametersConfig = useWorkspace().__internal.options[DECISION_PARAMETERS_SCHEMA]
36+
const decisionParameters = decisionParametersConfig ? decisionParametersConfig() : undefined
37+
const propertyType = decisionParameters?.[propertyValue]?.type
38+
const operators = propertyType === 'number' ? numberOperators : stringOperators
39+
40+
return props.renderDefault({
41+
...props,
42+
schemaType: {
43+
...props.schemaType,
44+
options: {
45+
list: operators,
46+
},
47+
},
48+
})
49+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
DECISION_PARAMETERS_SCHEMA,
3+
type StringInputProps,
4+
type TitledListValue,
5+
useWorkspace,
6+
} from 'sanity'
7+
8+
/**
9+
* Custom input component for property selection that reads from sanity.config and
10+
* returns a list of properties with their titles
11+
*/
12+
export function PropertySelectInput(props: StringInputProps) {
13+
const decisionParametersConfig = useWorkspace().__internal.options[DECISION_PARAMETERS_SCHEMA]
14+
const decisionParameters = decisionParametersConfig ? decisionParametersConfig() : undefined
15+
16+
const properties = decisionParameters ? Object.keys(decisionParameters) : []
17+
18+
const optionsList = properties
19+
?.map((property) => {
20+
const decisionParameter = decisionParameters?.[property]
21+
if (!decisionParameter) {
22+
return null
23+
}
24+
return {
25+
title: decisionParameter.title || property,
26+
value: property,
27+
}
28+
})
29+
.filter(Boolean) as TitledListValue<string>[]
30+
31+
return props.renderDefault({
32+
...props,
33+
schemaType: {
34+
...props.schemaType,
35+
options: {
36+
list: optionsList,
37+
},
38+
},
39+
})
40+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {DECISION_PARAMETERS_SCHEMA, type StringInputProps, useFormValue, useWorkspace} from 'sanity'
2+
3+
/**
4+
* Custom input component for target value that reads from sanity.config and
5+
* returns a list of target values with their titles or a plain string input
6+
*/
7+
export function TargetValueInput(props: StringInputProps) {
8+
const propertyValue = useFormValue(props.path.slice(0, -1).concat('property')) as string
9+
const decisionParametersConfig = useWorkspace().__internal.options[DECISION_PARAMETERS_SCHEMA]
10+
const decisionParameters = decisionParametersConfig ? decisionParametersConfig() : undefined
11+
const targetValues = decisionParameters?.[propertyValue]?.options
12+
13+
return props.renderDefault({
14+
...props,
15+
schemaType: {
16+
...props.schemaType,
17+
options: targetValues
18+
? {
19+
list: targetValues,
20+
}
21+
: undefined,
22+
},
23+
})
24+
}

0 commit comments

Comments
 (0)