Skip to content

Commit

Permalink
Merge branch 'main' into plg-335
Browse files Browse the repository at this point in the history
  • Loading branch information
ronaldlangeveld authored Feb 4, 2025
2 parents e824d3d + 220af76 commit 7e214f6
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 36 deletions.
23 changes: 21 additions & 2 deletions packages/kg-default-nodes/lib/generate-decorator-node.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {KoenigDecoratorNode} from './KoenigDecoratorNode';
import readTextContent from './utils/read-text-content';
import {ALL_MEMBERS_SEGMENT, usesOldVisibilityFormat} from './utils/visibility';
import {ALL_MEMBERS_SEGMENT, buildDefaultVisibility, migrateOldVisibilityFormat, usesOldVisibilityFormat} from './utils/visibility';
/**
* Validates the required arguments passed to `generateDecoratorNode`
*/
Expand Down Expand Up @@ -36,9 +36,10 @@ function validateArguments(nodeType, properties) {
*
* @param {string} nodeType – The node's type (must be unique)
* @param {DecoratorNodeProperty[]} properties - An array of properties for the generated class
* @param {boolean} hasVisibility - Whether to add a visibility property to the node
* @returns {Object} - The generated class.
*/
export function generateDecoratorNode({nodeType, properties = [], version = 1}) {
export function generateDecoratorNode({nodeType, properties = [], version = 1, hasVisibility = false}) {
validateArguments(nodeType, properties);

// Adds a `privateName` field to the properties for convenience (e.g. `__name`):
Expand All @@ -47,6 +48,18 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
return {...prop, privateName: `__${prop.name}`};
});

// Adds `visibility` property to the properties array if `hasVisibility` is true
// uses a getter for `default` to avoid problems with mutation of nested objects
if (hasVisibility) {
properties.push({
name: 'visibility',
get default() {
return buildDefaultVisibility();
},
privateName: '__visibility'
});
}

class GeneratedDecoratorNode extends KoenigDecoratorNode {
constructor(data = {}, key) {
super(key);
Expand Down Expand Up @@ -134,6 +147,12 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
static importJSON(serializedNode) {
const data = {};

// migrate older nodes that were saved with an earlier version of the visibility format
const visibility = serializedNode.visibility;
if (visibility && usesOldVisibilityFormat(visibility)) {
migrateOldVisibilityFormat(visibility);
}

properties.forEach((prop) => {
data[prop.name] = serializedNode[prop.name];
});
Expand Down
3 changes: 3 additions & 0 deletions packages/kg-default-nodes/lib/kg-default-nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ export * from './nodes/TKNode';
export * from './nodes/at-link/index.js';
export * from './nodes/zwnj/ZWNJNode';

// export utility functions that are useful in other packages or tests
import * as visibilityUtils from './utils/visibility';
import {generateDecoratorNode} from './generate-decorator-node.js';
export const utils = {
generateDecoratorNode,
visibility: visibilityUtils
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// eslint-disable-next-line ghost/filenames/match-exported-class
import {generateDecoratorNode} from '../../generate-decorator-node';
import {renderCallToActionNode} from './calltoaction-renderer';
export class CallToActionNode extends generateDecoratorNode({nodeType: 'call-to-action',

export class CallToActionNode extends generateDecoratorNode({
nodeType: 'call-to-action',
hasVisibility: true,
properties: [
{name: 'layout', default: 'minimal'},
{name: 'textValue', default: '', wordCount: true},
Expand All @@ -14,8 +17,8 @@ export class CallToActionNode extends generateDecoratorNode({nodeType: 'call-to-
{name: 'backgroundColor', default: 'grey'},
{name: 'hasImage', default: false},
{name: 'imageUrl', default: ''}
]}
) {
]
}) {
/* overrides */
exportDOM(options = {}) {
return renderCallToActionNode(this, options);
Expand Down
25 changes: 2 additions & 23 deletions packages/kg-default-nodes/lib/nodes/html/HtmlNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,13 @@
import {generateDecoratorNode} from '../../generate-decorator-node';
import {renderHtmlNode} from './html-renderer';
import {parseHtmlNode} from './html-parser';
import {DEFAULT_VISIBILITY, usesOldVisibilityFormat, migrateOldVisibilityFormat} from '../../utils/visibility';

export class HtmlNode extends generateDecoratorNode({nodeType: 'html',
hasVisibility: true,
properties: [
{name: 'html', default: '', urlType: 'html', wordCount: true},
{name: 'visibility', default: {...DEFAULT_VISIBILITY}}
{name: 'html', default: '', urlType: 'html', wordCount: true}
]}
) {
constructor({
html = '',
visibility = {...DEFAULT_VISIBILITY}
} = {}, key) {
super(key);
this.html = html;
this.visibility = visibility;
}

static importJSON(serializedNode) {
const {visibility} = serializedNode;

// migrate older nodes that were saved with an earlier version of the visibility format
if (visibility && usesOldVisibilityFormat(visibility)) {
migrateOldVisibilityFormat(visibility);
}

return super.importJSON(serializedNode);
}

static importDOM() {
return parseHtmlNode(this);
}
Expand Down
7 changes: 6 additions & 1 deletion packages/kg-default-nodes/lib/utils/visibility.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const ALL_MEMBERS_SEGMENT = 'status:free,status:-free';
export const NO_MEMBERS_SEGMENT = '';

export const DEFAULT_VISIBILITY = {
const DEFAULT_VISIBILITY = {
web: {
nonMember: true,
memberSegment: 'status:free,status:-free'
Expand All @@ -11,6 +11,11 @@ export const DEFAULT_VISIBILITY = {
}
};

// ensure we always work with a deep copy to avoid accidental constant mutations
export function buildDefaultVisibility() {
return JSON.parse(JSON.stringify(DEFAULT_VISIBILITY));
}

export function usesOldVisibilityFormat(visibility) {
return !Object.prototype.hasOwnProperty.call(visibility, 'web')
|| !Object.prototype.hasOwnProperty.call(visibility, 'email')
Expand Down
129 changes: 129 additions & 0 deletions packages/kg-default-nodes/test/generate-decorator-node.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const {createHeadlessEditor} = require('@lexical/headless');
const {utils} = require('../');

const defaultVisibility = utils.visibility.buildDefaultVisibility();

describe('Utils: generateDecoratorNode', function () {
let editor;

// NOTE: all tests should use this function, without it you need manual
// try/catch and done handling to avoid assertion failures not triggering
// failed tests
const editorTest = testFn => function (done) {
editor.update(() => {
try {
testFn();
done();
} catch (e) {
done(e);
}
});
};

describe('hasVisibility', function () {
let NodeWithVisibility;
let $createNodeWithVisibility;

before(function () {
NodeWithVisibility = utils.generateDecoratorNode({
nodeType: 'visibility-test',
properties: [],
hasVisibility: true
});

$createNodeWithVisibility = (dataset) => {
return new NodeWithVisibility(dataset);
};

editor = createHeadlessEditor({nodes: [NodeWithVisibility]});
});

it('adds visibility property with default', editorTest(function () {
const node = $createNodeWithVisibility();

node.visibility.should.deepEqual(defaultVisibility, 'node.visibility');
node.getDataset().visibility.should.deepEqual(defaultVisibility, 'node.getDataset().visibility');
node.exportJSON().visibility.should.deepEqual(defaultVisibility, 'node.exportJSON().visibility');
}));

it('can update visibility', editorTest(function () {
const node = $createNodeWithVisibility();

const newVisibility = {
web: {
nonMember: false,
memberSegment: 'status:free'
},
email: {
memberSegment: 'status:free'
}
};

node.visibility = newVisibility;

node.visibility.should.deepEqual(newVisibility, 'node.visibility');
node.getDataset().visibility.should.deepEqual(newVisibility, 'node.getDataset().visibility');
node.exportJSON().visibility.should.deepEqual(newVisibility, 'node.exportJSON().visibility');
}));

it('ensures default doesn\'t change when nested visibility objects are updated', editorTest(function () {
const node = $createNodeWithVisibility();

// NOTE: this wouldn't trigger a Lexical node update, it's just to show
// that the default can't be accidentally changed by reference
node.visibility.web.nonMember = false;

NodeWithVisibility.getPropertyDefaults().visibility.should.deepEqual(defaultVisibility);
}));

// During the early visibility beta period we had a different format for visibility
// when importing we convert to the new format so it keeps working with later UI iterations
it('migrates old visibility format when importing JSON', editorTest(function () {
const node = NodeWithVisibility.importJSON({
visibility: {
showOnWeb: false,
showOnEmail: true,
segment: 'status:free'
}
});

// old values are kept, new values are added
node.visibility.should.deepEqual({
showOnWeb: false,
showOnEmail: true,
segment: 'status:free',
web: {
nonMember: false,
memberSegment: ''
},
email: {
memberSegment: 'status:free'
}
});
}));

it('can set visibility via constructor', editorTest(function () {
const node = $createNodeWithVisibility({
visibility: {
web: {
nonMember: false,
memberSegment: 'status:free'
},
email: {
memberSegment: 'status:free'
}
}
});

node.visibility.should.deepEqual({
web: {
nonMember: false,
memberSegment: 'status:free'
},
email: {
memberSegment: 'status:free'
}
});
}));
});
});
30 changes: 26 additions & 4 deletions packages/kg-default-nodes/test/nodes/call-to-action.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
const {dom} = require('../test-utils');

const {createHeadlessEditor} = require('@lexical/headless');

const {CallToActionNode, $isCallToActionNode} = require('../../');
const {CallToActionNode, $isCallToActionNode, utils} = require('../../');

const editorNodes = [CallToActionNode];

Expand Down Expand Up @@ -66,6 +64,7 @@ describe('CallToActionNode', function () {
callToActionNode.backgroundColor.should.equal(dataset.backgroundColor);
callToActionNode.hasImage.should.equal(dataset.hasImage);
callToActionNode.imageUrl.should.equal(dataset.imageUrl);
callToActionNode.visibility.should.deepEqual(utils.visibility.buildDefaultVisibility());
}));

it('has setters for all properties', editorTest(function () {
Expand Down Expand Up @@ -109,17 +108,40 @@ describe('CallToActionNode', function () {

callToActionNode.hasImage.should.equal(false);
callToActionNode.hasImage = true;
callToActionNode.hasImage.should.equal(true);

callToActionNode.imageUrl.should.equal('');
callToActionNode.imageUrl = 'http://blog.com/image1.jpg';
callToActionNode.imageUrl.should.equal('http://blog.com/image1.jpg');

callToActionNode.visibility.should.deepEqual(utils.visibility.buildDefaultVisibility());
callToActionNode.visibility = {
web: {
nonMember: false,
memberSegment: ''
},
email: {
memberSegment: ''
}
};
callToActionNode.visibility.should.deepEqual({
web: {
nonMember: false,
memberSegment: ''
},
email: {
memberSegment: ''
}
});
}));

it('has getDataset() convenience method', editorTest(function () {
const callToActionNode = new CallToActionNode(dataset);
const callToActionNodeDataset = callToActionNode.getDataset();

callToActionNodeDataset.should.deepEqual({
...dataset
...dataset,
...{visibility: utils.visibility.buildDefaultVisibility()}
});
}));
});
Expand Down
5 changes: 2 additions & 3 deletions packages/koenig-lexical/src/utils/visibility.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {utils} from '@tryghost/kg-default-nodes';

const DEFAULT_VISIBILITY = utils.visibility.DEFAULT_VISIBILITY;

export function parseVisibilityToToggles(visibility) {
return {
web: {
Expand Down Expand Up @@ -45,7 +43,8 @@ export function serializeTogglesToVisibility(toggles) {
}

// used for building UI
export function getVisibilityOptions(visibility = DEFAULT_VISIBILITY, {isStripeEnabled = true} = {}) {
export function getVisibilityOptions(visibility, {isStripeEnabled = true} = {}) {
visibility = visibility || utils.visibility.buildDefaultVisibility();
const toggles = parseVisibilityToToggles(visibility);

// use arrays to ensure consistent order when using to build UI
Expand Down

0 comments on commit 7e214f6

Please sign in to comment.