@opensumi/ide-comments 2.21.13 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/lib/browser/comments-feature.registry.js.map +1 -1
  2. package/lib/browser/comments-panel.view.d.ts +2 -3
  3. package/lib/browser/comments-panel.view.d.ts.map +1 -1
  4. package/lib/browser/comments-panel.view.js +32 -70
  5. package/lib/browser/comments-panel.view.js.map +1 -1
  6. package/lib/browser/comments-thread.d.ts +4 -2
  7. package/lib/browser/comments-thread.d.ts.map +1 -1
  8. package/lib/browser/comments-thread.js +14 -7
  9. package/lib/browser/comments-thread.js.map +1 -1
  10. package/lib/browser/comments-zone.service.js.map +1 -1
  11. package/lib/browser/comments-zone.view.d.ts +1 -1
  12. package/lib/browser/comments-zone.view.d.ts.map +1 -1
  13. package/lib/browser/comments-zone.view.js +1 -1
  14. package/lib/browser/comments-zone.view.js.map +1 -1
  15. package/lib/browser/comments.contribution.d.ts +1 -0
  16. package/lib/browser/comments.contribution.d.ts.map +1 -1
  17. package/lib/browser/comments.contribution.js +6 -1
  18. package/lib/browser/comments.contribution.js.map +1 -1
  19. package/lib/browser/comments.service.d.ts +7 -4
  20. package/lib/browser/comments.service.d.ts.map +1 -1
  21. package/lib/browser/comments.service.js +60 -87
  22. package/lib/browser/comments.service.js.map +1 -1
  23. package/lib/browser/index.d.ts +0 -1
  24. package/lib/browser/index.d.ts.map +1 -1
  25. package/lib/browser/index.js +5 -1
  26. package/lib/browser/index.js.map +1 -1
  27. package/lib/browser/tree/comment-node.d.ts +14 -0
  28. package/lib/browser/tree/comment-node.d.ts.map +1 -0
  29. package/lib/browser/tree/comment-node.js +64 -0
  30. package/lib/browser/tree/comment-node.js.map +1 -0
  31. package/lib/browser/tree/tree-model.service.d.ts +43 -0
  32. package/lib/browser/tree/tree-model.service.d.ts.map +1 -0
  33. package/lib/browser/tree/tree-model.service.js +150 -0
  34. package/lib/browser/tree/tree-model.service.js.map +1 -0
  35. package/lib/browser/tree/tree-node.defined.d.ts +61 -0
  36. package/lib/browser/tree/tree-node.defined.d.ts.map +1 -0
  37. package/lib/browser/tree/tree-node.defined.js +116 -0
  38. package/lib/browser/tree/tree-node.defined.js.map +1 -0
  39. package/lib/browser/tree/tree-node.module.less +154 -0
  40. package/lib/common/index.d.ts +38 -36
  41. package/lib/common/index.d.ts.map +1 -1
  42. package/lib/common/index.js.map +1 -1
  43. package/package.json +13 -12
  44. package/src/browser/comment-reactions.view.tsx +109 -0
  45. package/src/browser/comments-body.tsx +57 -0
  46. package/src/browser/comments-feature.registry.ts +91 -0
  47. package/src/browser/comments-item.view.tsx +362 -0
  48. package/src/browser/comments-panel.view.tsx +90 -0
  49. package/src/browser/comments-textarea.view.tsx +194 -0
  50. package/src/browser/comments-thread.ts +309 -0
  51. package/src/browser/comments-zone.service.ts +29 -0
  52. package/src/browser/comments-zone.view.tsx +206 -0
  53. package/src/browser/comments.contribution.ts +201 -0
  54. package/src/browser/comments.module.less +210 -0
  55. package/src/browser/comments.service.ts +546 -0
  56. package/src/browser/index.ts +29 -0
  57. package/src/browser/markdown.style.ts +25 -0
  58. package/src/browser/mentions.style.ts +55 -0
  59. package/src/browser/tree/comment-node.tsx +130 -0
  60. package/src/browser/tree/tree-model.service.ts +173 -0
  61. package/src/browser/tree/tree-node.defined.ts +167 -0
  62. package/src/browser/tree/tree-node.module.less +154 -0
  63. package/src/common/index.ts +710 -0
  64. package/src/index.ts +1 -0
@@ -0,0 +1,130 @@
1
+ import cls from 'classnames';
2
+ import React, { useCallback } from 'react';
3
+
4
+ import { INodeRendererProps, ClasslistComposite } from '@opensumi/ide-components';
5
+ import { getIcon } from '@opensumi/ide-core-browser';
6
+
7
+ import { CommentContentNode, CommentFileNode, CommentReplyNode } from './tree-node.defined';
8
+ import styles from './tree-node.module.less';
9
+
10
+ export interface ICommentNodeProps {
11
+ item: any;
12
+ defaultLeftPadding?: number;
13
+ leftPadding?: number;
14
+ decorations?: ClasslistComposite;
15
+ onClick: (ev: React.MouseEvent, item: CommentContentNode | CommentFileNode) => void;
16
+ }
17
+
18
+ export type ICommentNodeRenderedProps = ICommentNodeProps & INodeRendererProps;
19
+
20
+ export const CommentNodeRendered: React.FC<ICommentNodeRenderedProps> = ({
21
+ item,
22
+ defaultLeftPadding = 8,
23
+ leftPadding = 8,
24
+ decorations,
25
+ onClick,
26
+ }: ICommentNodeRenderedProps) => {
27
+ const handleClick = useCallback(
28
+ (ev: React.MouseEvent) => {
29
+ if (item.onSelect) {
30
+ item.onSelect(item);
31
+ }
32
+ onClick(ev, item as CommentContentNode);
33
+ },
34
+ [onClick],
35
+ );
36
+
37
+ const paddingLeft = `${
38
+ defaultLeftPadding +
39
+ (item.depth || 0) * (leftPadding || 0) +
40
+ (CommentContentNode.is(item) ? 16 : CommentFileNode.is(item) ? 0 : 28)
41
+ }px`;
42
+
43
+ const renderedNodeStyle = {
44
+ height: COMMENT_TREE_NODE_HEIGHT,
45
+ lineHeight: `${COMMENT_TREE_NODE_HEIGHT}px`,
46
+ paddingLeft,
47
+ } as React.CSSProperties;
48
+
49
+ const renderIcon = useCallback((node: CommentFileNode | CommentContentNode | CommentReplyNode) => {
50
+ if (CommentContentNode.is(node) || CommentFileNode.is(node)) {
51
+ return (
52
+ <div
53
+ className={cls(styles.icon, node.icon)}
54
+ style={{
55
+ height: COMMENT_TREE_NODE_HEIGHT,
56
+ lineHeight: `${COMMENT_TREE_NODE_HEIGHT}px`,
57
+ }}
58
+ ></div>
59
+ );
60
+ }
61
+ }, []);
62
+
63
+ const renderDisplayName = useCallback((node: CommentFileNode | CommentContentNode | CommentReplyNode) => {
64
+ if (CommentContentNode.is(node)) {
65
+ return (
66
+ <div className={cls(styles.segment, styles.displayname)}>
67
+ {node.renderedLabel ? (
68
+ node.renderedLabel
69
+ ) : (
70
+ <>
71
+ {node.comment}
72
+ <span className={styles.separator}>·</span>
73
+ {node.author.name}
74
+ </>
75
+ )}
76
+ </div>
77
+ );
78
+ } else {
79
+ return <div className={cls(styles.segment, styles.displayname)}>{node.renderedLabel}</div>;
80
+ }
81
+ }, []);
82
+
83
+ const renderDescription = useCallback(
84
+ (node: CommentFileNode | CommentContentNode | CommentReplyNode) => (
85
+ <div className={cls(styles.segment_grow, styles.description)}>{node.renderedDescription}</div>
86
+ ),
87
+ [],
88
+ );
89
+
90
+ const renderFolderToggle = useCallback(
91
+ (node: CommentFileNode) => (
92
+ <div
93
+ className={cls(styles.segment, styles.expansion_toggle, getIcon('arrow-right'), {
94
+ [`${styles.mod_collapsed}`]: !(node as CommentFileNode).expanded,
95
+ })}
96
+ />
97
+ ),
98
+ [],
99
+ );
100
+
101
+ const renderTwice = useCallback((node: CommentFileNode | CommentContentNode | CommentReplyNode) => {
102
+ if (CommentFileNode.is(node)) {
103
+ return renderFolderToggle(node as CommentFileNode);
104
+ }
105
+ }, []);
106
+
107
+ const getItemTooltip = useCallback(() => item.tooltip, [item]);
108
+
109
+ return (
110
+ <div
111
+ key={item.id}
112
+ onClick={handleClick}
113
+ title={getItemTooltip()}
114
+ className={cls(styles.search_node, decorations ? decorations.classlist : null)}
115
+ style={renderedNodeStyle}
116
+ data-id={item.id}
117
+ >
118
+ <div className={styles.content}>
119
+ {renderTwice(item)}
120
+ {renderIcon(item)}
121
+ <div className={styles.overflow_wrap}>
122
+ {renderDisplayName(item)}
123
+ {renderDescription(item)}
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ };
129
+
130
+ export const COMMENT_TREE_NODE_HEIGHT = 22;
@@ -0,0 +1,173 @@
1
+ /* eslint-disable import/order */
2
+ import { Injectable, Autowired, INJECTOR_TOKEN, Injector, Optional } from '@opensumi/di';
3
+ import { DecorationsManager, Decoration, IRecycleTreeHandle, TreeModel } from '@opensumi/ide-components';
4
+ import { DisposableCollection, Emitter, Event, Disposable } from '@opensumi/ide-core-browser';
5
+ import { ICommentsService } from '../../common/index';
6
+
7
+ import { CommentContentNode, CommentFileNode, CommentReplyNode, CommentRoot } from './tree-node.defined';
8
+ import styles from './tree-node.module.less';
9
+
10
+ export interface IEditorTreeHandle extends IRecycleTreeHandle {
11
+ hasDirectFocus: () => boolean;
12
+ }
13
+
14
+ @Injectable({ multiple: true })
15
+ export class CommentTreeModel extends TreeModel {
16
+ constructor(@Optional() root: CommentRoot) {
17
+ super();
18
+ this.init(root);
19
+ }
20
+ }
21
+
22
+ @Injectable()
23
+ export class CommentModelService extends Disposable {
24
+ @Autowired(ICommentsService)
25
+ private readonly commentService: ICommentsService;
26
+
27
+ @Autowired(INJECTOR_TOKEN)
28
+ private readonly injector: Injector;
29
+
30
+ private _treeModel: CommentTreeModel;
31
+
32
+ private _whenReady: Promise<void>;
33
+
34
+ private _decorations: DecorationsManager;
35
+ private _commentTreeHandle: IRecycleTreeHandle;
36
+
37
+ // All decoration
38
+ private selectedDecoration: Decoration = new Decoration(styles.mod_selected); // selected
39
+ private focusedDecoration: Decoration = new Decoration(styles.mod_focused); // focused
40
+
41
+ private _focusedNode: CommentFileNode | CommentContentNode | CommentReplyNode | null;
42
+ private _selectedNodes: (CommentFileNode | CommentContentNode | CommentReplyNode)[] = [];
43
+
44
+ private onDidUpdateTreeModelEmitter: Emitter<CommentTreeModel | undefined> = new Emitter();
45
+
46
+ private disposableCollection: DisposableCollection = new DisposableCollection();
47
+
48
+ constructor() {
49
+ super();
50
+ this._whenReady = this.initTreeModel();
51
+ }
52
+
53
+ get onDidUpdateTreeModel(): Event<CommentTreeModel | undefined> {
54
+ return this.onDidUpdateTreeModelEmitter.event;
55
+ }
56
+
57
+ get whenReady() {
58
+ return this._whenReady;
59
+ }
60
+
61
+ get commentTreeHandle() {
62
+ return this._commentTreeHandle;
63
+ }
64
+
65
+ get decorations() {
66
+ return this._decorations;
67
+ }
68
+
69
+ get treeModel() {
70
+ return this._treeModel;
71
+ }
72
+
73
+ get focusedNode() {
74
+ return this._focusedNode;
75
+ }
76
+
77
+ get selectedNodes() {
78
+ return this._selectedNodes;
79
+ }
80
+
81
+ async initTreeModel() {
82
+ const childs = await this.commentService.resolveChildren();
83
+ if (!childs) {
84
+ return;
85
+ }
86
+ const root = childs[0];
87
+ if (!root) {
88
+ return;
89
+ }
90
+ this._treeModel = this.injector.get<any>(CommentTreeModel, [root]);
91
+ await this._treeModel.ensureReady;
92
+
93
+ this.initDecorations(root);
94
+
95
+ this.disposables.push(
96
+ this.commentService.onThreadsCommentChange(() => {
97
+ this.refresh();
98
+ }),
99
+ );
100
+
101
+ this.onDidUpdateTreeModelEmitter.fire(this._treeModel);
102
+ }
103
+
104
+ initDecorations(root) {
105
+ this._decorations = new DecorationsManager(root as any);
106
+ this._decorations.addDecoration(this.selectedDecoration);
107
+ this._decorations.addDecoration(this.focusedDecoration);
108
+ return this._decorations;
109
+ }
110
+
111
+ handleTreeHandler(handle: IRecycleTreeHandle) {
112
+ this._commentTreeHandle = handle;
113
+ }
114
+
115
+ applyFocusedDecoration = (target: CommentFileNode | CommentContentNode | CommentReplyNode, dispatch = true) => {
116
+ if (target) {
117
+ for (const target of this._selectedNodes) {
118
+ this.selectedDecoration.removeTarget(target);
119
+ }
120
+ if (this.focusedNode) {
121
+ this.focusedDecoration.removeTarget(this.focusedNode);
122
+ }
123
+ this.selectedDecoration.addTarget(target);
124
+ this.focusedDecoration.addTarget(target);
125
+ this._focusedNode = target;
126
+ this._selectedNodes = [target];
127
+
128
+ dispatch && this.treeModel?.dispatchChange();
129
+ }
130
+ };
131
+
132
+ removeFocusedDecoration = () => {
133
+ if (this.focusedNode) {
134
+ this.focusedDecoration.removeTarget(this.focusedNode);
135
+ this.treeModel?.dispatchChange();
136
+ }
137
+ this._focusedNode = null;
138
+ };
139
+
140
+ handleTreeBlur = () => {
141
+ this.removeFocusedDecoration();
142
+ };
143
+
144
+ handleItemClick = async (ev: React.MouseEvent, node: CommentFileNode | CommentContentNode | CommentReplyNode) => {
145
+ this.applyFocusedDecoration(node);
146
+ if (CommentFileNode.is(node)) {
147
+ this.toggleDirectory(node);
148
+ } else if (node) {
149
+ }
150
+ };
151
+
152
+ toggleDirectory = (item: CommentFileNode | CommentContentNode) => {
153
+ if (item.expanded) {
154
+ this.commentTreeHandle.collapseNode(item);
155
+ } else {
156
+ this.commentTreeHandle.expandNode(item);
157
+ }
158
+ };
159
+
160
+ async refresh() {
161
+ await this.whenReady;
162
+ this.treeModel.root.refresh();
163
+ }
164
+
165
+ async collapsedAll() {
166
+ await this.whenReady;
167
+ return this.treeModel.root.collapsedAll();
168
+ }
169
+
170
+ dispose() {
171
+ this.disposableCollection.dispose();
172
+ }
173
+ }
@@ -0,0 +1,167 @@
1
+ import React from 'react';
2
+
3
+ import { TreeNode, CompositeTreeNode, ITree } from '@opensumi/ide-components';
4
+ import { URI } from '@opensumi/ide-core-common';
5
+
6
+ import { ICommentAuthorInformation, ICommentsService, ICommentsThread } from '../../common/index';
7
+
8
+ export class CommentRoot extends CompositeTreeNode {
9
+ static is(node: CommentFileNode | CommentRoot): node is CommentRoot {
10
+ return !!node && !node.parent;
11
+ }
12
+
13
+ constructor(tree: ICommentsService) {
14
+ super(tree as ITree, undefined);
15
+ }
16
+
17
+ get expanded() {
18
+ return true;
19
+ }
20
+ }
21
+
22
+ export class CommentFileNode extends CompositeTreeNode {
23
+ public static is(node: any): node is CommentFileNode {
24
+ return CompositeTreeNode.is(node) && 'threads' in node;
25
+ }
26
+
27
+ private _renderedLabel: string | React.ReactNode;
28
+ private _renderedDescription: string | React.ReactNode;
29
+
30
+ private _onSelectHandler: (node?: CommentFileNode) => void;
31
+
32
+ constructor(
33
+ tree: ICommentsService,
34
+ public threads: ICommentsThread[],
35
+ description = '',
36
+ public tooltip: string,
37
+ public icon: string,
38
+ public resource: URI,
39
+ parent: CommentRoot,
40
+ ) {
41
+ super(tree as ITree, parent);
42
+ this.isExpanded = true;
43
+ this._renderedLabel = this.resource.displayName;
44
+ this._renderedDescription = description;
45
+ }
46
+
47
+ set label(value: any) {
48
+ this._renderedLabel = value as string | React.ReactNode;
49
+ }
50
+
51
+ set description(value: any) {
52
+ this._renderedDescription = value as string | React.ReactNode;
53
+ }
54
+
55
+ get renderedLabel() {
56
+ return this._renderedLabel;
57
+ }
58
+
59
+ get renderedDescription() {
60
+ return this._renderedDescription;
61
+ }
62
+
63
+ set onSelect(handler: (node?: CommentFileNode) => void) {
64
+ this._onSelectHandler = handler;
65
+ }
66
+
67
+ get onSelect() {
68
+ return this._onSelectHandler;
69
+ }
70
+ }
71
+
72
+ export class CommentContentNode extends CompositeTreeNode {
73
+ public static is(node: any): node is CommentContentNode {
74
+ return CompositeTreeNode.is(node) && 'author' in node;
75
+ }
76
+
77
+ private _renderedLabel: string | React.ReactNode;
78
+ private _renderedDescription: string | React.ReactNode;
79
+
80
+ private _onSelectHandler: (node?: CommentContentNode) => void;
81
+
82
+ constructor(
83
+ tree: ICommentsService,
84
+ public thread: ICommentsThread,
85
+ public comment: string,
86
+ description = '',
87
+ public icon: string,
88
+ public author: ICommentAuthorInformation,
89
+ public resource: URI,
90
+ parent: CommentFileNode | undefined,
91
+ ) {
92
+ super(tree as ITree, parent);
93
+ this._renderedDescription = description;
94
+ }
95
+
96
+ get expanded() {
97
+ return true;
98
+ }
99
+
100
+ set label(value: string | React.ReactNode) {
101
+ this._renderedLabel = value;
102
+ }
103
+
104
+ set description(value: string | React.ReactNode) {
105
+ this._renderedDescription = value;
106
+ }
107
+
108
+ get renderedLabel() {
109
+ return this._renderedLabel;
110
+ }
111
+
112
+ get renderedDescription() {
113
+ return this._renderedDescription;
114
+ }
115
+
116
+ set onSelect(handler: (node?: CommentContentNode) => void) {
117
+ this._onSelectHandler = handler;
118
+ }
119
+
120
+ get onSelect() {
121
+ return this._onSelectHandler;
122
+ }
123
+ }
124
+
125
+ export class CommentReplyNode extends TreeNode {
126
+ private _renderedLabel: string | React.ReactNode;
127
+ private _renderedDescription: string | React.ReactNode;
128
+ private _onSelectHandler: (node?: CommentReplyNode) => void;
129
+
130
+ constructor(
131
+ tree: ICommentsService,
132
+ public thread: ICommentsThread,
133
+ label: string,
134
+ description = '',
135
+ public icon: string,
136
+ public resource: URI,
137
+ parent: CommentContentNode | undefined,
138
+ ) {
139
+ super(tree as ITree, parent);
140
+ this._renderedLabel = label;
141
+ this._renderedDescription = description;
142
+ }
143
+
144
+ set label(value: any) {
145
+ this._renderedLabel = value as string | React.ReactNode;
146
+ }
147
+
148
+ set description(value: any) {
149
+ this._renderedDescription = value as string | React.ReactNode;
150
+ }
151
+
152
+ get renderedLabel() {
153
+ return this._renderedLabel;
154
+ }
155
+
156
+ get renderedDescription() {
157
+ return this._renderedDescription;
158
+ }
159
+
160
+ set onSelect(handler: (node?: CommentReplyNode) => void) {
161
+ this._onSelectHandler = handler;
162
+ }
163
+
164
+ get onSelect() {
165
+ return this._onSelectHandler;
166
+ }
167
+ }
@@ -0,0 +1,154 @@
1
+ .search_node {
2
+ height: 22px;
3
+ line-height: 22px;
4
+ display: flex;
5
+ flex-direction: column;
6
+ position: relative;
7
+ cursor: pointer;
8
+
9
+ .displayname {
10
+ color: var(--foreground);
11
+ margin-right: 6px;
12
+ display: inline;
13
+ white-space: pre;
14
+ }
15
+
16
+ .description {
17
+ display: inline;
18
+ color: var(--descriptionForeground);
19
+ }
20
+
21
+ .separator {
22
+ margin: 0 3px;
23
+ }
24
+
25
+ .content {
26
+ position: relative;
27
+ display: flex;
28
+ align-items: center;
29
+ width: 100%;
30
+ }
31
+
32
+ .status {
33
+ text-align: center;
34
+ font-size: 12px;
35
+ }
36
+
37
+ .segment {
38
+ flex-grow: 0;
39
+ white-space: nowrap;
40
+ color: inherit;
41
+ &_grow {
42
+ flex-grow: 1 !important;
43
+ text-align: left;
44
+ z-index: 10;
45
+ padding-right: 5px;
46
+
47
+ &.overflow_visible {
48
+ overflow: visible !important;
49
+ }
50
+ }
51
+ }
52
+
53
+ .segment_grow {
54
+ flex-grow: 1 !important;
55
+ text-align: left;
56
+ z-index: 10;
57
+ padding-right: 5px;
58
+
59
+ &.overflow_visible {
60
+ overflow: visible !important;
61
+ }
62
+ }
63
+
64
+ .tail {
65
+ text-align: center;
66
+ margin-right: 10px;
67
+ position: relative;
68
+ height: 22px;
69
+ display: flex;
70
+ align-items: center;
71
+ }
72
+
73
+ .overflow_wrap {
74
+ flex: 1;
75
+ flex-shrink: 0;
76
+ overflow: hidden;
77
+ text-overflow: ellipsis;
78
+ white-space: nowrap;
79
+ text-align: left;
80
+ color: var(--foreground);
81
+ }
82
+
83
+ .flex_wrap {
84
+ display: flex;
85
+ flex-direction: row;
86
+ }
87
+
88
+ .icon {
89
+ position: relative;
90
+ color: var(--icon-foreground);
91
+ margin-right: 4px;
92
+ &:before {
93
+ font-size: 16px;
94
+ text-align: center;
95
+ }
96
+ }
97
+ &:hover {
98
+ color: var(--kt-tree-hoverForeground);
99
+ background: var(--kt-tree-hoverBackground);
100
+ .action_bar {
101
+ display: block;
102
+ }
103
+ &::before {
104
+ display: none;
105
+ }
106
+ }
107
+
108
+ &.mod_selected {
109
+ color: var(--kt-tree-inactiveSelectionForeground);
110
+ background: var(--kt-tree-inactiveSelectionBackground);
111
+ .expansion_toggle,
112
+ .description {
113
+ color: var(--kt-tree-inactiveSelectionForeground);
114
+ }
115
+ }
116
+
117
+ &.mod_focused {
118
+ outline: 1px solid var(--list-focusOutline);
119
+ outline-offset: -1px;
120
+ .expansion_toggle,
121
+ .description {
122
+ color: var(--kt-tree-activeSelectionForeground);
123
+ }
124
+ }
125
+
126
+ &.mod_actived {
127
+ outline: 1px solid var(--list-focusOutline);
128
+ outline-offset: -1px;
129
+ .expansion_toggle {
130
+ color: var(--kt-tree-activeSelectionForeground);
131
+ }
132
+ }
133
+ }
134
+
135
+ .expansion_toggle {
136
+ min-width: 20px;
137
+ display: flex;
138
+ justify-content: center;
139
+ flex-shrink: 0;
140
+ font-size: 16px;
141
+ color: var(--foreground);
142
+ &.mod_collapsed {
143
+ &:before {
144
+ display: block;
145
+ }
146
+ }
147
+
148
+ &:not(.mod_collapsed) {
149
+ &:before {
150
+ display: block;
151
+ transform: rotate(90deg);
152
+ }
153
+ }
154
+ }