Skip to content

Commit 54d7d8e

Browse files
authored
QA-15281 : copy react-matrials component to design-system-kit package to prevent endless loop in dependencies resolution. (#276)
1 parent 2179eb3 commit 54d7d8e

File tree

12 files changed

+353
-4
lines changed

12 files changed

+353
-4
lines changed

packages/design-system-kit/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"license": "MIT",
2323
"peerDependencies": {
2424
"@material-ui/core": "^3.9.3",
25-
"react": "^16.8.0"
25+
"react": "^16.8.0",
26+
"rxjs": "^6.4.0"
2627
},
2728
"dependencies": {
2829
"@material-ui/core": "^3.9.3",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as React from 'react';
2+
3+
export interface DisplayActionProps {
4+
}
5+
6+
export class DisplayAction extends React.Component<DisplayActionProps, any> {
7+
render(): JSX.Element;
8+
9+
}
10+
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {actionsRegistry} from './actionsRegistry';
4+
import * as _ from 'lodash';
5+
import {Observable, combineLatest, of} from 'rxjs';
6+
import {first} from 'rxjs/operators';
7+
8+
let count = 0;
9+
10+
class StateActionComponent extends React.Component {
11+
constructor(props) {
12+
super(props);
13+
this.state = {};
14+
}
15+
16+
render() {
17+
const enhancedContext = {...this.props.context, ...this.state};
18+
19+
if (enhancedContext.displayDisabled || (enhancedContext.enabled !== false && enhancedContext.enabled !== null)) {
20+
const Render = this.props.render;
21+
if (enhancedContext.actions) {
22+
return _.map(enhancedContext.actions, action => (
23+
<Render key={action.key}
24+
context={{
25+
...enhancedContext,
26+
...action
27+
}}/>
28+
));
29+
}
30+
31+
return <Render context={enhancedContext}/>;
32+
}
33+
34+
return false;
35+
}
36+
}
37+
38+
StateActionComponent.propTypes = {
39+
context: PropTypes.object.isRequired,
40+
render: PropTypes.func.isRequired
41+
};
42+
43+
class DisplayActionComponent extends React.Component {
44+
constructor(props) {
45+
super(props);
46+
this.innerRef = React.createRef();
47+
this.state = {
48+
};
49+
}
50+
51+
render() {
52+
const {context, render} = this.props;
53+
54+
const {subscription} = this;
55+
if (subscription) {
56+
subscription.unsubscribe();
57+
}
58+
59+
let enhancedContext = {...context};
60+
if (enhancedContext.init) {
61+
enhancedContext.init(enhancedContext, _.omit(this.props, ['context']));
62+
}
63+
64+
// Check observers
65+
const observersObj = _.pickBy(enhancedContext, value => value instanceof Observable);
66+
const keys = Object.keys(observersObj);
67+
68+
if (keys.length > 0) {
69+
// Prepare an updateContext method for subscription - first set it as synchronous update of the context object
70+
const update = v => {
71+
if (this.innerRef.current) {
72+
this.innerRef.current.setState(v);
73+
} else {
74+
enhancedContext = _.assign(enhancedContext, v);
75+
}
76+
};
77+
78+
// Concat with a sync observer to always get an initial value
79+
const observers = Object.values(observersObj);
80+
81+
keys.forEach(k => _.set(enhancedContext, k, null));
82+
83+
// Related to https://jira.jahia.org/browse/QA-11271
84+
// this empty subscription is auto cancelled with the first operator
85+
// and resolve a problem where the observer was never resolved is some cases
86+
_.each(observers, observer => observer.pipe(first()).subscribe());
87+
88+
// Combine all observers into one
89+
const combinedObserver = combineLatest(...observers, (...vals) => _.zipObject(keys, vals));
90+
this.subscription = combinedObserver.subscribe(v => update(v));
91+
if (this.props.observerRef) {
92+
this.props.observerRef(combinedObserver);
93+
}
94+
} else if (this.props.observerRef) {
95+
this.props.observerRef(of(null));
96+
}
97+
98+
return <StateActionComponent ref={this.innerRef} context={enhancedContext} render={render}/>;
99+
}
100+
101+
componentWillUnmount() {
102+
if (this.subscription) {
103+
this.subscription.unsubscribe();
104+
}
105+
106+
const {context} = this.props;
107+
if (context.destroy) {
108+
context.destroy(context);
109+
}
110+
}
111+
}
112+
113+
DisplayActionComponent.defaultProps = {
114+
observerRef: null
115+
};
116+
117+
DisplayActionComponent.propTypes = {
118+
context: PropTypes.object.isRequired,
119+
render: PropTypes.func.isRequired,
120+
observerRef: PropTypes.func
121+
};
122+
123+
const shallowEquals = (obj1, obj2) =>
124+
Object.keys(obj1).length === Object.keys(obj2).length &&
125+
Object.keys(obj1).every(key => obj1[key] === obj2[key]);
126+
127+
class DisplayAction extends React.Component {
128+
constructor(props) {
129+
super(props);
130+
this.id = props.actionKey + '-' + (count++);
131+
132+
const {actionKey} = this.props;
133+
const action = actionsRegistry.get(actionKey);
134+
135+
let Component = DisplayActionComponent;
136+
137+
if (action.wrappers) {
138+
Component = _.reduce(action.wrappers, this.wrap.bind(this), DisplayActionComponent);
139+
}
140+
141+
this.Component = Component;
142+
}
143+
144+
shouldComponentUpdate(nextProps) {
145+
return !shallowEquals(nextProps.context, this.props.context);
146+
}
147+
148+
wrap(Render, wrapper) {
149+
return props => wrapper(<Render key={this.id} {...props}/>);
150+
}
151+
152+
render() {
153+
const {context, actionKey, render, observerRef} = this.props;
154+
const action = actionsRegistry.get(actionKey);
155+
const enhancedContext = {...action, ...context, originalContext: context, id: this.id, actionKey};
156+
157+
const {Component} = this;
158+
159+
return <Component key={this.id} context={enhancedContext} render={render} actionKey={actionKey} observerRef={observerRef}/>;
160+
}
161+
}
162+
163+
DisplayAction.defaultProps = {
164+
observerRef: null
165+
};
166+
167+
DisplayAction.propTypes = {
168+
actionKey: PropTypes.string.isRequired,
169+
context: PropTypes.object.isRequired,
170+
render: PropTypes.func.isRequired,
171+
observerRef: PropTypes.func
172+
};
173+
174+
export {DisplayAction};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as React from 'react';
2+
3+
export interface DisplayActionsProps {
4+
}
5+
6+
export class DisplayActions extends React.Component<DisplayActionsProps, any> {
7+
render(): JSX.Element;
8+
9+
}
10+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {actionsRegistry} from './actionsRegistry';
4+
import * as _ from 'lodash';
5+
import {DisplayAction} from './DisplayAction';
6+
7+
class DisplayActions extends React.Component {
8+
constructor(props) {
9+
super(props);
10+
this.observerRefs = [];
11+
}
12+
13+
render() {
14+
const {target, context, render, filter} = this.props;
15+
16+
let actionsToDisplay = _.filter(actionsRegistry.getAll(), action => _.includes(_.map(action.target, 'id'), target));
17+
actionsToDisplay = _.sortBy(actionsToDisplay, [function (o) {
18+
const found = _.find(o.target, t => t.id === target);
19+
20+
if (found && found.priority) {
21+
const priority = Number(found.priority);
22+
if (!isNaN(priority) && priority !== 0) {
23+
return priority;
24+
}
25+
}
26+
27+
// Should be placed at the end if no priority defined, returning 'undefined' is making the ordering bug on FF and Opera
28+
return 99999;
29+
}]);
30+
31+
if (filter) {
32+
actionsToDisplay = _.filter(actionsToDisplay, filter);
33+
}
34+
35+
return _.map(actionsToDisplay, action => <DisplayAction key={action.key} context={context} actionKey={action.key} render={render} observerRef={obs => this.observerRefs.push(obs)}/>);
36+
}
37+
}
38+
39+
DisplayActions.defaultProps = {
40+
filter: null
41+
};
42+
43+
DisplayActions.propTypes = {
44+
target: PropTypes.string.isRequired,
45+
context: PropTypes.object.isRequired,
46+
render: PropTypes.func.isRequired,
47+
filter: PropTypes.func
48+
};
49+
50+
export {DisplayActions};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as _ from 'lodash';
2+
import {composeActions} from './composeActions';
3+
4+
class Registry {
5+
constructor() {
6+
this.registry = {};
7+
}
8+
9+
add(key, ...actions) {
10+
const action = composeActions(this.registry[key], ...actions);
11+
action.key = key;
12+
13+
if (action.target) {
14+
action.target = _.map(action.target, t => {
15+
if (typeof t === 'string') {
16+
const spl = t.split(':');
17+
return ({id: spl[0], priority: spl[1] ? spl[1] : 0});
18+
}
19+
20+
return t;
21+
});
22+
}
23+
24+
this.registry[key] = action;
25+
}
26+
27+
get(key) {
28+
return this.registry[key];
29+
}
30+
31+
getAll() {
32+
return _.values(this.registry);
33+
}
34+
}
35+
36+
const actionsRegistry = new Registry();
37+
38+
export {actionsRegistry};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as _ from 'lodash';
2+
3+
function composeActions(...actions) {
4+
return _.reduce(actions, (acc, action) => {
5+
if (action) {
6+
_.forEach(action, (value, key) => {
7+
const previous = acc[key];
8+
if (typeof previous === 'function') {
9+
acc[key] = function (...args) {
10+
previous.apply(this, args);
11+
value.apply(this, args);
12+
};
13+
} else if (Array.isArray(previous)) {
14+
acc[key] = _.concat(previous, value);
15+
} else {
16+
acc[key] = value;
17+
}
18+
});
19+
}
20+
21+
return acc;
22+
}, {});
23+
}
24+
25+
export {composeActions};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export {actionsRegistry} from './actionsRegistry';
2+
export {DisplayAction} from './DisplayAction';
3+
export {DisplayActions} from './DisplayActions';
4+
export {toIconComponent} from './toIconComponent';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import {SvgIcon} from '@material-ui/core';
3+
4+
function toIconComponent(icon, props) {
5+
const camelCased = s => s.replace(/-([a-z])/g, g => g[1].toUpperCase());
6+
7+
const toComp = function (node, idx) {
8+
if (node.nodeType === 1) {
9+
const props = {key: idx};
10+
Array.prototype.slice.call(node.attributes).forEach(attr => {
11+
props[camelCased(attr.name)] = attr.value;
12+
});
13+
const children = Array.prototype.slice.call(node.childNodes).map((child, idx) => toComp(child, idx));
14+
return React.createElement(node.tagName, props, children);
15+
}
16+
};
17+
18+
if (typeof icon === 'string') {
19+
if (icon.startsWith('<svg')) {
20+
const parser = new DOMParser();
21+
const doc = parser.parseFromString(icon, 'image/svg+xml');
22+
const viewBox = doc.documentElement.attributes.viewBox ? doc.documentElement.attributes.viewBox.value : null;
23+
const children = Array.prototype.slice.call(doc.documentElement.childNodes).map((child, idx) => toComp(child, idx));
24+
return <SvgIcon viewBox={viewBox} {...props}>{children}</SvgIcon>;
25+
}
26+
27+
return <img src={icon}/>;
28+
}
29+
30+
if (props && icon) {
31+
return React.cloneElement(icon, props);
32+
}
33+
34+
return icon;
35+
}
36+
37+
export {toIconComponent};

packages/design-system-kit/src/components/LeftNavigation/LeftDrawerContent/LeftDrawerListItems.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {List, ListItem, Typography, withStyles} from '@material-ui/core';
33
import {ChevronRight, ExpandMore} from '@material-ui/icons';
44
import React from 'react';
55
import {lodash as _} from 'lodash';
6-
import {DisplayActions, toIconComponent} from '@jahia/react-material';
6+
import {DisplayActions, toIconComponent} from '../../../actions';
77
import {compose} from 'recompose';
88

99
const styles = theme => ({

0 commit comments

Comments
 (0)