Skip to content

Commit

Permalink
Add nodeReducer and related functions
Browse files Browse the repository at this point in the history
  • Loading branch information
dabbott committed Dec 18, 2023
1 parent 704602d commit ff8b53d
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 53 deletions.
91 changes: 91 additions & 0 deletions packages/noya-component/src/__tests__/nodeReducer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { IndexPath } from 'tree-visit';
import { Model } from '../builders';
import { resolvedNodeReducer } from '../nodeReducer';
import { ResolvedHierarchy } from '../resolvedHierarchy';
import { createResolvedNode } from '../traversal';
import { NoyaResolvedNode } from '../types';

function createSimpleBox() {
const component = Model.component({
componentID: 'custom',
rootElement: Model.primitiveElement('box'),
});

const components = {
[component.componentID]: component,
};

const element = Model.compositeElement({
componentID: component.componentID,
});

return {
resolvedNode: createResolvedNode(
(componentID) => components[componentID],
element,
),
};
}

function classNamesAtIndexPath(
resolvedNode: NoyaResolvedNode,
indexPath: IndexPath,
) {
const node = ResolvedHierarchy.access(resolvedNode, indexPath);

if (node.type !== 'noyaPrimitiveElement') {
throw new Error('Expected primitive element');
}

return node.classNames.map((c) => c.value);
}

it('adds class name', () => {
let { resolvedNode } = createSimpleBox();

expect(classNamesAtIndexPath(resolvedNode, [0])).toEqual([]);

resolvedNode = resolvedNodeReducer(resolvedNode, {
type: 'addClassNames',
indexPath: [0],
classNames: ['foo'],
});

expect(classNamesAtIndexPath(resolvedNode, [0])).toEqual(['foo']);
});

it('removes class name', () => {
let { resolvedNode } = createSimpleBox();

expect(classNamesAtIndexPath(resolvedNode, [0])).toEqual([]);

resolvedNode = resolvedNodeReducer(resolvedNode, {
type: 'addClassNames',
indexPath: [0],
classNames: ['foo'],
});

expect(classNamesAtIndexPath(resolvedNode, [0])).toEqual(['foo']);

resolvedNode = resolvedNodeReducer(resolvedNode, {
type: 'removeClassNames',
indexPath: [0],
classNames: ['foo'],
});

expect(classNamesAtIndexPath(resolvedNode, [0])).toEqual([]);
});

it('sets name', () => {
let { resolvedNode } = createSimpleBox();

expect(ResolvedHierarchy.access(resolvedNode, [0]).name).toEqual(undefined);

resolvedNode = resolvedNodeReducer(resolvedNode, {
type: 'setName',
indexPath: [0],
name: 'foo',
});

expect(ResolvedHierarchy.access(resolvedNode, [0]).name).toEqual('foo');
});
1 change: 1 addition & 0 deletions packages/noya-component/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './applyDiff';
export * from './arrayDiff';
export * from './builders';
export * from './nodeReducer';
export * from './partitionDiff';
export * from './patterns';
export * from './renderResolvedNode';
Expand Down
126 changes: 126 additions & 0 deletions packages/noya-component/src/nodeReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import cloneDeep from 'lodash/cloneDeep';
import { IndexPath } from 'tree-visit';
import { Model } from './builders';
import { ResolvedHierarchy } from './resolvedHierarchy';
import { NoyaResolvedNode } from './types';

type Action =
| {
type: 'setName';
indexPath: IndexPath;
name: string;
}
| {
type: 'addClassNames';
indexPath: IndexPath;
classNames: string[];
}
| {
type: 'removeClassNames';
indexPath: IndexPath;
classNames: string[];
}
| {
type: 'insertNode';
indexPath: IndexPath;
node: NoyaResolvedNode;
}
| {
type: 'removeNode';
indexPath: IndexPath;
}
| {
type: 'duplicateNode';
indexPath: IndexPath;
};

export function resolvedNodeReducer(
resolvedNode: NoyaResolvedNode,
action: Action,
): NoyaResolvedNode {
switch (action.type) {
case 'setName': {
const { indexPath, name } = action;

const node = ResolvedHierarchy.access(resolvedNode, indexPath);

if (node?.type !== 'noyaPrimitiveElement') return resolvedNode;

return ResolvedHierarchy.replace(resolvedNode, {
at: indexPath,
node: {
...cloneDeep(node),
name,
},
});
}
case 'addClassNames': {
const { indexPath, classNames: className } = action;

const node = ResolvedHierarchy.access(resolvedNode, indexPath);

if (node?.type !== 'noyaPrimitiveElement') return resolvedNode;

return ResolvedHierarchy.replace(resolvedNode, {
at: indexPath,
node: {
...cloneDeep(node),
classNames: [
...node.classNames,
...className.map((c) => Model.className(c)),
],
},
});
}
case 'removeClassNames': {
const { indexPath, classNames: className } = action;

const node = ResolvedHierarchy.access(resolvedNode, indexPath);

if (node?.type !== 'noyaPrimitiveElement') return resolvedNode;

return ResolvedHierarchy.replace(resolvedNode, {
at: indexPath,
node: {
...cloneDeep(node),
classNames: node.classNames.filter(
(c) => !className.includes(c.value),
),
},
});
}
case 'insertNode': {
const { indexPath, node: child } = action;

const node = ResolvedHierarchy.access(resolvedNode, indexPath);

if (node?.type !== 'noyaPrimitiveElement') return resolvedNode;

return ResolvedHierarchy.insert(resolvedNode, {
at: [...indexPath, node.children.length],
nodes: [child],
});
}
case 'removeNode': {
const { indexPath } = action;

const node = ResolvedHierarchy.access(resolvedNode, indexPath);

if (node?.type !== 'noyaPrimitiveElement') return resolvedNode;

return ResolvedHierarchy.remove(resolvedNode, {
indexPaths: [indexPath],
});
}
case 'duplicateNode': {
const { indexPath } = action;

const node = ResolvedHierarchy.access(resolvedNode, indexPath);

return ResolvedHierarchy.insert(resolvedNode, {
at: [...indexPath.slice(0, -1), indexPath.at(-1)! + 1],
nodes: [ResolvedHierarchy.clone(node)],
});
}
}
}
27 changes: 25 additions & 2 deletions packages/noya-component/src/resolvedHierarchy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { uuid } from 'noya-utils';
import { isDeepEqual, uuid } from 'noya-utils';
import { defineTree } from 'tree-visit';
import { NoyaResolvedNode } from './types';

Expand Down Expand Up @@ -84,4 +84,27 @@ function clone<T extends NoyaResolvedNode>(node: T): T {
}) as T;
}

export const ResolvedHierarchy = { ...Hierarchy, clone };
function findByPath(
node: NoyaResolvedNode,
path: string[] | undefined,
): NoyaResolvedNode | undefined {
return ResolvedHierarchy.find(node, (n) => isDeepEqual(n.path, path));
}

function findTypeByPath<T extends NoyaResolvedNode['type']>(
node: NoyaResolvedNode,
path: string[] | undefined,
type: T,
) {
return ResolvedHierarchy.find(
node,
(n) => n.type === type && isDeepEqual(n.path, path),
) as Extract<NoyaResolvedNode, { type: T }> | undefined;
}

export const ResolvedHierarchy = {
...Hierarchy,
clone,
findByPath,
findTypeByPath,
};
Loading

0 comments on commit ff8b53d

Please sign in to comment.