Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/app-framework/index.ts
1503 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
return useSelector<any, T>((_) => {
160
let projectStore = undefined;
161
if (project_id) {
162
projectStore = this.getStore(project_redux_name(project_id)) as any;
163
}
164
return selectFrom(projectStore);
165
});
166
}
167
168
// getProject... is safe to call any time. All structures will be created
169
// if they don't exist
170
getProjectStore(project_id: string): ProjectStore {
171
if (!is_valid_uuid_string(project_id)) {
172
throw Error(`getProjectStore: INVALID project_id -- "${project_id}"`);
173
}
174
if (!this.hasProjectStore(project_id)) {
175
// Right now importing project_store breaks the share server,
176
// so we don't yet.
177
return require("../project_store").init(project_id, this);
178
} else {
179
return this.getStore(project_redux_name(project_id)) as any;
180
}
181
}
182
183
// TODO -- Typing: Type project Actions
184
// T, C extends Actions<T>
185
getProjectActions(project_id: string): ProjectActions {
186
if (!is_valid_uuid_string(project_id)) {
187
throw Error(`getProjectActions: INVALID project_id -- "${project_id}"`);
188
}
189
if (!this.hasProjectStore(project_id)) {
190
require("../project_store").init(project_id, this);
191
}
192
return this.getActions(project_redux_name(project_id)) as any;
193
}
194
// TODO -- Typing: Type project Table
195
getProjectTable(project_id: string, name: string): any {
196
if (!is_valid_uuid_string(project_id)) {
197
throw Error(`getProjectTable: INVALID project_id -- "${project_id}"`);
198
}
199
if (!this.hasProjectStore(project_id)) {
200
require("../project_store").init(project_id, this);
201
}
202
return this.getTable(project_redux_name(project_id, name));
203
}
204
205
removeProjectReferences(project_id: string): void {
206
if (!is_valid_uuid_string(project_id)) {
207
throw Error(
208
`getProjectReferences: INVALID project_id -- "${project_id}"`,
209
);
210
}
211
const name = project_redux_name(project_id);
212
const store = this.getStore(name);
213
store?.destroy?.();
214
this.removeActions(name);
215
this.removeStore(name);
216
}
217
218
// getEditorActions but for whatever editor -- this is mainly meant to be used
219
// from the console when debugging, e.g., smc.redux.currentEditorActions()
220
public currentEditor = (): {
221
project_id?: string;
222
path?: string;
223
account_id?: string;
224
actions?: Actions<any>;
225
store?: Store<any>;
226
} => {
227
const project_id = this.getStore("page").get("active_top_tab");
228
const current: {
229
project_id?: string;
230
path?: string;
231
account_id?: string;
232
actions?: Actions<any>;
233
store?: Store<any>;
234
} = { account_id: this.getStore("account")?.get("account_id") };
235
if (!is_valid_uuid_string(project_id)) {
236
return current;
237
}
238
current.project_id = project_id;
239
const store = this.getProjectStore(project_id);
240
const tab = store.get("active_project_tab");
241
if (!tab.startsWith("editor-")) {
242
return current;
243
}
244
const path = tab.slice("editor-".length);
245
current.path = path;
246
current.actions = this.getEditorActions(project_id, path);
247
current.store = this.getEditorStore(project_id, path);
248
return current;
249
};
250
}
251
252
const computed = (rtype) => {
253
const clone = rtype.bind({});
254
clone.is_computed = true;
255
return clone;
256
};
257
258
const rtypes = require("@cocalc/util/opts").types;
259
260
/*
261
Used by Provider to map app state to component props
262
263
rclass
264
reduxProps:
265
store_name :
266
prop : type
267
268
WARNING: If store not yet defined, then props will all be undefined for that store! There
269
is no warning/error in this case.
270
271
*/
272
const connect_component = (spec) => {
273
const map_state_to_props = function (state) {
274
const props = {};
275
if (state == null) {
276
return props;
277
}
278
for (const store_name in spec) {
279
if (store_name === "undefined") {
280
// "undefined" gets turned into this string when making a common mistake
281
console.warn("spec = ", spec);
282
throw Error(
283
"WARNING: redux spec is invalid because it contains 'undefined' as a key. " +
284
JSON.stringify(spec),
285
);
286
}
287
const info = spec[store_name];
288
const store: Store<any> | undefined = redux.getStore(store_name);
289
for (const prop in info) {
290
var val;
291
const type = info[prop];
292
293
if (type == null) {
294
throw Error(
295
`ERROR invalid redux spec: no type info set for prop '${prop}' in store '${store_name}', ` +
296
`where full spec has keys '${Object.keys(spec)}' ` +
297
`-- e.g. rtypes.bool vs. rtypes.boolean`,
298
);
299
}
300
301
if (store == undefined) {
302
val = undefined;
303
} else {
304
val = store.get(prop);
305
}
306
307
if (type.category === "IMMUTABLE") {
308
props[prop] = val;
309
} else {
310
props[prop] =
311
(val != null ? val.toJS : undefined) != null ? val.toJS() : val;
312
}
313
}
314
}
315
return props;
316
};
317
return connect(map_state_to_props);
318
};
319
320
/*
321
322
Takes an object to create a reactClass or a function which returns such an object.
323
324
Objects should be shaped like a react class save for a few exceptions:
325
x.reduxProps =
326
redux_store_name :
327
fields : value_type
328
name : type
329
330
x.actions must not be defined.
331
332
*/
333
334
// Uncomment (and also use below) for working on
335
// https://github.com/sagemathinc/cocalc/issues/4176
336
/*
337
function reduxPropsCheck(reduxProps: object) {
338
for (let store in reduxProps) {
339
const x = reduxProps[store];
340
if (x == null) continue;
341
for (let field in x) {
342
if (x[field] == rtypes.object) {
343
console.log(`WARNING: reduxProps object ${store}.${field}`);
344
}
345
}
346
}
347
}
348
*/
349
350
function compute_cache_key(data: { [key: string]: any }): string {
351
return json_stable(keys(data).sort())!;
352
}
353
354
rclass = function (x: any) {
355
let C;
356
if (typeof x === "function" && typeof x.reduxProps === "function") {
357
// using an ES6 class *and* reduxProps...
358
C = createReactClass({
359
render() {
360
if (this.cache0 == null) {
361
this.cache0 = {};
362
}
363
const reduxProps = x.reduxProps(this.props);
364
//reduxPropsCheck(reduxProps);
365
const key = compute_cache_key(reduxProps);
366
// console.log("ES6 rclass render", key);
367
if (this.cache0[key] == null) {
368
this.cache0[key] = connect_component(reduxProps)(x);
369
}
370
return React.createElement(
371
this.cache0[key],
372
this.props,
373
this.props.children,
374
);
375
},
376
});
377
return C;
378
} else if (typeof x === "function") {
379
// Creates a react class that wraps the eventual component.
380
// It calls the generator function with props as a parameter
381
// and caches the result based on reduxProps
382
const cached = createReactClass({
383
// This only caches per Component. No memory leak, but could be faster for multiple components with the same signature
384
render() {
385
if (this.cache == null) {
386
this.cache = {};
387
}
388
// OPTIMIZATION: Cache props before generating a new key.
389
// currently assumes making a new object is fast enough
390
const definition = x(this.props);
391
//reduxPropsCheck(definition.reduxProps);
392
const key = compute_cache_key(definition.reduxProps);
393
// console.log("function rclass render", key);
394
395
if (definition.actions != null) {
396
throw Error(
397
"You may not define a method named actions in an rclass. This is used to expose redux actions",
398
);
399
}
400
401
definition.actions = redux.getActions;
402
403
if (this.cache[key] == null) {
404
this.cache[key] = rclass(definition);
405
} // wait.. is this even the slow part?
406
407
return React.createElement(
408
this.cache[key],
409
this.props,
410
this.props.children,
411
);
412
},
413
});
414
415
return cached;
416
} else {
417
if (x.reduxProps != null) {
418
// Inject the propTypes based on the ones injected by reduxProps.
419
const propTypes = x.propTypes != null ? x.propTypes : {};
420
for (const store_name in x.reduxProps) {
421
const info = x.reduxProps[store_name];
422
for (const prop in info) {
423
const type = info[prop];
424
if (type !== rtypes.immutable) {
425
propTypes[prop] = type;
426
} else {
427
propTypes[prop] = rtypes.object;
428
}
429
}
430
}
431
x.propTypes = propTypes;
432
//reduxPropsCheck(propTypes);
433
}
434
435
if (x.actions != null && x.actions !== redux.getActions) {
436
throw Error(
437
"You may not define a method named actions in an rclass. This is used to expose redux actions",
438
);
439
}
440
441
x.actions = redux.getActions;
442
443
C = createReactClass(x);
444
if (x.reduxProps != null) {
445
// Make the ones comming from redux get automatically injected, as long
446
// as this component is in a heierarchy wrapped by <Redux>...</Redux>
447
C = connect_component(x.reduxProps)(C);
448
}
449
}
450
return C;
451
};
452
453
const redux = new AppRedux();
454
455
// Public interface
456
export function is_redux(obj) {
457
return obj instanceof AppRedux;
458
}
459
export function is_redux_actions(obj) {
460
return obj instanceof Actions;
461
}
462
463
/*
464
The non-tsx version of this:
465
<Provider store={redux.reduxStore}>
466
{children}
467
</Provider>
468
*/
469
export function Redux({ children }) {
470
return React.createElement(Provider, {
471
store: redux.reduxStore,
472
children,
473
}) as any;
474
}
475
476
export const Component = React.Component;
477
export type Rendered = React.ReactElement<any> | undefined;
478
export { rclass }; // use rclass to get access to reduxProps support
479
export { rtypes }; // has extra rtypes.immutable, needed for reduxProps to leave value as immutable
480
export { computed };
481
export { React };
482
export type CSS = React.CSSProperties;
483
export const { Fragment } = React;
484
export { redux }; // global redux singleton
485
export { Actions };
486
export { Table };
487
export { Store };
488
function UNSAFE_NONNULLABLE<T>(arg: T): NonNullable<T> {
489
return arg as any;
490
}
491
export { UNSAFE_NONNULLABLE };
492
493
declare var cc;
494
if (DEBUG) {
495
if (typeof cc !== "undefined" && cc !== null) {
496
cc.redux = redux;
497
} // for convenience in the browser (mainly for debugging)
498
}
499
500
/*
501
Given
502
spec =
503
foo :
504
bar : ...
505
stuff : ...
506
foo2 :
507
other : ...
508
509
the redux_fields function returns ['bar', 'stuff', 'other'].
510
*/
511
export function redux_fields(spec) {
512
const v: any[] = [];
513
for (let _ in spec) {
514
const val = spec[_];
515
for (const key in val) {
516
_ = val[key];
517
v.push(key);
518
}
519
}
520
return v;
521
}
522
523
// Export common React Hooks for convenience:
524
export * from "./hooks";
525
export * from "./redux-hooks";
526
527