-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
plugin.tsx
196 lines (177 loc) · 5.87 KB
/
plugin.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import {
ComponentType,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { DocExplorer, useExplorerContext } from './explorer';
import { History, useHistoryContext } from './history';
import { DocsFilledIcon, DocsIcon, HistoryIcon } from './icons';
import { useStorageContext } from './storage';
import { createContextHook, createNullableContext } from './utility/context';
export type GraphiQLPlugin = {
/**
* A component that renders content into the plugin pane.
*/
content: ComponentType;
/**
* A component that renders an icon that will be shown inside a button that
* toggles the plugin visibility.
*/
icon: ComponentType;
/**
* The unique title of the plugin. If two plugins are present with the same
* title the provider component will throw an error.
*/
title: string;
};
export const DOC_EXPLORER_PLUGIN: GraphiQLPlugin = {
title: 'Documentation Explorer',
icon: function Icon() {
const pluginContext = usePluginContext();
return pluginContext?.visiblePlugin === DOC_EXPLORER_PLUGIN ? (
<DocsFilledIcon />
) : (
<DocsIcon />
);
},
content: DocExplorer,
};
export const HISTORY_PLUGIN: GraphiQLPlugin = {
title: 'History',
icon: HistoryIcon,
content: History,
};
export type PluginContextType = {
/**
* A list of all current plugins, including the built-in ones (the doc
* explorer and the history).
*/
plugins: GraphiQLPlugin[];
/**
* Defines the plugin which is currently visible.
* @param plugin The plugin that should become visible. You can either pass
* the plugin object (has to be referentially equal to the one passed as
* prop) or the plugin title as string. If `null` is passed, no plugin will
* be visible.
*/
setVisiblePlugin(plugin: GraphiQLPlugin | string | null): void;
/**
* The plugin which is currently visible.
*/
visiblePlugin: GraphiQLPlugin | null;
};
export const PluginContext =
createNullableContext<PluginContextType>('PluginContext');
export type PluginContextProviderProps = {
children: ReactNode;
/**
* Invoked when the visibility state of any plugin changes.
* @param visiblePlugin The plugin object that is now visible. If no plugin
* is visible, the function will be invoked with `null`.
*/
onTogglePluginVisibility?(visiblePlugin: GraphiQLPlugin | null): void;
/**
* This props accepts a list of plugins that will be shown in addition to the
* built-in ones (the doc explorer and the history).
*/
plugins?: GraphiQLPlugin[];
/**
* This prop can be used to set the visibility state of plugins. Every time
* this prop changes, the visibility state will be overridden. Note that the
* visibility state can change in between these updates, for example by
* calling the `setVisiblePlugin` function provided by the context.
*/
visiblePlugin?: GraphiQLPlugin | string;
};
export function PluginContextProvider(props: PluginContextProviderProps) {
const storage = useStorageContext();
const explorerContext = useExplorerContext();
const historyContext = useHistoryContext();
const hasExplorerContext = Boolean(explorerContext);
const hasHistoryContext = Boolean(historyContext);
const plugins = useMemo(() => {
const pluginList: GraphiQLPlugin[] = [];
const pluginTitles: Record<string, true> = {};
if (hasExplorerContext) {
pluginList.push(DOC_EXPLORER_PLUGIN);
pluginTitles[DOC_EXPLORER_PLUGIN.title] = true;
}
if (hasHistoryContext) {
pluginList.push(HISTORY_PLUGIN);
pluginTitles[HISTORY_PLUGIN.title] = true;
}
for (const plugin of props.plugins || []) {
if (typeof plugin.title !== 'string' || !plugin.title) {
throw new Error('All GraphiQL plugins must have a unique title');
}
if (pluginTitles[plugin.title]) {
throw new Error(
`All GraphiQL plugins must have a unique title, found two plugins with the title '${plugin.title}'`,
);
} else {
pluginList.push(plugin);
pluginTitles[plugin.title] = true;
}
}
return pluginList;
}, [hasExplorerContext, hasHistoryContext, props.plugins]);
const [visiblePlugin, internalSetVisiblePlugin] =
useState<GraphiQLPlugin | null>(() => {
const storedValue = storage?.get(STORAGE_KEY);
const pluginForStoredValue = plugins.find(
plugin => plugin.title === storedValue,
);
if (pluginForStoredValue) {
return pluginForStoredValue;
}
if (storedValue) {
storage?.set(STORAGE_KEY, '');
}
if (!props.visiblePlugin) {
return null;
}
return (
plugins.find(
plugin =>
(typeof props.visiblePlugin === 'string'
? plugin.title
: plugin) === props.visiblePlugin,
) || null
);
});
const { onTogglePluginVisibility, children } = props;
const setVisiblePlugin = useCallback<PluginContextType['setVisiblePlugin']>(
plugin => {
const newVisiblePlugin = plugin
? plugins.find(
p => (typeof plugin === 'string' ? p.title : p) === plugin,
) || null
: null;
internalSetVisiblePlugin(current => {
if (newVisiblePlugin === current) {
return current;
}
onTogglePluginVisibility?.(newVisiblePlugin);
return newVisiblePlugin;
});
},
[onTogglePluginVisibility, plugins],
);
useEffect(() => {
if (props.visiblePlugin) {
setVisiblePlugin(props.visiblePlugin);
}
}, [plugins, props.visiblePlugin, setVisiblePlugin]);
const value = useMemo<PluginContextType>(
() => ({ plugins, setVisiblePlugin, visiblePlugin }),
[plugins, setVisiblePlugin, visiblePlugin],
);
return (
<PluginContext.Provider value={value}>{children}</PluginContext.Provider>
);
}
export const usePluginContext = createContextHook(PluginContext);
const STORAGE_KEY = 'visiblePlugin';