react-iframe-bridge 1.0.0 → 2.0.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/README.md +0 -7
- package/{lib-cjs/components/ErrorPage.d.ts → lib/components/error_page.d.ts} +1 -0
- package/lib/components/error_page.d.ts.map +1 -0
- package/lib/components/error_page.js +15 -0
- package/lib/components/error_page.js.map +1 -0
- package/lib/components/home/{Home.d.ts → home.d.ts} +1 -0
- package/lib/components/home/home.d.ts.map +1 -0
- package/lib/components/home/home.js +44 -0
- package/lib/components/home/home.js.map +1 -0
- package/{lib-cjs/components/home/HomeContext.d.ts → lib/components/home/home_context.d.ts} +2 -1
- package/lib/components/home/home_context.d.ts.map +1 -0
- package/lib/components/home/{HomeContext.js → home_context.js} +4 -3
- package/lib/components/home/home_context.js.map +1 -0
- package/{lib-cjs/components/home/HomeHeader.d.ts → lib/components/home/home_header.d.ts} +1 -0
- package/lib/components/home/home_header.d.ts.map +1 -0
- package/lib/components/home/home_header.js +16 -0
- package/lib/components/home/home_header.js.map +1 -0
- package/lib/components/home/{HomeIframe.d.ts → home_iframe.d.ts} +1 -0
- package/lib/components/home/home_iframe.d.ts.map +1 -0
- package/lib/components/home/{HomeIframe.js → home_iframe.js} +8 -2
- package/lib/components/home/home_iframe.js.map +1 -0
- package/{lib-cjs/components/home/HomeNoSample.d.ts → lib/components/home/home_no_sample.d.ts} +1 -0
- package/lib/components/home/home_no_sample.d.ts.map +1 -0
- package/lib/components/home/{HomeNoSample.js → home_no_sample.js} +3 -2
- package/lib/components/home/home_no_sample.js.map +1 -0
- package/{lib-cjs/components/home/HomeSamples.d.ts → lib/components/home/home_samples.d.ts} +1 -0
- package/lib/components/home/home_samples.d.ts.map +1 -0
- package/lib/components/home/home_samples.js +29 -0
- package/lib/components/home/home_samples.js.map +1 -0
- package/{lib-cjs/components/home/HomeSelector.d.ts → lib/components/home/home_selector.d.ts} +1 -0
- package/lib/components/home/home_selector.d.ts.map +1 -0
- package/lib/components/home/home_selector.js +15 -0
- package/lib/components/home/home_selector.js.map +1 -0
- package/{lib-cjs/components/Input.d.ts → lib/components/input.d.ts} +3 -2
- package/lib/components/input.d.ts.map +1 -0
- package/lib/components/input.js +14 -0
- package/lib/components/input.js.map +1 -0
- package/{lib-cjs/components/LoadingFull.d.ts → lib/components/loading_full.d.ts} +1 -0
- package/lib/components/loading_full.d.ts.map +1 -0
- package/lib/components/loading_full.js +12 -0
- package/lib/components/loading_full.js.map +1 -0
- package/lib/components/{Spinner.d.ts → spinner.d.ts} +3 -1
- package/lib/components/spinner.d.ts.map +1 -0
- package/lib/components/spinner.js +18 -0
- package/lib/components/spinner.js.map +1 -0
- package/lib/contexts/{iframeBridge.d.ts → iframe_bridge.d.ts} +2 -1
- package/lib/contexts/iframe_bridge.d.ts.map +1 -0
- package/lib/contexts/{iframeBridge.js → iframe_bridge.js} +5 -6
- package/lib/contexts/iframe_bridge.js.map +1 -0
- package/lib/contexts/roc.d.ts +1 -0
- package/lib/contexts/roc.d.ts.map +1 -0
- package/lib/contexts/roc.js +1 -0
- package/lib/contexts/roc.js.map +1 -0
- package/{lib-cjs/hooks/localStorage.d.ts → lib/hooks/local_storage.d.ts} +1 -0
- package/lib/hooks/local_storage.d.ts.map +1 -0
- package/lib/hooks/{localStorage.js → local_storage.js} +2 -1
- package/lib/hooks/local_storage.js.map +1 -0
- package/{lib-cjs/hooks/useRocQuery.d.ts → lib/hooks/use_roc_query.d.ts} +1 -0
- package/lib/hooks/use_roc_query.d.ts.map +1 -0
- package/lib/hooks/{useRocQuery.js → use_roc_query.js} +2 -1
- package/lib/hooks/use_roc_query.js.map +1 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +4 -3
- package/lib/index.js.map +1 -0
- package/lib/types/db.d.ts +5 -0
- package/lib/types/db.d.ts.map +1 -0
- package/lib/types/db.js +1 -0
- package/lib/types/db.js.map +1 -0
- package/lib/types/util.d.ts +1 -0
- package/lib/types/util.d.ts.map +1 -0
- package/lib/types/util.js +1 -0
- package/lib/types/util.js.map +1 -0
- package/{lib-cjs/utils/localStorage.d.ts → lib/utils/local_storage.d.ts} +1 -0
- package/lib/utils/local_storage.d.ts.map +1 -0
- package/lib/utils/{localStorage.js → local_storage.js} +1 -0
- package/lib/utils/local_storage.js.map +1 -0
- package/package.json +7 -7
- package/src/components/error_page.tsx +39 -0
- package/src/components/home/home.tsx +98 -0
- package/src/components/home/home_context.tsx +101 -0
- package/src/components/home/home_header.tsx +29 -0
- package/src/components/home/home_iframe.tsx +73 -0
- package/src/components/home/home_no_sample.tsx +16 -0
- package/src/components/home/home_samples.tsx +62 -0
- package/src/components/home/home_selector.tsx +23 -0
- package/src/components/input.tsx +31 -0
- package/src/components/loading_full.tsx +19 -0
- package/src/components/spinner.tsx +44 -0
- package/src/contexts/iframe_bridge.tsx +234 -0
- package/src/contexts/roc.tsx +23 -0
- package/src/hooks/local_storage.ts +45 -0
- package/src/hooks/use_roc_query.ts +75 -0
- package/src/index.ts +7 -0
- package/src/types/db.ts +81 -0
- package/src/types/util.ts +3 -0
- package/src/utils/local_storage.ts +35 -0
- package/lib/components/ErrorPage.d.ts +0 -8
- package/lib/components/ErrorPage.js +0 -4
- package/lib/components/Input.d.ts +0 -10
- package/lib/components/Input.js +0 -5
- package/lib/components/LoadingFull.d.ts +0 -1
- package/lib/components/LoadingFull.js +0 -5
- package/lib/components/Spinner.js +0 -5
- package/lib/components/home/Home.js +0 -22
- package/lib/components/home/HomeContext.d.ts +0 -20
- package/lib/components/home/HomeHeader.d.ts +0 -1
- package/lib/components/home/HomeHeader.js +0 -10
- package/lib/components/home/HomeNoSample.d.ts +0 -1
- package/lib/components/home/HomeSamples.d.ts +0 -1
- package/lib/components/home/HomeSamples.js +0 -23
- package/lib/components/home/HomeSelector.d.ts +0 -5
- package/lib/components/home/HomeSelector.js +0 -5
- package/lib/hooks/localStorage.d.ts +0 -13
- package/lib/hooks/useRocQuery.d.ts +0 -13
- package/lib/utils/localStorage.d.ts +0 -3
- package/lib-cjs/components/ErrorPage.js +0 -7
- package/lib-cjs/components/Input.js +0 -11
- package/lib-cjs/components/LoadingFull.js +0 -11
- package/lib-cjs/components/Spinner.d.ts +0 -4
- package/lib-cjs/components/Spinner.js +0 -11
- package/lib-cjs/components/home/Home.d.ts +0 -26
- package/lib-cjs/components/home/Home.js +0 -28
- package/lib-cjs/components/home/HomeContext.js +0 -54
- package/lib-cjs/components/home/HomeHeader.js +0 -16
- package/lib-cjs/components/home/HomeIframe.d.ts +0 -5
- package/lib-cjs/components/home/HomeIframe.js +0 -37
- package/lib-cjs/components/home/HomeNoSample.js +0 -14
- package/lib-cjs/components/home/HomeSamples.js +0 -29
- package/lib-cjs/components/home/HomeSelector.js +0 -11
- package/lib-cjs/contexts/iframeBridge.d.ts +0 -33
- package/lib-cjs/contexts/iframeBridge.js +0 -130
- package/lib-cjs/contexts/roc.d.ts +0 -8
- package/lib-cjs/contexts/roc.js +0 -20
- package/lib-cjs/hooks/localStorage.js +0 -38
- package/lib-cjs/hooks/useRocQuery.js +0 -45
- package/lib-cjs/index.d.ts +0 -3
- package/lib-cjs/index.js +0 -12
- package/lib-cjs/types/db.d.ts +0 -78
- package/lib-cjs/types/db.js +0 -2
- package/lib-cjs/types/util.d.ts +0 -6
- package/lib-cjs/types/util.js +0 -2
- package/lib-cjs/utils/localStorage.js +0 -34
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// https://github.com/import-js/eslint-plugin-import/issues/1810
|
|
2
|
+
|
|
3
|
+
import { onMessage, ready } from 'iframe-bridge/iframe';
|
|
4
|
+
import { produce } from 'immer';
|
|
5
|
+
import type { ReactNode, Reducer } from 'react';
|
|
6
|
+
import { createContext, useContext, useEffect, useReducer } from 'react';
|
|
7
|
+
import type { RocDocument } from 'rest-on-couch-client';
|
|
8
|
+
import { Roc } from 'rest-on-couch-client';
|
|
9
|
+
|
|
10
|
+
import ErrorPage from '../components/error_page.js';
|
|
11
|
+
import LoadingFull from '../components/loading_full.js';
|
|
12
|
+
import type { SampleEntryContent, SampleEntryId } from '../types/db.js';
|
|
13
|
+
import type { ActionType } from '../types/util.js';
|
|
14
|
+
|
|
15
|
+
const iframeBridgeContext = createContext<IframeBridgeReadyContextType<
|
|
16
|
+
any,
|
|
17
|
+
any
|
|
18
|
+
> | null>(null);
|
|
19
|
+
|
|
20
|
+
export function useIframeBridgeContext<
|
|
21
|
+
PublicUserInfo = unknown,
|
|
22
|
+
PrivateUserInfo = unknown,
|
|
23
|
+
>(): IframeBridgeReadyContextType<PublicUserInfo, PrivateUserInfo> {
|
|
24
|
+
const context = useContext(iframeBridgeContext);
|
|
25
|
+
if (!context) {
|
|
26
|
+
throw new Error('Iframe bridge context is not ready');
|
|
27
|
+
}
|
|
28
|
+
return context;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useIframeBridgeSample(): RocDocument<
|
|
32
|
+
SampleEntryContent,
|
|
33
|
+
SampleEntryId
|
|
34
|
+
> {
|
|
35
|
+
const context = useIframeBridgeContext();
|
|
36
|
+
if (!context.sample) {
|
|
37
|
+
throw new Error('No sample in context');
|
|
38
|
+
}
|
|
39
|
+
return context.sample;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface IframeBridgeContextType<
|
|
43
|
+
PublicUserInfo = unknown,
|
|
44
|
+
PrivateUserInfo = unknown,
|
|
45
|
+
> {
|
|
46
|
+
state: 'initial' | 'loading' | 'ready' | 'standalone-error';
|
|
47
|
+
data: IframeDataMessage | null;
|
|
48
|
+
roc: Roc<PublicUserInfo, PrivateUserInfo> | null;
|
|
49
|
+
hasSample: boolean;
|
|
50
|
+
sample: RocDocument<SampleEntryContent, SampleEntryId> | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface IframeBridgeReadyContextTypeBase<PublicUserInfo, PrivateUserInfo> {
|
|
54
|
+
state: 'ready';
|
|
55
|
+
data: IframeDataMessage;
|
|
56
|
+
roc: Roc<PublicUserInfo, PrivateUserInfo>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface IframeBridgeReadyContextTypeWithSample<
|
|
60
|
+
PublicUserInfo,
|
|
61
|
+
PrivateUserInfo,
|
|
62
|
+
> extends IframeBridgeReadyContextTypeBase<PublicUserInfo, PrivateUserInfo> {
|
|
63
|
+
hasSample: true;
|
|
64
|
+
sample: RocDocument<SampleEntryContent, SampleEntryId>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface IframeBridgeReadyContextTypeWithoutSample<
|
|
68
|
+
PublicUserInfo,
|
|
69
|
+
PrivateUserInfo,
|
|
70
|
+
> extends IframeBridgeReadyContextTypeBase<PublicUserInfo, PrivateUserInfo> {
|
|
71
|
+
hasSample: false;
|
|
72
|
+
sample: null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type IframeBridgeReadyContextType<
|
|
76
|
+
PublicUserInfo = unknown,
|
|
77
|
+
PrivateUserInfo = unknown,
|
|
78
|
+
> =
|
|
79
|
+
| IframeBridgeReadyContextTypeWithSample<PublicUserInfo, PrivateUserInfo>
|
|
80
|
+
| IframeBridgeReadyContextTypeWithoutSample<PublicUserInfo, PrivateUserInfo>;
|
|
81
|
+
|
|
82
|
+
interface IframeDataMessage {
|
|
83
|
+
couchDB: {
|
|
84
|
+
url: string;
|
|
85
|
+
database: string;
|
|
86
|
+
};
|
|
87
|
+
uuid: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type IframeMessage =
|
|
91
|
+
| {
|
|
92
|
+
type: 'tab.data';
|
|
93
|
+
message: IframeDataMessage;
|
|
94
|
+
}
|
|
95
|
+
| { type: 'tab.focus' };
|
|
96
|
+
|
|
97
|
+
type IframeBridgeContextAction =
|
|
98
|
+
| ActionType<'RECEIVE_DATA', IframeDataMessage>
|
|
99
|
+
| ActionType<'SET_SAMPLE', RocDocument<SampleEntryContent, SampleEntryId>>
|
|
100
|
+
| ActionType<'STANDALONE_TIMEOUT'>;
|
|
101
|
+
|
|
102
|
+
const iframeBridgeReducer: Reducer<
|
|
103
|
+
IframeBridgeContextType,
|
|
104
|
+
IframeBridgeContextAction
|
|
105
|
+
> = produce(
|
|
106
|
+
(state: IframeBridgeContextType, action: IframeBridgeContextAction) => {
|
|
107
|
+
switch (action.type) {
|
|
108
|
+
case 'RECEIVE_DATA': {
|
|
109
|
+
state.data = action.payload;
|
|
110
|
+
state.roc = new Roc(action.payload.couchDB);
|
|
111
|
+
if (action.payload.uuid) {
|
|
112
|
+
state.state = 'loading';
|
|
113
|
+
state.hasSample = true;
|
|
114
|
+
state.sample = null;
|
|
115
|
+
} else {
|
|
116
|
+
state.state = 'ready';
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'SET_SAMPLE': {
|
|
121
|
+
state.sample = action.payload;
|
|
122
|
+
state.state = 'ready';
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'STANDALONE_TIMEOUT': {
|
|
126
|
+
state.state = 'standalone-error';
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
throw new Error('unreachable');
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const initialState: IframeBridgeContextType = {
|
|
136
|
+
state: 'initial',
|
|
137
|
+
data: null,
|
|
138
|
+
roc: null,
|
|
139
|
+
hasSample: false,
|
|
140
|
+
sample: null,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export function IframeBridgeProvider(props: {
|
|
144
|
+
children: ReactNode;
|
|
145
|
+
requireSample?: boolean;
|
|
146
|
+
allowStandalone?: boolean;
|
|
147
|
+
}) {
|
|
148
|
+
const [state, dispatch] = useReducer(iframeBridgeReducer, initialState);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
onMessage((message: IframeMessage) => {
|
|
152
|
+
switch (message.type) {
|
|
153
|
+
case 'tab.data': {
|
|
154
|
+
dispatch({ type: 'RECEIVE_DATA', payload: message.message });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'tab.focus': {
|
|
158
|
+
// Ignore this event. Happens in C6H6 when an already opened tab is
|
|
159
|
+
// refocused.
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
// eslint-disable-next-line no-console
|
|
164
|
+
console.error(message);
|
|
165
|
+
throw new Error('unreachable');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
ready();
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!props.allowStandalone && state.state === 'initial') {
|
|
173
|
+
const timeout = setTimeout(() => {
|
|
174
|
+
dispatch({ type: 'STANDALONE_TIMEOUT' });
|
|
175
|
+
}, 3000);
|
|
176
|
+
return () => clearTimeout(timeout);
|
|
177
|
+
}
|
|
178
|
+
}, [props.allowStandalone, state.state]);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!state.roc || !state.data?.uuid) return;
|
|
182
|
+
let cancelled = false;
|
|
183
|
+
const document = state.roc.getDocument<SampleEntryContent, SampleEntryId>(
|
|
184
|
+
state.data.uuid,
|
|
185
|
+
);
|
|
186
|
+
document
|
|
187
|
+
.fetch()
|
|
188
|
+
.then(() => {
|
|
189
|
+
if (!cancelled) {
|
|
190
|
+
dispatch({ type: 'SET_SAMPLE', payload: document });
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.catch((error: unknown) => {
|
|
194
|
+
// TODO: handle error
|
|
195
|
+
// eslint-disable-next-line no-console
|
|
196
|
+
console.error(error);
|
|
197
|
+
});
|
|
198
|
+
return () => {
|
|
199
|
+
cancelled = true;
|
|
200
|
+
};
|
|
201
|
+
}, [state.roc, state.data]);
|
|
202
|
+
|
|
203
|
+
if (state.state === 'standalone-error') {
|
|
204
|
+
return (
|
|
205
|
+
<ErrorPage
|
|
206
|
+
title="Invalid access"
|
|
207
|
+
subtitle="This page cannot be accessed without iframe-bridge."
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (state.state !== 'ready') {
|
|
213
|
+
return (
|
|
214
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
215
|
+
<LoadingFull />
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!state.hasSample && props.requireSample) {
|
|
221
|
+
return (
|
|
222
|
+
<ErrorPage
|
|
223
|
+
title="Invalid access"
|
|
224
|
+
subtitle="This page must be accessed with a sample."
|
|
225
|
+
/>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<iframeBridgeContext.Provider value={state as IframeBridgeReadyContextType}>
|
|
231
|
+
{props.children}
|
|
232
|
+
</iframeBridgeContext.Provider>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
3
|
+
import { Roc } from 'rest-on-couch-client';
|
|
4
|
+
|
|
5
|
+
const rocContext = createContext<Roc | null>(null);
|
|
6
|
+
|
|
7
|
+
export function useRoc<PublicUserInfo = unknown, PrivateUserInfo = unknown>() {
|
|
8
|
+
const roc = useContext(rocContext) as Roc<PublicUserInfo, PrivateUserInfo>;
|
|
9
|
+
if (!roc) {
|
|
10
|
+
throw new Error('missing roc');
|
|
11
|
+
}
|
|
12
|
+
return roc;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function RocProvider(props: {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
url: string;
|
|
18
|
+
database: string;
|
|
19
|
+
}) {
|
|
20
|
+
const { url, database, children } = props;
|
|
21
|
+
const roc = useMemo(() => new Roc({ url, database }), [url, database]);
|
|
22
|
+
return <rocContext.Provider value={roc}>{children}</rocContext.Provider>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { getItem, setItem } from '../utils/local_storage.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Like `useState`, but initializing from `localStorage` if available and saving
|
|
7
|
+
* to `localStorage` everytime the state is changed.
|
|
8
|
+
* @param key localStorage key. Will be appended to the `react-iframe-bridge-` prefix.
|
|
9
|
+
* @param initialValue Value to use if the storage is empty.
|
|
10
|
+
*/
|
|
11
|
+
export function useLocalStorage<ValueType>(
|
|
12
|
+
key: string,
|
|
13
|
+
initialValue: ValueType,
|
|
14
|
+
) {
|
|
15
|
+
const [storedValue, setStoredValue] = useState<ValueType>(() => {
|
|
16
|
+
// Get from local storage by key.
|
|
17
|
+
const item = getItem(key);
|
|
18
|
+
// Parse stored json or if none return initialValue.
|
|
19
|
+
return item ? (item as ValueType) : initialValue;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Return a wrapped version of useState's setter function that persists the new value to localStorage.
|
|
23
|
+
const setValue = useCallback(
|
|
24
|
+
(value: ValueType) => {
|
|
25
|
+
// Save state.
|
|
26
|
+
setStoredValue(value);
|
|
27
|
+
// Save to local storage.
|
|
28
|
+
setItem(key, value);
|
|
29
|
+
},
|
|
30
|
+
[key],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return [storedValue, setValue] as const;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Save the provided value to `localStorage` everytime it changes.
|
|
38
|
+
* @param key localStorage key. Will be appended to the `react-iframe-bridge-` prefix.
|
|
39
|
+
* @param value Value to save.
|
|
40
|
+
*/
|
|
41
|
+
export function useSaveToLocalStorage(key: string, value: unknown): void {
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setItem(key, value);
|
|
44
|
+
}, [key, value]);
|
|
45
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Reducer } from 'react';
|
|
2
|
+
import { useEffect, useReducer } from 'react';
|
|
3
|
+
import type { IQueryResult } from 'rest-on-couch-client';
|
|
4
|
+
|
|
5
|
+
import { useRoc } from '../contexts/roc.js';
|
|
6
|
+
|
|
7
|
+
export type RocQueryResult<T> = IQueryResult<[string, string], T>;
|
|
8
|
+
|
|
9
|
+
interface RocQueryState<T = unknown> {
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: null | Error;
|
|
12
|
+
result: null | Array<RocQueryResult<T>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type RocQueryHookResult<T> = RocQueryState<T>;
|
|
16
|
+
|
|
17
|
+
type RocQueryAction<T> =
|
|
18
|
+
| { type: 'SET_RESULT'; value: Array<RocQueryResult<T>> }
|
|
19
|
+
| { type: 'ERROR'; value: Error }
|
|
20
|
+
| { type: 'LOAD' };
|
|
21
|
+
|
|
22
|
+
function rocQueryReducer<T>(
|
|
23
|
+
state: RocQueryState<T>,
|
|
24
|
+
action: RocQueryAction<T>,
|
|
25
|
+
): RocQueryState<T> {
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
case 'LOAD':
|
|
28
|
+
return {
|
|
29
|
+
...state,
|
|
30
|
+
error: null,
|
|
31
|
+
loading: true,
|
|
32
|
+
};
|
|
33
|
+
case 'SET_RESULT':
|
|
34
|
+
return {
|
|
35
|
+
loading: false,
|
|
36
|
+
error: null,
|
|
37
|
+
result: action.value,
|
|
38
|
+
};
|
|
39
|
+
case 'ERROR':
|
|
40
|
+
return { loading: false, error: action.value, result: null };
|
|
41
|
+
default:
|
|
42
|
+
throw new Error('unreachable');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RocQueryHookOptions {
|
|
47
|
+
mine?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useRocQuery<T = unknown>(
|
|
51
|
+
viewName: string,
|
|
52
|
+
options: RocQueryHookOptions = {},
|
|
53
|
+
): RocQueryHookResult<T> {
|
|
54
|
+
const { mine = false } = options;
|
|
55
|
+
const roc = useRoc();
|
|
56
|
+
const [state, dispatch] = useReducer<
|
|
57
|
+
Reducer<RocQueryState<T>, RocQueryAction<T>>
|
|
58
|
+
>(rocQueryReducer, {
|
|
59
|
+
loading: true,
|
|
60
|
+
error: null,
|
|
61
|
+
result: null,
|
|
62
|
+
});
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
dispatch({ type: 'LOAD' });
|
|
65
|
+
const query = roc.getQuery<[string, string], T>(viewName, { mine });
|
|
66
|
+
query
|
|
67
|
+
.fetch()
|
|
68
|
+
.then((result) => dispatch({ type: 'SET_RESULT', value: result }))
|
|
69
|
+
.catch((err: unknown) => {
|
|
70
|
+
dispatch({ type: 'ERROR', value: err as Error });
|
|
71
|
+
});
|
|
72
|
+
}, [roc, viewName, mine]);
|
|
73
|
+
|
|
74
|
+
return state;
|
|
75
|
+
}
|
package/src/index.ts
ADDED
package/src/types/db.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface TocEntry {
|
|
2
|
+
mf: string;
|
|
3
|
+
mw: number;
|
|
4
|
+
keyword: string[];
|
|
5
|
+
meta: Record<string, unknown>;
|
|
6
|
+
title: string;
|
|
7
|
+
nbNmr: number;
|
|
8
|
+
nbIR: number;
|
|
9
|
+
nbRaman: number;
|
|
10
|
+
nbMass: number;
|
|
11
|
+
nb1d: number;
|
|
12
|
+
nb2d: number;
|
|
13
|
+
nb1h: number;
|
|
14
|
+
nb13c: number;
|
|
15
|
+
nbTGA: number;
|
|
16
|
+
nbDSC: number;
|
|
17
|
+
nbXRD: number;
|
|
18
|
+
nbXPS: number;
|
|
19
|
+
nbUV: number;
|
|
20
|
+
nbChromatogram: number;
|
|
21
|
+
nbXray: number;
|
|
22
|
+
nbNucleic: number;
|
|
23
|
+
nbPeptidic: number;
|
|
24
|
+
modificationDate: number;
|
|
25
|
+
b64ShortId: string;
|
|
26
|
+
hidden: boolean;
|
|
27
|
+
names: string[];
|
|
28
|
+
reference: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SampleEntryId = [string, string];
|
|
32
|
+
export interface SampleEntry {
|
|
33
|
+
_id: string;
|
|
34
|
+
_rev: string;
|
|
35
|
+
$type: 'entry';
|
|
36
|
+
$id: SampleEntryId;
|
|
37
|
+
$kind: 'sample';
|
|
38
|
+
$owners: string[];
|
|
39
|
+
$content: SampleEntryContent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SampleEntryContent {
|
|
43
|
+
general: {
|
|
44
|
+
title?: string;
|
|
45
|
+
name?: Array<{ value: string }>;
|
|
46
|
+
mf: string;
|
|
47
|
+
mw: number;
|
|
48
|
+
em: number;
|
|
49
|
+
molfile: string;
|
|
50
|
+
ocl: {
|
|
51
|
+
value: string;
|
|
52
|
+
coordinates: string;
|
|
53
|
+
index: number[];
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
identifier: {
|
|
57
|
+
cas: Array<{ value: string }>;
|
|
58
|
+
};
|
|
59
|
+
spectra: {
|
|
60
|
+
nmr: SampleEntrySpectraNmr[];
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SampleEntrySpectraNmr {
|
|
65
|
+
dimension: number;
|
|
66
|
+
nucleus: string[];
|
|
67
|
+
isFid: boolean;
|
|
68
|
+
isFt: boolean;
|
|
69
|
+
title: string;
|
|
70
|
+
solvent: string;
|
|
71
|
+
pulse: string;
|
|
72
|
+
experiment: string;
|
|
73
|
+
temperature: number;
|
|
74
|
+
frequency: number;
|
|
75
|
+
type: string;
|
|
76
|
+
date: string;
|
|
77
|
+
range: unknown[];
|
|
78
|
+
jcamp: {
|
|
79
|
+
filename: string;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// "Polyfill" localStorage for SSR and to add a prefix to the keys.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prefix a `localStorage` key to avoid conflicts with other applications.
|
|
5
|
+
* @param key The key to prefix.
|
|
6
|
+
* @returns
|
|
7
|
+
*/
|
|
8
|
+
function prefixKey(key: string): string {
|
|
9
|
+
if (key.length === 0) {
|
|
10
|
+
throw new Error('key must have at least one character');
|
|
11
|
+
}
|
|
12
|
+
return `react-iframe-bridge-${key}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let getItem: (key: string) => unknown | null;
|
|
16
|
+
let setItem: (key: string, value: unknown) => void;
|
|
17
|
+
|
|
18
|
+
if (typeof localStorage !== 'undefined') {
|
|
19
|
+
getItem = function getItem(key: string) {
|
|
20
|
+
const value = localStorage.getItem(prefixKey(key));
|
|
21
|
+
if (!value) return null;
|
|
22
|
+
return JSON.parse(value);
|
|
23
|
+
};
|
|
24
|
+
setItem = (key: string, value: unknown) =>
|
|
25
|
+
localStorage.setItem(prefixKey(key), JSON.stringify(value));
|
|
26
|
+
} else {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
28
|
+
getItem = (key: string) => null;
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
30
|
+
setItem = (key: string, value: unknown): void => {
|
|
31
|
+
// noop
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { getItem, setItem };
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
export default function ErrorPage(props) {
|
|
3
|
-
return (_jsx("div", { className: "max-w-2xl m-auto md:max-w-4xl", children: _jsx("div", { className: "flex justify-between px-2 pt-4", children: _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-5xl font-bold sm:mt-16 text-primary-900", children: props.title }), _jsx("h2", { className: "mt-16 text-lg sm:mt-8", children: props.subtitle }), _jsx("div", { className: "mt-4", children: props.children })] }) }) }));
|
|
4
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { ChangeEvent } from 'react';
|
|
2
|
-
interface InputProps {
|
|
3
|
-
name: string;
|
|
4
|
-
className?: string;
|
|
5
|
-
value: string;
|
|
6
|
-
readOnly?: boolean;
|
|
7
|
-
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
8
|
-
}
|
|
9
|
-
export default function Input(props: InputProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
-
export {};
|
package/lib/components/Input.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import clsx from 'clsx';
|
|
3
|
-
export default function Input(props) {
|
|
4
|
-
return (_jsx("input", { name: props.name, type: "text", className: clsx('appearance-none border border-neutral-600 bg-white px-3 py-2 text-base leading-none', props.className), value: props.value, readOnly: props.readOnly, onChange: props.onChange }));
|
|
5
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default function LoadingFull(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import Spinner from './Spinner';
|
|
3
|
-
export default function LoadingFull() {
|
|
4
|
-
return (_jsx("div", { className: "flex items-center justify-center w-full h-full", children: _jsx(Spinner, { className: "w-10 h-10 text-alternative-500" }) }));
|
|
5
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import clsx from 'clsx';
|
|
3
|
-
export default function Spinner(props) {
|
|
4
|
-
return (_jsxs("svg", { className: clsx('animate-spin', props.className), xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }));
|
|
5
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect } from 'react';
|
|
3
|
-
import { HomeContextProvider, useHomeDispatchContext } from './HomeContext';
|
|
4
|
-
import HomeHeader from './HomeHeader';
|
|
5
|
-
import HomeIframe from './HomeIframe';
|
|
6
|
-
import HomeNoSample from './HomeNoSample';
|
|
7
|
-
import HomeSamples from './HomeSamples';
|
|
8
|
-
export function Home(props) {
|
|
9
|
-
const { baseUrl, noSampleSelection, rocUrl, database, defaultPath } = props;
|
|
10
|
-
return (_jsx(HomeContextProvider, { rocUrl: rocUrl, database: database, defaultPath: defaultPath, children: _jsx(HomeInternal, { noSampleSelection: noSampleSelection, baseUrl: baseUrl }) }));
|
|
11
|
-
}
|
|
12
|
-
function HomeInternal(props) {
|
|
13
|
-
const homeDispatch = useHomeDispatchContext();
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (props.noSampleSelection) {
|
|
16
|
-
homeDispatch({
|
|
17
|
-
type: 'OPEN_NO_SAMPLE',
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}, [props.noSampleSelection, homeDispatch]);
|
|
21
|
-
return (_jsxs("div", { className: "flex flex-col w-screen h-screen", children: [_jsx(HomeHeader, {}), _jsxs("div", { className: "flex flex-row flex-1 mt-2 border-t border-neutral-300 min-h-0", children: [!props.noSampleSelection && (_jsxs("div", { className: "flex flex-col w-48 px-2 pt-4 space-y-3 border-r border-neutral-300 overflow-auto", children: [_jsx(HomeNoSample, {}), _jsx(HomeSamples, {})] })), _jsx(HomeIframe, { baseUrl: props.baseUrl })] })] }));
|
|
22
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Dispatch, ReactNode } from 'react';
|
|
2
|
-
import type { ActionType } from '../../types/util';
|
|
3
|
-
interface HomeContextType {
|
|
4
|
-
rocUrl: string;
|
|
5
|
-
database: string;
|
|
6
|
-
iframePath: string;
|
|
7
|
-
iframeMode: 'closed' | 'sample' | 'no-sample';
|
|
8
|
-
selectedSample: string | null;
|
|
9
|
-
}
|
|
10
|
-
type HomeContextAction = ActionType<'SELECT_SAMPLE', string> | ActionType<'OPEN_NO_SAMPLE'> | ActionType<'SET_IFRAME_PAGE', string>;
|
|
11
|
-
interface HomeContextProviderProps {
|
|
12
|
-
children: ReactNode;
|
|
13
|
-
rocUrl?: string;
|
|
14
|
-
database?: string;
|
|
15
|
-
defaultPath?: string;
|
|
16
|
-
}
|
|
17
|
-
export declare function HomeContextProvider(props: HomeContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
-
export declare function useHomeContext(): HomeContextType;
|
|
19
|
-
export declare function useHomeDispatchContext(): Dispatch<HomeContextAction>;
|
|
20
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default function HomeHeader(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import Input from '../Input';
|
|
3
|
-
import { useHomeContext, useHomeDispatchContext } from './HomeContext';
|
|
4
|
-
export default function HomeHeader() {
|
|
5
|
-
const { rocUrl, database, iframePath } = useHomeContext();
|
|
6
|
-
const dispatch = useHomeDispatchContext();
|
|
7
|
-
return (_jsxs("header", { className: "flex flex-row p-2 space-x-4", children: [_jsx(Input, { name: "rocUrl", className: "flex-1", value: rocUrl, readOnly: true }), _jsx(Input, { name: "database", value: database, readOnly: true }), _jsx(Input, { name: "iframe-page", value: iframePath, className: "flex-1", onChange: (event) => {
|
|
8
|
-
dispatch({ type: 'SET_IFRAME_PAGE', payload: event.target.value });
|
|
9
|
-
} })] }));
|
|
10
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default function HomeNoSample(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default function HomeSamples(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRocQuery } from '../../hooks/useRocQuery';
|
|
3
|
-
import Spinner from '../Spinner';
|
|
4
|
-
import { useHomeContext, useHomeDispatchContext } from './HomeContext';
|
|
5
|
-
import HomeSelector from './HomeSelector';
|
|
6
|
-
export default function HomeSamples() {
|
|
7
|
-
const { loading, error, result } = useRocQuery('sample_toc');
|
|
8
|
-
if (error) {
|
|
9
|
-
throw error;
|
|
10
|
-
}
|
|
11
|
-
return (_jsxs(_Fragment, { children: [_jsx("h1", { className: "mb-4 text-lg font-bold text-center", children: "Sample TOC" }), _jsx("div", { className: "flex-1", children: loading || !result ? _jsx(Loading, {}) : _jsx(SampleToc, { samples: result }) })] }));
|
|
12
|
-
}
|
|
13
|
-
function SampleToc(props) {
|
|
14
|
-
const { selectedSample } = useHomeContext();
|
|
15
|
-
const dispatch = useHomeDispatchContext();
|
|
16
|
-
function selectSample(id) {
|
|
17
|
-
dispatch({ type: 'SELECT_SAMPLE', payload: id });
|
|
18
|
-
}
|
|
19
|
-
return (_jsx("div", { className: "space-y-2", children: props.samples.map((sample) => (_jsx(HomeSelector, { selected: sample.id === selectedSample, text: sample.value.reference, onClick: () => selectSample(sample.id) }, sample.id))) }));
|
|
20
|
-
}
|
|
21
|
-
function Loading() {
|
|
22
|
-
return (_jsx("div", { className: "flex justify-center mt-8", children: _jsx(Spinner, { className: "w-8 h-8 text-alternative-500" }) }));
|
|
23
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import clsx from 'clsx';
|
|
3
|
-
export default function HomeSelector(props) {
|
|
4
|
-
return (_jsx("div", { className: clsx('p-1 border rounded cursor-pointer border-neutral-400', props.selected && 'bg-primary-50 shadow-inner'), onClick: props.onClick, children: props.text }));
|
|
5
|
-
}
|