Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/app-framework/index.ts
5759 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// Not sure where this should go...
7
declare global {
8
interface Window {
9
Primus: any;
10
}
11
}
12
13
// Important: code below now assumes that a global variable called "DEBUG" is **defined**!
14
declare var DEBUG: boolean;
15
if (DEBUG == null) {
16
var DEBUG = false;
17
}
18
19
let rclass: <P extends object>(
20
Component: React.ComponentType<P>,
21
) => React.ComponentType<P>;
22
23
import React from "react";
24
import createReactClass from "create-react-class";
25
import { Provider, connect, useSelector } from "react-redux";
26
import json_stable from "json-stable-stringify";
27
28
import { Store } from "@cocalc/util/redux/Store";
29
import { Actions } from "@cocalc/util/redux/Actions";
30
import { AppRedux as AppReduxBase } from "@cocalc/util/redux/AppRedux";
31
import { Table, TableConstructor } from "./Table";
32
33
// Relative import is temporary, until I figure this out -- needed for *project*
34
import { bind_methods, keys, is_valid_uuid_string } from "@cocalc/util/misc";
35
export { TypedMap, createTypedMap } from "@cocalc/util/redux/TypedMap";
36
import type { ClassMap } from "@cocalc/util/redux/types";
37
import { redux_name, project_redux_name } from "@cocalc/util/redux/name";
38
export { redux_name, project_redux_name };
39
import { NAME_TYPE as ComputeImageStoreType } from "../custom-software/util";
40
import { NEWS } from "@cocalc/frontend/notifications/news/init";
41
42
import * as types from "./actions-and-stores";
43
import type { ProjectStore } from "../project_store";
44
import type { ProjectActions } from "../project_actions";
45
export type { ProjectStore, ProjectActions };
46
47
export class AppRedux extends AppReduxBase {
48
private _tables: ClassMap<any, Table>;
49
50
constructor() {
51
super();
52
bind_methods(this);
53
this._tables = {};
54
}
55
56
getActions(name: "account"): types.AccountActions;
57
getActions(name: "projects"): types.ProjectsActions;
58
getActions(name: "billing"): types.BillingActions;
59
getActions(name: "page"): types.PageActions;
60
getActions(name: "users"): types.UsersActions;
61
getActions(name: "admin-users"): types.AdminUsersActions;
62
getActions(name: "admin-site-licenses"): types.SiteLicensesActions;
63
getActions(name: "mentions"): types.MentionsActions;
64
getActions(name: "messages"): types.MessagesActions;
65
getActions(name: "file_use"): types.FileUseActions;
66
getActions(name: typeof NEWS): types.NewsActions;
67
getActions(name: { project_id: string }): ProjectActions;
68
getActions<T, C extends Actions<T>>(name: string): C;
69
getActions<T, C extends Actions<T>>(
70
name: string | { project_id: string },
71
): C | ProjectActions | undefined {
72
if (typeof name === "string") {
73
if (!this.hasActions(name)) {
74
return undefined;
75
} else {
76
return this._actions[name];
77
}
78
} else {
79
if (name.project_id == null) {
80
throw Error("Object must have project_id attribute");
81
}
82
return this.getProjectActions(name.project_id);
83
}
84
}
85
86
getStore(name: "account"): types.AccountStore;
87
getStore(name: "projects"): types.ProjectsStore;
88
getStore(name: "billing"): types.BillingStore;
89
getStore(name: "page"): types.PageStore;
90
getStore(name: "admin-users"): types.AdminUsersStore;
91
getStore(name: "admin-site-licenses"): types.SiteLicensesStore;
92
getStore(name: "mentions"): types.MentionsStore;
93
getStore(name: "messages"): types.MessagesStore;
94
getStore(name: "file_use"): types.FileUseStore;
95
getStore(name: "customize"): types.CustomizeStore;
96
getStore(name: "users"): types.UsersStore;
97
getStore(name: ComputeImageStoreType): types.ComputeImagesStore;
98
getStore(name: typeof NEWS): types.NewsStore;
99
getStore<State extends Record<string, any>>(name: string): Store<State>;
100
getStore<State extends Record<string, any>, C extends Store<State>>(
101
nam: string,
102
): C | undefined;
103
getStore(name) {
104
return super.getStore(name);
105
}
106
107
getProjectsStore(): types.ProjectsStore {
108
return this.getStore("projects");
109
}
110
111
createTable<T extends Table>(
112
name: string,
113
table_class: TableConstructor<T>,
114
): T {
115
const tables = this._tables;
116
if (tables[name] != null) {
117
throw Error(`createTable: table "${name}" already exists`);
118
}
119
const table = new table_class(name, this);
120
return (tables[name] = table);
121
}
122
123
// Set the table; we assume that the table being overwritten
124
// has been cleaned up properly somehow...
125
setTable(name: string, table: Table): void {
126
this._tables[name] = table;
127
}
128
129
removeTable(name: string): void {
130
if (this._tables[name] != null) {
131
if (this._tables[name]._table != null) {
132
this._tables[name]._table.close();
133
}
134
delete this._tables[name];
135
}
136
}
137
138
getTable<T extends Table>(name: string): T {
139
if (this._tables[name] == null) {
140
throw Error(`getTable: table "${name}" not registered`);
141
}
142
return this._tables[name];
143
}
144
145
/**
146
* A React Hook to connect a function component to a project store.
147
* Opposed to `getProjectStore`, the project store will not initialize
148
* if it's not defined already.
149
*
150
* @param selectFrom selector to run on the store.
151
* The result will be compared to the previous result to determine
152
* if the component should rerender
153
* @param project_id id of the project to connect to
154
*/
155
useProjectStore<T>(
156
selectFrom: (store?: ProjectStore) => T,
157
project_id?: string,
158
): T {
159
// eslint-disable-next-line react-hooks/rules-of-hooks
160
return useSelector<any, T>((_) => {
161
let projectStore = undefined;
162
if (project_id) {
163
projectStore = this.getStore(project_redux_name(project_id)) as any;
164
}
165
return selectFrom(projectStore);
166
});
167
}
168
169
// getProject... is safe to call any time. All structures will be created
170
// if they don't exist
171
getProjectStore(project_id: string): ProjectStore {
172
if (!is_valid_uuid_string(project_id)) {
173
throw Error(`getProjectStore: INVALID project_id -- "${project_id}"`);
174
}
175
if (!this.hasProjectStore(project_id)) {
176
// Right now importing project_store breaks the share server,
177
// so we don't yet.
178
return require("../project_store").init(project_id, this);
179
} else {
180
return this.getStore(project_redux_name(project_id)) as any;
181
}
182
}
183
184
// TODO -- Typing: Type project Actions
185
// T, C extends Actions<T>
186
getProjectActions(project_id: string): ProjectActions {
187
if (!is_valid_uuid_string(project_id)) {
188
throw Error(`getProjectActions: INVALID project_id -- "${project_id}"`);
189
}
190
if (!this.hasProjectStore(project_id)) {
191
require("../project_store").init(project_id, this);
192
}
193
return this.getActions(project_redux_name(project_id)) as any;
194
}
195
// TODO -- Typing: Type project Table
196
getProjectTable(project_id: string, name: string): any {
197
if (!is_valid_uuid_string(project_id)) {
198
throw Error(`getProjectTable: INVALID project_id -- "${project_id}"`);
199
}
200
if (!this.hasProjectStore(project_id)) {
201
require("../project_store").init(project_id, this);
202
}
203
return this.getTable(project_redux_name(project_id, name));
204
}
205
206
removeProjectReferences(project_id: string): void {
207
if (!is_valid_uuid_string(project_id)) {
208
throw Error(
209
`getProjectReferences: INVALID project_id -- "${project_id}"`,
210
);
211
}
212
const name = project_redux_name(project_id);
213
const store = this.getStore(name);
214
store?.destroy?.();
215
this.removeActions(name);
216
this.removeStore(name);
217
}
218
219
// getEditorActions but for whatever editor -- this is mainly meant to be used
220
// from the console when debugging, e.g., smc.redux.currentEditorActions()
221
public currentEditor = (): {
222
project_id?: string;
223
path?: string;
224
account_id?: string;
225
actions?: Actions<any>;
226
store?: Store<any>;
227
} => {
228
const project_id = this.getStore("page").get("active_top_tab");
229
const current: {
230
project_id?: string;
231
path?: string;
232
account_id?: string;
233
actions?: Actions<any>;
234
store?: Store<any>;
235
} = { account_id: this.getStore("account")?.get("account_id") };
236
if (!is_valid_uuid_string(project_id)) {
237
return current;
238
}
239
current.project_id = project_id;
240
const store = this.getProjectStore(project_id);
241
const tab = store.get("active_project_tab");
242
if (!tab.startsWith("editor-")) {
243
return current;
244
}
245
const path = tab.slice("editor-".length);
246
current.path = path;
247
current.actions = this.getEditorActions(project_id, path);
248
current.store = this.getEditorStore(project_id, path);
249
return current;
250
};
251
}
252
253
const computed = (rtype) => {
254
const clone = rtype.bind({});
255
clone.is_computed = true;
256
return clone;
257
};
258
259
const rtypes = require("@cocalc/util/opts").types;
260
261
/*
262
Used by Provider to map app state to component props
263
264
rclass
265
reduxProps:
266
store_name :
267
prop : type
268
269
WARNING: If store not yet defined, then props will all be undefined for that store! There
270
is no warning/error in this case.
271
272
*/
273
const connect_component = (spec) => {
274
const map_state_to_props = function (state) {
275
const props = {};
276
if (state == null) {
277
return props;
278
}
279
for (const store_name in spec) {
280
if (store_name === "undefined") {
281
// "undefined" gets turned into this string when making a common mistake
282
console.warn("spec = ", spec);
283
throw Error(
284
"WARNING: redux spec is invalid because it contains 'undefined' as a key. " +
285
JSON.stringify(spec),
286
);
287
}
288
const info = spec[store_name];
289
const store: Store<any> | undefined = redux.getStore(store_name);
290
for (const prop in info) {
291
var val;
292
const type = info[prop];
293
294
if (type == null) {
295
throw Error(
296
`ERROR invalid redux spec: no type info set for prop '${prop}' in store '${store_name}', ` +
297
`where full spec has keys '${Object.keys(spec)}' ` +
298
`-- e.g. rtypes.bool vs. rtypes.boolean`,
299
);
300
}
301
302
if (store == undefined) {
303
val = undefined;
304
} else {
305
val = store.get(prop);
306
}
307
308
if (type.category === "IMMUTABLE") {
309
props[prop] = val;
310
} else {
311
props[prop] =
312
(val != null ? val.toJS : undefined) != null ? val.toJS() : val;
313
}
314
}
315
}
316
return props;
317
};
318
return connect(map_state_to_props);
319
};
320
321
/*
322
323
Takes an object to create a reactClass or a function which returns such an object.
324
325
Objects should be shaped like a react class save for a few exceptions:
326
x.reduxProps =
327
redux_store_name :
328
fields : value_type
329
name : type
330
331
x.actions must not be defined.
332
333
*/
334
335
// Uncomment (and also use below) for working on
336
// https://github.com/sagemathinc/cocalc/issues/4176
337
/*
338
function reduxPropsCheck(reduxProps: object) {
339
for (let store in reduxProps) {
340
const x = reduxProps[store];
341
if (x == null) continue;
342
for (let field in x) {
343
if (x[field] == rtypes.object) {
344
console.log(`WARNING: reduxProps object ${store}.${field}`);
345
}
346
}
347
}
348
}
349
*/
350
351
function compute_cache_key(data: { [key: string]: any }): string {
352
return json_stable(keys(data).sort())!;
353
}
354
355
rclass = function (x: any) {
356
let C;
357
if (typeof x === "function" && typeof x.reduxProps === "function") {
358
// using an ES6 class *and* reduxProps...
359
C = createReactClass({
360
render() {
361
if (this.cache0 == null) {
362
this.cache0 = {};
363
}
364
const reduxProps = x.reduxProps(this.props);
365
//reduxPropsCheck(reduxProps);
366
const key = compute_cache_key(reduxProps);
367
// console.log("ES6 rclass render", key);
368
if (this.cache0[key] == null) {
369
this.cache0[key] = connect_component(reduxProps)(x);
370
}
371
return React.createElement(
372
this.cache0[key],
373
this.props,
374
this.props.children,
375
);
376
},
377
});
378
return C;
379
} else if (typeof x === "function") {
380
// Creates a react class that wraps the eventual component.
381
// It calls the generator function with props as a parameter
382
// and caches the result based on reduxProps
383
const cached = createReactClass({
384
// This only caches per Component. No memory leak, but could be faster for multiple components with the same signature
385
render() {
386
if (this.cache == null) {
387
this.cache = {};
388
}
389
// OPTIMIZATION: Cache props before generating a new key.
390
// currently assumes making a new object is fast enough
391
const definition = x(this.props);
392
//reduxPropsCheck(definition.reduxProps);
393
const key = compute_cache_key(definition.reduxProps);
394
// console.log("function rclass render", key);
395
396
if (definition.actions != null) {
397
throw Error(
398
"You may not define a method named actions in an rclass. This is used to expose redux actions",
399
);
400
}
401
402
definition.actions = redux.getActions;
403
404
if (this.cache[key] == null) {
405
this.cache[key] = rclass(definition);
406
} // wait.. is this even the slow part?
407
408
return React.createElement(
409
this.cache[key],
410
this.props,
411
this.props.children,
412
);
413
},
414
});
415
416
return cached;
417
} else {
418
if (x.reduxProps != null) {
419
// Inject the propTypes based on the ones injected by reduxProps.
420
const propTypes = x.propTypes != null ? x.propTypes : {};
421
for (const store_name in x.reduxProps) {
422
const info = x.reduxProps[store_name];
423
for (const prop in info) {
424
const type = info[prop];
425
if (type !== rtypes.immutable) {
426
propTypes[prop] = type;
427
} else {
428
propTypes[prop] = rtypes.object;
429
}
430
}
431
}
432
x.propTypes = propTypes;
433
//reduxPropsCheck(propTypes);
434
}
435
436
if (x.actions != null && x.actions !== redux.getActions) {
437
throw Error(
438
"You may not define a method named actions in an rclass. This is used to expose redux actions",
439
);
440
}
441
442
x.actions = redux.getActions;
443
444
C = createReactClass(x);
445
if (x.reduxProps != null) {
446
// Make the ones comming from redux get automatically injected, as long
447
// as this component is in a heierarchy wrapped by <Redux>...</Redux>
448
C = connect_component(x.reduxProps)(C);
449
}
450
}
451
return C;
452
};
453
454
const redux = new AppRedux();
455
456
// Public interface
457
export function is_redux(obj) {
458
return obj instanceof AppRedux;
459
}
460
export function is_redux_actions(obj) {
461
return obj instanceof Actions;
462
}
463
464
/*
465
The non-tsx version of this:
466
<Provider store={redux.reduxStore}>
467
{children}
468
</Provider>
469
*/
470
export function Redux({ children }) {
471
return React.createElement(Provider, {
472
store: redux.reduxStore,
473
children,
474
}) as any;
475
}
476
477
export const Component = React.Component;
478
export type Rendered = React.ReactElement<any> | undefined;
479
export { rclass }; // use rclass to get access to reduxProps support
480
export { rtypes }; // has extra rtypes.immutable, needed for reduxProps to leave value as immutable
481
export { computed };
482
export { React };
483
export type CSS = React.CSSProperties;
484
export const { Fragment } = React;
485
export { redux }; // global redux singleton
486
export { Actions };
487
export { Table };
488
export { Store };
489
function UNSAFE_NONNULLABLE<T>(arg: T): NonNullable<T> {
490
return arg as any;
491
}
492
export { UNSAFE_NONNULLABLE };
493
494
declare var cc;
495
if (DEBUG) {
496
if (typeof cc !== "undefined" && cc !== null) {
497
cc.redux = redux;
498
} // for convenience in the browser (mainly for debugging)
499
}
500
501
/*
502
Given
503
spec =
504
foo :
505
bar : ...
506
stuff : ...
507
foo2 :
508
other : ...
509
510
the redux_fields function returns ['bar', 'stuff', 'other'].
511
*/
512
export function redux_fields(spec) {
513
const v: any[] = [];
514
for (let _ in spec) {
515
const val = spec[_];
516
for (const key in val) {
517
_ = val[key];
518
v.push(key);
519
}
520
}
521
return v;
522
}
523
524
// Export common React Hooks for convenience:
525
export * from "./hooks";
526
export * from "./redux-hooks";
527
528