mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
feat: full screen onboarding with a plugin picker and per step versions
This commit is contained in:
1 parent
1e74ce4827
commit
b9dd3488a5
59 files changed
+5413
-643
No files matched your search
@@ -45,6 +45,11 @@ import {
|
||||
} from "@/shell/action-registry";
|
||||
import { pluginApiFor, pluginFetch, pluginWsUrl } from "@/lib/plugin-transport";
|
||||
import { registerPluginComponent } from "./component-registry";
|
||||
import {
|
||||
onboardingSteps,
|
||||
sectionPosition,
|
||||
} from "@/onboarding/onboarding-registry";
|
||||
import { pluginStepKey } from "@/types/onboarding";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
registerLoginMethod,
|
||||
@@ -70,6 +75,8 @@ import { pluginKey } from "@/lib/plugin-i18n";
|
||||
import { hasPermission } from "@/hooks/use-permissions";
|
||||
import i18n from "@/i18n/i18n";
|
||||
|
||||
const ONBOARDING_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
||||
|
||||
export interface PluginAppHandle {
|
||||
app: TermixApp;
|
||||
/** Runs every disposer, newest first, isolating failures. */
|
||||
@@ -122,6 +129,12 @@ export function createPluginApp(
|
||||
fn: F | undefined,
|
||||
): F | undefined => scopeHostCallback(fn, pluginId);
|
||||
|
||||
const requireStepId = (id: string, what: string) => {
|
||||
if (typeof id !== "string" || !ONBOARDING_ID.test(id)) {
|
||||
throw new Error(`${pluginId}: ${what} id "${id}" is not valid`);
|
||||
}
|
||||
};
|
||||
|
||||
const requireDeclared = (kind: ViewKind, id: string, what: string) => {
|
||||
if (!manifestDeclares(contributes, kind, id)) {
|
||||
const field =
|
||||
@@ -463,6 +476,45 @@ export function createPluginApp(
|
||||
);
|
||||
},
|
||||
|
||||
registerOnboardingStep(step) {
|
||||
requireStepId(step.id, "onboarding step");
|
||||
const version = step.version ?? 1;
|
||||
if (!Number.isInteger(version) || version < 1) {
|
||||
throw new Error(
|
||||
`${pluginId}: onboarding step "${step.id}" needs a whole version of 1 or more`,
|
||||
);
|
||||
}
|
||||
const when = guardCallback(pluginId, step.when, false);
|
||||
return track(
|
||||
onboardingSteps.register({
|
||||
id: pluginStepKey(pluginId, step.id),
|
||||
pluginId,
|
||||
version,
|
||||
titleKey: key(step.titleKey),
|
||||
descriptionKey: step.descriptionKey
|
||||
? key(step.descriptionKey)
|
||||
: undefined,
|
||||
icon: step.icon
|
||||
? (withIconBoundary(pluginId, step.icon as never) as never)
|
||||
: undefined,
|
||||
Component: scoped(step.component),
|
||||
audience: step.audience ?? "all",
|
||||
permission: step.permission
|
||||
? resolvePluginPermission(pluginId, step.permission)
|
||||
: undefined,
|
||||
position: sectionPosition(step.section, step.order),
|
||||
isRelevant: when
|
||||
? (ctx) =>
|
||||
when({
|
||||
isAdmin: ctx.isAdmin,
|
||||
isDesktop: ctx.isDesktop,
|
||||
mode: ctx.mode,
|
||||
}) === true
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
invokeAction: (id, ...args) => invokeAction(id, ...args),
|
||||
|
||||
registerComponent(id, component) {
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
import { PluginViewPlaceholder } from "./PluginViewPlaceholder";
|
||||
import { resolveLayoutTabTarget } from "@/shell/shell-layout";
|
||||
import { getExtension, listExtensions } from "./extension-registry";
|
||||
import { onboardingSteps } from "@/onboarding/onboarding-registry";
|
||||
import type { WorkspaceTabSnapshot } from "@/types/ui-types";
|
||||
|
||||
function recordingShell(calls: ShellCall[]): TabShellCallbacks {
|
||||
@@ -201,6 +202,8 @@ export async function renderPlugin(
|
||||
|
||||
const mine = <T extends { pluginId?: string }>(items: T[]) =>
|
||||
items.filter((item) => item.pluginId === pluginId);
|
||||
const unprefixed = (items: { id: string; pluginId?: string }[]) =>
|
||||
mine(items).map((item) => item.id.slice(pluginId.length + 1));
|
||||
|
||||
const tabRecord = (type: string, host?: Host): Tab => ({
|
||||
id: `${type}-test`,
|
||||
@@ -257,6 +260,7 @@ export async function renderPlugin(
|
||||
mine(listKeybindingActions()).map((action) => action.id),
|
||||
keybindingDefaults: () =>
|
||||
mine(listKeybindingDefaults()).map((binding) => binding.id),
|
||||
onboardingSteps: () => unprefixed(onboardingSteps.list()),
|
||||
},
|
||||
|
||||
async loadPaletteGroup(id) {
|
||||
@@ -332,6 +336,35 @@ export async function renderPlugin(
|
||||
return wrap(<Component {...props} />);
|
||||
},
|
||||
|
||||
renderOnboardingStep(id, props = {}) {
|
||||
const step =
|
||||
onboardingSteps.get(`${pluginId}:${id}`) ??
|
||||
missing("an onboarding step", id);
|
||||
const Component = step.Component;
|
||||
let canContinue = true;
|
||||
let beforeNext: (() => boolean | Promise<boolean>) | null = null;
|
||||
const element = wrap(
|
||||
<Component
|
||||
mode="full"
|
||||
isAdmin
|
||||
isDesktop={false}
|
||||
shellReady={false}
|
||||
setCanContinue={(ok) => {
|
||||
canContinue = ok;
|
||||
}}
|
||||
setBeforeNext={(fn) => {
|
||||
beforeNext = fn;
|
||||
}}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
return {
|
||||
element,
|
||||
canContinue: () => canContinue,
|
||||
next: async () => (beforeNext ? await beforeNext() : true),
|
||||
};
|
||||
},
|
||||
|
||||
renderDashboardCard(id) {
|
||||
const card = getRegisteredDashboardCard(id) ?? missing("a card", id);
|
||||
const Component = card.component;
|
||||
|
||||
Reference in new issue
Block a user