mirror of https://github.com/stashapp/stash.git
UI plugin dependencies (#4307)
* Add requires field to UI plugin config * Use defer instead of async for useScript * Load plugins based on dependency * Document new field
This commit is contained in:
parent
910ff27730
commit
11be56cc42
|
@ -25,6 +25,8 @@ query Plugins {
|
|||
type
|
||||
}
|
||||
|
||||
requires
|
||||
|
||||
paths {
|
||||
css
|
||||
javascript
|
||||
|
|
|
@ -18,6 +18,12 @@ type Plugin {
|
|||
hooks: [PluginHook!]
|
||||
settings: [PluginSetting!]
|
||||
|
||||
"""
|
||||
Plugin IDs of plugins that this plugin depends on.
|
||||
Applies only for UI plugins to indicate css/javascript load order.
|
||||
"""
|
||||
requires: [ID!]
|
||||
|
||||
paths: PluginPaths!
|
||||
}
|
||||
|
||||
|
|
|
@ -55,3 +55,7 @@ func (r *pluginResolver) Paths(ctx context.Context, obj *plugin.Plugin) (*Plugin
|
|||
|
||||
return b.paths(), nil
|
||||
}
|
||||
|
||||
func (r *pluginResolver) Requires(ctx context.Context, obj *plugin.Plugin) ([]string, error) {
|
||||
return obj.UI.Requires, nil
|
||||
}
|
||||
|
|
|
@ -72,6 +72,10 @@ type PluginCSP struct {
|
|||
}
|
||||
|
||||
type UIConfig struct {
|
||||
// Requires is a list of plugin IDs that this plugin depends on.
|
||||
// These plugins will be loaded before this plugin.
|
||||
Requires []string `yaml:"requires"`
|
||||
|
||||
// Content Security Policy configuration for the plugin.
|
||||
CSP PluginCSP `yaml:"csp"`
|
||||
|
||||
|
@ -239,6 +243,7 @@ func (c Config) toPlugin() *Plugin {
|
|||
Tasks: c.getPluginTasks(false),
|
||||
Hooks: c.getPluginHooks(false),
|
||||
UI: PluginUI{
|
||||
Requires: c.UI.Requires,
|
||||
ExternalScript: c.UI.getExternalScripts(),
|
||||
ExternalCSS: c.UI.getExternalCSS(),
|
||||
Javascript: c.UI.getJavascriptFiles(c),
|
||||
|
|
|
@ -42,6 +42,10 @@ type Plugin struct {
|
|||
}
|
||||
|
||||
type PluginUI struct {
|
||||
// Requires is a list of plugin IDs that this plugin depends on.
|
||||
// These plugins will be loaded before this plugin.
|
||||
Requires []string `json:"requires"`
|
||||
|
||||
// Content Security Policy configuration for the plugin.
|
||||
CSP PluginCSP `json:"csp"`
|
||||
|
||||
|
|
|
@ -90,6 +90,54 @@ function languageMessageString(language: string) {
|
|||
return language.replace(/-/, "");
|
||||
}
|
||||
|
||||
type PluginList = NonNullable<Required<GQL.PluginsQuery["plugins"]>>;
|
||||
|
||||
// sort plugins by their dependencies
|
||||
function sortPlugins(plugins: PluginList) {
|
||||
type Node = { id: string; afters: string[] };
|
||||
|
||||
let nodes: Record<string, Node> = {};
|
||||
let sorted: PluginList = [];
|
||||
let visited: Record<string, boolean> = {};
|
||||
|
||||
plugins.forEach((v) => {
|
||||
let from = v.id;
|
||||
|
||||
if (!nodes[from]) nodes[from] = { id: from, afters: [] };
|
||||
|
||||
v.requires?.forEach((to) => {
|
||||
if (!nodes[to]) nodes[to] = { id: to, afters: [] };
|
||||
if (!nodes[to].afters.includes(from)) nodes[to].afters.push(from);
|
||||
});
|
||||
});
|
||||
|
||||
function visit(idstr: string, ancestors: string[] = []) {
|
||||
let node = nodes[idstr];
|
||||
const { id } = node;
|
||||
|
||||
if (visited[idstr]) return;
|
||||
|
||||
ancestors.push(id);
|
||||
visited[idstr] = true;
|
||||
node.afters.forEach(function (afterID) {
|
||||
if (ancestors.indexOf(afterID) >= 0)
|
||||
throw new Error("closed chain : " + afterID + " is in " + id);
|
||||
visit(afterID.toString(), ancestors.slice());
|
||||
});
|
||||
|
||||
const plugin = plugins.find((v) => v.id === id);
|
||||
if (plugin) {
|
||||
sorted.unshift(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(nodes).forEach((n) => {
|
||||
visit(n);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const config = useConfiguration();
|
||||
const [saveUI] = useConfigureUI();
|
||||
|
@ -159,29 +207,36 @@ export const App: React.FC = () => {
|
|||
error: pluginsError,
|
||||
} = usePlugins();
|
||||
|
||||
const pluginJavascripts = useMemoOnce(() => {
|
||||
const sortedPlugins = useMemoOnce(() => {
|
||||
return [
|
||||
uniq(
|
||||
plugins?.plugins
|
||||
?.filter((plugin) => plugin.enabled && plugin.paths.javascript)
|
||||
.map((plugin) => plugin.paths.javascript!)
|
||||
.flat() ?? []
|
||||
),
|
||||
sortPlugins(plugins?.plugins ?? []),
|
||||
!pluginsLoading && !pluginsError,
|
||||
];
|
||||
}, [plugins?.plugins, pluginsLoading, pluginsError]);
|
||||
|
||||
const pluginJavascripts = useMemoOnce(() => {
|
||||
return [
|
||||
uniq(
|
||||
sortedPlugins
|
||||
?.filter((plugin) => plugin.enabled && plugin.paths.javascript)
|
||||
.map((plugin) => plugin.paths.javascript!)
|
||||
.flat() ?? []
|
||||
),
|
||||
!!sortedPlugins && !pluginsLoading && !pluginsError,
|
||||
];
|
||||
}, [sortedPlugins, pluginsLoading, pluginsError]);
|
||||
|
||||
const pluginCSS = useMemoOnce(() => {
|
||||
return [
|
||||
uniq(
|
||||
plugins?.plugins
|
||||
sortedPlugins
|
||||
?.filter((plugin) => plugin.enabled && plugin.paths.css)
|
||||
.map((plugin) => plugin.paths.css!)
|
||||
.flat() ?? []
|
||||
),
|
||||
!pluginsLoading && !pluginsError,
|
||||
!!sortedPlugins && !pluginsLoading && !pluginsError,
|
||||
];
|
||||
}, [plugins, pluginsLoading, pluginsError]);
|
||||
}, [sortedPlugins, pluginsLoading, pluginsError]);
|
||||
|
||||
useScript(pluginJavascripts ?? [], !pluginsLoading && !pluginsError);
|
||||
useCSS(pluginCSS ?? [], !pluginsLoading && !pluginsError);
|
||||
|
|
|
@ -43,6 +43,10 @@ ui:
|
|||
javascript:
|
||||
- <path to javascript file>
|
||||
|
||||
# optional list of plugin IDs to load prior to this plugin
|
||||
requires:
|
||||
- <plugin ID>
|
||||
|
||||
# optional list of assets
|
||||
assets:
|
||||
urlPrefix: fsLocation
|
||||
|
@ -77,6 +81,9 @@ The `exec`, `interface`, `errLog` and `tasks` fields are used only for plugins w
|
|||
The `css` and `javascript` field values may be relative paths to the plugin configuration file, or
|
||||
may be full external URLs.
|
||||
|
||||
The `requires` field is a list of plugin IDs which must have their javascript/css files loaded
|
||||
before this plugins javascript/css files.
|
||||
|
||||
The `assets` field is a map of URL prefixes to filesystem paths relative to the plugin configuration file.
|
||||
Assets are mounted to the `/plugin/{pluginID}/assets` path.
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ const useScript = (urls: string | string[], condition?: boolean) => {
|
|||
const script = document.createElement("script");
|
||||
|
||||
script.src = url;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
return script;
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue