@360crewing/marketplace-sdk 0.1.3
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 +164 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +20 -0
- package/dist/defineExtension.d.ts +7 -0
- package/dist/defineExtension.js +69 -0
- package/dist/hooks/index.d.ts +49 -0
- package/dist/hooks/index.js +82 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.html +1 -0
- package/dist/index.js +14 -0
- package/dist/mf.d.ts +9 -0
- package/dist/mf.js +11 -0
- package/dist/platformClient.d.ts +5 -0
- package/dist/platformClient.js +21 -0
- package/dist/static/js/index.cc3a40625d.js +2 -0
- package/dist/static/js/index.cc3a40625d.js.LICENSE.txt +9 -0
- package/dist/types/host.d.ts +141 -0
- package/dist/types/host.js +1 -0
- package/dist/types/locale.d.ts +6 -0
- package/dist/types/locale.js +1 -0
- package/dist/types/manifest.d.ts +39 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/platform.d.ts +185 -0
- package/dist/types/platform.js +6 -0
- package/dist/types/sdk.d.ts +24 -0
- package/dist/types/sdk.js +1 -0
- package/dist/types/slots.d.ts +134 -0
- package/dist/types/slots.js +18 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @360crewing/marketplace-sdk
|
|
2
|
+
|
|
3
|
+
Public SDK contract for 360crewing marketplace extensions. This is a
|
|
4
|
+
**contract**: after `0.1.0`, breaking API changes ship only in a major version.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i @360crewing/marketplace-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Peer dependencies (provided by the host via Module Federation `shared` — an
|
|
13
|
+
extension does NOT bundle its own copies): `react`, `axios`, `react-i18next`,
|
|
14
|
+
`@360crewing/ui`.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import {defineExtension, useUser, useI18n, Card, Button, Slot} from "@360crewing/marketplace-sdk";
|
|
20
|
+
import en from "./locales/en.json";
|
|
21
|
+
|
|
22
|
+
// dashboard.widget — a card on the dashboard (bare component).
|
|
23
|
+
function KpiWidget() {
|
|
24
|
+
const user = useUser();
|
|
25
|
+
const {t} = useI18n();
|
|
26
|
+
return (
|
|
27
|
+
<Card title={t("title")}>
|
|
28
|
+
<p>{t("greeting", {name: user.full_name})}</p>
|
|
29
|
+
<Button title={t("refresh")} onClick={() => {}}/>
|
|
30
|
+
</Card>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// seafarer.menu — a menu item on the seafarer card + the page it opens.
|
|
35
|
+
function CrewAnalyticsPage(props: {seafarer: {uuid: string}}) {
|
|
36
|
+
return <div>Analytics for seafarer {props.seafarer.uuid}</div>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default defineExtension({
|
|
40
|
+
id: "com.acme.crew-analytics", // reverse-DNS, unique
|
|
41
|
+
locales: {en}, // >=1 locale; files in locales/*.json
|
|
42
|
+
slots: {
|
|
43
|
+
[Slot.Dashboard.Widget]: KpiWidget, // widget: a component
|
|
44
|
+
[Slot.Seafarer.Menu]: { // menu: {title,icon?,page}
|
|
45
|
+
title: "Crew Analytics",
|
|
46
|
+
page: CrewAnalyticsPage,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Slot model
|
|
53
|
+
|
|
54
|
+
A slot is a named mount point in the host. The slot name = **where** the
|
|
55
|
+
extension appears. Two archetypes:
|
|
56
|
+
|
|
57
|
+
| Archetype | Slots | What you register |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| **widget** | `Slot.Dashboard.Widget` | a bare React component (a card in the dashboard grid) |
|
|
60
|
+
| **menu** | `Slot.Seafarer.Menu`, `Slot.Vessel.Menu`, `Slot.Contract.Menu`, `Slot.Nav.{Admin,Data,Finance,Fleet,Reports}.Menu` | `MenuRegistration { title, icon?, page }` |
|
|
61
|
+
|
|
62
|
+
- `Slot` is a nested namespace (one import): `Slot.Seafarer.Menu`,
|
|
63
|
+
`Slot.Nav.Data.Menu`, `Slot.Dashboard.Widget`. A raw string key
|
|
64
|
+
(`"seafarer.menu"`) is also accepted and type-safe.
|
|
65
|
+
- **menu slot**: `title` is the item label, `page` is the page component the
|
|
66
|
+
host renders on click (the route is derived from the extension `id`).
|
|
67
|
+
- One extension can occupy several slots; several extensions in one slot →
|
|
68
|
+
several menu items.
|
|
69
|
+
- Context in props: widget → `{ widget:{id,settings}, sdk }`; entity-menu →
|
|
70
|
+
`{ seafarer|vessel|contract:{uuid}, sdk }`; nav-menu → `{ sdk }`.
|
|
71
|
+
|
|
72
|
+
`defineExtension` validates: reverse-DNS `id`, >=1 locale, >=1 slot, and that a
|
|
73
|
+
menu slot is `{ title, page }`.
|
|
74
|
+
|
|
75
|
+
## i18n
|
|
76
|
+
|
|
77
|
+
- Text lives in `locales/<lang>.json`; `defineExtension({ locales: { en, ru } })`.
|
|
78
|
+
- The host registers them as the i18next namespace `app.<id>` on load.
|
|
79
|
+
- `useI18n()` is `react-i18next` bound to that namespace:
|
|
80
|
+
`const {t} = useI18n(); t("title")`. Content re-renders when the user changes
|
|
81
|
+
language (no reload).
|
|
82
|
+
- No translation for the active language → falls back to the host default.
|
|
83
|
+
|
|
84
|
+
## Storage
|
|
85
|
+
|
|
86
|
+
The platform gives an extension **two primitives** (no backend of your own):
|
|
87
|
+
|
|
88
|
+
**`useUserSettings()` / `sdk.userSettings`** — per-user settings (saved filters,
|
|
89
|
+
UI prefs). Small JSON, last-write-wins, scoped per user.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
const us = useUserSettings<{ visaFilter?: object }>();
|
|
93
|
+
await us.set({ visaFilter }); // shallow-merge
|
|
94
|
+
const { visaFilter } = await us.get(); // whole document
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**`useDb()` / `sdk.db`** — the extension's own database: one isolated SQLite
|
|
98
|
+
file per `(tenant, installation)`. **Full SQL**, the extension owns its schema.
|
|
99
|
+
`query` (SELECT), `exec` (INSERT/UPDATE/DELETE/DDL), `tx`.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
const db = useDb();
|
|
103
|
+
|
|
104
|
+
// schema — once (on install / migration)
|
|
105
|
+
await db.exec(`CREATE TABLE IF NOT EXISTS visa(
|
|
106
|
+
id INTEGER PRIMARY KEY, seafarer_uuid TEXT, country TEXT,
|
|
107
|
+
status TEXT, applied_at TEXT, docs JSON)`);
|
|
108
|
+
|
|
109
|
+
// CREATE
|
|
110
|
+
await db.exec(
|
|
111
|
+
`INSERT INTO visa(seafarer_uuid,country,status,applied_at,docs) VALUES(?,?,?,?,?)`,
|
|
112
|
+
[uuid, "US", "pending", "2026-05-18", JSON.stringify(["passport","photo"])]);
|
|
113
|
+
|
|
114
|
+
// READ + filter, including an array column (JSON1)
|
|
115
|
+
const rows = await db.query(`
|
|
116
|
+
SELECT * FROM visa
|
|
117
|
+
WHERE status IN ('pending','submitted') AND country = ?
|
|
118
|
+
AND EXISTS (SELECT 1 FROM json_each(docs) WHERE value = 'passport')
|
|
119
|
+
ORDER BY applied_at DESC LIMIT 50`, ["US"]);
|
|
120
|
+
|
|
121
|
+
// UPDATE / DELETE / transaction
|
|
122
|
+
await db.exec(`UPDATE visa SET status=? WHERE id=?`, ["approved", 1]);
|
|
123
|
+
await db.tx(async (t) => {
|
|
124
|
+
await t.exec(`DELETE FROM visa WHERE status='rejected'`);
|
|
125
|
+
await t.exec(`INSERT INTO audit(ts) VALUES(?)`, [Date.now()]);
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Isolation: the server derives `tenant`/`installation` from the app-JWT, not the
|
|
130
|
+
request body; SQL is pinned to that file — no cross-extension data. Host guards:
|
|
131
|
+
SQLite authorizer denylist (no `ATTACH`/`load_extension`/file pragmas),
|
|
132
|
+
per-query timeout, result cap, file quota, write serialization. The SQLite
|
|
133
|
+
dialect is a stable public contract.
|
|
134
|
+
|
|
135
|
+
## What's included
|
|
136
|
+
|
|
137
|
+
| Group | API |
|
|
138
|
+
|---|---|
|
|
139
|
+
| Entry point | `defineExtension({ id, locales, slots, onActivate?, onDeactivate? })` |
|
|
140
|
+
| Hooks | `usePlatform`, `useUser`, `useTenant`, `useNotify`, `useModal`, `useConfirm`, `useForm`, `useTheme`, `useI18n`, `useNavigate`, `useUsage`, `useDb`, `useUserSettings` |
|
|
141
|
+
| Slots | `Slot.*` + `MenuRegistration` + context types (`DashboardWidgetContext`, `SeafarerMenuContext`, `VesselMenuContext`, `ContractMenuContext`, `NavMenuContext`) |
|
|
142
|
+
| UI | re-exports all `@360crewing/ui` primitives (`Button`, `Card`, `TextField`, …) |
|
|
143
|
+
|
|
144
|
+
## Boundaries
|
|
145
|
+
|
|
146
|
+
- Hooks are the **only** way to use host services (Modal / Toast / Confirm /
|
|
147
|
+
Form / platform data / i18n / navigation). Extensions have no direct access to
|
|
148
|
+
host managers.
|
|
149
|
+
- `usePlatform()` is a typed client for platform data
|
|
150
|
+
(`platform.seafarers/vessels/contracts` → `.list()` / `.search(params)` /
|
|
151
|
+
`.get(uuid)`). No hand-written URLs or JSON; the host injects an app-scoped
|
|
152
|
+
JWT and the server checks scope. No raw axios.
|
|
153
|
+
- `useNavigate()` navigates the host app (host-absolute paths).
|
|
154
|
+
- Hooks work only inside a component the host mounted into a slot. Calling them
|
|
155
|
+
at module scope throws a clear error.
|
|
156
|
+
|
|
157
|
+
## Building & shipping an extension
|
|
158
|
+
|
|
159
|
+
An extension is a Module Federation **remote**, built with the reference MF
|
|
160
|
+
tooling (Rsbuild/Rspack, `@module-federation/rsbuild-plugin`), that **exposes**
|
|
161
|
+
`./extension` = the default export of `defineExtension(...)`. `@360crewing/*` and
|
|
162
|
+
`react` are declared `shared` (`import:false`) so the extension always uses the
|
|
163
|
+
host's instance (deduped across the MF boundary). A working reference lives in
|
|
164
|
+
this package's `example/` directory (`rsbuild.config.ts` + `locales/`).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ExtensionHost } from "./types/host";
|
|
3
|
+
export interface ExtensionHostProviderProps {
|
|
4
|
+
host: ExtensionHost;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Host-runtime-only: wraps every slot component with the per-extension host
|
|
9
|
+
* bridge. Extension authors never render this themselves.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ExtensionHostProvider({ host, children }: ExtensionHostProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
/** Internal accessor for every SDK hook; throws if used outside a host-mounted slot. */
|
|
13
|
+
export declare function useExtensionHost(): ExtensionHost;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const ExtensionHostContext = createContext(null);
|
|
4
|
+
/**
|
|
5
|
+
* Host-runtime-only: wraps every slot component with the per-extension host
|
|
6
|
+
* bridge. Extension authors never render this themselves.
|
|
7
|
+
*/
|
|
8
|
+
export function ExtensionHostProvider({ host, children }) {
|
|
9
|
+
return (_jsx(ExtensionHostContext.Provider, { value: host, children: children }));
|
|
10
|
+
}
|
|
11
|
+
/** Internal accessor for every SDK hook; throws if used outside a host-mounted slot. */
|
|
12
|
+
export function useExtensionHost() {
|
|
13
|
+
const host = useContext(ExtensionHostContext);
|
|
14
|
+
if (!host) {
|
|
15
|
+
throw new Error("[@360crewing/marketplace-sdk] SDK hook used outside the host runtime. " +
|
|
16
|
+
"Hooks (usePlatform, useUser, ...) only work inside a component the host " +
|
|
17
|
+
"mounts via a slot from defineExtension(). Do not call them at module scope.");
|
|
18
|
+
}
|
|
19
|
+
return host;
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DefineExtensionConfig, ExtensionDefinition } from "./types/manifest";
|
|
2
|
+
/**
|
|
3
|
+
* Validates config and packages it into a serializable `ExtensionDefinition`.
|
|
4
|
+
* Does NOT register i18n or touch the host — the host runtime reads
|
|
5
|
+
* `locales`/`slots` and mounts everything.
|
|
6
|
+
*/
|
|
7
|
+
export declare function defineExtension(config: DefineExtensionConfig): ExtensionDefinition;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const APP_ID_RE = /^[a-z0-9]+(\.[a-z0-9-]+)+$/;
|
|
2
|
+
/** Slots whose registration is a bare component (not a MenuRegistration). */
|
|
3
|
+
const WIDGET_SLOTS = new Set(["dashboard.widget"]);
|
|
4
|
+
const isMenuSlot = (slot) => !WIDGET_SLOTS.has(slot);
|
|
5
|
+
/**
|
|
6
|
+
* Validates config and packages it into a serializable `ExtensionDefinition`.
|
|
7
|
+
* Does NOT register i18n or touch the host — the host runtime reads
|
|
8
|
+
* `locales`/`slots` and mounts everything.
|
|
9
|
+
*/
|
|
10
|
+
export function defineExtension(config) {
|
|
11
|
+
const { id, locales, slots, onActivate, onDeactivate } = config;
|
|
12
|
+
if (!id || !APP_ID_RE.test(id)) {
|
|
13
|
+
throw new Error(`[@360crewing/marketplace-sdk] Invalid extension id "${id}". ` +
|
|
14
|
+
`Use reverse-DNS, e.g. "com.acme.crew-analytics".`);
|
|
15
|
+
}
|
|
16
|
+
const localeKeys = Object.keys(locales);
|
|
17
|
+
if (localeKeys.length === 0) {
|
|
18
|
+
throw new Error(`[@360crewing/marketplace-sdk] Extension "${id}" must declare at least one locale.`);
|
|
19
|
+
}
|
|
20
|
+
const slotNames = Object.keys(slots);
|
|
21
|
+
if (slotNames.length === 0) {
|
|
22
|
+
throw new Error(`[@360crewing/marketplace-sdk] Extension "${id}" registers no slots — it would render nothing.`);
|
|
23
|
+
}
|
|
24
|
+
const menus = [];
|
|
25
|
+
for (const slot of slotNames) {
|
|
26
|
+
const reg = slots[slot];
|
|
27
|
+
if (isMenuSlot(slot)) {
|
|
28
|
+
const menu = reg;
|
|
29
|
+
if (!menu ||
|
|
30
|
+
typeof menu !== "object" ||
|
|
31
|
+
typeof menu.title !== "string" ||
|
|
32
|
+
!menu.title ||
|
|
33
|
+
typeof menu.page !== "function") {
|
|
34
|
+
throw new Error(`[@360crewing/marketplace-sdk] Extension "${id}" slot "${slot}" must be ` +
|
|
35
|
+
`a { title, icon?, page } menu registration.`);
|
|
36
|
+
}
|
|
37
|
+
menus.push({ slot, title: menu.title });
|
|
38
|
+
}
|
|
39
|
+
else if (slot === "dashboard.widget") {
|
|
40
|
+
// Accept a bare React component OR a WidgetRegistration object.
|
|
41
|
+
const isFn = typeof reg === "function";
|
|
42
|
+
const isObjectWithPage = typeof reg === "object" &&
|
|
43
|
+
reg !== null &&
|
|
44
|
+
typeof reg.page === "function";
|
|
45
|
+
if (!isFn && !isObjectWithPage) {
|
|
46
|
+
throw new Error(`[@360crewing/marketplace-sdk] Extension "${id}" slot "dashboard.widget" must be ` +
|
|
47
|
+
`a React component OR a WidgetRegistration { page, category?, is_tab?, min_size?, max_size?, title?, description? }.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (typeof reg !== "function") {
|
|
51
|
+
throw new Error(`[@360crewing/marketplace-sdk] Extension "${id}" slot "${slot}" must be ` +
|
|
52
|
+
`a React component.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const defaultLocale = localeKeys.includes("en") ? "en" : localeKeys[0];
|
|
56
|
+
return {
|
|
57
|
+
manifest: {
|
|
58
|
+
id,
|
|
59
|
+
slots: slotNames,
|
|
60
|
+
menus,
|
|
61
|
+
locales: localeKeys,
|
|
62
|
+
defaultLocale,
|
|
63
|
+
},
|
|
64
|
+
locales,
|
|
65
|
+
slots,
|
|
66
|
+
onActivate,
|
|
67
|
+
onDeactivate,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ExtensionUser, ExtensionTenant, ExtensionTheme, ExtensionNotify, ExtensionModalApi, ExtensionI18n, ExtensionNavigate, ExtensionForm, ExtensionDb, ExtensionUserSettings, ConfirmOptions } from "../types/host";
|
|
2
|
+
import type { UsageApi } from "../types/sdk";
|
|
3
|
+
import type { PlatformClient } from "../types/platform";
|
|
4
|
+
/** Typed, scope-gated platform-data client (seafarers/vessels/contracts).
|
|
5
|
+
* Replaces the old raw-axios `useApi()` — call functions, not endpoints. */
|
|
6
|
+
export declare function usePlatform(): PlatformClient;
|
|
7
|
+
/** The current authenticated user. */
|
|
8
|
+
export declare function useUser(): ExtensionUser;
|
|
9
|
+
/** The tenant the extension is installed in. */
|
|
10
|
+
export declare function useTenant(): ExtensionTenant;
|
|
11
|
+
/** Toast notifications — wrapper over the host ToastManager. */
|
|
12
|
+
export declare function useNotify(): ExtensionNotify;
|
|
13
|
+
/** Open/close a host modal rendering an extension-owned node. */
|
|
14
|
+
export declare function useModal(): ExtensionModalApi;
|
|
15
|
+
/** Confirmation dialog. Resolves true/false by the user's choice. */
|
|
16
|
+
export declare function useConfirm(): (options: ConfirmOptions) => Promise<boolean>;
|
|
17
|
+
/** Platform design tokens. */
|
|
18
|
+
export declare function useTheme(): ExtensionTheme;
|
|
19
|
+
/**
|
|
20
|
+
* react-i18next bound to this extension's `app.<id>` namespace (host registers
|
|
21
|
+
* the extension's `locales/*.json` there on load). Re-renders on changeLanguage.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useI18n(): ExtensionI18n;
|
|
24
|
+
/** Scoped navigation — the host restricts targets to `/apps/<appId>/*`. */
|
|
25
|
+
export declare function useNavigate(): ExtensionNavigate;
|
|
26
|
+
/** Metered usage reporting. */
|
|
27
|
+
export declare function useUsage(): UsageApi;
|
|
28
|
+
/**
|
|
29
|
+
* The extension's own SQLite database (one isolated file per
|
|
30
|
+
* tenant+installation). Full SQL — `query` (SELECT), `exec`
|
|
31
|
+
* (INSERT/UPDATE/DELETE/DDL), `tx`. The extension owns its schema. BRD БП-13.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useDb(): ExtensionDb;
|
|
34
|
+
/**
|
|
35
|
+
* Per-user settings store (saved filters, UI prefs). Small JSON,
|
|
36
|
+
* last-write-wins, scoped to the current user. BRD БП-13.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useUserSettings<T extends Record<string, unknown> = Record<string, unknown>>(): ExtensionUserSettings<T>;
|
|
39
|
+
export interface UseFormConfig<T extends Record<string, unknown>> {
|
|
40
|
+
initial: T;
|
|
41
|
+
onSubmit: (values: T) => Promise<void> | void;
|
|
42
|
+
validate?: (values: T) => Partial<Record<keyof T, string>>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Controlled-form helper for the extension's OWN forms (values/errors/dirty/
|
|
46
|
+
* submit). NOT the host entity FormsManager (coupled to host backend URLs);
|
|
47
|
+
* host adapter implements `ExtensionForm` standalone. Instance created once per mount.
|
|
48
|
+
*/
|
|
49
|
+
export declare function useForm<T extends Record<string, unknown>>(config: UseFormConfig<T>): ExtensionForm<T>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import { useExtensionHost } from "../context";
|
|
4
|
+
/** Typed, scope-gated platform-data client (seafarers/vessels/contracts).
|
|
5
|
+
* Replaces the old raw-axios `useApi()` — call functions, not endpoints. */
|
|
6
|
+
export function usePlatform() {
|
|
7
|
+
return useExtensionHost().platform;
|
|
8
|
+
}
|
|
9
|
+
/** The current authenticated user. */
|
|
10
|
+
export function useUser() {
|
|
11
|
+
return useExtensionHost().user;
|
|
12
|
+
}
|
|
13
|
+
/** The tenant the extension is installed in. */
|
|
14
|
+
export function useTenant() {
|
|
15
|
+
return useExtensionHost().tenant;
|
|
16
|
+
}
|
|
17
|
+
/** Toast notifications — wrapper over the host ToastManager. */
|
|
18
|
+
export function useNotify() {
|
|
19
|
+
return useExtensionHost().notify;
|
|
20
|
+
}
|
|
21
|
+
/** Open/close a host modal rendering an extension-owned node. */
|
|
22
|
+
export function useModal() {
|
|
23
|
+
return useExtensionHost().modal;
|
|
24
|
+
}
|
|
25
|
+
/** Confirmation dialog. Resolves true/false by the user's choice. */
|
|
26
|
+
export function useConfirm() {
|
|
27
|
+
return useExtensionHost().confirm;
|
|
28
|
+
}
|
|
29
|
+
/** Platform design tokens. */
|
|
30
|
+
export function useTheme() {
|
|
31
|
+
return useExtensionHost().theme;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* react-i18next bound to this extension's `app.<id>` namespace (host registers
|
|
35
|
+
* the extension's `locales/*.json` there on load). Re-renders on changeLanguage.
|
|
36
|
+
*/
|
|
37
|
+
export function useI18n() {
|
|
38
|
+
const { appId } = useExtensionHost();
|
|
39
|
+
const { t, i18n } = useTranslation(`app.${appId}`);
|
|
40
|
+
return {
|
|
41
|
+
t: (key, options) => t(key, options),
|
|
42
|
+
i18n: {
|
|
43
|
+
get language() {
|
|
44
|
+
return i18n.language;
|
|
45
|
+
},
|
|
46
|
+
changeLanguage: (locale) => i18n.changeLanguage(locale),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Scoped navigation — the host restricts targets to `/apps/<appId>/*`. */
|
|
51
|
+
export function useNavigate() {
|
|
52
|
+
return useExtensionHost().navigate;
|
|
53
|
+
}
|
|
54
|
+
/** Metered usage reporting. */
|
|
55
|
+
export function useUsage() {
|
|
56
|
+
return useExtensionHost().usage;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The extension's own SQLite database (one isolated file per
|
|
60
|
+
* tenant+installation). Full SQL — `query` (SELECT), `exec`
|
|
61
|
+
* (INSERT/UPDATE/DELETE/DDL), `tx`. The extension owns its schema. BRD БП-13.
|
|
62
|
+
*/
|
|
63
|
+
export function useDb() {
|
|
64
|
+
return useExtensionHost().db;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Per-user settings store (saved filters, UI prefs). Small JSON,
|
|
68
|
+
* last-write-wins, scoped to the current user. BRD БП-13.
|
|
69
|
+
*/
|
|
70
|
+
export function useUserSettings() {
|
|
71
|
+
return useExtensionHost().userSettings;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Controlled-form helper for the extension's OWN forms (values/errors/dirty/
|
|
75
|
+
* submit). NOT the host entity FormsManager (coupled to host backend URLs);
|
|
76
|
+
* host adapter implements `ExtensionForm` standalone. Instance created once per mount.
|
|
77
|
+
*/
|
|
78
|
+
export function useForm(config) {
|
|
79
|
+
const host = useExtensionHost();
|
|
80
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
+
return useMemo(() => host.form.create(config), [host]);
|
|
82
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { defineExtension } from "./defineExtension";
|
|
2
|
+
export { mfScope, EXPOSED_MODULE } from "./mf";
|
|
3
|
+
export { createPlatformClient } from "./platformClient";
|
|
4
|
+
export type { PlatformClient, PlatformTransport, PlatformRequest, HttpMethod, Paginated, Pagination, SearchParams, ReadableResource, SearchableResource, Ref, IdName, CountryRef, OrgRef, Address, SeafarerListItem, SeafarerDetail, VesselListItem, VesselDetail, ContractListItem, ContractDetail, } from "./types/platform";
|
|
5
|
+
export { ExtensionHostProvider } from "./context";
|
|
6
|
+
export type { ExtensionHostProviderProps } from "./context";
|
|
7
|
+
export { usePlatform, useUser, useTenant, useNotify, useModal, useConfirm, useTheme, useI18n, useNavigate, useUsage, useForm, useDb, useUserSettings, } from "./hooks";
|
|
8
|
+
export type { UseFormConfig } from "./hooks";
|
|
9
|
+
export { Slot } from "./types/slots";
|
|
10
|
+
export type { SlotName, SlotComponent, SlotContextBase, SlotContextMap, MenuRegistration, WidgetRegistration, WidgetSizeName, SlotRegistrationMap, SlotRegistration, DashboardWidgetContext, SeafarerMenuContext, VesselMenuContext, ContractMenuContext, NavMenuContext, } from "./types/slots";
|
|
11
|
+
export type { DefineExtensionConfig, ExtensionSlots, ManifestMenuEntry, ExtensionManifest, ExtensionDefinition, } from "./types/manifest";
|
|
12
|
+
export type { Locale, LocaleMessages } from "./types/locale";
|
|
13
|
+
export type { ExtensionSdk, UsageApi } from "./types/sdk";
|
|
14
|
+
export type { ExtensionHost, ExtensionUser, ExtensionTenant, ExtensionTheme, ExtensionNotify, ExtensionModalApi, ModalOptions, ConfirmOptions, ExtensionI18n, ExtensionForm, ExtensionField, ExtensionFormFactory, ExtensionNavigate, ExtensionDb, ExtensionDbQuerier, ExtensionUserSettings, SqlParam, DbRow, DbExecResult, } from "./types/host";
|
|
15
|
+
export * from "@360crewing/ui";
|
package/dist/index.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!DOCTYPE html><html><head><title>Rsbuild App</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="/static/js/index.cc3a40625d.js"></script></head><body><div id="root"></div></body></html>
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Entry point
|
|
2
|
+
export { defineExtension } from "./defineExtension";
|
|
3
|
+
// Module Federation contract (scope + exposed-module convention)
|
|
4
|
+
export { mfScope, EXPOSED_MODULE } from "./mf";
|
|
5
|
+
// Typed platform-data client (host wires the transport)
|
|
6
|
+
export { createPlatformClient } from "./platformClient";
|
|
7
|
+
// Host bridge (provided by the host runtime — extensions don't render this)
|
|
8
|
+
export { ExtensionHostProvider } from "./context";
|
|
9
|
+
// Hooks
|
|
10
|
+
export { usePlatform, useUser, useTenant, useNotify, useModal, useConfirm, useTheme, useI18n, useNavigate, useUsage, useForm, useDb, useUserSettings, } from "./hooks";
|
|
11
|
+
// Slot contracts
|
|
12
|
+
export { Slot } from "./types/slots";
|
|
13
|
+
// Re-export the shared UI primitives so extensions install a single dependency.
|
|
14
|
+
export * from "@360crewing/ui";
|
package/dist/mf.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Module Federation contract (host, catalog backend, extension build config share it).
|
|
2
|
+
* An extension is an MF remote whose container `name` MUST equal `mfScope(appId)` and
|
|
3
|
+
* exposes `defineExtension()` under `EXPOSED_MODULE`; host calls
|
|
4
|
+
* `loadRemote(`${mfScope(appId)}/${EXPOSED_MODULE}`)`.
|
|
5
|
+
*/
|
|
6
|
+
/** The single MF-exposed module returning the `defineExtension()` result. */
|
|
7
|
+
export declare const EXPOSED_MODULE = "./extension";
|
|
8
|
+
/** Stable MF container name from reverse-DNS app id, e.g. `com.acme.crew-analytics` → `ext_com_acme_crew_analytics`. */
|
|
9
|
+
export declare function mfScope(appId: string): string;
|
package/dist/mf.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Module Federation contract (host, catalog backend, extension build config share it).
|
|
2
|
+
* An extension is an MF remote whose container `name` MUST equal `mfScope(appId)` and
|
|
3
|
+
* exposes `defineExtension()` under `EXPOSED_MODULE`; host calls
|
|
4
|
+
* `loadRemote(`${mfScope(appId)}/${EXPOSED_MODULE}`)`.
|
|
5
|
+
*/
|
|
6
|
+
/** The single MF-exposed module returning the `defineExtension()` result. */
|
|
7
|
+
export const EXPOSED_MODULE = "./extension";
|
|
8
|
+
/** Stable MF container name from reverse-DNS app id, e.g. `com.acme.crew-analytics` → `ext_com_acme_crew_analytics`. */
|
|
9
|
+
export function mfScope(appId) {
|
|
10
|
+
return "ext_" + appId.replace(/[^a-zA-Z0-9]/g, "_");
|
|
11
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Builds the typed PlatformClient over a host-injected transport. The host
|
|
2
|
+
* wires the transport to its app-scoped axios; extensions only ever see the
|
|
3
|
+
* typed resource methods. */
|
|
4
|
+
import type { PlatformClient, PlatformTransport } from "./types/platform";
|
|
5
|
+
export declare function createPlatformClient(transport: PlatformTransport): PlatformClient;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function readable(transport, base) {
|
|
2
|
+
return {
|
|
3
|
+
list: () => transport({ method: "GET", path: base }),
|
|
4
|
+
search: (params) => transport({ method: "POST", path: `${base}/search`, body: params ?? {} }),
|
|
5
|
+
get: (uuid) => transport({ method: "GET", path: `${base}/${encodeURIComponent(uuid)}` }),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function searchable(transport, base) {
|
|
9
|
+
return {
|
|
10
|
+
search: (params) => transport({ method: "POST", path: `${base}/search`, body: params ?? {} }),
|
|
11
|
+
get: (uuid) => transport({ method: "GET", path: `${base}/${encodeURIComponent(uuid)}` }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createPlatformClient(transport) {
|
|
15
|
+
return {
|
|
16
|
+
seafarers: readable(transport, "/seafarers"),
|
|
17
|
+
vessels: searchable(transport, "/vessels"),
|
|
18
|
+
contracts: readable(transport, "/contracts"),
|
|
19
|
+
request: (req) => transport(req),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! LICENSE: index.cc3a40625d.js.LICENSE.txt */
|
|
2
|
+
(()=>{"use strict";var t={62(t,e){var o=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),n=(Symbol.for("react.fragment"),Symbol.for("react.strict_mode"),Symbol.for("react.profiler"),Symbol.for("react.consumer"),Symbol.for("react.context"),Symbol.for("react.forward_ref"),Symbol.for("react.suspense"),Symbol.for("react.memo"),Symbol.for("react.lazy")),a=Symbol.iterator,c={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},p=Object.assign,s={};function i(t,e,o){this.props=t,this.context=e,this.refs=s,this.updater=o||c}function f(){}function u(t,e,o){this.props=t,this.context=e,this.refs=s,this.updater=o||c}i.prototype.isReactComponent={},i.prototype.setState=function(t,e){if("object"!=typeof t&&"function"!=typeof t&&null!=t)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,t,e,"setState")},i.prototype.forceUpdate=function(t){this.updater.enqueueForceUpdate(this,t,"forceUpdate")},f.prototype=i.prototype;var y=u.prototype=new f;y.constructor=u,p(y,i.prototype),y.isPureReactComponent=!0;Object.prototype.hasOwnProperty;"function"==typeof reportError&&reportError},41(t,e,o){o(62)}},e={};!function o(r){var n=e[r];if(void 0!==n)return n.exports;var a=e[r]={exports:{}};return t[r](a,a.exports,o),a.exports}(41)})();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { UsageApi } from "./sdk";
|
|
3
|
+
import type { PlatformClient } from "./platform";
|
|
4
|
+
export interface ExtensionUser {
|
|
5
|
+
uuid: string;
|
|
6
|
+
full_name: string;
|
|
7
|
+
role: string;
|
|
8
|
+
locale: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ExtensionTenant {
|
|
11
|
+
subdomain: string;
|
|
12
|
+
region: string;
|
|
13
|
+
locale: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ExtensionTheme {
|
|
16
|
+
colors: Record<string, string>;
|
|
17
|
+
fonts: Record<string, string>;
|
|
18
|
+
spacing: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export interface ExtensionNotify {
|
|
21
|
+
success(message: string): void;
|
|
22
|
+
error(message: string): void;
|
|
23
|
+
info(message: string): void;
|
|
24
|
+
warning(message: string): void;
|
|
25
|
+
}
|
|
26
|
+
export interface ModalOptions {
|
|
27
|
+
/** Extra class names forwarded to the host modal content. */
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ExtensionModalApi {
|
|
31
|
+
/** Render an extension-owned node/component inside the host modal shell. */
|
|
32
|
+
open(content: ReactNode, options?: ModalOptions): void;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
export interface ConfirmOptions {
|
|
36
|
+
title: string;
|
|
37
|
+
message?: string;
|
|
38
|
+
confirmTitle?: string;
|
|
39
|
+
cancelTitle?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ExtensionI18n {
|
|
42
|
+
t(key: string, options?: Record<string, unknown>): string;
|
|
43
|
+
i18n: {
|
|
44
|
+
language: string;
|
|
45
|
+
changeLanguage(locale: string): Promise<unknown>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface ExtensionField<T> {
|
|
49
|
+
name: keyof T & string;
|
|
50
|
+
value: unknown;
|
|
51
|
+
error?: string | null;
|
|
52
|
+
onChange(value: unknown): void;
|
|
53
|
+
}
|
|
54
|
+
export interface ExtensionForm<T extends Record<string, unknown>> {
|
|
55
|
+
values: T;
|
|
56
|
+
errors: Partial<Record<keyof T, string>>;
|
|
57
|
+
isDirty: boolean;
|
|
58
|
+
isSubmitting: boolean;
|
|
59
|
+
setValue<K extends keyof T>(key: K, value: T[K]): void;
|
|
60
|
+
/** Binding object spreadable onto a `@360crewing/ui` input. */
|
|
61
|
+
field<K extends keyof T & string>(key: K): ExtensionField<T>;
|
|
62
|
+
submit(): Promise<void>;
|
|
63
|
+
reset(): void;
|
|
64
|
+
}
|
|
65
|
+
export interface ExtensionFormFactory {
|
|
66
|
+
create<T extends Record<string, unknown>>(config: {
|
|
67
|
+
initial: T;
|
|
68
|
+
onSubmit: (values: T) => Promise<void> | void;
|
|
69
|
+
validate?: (values: T) => Partial<Record<keyof T, string>>;
|
|
70
|
+
}): ExtensionForm<T>;
|
|
71
|
+
}
|
|
72
|
+
export interface ExtensionNavigate {
|
|
73
|
+
(to: string, options?: {
|
|
74
|
+
replace?: boolean;
|
|
75
|
+
}): void;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Per-user settings store: saved filters, UI prefs. Small JSON document,
|
|
79
|
+
* last-write-wins, scope `(tenant, installation, user)`. Backed server-side
|
|
80
|
+
* by a namespaced row (no SQL). Tenant-wide config is intentionally deferred.
|
|
81
|
+
*/
|
|
82
|
+
export interface ExtensionUserSettings<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
83
|
+
/** The whole settings document for the current user (empty if unset). */
|
|
84
|
+
get(): Promise<Partial<T>>;
|
|
85
|
+
/** Shallow-merge `patch` into the document (last-write-wins). */
|
|
86
|
+
set(patch: Partial<T>): Promise<void>;
|
|
87
|
+
/** Remove the current user's settings document. */
|
|
88
|
+
clear(): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
/** SQLite-bindable scalar. Store JSON as TEXT (`JSON.stringify` / `json_*`). */
|
|
91
|
+
export type SqlParam = string | number | boolean | null;
|
|
92
|
+
export type DbRow = Record<string, unknown>;
|
|
93
|
+
export interface DbExecResult {
|
|
94
|
+
rowsAffected: number;
|
|
95
|
+
/** `last_insert_rowid()` for INSERT, when applicable. */
|
|
96
|
+
lastInsertRowid?: number;
|
|
97
|
+
}
|
|
98
|
+
export interface ExtensionDbQuerier {
|
|
99
|
+
/** Run a SELECT, returns rows. */
|
|
100
|
+
query<R = DbRow>(sql: string, params?: SqlParam[]): Promise<R[]>;
|
|
101
|
+
/** Run INSERT / UPDATE / DELETE / DDL. */
|
|
102
|
+
exec(sql: string, params?: SqlParam[]): Promise<DbExecResult>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The extension's own database — one isolated SQLite file per
|
|
106
|
+
* `(tenant, installation)`. Full SQL; the extension owns its schema
|
|
107
|
+
* (`CREATE/ALTER`). Cross-tenant access is impossible (the connection is
|
|
108
|
+
* physically scoped to this extension's file). Host enforces an authorizer
|
|
109
|
+
* denylist, per-query timeout, result/size caps and serialized writes.
|
|
110
|
+
*/
|
|
111
|
+
export interface ExtensionDb extends ExtensionDbQuerier {
|
|
112
|
+
/** Run `fn` atomically. Atomicity is enforced server-side (БП-13). */
|
|
113
|
+
tx<T>(fn: (t: ExtensionDbQuerier) => Promise<T>): Promise<T>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Host->extension bridge injected via ExtensionHostContext. SDK only defines
|
|
117
|
+
* the contract (never imports host code); host supplies a per-extension impl.
|
|
118
|
+
* `form` is a standalone `ExtensionForm`, NOT the host entity FormsManager
|
|
119
|
+
* (that is coupled to host backend entity URLs, not exposed to extensions).
|
|
120
|
+
*/
|
|
121
|
+
export interface ExtensionHost {
|
|
122
|
+
appId: string;
|
|
123
|
+
installationUuid: string;
|
|
124
|
+
/** Typed, scope-gated access to platform data (seafarers/vessels/contracts).
|
|
125
|
+
* Replaces the old raw axios — extensions call functions, not endpoints. */
|
|
126
|
+
platform: PlatformClient;
|
|
127
|
+
user: ExtensionUser;
|
|
128
|
+
tenant: ExtensionTenant;
|
|
129
|
+
theme: ExtensionTheme;
|
|
130
|
+
notify: ExtensionNotify;
|
|
131
|
+
modal: ExtensionModalApi;
|
|
132
|
+
confirm(options: ConfirmOptions): Promise<boolean>;
|
|
133
|
+
form: ExtensionFormFactory;
|
|
134
|
+
i18n: ExtensionI18n;
|
|
135
|
+
navigate: ExtensionNavigate;
|
|
136
|
+
usage: UsageApi;
|
|
137
|
+
/** BRD БП-13: extension's own SQLite DB (per tenant+installation). */
|
|
138
|
+
db: ExtensionDb;
|
|
139
|
+
/** BRD БП-13: per-user settings (saved filters, UI prefs). */
|
|
140
|
+
userSettings: ExtensionUserSettings;
|
|
141
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locales the host ships today. Extensions may declare additional locale codes
|
|
3
|
+
* (the string fallback keeps autocomplete for the known ones while allowing more).
|
|
4
|
+
*/
|
|
5
|
+
export type Locale = "en" | "ru" | (string & {});
|
|
6
|
+
export type LocaleMessages = Record<string, string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SlotName, SlotRegistrationMap } from "./slots";
|
|
2
|
+
import type { Locale, LocaleMessages } from "./locale";
|
|
3
|
+
/** Per-slot registration the extension provides (menu obj or widget component). */
|
|
4
|
+
export type ExtensionSlots = Partial<SlotRegistrationMap>;
|
|
5
|
+
export interface DefineExtensionConfig {
|
|
6
|
+
/** Reverse-DNS unique id, e.g. "com.acme.crew-analytics". */
|
|
7
|
+
id: string;
|
|
8
|
+
/**
|
|
9
|
+
* i18n bundles per locale, registered by the host under namespace `app.<id>`.
|
|
10
|
+
* Partial so an extension can ship any subset; `defineExtension` enforces ≥1.
|
|
11
|
+
*/
|
|
12
|
+
locales: Partial<Record<Locale, LocaleMessages>>;
|
|
13
|
+
/** Slot name -> menu registration / widget component the host renders. */
|
|
14
|
+
slots: ExtensionSlots;
|
|
15
|
+
onActivate?: () => void;
|
|
16
|
+
onDeactivate?: () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Serializable menu descriptor — lets the host build menus without executing
|
|
19
|
+
* slot code. `icon` is a React node (runtime-only) so it is NOT carried here. */
|
|
20
|
+
export interface ManifestMenuEntry {
|
|
21
|
+
slot: SlotName;
|
|
22
|
+
title: string;
|
|
23
|
+
}
|
|
24
|
+
/** Derived, serializable description the host reads without executing slots. */
|
|
25
|
+
export interface ExtensionManifest {
|
|
26
|
+
id: string;
|
|
27
|
+
slots: SlotName[];
|
|
28
|
+
/** Menu items declared by the extension (slot + title), for menu slots. */
|
|
29
|
+
menus: ManifestMenuEntry[];
|
|
30
|
+
locales: Locale[];
|
|
31
|
+
defaultLocale: Locale;
|
|
32
|
+
}
|
|
33
|
+
export interface ExtensionDefinition {
|
|
34
|
+
manifest: ExtensionManifest;
|
|
35
|
+
locales: Partial<Record<Locale, LocaleMessages>>;
|
|
36
|
+
slots: ExtensionSlots;
|
|
37
|
+
onActivate?: () => void;
|
|
38
|
+
onDeactivate?: () => void;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** Typed platform-data client; extensions call functions, never raw HTTP.
|
|
2
|
+
* Resources map to scope-gated tenant endpoints (`@require_app_scope`).
|
|
3
|
+
* Types mirror marshmallow schemas: list shapes exact, detail shapes cover
|
|
4
|
+
* common fields and stay open (`[k: string]: unknown`) for rare nested parts.
|
|
5
|
+
*/
|
|
6
|
+
export interface Ref {
|
|
7
|
+
id: number;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
export interface IdName {
|
|
11
|
+
id: number;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CountryRef {
|
|
15
|
+
id: number;
|
|
16
|
+
name: string;
|
|
17
|
+
code: string;
|
|
18
|
+
}
|
|
19
|
+
export interface OrgRef {
|
|
20
|
+
uuid: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
export interface Address {
|
|
24
|
+
uuid?: string;
|
|
25
|
+
line1?: string;
|
|
26
|
+
line2?: string;
|
|
27
|
+
zip?: string;
|
|
28
|
+
city?: {
|
|
29
|
+
id: number | null;
|
|
30
|
+
name: string | null;
|
|
31
|
+
};
|
|
32
|
+
region?: IdName;
|
|
33
|
+
country?: CountryRef;
|
|
34
|
+
[k: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface Pagination {
|
|
37
|
+
active_page: number;
|
|
38
|
+
per_page: number;
|
|
39
|
+
total: number;
|
|
40
|
+
}
|
|
41
|
+
export interface Paginated<T> {
|
|
42
|
+
items: T[];
|
|
43
|
+
pagination: Pagination;
|
|
44
|
+
}
|
|
45
|
+
export interface SearchParams {
|
|
46
|
+
filters?: Record<string, unknown>;
|
|
47
|
+
pagination?: {
|
|
48
|
+
page?: number;
|
|
49
|
+
per_page?: number;
|
|
50
|
+
};
|
|
51
|
+
sort?: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
export interface SeafarerListItem {
|
|
55
|
+
uuid: string;
|
|
56
|
+
name: string;
|
|
57
|
+
surname: string;
|
|
58
|
+
date_of_birth: string | null;
|
|
59
|
+
nationality_country: CountryRef | null;
|
|
60
|
+
rank: Ref | null;
|
|
61
|
+
address: Address | null;
|
|
62
|
+
additional_ranks: Ref[];
|
|
63
|
+
}
|
|
64
|
+
/** Detail (`GET /seafarers/<uuid>`). Top-level structure typed; nested
|
|
65
|
+
* collections kept open — extend as your extension needs them. */
|
|
66
|
+
export interface SeafarerDetail {
|
|
67
|
+
uuid: string;
|
|
68
|
+
created_at: string;
|
|
69
|
+
updated_at: string;
|
|
70
|
+
main: {
|
|
71
|
+
name: string;
|
|
72
|
+
surname: string;
|
|
73
|
+
middle_name: string | null;
|
|
74
|
+
photo: string | null;
|
|
75
|
+
rank: Ref | null;
|
|
76
|
+
additional_ranks: Ref[] | null;
|
|
77
|
+
date_of_birth: string | null;
|
|
78
|
+
gender: Ref | null;
|
|
79
|
+
nationality_country: CountryRef | null;
|
|
80
|
+
emails: Array<Record<string, unknown>>;
|
|
81
|
+
phone_numbers: Array<Record<string, unknown>>;
|
|
82
|
+
languages: Ref[];
|
|
83
|
+
[k: string]: unknown;
|
|
84
|
+
};
|
|
85
|
+
addresses: Address[] | null;
|
|
86
|
+
certificates: Array<Record<string, unknown>> | null;
|
|
87
|
+
education: Array<Record<string, unknown>> | null;
|
|
88
|
+
contracts: ContractListItem[];
|
|
89
|
+
bank_accounts: Array<Record<string, unknown>>;
|
|
90
|
+
[k: string]: unknown;
|
|
91
|
+
}
|
|
92
|
+
export interface VesselListItem {
|
|
93
|
+
uuid: string;
|
|
94
|
+
imo_no: string | number;
|
|
95
|
+
name: string;
|
|
96
|
+
type: Ref | null;
|
|
97
|
+
flag_country: CountryRef | null;
|
|
98
|
+
}
|
|
99
|
+
export interface VesselDetail {
|
|
100
|
+
uuid: string;
|
|
101
|
+
updated_at: string;
|
|
102
|
+
details: {
|
|
103
|
+
name: string;
|
|
104
|
+
imo_no: string | number;
|
|
105
|
+
photo: string | null;
|
|
106
|
+
call_sign: string;
|
|
107
|
+
type: Ref | null;
|
|
108
|
+
flag_country: CountryRef | null;
|
|
109
|
+
client: OrgRef | null;
|
|
110
|
+
ship_owner: OrgRef | null;
|
|
111
|
+
employer: OrgRef | null;
|
|
112
|
+
[k: string]: unknown;
|
|
113
|
+
};
|
|
114
|
+
vessel_certificates: Array<Record<string, unknown>>;
|
|
115
|
+
[k: string]: unknown;
|
|
116
|
+
}
|
|
117
|
+
export interface ContractListItem {
|
|
118
|
+
uuid: string;
|
|
119
|
+
is_deleted: boolean;
|
|
120
|
+
contract_no: number;
|
|
121
|
+
contract_type: Ref | null;
|
|
122
|
+
agreement_type: Ref | null;
|
|
123
|
+
signed_on: string;
|
|
124
|
+
signed_off: string | null;
|
|
125
|
+
rank: Ref | null;
|
|
126
|
+
seafarer: {
|
|
127
|
+
uuid: string;
|
|
128
|
+
name: string;
|
|
129
|
+
surname: string;
|
|
130
|
+
} | null;
|
|
131
|
+
employer: OrgRef | null;
|
|
132
|
+
joinings: Array<Record<string, unknown>>;
|
|
133
|
+
}
|
|
134
|
+
export interface ContractDetail {
|
|
135
|
+
uuid: string;
|
|
136
|
+
main: {
|
|
137
|
+
contract_no: number;
|
|
138
|
+
contract_type: Ref | null;
|
|
139
|
+
agreement_type: Ref | null;
|
|
140
|
+
employer: OrgRef | null;
|
|
141
|
+
client: OrgRef | null;
|
|
142
|
+
seafarer: {
|
|
143
|
+
uuid: string;
|
|
144
|
+
name: string;
|
|
145
|
+
surname: string;
|
|
146
|
+
address: Address | null;
|
|
147
|
+
} | null;
|
|
148
|
+
rank: Ref | null;
|
|
149
|
+
signed_on: string | null;
|
|
150
|
+
signed_off: string | null;
|
|
151
|
+
[k: string]: unknown;
|
|
152
|
+
};
|
|
153
|
+
joinings: Array<Record<string, unknown>>;
|
|
154
|
+
billing: Array<Record<string, unknown>>;
|
|
155
|
+
[k: string]: unknown;
|
|
156
|
+
}
|
|
157
|
+
export type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
158
|
+
export interface PlatformRequest {
|
|
159
|
+
method: HttpMethod;
|
|
160
|
+
path: string;
|
|
161
|
+
params?: Record<string, unknown>;
|
|
162
|
+
body?: unknown;
|
|
163
|
+
}
|
|
164
|
+
/** Host-injected transport. The SDK builds requests; the host adds the
|
|
165
|
+
* app-scoped JWT and performs the HTTP call. */
|
|
166
|
+
export type PlatformTransport = <T = unknown>(req: PlatformRequest) => Promise<T>;
|
|
167
|
+
/** Resource with a quick recent `list()`, paginated `search()`, and `get()`. */
|
|
168
|
+
export interface ReadableResource<List, Detail> {
|
|
169
|
+
list(): Promise<List[]>;
|
|
170
|
+
search(params?: SearchParams): Promise<Paginated<List>>;
|
|
171
|
+
get(uuid: string): Promise<Detail>;
|
|
172
|
+
}
|
|
173
|
+
/** Vessels have no plain list endpoint — search or get only. */
|
|
174
|
+
export interface SearchableResource<List, Detail> {
|
|
175
|
+
search(params?: SearchParams): Promise<Paginated<List>>;
|
|
176
|
+
get(uuid: string): Promise<Detail>;
|
|
177
|
+
}
|
|
178
|
+
export interface PlatformClient {
|
|
179
|
+
seafarers: ReadableResource<SeafarerListItem, SeafarerDetail>;
|
|
180
|
+
vessels: SearchableResource<VesselListItem, VesselDetail>;
|
|
181
|
+
contracts: ReadableResource<ContractListItem, ContractDetail>;
|
|
182
|
+
/** Low-level escape hatch for endpoints not yet wrapped as typed methods.
|
|
183
|
+
* Prefer the typed resources above. */
|
|
184
|
+
request<T = unknown>(req: PlatformRequest): Promise<T>;
|
|
185
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Typed platform-data client; extensions call functions, never raw HTTP.
|
|
2
|
+
* Resources map to scope-gated tenant endpoints (`@require_app_scope`).
|
|
3
|
+
* Types mirror marshmallow schemas: list shapes exact, detail shapes cover
|
|
4
|
+
* common fields and stay open (`[k: string]: unknown`) for rare nested parts.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ExtensionDb, ExtensionUserSettings } from "./host";
|
|
2
|
+
import type { PlatformClient } from "./platform";
|
|
3
|
+
/**
|
|
4
|
+
* Imperative SDK surface on `props.sdk` for non-hook call sites.
|
|
5
|
+
* Hooks (`usePlatform()` etc.) are the preferred API.
|
|
6
|
+
*/
|
|
7
|
+
export interface ExtensionSdk {
|
|
8
|
+
/** Reverse-DNS app id, e.g. "com.acme.crew-analytics". */
|
|
9
|
+
appId: string;
|
|
10
|
+
/** Typed, scope-gated platform data (seafarers/vessels/contracts). */
|
|
11
|
+
platform: PlatformClient;
|
|
12
|
+
usage: UsageApi;
|
|
13
|
+
/** BRD BP-13: extension's own SQLite DB. */
|
|
14
|
+
db: ExtensionDb;
|
|
15
|
+
/** BRD BP-13: per-user settings store. */
|
|
16
|
+
userSettings: ExtensionUserSettings;
|
|
17
|
+
}
|
|
18
|
+
export interface UsageApi {
|
|
19
|
+
/**
|
|
20
|
+
* Report a metered usage event (quantity defaults to 1).
|
|
21
|
+
* Host POSTs `/api/v1/extensions/usage` with the app-scoped JWT.
|
|
22
|
+
*/
|
|
23
|
+
record(metric: string, quantity?: number): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import type { ExtensionSdk } from "./sdk";
|
|
3
|
+
/**
|
|
4
|
+
* Render locations for extensions (e.g. `Slot.Seafarer.Menu`). Leaf strings are
|
|
5
|
+
* the stable wire contract (also accepted raw via `SlotName`). Menu slots take a
|
|
6
|
+
* `MenuRegistration`; `Dashboard.Widget` takes a bare grid component.
|
|
7
|
+
*/
|
|
8
|
+
export declare const Slot: {
|
|
9
|
+
readonly Dashboard: {
|
|
10
|
+
readonly Widget: "dashboard.widget";
|
|
11
|
+
};
|
|
12
|
+
readonly Seafarer: {
|
|
13
|
+
readonly Menu: "seafarer.menu";
|
|
14
|
+
};
|
|
15
|
+
readonly Vessel: {
|
|
16
|
+
readonly Menu: "vessel.menu";
|
|
17
|
+
};
|
|
18
|
+
readonly Contract: {
|
|
19
|
+
readonly Menu: "contract.menu";
|
|
20
|
+
};
|
|
21
|
+
readonly Nav: {
|
|
22
|
+
readonly Admin: {
|
|
23
|
+
readonly Menu: "nav.admin.menu";
|
|
24
|
+
};
|
|
25
|
+
readonly Data: {
|
|
26
|
+
readonly Menu: "nav.data.menu";
|
|
27
|
+
};
|
|
28
|
+
readonly Finance: {
|
|
29
|
+
readonly Menu: "nav.finance.menu";
|
|
30
|
+
};
|
|
31
|
+
readonly Fleet: {
|
|
32
|
+
readonly Menu: "nav.fleet.menu";
|
|
33
|
+
};
|
|
34
|
+
readonly Reports: {
|
|
35
|
+
readonly Menu: "nav.reports.menu";
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/** Recursively extract every leaf string value of the nested `Slot` tree. */
|
|
40
|
+
type Leaves<T> = T extends string ? T : {
|
|
41
|
+
[K in keyof T]: Leaves<T[K]>;
|
|
42
|
+
}[keyof T];
|
|
43
|
+
export type SlotName = Leaves<typeof Slot>;
|
|
44
|
+
interface EntityRef {
|
|
45
|
+
uuid: string;
|
|
46
|
+
}
|
|
47
|
+
/** Common to every slot: the imperative SDK surface for this extension. */
|
|
48
|
+
export interface SlotContextBase {
|
|
49
|
+
sdk: ExtensionSdk;
|
|
50
|
+
}
|
|
51
|
+
export interface DashboardWidgetContext extends SlotContextBase {
|
|
52
|
+
widget: {
|
|
53
|
+
id: string;
|
|
54
|
+
/** Values produced by the widget's `settings_schema` form. */
|
|
55
|
+
settings: Record<string, unknown>;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export interface SeafarerMenuContext extends SlotContextBase {
|
|
59
|
+
seafarer: EntityRef;
|
|
60
|
+
}
|
|
61
|
+
export interface VesselMenuContext extends SlotContextBase {
|
|
62
|
+
vessel: EntityRef;
|
|
63
|
+
}
|
|
64
|
+
export interface ContractMenuContext extends SlotContextBase {
|
|
65
|
+
contract: EntityRef;
|
|
66
|
+
}
|
|
67
|
+
/** Global-nav menu items have no entity — only the SDK surface. */
|
|
68
|
+
export type NavMenuContext = SlotContextBase;
|
|
69
|
+
/** Maps each slot name to the props the host passes to the slot component. */
|
|
70
|
+
export interface SlotContextMap {
|
|
71
|
+
"dashboard.widget": DashboardWidgetContext;
|
|
72
|
+
"seafarer.menu": SeafarerMenuContext;
|
|
73
|
+
"vessel.menu": VesselMenuContext;
|
|
74
|
+
"contract.menu": ContractMenuContext;
|
|
75
|
+
"nav.admin.menu": NavMenuContext;
|
|
76
|
+
"nav.data.menu": NavMenuContext;
|
|
77
|
+
"nav.finance.menu": NavMenuContext;
|
|
78
|
+
"nav.fleet.menu": NavMenuContext;
|
|
79
|
+
"nav.reports.menu": NavMenuContext;
|
|
80
|
+
}
|
|
81
|
+
export type SlotComponent<S extends SlotName = SlotName> = ComponentType<SlotContextMap[S]>;
|
|
82
|
+
/**
|
|
83
|
+
* One menu item in a menu slot. Host derives the route from the extension id;
|
|
84
|
+
* multiple extensions in a slot -> multiple items. `title` resolves as an i18n
|
|
85
|
+
* key in `app.<id>`, falling back to the raw string (literal label also valid).
|
|
86
|
+
*/
|
|
87
|
+
export interface MenuRegistration<S extends SlotName = SlotName> {
|
|
88
|
+
/** Literal label or i18n key in this extension's `app.<id>` namespace. */
|
|
89
|
+
title: string;
|
|
90
|
+
icon?: ReactNode;
|
|
91
|
+
/** The page rendered when this menu item is opened. */
|
|
92
|
+
page: SlotComponent<S>;
|
|
93
|
+
}
|
|
94
|
+
/** Widget grid sizes — subset of host `WIDGET_SIZES`. */
|
|
95
|
+
export type WidgetSizeName = "1x1" | "2x1" | "2x2" | "3x2" | "4x2" | "6x2" | "4x3" | "6x3" | "12x3" | "4x4" | "6x4" | "12x4" | "4x5" | "6x5" | "12x5";
|
|
96
|
+
/**
|
|
97
|
+
* Extended `dashboard.widget` registration with catalog metadata (runtime also
|
|
98
|
+
* accepts a bare `SlotComponent`). `title`/`description` follow the same i18n
|
|
99
|
+
* convention as `MenuRegistration.title`.
|
|
100
|
+
*/
|
|
101
|
+
export interface WidgetRegistration<S extends SlotName = "dashboard.widget"> {
|
|
102
|
+
/** Component rendered in `DashboardWidgetHost` — required. */
|
|
103
|
+
page: SlotComponent<S>;
|
|
104
|
+
/** Grouping in the Add-widget catalog. Default "All". */
|
|
105
|
+
category?: string;
|
|
106
|
+
/** "Tab" badge on the card. Default false. */
|
|
107
|
+
is_tab?: boolean;
|
|
108
|
+
/** Minimum grid size. Default "1x1". */
|
|
109
|
+
min_size?: WidgetSizeName;
|
|
110
|
+
/** Maximum grid size. Default "6x2". */
|
|
111
|
+
max_size?: WidgetSizeName;
|
|
112
|
+
/** Widget title in the catalog card. Default appId. */
|
|
113
|
+
title?: string;
|
|
114
|
+
/** Subtitle on the card. Default "" (hidden). */
|
|
115
|
+
description?: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Per-slot registration shape. Menu slots take a `MenuRegistration`;
|
|
119
|
+
* `dashboard.widget` takes a bare component or a `WidgetRegistration`.
|
|
120
|
+
*/
|
|
121
|
+
export interface SlotRegistrationMap {
|
|
122
|
+
"dashboard.widget": SlotComponent<"dashboard.widget"> | WidgetRegistration<"dashboard.widget">;
|
|
123
|
+
"seafarer.menu": MenuRegistration<"seafarer.menu">;
|
|
124
|
+
"vessel.menu": MenuRegistration<"vessel.menu">;
|
|
125
|
+
"contract.menu": MenuRegistration<"contract.menu">;
|
|
126
|
+
"nav.admin.menu": MenuRegistration<"nav.admin.menu">;
|
|
127
|
+
"nav.data.menu": MenuRegistration<"nav.data.menu">;
|
|
128
|
+
"nav.finance.menu": MenuRegistration<"nav.finance.menu">;
|
|
129
|
+
"nav.fleet.menu": MenuRegistration<"nav.fleet.menu">;
|
|
130
|
+
"nav.reports.menu": MenuRegistration<"nav.reports.menu">;
|
|
131
|
+
}
|
|
132
|
+
/** A registration for any slot — discriminated by the slot key. */
|
|
133
|
+
export type SlotRegistration<S extends SlotName = SlotName> = SlotRegistrationMap[S];
|
|
134
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render locations for extensions (e.g. `Slot.Seafarer.Menu`). Leaf strings are
|
|
3
|
+
* the stable wire contract (also accepted raw via `SlotName`). Menu slots take a
|
|
4
|
+
* `MenuRegistration`; `Dashboard.Widget` takes a bare grid component.
|
|
5
|
+
*/
|
|
6
|
+
export const Slot = {
|
|
7
|
+
Dashboard: { Widget: "dashboard.widget" },
|
|
8
|
+
Seafarer: { Menu: "seafarer.menu" },
|
|
9
|
+
Vessel: { Menu: "vessel.menu" },
|
|
10
|
+
Contract: { Menu: "contract.menu" },
|
|
11
|
+
Nav: {
|
|
12
|
+
Admin: { Menu: "nav.admin.menu" },
|
|
13
|
+
Data: { Menu: "nav.data.menu" },
|
|
14
|
+
Finance: { Menu: "nav.finance.menu" },
|
|
15
|
+
Fleet: { Menu: "nav.fleet.menu" },
|
|
16
|
+
Reports: { Menu: "nav.reports.menu" },
|
|
17
|
+
},
|
|
18
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@360crewing/marketplace-sdk",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Public SDK contract for 360crewing marketplace extensions: defineExtension, hooks, slot types.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"homepage": "https://dev.360crewing.com",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
32
|
+
"axios": "^1.9.0",
|
|
33
|
+
"react-i18next": ">=14",
|
|
34
|
+
"@360crewing/ui": "^0.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|