Skip to content

Commit e9f9c8d

Browse files
authored
Merge pull request #49 from rezo-labs/feature/v1.8.0
v1.8.0
2 parents ce9523f + 47c97d7 commit e9f9c8d

File tree

6 files changed

+194
-26
lines changed

6 files changed

+194
-26
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ npm i directus-extension-computed-interface
1818

1919
# Get Started
2020
1. Go to **Settings**, create a new field with type string or number.
21-
2. In the **Interface** panel, choose **Computed** interface. There are 5 options:
21+
2. In the **Interface** panel, choose **Computed** interface. There are 8 options:
2222
1. **Template**: Similar to M2M interface, determine how the field is calculated. Learn more about syntax in the next section.
2323
2. **Field Mode**: Choose how the value is displayed.
2424
- **null**: Default option. Show an input with the computed value but still allow manual editing.
@@ -27,6 +27,9 @@ npm i directus-extension-computed-interface
2727
3. **Prefix**: a string to prefix the computed value.
2828
4. **Suffix**: a string to suffix the computed value.
2929
5. **Custom CSS**: an object for inline style binding. Only works with **Display Only** and **Read Only** mode. You can use this option to customize the appearance of the computed value such as font size, color, etc.
30+
6. **Debug Mode**: Used for debugging the template. It will show an error message if the template is invalid. It will also log to console the result of each component of the template.
31+
7. **Compute If Empty**: Compute the value if the field is empty. This is useful if you want a value to be computed once such as the created date or a unique ID.
32+
8. **Initial Compute**: Compute the value when opening the form. This is useful if you want to compute a value based on the current date or other dynamic values.
3033

3134
# Syntax
3235

@@ -171,13 +174,21 @@ Operator | Description
171174

172175
Operator | Description
173176
--- | ---
174-
`ASUM(a, b)` | Aggregated sum of O2M field. For example: calculate shopping cart total price with `ASUM(products, MULTIPLY(price, quantity))` where `products` is the O2M field in the shopping cart and `price` & `quantity` are 2 fields of `products`.
177+
`ASUM(a, b)` | Aggregated sum of O2M field. For example: calculate shopping cart total price with `ASUM(products, MULTIPLY(price, quantity))`, where `products` is the O2M field in the shopping cart and `price` & `quantity` are 2 fields of `products`.
178+
`AMIN(a, b)` | Aggregated min of O2M field.
179+
`AMAX(a, b)` | Aggregated max of O2M field.
180+
`AAVG(a, b)` | Aggregated average of O2M field.
181+
`AMUL(a, b)` | Aggregated multiplication of O2M field.
182+
`AAND(a, b)` | Aggregated logical AND of O2M field. Only return `true` if all values are `true`.
183+
`AOR(a, b)` | Aggregated logical OR of O2M field. Only return `true` if at least one value is `true`.
184+
`ACOUNT(a, b)` | Aggregated count of O2M field. Only count true values. For example: count the number of products that are in stock with `ACOUNT(products, GT(stock, 0))`, where `stock` is a field of `products`.
175185

176186
### Condition
177187

178188
Operator | Description
179189
--- | ---
180190
`IF(A, B, C)` | return `B` if `A` is `true`, otherwise `C`
191+
`IFS(A1, B1, A2, B2, ..., An, Bn)` | return `Bi` if `Ai` is the first to be `true`, if none of `Ai` is `true`, return `null`
181192

182193
## Dynamic Variables
183194

src/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,35 @@ export default defineInterface({
7171
},
7272
},
7373
},
74+
{
75+
field: 'debugMode',
76+
name: 'Debug Mode',
77+
type: 'boolean',
78+
meta: {
79+
width: 'full',
80+
interface: 'boolean',
81+
note: 'Used for debugging the template. It will show an error message if the template is invalid. It will also log to console the result of each component of the template.',
82+
},
83+
},
84+
{
85+
field: 'computeIfEmpty',
86+
name: 'Compute If Empty',
87+
type: 'boolean',
88+
meta: {
89+
width: 'full',
90+
interface: 'boolean',
91+
note: 'Compute the value if the field is empty. This is useful if you want a value to be computed once such as the created date or a unique ID.',
92+
},
93+
},
94+
{
95+
field: 'initialCompute',
96+
name: 'Initial Compute',
97+
type: 'boolean',
98+
meta: {
99+
width: 'full',
100+
interface: 'boolean',
101+
note: 'Compute the value when opening the form. This is useful if you want to compute a value based on the current date or other dynamic values.',
102+
},
103+
},
74104
],
75105
});

src/interface.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ export default defineComponent({
6464
type: Object,
6565
default: null,
6666
},
67+
debugMode: {
68+
type: Boolean,
69+
default: false,
70+
},
71+
computeIfEmpty: {
72+
type: Boolean,
73+
default: false,
74+
},
75+
initialCompute: {
76+
type: Boolean,
77+
default: false,
78+
},
6779
},
6880
emits: ['input'],
6981
setup(props, { emit }) {
@@ -93,6 +105,9 @@ export default defineComponent({
93105
emit('input', newValue);
94106
}, 1);
95107
}
108+
}, {
109+
immediate: props.initialCompute ||
110+
(props.computeIfEmpty && (props.value === null || props.value === undefined)),
96111
});
97112
}
98113
@@ -105,7 +120,7 @@ export default defineComponent({
105120
try {
106121
const res = props.template.replace(/{{.*?}}/g, (match) => {
107122
const expression = match.slice(2, -2).trim();
108-
return parseExpression(expression, values.value, defaultValues.value);
123+
return parseExpression(expression, values.value, defaultValues.value, props.debugMode);
109124
});
110125
111126
errorMsg.value = null;

src/operations.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,46 @@ describe('Test parseExpression', () => {
334334
expect(parseExpression('ASUM(a, b)', { a: [{b: 5}, {b: 10}, {b: 0}, {b: 15}] })).toBe(30);
335335
expect(parseExpression('ASUM(a, MULTIPLY(b, c))', { a: [{b: 5, c: 1}, {b: 10, c: 2}, {b: 1000, c: 0}, {b: 15, c: 10}] })).toBe(175);
336336
});
337+
338+
test('AMIN op', () => {
339+
expect(parseExpression('AMIN(a, b)', { a: [{b: 5}, {b: 10}, {b: -5}, {b: 15}] })).toBe(-5);
340+
expect(parseExpression('AMIN(a, SUM(b, c))', { a: [{b: 5, c: -5}, {b: 10, c: 0}, {b: -5, c: 5}, {b: 15, c: -30}] })).toBe(-15);
341+
});
342+
343+
test('AMAX op', () => {
344+
expect(parseExpression('AMAX(a, b)', { a: [{b: 5}, {b: 10}, {b: -5}, {b: 15}] })).toBe(15);
345+
expect(parseExpression('AMAX(a, SUM(b, c))', { a: [{b: 5, c: -5}, {b: 10, c: 0}, {b: -5, c: 5}, {b: 15, c: -30}] })).toBe(10);
346+
});
347+
348+
test('AAVG op', () => {
349+
expect(parseExpression('AAVG(a, b)', { a: [{b: 5}, {b: 10}, {b: 0}, {b: 15}] })).toBe(7.5);
350+
expect(parseExpression('AAVG(a, SUM(b, c))', { a: [{b: 5, c: -5}, {b: 10, c: 0}, {b: -5, c: 5}, {b: 15, c: -30}] })).toBe(-1.25);
351+
});
352+
353+
test('AMUL op', () => {
354+
expect(parseExpression('AMUL(a, b)', { a: [{b: 5}, {b: 10}, {b: 1}, {b: 15}] })).toBe(750);
355+
expect(parseExpression('AMUL(a, SUM(b, c))', { a: [{b: 10, c: 0}, {b: 15, c: -30}] })).toBe(-150);
356+
});
357+
358+
test('AAND op', () => {
359+
expect(parseExpression('AAND(a, b)', { a: [{b: true}, {b: true}, {b: true}, {b: true}] })).toBe(true);
360+
expect(parseExpression('AAND(a, b)', { a: [{b: true}, {b: true}, {b: false}, {b: true}] })).toBe(false);
361+
expect(parseExpression('AAND(a, GT(b, 0))', { a: [{b: 1}, {b: 2}, {b: 3}, {b: 4}] })).toBe(true);
362+
expect(parseExpression('AAND(a, GT(b, 2))', { a: [{b: 1}, {b: 2}, {b: 3}, {b: 4}] })).toBe(false);
363+
});
364+
365+
test('AOR op', () => {
366+
expect(parseExpression('AOR(a, b)', { a: [{b: false}, {b: false}, {b: false}, {b: false}] })).toBe(false);
367+
expect(parseExpression('AOR(a, b)', { a: [{b: false}, {b: false}, {b: true}, {b: false}] })).toBe(true);
368+
expect(parseExpression('AOR(a, EQUAL(b, 1))', { a: [{b: 1}, {b: 2}, {b: 3}, {b: 4}] })).toBe(true);
369+
expect(parseExpression('AOR(a, EQUAL(b, 0))', { a: [{b: 1}, {b: 2}, {b: 3}, {b: 4}] })).toBe(false);
370+
});
371+
372+
test('ACOUNT op', () => {
373+
expect(parseExpression('ACOUNT(a, b)', { a: [{b: 1}, {b: 2}, {b: 3}, {b: 4}] })).toBe(4);
374+
expect(parseExpression('ACOUNT(a, GT(b, 1))', { a: [{b: 1}, {b: 2}, {b: 3}, {b: 4}] })).toBe(3);
375+
expect(parseExpression('ACOUNT(a, LTE(b, 2))', { a: [{b: 1}, {b: 2}, {b: 3}, {b: 4}] })).toBe(2);
376+
});
337377
});
338378

339379
describe('Condition ops', () => {
@@ -347,6 +387,16 @@ describe('Test parseExpression', () => {
347387
expect(parseExpression('IF(EQUAL(a, 5), b, c)', { a: 5, b: 1, c: 2})).toBe(1);
348388
expect(parseExpression('IF(AND(GT(a, 0), LT(a, 10)), b, c)', { a: 5, b: 1, c: 2})).toBe(1);
349389
});
390+
391+
test('IFS op', () => {
392+
expect(parseExpression('IFS(a, b, c, d)', { a: true, b: 1, c: true, d: 2})).toBe(1);
393+
expect(parseExpression('IFS(a, b, c, d)', { a: true, b: 1, c: false, d: 2})).toBe(1);
394+
expect(parseExpression('IFS(a, b, c, d)', { a: false, b: 1, c: true, d: 2})).toBe(2);
395+
expect(parseExpression('IFS(a, b, c, d)', { a: false, b: 1, c: false, d: 2})).toBe(null);
396+
expect(parseExpression('IFS(a, b, c, d, e, f)', { a: true, b: 1, c: true, d: 2, e: true, f: 3})).toBe(1);
397+
expect(parseExpression('IFS(a, b, c, d, e, f)', { a: false, b: 1, c: true, d: 2, e: true, f: 3})).toBe(2);
398+
expect(parseExpression('IFS(a, b, c, d, e, f)', { a: false, b: 1, c: false, d: 2, e: true, f: 3})).toBe(3);
399+
});
350400
});
351401

352402
describe('Nested expressions', () => {

src/operations.ts

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import { findValueByPath } from './utils';
22

3-
export function parseExpression(exp: string, values: Record<string, any>, defaultValues: Record<string, any> = {}): any {
3+
export function parseExpression(
4+
exp: string,
5+
values: Record<string, any>,
6+
defaultValues: Record<string, any> = {},
7+
debug: boolean = false,
8+
): any {
9+
const result = _parseExpression(exp, values, defaultValues, debug);
10+
if (debug) {
11+
console.log(`${exp} =`, result);
12+
}
13+
14+
return result;
15+
}
16+
17+
function _parseExpression(
18+
exp: string,
19+
values: Record<string, any>,
20+
defaultValues: Record<string, any> = {},
21+
debug: boolean = false,
22+
): any {
423
if (values) {
524
exp = exp.trim();
625

@@ -39,7 +58,7 @@ export function parseExpression(exp: string, values: Record<string, any>, defaul
3958

4059
// unary operators
4160
if (args.length === 1) {
42-
const valueA = parseExpression(args[0], values, defaultValues);
61+
const valueA = parseExpression(args[0], values, defaultValues, debug);
4362
// type conversion
4463
if (op === 'INT') {
4564
return parseInt(valueA);
@@ -178,13 +197,45 @@ export function parseExpression(exp: string, values: Record<string, any>, defaul
178197
}
179198
return 0;
180199
}
181-
} else if (op === 'ASUM' && args.length === 2) {
182-
// aggregated sum
183-
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc + parseExpression(args[1], item as typeof values, {}), 0) ?? 0;
184200
} else if (args.length === 2) {
201+
// aggregated operators
202+
if (op === 'ASUM') {
203+
// aggregated sum
204+
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc + parseExpression(args[1], item as typeof values, {}, debug), 0) ?? 0;
205+
}
206+
if (op === 'AMIN') {
207+
// aggregated min
208+
return (values[args[0]] as unknown[])?.reduce((acc, item) => Math.min(acc, parseExpression(args[1], item as typeof values, {}, debug)), Infinity) ?? 0;
209+
}
210+
if (op === 'AMAX') {
211+
// aggregated max
212+
return (values[args[0]] as unknown[])?.reduce((acc, item) => Math.max(acc, parseExpression(args[1], item as typeof values, {}, debug)), -Infinity) ?? 0;
213+
}
214+
if (op === 'AAVG') {
215+
// aggregated average
216+
const arr = (values[args[0]] as unknown[]) ?? [];
217+
return arr.reduce((acc, item) => acc + parseExpression(args[1], item as typeof values, {}, debug), 0) / arr.length;
218+
}
219+
if (op === 'AMUL') {
220+
// aggregated multiplication
221+
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc * parseExpression(args[1], item as typeof values, {}, debug), 1) ?? 0;
222+
}
223+
if (op === 'AAND') {
224+
// aggregated and
225+
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc && parseExpression(args[1], item as typeof values, {}, debug), true) ?? false;
226+
}
227+
if (op === 'AOR') {
228+
// aggregated or
229+
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc || parseExpression(args[1], item as typeof values, {}, debug), false) ?? false;
230+
}
231+
if (op === 'ACOUNT') {
232+
// aggregated count
233+
return (values[args[0]] as unknown[])?.reduce((acc, item) => acc + (parseExpression(args[1], item as typeof values, {}, debug) ? 1 : 0), 0) ?? 0;
234+
}
235+
185236
// binary operators
186-
const valueA = parseExpression(args[0], values, defaultValues);
187-
const valueB = parseExpression(args[1], values, defaultValues);
237+
const valueA = parseExpression(args[0], values, defaultValues, debug);
238+
const valueB = parseExpression(args[1], values, defaultValues, debug);
188239

189240
// arithmetic
190241
if (op === 'SUM') {
@@ -237,8 +288,8 @@ export function parseExpression(exp: string, values: Record<string, any>, defaul
237288
return String(valueA).split(String(valueB));
238289
}
239290
if (op === 'SEARCH') {
240-
const str = String(parseExpression(args[0], values, defaultValues));
241-
const find = String(parseExpression(args[1], values, defaultValues));
291+
const str = String(valueA);
292+
const find = String(valueB);
242293
return str.indexOf(find);
243294
}
244295
// boolean
@@ -268,29 +319,38 @@ export function parseExpression(exp: string, values: Record<string, any>, defaul
268319
}
269320
} else if (args.length === 3) {
270321
if (op === 'IF') {
271-
if (parseExpression(args[0], values, defaultValues) === true) {
272-
return parseExpression(args[1], values, defaultValues);
322+
if (parseExpression(args[0], values, defaultValues, debug) === true) {
323+
return parseExpression(args[1], values, defaultValues, debug);
273324
}
274-
return parseExpression(args[2], values, defaultValues);
325+
return parseExpression(args[2], values, defaultValues, debug);
275326
}
276327
if (op === 'MID') {
277-
const str = String(parseExpression(args[0], values, defaultValues));
278-
const startAt = Number(parseExpression(args[1], values, defaultValues));
279-
const count = Number(parseExpression(args[2], values, defaultValues));
328+
const str = String(parseExpression(args[0], values, defaultValues, debug));
329+
const startAt = Number(parseExpression(args[1], values, defaultValues, debug));
330+
const count = Number(parseExpression(args[2], values, defaultValues, debug));
280331
return str.slice(startAt, startAt + count);
281332
}
282333
if (op === 'SUBSTITUTE') {
283-
const str = String(parseExpression(args[0], values, defaultValues));
284-
const old = String(parseExpression(args[1], values, defaultValues));
285-
const newStr = String(parseExpression(args[2], values, defaultValues));
334+
const str = String(parseExpression(args[0], values, defaultValues, debug));
335+
const old = String(parseExpression(args[1], values, defaultValues, debug));
336+
const newStr = String(parseExpression(args[2], values, defaultValues, debug));
286337
return str.split(old).join(newStr);
287338
}
288339
if (op === 'SEARCH') {
289-
const str = String(parseExpression(args[0], values, defaultValues));
290-
const find = String(parseExpression(args[1], values, defaultValues));
291-
const startAt = Number(parseExpression(args[2], values, defaultValues));
340+
const str = String(parseExpression(args[0], values, defaultValues, debug));
341+
const find = String(parseExpression(args[1], values, defaultValues, debug));
342+
const startAt = Number(parseExpression(args[2], values, defaultValues, debug));
292343
return str.indexOf(find, startAt);
293344
}
345+
} else if (args.length % 2 === 0) {
346+
if (op === 'IFS') {
347+
for (let i = 0; i < args.length; i += 2) {
348+
if (parseExpression(args[i], values, defaultValues, debug) === true) {
349+
return parseExpression(args[i + 1], values, defaultValues, debug);
350+
}
351+
}
352+
return null;
353+
}
294354
}
295355
}
296356

@@ -299,7 +359,9 @@ export function parseExpression(exp: string, values: Record<string, any>, defaul
299359
return parseFloat(exp);
300360
}
301361

302-
throw new Error(`Cannot parse expression: ${exp}`);
362+
if (debug) {
363+
throw new Error(`Cannot parse expression: ${exp}`);
364+
}
303365
}
304366
return '';
305367
}

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export const useDeepValues = (
167167
data = arrayOfIds.map(id => itemCache[relatedCollection][id]);
168168
} else {
169169
data = (await api.get(path, {
170-
params: { filter: { id: { _in: arrayOfIds } } },
170+
params: { filter: { id: { _in: arrayOfIds.join(',') } } },
171171
})).data.data;
172172
}
173173

0 commit comments

Comments
 (0)