@jupyter/collaboration 3.1.0 → 4.0.0-beta.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.
- package/lib/collaboratorspanel.js +19 -15
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/users-item.d.ts +85 -0
- package/lib/users-item.js +90 -0
- package/package.json +7 -7
- package/style/base.css +1 -0
- package/style/users-item.css +20 -0
|
@@ -49,15 +49,19 @@ export class CollaboratorsPanel extends Panel {
|
|
|
49
49
|
*/
|
|
50
50
|
this._onAwarenessChanged = () => {
|
|
51
51
|
const state = this._awareness.getStates();
|
|
52
|
-
const
|
|
52
|
+
const collaboratorsMap = new Map();
|
|
53
53
|
state.forEach((value, key) => {
|
|
54
54
|
if (this._currentUser.isReady &&
|
|
55
55
|
value.user &&
|
|
56
56
|
value.user.username !== this._currentUser.identity.username) {
|
|
57
|
-
|
|
57
|
+
const uniqueKey = `${value.user.username}-${value.current || 'no-current'}`;
|
|
58
|
+
if (!collaboratorsMap.has(uniqueKey)) {
|
|
59
|
+
collaboratorsMap.set(uniqueKey, value);
|
|
60
|
+
}
|
|
58
61
|
}
|
|
59
62
|
});
|
|
60
|
-
|
|
63
|
+
// Convert map to array to maintain the same emit interface
|
|
64
|
+
this._collaboratorsChanged.emit(Array.from(collaboratorsMap.values()));
|
|
61
65
|
};
|
|
62
66
|
this._collaboratorsChanged = new Signal(this);
|
|
63
67
|
this._awareness = awareness;
|
|
@@ -72,8 +76,9 @@ export function CollaboratorsBody(props) {
|
|
|
72
76
|
props.collaboratorsChanged.connect((_, value) => {
|
|
73
77
|
setCollaborators(value);
|
|
74
78
|
});
|
|
75
|
-
return (React.createElement("div", { className: COLLABORATORS_LIST_CLASS }, collaborators.map(
|
|
76
|
-
|
|
79
|
+
return (React.createElement("div", { className: COLLABORATORS_LIST_CLASS }, collaborators.map(collaborator => {
|
|
80
|
+
const uniqueKey = `${collaborator.user.username}-${collaborator.current || 'no-current'}`;
|
|
81
|
+
return (React.createElement(Collaborator, { key: uniqueKey, collaborator: collaborator, fileopener: props.fileopener, docRegistry: props.docRegistry }));
|
|
77
82
|
})));
|
|
78
83
|
}
|
|
79
84
|
export function Collaborator(props) {
|
|
@@ -81,26 +86,25 @@ export function Collaborator(props) {
|
|
|
81
86
|
const { collaborator, fileopener } = props;
|
|
82
87
|
let currentMain = '';
|
|
83
88
|
if (collaborator.current) {
|
|
89
|
+
// Discard widget tracker prefix (e.g. `notebook:` or `editor:`)
|
|
84
90
|
const path = collaborator.current.split(':');
|
|
85
|
-
currentMain = `${path[1]}
|
|
91
|
+
currentMain = `${path[1]}`;
|
|
86
92
|
}
|
|
87
93
|
const documents = collaborator.documents || [];
|
|
88
94
|
const docs = documents.map(document => {
|
|
89
95
|
var _a, _b;
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const icon = fileTypes ? fileTypes[0].icon : fileIcon;
|
|
96
|
+
const fileTypes = (_b = (_a = props.docRegistry) === null || _a === void 0 ? void 0 : _a.getFileTypesForPath(document)) === null || _b === void 0 ? void 0 : _b.filter(ft => ft.icon !== undefined);
|
|
97
|
+
const icon = (fileTypes === null || fileTypes === void 0 ? void 0 : fileTypes.length) ? fileTypes[0].icon : fileIcon;
|
|
93
98
|
const iconClass = fileTypes
|
|
94
99
|
? fileTypes[0].iconClass
|
|
95
100
|
: undefined;
|
|
96
101
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
? path[1]
|
|
102
|
+
filename: document.length > 40
|
|
103
|
+
? document
|
|
100
104
|
.slice(0, 10)
|
|
101
105
|
.concat('…')
|
|
102
|
-
.concat(
|
|
103
|
-
:
|
|
106
|
+
.concat(document.slice(document.length - 15))
|
|
107
|
+
: document,
|
|
104
108
|
fileLocation: document,
|
|
105
109
|
icon,
|
|
106
110
|
iconClass
|
|
@@ -127,6 +131,6 @@ export function Collaborator(props) {
|
|
|
127
131
|
? `${COLLABORATOR_FILE_CLASS} jp-mod-running`
|
|
128
132
|
: COLLABORATOR_FILE_CLASS), key: doc.filename, onClick: () => fileopener(doc.fileLocation) },
|
|
129
133
|
React.createElement(LabIcon.resolveReact, { icon: doc.icon, iconClass: doc.iconClass, tag: 'span', className: 'jp-DirListing-itemIcon', stylesheet: 'listing' }),
|
|
130
|
-
React.createElement("span", { className: 'jp-DirListing-itemText', title: doc.
|
|
134
|
+
React.createElement("span", { className: 'jp-DirListing-itemText', title: doc.fileLocation }, doc.filename)));
|
|
131
135
|
})))));
|
|
132
136
|
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
2
|
+
import { User } from '@jupyterlab/services';
|
|
3
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
/**
|
|
6
|
+
* The namespace for the UsersItem component.
|
|
7
|
+
*/
|
|
8
|
+
export declare namespace UsersItem {
|
|
9
|
+
/**
|
|
10
|
+
* Properties of the component.
|
|
11
|
+
*/
|
|
12
|
+
interface IProps {
|
|
13
|
+
/**
|
|
14
|
+
* The model of the document.
|
|
15
|
+
*/
|
|
16
|
+
model: DocumentRegistry.IModel | null;
|
|
17
|
+
/**
|
|
18
|
+
* A function to display the user icons, optional.
|
|
19
|
+
* This function will overwrite the default one, and can be used to handle event on
|
|
20
|
+
* icons.
|
|
21
|
+
*/
|
|
22
|
+
iconRenderer?: (props: UsersItem.IIconRendererProps) => JSX.Element;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The state of the component.
|
|
26
|
+
*/
|
|
27
|
+
interface IState {
|
|
28
|
+
/**
|
|
29
|
+
* The user list.
|
|
30
|
+
*/
|
|
31
|
+
usersList: IUserData[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Properties send to the iconRenderer function.
|
|
35
|
+
*/
|
|
36
|
+
interface IIconRendererProps extends React.HTMLAttributes<HTMLElement> {
|
|
37
|
+
/**
|
|
38
|
+
* The user.
|
|
39
|
+
*/
|
|
40
|
+
user: IUserData;
|
|
41
|
+
/**
|
|
42
|
+
* The document's model.
|
|
43
|
+
*/
|
|
44
|
+
model?: DocumentRegistry.IModel;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The user data type.
|
|
48
|
+
*/
|
|
49
|
+
type IUserData = {
|
|
50
|
+
/**
|
|
51
|
+
* User id (the client id of the awareness).
|
|
52
|
+
*/
|
|
53
|
+
userId: number;
|
|
54
|
+
/**
|
|
55
|
+
* User data.
|
|
56
|
+
*/
|
|
57
|
+
userData: User.IIdentity;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A component displaying the collaborative users of a document.
|
|
62
|
+
*/
|
|
63
|
+
export declare class UsersItem extends React.Component<UsersItem.IProps, UsersItem.IState> {
|
|
64
|
+
constructor(props: UsersItem.IProps);
|
|
65
|
+
/**
|
|
66
|
+
* Static method to create a widget.
|
|
67
|
+
*/
|
|
68
|
+
static createWidget(options: UsersItem.IProps): ReactWidget;
|
|
69
|
+
componentDidMount(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Filter out the duplicated users, which can happen temporary on reload.
|
|
72
|
+
*/
|
|
73
|
+
private filterDuplicated;
|
|
74
|
+
render(): React.ReactNode;
|
|
75
|
+
/**
|
|
76
|
+
* Triggered when a change occurs in the document awareness, to build again the users list.
|
|
77
|
+
*/
|
|
78
|
+
private _awarenessChange;
|
|
79
|
+
private _model;
|
|
80
|
+
private _iconRenderer;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Default renderer for the user icon.
|
|
84
|
+
*/
|
|
85
|
+
export declare function DefaultIconRenderer(props: UsersItem.IIconRendererProps): JSX.Element;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { classes, ReactWidget } from '@jupyterlab/ui-components';
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
const USERS_ITEM_CLASS = 'jp-toolbar-users-item';
|
|
8
|
+
/**
|
|
9
|
+
* A component displaying the collaborative users of a document.
|
|
10
|
+
*/
|
|
11
|
+
export class UsersItem extends React.Component {
|
|
12
|
+
constructor(props) {
|
|
13
|
+
var _a;
|
|
14
|
+
super(props);
|
|
15
|
+
/**
|
|
16
|
+
* Triggered when a change occurs in the document awareness, to build again the users list.
|
|
17
|
+
*/
|
|
18
|
+
this._awarenessChange = () => {
|
|
19
|
+
var _a;
|
|
20
|
+
const clients = (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.getStates();
|
|
21
|
+
const users = [];
|
|
22
|
+
if (clients) {
|
|
23
|
+
clients.forEach((val, key) => {
|
|
24
|
+
if (val.user) {
|
|
25
|
+
users.push({ userId: key, userData: val.user });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
this.setState(old => ({ ...old, usersList: users }));
|
|
30
|
+
};
|
|
31
|
+
this._model = props.model;
|
|
32
|
+
this._iconRenderer = (_a = props.iconRenderer) !== null && _a !== void 0 ? _a : null;
|
|
33
|
+
this.state = { usersList: [] };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Static method to create a widget.
|
|
37
|
+
*/
|
|
38
|
+
static createWidget(options) {
|
|
39
|
+
return ReactWidget.create(React.createElement(UsersItem, { ...options }));
|
|
40
|
+
}
|
|
41
|
+
componentDidMount() {
|
|
42
|
+
var _a;
|
|
43
|
+
(_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.on('change', this._awarenessChange);
|
|
44
|
+
this._awarenessChange();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Filter out the duplicated users, which can happen temporary on reload.
|
|
48
|
+
*/
|
|
49
|
+
filterDuplicated(usersList) {
|
|
50
|
+
var _a;
|
|
51
|
+
const newList = [];
|
|
52
|
+
const selected = new Set();
|
|
53
|
+
for (const element of usersList) {
|
|
54
|
+
if (((_a = element === null || element === void 0 ? void 0 : element.userData) === null || _a === void 0 ? void 0 : _a.username) &&
|
|
55
|
+
!selected.has(element.userData.username)) {
|
|
56
|
+
selected.add(element.userData.username);
|
|
57
|
+
newList.push(element);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return newList;
|
|
61
|
+
}
|
|
62
|
+
render() {
|
|
63
|
+
var _a;
|
|
64
|
+
const IconRenderer = (_a = this._iconRenderer) !== null && _a !== void 0 ? _a : DefaultIconRenderer;
|
|
65
|
+
return (React.createElement("div", { className: USERS_ITEM_CLASS }, this.filterDuplicated(this.state.usersList).map(user => {
|
|
66
|
+
if (this._model &&
|
|
67
|
+
user.userId !== this._model.sharedModel.awareness.clientID) {
|
|
68
|
+
return IconRenderer({ user, model: this._model });
|
|
69
|
+
}
|
|
70
|
+
})));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Default renderer for the user icon.
|
|
75
|
+
*/
|
|
76
|
+
export function DefaultIconRenderer(props) {
|
|
77
|
+
let el;
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
79
|
+
const { user, model, ...htmlProps } = props;
|
|
80
|
+
const iconClasses = classes('lm-MenuBar-itemIcon', props.className || '');
|
|
81
|
+
if (user.userData.avatar_url) {
|
|
82
|
+
el = (React.createElement("div", { ...htmlProps, key: user.userId, title: user.userData.display_name, className: classes(iconClasses, 'jp-MenuBar-imageIcon') },
|
|
83
|
+
React.createElement("img", { src: user.userData.avatar_url, alt: "" })));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
el = (React.createElement("div", { ...htmlProps, key: user.userId, title: user.userData.display_name, className: classes(iconClasses, 'jp-MenuBar-anonymousIcon'), style: { backgroundColor: user.userData.color } },
|
|
87
|
+
React.createElement("span", null, user.userData.initials)));
|
|
88
|
+
}
|
|
89
|
+
return el;
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/collaboration",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-beta.0",
|
|
4
4
|
"description": "JupyterLab - Real-Time Collaboration Widgets",
|
|
5
5
|
"homepage": "https://github.com/jupyterlab/jupyter-collaboration",
|
|
6
6
|
"bugs": {
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@codemirror/state": "^6.2.0",
|
|
43
43
|
"@codemirror/view": "^6.7.0",
|
|
44
|
-
"@jupyterlab/apputils": "^4.0.
|
|
45
|
-
"@jupyterlab/coreutils": "^6.0.
|
|
46
|
-
"@jupyterlab/docregistry": "^4.0.
|
|
47
|
-
"@jupyterlab/services": "^7.0.
|
|
48
|
-
"@jupyterlab/ui-components": "^4.0.
|
|
44
|
+
"@jupyterlab/apputils": "^4.4.0-alpha.2",
|
|
45
|
+
"@jupyterlab/coreutils": "^6.4.0-alpha.2",
|
|
46
|
+
"@jupyterlab/docregistry": "^4.4.0-alpha.2",
|
|
47
|
+
"@jupyterlab/services": "^7.4.0-alpha.2",
|
|
48
|
+
"@jupyterlab/ui-components": "^4.4.0-alpha.2",
|
|
49
49
|
"@lumino/coreutils": "^2.1.0",
|
|
50
50
|
"@lumino/signaling": "^2.0.0",
|
|
51
51
|
"@lumino/virtualdom": "^2.0.0",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/react": "~18.3.1",
|
|
59
59
|
"rimraf": "^4.1.2",
|
|
60
|
-
"typescript": "~5.
|
|
60
|
+
"typescript": "~5.1.6"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"access": "public"
|
package/style/base.css
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
| Copyright (c) Jupyter Development Team.
|
|
3
|
+
| Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|---------------------------------------------------------------------------- */
|
|
5
|
+
|
|
6
|
+
.jp-toolbar-users-item {
|
|
7
|
+
flex-grow: 1;
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: row;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.jp-toolbar-users-item .jp-MenuBar-anonymousIcon,
|
|
13
|
+
.jp-toolbar-users-item .jp-MenuBar-imageIcon {
|
|
14
|
+
position: relative;
|
|
15
|
+
left: 0;
|
|
16
|
+
height: 22px;
|
|
17
|
+
width: 22px;
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
cursor: default;
|
|
20
|
+
}
|