Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07ca566
feat: allow selection of parent level in field condition via `parent`…
florianbrinkmann Jan 24, 2024
901983b
Merge branch '4.x' into 783-add-possibility-to-access-parent-field-in…
florianbrinkmann Jan 24, 2024
f14c44b
Merge branch '5.x' into pr/9385
duncanmcclean May 14, 2024
2adc37b
Merge branch '5.x' into feature/add-parent-keyword-to-field-conditions
jasonvarga Jul 29, 2024
06d0515
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 16, 2024
ad02cbf
Port #9960’s `fieldPath` param into this PR.
jesseleite Aug 16, 2024
aad3289
Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow …
jesseleite Aug 16, 2024
c5a4d84
Update blueprint converter as well.
jesseleite Aug 16, 2024
800e6d5
Add test coverage and fix revealer issue.
jesseleite Aug 16, 2024
b40e21c
Technically these should match, though it doesn’t matter for tests.
jesseleite Aug 16, 2024
211840d
Escape dot.
jesseleite Aug 16, 2024
5d633ce
Add failing test for handing of `$parent.` in revealer condition logic.
jesseleite Aug 16, 2024
5c24c8c
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 21, 2024
c9f59d7
Kick test.
jesseleite Aug 21, 2024
b4e8de6
Unkick test.
jesseleite Sep 24, 2024
3f5a65a
Add failing test for `$parent` at multiple levels in values payload.
jesseleite Sep 26, 2024
d234df2
Extract and fix `$parent` resolving logic to pass test.
jesseleite Sep 26, 2024
d6e9068
These get treated as regex by my test runner, just word it better.
jesseleite Sep 26, 2024
b4a5315
Actually pass failing tests.
jesseleite Sep 26, 2024
7195c9d
Revealer tests wip.
jesseleite Sep 26, 2024
52bb8ae
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Sep 26, 2024
92d1b5a
Flesh out `$parent` handling and test coverage (WIP).
jesseleite Sep 26, 2024
4b82755
Avoid regex symbols in test names for test runners.
jesseleite Sep 27, 2024
d5527b0
Okay, rip out all this in favour of `$parent` to `$root` resolving idea.
jesseleite Sep 27, 2024
bd82563
Spec it all out with unit tests.
jesseleite Sep 27, 2024
690ffa5
Make it all work for real.
jesseleite Sep 27, 2024
0e4f6f2
Resolve `$parent` to absolute `$root` field path (since that already …
jesseleite Sep 27, 2024
c21f55a
Now this all just works! 🎉🎉🎉
jesseleite Sep 27, 2024
5ea49a5
Flesh this out a little more to cover douple replicator with multiple…
jesseleite Sep 27, 2024
d3cc919
Failing test to show data loss because of improper `revealer` hidden …
jesseleite Sep 27, 2024
df8bf21
Prevent data loss with `$parent` syntax on revealers & pass failing t…
jesseleite Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions resources/js/components/field-conditions/Converter.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,11 +32,17 @@ export default class {
}

getScopedFieldHandle(field, prefix) {
if (field.startsWith('root.') || ! prefix) {
if (field.startsWith('$root.') || field.startsWith('root.')) {
return field;
}

return prefix + field;
if (field.startsWith('$parent.')) {
return field;
}

return prefix
? prefix + field
: field;
}

getOperatorFromRhs(condition) {
Expand Down
42 changes: 42 additions & 0 deletions resources/js/components/field-conditions/ParentResolver.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
export default class {
constructor(currentFieldPath) {
this.currentFieldPath = currentFieldPath;
}

resolve(pathWithParent) {
let parentPath = this.getParentFieldPath(this.currentFieldPath, true);
let fieldPath = this.removeOneParentKeyword(pathWithParent);

while (fieldPath.startsWith('$parent.')) {
parentPath = this.getParentFieldPath(parentPath);
fieldPath = this.removeOneParentKeyword(fieldPath);
}

let resolved = parentPath
? `${parentPath}.${fieldPath}`
: fieldPath;

return `$root.${resolved}`;
}

getParentFieldPath(dottedFieldPath, removeCurrentField) {
const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$');

if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) {
dottedFieldPath = dottedFieldPath.replace(regex, '$1');
}

return dottedFieldPath.includes('.')
? dottedFieldPath.replace(regex, '$1$2')
: '';
}

isAtSetLevel(dottedFieldPath) {
return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$'));
}

removeOneParentKeyword(dottedFieldPath) {
return dottedFieldPath.replace(new RegExp('^\\$parent.'), '');
}

}
33 changes: 23 additions & 10 deletions resources/js/components/field-conditions/Validator.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Converter from './Converter.js';
import ParentResolver from './ParentResolver.js';
import { KEYS } from './Constants.js';
import { data_get } from '../../bootstrap/globals.js'
import isString from 'underscore/modules/isString.js'
Expand All@@ -20,12 +21,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [
];

export default class {
constructor(field, values, store, storeName) {
constructor(field, values, dottedFieldPath, store, storeName) {
this.field = field;
this.values = values;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.dottedFieldPath = dottedFieldPath;
this.store = store;
this.storeName = storeName;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter;
Expand DownExpand Up@@ -204,9 +206,15 @@ export default class {
}

getFieldValue(field) {
return field.startsWith('root.')
? data_get(this.rootValues, field.replace(new RegExp('^root.'), ''))
: data_get(this.values, field);
if (field.startsWith('$parent.')) {
field = new ParentResolver(this.dottedFieldPath).resolve(field);
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
}

return data_get(this.values, field);
}

passesCondition(condition) {
Expand DownExpand Up@@ -264,6 +272,7 @@ export default class {
root: this.rootValues,
store: this.store,
storeName: this.storeName,
fieldPath: this.dottedFieldPath,
});

return this.showOnPass ? passes : ! passes;
Expand All@@ -286,12 +295,16 @@ export default class {
}

relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) {
if (! dottedPrefix) {
return lhs;
if (lhs.startsWith('$parent.')) {
lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs);
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
}

return lhs.startsWith('root.')
? lhs.replace(/^root\./, '')
: dottedPrefix + '.' + lhs;
return dottedPrefix
? dottedPrefix + '.' + lhs
: lhs;
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ export default {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, this.values, this.$store, this.storeName);
let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand Down
30 changes: 29 additions & 1 deletion resources/js/tests/FieldConditionsConverter.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => {
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to root field conditions', () => {
test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$root.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$root.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'root.title': 'not empty',
Expand All@@ -49,6 +63,20 @@ test('it converts from blueprint format and does not apply prefix to root field
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$parent.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$parent.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts to blueprint format', () => {
let converted = FieldConditionsConverter.toBlueprint([
{field: 'name', operator: 'isnt', value: 'joe'},
Expand Down
68 changes: 68 additions & 0 deletions resources/js/tests/FieldConditionsParentResolver.test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import ParentResolver from '../components/field-conditions/ParentResolver.js';

let resolve = function (currentFieldPath, pathWithParent) {
return new ParentResolver(currentFieldPath).resolve(pathWithParent);
}

test('it resolves from group to top level', () => {
expect(resolve('group.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from set/row to top level', () => {
expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from nested group to parent group', () => {
expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from nested set to parent set', () => {
expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name');
expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name');
expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name');
});

test('it resolves from nested group to parent set', () => {
expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name');
expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name');
});

test('it resolves from nested set to parent group', () => {
expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from deeply nested groups all the way up to top level', () => {
let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested sets all the way up to top level', () => {
let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested mix of everything all the way up to top level', () => {
let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[5.x] Add `parent` keyword to field conditions by florianbrinkmann · Pull Request #9385 · statamic/cms · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07ca566
feat: allow selection of parent level in field condition via `parent`…
florianbrinkmann Jan 24, 2024
901983b
Merge branch '4.x' into 783-add-possibility-to-access-parent-field-in…
florianbrinkmann Jan 24, 2024
f14c44b
Merge branch '5.x' into pr/9385
duncanmcclean May 14, 2024
2adc37b
Merge branch '5.x' into feature/add-parent-keyword-to-field-conditions
jasonvarga Jul 29, 2024
06d0515
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 16, 2024
ad02cbf
Port #9960’s `fieldPath` param into this PR.
jesseleite Aug 16, 2024
aad3289
Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow …
jesseleite Aug 16, 2024
c5a4d84
Update blueprint converter as well.
jesseleite Aug 16, 2024
800e6d5
Add test coverage and fix revealer issue.
jesseleite Aug 16, 2024
b40e21c
Technically these should match, though it doesn’t matter for tests.
jesseleite Aug 16, 2024
211840d
Escape dot.
jesseleite Aug 16, 2024
5d633ce
Add failing test for handing of `$parent.` in revealer condition logic.
jesseleite Aug 16, 2024
5c24c8c
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 21, 2024
c9f59d7
Kick test.
jesseleite Aug 21, 2024
b4e8de6
Unkick test.
jesseleite Sep 24, 2024
3f5a65a
Add failing test for `$parent` at multiple levels in values payload.
jesseleite Sep 26, 2024
d234df2
Extract and fix `$parent` resolving logic to pass test.
jesseleite Sep 26, 2024
d6e9068
These get treated as regex by my test runner, just word it better.
jesseleite Sep 26, 2024
b4a5315
Actually pass failing tests.
jesseleite Sep 26, 2024
7195c9d
Revealer tests wip.
jesseleite Sep 26, 2024
52bb8ae
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Sep 26, 2024
92d1b5a
Flesh out `$parent` handling and test coverage (WIP).
jesseleite Sep 26, 2024
4b82755
Avoid regex symbols in test names for test runners.
jesseleite Sep 27, 2024
d5527b0
Okay, rip out all this in favour of `$parent` to `$root` resolving idea.
jesseleite Sep 27, 2024
bd82563
Spec it all out with unit tests.
jesseleite Sep 27, 2024
690ffa5
Make it all work for real.
jesseleite Sep 27, 2024
0e4f6f2
Resolve `$parent` to absolute `$root` field path (since that already …
jesseleite Sep 27, 2024
c21f55a
Now this all just works! 🎉🎉🎉
jesseleite Sep 27, 2024
5ea49a5
Flesh this out a little more to cover douple replicator with multiple…
jesseleite Sep 27, 2024
d3cc919
Failing test to show data loss because of improper `revealer` hidden …
jesseleite Sep 27, 2024
df8bf21
Prevent data loss with `$parent` syntax on revealers & pass failing t…
jesseleite Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions resources/js/components/field-conditions/Converter.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,11 +32,17 @@ export default class {
}

getScopedFieldHandle(field, prefix) {
if (field.startsWith('root.') || ! prefix) {
if (field.startsWith('$root.') || field.startsWith('root.')) {
return field;
}

return prefix + field;
if (field.startsWith('$parent.')) {
return field;
}

return prefix
? prefix + field
: field;
}

getOperatorFromRhs(condition) {
Expand Down
42 changes: 42 additions & 0 deletions resources/js/components/field-conditions/ParentResolver.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
export default class {
constructor(currentFieldPath) {
this.currentFieldPath = currentFieldPath;
}

resolve(pathWithParent) {
let parentPath = this.getParentFieldPath(this.currentFieldPath, true);
let fieldPath = this.removeOneParentKeyword(pathWithParent);

while (fieldPath.startsWith('$parent.')) {
parentPath = this.getParentFieldPath(parentPath);
fieldPath = this.removeOneParentKeyword(fieldPath);
}

let resolved = parentPath
? `${parentPath}.${fieldPath}`
: fieldPath;

return `$root.${resolved}`;
}

getParentFieldPath(dottedFieldPath, removeCurrentField) {
const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$');

if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) {
dottedFieldPath = dottedFieldPath.replace(regex, '$1');
}

return dottedFieldPath.includes('.')
? dottedFieldPath.replace(regex, '$1$2')
: '';
}

isAtSetLevel(dottedFieldPath) {
return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$'));
}

removeOneParentKeyword(dottedFieldPath) {
return dottedFieldPath.replace(new RegExp('^\\$parent.'), '');
}

}
33 changes: 23 additions & 10 deletions resources/js/components/field-conditions/Validator.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Converter from './Converter.js';
import ParentResolver from './ParentResolver.js';
import { KEYS } from './Constants.js';
import { data_get } from '../../bootstrap/globals.js'
import isString from 'underscore/modules/isString.js'
Expand All@@ -20,12 +21,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [
];

export default class {
constructor(field, values, store, storeName) {
constructor(field, values, dottedFieldPath, store, storeName) {
this.field = field;
this.values = values;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.dottedFieldPath = dottedFieldPath;
this.store = store;
this.storeName = storeName;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter;
Expand DownExpand Up@@ -204,9 +206,15 @@ export default class {
}

getFieldValue(field) {
return field.startsWith('root.')
? data_get(this.rootValues, field.replace(new RegExp('^root.'), ''))
: data_get(this.values, field);
if (field.startsWith('$parent.')) {
field = new ParentResolver(this.dottedFieldPath).resolve(field);
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
}

return data_get(this.values, field);
}

passesCondition(condition) {
Expand DownExpand Up@@ -264,6 +272,7 @@ export default class {
root: this.rootValues,
store: this.store,
storeName: this.storeName,
fieldPath: this.dottedFieldPath,
});

return this.showOnPass ? passes : ! passes;
Expand All@@ -286,12 +295,16 @@ export default class {
}

relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) {
if (! dottedPrefix) {
return lhs;
if (lhs.startsWith('$parent.')) {
lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs);
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
}

return lhs.startsWith('root.')
? lhs.replace(/^root\./, '')
: dottedPrefix + '.' + lhs;
return dottedPrefix
? dottedPrefix + '.' + lhs
: lhs;
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ export default {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, this.values, this.$store, this.storeName);
let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand Down
30 changes: 29 additions & 1 deletion resources/js/tests/FieldConditionsConverter.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => {
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to root field conditions', () => {
test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$root.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$root.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'root.title': 'not empty',
Expand All@@ -49,6 +63,20 @@ test('it converts from blueprint format and does not apply prefix to root field
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$parent.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$parent.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts to blueprint format', () => {
let converted = FieldConditionsConverter.toBlueprint([
{field: 'name', operator: 'isnt', value: 'joe'},
Expand Down
68 changes: 68 additions & 0 deletions resources/js/tests/FieldConditionsParentResolver.test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import ParentResolver from '../components/field-conditions/ParentResolver.js';

let resolve = function (currentFieldPath, pathWithParent) {
return new ParentResolver(currentFieldPath).resolve(pathWithParent);
}

test('it resolves from group to top level', () => {
expect(resolve('group.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from set/row to top level', () => {
expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from nested group to parent group', () => {
expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from nested set to parent set', () => {
expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name');
expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name');
expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name');
});

test('it resolves from nested group to parent set', () => {
expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name');
expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name');
});

test('it resolves from nested set to parent group', () => {
expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from deeply nested groups all the way up to top level', () => {
let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested sets all the way up to top level', () => {
let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested mix of everything all the way up to top level', () => {
let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [5.x] Add `parent` keyword to field conditions by florianbrinkmann · Pull Request #9385 · statamic/cms · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07ca566
feat: allow selection of parent level in field condition via `parent`…
florianbrinkmann Jan 24, 2024
901983b
Merge branch '4.x' into 783-add-possibility-to-access-parent-field-in…
florianbrinkmann Jan 24, 2024
f14c44b
Merge branch '5.x' into pr/9385
duncanmcclean May 14, 2024
2adc37b
Merge branch '5.x' into feature/add-parent-keyword-to-field-conditions
jasonvarga Jul 29, 2024
06d0515
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 16, 2024
ad02cbf
Port #9960’s `fieldPath` param into this PR.
jesseleite Aug 16, 2024
aad3289
Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow …
jesseleite Aug 16, 2024
c5a4d84
Update blueprint converter as well.
jesseleite Aug 16, 2024
800e6d5
Add test coverage and fix revealer issue.
jesseleite Aug 16, 2024
b40e21c
Technically these should match, though it doesn’t matter for tests.
jesseleite Aug 16, 2024
211840d
Escape dot.
jesseleite Aug 16, 2024
5d633ce
Add failing test for handing of `$parent.` in revealer condition logic.
jesseleite Aug 16, 2024
5c24c8c
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 21, 2024
c9f59d7
Kick test.
jesseleite Aug 21, 2024
b4e8de6
Unkick test.
jesseleite Sep 24, 2024
3f5a65a
Add failing test for `$parent` at multiple levels in values payload.
jesseleite Sep 26, 2024
d234df2
Extract and fix `$parent` resolving logic to pass test.
jesseleite Sep 26, 2024
d6e9068
These get treated as regex by my test runner, just word it better.
jesseleite Sep 26, 2024
b4a5315
Actually pass failing tests.
jesseleite Sep 26, 2024
7195c9d
Revealer tests wip.
jesseleite Sep 26, 2024
52bb8ae
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Sep 26, 2024
92d1b5a
Flesh out `$parent` handling and test coverage (WIP).
jesseleite Sep 26, 2024
4b82755
Avoid regex symbols in test names for test runners.
jesseleite Sep 27, 2024
d5527b0
Okay, rip out all this in favour of `$parent` to `$root` resolving idea.
jesseleite Sep 27, 2024
bd82563
Spec it all out with unit tests.
jesseleite Sep 27, 2024
690ffa5
Make it all work for real.
jesseleite Sep 27, 2024
0e4f6f2
Resolve `$parent` to absolute `$root` field path (since that already …
jesseleite Sep 27, 2024
c21f55a
Now this all just works! 🎉🎉🎉
jesseleite Sep 27, 2024
5ea49a5
Flesh this out a little more to cover douple replicator with multiple…
jesseleite Sep 27, 2024
d3cc919
Failing test to show data loss because of improper `revealer` hidden …
jesseleite Sep 27, 2024
df8bf21
Prevent data loss with `$parent` syntax on revealers & pass failing t…
jesseleite Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions resources/js/components/field-conditions/Converter.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,11 +32,17 @@ export default class {
}

getScopedFieldHandle(field, prefix) {
if (field.startsWith('root.') || ! prefix) {
if (field.startsWith('$root.') || field.startsWith('root.')) {
return field;
}

return prefix + field;
if (field.startsWith('$parent.')) {
return field;
}

return prefix
? prefix + field
: field;
}

getOperatorFromRhs(condition) {
Expand Down
42 changes: 42 additions & 0 deletions resources/js/components/field-conditions/ParentResolver.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
export default class {
constructor(currentFieldPath) {
this.currentFieldPath = currentFieldPath;
}

resolve(pathWithParent) {
let parentPath = this.getParentFieldPath(this.currentFieldPath, true);
let fieldPath = this.removeOneParentKeyword(pathWithParent);

while (fieldPath.startsWith('$parent.')) {
parentPath = this.getParentFieldPath(parentPath);
fieldPath = this.removeOneParentKeyword(fieldPath);
}

let resolved = parentPath
? `${parentPath}.${fieldPath}`
: fieldPath;

return `$root.${resolved}`;
}

getParentFieldPath(dottedFieldPath, removeCurrentField) {
const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$');

if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) {
dottedFieldPath = dottedFieldPath.replace(regex, '$1');
}

return dottedFieldPath.includes('.')
? dottedFieldPath.replace(regex, '$1$2')
: '';
}

isAtSetLevel(dottedFieldPath) {
return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$'));
}

removeOneParentKeyword(dottedFieldPath) {
return dottedFieldPath.replace(new RegExp('^\\$parent.'), '');
}

}
33 changes: 23 additions & 10 deletions resources/js/components/field-conditions/Validator.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Converter from './Converter.js';
import ParentResolver from './ParentResolver.js';
import { KEYS } from './Constants.js';
import { data_get } from '../../bootstrap/globals.js'
import isString from 'underscore/modules/isString.js'
Expand All@@ -20,12 +21,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [
];

export default class {
constructor(field, values, store, storeName) {
constructor(field, values, dottedFieldPath, store, storeName) {
this.field = field;
this.values = values;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.dottedFieldPath = dottedFieldPath;
this.store = store;
this.storeName = storeName;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter;
Expand DownExpand Up@@ -204,9 +206,15 @@ export default class {
}

getFieldValue(field) {
return field.startsWith('root.')
? data_get(this.rootValues, field.replace(new RegExp('^root.'), ''))
: data_get(this.values, field);
if (field.startsWith('$parent.')) {
field = new ParentResolver(this.dottedFieldPath).resolve(field);
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
}

return data_get(this.values, field);
}

passesCondition(condition) {
Expand DownExpand Up@@ -264,6 +272,7 @@ export default class {
root: this.rootValues,
store: this.store,
storeName: this.storeName,
fieldPath: this.dottedFieldPath,
});

return this.showOnPass ? passes : ! passes;
Expand All@@ -286,12 +295,16 @@ export default class {
}

relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) {
if (! dottedPrefix) {
return lhs;
if (lhs.startsWith('$parent.')) {
lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs);
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
}

return lhs.startsWith('root.')
? lhs.replace(/^root\./, '')
: dottedPrefix + '.' + lhs;
return dottedPrefix
? dottedPrefix + '.' + lhs
: lhs;
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ export default {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, this.values, this.$store, this.storeName);
let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand Down
30 changes: 29 additions & 1 deletion resources/js/tests/FieldConditionsConverter.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => {
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to root field conditions', () => {
test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$root.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$root.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'root.title': 'not empty',
Expand All@@ -49,6 +63,20 @@ test('it converts from blueprint format and does not apply prefix to root field
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$parent.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$parent.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts to blueprint format', () => {
let converted = FieldConditionsConverter.toBlueprint([
{field: 'name', operator: 'isnt', value: 'joe'},
Expand Down
68 changes: 68 additions & 0 deletions resources/js/tests/FieldConditionsParentResolver.test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import ParentResolver from '../components/field-conditions/ParentResolver.js';

let resolve = function (currentFieldPath, pathWithParent) {
return new ParentResolver(currentFieldPath).resolve(pathWithParent);
}

test('it resolves from group to top level', () => {
expect(resolve('group.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from set/row to top level', () => {
expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from nested group to parent group', () => {
expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from nested set to parent set', () => {
expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name');
expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name');
expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name');
});

test('it resolves from nested group to parent set', () => {
expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name');
expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name');
});

test('it resolves from nested set to parent group', () => {
expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from deeply nested groups all the way up to top level', () => {
let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested sets all the way up to top level', () => {
let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested mix of everything all the way up to top level', () => {
let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [5.x] Add `parent` keyword to field conditions by florianbrinkmann · Pull Request #9385 · statamic/cms · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07ca566
feat: allow selection of parent level in field condition via `parent`…
florianbrinkmann Jan 24, 2024
901983b
Merge branch '4.x' into 783-add-possibility-to-access-parent-field-in…
florianbrinkmann Jan 24, 2024
f14c44b
Merge branch '5.x' into pr/9385
duncanmcclean May 14, 2024
2adc37b
Merge branch '5.x' into feature/add-parent-keyword-to-field-conditions
jasonvarga Jul 29, 2024
06d0515
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 16, 2024
ad02cbf
Port #9960’s `fieldPath` param into this PR.
jesseleite Aug 16, 2024
aad3289
Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow …
jesseleite Aug 16, 2024
c5a4d84
Update blueprint converter as well.
jesseleite Aug 16, 2024
800e6d5
Add test coverage and fix revealer issue.
jesseleite Aug 16, 2024
b40e21c
Technically these should match, though it doesn’t matter for tests.
jesseleite Aug 16, 2024
211840d
Escape dot.
jesseleite Aug 16, 2024
5d633ce
Add failing test for handing of `$parent.` in revealer condition logic.
jesseleite Aug 16, 2024
5c24c8c
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 21, 2024
c9f59d7
Kick test.
jesseleite Aug 21, 2024
b4e8de6
Unkick test.
jesseleite Sep 24, 2024
3f5a65a
Add failing test for `$parent` at multiple levels in values payload.
jesseleite Sep 26, 2024
d234df2
Extract and fix `$parent` resolving logic to pass test.
jesseleite Sep 26, 2024
d6e9068
These get treated as regex by my test runner, just word it better.
jesseleite Sep 26, 2024
b4a5315
Actually pass failing tests.
jesseleite Sep 26, 2024
7195c9d
Revealer tests wip.
jesseleite Sep 26, 2024
52bb8ae
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Sep 26, 2024
92d1b5a
Flesh out `$parent` handling and test coverage (WIP).
jesseleite Sep 26, 2024
4b82755
Avoid regex symbols in test names for test runners.
jesseleite Sep 27, 2024
d5527b0
Okay, rip out all this in favour of `$parent` to `$root` resolving idea.
jesseleite Sep 27, 2024
bd82563
Spec it all out with unit tests.
jesseleite Sep 27, 2024
690ffa5
Make it all work for real.
jesseleite Sep 27, 2024
0e4f6f2
Resolve `$parent` to absolute `$root` field path (since that already …
jesseleite Sep 27, 2024
c21f55a
Now this all just works! 🎉🎉🎉
jesseleite Sep 27, 2024
5ea49a5
Flesh this out a little more to cover douple replicator with multiple…
jesseleite Sep 27, 2024
d3cc919
Failing test to show data loss because of improper `revealer` hidden …
jesseleite Sep 27, 2024
df8bf21
Prevent data loss with `$parent` syntax on revealers & pass failing t…
jesseleite Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions resources/js/components/field-conditions/Converter.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,11 +32,17 @@ export default class {
}

getScopedFieldHandle(field, prefix) {
if (field.startsWith('root.') || ! prefix) {
if (field.startsWith('$root.') || field.startsWith('root.')) {
return field;
}

return prefix + field;
if (field.startsWith('$parent.')) {
return field;
}

return prefix
? prefix + field
: field;
}

getOperatorFromRhs(condition) {
Expand Down
42 changes: 42 additions & 0 deletions resources/js/components/field-conditions/ParentResolver.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
export default class {
constructor(currentFieldPath) {
this.currentFieldPath = currentFieldPath;
}

resolve(pathWithParent) {
let parentPath = this.getParentFieldPath(this.currentFieldPath, true);
let fieldPath = this.removeOneParentKeyword(pathWithParent);

while (fieldPath.startsWith('$parent.')) {
parentPath = this.getParentFieldPath(parentPath);
fieldPath = this.removeOneParentKeyword(fieldPath);
}

let resolved = parentPath
? `${parentPath}.${fieldPath}`
: fieldPath;

return `$root.${resolved}`;
}

getParentFieldPath(dottedFieldPath, removeCurrentField) {
const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$');

if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) {
dottedFieldPath = dottedFieldPath.replace(regex, '$1');
}

return dottedFieldPath.includes('.')
? dottedFieldPath.replace(regex, '$1$2')
: '';
}

isAtSetLevel(dottedFieldPath) {
return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$'));
}

removeOneParentKeyword(dottedFieldPath) {
return dottedFieldPath.replace(new RegExp('^\\$parent.'), '');
}

}
33 changes: 23 additions & 10 deletions resources/js/components/field-conditions/Validator.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Converter from './Converter.js';
import ParentResolver from './ParentResolver.js';
import { KEYS } from './Constants.js';
import { data_get } from '../../bootstrap/globals.js'
import isString from 'underscore/modules/isString.js'
Expand All@@ -20,12 +21,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [
];

export default class {
constructor(field, values, store, storeName) {
constructor(field, values, dottedFieldPath, store, storeName) {
this.field = field;
this.values = values;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.dottedFieldPath = dottedFieldPath;
this.store = store;
this.storeName = storeName;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter;
Expand DownExpand Up@@ -204,9 +206,15 @@ export default class {
}

getFieldValue(field) {
return field.startsWith('root.')
? data_get(this.rootValues, field.replace(new RegExp('^root.'), ''))
: data_get(this.values, field);
if (field.startsWith('$parent.')) {
field = new ParentResolver(this.dottedFieldPath).resolve(field);
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
}

return data_get(this.values, field);
}

passesCondition(condition) {
Expand DownExpand Up@@ -264,6 +272,7 @@ export default class {
root: this.rootValues,
store: this.store,
storeName: this.storeName,
fieldPath: this.dottedFieldPath,
});

return this.showOnPass ? passes : ! passes;
Expand All@@ -286,12 +295,16 @@ export default class {
}

relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) {
if (! dottedPrefix) {
return lhs;
if (lhs.startsWith('$parent.')) {
lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs);
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
}

return lhs.startsWith('root.')
? lhs.replace(/^root\./, '')
: dottedPrefix + '.' + lhs;
return dottedPrefix
? dottedPrefix + '.' + lhs
: lhs;
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ export default {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, this.values, this.$store, this.storeName);
let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand Down
30 changes: 29 additions & 1 deletion resources/js/tests/FieldConditionsConverter.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => {
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to root field conditions', () => {
test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$root.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$root.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'root.title': 'not empty',
Expand All@@ -49,6 +63,20 @@ test('it converts from blueprint format and does not apply prefix to root field
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$parent.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$parent.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts to blueprint format', () => {
let converted = FieldConditionsConverter.toBlueprint([
{field: 'name', operator: 'isnt', value: 'joe'},
Expand Down
68 changes: 68 additions & 0 deletions resources/js/tests/FieldConditionsParentResolver.test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import ParentResolver from '../components/field-conditions/ParentResolver.js';

let resolve = function (currentFieldPath, pathWithParent) {
return new ParentResolver(currentFieldPath).resolve(pathWithParent);
}

test('it resolves from group to top level', () => {
expect(resolve('group.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from set/row to top level', () => {
expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from nested group to parent group', () => {
expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from nested set to parent set', () => {
expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name');
expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name');
expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name');
});

test('it resolves from nested group to parent set', () => {
expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name');
expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name');
});

test('it resolves from nested set to parent group', () => {
expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from deeply nested groups all the way up to top level', () => {
let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested sets all the way up to top level', () => {
let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested mix of everything all the way up to top level', () => {
let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [5.x] Add `parent` keyword to field conditions by florianbrinkmann · Pull Request #9385 · statamic/cms · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07ca566
feat: allow selection of parent level in field condition via `parent`…
florianbrinkmann Jan 24, 2024
901983b
Merge branch '4.x' into 783-add-possibility-to-access-parent-field-in…
florianbrinkmann Jan 24, 2024
f14c44b
Merge branch '5.x' into pr/9385
duncanmcclean May 14, 2024
2adc37b
Merge branch '5.x' into feature/add-parent-keyword-to-field-conditions
jasonvarga Jul 29, 2024
06d0515
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 16, 2024
ad02cbf
Port #9960’s `fieldPath` param into this PR.
jesseleite Aug 16, 2024
aad3289
Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow …
jesseleite Aug 16, 2024
c5a4d84
Update blueprint converter as well.
jesseleite Aug 16, 2024
800e6d5
Add test coverage and fix revealer issue.
jesseleite Aug 16, 2024
b40e21c
Technically these should match, though it doesn’t matter for tests.
jesseleite Aug 16, 2024
211840d
Escape dot.
jesseleite Aug 16, 2024
5d633ce
Add failing test for handing of `$parent.` in revealer condition logic.
jesseleite Aug 16, 2024
5c24c8c
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 21, 2024
c9f59d7
Kick test.
jesseleite Aug 21, 2024
b4e8de6
Unkick test.
jesseleite Sep 24, 2024
3f5a65a
Add failing test for `$parent` at multiple levels in values payload.
jesseleite Sep 26, 2024
d234df2
Extract and fix `$parent` resolving logic to pass test.
jesseleite Sep 26, 2024
d6e9068
These get treated as regex by my test runner, just word it better.
jesseleite Sep 26, 2024
b4a5315
Actually pass failing tests.
jesseleite Sep 26, 2024
7195c9d
Revealer tests wip.
jesseleite Sep 26, 2024
52bb8ae
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Sep 26, 2024
92d1b5a
Flesh out `$parent` handling and test coverage (WIP).
jesseleite Sep 26, 2024
4b82755
Avoid regex symbols in test names for test runners.
jesseleite Sep 27, 2024
d5527b0
Okay, rip out all this in favour of `$parent` to `$root` resolving idea.
jesseleite Sep 27, 2024
bd82563
Spec it all out with unit tests.
jesseleite Sep 27, 2024
690ffa5
Make it all work for real.
jesseleite Sep 27, 2024
0e4f6f2
Resolve `$parent` to absolute `$root` field path (since that already …
jesseleite Sep 27, 2024
c21f55a
Now this all just works! 🎉🎉🎉
jesseleite Sep 27, 2024
5ea49a5
Flesh this out a little more to cover douple replicator with multiple…
jesseleite Sep 27, 2024
d3cc919
Failing test to show data loss because of improper `revealer` hidden …
jesseleite Sep 27, 2024
df8bf21
Prevent data loss with `$parent` syntax on revealers & pass failing t…
jesseleite Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions resources/js/components/field-conditions/Converter.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,11 +32,17 @@ export default class {
}

getScopedFieldHandle(field, prefix) {
if (field.startsWith('root.') || ! prefix) {
if (field.startsWith('$root.') || field.startsWith('root.')) {
return field;
}

return prefix + field;
if (field.startsWith('$parent.')) {
return field;
}

return prefix
? prefix + field
: field;
}

getOperatorFromRhs(condition) {
Expand Down
42 changes: 42 additions & 0 deletions resources/js/components/field-conditions/ParentResolver.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
export default class {
constructor(currentFieldPath) {
this.currentFieldPath = currentFieldPath;
}

resolve(pathWithParent) {
let parentPath = this.getParentFieldPath(this.currentFieldPath, true);
let fieldPath = this.removeOneParentKeyword(pathWithParent);

while (fieldPath.startsWith('$parent.')) {
parentPath = this.getParentFieldPath(parentPath);
fieldPath = this.removeOneParentKeyword(fieldPath);
}

let resolved = parentPath
? `${parentPath}.${fieldPath}`
: fieldPath;

return `$root.${resolved}`;
}

getParentFieldPath(dottedFieldPath, removeCurrentField) {
const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$');

if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) {
dottedFieldPath = dottedFieldPath.replace(regex, '$1');
}

return dottedFieldPath.includes('.')
? dottedFieldPath.replace(regex, '$1$2')
: '';
}

isAtSetLevel(dottedFieldPath) {
return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$'));
}

removeOneParentKeyword(dottedFieldPath) {
return dottedFieldPath.replace(new RegExp('^\\$parent.'), '');
}

}
33 changes: 23 additions & 10 deletions resources/js/components/field-conditions/Validator.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Converter from './Converter.js';
import ParentResolver from './ParentResolver.js';
import { KEYS } from './Constants.js';
import { data_get } from '../../bootstrap/globals.js'
import isString from 'underscore/modules/isString.js'
Expand All@@ -20,12 +21,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [
];

export default class {
constructor(field, values, store, storeName) {
constructor(field, values, dottedFieldPath, store, storeName) {
this.field = field;
this.values = values;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.dottedFieldPath = dottedFieldPath;
this.store = store;
this.storeName = storeName;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter;
Expand DownExpand Up@@ -204,9 +206,15 @@ export default class {
}

getFieldValue(field) {
return field.startsWith('root.')
? data_get(this.rootValues, field.replace(new RegExp('^root.'), ''))
: data_get(this.values, field);
if (field.startsWith('$parent.')) {
field = new ParentResolver(this.dottedFieldPath).resolve(field);
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
}

return data_get(this.values, field);
}

passesCondition(condition) {
Expand DownExpand Up@@ -264,6 +272,7 @@ export default class {
root: this.rootValues,
store: this.store,
storeName: this.storeName,
fieldPath: this.dottedFieldPath,
});

return this.showOnPass ? passes : ! passes;
Expand All@@ -286,12 +295,16 @@ export default class {
}

relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) {
if (! dottedPrefix) {
return lhs;
if (lhs.startsWith('$parent.')) {
lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs);
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
}

return lhs.startsWith('root.')
? lhs.replace(/^root\./, '')
: dottedPrefix + '.' + lhs;
return dottedPrefix
? dottedPrefix + '.' + lhs
: lhs;
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ export default {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, this.values, this.$store, this.storeName);
let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand Down
30 changes: 29 additions & 1 deletion resources/js/tests/FieldConditionsConverter.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => {
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to root field conditions', () => {
test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$root.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$root.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'root.title': 'not empty',
Expand All@@ -49,6 +63,20 @@ test('it converts from blueprint format and does not apply prefix to root field
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$parent.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$parent.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts to blueprint format', () => {
let converted = FieldConditionsConverter.toBlueprint([
{field: 'name', operator: 'isnt', value: 'joe'},
Expand Down
68 changes: 68 additions & 0 deletions resources/js/tests/FieldConditionsParentResolver.test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import ParentResolver from '../components/field-conditions/ParentResolver.js';

let resolve = function (currentFieldPath, pathWithParent) {
return new ParentResolver(currentFieldPath).resolve(pathWithParent);
}

test('it resolves from group to top level', () => {
expect(resolve('group.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from set/row to top level', () => {
expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from nested group to parent group', () => {
expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from nested set to parent set', () => {
expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name');
expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name');
expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name');
});

test('it resolves from nested group to parent set', () => {
expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name');
expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name');
});

test('it resolves from nested set to parent group', () => {
expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from deeply nested groups all the way up to top level', () => {
let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested sets all the way up to top level', () => {
let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested mix of everything all the way up to top level', () => {
let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [5.x] Add `parent` keyword to field conditions by florianbrinkmann · Pull Request #9385 · statamic/cms · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07ca566
feat: allow selection of parent level in field condition via `parent`…
florianbrinkmann Jan 24, 2024
901983b
Merge branch '4.x' into 783-add-possibility-to-access-parent-field-in…
florianbrinkmann Jan 24, 2024
f14c44b
Merge branch '5.x' into pr/9385
duncanmcclean May 14, 2024
2adc37b
Merge branch '5.x' into feature/add-parent-keyword-to-field-conditions
jasonvarga Jul 29, 2024
06d0515
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 16, 2024
ad02cbf
Port #9960’s `fieldPath` param into this PR.
jesseleite Aug 16, 2024
aad3289
Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow …
jesseleite Aug 16, 2024
c5a4d84
Update blueprint converter as well.
jesseleite Aug 16, 2024
800e6d5
Add test coverage and fix revealer issue.
jesseleite Aug 16, 2024
b40e21c
Technically these should match, though it doesn’t matter for tests.
jesseleite Aug 16, 2024
211840d
Escape dot.
jesseleite Aug 16, 2024
5d633ce
Add failing test for handing of `$parent.` in revealer condition logic.
jesseleite Aug 16, 2024
5c24c8c
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 21, 2024
c9f59d7
Kick test.
jesseleite Aug 21, 2024
b4e8de6
Unkick test.
jesseleite Sep 24, 2024
3f5a65a
Add failing test for `$parent` at multiple levels in values payload.
jesseleite Sep 26, 2024
d234df2
Extract and fix `$parent` resolving logic to pass test.
jesseleite Sep 26, 2024
d6e9068
These get treated as regex by my test runner, just word it better.
jesseleite Sep 26, 2024
b4a5315
Actually pass failing tests.
jesseleite Sep 26, 2024
7195c9d
Revealer tests wip.
jesseleite Sep 26, 2024
52bb8ae
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Sep 26, 2024
92d1b5a
Flesh out `$parent` handling and test coverage (WIP).
jesseleite Sep 26, 2024
4b82755
Avoid regex symbols in test names for test runners.
jesseleite Sep 27, 2024
d5527b0
Okay, rip out all this in favour of `$parent` to `$root` resolving idea.
jesseleite Sep 27, 2024
bd82563
Spec it all out with unit tests.
jesseleite Sep 27, 2024
690ffa5
Make it all work for real.
jesseleite Sep 27, 2024
0e4f6f2
Resolve `$parent` to absolute `$root` field path (since that already …
jesseleite Sep 27, 2024
c21f55a
Now this all just works! 🎉🎉🎉
jesseleite Sep 27, 2024
5ea49a5
Flesh this out a little more to cover douple replicator with multiple…
jesseleite Sep 27, 2024
d3cc919
Failing test to show data loss because of improper `revealer` hidden …
jesseleite Sep 27, 2024
df8bf21
Prevent data loss with `$parent` syntax on revealers & pass failing t…
jesseleite Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions resources/js/components/field-conditions/Converter.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,11 +32,17 @@ export default class {
}

getScopedFieldHandle(field, prefix) {
if (field.startsWith('root.') || ! prefix) {
if (field.startsWith('$root.') || field.startsWith('root.')) {
return field;
}

return prefix + field;
if (field.startsWith('$parent.')) {
return field;
}

return prefix
? prefix + field
: field;
}

getOperatorFromRhs(condition) {
Expand Down
42 changes: 42 additions & 0 deletions resources/js/components/field-conditions/ParentResolver.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
export default class {
constructor(currentFieldPath) {
this.currentFieldPath = currentFieldPath;
}

resolve(pathWithParent) {
let parentPath = this.getParentFieldPath(this.currentFieldPath, true);
let fieldPath = this.removeOneParentKeyword(pathWithParent);

while (fieldPath.startsWith('$parent.')) {
parentPath = this.getParentFieldPath(parentPath);
fieldPath = this.removeOneParentKeyword(fieldPath);
}

let resolved = parentPath
? `${parentPath}.${fieldPath}`
: fieldPath;

return `$root.${resolved}`;
}

getParentFieldPath(dottedFieldPath, removeCurrentField) {
const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$');

if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) {
dottedFieldPath = dottedFieldPath.replace(regex, '$1');
}

return dottedFieldPath.includes('.')
? dottedFieldPath.replace(regex, '$1$2')
: '';
}

isAtSetLevel(dottedFieldPath) {
return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$'));
}

removeOneParentKeyword(dottedFieldPath) {
return dottedFieldPath.replace(new RegExp('^\\$parent.'), '');
}

}
33 changes: 23 additions & 10 deletions resources/js/components/field-conditions/Validator.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Converter from './Converter.js';
import ParentResolver from './ParentResolver.js';
import { KEYS } from './Constants.js';
import { data_get } from '../../bootstrap/globals.js'
import isString from 'underscore/modules/isString.js'
Expand All@@ -20,12 +21,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [
];

export default class {
constructor(field, values, store, storeName) {
constructor(field, values, dottedFieldPath, store, storeName) {
this.field = field;
this.values = values;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.dottedFieldPath = dottedFieldPath;
this.store = store;
this.storeName = storeName;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter;
Expand DownExpand Up@@ -204,9 +206,15 @@ export default class {
}

getFieldValue(field) {
return field.startsWith('root.')
? data_get(this.rootValues, field.replace(new RegExp('^root.'), ''))
: data_get(this.values, field);
if (field.startsWith('$parent.')) {
field = new ParentResolver(this.dottedFieldPath).resolve(field);
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
}

return data_get(this.values, field);
}

passesCondition(condition) {
Expand DownExpand Up@@ -264,6 +272,7 @@ export default class {
root: this.rootValues,
store: this.store,
storeName: this.storeName,
fieldPath: this.dottedFieldPath,
});

return this.showOnPass ? passes : ! passes;
Expand All@@ -286,12 +295,16 @@ export default class {
}

relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) {
if (! dottedPrefix) {
return lhs;
if (lhs.startsWith('$parent.')) {
lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs);
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
}

return lhs.startsWith('root.')
? lhs.replace(/^root\./, '')
: dottedPrefix + '.' + lhs;
return dottedPrefix
? dottedPrefix + '.' + lhs
: lhs;
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ export default {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, this.values, this.$store, this.storeName);
let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand Down
30 changes: 29 additions & 1 deletion resources/js/tests/FieldConditionsConverter.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => {
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to root field conditions', () => {
test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$root.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$root.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'root.title': 'not empty',
Expand All@@ -49,6 +63,20 @@ test('it converts from blueprint format and does not apply prefix to root field
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$parent.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$parent.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts to blueprint format', () => {
let converted = FieldConditionsConverter.toBlueprint([
{field: 'name', operator: 'isnt', value: 'joe'},
Expand Down
68 changes: 68 additions & 0 deletions resources/js/tests/FieldConditionsParentResolver.test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import ParentResolver from '../components/field-conditions/ParentResolver.js';

let resolve = function (currentFieldPath, pathWithParent) {
return new ParentResolver(currentFieldPath).resolve(pathWithParent);
}

test('it resolves from group to top level', () => {
expect(resolve('group.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from set/row to top level', () => {
expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from nested group to parent group', () => {
expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from nested set to parent set', () => {
expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name');
expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name');
expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name');
});

test('it resolves from nested group to parent set', () => {
expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name');
expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name');
});

test('it resolves from nested set to parent group', () => {
expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from deeply nested groups all the way up to top level', () => {
let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested sets all the way up to top level', () => {
let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested mix of everything all the way up to top level', () => {
let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [5.x] Add `parent` keyword to field conditions by florianbrinkmann · Pull Request #9385 · statamic/cms · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07ca566
feat: allow selection of parent level in field condition via `parent`…
florianbrinkmann Jan 24, 2024
901983b
Merge branch '4.x' into 783-add-possibility-to-access-parent-field-in…
florianbrinkmann Jan 24, 2024
f14c44b
Merge branch '5.x' into pr/9385
duncanmcclean May 14, 2024
2adc37b
Merge branch '5.x' into feature/add-parent-keyword-to-field-conditions
jasonvarga Jul 29, 2024
06d0515
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 16, 2024
ad02cbf
Port #9960’s `fieldPath` param into this PR.
jesseleite Aug 16, 2024
aad3289
Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow …
jesseleite Aug 16, 2024
c5a4d84
Update blueprint converter as well.
jesseleite Aug 16, 2024
800e6d5
Add test coverage and fix revealer issue.
jesseleite Aug 16, 2024
b40e21c
Technically these should match, though it doesn’t matter for tests.
jesseleite Aug 16, 2024
211840d
Escape dot.
jesseleite Aug 16, 2024
5d633ce
Add failing test for handing of `$parent.` in revealer condition logic.
jesseleite Aug 16, 2024
5c24c8c
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 21, 2024
c9f59d7
Kick test.
jesseleite Aug 21, 2024
b4e8de6
Unkick test.
jesseleite Sep 24, 2024
3f5a65a
Add failing test for `$parent` at multiple levels in values payload.
jesseleite Sep 26, 2024
d234df2
Extract and fix `$parent` resolving logic to pass test.
jesseleite Sep 26, 2024
d6e9068
These get treated as regex by my test runner, just word it better.
jesseleite Sep 26, 2024
b4a5315
Actually pass failing tests.
jesseleite Sep 26, 2024
7195c9d
Revealer tests wip.
jesseleite Sep 26, 2024
52bb8ae
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Sep 26, 2024
92d1b5a
Flesh out `$parent` handling and test coverage (WIP).
jesseleite Sep 26, 2024
4b82755
Avoid regex symbols in test names for test runners.
jesseleite Sep 27, 2024
d5527b0
Okay, rip out all this in favour of `$parent` to `$root` resolving idea.
jesseleite Sep 27, 2024
bd82563
Spec it all out with unit tests.
jesseleite Sep 27, 2024
690ffa5
Make it all work for real.
jesseleite Sep 27, 2024
0e4f6f2
Resolve `$parent` to absolute `$root` field path (since that already …
jesseleite Sep 27, 2024
c21f55a
Now this all just works! 🎉🎉🎉
jesseleite Sep 27, 2024
5ea49a5
Flesh this out a little more to cover douple replicator with multiple…
jesseleite Sep 27, 2024
d3cc919
Failing test to show data loss because of improper `revealer` hidden …
jesseleite Sep 27, 2024
df8bf21
Prevent data loss with `$parent` syntax on revealers & pass failing t…
jesseleite Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions resources/js/components/field-conditions/Converter.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,11 +32,17 @@ export default class {
}

getScopedFieldHandle(field, prefix) {
if (field.startsWith('root.') || ! prefix) {
if (field.startsWith('$root.') || field.startsWith('root.')) {
return field;
}

return prefix + field;
if (field.startsWith('$parent.')) {
return field;
}

return prefix
? prefix + field
: field;
}

getOperatorFromRhs(condition) {
Expand Down
42 changes: 42 additions & 0 deletions resources/js/components/field-conditions/ParentResolver.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
export default class {
constructor(currentFieldPath) {
this.currentFieldPath = currentFieldPath;
}

resolve(pathWithParent) {
let parentPath = this.getParentFieldPath(this.currentFieldPath, true);
let fieldPath = this.removeOneParentKeyword(pathWithParent);

while (fieldPath.startsWith('$parent.')) {
parentPath = this.getParentFieldPath(parentPath);
fieldPath = this.removeOneParentKeyword(fieldPath);
}

let resolved = parentPath
? `${parentPath}.${fieldPath}`
: fieldPath;

return `$root.${resolved}`;
}

getParentFieldPath(dottedFieldPath, removeCurrentField) {
const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$');

if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) {
dottedFieldPath = dottedFieldPath.replace(regex, '$1');
}

return dottedFieldPath.includes('.')
? dottedFieldPath.replace(regex, '$1$2')
: '';
}

isAtSetLevel(dottedFieldPath) {
return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$'));
}

removeOneParentKeyword(dottedFieldPath) {
return dottedFieldPath.replace(new RegExp('^\\$parent.'), '');
}

}
33 changes: 23 additions & 10 deletions resources/js/components/field-conditions/Validator.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Converter from './Converter.js';
import ParentResolver from './ParentResolver.js';
import { KEYS } from './Constants.js';
import { data_get } from '../../bootstrap/globals.js'
import isString from 'underscore/modules/isString.js'
Expand All@@ -20,12 +21,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [
];

export default class {
constructor(field, values, store, storeName) {
constructor(field, values, dottedFieldPath, store, storeName) {
this.field = field;
this.values = values;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.dottedFieldPath = dottedFieldPath;
this.store = store;
this.storeName = storeName;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter;
Expand DownExpand Up@@ -204,9 +206,15 @@ export default class {
}

getFieldValue(field) {
return field.startsWith('root.')
? data_get(this.rootValues, field.replace(new RegExp('^root.'), ''))
: data_get(this.values, field);
if (field.startsWith('$parent.')) {
field = new ParentResolver(this.dottedFieldPath).resolve(field);
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
}

return data_get(this.values, field);
}

passesCondition(condition) {
Expand DownExpand Up@@ -264,6 +272,7 @@ export default class {
root: this.rootValues,
store: this.store,
storeName: this.storeName,
fieldPath: this.dottedFieldPath,
});

return this.showOnPass ? passes : ! passes;
Expand All@@ -286,12 +295,16 @@ export default class {
}

relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) {
if (! dottedPrefix) {
return lhs;
if (lhs.startsWith('$parent.')) {
lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs);
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
}

return lhs.startsWith('root.')
? lhs.replace(/^root\./, '')
: dottedPrefix + '.' + lhs;
return dottedPrefix
? dottedPrefix + '.' + lhs
: lhs;
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ export default {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, this.values, this.$store, this.storeName);
let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand Down
30 changes: 29 additions & 1 deletion resources/js/tests/FieldConditionsConverter.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => {
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to root field conditions', () => {
test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$root.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$root.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'root.title': 'not empty',
Expand All@@ -49,6 +63,20 @@ test('it converts from blueprint format and does not apply prefix to root field
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$parent.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$parent.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts to blueprint format', () => {
let converted = FieldConditionsConverter.toBlueprint([
{field: 'name', operator: 'isnt', value: 'joe'},
Expand Down
68 changes: 68 additions & 0 deletions resources/js/tests/FieldConditionsParentResolver.test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import ParentResolver from '../components/field-conditions/ParentResolver.js';

let resolve = function (currentFieldPath, pathWithParent) {
return new ParentResolver(currentFieldPath).resolve(pathWithParent);
}

test('it resolves from group to top level', () => {
expect(resolve('group.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from set/row to top level', () => {
expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from nested group to parent group', () => {
expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from nested set to parent set', () => {
expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name');
expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name');
expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name');
});

test('it resolves from nested group to parent set', () => {
expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name');
expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name');
});

test('it resolves from nested set to parent group', () => {
expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from deeply nested groups all the way up to top level', () => {
let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested sets all the way up to top level', () => {
let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested mix of everything all the way up to top level', () => {
let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [5.x] Add `parent` keyword to field conditions by florianbrinkmann · Pull Request #9385 · statamic/cms · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07ca566
feat: allow selection of parent level in field condition via `parent`…
florianbrinkmann Jan 24, 2024
901983b
Merge branch '4.x' into 783-add-possibility-to-access-parent-field-in…
florianbrinkmann Jan 24, 2024
f14c44b
Merge branch '5.x' into pr/9385
duncanmcclean May 14, 2024
2adc37b
Merge branch '5.x' into feature/add-parent-keyword-to-field-conditions
jasonvarga Jul 29, 2024
06d0515
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 16, 2024
ad02cbf
Port #9960’s `fieldPath` param into this PR.
jesseleite Aug 16, 2024
aad3289
Rename `root.` & `parent.` to `$root.` & `$parent.` (but still allow …
jesseleite Aug 16, 2024
c5a4d84
Update blueprint converter as well.
jesseleite Aug 16, 2024
800e6d5
Add test coverage and fix revealer issue.
jesseleite Aug 16, 2024
b40e21c
Technically these should match, though it doesn’t matter for tests.
jesseleite Aug 16, 2024
211840d
Escape dot.
jesseleite Aug 16, 2024
5d633ce
Add failing test for handing of `$parent.` in revealer condition logic.
jesseleite Aug 16, 2024
5c24c8c
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Aug 21, 2024
c9f59d7
Kick test.
jesseleite Aug 21, 2024
b4e8de6
Unkick test.
jesseleite Sep 24, 2024
3f5a65a
Add failing test for `$parent` at multiple levels in values payload.
jesseleite Sep 26, 2024
d234df2
Extract and fix `$parent` resolving logic to pass test.
jesseleite Sep 26, 2024
d6e9068
These get treated as regex by my test runner, just word it better.
jesseleite Sep 26, 2024
b4a5315
Actually pass failing tests.
jesseleite Sep 26, 2024
7195c9d
Revealer tests wip.
jesseleite Sep 26, 2024
52bb8ae
Merge branch '5.x' of https://github.com/statamic/cms into feature/ad…
jesseleite Sep 26, 2024
92d1b5a
Flesh out `$parent` handling and test coverage (WIP).
jesseleite Sep 26, 2024
4b82755
Avoid regex symbols in test names for test runners.
jesseleite Sep 27, 2024
d5527b0
Okay, rip out all this in favour of `$parent` to `$root` resolving idea.
jesseleite Sep 27, 2024
bd82563
Spec it all out with unit tests.
jesseleite Sep 27, 2024
690ffa5
Make it all work for real.
jesseleite Sep 27, 2024
0e4f6f2
Resolve `$parent` to absolute `$root` field path (since that already …
jesseleite Sep 27, 2024
c21f55a
Now this all just works! 🎉🎉🎉
jesseleite Sep 27, 2024
5ea49a5
Flesh this out a little more to cover douple replicator with multiple…
jesseleite Sep 27, 2024
d3cc919
Failing test to show data loss because of improper `revealer` hidden …
jesseleite Sep 27, 2024
df8bf21
Prevent data loss with `$parent` syntax on revealers & pass failing t…
jesseleite Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions resources/js/components/field-conditions/Converter.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,11 +32,17 @@ export default class {
}

getScopedFieldHandle(field, prefix) {
if (field.startsWith('root.') || ! prefix) {
if (field.startsWith('$root.') || field.startsWith('root.')) {
return field;
}

return prefix + field;
if (field.startsWith('$parent.')) {
return field;
}

return prefix
? prefix + field
: field;
}

getOperatorFromRhs(condition) {
Expand Down
42 changes: 42 additions & 0 deletions resources/js/components/field-conditions/ParentResolver.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
export default class {
constructor(currentFieldPath) {
this.currentFieldPath = currentFieldPath;
}

resolve(pathWithParent) {
let parentPath = this.getParentFieldPath(this.currentFieldPath, true);
let fieldPath = this.removeOneParentKeyword(pathWithParent);

while (fieldPath.startsWith('$parent.')) {
parentPath = this.getParentFieldPath(parentPath);
fieldPath = this.removeOneParentKeyword(fieldPath);
}

let resolved = parentPath
? `${parentPath}.${fieldPath}`
: fieldPath;

return `$root.${resolved}`;
}

getParentFieldPath(dottedFieldPath, removeCurrentField) {
const regex = new RegExp('(.*?[^\\.]+)(\\.[0-9]+)*\\.[^\\.]*$');

if (removeCurrentField || this.isAtSetLevel(dottedFieldPath)) {
dottedFieldPath = dottedFieldPath.replace(regex, '$1');
}

return dottedFieldPath.includes('.')
? dottedFieldPath.replace(regex, '$1$2')
: '';
}

isAtSetLevel(dottedFieldPath) {
return dottedFieldPath.match(new RegExp('(\\.[0-9]+)$'));
}

removeOneParentKeyword(dottedFieldPath) {
return dottedFieldPath.replace(new RegExp('^\\$parent.'), '');
}

}
33 changes: 23 additions & 10 deletions resources/js/components/field-conditions/Validator.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Converter from './Converter.js';
import ParentResolver from './ParentResolver.js';
import { KEYS } from './Constants.js';
import { data_get } from '../../bootstrap/globals.js'
import isString from 'underscore/modules/isString.js'
Expand All@@ -20,12 +21,13 @@ const NUMBER_SPECIFIC_COMPARISONS = [
];

export default class {
constructor(field, values, store, storeName) {
constructor(field, values, dottedFieldPath, store, storeName) {
this.field = field;
this.values = values;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.dottedFieldPath = dottedFieldPath;
this.store = store;
this.storeName = storeName;
this.rootValues = store ? store.state.publish[storeName].values : false;
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter;
Expand DownExpand Up@@ -204,9 +206,15 @@ export default class {
}

getFieldValue(field) {
return field.startsWith('root.')
? data_get(this.rootValues, field.replace(new RegExp('^root.'), ''))
: data_get(this.values, field);
if (field.startsWith('$parent.')) {
field = new ParentResolver(this.dottedFieldPath).resolve(field);
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
}

return data_get(this.values, field);
}

passesCondition(condition) {
Expand DownExpand Up@@ -264,6 +272,7 @@ export default class {
root: this.rootValues,
store: this.store,
storeName: this.storeName,
fieldPath: this.dottedFieldPath,
});

return this.showOnPass ? passes : ! passes;
Expand All@@ -286,12 +295,16 @@ export default class {
}

relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) {
if (! dottedPrefix) {
return lhs;
if (lhs.startsWith('$parent.')) {
lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs);
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
}

return lhs.startsWith('root.')
? lhs.replace(/^root\./, '')
: dottedPrefix + '.' + lhs;
return dottedPrefix
? dottedPrefix + '.' + lhs
: lhs;
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ export default {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, this.values, this.$store, this.storeName);
let validator = new Validator(field, this.values, dottedFieldPath, this.$store, this.storeName);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand Down
30 changes: 29 additions & 1 deletion resources/js/tests/FieldConditionsConverter.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,21 @@ test('it converts from blueprint format and applies prefixes', () => {
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to root field conditions', () => {
test('it converts from blueprint format and does not apply prefix to field conditions with root syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$root.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$root.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with legacy root syntax for backwards compatibility', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'root.title': 'not empty',
Expand All@@ -49,6 +63,20 @@ test('it converts from blueprint format and does not apply prefix to root field
expect(converted).toEqual(expected);
});

test('it converts from blueprint format and does not apply prefix to field conditions with parent syntax', () => {
let converted = FieldConditionsConverter.fromBlueprint({
'name': 'isnt joe',
'$parent.title': 'not empty',
}, 'nested_');

let expected = [
{field: 'nested_name', operator: 'not', value: 'joe'},
{field: '$parent.title', operator: 'not', value: 'empty'}
];

expect(converted).toEqual(expected);
});

test('it converts to blueprint format', () => {
let converted = FieldConditionsConverter.toBlueprint([
{field: 'name', operator: 'isnt', value: 'joe'},
Expand Down
68 changes: 68 additions & 0 deletions resources/js/tests/FieldConditionsParentResolver.test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
import ParentResolver from '../components/field-conditions/ParentResolver.js';

let resolve = function (currentFieldPath, pathWithParent) {
return new ParentResolver(currentFieldPath).resolve(pathWithParent);
}

test('it resolves from group to top level', () => {
expect(resolve('group.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from set/row to top level', () => {
expect(resolve('replicator.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('grid.0.field', '$parent.name')).toEqual('$root.name');
expect(resolve('bard.0.field', '$parent.name')).toEqual('$root.name');
});

test('it resolves from nested group to parent group', () => {
expect(resolve('group.nested_group.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from nested set to parent set', () => {
expect(resolve('replicator.2.nested_replicator.1.field', '$parent.name')).toEqual('$root.replicator.2.name');
expect(resolve('grid.2.nested_grid.1.field', '$parent.name')).toEqual('$root.grid.2.name');
expect(resolve('bard.2.nested_bard.1.field', '$parent.name')).toEqual('$root.bard.2.name');
});

test('it resolves from nested group to parent set', () => {
expect(resolve('replicator.1.group.field', '$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve('grid.1.group.field', '$parent.name')).toEqual('$root.grid.1.name');
expect(resolve('bard.1.group.field', '$parent.name')).toEqual('$root.bard.1.name');
});

test('it resolves from nested set to parent group', () => {
expect(resolve('group.replicator.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.grid.1.field', '$parent.name')).toEqual('$root.group.name');
expect(resolve('group.bard.1.field', '$parent.name')).toEqual('$root.group.name');
});

test('it resolves from deeply nested groups all the way up to top level', () => {
let fromField = 'group.nested_group.deeper_group.deeeeeeeper_group.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.nested_group.deeper_group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.nested_group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested sets all the way up to top level', () => {
let fromField = 'replicator.1.bard.4.grid.0.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.replicator.1.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.replicator.1.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});

test('it resolves from deeply nested mix of everything all the way up to top level', () => {
let fromField = 'group.replicator.1.group.bard.4.grid.0.group.group.replicator.6.field';

expect(resolve(fromField, '$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.group.name');
expect(resolve(fromField, '$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.grid.0.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.bard.4.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.replicator.1.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.group.name');
expect(resolve(fromField, '$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.name')).toEqual('$root.name');
});
Loading