@stapel/workspaces-react 0.1.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/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/MODULE.md +54 -0
- package/README.md +73 -0
- package/dist/api/extensions.d.ts +8 -0
- package/dist/api/extensions.d.ts.map +1 -0
- package/dist/api/extensions.js +8 -0
- package/dist/api/extensions.js.map +1 -0
- package/dist/api/types.d.ts +51 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/workspacesApi.d.ts +43 -0
- package/dist/api/workspacesApi.d.ts.map +1 -0
- package/dist/api/workspacesApi.js +31 -0
- package/dist/api/workspacesApi.js.map +1 -0
- package/dist/flows/errors.d.ts +12 -0
- package/dist/flows/errors.d.ts.map +1 -0
- package/dist/flows/errors.js +13 -0
- package/dist/flows/errors.js.map +1 -0
- package/dist/flows/generated/flows.gen.d.ts +19 -0
- package/dist/flows/generated/flows.gen.d.ts.map +1 -0
- package/dist/flows/generated/flows.gen.js +23 -0
- package/dist/flows/generated/flows.gen.js.map +1 -0
- package/dist/headless/AcceptInvitation.d.ts +39 -0
- package/dist/headless/AcceptInvitation.d.ts.map +1 -0
- package/dist/headless/AcceptInvitation.js +27 -0
- package/dist/headless/AcceptInvitation.js.map +1 -0
- package/dist/headless/Members.d.ts +47 -0
- package/dist/headless/Members.d.ts.map +1 -0
- package/dist/headless/Members.js +50 -0
- package/dist/headless/Members.js.map +1 -0
- package/dist/headless/WorkspaceList.d.ts +38 -0
- package/dist/headless/WorkspaceList.d.ts.map +1 -0
- package/dist/headless/WorkspaceList.js +33 -0
- package/dist/headless/WorkspaceList.js.map +1 -0
- package/dist/headless/WorkspacesProvider.d.ts +18 -0
- package/dist/headless/WorkspacesProvider.d.ts.map +1 -0
- package/dist/headless/WorkspacesProvider.js +17 -0
- package/dist/headless/WorkspacesProvider.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +10 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +20 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +347 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +177 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +9 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +61 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +55 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +76 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +28 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +49 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/model/context.d.ts +14 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +21 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/mutations.d.ts +55 -0
- package/dist/model/mutations.d.ts.map +1 -0
- package/dist/model/mutations.js +138 -0
- package/dist/model/mutations.js.map +1 -0
- package/dist/model/queries.d.ts +21 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +41 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +7 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +15 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/runtime.d.ts +26 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +18 -0
- package/dist/model/runtime.js.map +1 -0
- package/llms.txt +95 -0
- package/manifest.json +705 -0
- package/package.json +85 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/extensions.ts +7 -0
- package/src/api/types.ts +58 -0
- package/src/api/workspacesApi.ts +139 -0
- package/src/flows/errors.ts +16 -0
- package/src/flows/generated/flows.gen.ts +38 -0
- package/src/flows/generated/flows.json +1 -0
- package/src/headless/AcceptInvitation.tsx +53 -0
- package/src/headless/Members.tsx +89 -0
- package/src/headless/WorkspaceList.tsx +58 -0
- package/src/headless/WorkspacesProvider.tsx +25 -0
- package/src/i18n/errorsMap.ts +31 -0
- package/src/i18n/generated/errors.gen.ts +208 -0
- package/src/i18n/generated/errors.json +394 -0
- package/src/i18n/generated/errors.ru.gen.ts +67 -0
- package/src/i18n/keys.ts +83 -0
- package/src/i18n/ru.ts +53 -0
- package/src/index.ts +113 -0
- package/src/model/context.tsx +29 -0
- package/src/model/mutations.ts +196 -0
- package/src/model/queries.ts +51 -0
- package/src/model/queryKeys.ts +20 -0
- package/src/model/runtime.ts +46 -0
- package/tsconfig.json +26 -0
package/src/i18n/keys.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { I18nDictionary, I18nEngine } from "@stapel/core";
|
|
2
|
+
import { workspacesErrorBundleEn } from "./generated/errors.gen.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* workspaces-react's own translation KEYS (frontend-standard §4.2): headless
|
|
6
|
+
* components never render literal strings — hosts resolve these via core's i18n
|
|
7
|
+
* engine (`useT`). Backend error codes flow through the SAME contour: a
|
|
8
|
+
* `StapelApiError.code` is already a key, so the default bundle below ships
|
|
9
|
+
* English fallbacks for both the backend error codes (generated) and the
|
|
10
|
+
* pair's own UI keys. Point core's `loadLocale` at stapel-translate to override
|
|
11
|
+
* per locale. Add UI keys under the `workspaces.` namespace as you build flows.
|
|
12
|
+
*/
|
|
13
|
+
export const WORKSPACES_I18N_KEYS = {
|
|
14
|
+
unknownError: "workspaces.error.unknown",
|
|
15
|
+
// Workspace list (WorkspaceList headless)
|
|
16
|
+
listLoading: "workspaces.list.loading",
|
|
17
|
+
listEmpty: "workspaces.list.empty",
|
|
18
|
+
listCreate: "workspaces.list.create",
|
|
19
|
+
listCreating: "workspaces.list.creating",
|
|
20
|
+
// Members (Members headless)
|
|
21
|
+
membersLoading: "workspaces.members.loading",
|
|
22
|
+
membersInvite: "workspaces.members.invite",
|
|
23
|
+
membersInviting: "workspaces.members.inviting",
|
|
24
|
+
membersUpdateRole: "workspaces.members.update_role",
|
|
25
|
+
membersRemove: "workspaces.members.remove",
|
|
26
|
+
// Accept invitation (AcceptInvitation headless)
|
|
27
|
+
acceptAccept: "workspaces.accept.accept",
|
|
28
|
+
acceptAccepting: "workspaces.accept.accepting",
|
|
29
|
+
acceptAccepted: "workspaces.accept.accepted",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type WorkspacesI18nKey =
|
|
33
|
+
(typeof WORKSPACES_I18N_KEYS)[keyof typeof WORKSPACES_I18N_KEYS];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* English fallback bundle for workspaces-react UI keys + backend error codes.
|
|
37
|
+
* The generated `workspacesErrorBundleEn` (from stapel-workspaces's error registry,
|
|
38
|
+
* `pnpm gen:errors`) is spread FIRST so every backend `error.*` key has a
|
|
39
|
+
* fallback — a `StapelApiError.code` never renders as a raw key. Hand-polished
|
|
40
|
+
* copy below then OVERRIDES the generated English for the keys users see most.
|
|
41
|
+
*/
|
|
42
|
+
export const workspacesI18nBundleEn: I18nDictionary = {
|
|
43
|
+
// Backend error codes — generated en fallbacks (coverage by construction).
|
|
44
|
+
...workspacesErrorBundleEn,
|
|
45
|
+
|
|
46
|
+
// workspaces-react UI
|
|
47
|
+
"workspaces.error.unknown": "Something went wrong. Please try again.",
|
|
48
|
+
"workspaces.list.loading": "Loading workspaces…",
|
|
49
|
+
"workspaces.list.empty": "No workspaces yet.",
|
|
50
|
+
"workspaces.list.create": "Create workspace",
|
|
51
|
+
"workspaces.list.creating": "Creating…",
|
|
52
|
+
"workspaces.members.loading": "Loading members…",
|
|
53
|
+
"workspaces.members.invite": "Invite",
|
|
54
|
+
"workspaces.members.inviting": "Inviting…",
|
|
55
|
+
"workspaces.members.update_role": "Change role",
|
|
56
|
+
"workspaces.members.remove": "Remove",
|
|
57
|
+
"workspaces.accept.accept": "Accept invitation",
|
|
58
|
+
"workspaces.accept.accepting": "Accepting…",
|
|
59
|
+
"workspaces.accept.accepted": "You've joined the workspace.",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register workspaces-react's key bundle into a core i18n engine (call once at
|
|
64
|
+
* startup). Registers under the given locale (default `"en"`); a later
|
|
65
|
+
* `loadLocale` from stapel-translate can layer localized overrides.
|
|
66
|
+
*
|
|
67
|
+
* MERGE-PRIORITY CONVENTION (pair checklist rule; i18n-shipping.md §3 — every
|
|
68
|
+
* `@stapel/*-react` pair follows it): registration order IS override
|
|
69
|
+
* priority, later wins per key. Within a locale, layers register bottom-up:
|
|
70
|
+
*
|
|
71
|
+
* 1. generated en floor (`WorkspacesErrorBundleEn` — coverage by construction),
|
|
72
|
+
* 2. the pair's polish / UI copy (this bundle spreads 1 then overrides),
|
|
73
|
+
* 3. the pair's locale bundle from the `./i18n/<locale>` subpath
|
|
74
|
+
* (e.g. `registerWorkspacesI18nRu` — registers the en floor UNDER the
|
|
75
|
+
* locale texts so a missing key degrades to English, never a raw key),
|
|
76
|
+
* 4. the HOST's own bundle — always registered LAST, so a host overrides any
|
|
77
|
+
* pair text without a fork.
|
|
78
|
+
*
|
|
79
|
+
* Dynamic overrides (stapel-translate `loadLocale`) layer on top at runtime.
|
|
80
|
+
*/
|
|
81
|
+
export function registerWorkspacesI18n(engine: I18nEngine, locale = "en"): void {
|
|
82
|
+
engine.registerBundle(locale, workspacesI18nBundleEn);
|
|
83
|
+
}
|
package/src/i18n/ru.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { I18nDictionary, I18nEngine } from "@stapel/core";
|
|
2
|
+
import { workspacesI18nBundleEn } from "./keys.js";
|
|
3
|
+
import { workspacesErrorBundleRu } from "./generated/errors.ru.gen.js";
|
|
4
|
+
|
|
5
|
+
export { workspacesErrorBundleRu } from "./generated/errors.ru.gen.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Russian bundle for workspaces-react — the pair's `ru` locale, shipped as
|
|
9
|
+
* the `@stapel/workspaces-react/i18n/ru` subpath (i18n-shipping.md §2) so
|
|
10
|
+
* the locale is opt-in: hosts that don't register it never carry these
|
|
11
|
+
* strings (the main entry does not import this module — gated by size-limit
|
|
12
|
+
* + the bundle-purity test).
|
|
13
|
+
*
|
|
14
|
+
* Composition mirrors {@link workspacesI18nBundleEn}: the GENERATED backend
|
|
15
|
+
* error texts (from stapel-workspaces's `translations/errors.ru.json`
|
|
16
|
+
* catalog, seeded from the curated stapel-translate corpus — `pnpm
|
|
17
|
+
* gen:errors`) are spread first for coverage by construction; the
|
|
18
|
+
* hand-written ru UI copy for the pair-owned {@link WORKSPACES_I18N_KEYS}
|
|
19
|
+
* follows. Override any key by registering a host bundle AFTER this one
|
|
20
|
+
* (merge-priority convention — see keys.ts).
|
|
21
|
+
*/
|
|
22
|
+
export const workspacesI18nBundleRu: I18nDictionary = {
|
|
23
|
+
// Backend error codes — generated ru texts (coverage by construction).
|
|
24
|
+
...workspacesErrorBundleRu,
|
|
25
|
+
|
|
26
|
+
// workspaces-react UI (hand-written ru mirror of the en copy in keys.ts)
|
|
27
|
+
"workspaces.error.unknown": "Что-то пошло не так. Попробуйте ещё раз.",
|
|
28
|
+
"workspaces.list.loading": "Загрузка рабочих пространств…",
|
|
29
|
+
"workspaces.list.empty": "Пока нет рабочих пространств.",
|
|
30
|
+
"workspaces.list.create": "Создать рабочее пространство",
|
|
31
|
+
"workspaces.list.creating": "Создание…",
|
|
32
|
+
"workspaces.members.loading": "Загрузка участников…",
|
|
33
|
+
"workspaces.members.invite": "Пригласить",
|
|
34
|
+
"workspaces.members.inviting": "Приглашение…",
|
|
35
|
+
"workspaces.members.update_role": "Изменить роль",
|
|
36
|
+
"workspaces.members.remove": "Удалить",
|
|
37
|
+
"workspaces.accept.accept": "Принять приглашение",
|
|
38
|
+
"workspaces.accept.accepting": "Принятие…",
|
|
39
|
+
"workspaces.accept.accepted": "Вы присоединились к рабочему пространству.",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register the pair's `ru` locale into a core i18n engine (call once at
|
|
44
|
+
* startup, after {@link registerWorkspacesI18n}). Layers per the
|
|
45
|
+
* merge-priority convention (i18n-shipping.md §3): the en floor is registered
|
|
46
|
+
* UNDER the ru texts inside the `ru` locale, so a key the ru bundle ever
|
|
47
|
+
* misses degrades to its English text — never to a raw key. A host bundle
|
|
48
|
+
* registered after this call overrides both.
|
|
49
|
+
*/
|
|
50
|
+
export function registerWorkspacesI18nRu(engine: I18nEngine): void {
|
|
51
|
+
engine.registerBundle("ru", workspacesI18nBundleEn);
|
|
52
|
+
engine.registerBundle("ru", workspacesI18nBundleRu);
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stapel/workspaces-react` — the headless React flow pair for stapel-workspaces
|
|
3
|
+
* (frontend-standard §2). Business + state only, zero visual opinion. Built on
|
|
4
|
+
* `@stapel/core`'s StapelClient (verification-403 interception, token refresh,
|
|
5
|
+
* i18n, analytics, query layer).
|
|
6
|
+
*
|
|
7
|
+
* Scaffolded by `stapel-new-react-lib`. Layers: api → model → flows → headless
|
|
8
|
+
* → i18n. Generated surfaces (flows registry, error map, manifest, llms.txt)
|
|
9
|
+
* are produced by the monorepo `gen:*` drivers and stand under drift gates.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── api ──────────────────────────────────────────────────────────────────────
|
|
13
|
+
export { createWorkspacesApi } from "./api/workspacesApi.js";
|
|
14
|
+
export type { WorkspacesApi } from "./api/workspacesApi.js";
|
|
15
|
+
export type { Schemas } from "./api/types.js";
|
|
16
|
+
|
|
17
|
+
// ── flows ────────────────────────────────────────────────────────────────────
|
|
18
|
+
// The flow-machine primitive lives in `@stapel/core` (one reviewed copy for
|
|
19
|
+
// every pair — frontend-core-architecture §4b). Re-exported for ergonomics.
|
|
20
|
+
export { createFlowMachine, useFlow, isErrorCode } from "@stapel/core";
|
|
21
|
+
export type {
|
|
22
|
+
FlowMachine,
|
|
23
|
+
FlowMachineOptions,
|
|
24
|
+
FlowStateBase,
|
|
25
|
+
FlowError,
|
|
26
|
+
} from "@stapel/core";
|
|
27
|
+
export { toFlowError } from "./flows/errors.js";
|
|
28
|
+
export { WORKSPACES_FLOWS, flowEndpoints } from "./flows/generated/flows.gen.js";
|
|
29
|
+
export type {
|
|
30
|
+
WorkspacesFlowId,
|
|
31
|
+
WorkspacesFlowSpec,
|
|
32
|
+
FlowEndpoint,
|
|
33
|
+
} from "./flows/generated/flows.gen.js";
|
|
34
|
+
|
|
35
|
+
// ── model (runtime wiring, query keys, context) ──────────────────────────────
|
|
36
|
+
export { createWorkspacesRuntime } from "./model/runtime.js";
|
|
37
|
+
export type {
|
|
38
|
+
WorkspacesRuntime,
|
|
39
|
+
CreateWorkspacesRuntimeOptions,
|
|
40
|
+
} from "./model/runtime.js";
|
|
41
|
+
export {
|
|
42
|
+
WorkspacesRuntimeContext,
|
|
43
|
+
useWorkspacesRuntime,
|
|
44
|
+
useWorkspacesApi,
|
|
45
|
+
useWorkspacesAnalytics,
|
|
46
|
+
} from "./model/context.js";
|
|
47
|
+
export { workspacesQueryKeys } from "./model/queryKeys.js";
|
|
48
|
+
|
|
49
|
+
// ── model (read hooks) ───────────────────────────────────────────────────────
|
|
50
|
+
export {
|
|
51
|
+
useWorkspaces,
|
|
52
|
+
useWorkspace,
|
|
53
|
+
useMembers,
|
|
54
|
+
} from "./model/queries.js";
|
|
55
|
+
|
|
56
|
+
// ── model (write hooks) ──────────────────────────────────────────────────────
|
|
57
|
+
export {
|
|
58
|
+
useCreateWorkspace,
|
|
59
|
+
useUpdateWorkspace,
|
|
60
|
+
useDeleteWorkspace,
|
|
61
|
+
useInviteMembers,
|
|
62
|
+
useUpdateMemberRole,
|
|
63
|
+
useRemoveMember,
|
|
64
|
+
useAcceptInvitation,
|
|
65
|
+
} from "./model/mutations.js";
|
|
66
|
+
export type { MemberRoleChange } from "./model/mutations.js";
|
|
67
|
+
|
|
68
|
+
// ── api (wire type aliases) ──────────────────────────────────────────────────
|
|
69
|
+
export type {
|
|
70
|
+
Workspace as WorkspaceData,
|
|
71
|
+
WorkspaceList as WorkspaceListData,
|
|
72
|
+
WorkspaceCreate,
|
|
73
|
+
WorkspaceUpdate,
|
|
74
|
+
Member as MemberData,
|
|
75
|
+
MemberList as MemberListData,
|
|
76
|
+
MemberInvite,
|
|
77
|
+
MemberInviteResult,
|
|
78
|
+
MemberRoleUpdate,
|
|
79
|
+
Invitation,
|
|
80
|
+
InvitationAccept,
|
|
81
|
+
WorkspaceRole,
|
|
82
|
+
WorkspaceKind,
|
|
83
|
+
} from "./api/types.js";
|
|
84
|
+
|
|
85
|
+
// ── headless (renderless components) ─────────────────────────────────────────
|
|
86
|
+
export { WorkspacesProvider } from "./headless/WorkspacesProvider.js";
|
|
87
|
+
export { WorkspaceList } from "./headless/WorkspaceList.js";
|
|
88
|
+
export type { WorkspaceListBag } from "./headless/WorkspaceList.js";
|
|
89
|
+
export { Members } from "./headless/Members.js";
|
|
90
|
+
export type { MembersBag } from "./headless/Members.js";
|
|
91
|
+
export { AcceptInvitation } from "./headless/AcceptInvitation.js";
|
|
92
|
+
export type { AcceptInvitationBag } from "./headless/AcceptInvitation.js";
|
|
93
|
+
|
|
94
|
+
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
95
|
+
export {
|
|
96
|
+
WORKSPACES_I18N_KEYS,
|
|
97
|
+
workspacesI18nBundleEn,
|
|
98
|
+
registerWorkspacesI18n,
|
|
99
|
+
} from "./i18n/keys.js";
|
|
100
|
+
export type { WorkspacesI18nKey } from "./i18n/keys.js";
|
|
101
|
+
|
|
102
|
+
// ── errors map (code → status/params/remediation/en; generated) ──────────────
|
|
103
|
+
export {
|
|
104
|
+
WORKSPACES_ERRORS,
|
|
105
|
+
WORKSPACES_ERROR_CODES,
|
|
106
|
+
workspacesErrorBundleEn,
|
|
107
|
+
explainWorkspacesError,
|
|
108
|
+
} from "./i18n/errorsMap.js";
|
|
109
|
+
export type {
|
|
110
|
+
WorkspacesErrorCode,
|
|
111
|
+
WorkspacesErrorSpec,
|
|
112
|
+
Remediation,
|
|
113
|
+
} from "./i18n/errorsMap.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { Context } from "react";
|
|
3
|
+
import type { Analytics } from "@stapel/core";
|
|
4
|
+
import type { WorkspacesApi } from "../api/workspacesApi.js";
|
|
5
|
+
import type { WorkspacesRuntime } from "./runtime.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The wired WorkspacesRuntime shared through React context by
|
|
9
|
+
* `<WorkspacesProvider>`. Hooks in `model/` and `headless/` read the singletons
|
|
10
|
+
* from here.
|
|
11
|
+
*/
|
|
12
|
+
export const WorkspacesRuntimeContext: Context<WorkspacesRuntime | null> =
|
|
13
|
+
createContext<WorkspacesRuntime | null>(null);
|
|
14
|
+
|
|
15
|
+
export function useWorkspacesRuntime(): WorkspacesRuntime {
|
|
16
|
+
const runtime = useContext(WorkspacesRuntimeContext);
|
|
17
|
+
if (runtime === null) {
|
|
18
|
+
throw new Error("Workspaces hooks must be used within a <WorkspacesProvider>");
|
|
19
|
+
}
|
|
20
|
+
return runtime;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useWorkspacesApi(): WorkspacesApi {
|
|
24
|
+
return useWorkspacesRuntime().api;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useWorkspacesAnalytics(): Analytics | null {
|
|
28
|
+
return useWorkspacesRuntime().analytics;
|
|
29
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type {
|
|
3
|
+
UseMutationOptions,
|
|
4
|
+
UseMutationResult,
|
|
5
|
+
} from "@tanstack/react-query";
|
|
6
|
+
import type { StapelApiError } from "@stapel/core";
|
|
7
|
+
import type {
|
|
8
|
+
Member,
|
|
9
|
+
MemberInvite,
|
|
10
|
+
MemberInviteResult,
|
|
11
|
+
InvitationAccept,
|
|
12
|
+
Workspace,
|
|
13
|
+
WorkspaceCreate,
|
|
14
|
+
WorkspaceRole,
|
|
15
|
+
WorkspaceUpdate,
|
|
16
|
+
} from "../api/types.js";
|
|
17
|
+
import { useWorkspacesApi } from "./context.js";
|
|
18
|
+
import { workspacesQueryKeys } from "./queryKeys.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write hooks (frontend-standard §2 — "мутации с инвалидацией"). Each mutation
|
|
22
|
+
* invalidates exactly the server state it can move. Membership and ownership are
|
|
23
|
+
* server truth (roles gate access in other services via the membership cache),
|
|
24
|
+
* so NONE of these are optimistic (frontend-core-architecture §2.6: optimistic
|
|
25
|
+
* is for local-echo UX, never for flows with server truth).
|
|
26
|
+
*
|
|
27
|
+
* Options are built as typed `UseMutationOptions` objects (not call-site
|
|
28
|
+
* generics) so `void` stays in type-reference position, which
|
|
29
|
+
* `no-invalid-void-type` permits. Hooks that act on one workspace take its id as
|
|
30
|
+
* a parameter (the mutation variable then carries only the payload).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Create a workspace (owner-seeded) — refreshes the workspace list. */
|
|
34
|
+
export function useCreateWorkspace(): UseMutationResult<
|
|
35
|
+
Workspace,
|
|
36
|
+
StapelApiError,
|
|
37
|
+
WorkspaceCreate
|
|
38
|
+
> {
|
|
39
|
+
const api = useWorkspacesApi();
|
|
40
|
+
const queryClient = useQueryClient();
|
|
41
|
+
const options: UseMutationOptions<Workspace, StapelApiError, WorkspaceCreate> =
|
|
42
|
+
{
|
|
43
|
+
mutationFn: (body) => api.createWorkspace(body),
|
|
44
|
+
onSuccess: () => {
|
|
45
|
+
void queryClient.invalidateQueries({
|
|
46
|
+
queryKey: workspacesQueryKeys.list(),
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return useMutation(options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Partially update a workspace — writes the fresh row into its detail cache and
|
|
55
|
+
* refreshes the list (name / slug may have changed there too).
|
|
56
|
+
*/
|
|
57
|
+
export function useUpdateWorkspace(
|
|
58
|
+
workspaceId: string
|
|
59
|
+
): UseMutationResult<Workspace, StapelApiError, WorkspaceUpdate> {
|
|
60
|
+
const api = useWorkspacesApi();
|
|
61
|
+
const queryClient = useQueryClient();
|
|
62
|
+
const options: UseMutationOptions<Workspace, StapelApiError, WorkspaceUpdate> =
|
|
63
|
+
{
|
|
64
|
+
mutationFn: (patch) => api.updateWorkspace(workspaceId, patch),
|
|
65
|
+
onSuccess: (updated) => {
|
|
66
|
+
queryClient.setQueryData(
|
|
67
|
+
workspacesQueryKeys.detail(workspaceId),
|
|
68
|
+
updated
|
|
69
|
+
);
|
|
70
|
+
void queryClient.invalidateQueries({
|
|
71
|
+
queryKey: workspacesQueryKeys.list(),
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
return useMutation(options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Soft-delete a workspace (owner only) — drops its detail cache + refreshes the list. */
|
|
79
|
+
export function useDeleteWorkspace(): UseMutationResult<
|
|
80
|
+
void,
|
|
81
|
+
StapelApiError,
|
|
82
|
+
string
|
|
83
|
+
> {
|
|
84
|
+
const api = useWorkspacesApi();
|
|
85
|
+
const queryClient = useQueryClient();
|
|
86
|
+
const options: UseMutationOptions<void, StapelApiError, string> = {
|
|
87
|
+
mutationFn: (workspaceId) => api.deleteWorkspace(workspaceId),
|
|
88
|
+
onSuccess: (_result, workspaceId) => {
|
|
89
|
+
queryClient.removeQueries({
|
|
90
|
+
queryKey: workspacesQueryKeys.detail(workspaceId),
|
|
91
|
+
});
|
|
92
|
+
void queryClient.invalidateQueries({
|
|
93
|
+
queryKey: workspacesQueryKeys.list(),
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
return useMutation(options);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Invite one or more emails to a workspace (admin+). Server truth — no optimism.
|
|
102
|
+
* A bad role or missing email surfaces as a localizable `StapelApiError`
|
|
103
|
+
* (`error.400.invalid_role`). Refreshes the member list (pending invites show
|
|
104
|
+
* there).
|
|
105
|
+
*/
|
|
106
|
+
export function useInviteMembers(
|
|
107
|
+
workspaceId: string
|
|
108
|
+
): UseMutationResult<MemberInviteResult, StapelApiError, MemberInvite> {
|
|
109
|
+
const api = useWorkspacesApi();
|
|
110
|
+
const queryClient = useQueryClient();
|
|
111
|
+
const options: UseMutationOptions<
|
|
112
|
+
MemberInviteResult,
|
|
113
|
+
StapelApiError,
|
|
114
|
+
MemberInvite
|
|
115
|
+
> = {
|
|
116
|
+
mutationFn: (body) => api.inviteMembers(workspaceId, body),
|
|
117
|
+
onSuccess: () => {
|
|
118
|
+
void queryClient.invalidateQueries({
|
|
119
|
+
queryKey: workspacesQueryKeys.members(workspaceId),
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
return useMutation(options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The variable for {@link useUpdateMemberRole}: which member, which new role. */
|
|
127
|
+
export interface MemberRoleChange {
|
|
128
|
+
readonly userId: string;
|
|
129
|
+
readonly role: WorkspaceRole;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Change a member's role (admin+; the backend guards owner grants and the
|
|
134
|
+
* last-owner invariant, surfacing `error.403.last_owner_cannot_be_removed`).
|
|
135
|
+
* Refreshes the member list.
|
|
136
|
+
*/
|
|
137
|
+
export function useUpdateMemberRole(
|
|
138
|
+
workspaceId: string
|
|
139
|
+
): UseMutationResult<Member, StapelApiError, MemberRoleChange> {
|
|
140
|
+
const api = useWorkspacesApi();
|
|
141
|
+
const queryClient = useQueryClient();
|
|
142
|
+
const options: UseMutationOptions<Member, StapelApiError, MemberRoleChange> = {
|
|
143
|
+
mutationFn: ({ userId, role }) =>
|
|
144
|
+
api.updateMemberRole(workspaceId, userId, { role }),
|
|
145
|
+
onSuccess: () => {
|
|
146
|
+
void queryClient.invalidateQueries({
|
|
147
|
+
queryKey: workspacesQueryKeys.members(workspaceId),
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
return useMutation(options);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Remove a member (admin+; the last owner is protected by the backend). The
|
|
156
|
+
* variable is the member's `userId`. Refreshes the member list.
|
|
157
|
+
*/
|
|
158
|
+
export function useRemoveMember(
|
|
159
|
+
workspaceId: string
|
|
160
|
+
): UseMutationResult<void, StapelApiError, string> {
|
|
161
|
+
const api = useWorkspacesApi();
|
|
162
|
+
const queryClient = useQueryClient();
|
|
163
|
+
const options: UseMutationOptions<void, StapelApiError, string> = {
|
|
164
|
+
mutationFn: (userId) => api.removeMember(workspaceId, userId),
|
|
165
|
+
onSuccess: () => {
|
|
166
|
+
void queryClient.invalidateQueries({
|
|
167
|
+
queryKey: workspacesQueryKeys.members(workspaceId),
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
return useMutation(options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Accept an invitation by its token — the caller joins the workspace. A dead
|
|
176
|
+
* token surfaces a localizable `StapelApiError` (`error.400.invitation_expired`
|
|
177
|
+
* / `error.400.invitation_revoked` / `error.400.invitation_already_used`).
|
|
178
|
+
* Refreshes the workspace list (the new membership appears there).
|
|
179
|
+
*/
|
|
180
|
+
export function useAcceptInvitation(): UseMutationResult<
|
|
181
|
+
Member,
|
|
182
|
+
StapelApiError,
|
|
183
|
+
InvitationAccept
|
|
184
|
+
> {
|
|
185
|
+
const api = useWorkspacesApi();
|
|
186
|
+
const queryClient = useQueryClient();
|
|
187
|
+
const options: UseMutationOptions<Member, StapelApiError, InvitationAccept> = {
|
|
188
|
+
mutationFn: (body) => api.acceptInvitation(body),
|
|
189
|
+
onSuccess: () => {
|
|
190
|
+
void queryClient.invalidateQueries({
|
|
191
|
+
queryKey: workspacesQueryKeys.list(),
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
return useMutation(options);
|
|
196
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import type { UseQueryResult } from "@tanstack/react-query";
|
|
3
|
+
import type { StapelApiError } from "@stapel/core";
|
|
4
|
+
import type { MemberList, Workspace, WorkspaceList } from "../api/types.js";
|
|
5
|
+
import { useWorkspacesApi } from "./context.js";
|
|
6
|
+
import { workspacesQueryKeys } from "./queryKeys.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read hooks over the workspaces API. Staleness follows core's query defaults;
|
|
10
|
+
* override per call site via a page that needs fresher data. Keys are
|
|
11
|
+
* namespaced (see `workspacesQueryKeys`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The caller's workspaces — their accepted memberships (GET /). */
|
|
15
|
+
export function useWorkspaces(): UseQueryResult<WorkspaceList, StapelApiError> {
|
|
16
|
+
const api = useWorkspacesApi();
|
|
17
|
+
return useQuery({
|
|
18
|
+
queryKey: workspacesQueryKeys.list(),
|
|
19
|
+
queryFn: () => api.listWorkspaces(),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A single workspace by id (GET /{id}). Disabled until a `workspaceId` is
|
|
25
|
+
* given, so a detail hook can mount before a selection exists.
|
|
26
|
+
*/
|
|
27
|
+
export function useWorkspace(
|
|
28
|
+
workspaceId: string | null
|
|
29
|
+
): UseQueryResult<Workspace, StapelApiError> {
|
|
30
|
+
const api = useWorkspacesApi();
|
|
31
|
+
return useQuery({
|
|
32
|
+
queryKey: workspacesQueryKeys.detail(workspaceId ?? ""),
|
|
33
|
+
queryFn: () => api.getWorkspace(workspaceId as string),
|
|
34
|
+
enabled: workspaceId !== null && workspaceId !== "",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A workspace's members (GET /{id}/members). Disabled until a `workspaceId` is
|
|
40
|
+
* given.
|
|
41
|
+
*/
|
|
42
|
+
export function useMembers(
|
|
43
|
+
workspaceId: string | null
|
|
44
|
+
): UseQueryResult<MemberList, StapelApiError> {
|
|
45
|
+
const api = useWorkspacesApi();
|
|
46
|
+
return useQuery({
|
|
47
|
+
queryKey: workspacesQueryKeys.members(workspaceId ?? ""),
|
|
48
|
+
queryFn: () => api.listMembers(workspaceId as string),
|
|
49
|
+
enabled: workspaceId !== null && workspaceId !== "",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespaced TanStack Query keys (frontend-standard §2 — "ключи неймспейснуты").
|
|
3
|
+
* Everything under the `"workspaces"` root so a host can invalidate the whole
|
|
4
|
+
* module or match a single resource. Persist scope is per-user via core's query
|
|
5
|
+
* runtime (`setPersistUser`). Explicit tuple return types satisfy
|
|
6
|
+
* `--isolatedDeclarations`. One entry per read-operation.
|
|
7
|
+
*/
|
|
8
|
+
const ROOT = "workspaces" as const;
|
|
9
|
+
|
|
10
|
+
export const workspacesQueryKeys: {
|
|
11
|
+
readonly all: readonly ["workspaces"];
|
|
12
|
+
list(): readonly ["workspaces", "list"];
|
|
13
|
+
detail(workspaceId: string): readonly ["workspaces", "detail", string];
|
|
14
|
+
members(workspaceId: string): readonly ["workspaces", "members", string];
|
|
15
|
+
} = {
|
|
16
|
+
all: [ROOT],
|
|
17
|
+
list: () => [ROOT, "list"],
|
|
18
|
+
detail: (workspaceId) => [ROOT, "detail", workspaceId],
|
|
19
|
+
members: (workspaceId) => [ROOT, "members", workspaceId],
|
|
20
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createStapelClient } from "@stapel/core";
|
|
2
|
+
import type { Analytics, StapelClient } from "@stapel/core";
|
|
3
|
+
import { createWorkspacesApi } from "../api/workspacesApi.js";
|
|
4
|
+
import type { WorkspacesApi } from "../api/workspacesApi.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The wired workspaces runtime — builds a {@link StapelClient} and the pair's
|
|
8
|
+
* API over it. The returned `client` is what the host injects into core's
|
|
9
|
+
* `StapelConfigProvider` (as the default or the `"workspaces"` module client),
|
|
10
|
+
* preserving the client-injection fork seam (frontend-standard §7.2). Auth
|
|
11
|
+
* token/refresh and the verification-403 seam are supplied by the host's auth
|
|
12
|
+
* runtime on the shared client — this pair does not re-implement them.
|
|
13
|
+
*/
|
|
14
|
+
export interface WorkspacesRuntime {
|
|
15
|
+
readonly client: StapelClient;
|
|
16
|
+
readonly api: WorkspacesApi;
|
|
17
|
+
readonly analytics: Analytics | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CreateWorkspacesRuntimeOptions {
|
|
21
|
+
/** e.g. `/workspaces/api/` or `https://app.example.com/workspaces/api/`. */
|
|
22
|
+
readonly baseUrl: string;
|
|
23
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
24
|
+
readonly credentials?: RequestCredentials;
|
|
25
|
+
readonly analytics?: Analytics | null;
|
|
26
|
+
/** Extra headers merged into every request (e.g. a tenant id). */
|
|
27
|
+
readonly defaultHeaders?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createWorkspacesRuntime(
|
|
31
|
+
options: CreateWorkspacesRuntimeOptions
|
|
32
|
+
): WorkspacesRuntime {
|
|
33
|
+
const analytics = options.analytics ?? null;
|
|
34
|
+
const client = createStapelClient({
|
|
35
|
+
baseUrl: options.baseUrl,
|
|
36
|
+
...(options.fetch !== undefined ? { fetch: options.fetch } : {}),
|
|
37
|
+
...(options.credentials !== undefined
|
|
38
|
+
? { credentials: options.credentials }
|
|
39
|
+
: {}),
|
|
40
|
+
...(options.defaultHeaders !== undefined
|
|
41
|
+
? { defaultHeaders: options.defaultHeaders }
|
|
42
|
+
: {}),
|
|
43
|
+
});
|
|
44
|
+
const api = createWorkspacesApi(client);
|
|
45
|
+
return { client, api, analytics };
|
|
46
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"_comment": "Self-contained on purpose: standalone-buildable per frontend-standard §7. Mirrors the root tsconfig.base.json settings.",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"exactOptionalPropertyTypes": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"isolatedDeclarations": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"declaration": true,
|
|
18
|
+
"declarationMap": true,
|
|
19
|
+
"sourceMap": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"forceConsistentCasingInFileNames": true,
|
|
22
|
+
"outDir": "dist",
|
|
23
|
+
"rootDir": "src"
|
|
24
|
+
},
|
|
25
|
+
"include": ["src"]
|
|
26
|
+
}
|